mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 19:02:03 +00:00
Testing PRs from within decky (#496)
* git no work so manually uploading files :( * argh i wish git was working * ok next time i'll make git work * Update updater.py * git please work next time this took ages without you * fix me locales * Update updater.py * Update en-US.json * Update updater.py * Update updater.py * i wish my python LSP stuff was working * fix it * Update updater.py * Update updater.py * Only show testing branch as an option if it is already selected * Initial implementation for fetching the open PRs. Still need testing and a token to complete this. * Wrong filter capitalization * Fix a couple of typos in the python backend updater. * Fix typos pt 3 * This should be the last one * Prepend the PR version number with PR- to make it clearer that's the PR number. * Update prettier to the latest version otherwise it will never be happy with the formatting. * fix merge mistake * fix pyright errors & type hint most new code * fix strict pyright errors... * not sure why my local linter didn't catch this * Reimplement the logic between PR and artifact build to limit API calls * Fix pyright errors * use nightly.link for downloads * remove accidental dollar sign * fix various logical errors. the code actually works now. * set branch to testing when user downloads a testing version --------- Co-authored-by: Marco Rodolfi <marco.rodolfi@tuta.io>
This commit is contained in:
+109
-33
@@ -1,19 +1,24 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
from asyncio import sleep
|
from asyncio import sleep
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
import os
|
||||||
from os import getcwd, path, remove
|
from os import getcwd, path, remove
|
||||||
from typing import TYPE_CHECKING, List, TypedDict
|
from typing import TYPE_CHECKING, List, TypedDict
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .main import PluginManager
|
from .main import PluginManager
|
||||||
from .localplatform.localplatform import chmod, service_restart, ON_LINUX, get_keep_systemd_service, get_selinux
|
from .localplatform.localplatform import chmod, service_restart, ON_LINUX, ON_WINDOWS, get_keep_systemd_service, get_selinux
|
||||||
|
import shutil
|
||||||
|
from typing import List, TYPE_CHECKING, TypedDict
|
||||||
|
import zipfile
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
|
|
||||||
from . import helpers
|
from . import helpers
|
||||||
from .injector import get_gamepadui_tab
|
from .injector import get_gamepadui_tab
|
||||||
from .settings import SettingsManager
|
from .settings import SettingsManager
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .main import PluginManager
|
||||||
|
|
||||||
|
|
||||||
logger = getLogger("Updater")
|
logger = getLogger("Updater")
|
||||||
|
|
||||||
@@ -24,6 +29,12 @@ class RemoteVer(TypedDict):
|
|||||||
tag_name: str
|
tag_name: str
|
||||||
prerelease: bool
|
prerelease: bool
|
||||||
assets: List[RemoteVerAsset]
|
assets: List[RemoteVerAsset]
|
||||||
|
class TestingVersion(TypedDict):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
link: str
|
||||||
|
head_sha: str
|
||||||
|
|
||||||
|
|
||||||
class Updater:
|
class Updater:
|
||||||
def __init__(self, context: PluginManager) -> None:
|
def __init__(self, context: PluginManager) -> None:
|
||||||
@@ -44,6 +55,8 @@ class Updater:
|
|||||||
context.ws.add_route("updater/check_for_updates", self.check_for_updates);
|
context.ws.add_route("updater/check_for_updates", self.check_for_updates);
|
||||||
context.ws.add_route("updater/do_restart", self.do_restart);
|
context.ws.add_route("updater/do_restart", self.do_restart);
|
||||||
context.ws.add_route("updater/do_update", self.do_update);
|
context.ws.add_route("updater/do_update", self.do_update);
|
||||||
|
context.ws.add_route("updater/get_testing_versions", self.get_testing_versions);
|
||||||
|
context.ws.add_route("updater/download_testing_version", self.download_testing_version);
|
||||||
context.loop.create_task(self.version_reloader())
|
context.loop.create_task(self.version_reloader())
|
||||||
|
|
||||||
def get_branch(self, manager: SettingsManager):
|
def get_branch(self, manager: SettingsManager):
|
||||||
@@ -126,6 +139,53 @@ class Updater:
|
|||||||
pass
|
pass
|
||||||
await sleep(60 * 60 * 6) # 6 hours
|
await sleep(60 * 60 * 6) # 6 hours
|
||||||
|
|
||||||
|
async def download_decky_binary(self, download_url: str, version: str, is_zip: bool = False):
|
||||||
|
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
|
||||||
|
download_temp_filename = download_filename + ".new"
|
||||||
|
tab = await get_gamepadui_tab()
|
||||||
|
await tab.open_websocket()
|
||||||
|
|
||||||
|
async with ClientSession() as web:
|
||||||
|
logger.debug("Downloading binary")
|
||||||
|
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
||||||
|
total = int(res.headers.get('content-length', 0))
|
||||||
|
with open(path.join(getcwd(), download_temp_filename), "wb") as out:
|
||||||
|
progress = 0
|
||||||
|
raw = 0
|
||||||
|
async for c in res.content.iter_chunked(512):
|
||||||
|
out.write(c)
|
||||||
|
if total != 0:
|
||||||
|
raw += len(c)
|
||||||
|
new_progress = round((raw / total) * 100)
|
||||||
|
if progress != new_progress:
|
||||||
|
self.context.loop.create_task(self.context.ws.emit("frontend/update_download_percentage", new_progress))
|
||||||
|
progress = new_progress
|
||||||
|
|
||||||
|
with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
|
||||||
|
out.write(version)
|
||||||
|
|
||||||
|
if ON_LINUX:
|
||||||
|
remove(path.join(getcwd(), download_filename))
|
||||||
|
if (is_zip):
|
||||||
|
with zipfile.ZipFile(path.join(getcwd(), download_temp_filename), 'r') as file:
|
||||||
|
file.getinfo(download_filename).filename = download_filename + ".unzipped"
|
||||||
|
file.extract(download_filename)
|
||||||
|
remove(path.join(getcwd(), download_temp_filename))
|
||||||
|
shutil.move(path.join(getcwd(), download_filename + ".unzipped"), path.join(getcwd(), download_filename))
|
||||||
|
else:
|
||||||
|
shutil.move(path.join(getcwd(), download_temp_filename), path.join(getcwd(), download_filename))
|
||||||
|
|
||||||
|
chmod(path.join(getcwd(), download_filename), 777, False)
|
||||||
|
if get_selinux():
|
||||||
|
from asyncio.subprocess import create_subprocess_exec
|
||||||
|
process = await create_subprocess_exec("chcon", "-t", "bin_t", path.join(getcwd(), download_filename))
|
||||||
|
logger.info(f"Setting the executable flag with chcon returned {await process.wait()}")
|
||||||
|
|
||||||
|
logger.info("Updated loader installation.")
|
||||||
|
await self.context.ws.emit("frontend/finish_download")
|
||||||
|
await self.do_restart()
|
||||||
|
await tab.close_websocket()
|
||||||
|
|
||||||
async def do_update(self):
|
async def do_update(self):
|
||||||
logger.debug("Starting update.")
|
logger.debug("Starting update.")
|
||||||
try:
|
try:
|
||||||
@@ -134,9 +194,9 @@ class Updater:
|
|||||||
logger.error("Unable to update as remoteVer is missing")
|
logger.error("Unable to update as remoteVer is missing")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
version = self.remoteVer["tag_name"]
|
||||||
download_url = None
|
download_url = None
|
||||||
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
|
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
|
||||||
download_temp_filename = download_filename + ".new"
|
|
||||||
|
|
||||||
for x in self.remoteVer["assets"]:
|
for x in self.remoteVer["assets"]:
|
||||||
if x["name"] == download_filename:
|
if x["name"] == download_filename:
|
||||||
@@ -149,8 +209,6 @@ class Updater:
|
|||||||
service_url = self.get_service_url()
|
service_url = self.get_service_url()
|
||||||
logger.debug("Retrieved service URL")
|
logger.debug("Retrieved service URL")
|
||||||
|
|
||||||
tab = await get_gamepadui_tab()
|
|
||||||
await tab.open_websocket()
|
|
||||||
async with ClientSession() as web:
|
async with ClientSession() as web:
|
||||||
if ON_LINUX and not get_keep_systemd_service():
|
if ON_LINUX and not get_keep_systemd_service():
|
||||||
logger.debug("Downloading systemd service")
|
logger.debug("Downloading systemd service")
|
||||||
@@ -178,33 +236,51 @@ class Updater:
|
|||||||
os.mkdir(path.join(getcwd(), ".systemd"))
|
os.mkdir(path.join(getcwd(), ".systemd"))
|
||||||
shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service")
|
shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service")
|
||||||
|
|
||||||
logger.debug("Downloading binary")
|
await self.download_decky_binary(download_url, version)
|
||||||
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
|
||||||
total = int(res.headers.get('content-length', 0))
|
|
||||||
with open(path.join(getcwd(), download_temp_filename), "wb") as out:
|
|
||||||
progress = 0
|
|
||||||
raw = 0
|
|
||||||
async for c in res.content.iter_chunked(512):
|
|
||||||
out.write(c)
|
|
||||||
raw += len(c)
|
|
||||||
new_progress = round((raw / total) * 100)
|
|
||||||
if progress != new_progress:
|
|
||||||
self.context.loop.create_task(self.context.ws.emit("frontend/update_download_percentage", new_progress))
|
|
||||||
progress = new_progress
|
|
||||||
|
|
||||||
if ON_LINUX:
|
|
||||||
remove(path.join(getcwd(), download_filename))
|
|
||||||
shutil.move(path.join(getcwd(), download_temp_filename), path.join(getcwd(), download_filename))
|
|
||||||
chmod(path.join(getcwd(), download_filename), 777, False)
|
|
||||||
if get_selinux():
|
|
||||||
from asyncio.subprocess import create_subprocess_exec
|
|
||||||
process = await create_subprocess_exec("chcon", "-t", "bin_t", path.join(getcwd(), download_filename))
|
|
||||||
logger.info(f"Setting the executable flag with chcon returned {await process.wait()}")
|
|
||||||
|
|
||||||
logger.info("Updated loader installation.")
|
|
||||||
await self.context.ws.emit("frontend/finish_download")
|
|
||||||
await self.do_restart()
|
|
||||||
await tab.close_websocket()
|
|
||||||
|
|
||||||
async def do_restart(self):
|
async def do_restart(self):
|
||||||
await service_restart("plugin_loader")
|
await service_restart("plugin_loader")
|
||||||
|
|
||||||
|
async def get_testing_versions(self) -> List[TestingVersion]:
|
||||||
|
result: List[TestingVersion] = []
|
||||||
|
async with ClientSession() as web:
|
||||||
|
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/pulls",
|
||||||
|
headers={'X-GitHub-Api-Version': '2022-11-28'}, params={'state':'open'}, ssl=helpers.get_ssl_context()) as res:
|
||||||
|
open_prs = await res.json()
|
||||||
|
for pr in open_prs:
|
||||||
|
result.append({
|
||||||
|
"id": int(pr['number']),
|
||||||
|
"name": pr['title'],
|
||||||
|
"link": pr['html_url'],
|
||||||
|
"head_sha": pr['head']['sha'],
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def download_testing_version(self, pr_id: int, sha_id: str):
|
||||||
|
down_id = ''
|
||||||
|
#Get all the associated workflow run for the given sha_id code hash
|
||||||
|
async with ClientSession() as web:
|
||||||
|
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/actions/runs",
|
||||||
|
headers={'X-GitHub-Api-Version': '2022-11-28'}, params={'event':'pull_request', 'head_sha': sha_id}, ssl=helpers.get_ssl_context()) as res:
|
||||||
|
works = await res.json()
|
||||||
|
#Iterate over the workflow_run to get the two builds if they exists
|
||||||
|
for work in works['workflow_runs']:
|
||||||
|
if ON_WINDOWS and work['name'] == 'Builder Win':
|
||||||
|
down_id=work['id']
|
||||||
|
break
|
||||||
|
elif ON_LINUX and work['name'] == 'Builder':
|
||||||
|
down_id=work['id']
|
||||||
|
break
|
||||||
|
if down_id != '':
|
||||||
|
async with ClientSession() as web:
|
||||||
|
async with web.request("GET", f"https://api.github.com/repos/SteamDeckHomebrew/decky-loader/actions/runs/{down_id}/artifacts",
|
||||||
|
headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res:
|
||||||
|
jresp = await res.json()
|
||||||
|
#If the request found at least one artifact to download...
|
||||||
|
if int(jresp['total_count']) != 0:
|
||||||
|
# this assumes that the artifact we want is the first one!
|
||||||
|
down_link = f"https://nightly.link/SteamDeckHomebrew/decky-loader/actions/artifacts/{jresp['artifacts'][0]['id']}.zip"
|
||||||
|
#Then fetch it and restart itself
|
||||||
|
await self.download_decky_binary(down_link, f'PR-{pr_id}' , True)
|
||||||
|
else:
|
||||||
|
logger.error("workflow run not found", str(works))
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"SettingsIndex": {
|
"SettingsIndex": {
|
||||||
"developer_title": "Developer",
|
"developer_title": "Developer",
|
||||||
"general_title": "General",
|
"general_title": "General",
|
||||||
"plugins_title": "Plugins"
|
"plugins_title": "Plugins",
|
||||||
|
"testing_title": "Testing"
|
||||||
},
|
},
|
||||||
"Store": {
|
"Store": {
|
||||||
"store_contrib": {
|
"store_contrib": {
|
||||||
@@ -260,5 +261,8 @@
|
|||||||
"reloading": "Reloading",
|
"reloading": "Reloading",
|
||||||
"updating": "Updating"
|
"updating": "Updating"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"Testing": {
|
||||||
|
"download": "Download"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"i18next-parser": "^8.0.0",
|
"i18next-parser": "^8.0.0",
|
||||||
"import-sort-style-module": "^6.0.0",
|
"import-sort-style-module": "^6.0.0",
|
||||||
"inquirer": "^8.2.5",
|
"inquirer": "^8.2.5",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-import-sort": "^0.0.7",
|
"prettier-plugin-import-sort": "^0.0.7",
|
||||||
"react": "16.14.0",
|
"react": "16.14.0",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "16.14.0",
|
||||||
|
|||||||
Generated
+8
-8
@@ -77,11 +77,11 @@ devDependencies:
|
|||||||
specifier: ^8.2.5
|
specifier: ^8.2.5
|
||||||
version: 8.2.5
|
version: 8.2.5
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^2.8.8
|
specifier: ^3.2.5
|
||||||
version: 2.8.8
|
version: 3.2.5
|
||||||
prettier-plugin-import-sort:
|
prettier-plugin-import-sort:
|
||||||
specifier: ^0.0.7
|
specifier: ^0.0.7
|
||||||
version: 0.0.7(prettier@2.8.8)
|
version: 0.0.7(prettier@3.2.5)
|
||||||
react:
|
react:
|
||||||
specifier: 16.14.0
|
specifier: 16.14.0
|
||||||
version: 16.14.0
|
version: 16.14.0
|
||||||
@@ -3104,7 +3104,7 @@ packages:
|
|||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/prettier-plugin-import-sort@0.0.7(prettier@2.8.8):
|
/prettier-plugin-import-sort@0.0.7(prettier@3.2.5):
|
||||||
resolution: {integrity: sha512-O0KlUSq+lwvh+UiN3wZDT6wWkf7TNxTVv2/XXE5KqpRNbFJq3nRg2ftzBYFFO8QGpdWIrOB0uCTCtFjIxmVKQw==}
|
resolution: {integrity: sha512-O0KlUSq+lwvh+UiN3wZDT6wWkf7TNxTVv2/XXE5KqpRNbFJq3nRg2ftzBYFFO8QGpdWIrOB0uCTCtFjIxmVKQw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
prettier: '>= 2.0'
|
prettier: '>= 2.0'
|
||||||
@@ -3113,14 +3113,14 @@ packages:
|
|||||||
import-sort-config: 6.0.0
|
import-sort-config: 6.0.0
|
||||||
import-sort-parser-babylon: 6.0.0
|
import-sort-parser-babylon: 6.0.0
|
||||||
import-sort-parser-typescript: 6.0.0
|
import-sort-parser-typescript: 6.0.0
|
||||||
prettier: 2.8.8
|
prettier: 3.2.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/prettier@2.8.8:
|
/prettier@3.2.5:
|
||||||
resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
|
resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
|||||||
@@ -30,11 +30,14 @@ const DeckyToaster: FC<DeckyToasterProps> = () => {
|
|||||||
// not actually node but TS is shit
|
// not actually node but TS is shit
|
||||||
let interval: NodeJS.Timer | null;
|
let interval: NodeJS.Timer | null;
|
||||||
if (renderedToast) {
|
if (renderedToast) {
|
||||||
interval = setTimeout(() => {
|
interval = setTimeout(
|
||||||
interval = null;
|
() => {
|
||||||
console.log('clear toast', renderedToast.data);
|
interval = null;
|
||||||
removeToast(renderedToast.data);
|
console.log('clear toast', renderedToast.data);
|
||||||
}, (renderedToast.data.duration || 5e3) + 1000);
|
removeToast(renderedToast.data);
|
||||||
|
},
|
||||||
|
(renderedToast.data.duration || 5e3) + 1000,
|
||||||
|
);
|
||||||
console.log('set int', interval);
|
console.log('set int', interval);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SidebarNavigation } from 'decky-frontend-lib';
|
import { SidebarNavigation } from 'decky-frontend-lib';
|
||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaCode, FaPlug } from 'react-icons/fa';
|
import { FaCode, FaFlask, FaPlug } from 'react-icons/fa';
|
||||||
|
|
||||||
import { useSetting } from '../../utils/hooks/useSetting';
|
import { useSetting } from '../../utils/hooks/useSetting';
|
||||||
import DeckyIcon from '../DeckyIcon';
|
import DeckyIcon from '../DeckyIcon';
|
||||||
@@ -10,6 +10,7 @@ import GeneralSettings from './pages/general';
|
|||||||
import PluginList from './pages/plugin_list';
|
import PluginList from './pages/plugin_list';
|
||||||
|
|
||||||
const DeveloperSettings = lazy(() => import('./pages/developer'));
|
const DeveloperSettings = lazy(() => import('./pages/developer'));
|
||||||
|
const TestingMenu = lazy(() => import('./pages/testing'));
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const [isDeveloper, setIsDeveloper] = useSetting<boolean>('developer.enabled', false);
|
const [isDeveloper, setIsDeveloper] = useSetting<boolean>('developer.enabled', false);
|
||||||
@@ -39,6 +40,17 @@ export default function SettingsPage() {
|
|||||||
icon: <FaCode />,
|
icon: <FaCode />,
|
||||||
visible: isDeveloper,
|
visible: isDeveloper,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('SettingsIndex.testing_title'),
|
||||||
|
content: (
|
||||||
|
<WithSuspense>
|
||||||
|
<TestingMenu />
|
||||||
|
</WithSuspense>
|
||||||
|
),
|
||||||
|
route: '/decky/settings/testing',
|
||||||
|
icon: <FaFlask />,
|
||||||
|
visible: isDeveloper,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return <SidebarNavigation pages={pages} />;
|
return <SidebarNavigation pages={pages} />;
|
||||||
|
|||||||
@@ -8,10 +8,15 @@ import { useSetting } from '../../../../utils/hooks/useSetting';
|
|||||||
|
|
||||||
const logger = new Logger('BranchSelect');
|
const logger = new Logger('BranchSelect');
|
||||||
|
|
||||||
enum UpdateBranch {
|
export enum UpdateBranch {
|
||||||
|
Stable,
|
||||||
|
Prerelease,
|
||||||
|
Testing,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LessUpdateBranch {
|
||||||
Stable,
|
Stable,
|
||||||
Prerelease,
|
Prerelease,
|
||||||
// Testing,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BranchSelect: FunctionComponent<{}> = () => {
|
const BranchSelect: FunctionComponent<{}> = () => {
|
||||||
@@ -24,11 +29,11 @@ const BranchSelect: FunctionComponent<{}> = () => {
|
|||||||
const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Stable);
|
const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Stable);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
|
// Returns numerical values from 0 to 2 (with current branch setup as of 6/16/23)
|
||||||
// 0 being stable, 1 being pre-release and 2 being nightly
|
// 0 being stable, 1 being pre-release and 2 being testing (not a branch!)
|
||||||
<Field label={t('BranchSelect.update_channel.label')} childrenContainerWidth={'fixed'}>
|
<Field label={t('BranchSelect.update_channel.label')} childrenContainerWidth={'fixed'}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
rgOptions={Object.values(UpdateBranch)
|
rgOptions={Object.values(selectedBranch == UpdateBranch.Testing ? UpdateBranch : LessUpdateBranch)
|
||||||
.filter((branch) => typeof branch == 'number')
|
.filter((branch) => typeof branch == 'number')
|
||||||
.map((branch) => ({
|
.map((branch) => ({
|
||||||
label: tBranches[branch as number],
|
label: tBranches[branch as number],
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ export default function UpdaterSettings() {
|
|||||||
{checkingForUpdates
|
{checkingForUpdates
|
||||||
? t('Updater.updates.checking')
|
? t('Updater.updates.checking')
|
||||||
: !versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
|
: !versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
|
||||||
? t('Updater.updates.check_button')
|
? t('Updater.updates.check_button')
|
||||||
: t('Updater.updates.install_button')}
|
: t('Updater.updates.install_button')}
|
||||||
</DialogButton>
|
</DialogButton>
|
||||||
) : (
|
) : (
|
||||||
<ProgressBarWithInfo
|
<ProgressBarWithInfo
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { DialogBody, DialogButton, DialogControlsSection, Focusable, Navigation } from 'decky-frontend-lib';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FaDownload, FaInfo } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import { setSetting } from '../../../../utils/settings';
|
||||||
|
import { UpdateBranch } from '../general/BranchSelect';
|
||||||
|
|
||||||
|
interface TestingVersion {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
link: string;
|
||||||
|
head_sha: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTestingVersions = DeckyBackend.callable<[], TestingVersion[]>('updater/get_testing_versions');
|
||||||
|
const downloadTestingVersion = DeckyBackend.callable<[pr_id: number, sha: string]>('updater/download_testing_version');
|
||||||
|
|
||||||
|
export default function TestingVersionList() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [testingVersions, setTestingVersions] = useState<TestingVersion[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setTestingVersions(await getTestingVersions());
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (testingVersions.length === 0) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>No open PRs found</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogBody>
|
||||||
|
<DialogControlsSection>
|
||||||
|
<ul style={{ listStyleType: 'none', padding: '0' }}>
|
||||||
|
{testingVersions.map((version) => {
|
||||||
|
return (
|
||||||
|
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
|
||||||
|
<span>
|
||||||
|
{version.name} <span style={{ opacity: '50%' }}>{'#' + version.id}</span>
|
||||||
|
</span>
|
||||||
|
<Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}>
|
||||||
|
<DialogButton
|
||||||
|
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||||
|
onClick={() => {
|
||||||
|
downloadTestingVersion(version.id, version.head_sha);
|
||||||
|
setSetting('branch', UpdateBranch.Testing);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
minWidth: '150px',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Testing.download')}
|
||||||
|
<FaDownload style={{ paddingLeft: '1rem' }} />
|
||||||
|
</div>
|
||||||
|
</DialogButton>
|
||||||
|
<DialogButton
|
||||||
|
style={{
|
||||||
|
height: '40px',
|
||||||
|
width: '40px',
|
||||||
|
padding: '10px 12px',
|
||||||
|
minWidth: '40px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
onClick={() => Navigation.NavigateToExternalWeb(version.link)}
|
||||||
|
>
|
||||||
|
<FaInfo />
|
||||||
|
</DialogButton>
|
||||||
|
</Focusable>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</DialogControlsSection>
|
||||||
|
</DialogBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user