From f32fbed9cf153c505fc3608a9ccb93c02a80008b Mon Sep 17 00:00:00 2001 From: Gianni Spadoni Date: Fri, 5 Jun 2026 14:39:14 +0800 Subject: [PATCH 01/11] feat(ci): add arm64 workflow, abstract build.yml build.yml is now build-linux.yml and takes two inputs (arch, output_name) This is called by build-linux-arm64.yml and build-linux-x86_64.yml Signed-off-by: Gianni Spadoni --- .github/workflows/build-linux-arm64.yml | 13 +++++++++ .github/workflows/build-linux-x86_64.yml | 13 +++++++++ .../workflows/{build.yml => build-linux.yml} | 29 ++++++++++++------- .github/workflows/release.yml | 24 ++++++++++----- 4 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/build-linux-arm64.yml create mode 100644 .github/workflows/build-linux-x86_64.yml rename .github/workflows/{build.yml => build-linux.yml} (64%) diff --git a/.github/workflows/build-linux-arm64.yml b/.github/workflows/build-linux-arm64.yml new file mode 100644 index 000000000..86e37e69d --- /dev/null +++ b/.github/workflows/build-linux-arm64.yml @@ -0,0 +1,13 @@ +name: Builder (Linux arm64) + +on: + push: + pull_request: + workflow_call: + +jobs: + build: + uses: ./.github/workflows/build-linux.yml + with: + arch: arm64 + output_name: PluginLoader-arm64 \ No newline at end of file diff --git a/.github/workflows/build-linux-x86_64.yml b/.github/workflows/build-linux-x86_64.yml new file mode 100644 index 000000000..115b49908 --- /dev/null +++ b/.github/workflows/build-linux-x86_64.yml @@ -0,0 +1,13 @@ +name: Builder (Linux x86_64) + +on: + push: + pull_request: + workflow_call: + +jobs: + build: + uses: ./.github/workflows/build-linux.yml + with: + arch: x86_64 + output_name: PluginLoader \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build-linux.yml similarity index 64% rename from .github/workflows/build.yml rename to .github/workflows/build-linux.yml index 46a106a11..c8c4ee42b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build-linux.yml @@ -1,16 +1,21 @@ -name: Builder +name: Builder (Linux) on: - push: - pull_request: workflow_call: - # schedule: - # - cron: '0 13 * * *' # run at 1 PM UTC + inputs: + arch: + description: Architecture to build for (x86_64, arm64) + type: string + default: x86_64 + output_name: + description: Output name + type: string + default: PluginLoader jobs: build: - name: Build PluginLoader - runs-on: ubuntu-22.04 + name: Build ${{ inputs.output_name }} (Linux ${{ inputs.arch }}) + runs-on: ${{ inputs.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-22.04' }} steps: - name: Checkout 🧰 @@ -55,15 +60,19 @@ jobs: poetry dynamic-versioning pyinstaller pyinstaller.spec + - name: Rename artifact 🛠️ + if: ${{ inputs.output_name != 'PluginLoader' }} + run: mv ./backend/dist/PluginLoader ./backend/dist/${{ inputs.output_name }} + - name: Upload package artifact ⬆️ if: ${{ !env.ACT }} uses: actions/upload-artifact@v4 with: - name: PluginLoader - path: ./backend/dist/PluginLoader + name: ${{ inputs.output_name }} + path: ./backend/dist/${{ inputs.output_name }} - name: Download package artifact locally if: ${{ env.ACT }} uses: actions/upload-artifact@v4 with: - path: ./backend/dist/PluginLoader + path: ./backend/dist/${{ inputs.output_name }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac1637e4e..bbf379564 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,18 +123,24 @@ jobs: build: name: Build tagged artifact - uses: ./.github/workflows/build.yml + uses: ./.github/workflows/build-linux-x86_64.yml + needs: [create_tag] + + build_arm64: + name: Build tagged artifact (arm64) + uses: ./.github/workflows/build-linux-arm64.yml needs: [create_tag] release: - name: Release tagged artifact + name: Release tagged artifacts runs-on: ubuntu-22.04 - needs: [create_tag, build] + needs: [create_tag, build, build_arm64] steps: - - name: Fetch package artifact ⬇️ + - name: Fetch package artifacts ⬇️ uses: actions/download-artifact@v4 with: - name: PluginLoader + pattern: PluginLoader* + merge-multiple: true - name: Pre-release 📦 if: github.event.inputs.release == 'prerelease' @@ -142,7 +148,9 @@ jobs: with: name: Prerelease ${{ needs.create_tag.outputs.tag_name }} tag_name: ${{ needs.create_tag.outputs.tag_name }} - files: ./PluginLoader + files: | + ./PluginLoader + ./PluginLoader-arm64 prerelease: true generate_release_notes: true - name: Release 📦 @@ -151,6 +159,8 @@ jobs: with: name: Release ${{ needs.create_tag.outputs.tag_name }} tag_name: ${{ needs.create_tag.outputs.tag_name }} - files: ./PluginLoader + files: | + ./PluginLoader + ./PluginLoader-arm64 prerelease: false generate_release_notes: true From 0fb4ba855db879101fedc4ef196d004a7c7574d0 Mon Sep 17 00:00:00 2001 From: Gianni Spadoni Date: Fri, 5 Jun 2026 14:40:16 +0800 Subject: [PATCH 02/11] fix(ci): update Windows workflow name brings it in line with the new Linux build names Signed-off-by: Gianni Spadoni --- .github/workflows/build-win.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-win.yml b/.github/workflows/build-win.yml index 6cf504c4b..ecf8928ea 100644 --- a/.github/workflows/build-win.yml +++ b/.github/workflows/build-win.yml @@ -1,4 +1,4 @@ -name: Builder Win +name: Builder (Windows) on: push: @@ -9,7 +9,7 @@ permissions: jobs: build-win: - name: Build PluginLoader for Win + name: Build PluginLoader (Windows x86_64) runs-on: windows-2022 steps: From 8e61fb50ec2d33f4c8daea19a08e0648089c909d Mon Sep 17 00:00:00 2001 From: Gianni Spadoni Date: Sun, 7 Jun 2026 15:47:39 +0800 Subject: [PATCH 03/11] feat(localplatform): add ON_ARM64 static bool The updater can use this to handle arm64 builds Signed-off-by: Gianni Spadoni --- backend/decky_loader/localplatform/localplatform.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/decky_loader/localplatform/localplatform.py b/backend/decky_loader/localplatform/localplatform.py index 6cb089824..e77603156 100644 --- a/backend/decky_loader/localplatform/localplatform.py +++ b/backend/decky_loader/localplatform/localplatform.py @@ -4,6 +4,9 @@ ON_MAC = platform.system() == "Darwin" ON_LINUX = not ON_WINDOWS and not ON_MAC +# macOS reports arm64, Linux reports aarch64 +ON_ARM64 = platform.machine() == "arm64" or platform.machine() == "aarch64" + if ON_WINDOWS: from .localplatformwin import * from . import localplatformwin as localplatform From a0b283d3e87cd9ba056f624977bcfaab94ceb2d6 Mon Sep 17 00:00:00 2001 From: Gianni Spadoni Date: Sun, 7 Jun 2026 15:49:43 +0800 Subject: [PATCH 04/11] feat(updater): add Linux arm64 support, use new workflow names Updates workflow name handling: Windows handles "Builder Win" (legacy) and "Builder (Windows)" Linux x86_64 handles "Builder" (legacy) and "Builder (Linux x86_64)" Linux arm64 handles "Builder (Linux arm64)" I've made sure to keep the file name the same on disk so the systemd service doesn't break Signed-off-by: Gianni Spadoni --- backend/decky_loader/updater.py | 45 ++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/backend/decky_loader/updater.py b/backend/decky_loader/updater.py index 3fdfcc048..108f6ea15 100644 --- a/backend/decky_loader/updater.py +++ b/backend/decky_loader/updater.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, List, TypedDict if TYPE_CHECKING: from .main import PluginManager -from .localplatform.localplatform import chmod, service_restart, service_stop, ON_LINUX, ON_WINDOWS, get_keep_systemd_service, get_selinux +from .localplatform.localplatform import chmod, service_restart, service_stop, ON_LINUX, ON_WINDOWS, ON_ARM64, get_keep_systemd_service, get_selinux import shutil from typing import List, TYPE_CHECKING, TypedDict import zipfile @@ -138,8 +138,35 @@ async def version_reloader(self): pass await sleep(60 * 60 * 6) # 6 hours + def get_remote_binary_name(self): + """ + The binaries on GitHub contain an extra part in their name to denote their architecture + + For example: + Windows: PluginLoader.exe + Linux (x86_64): PluginLoader + Linux (arm64): PluginLoader-arm64 + """ + binary_name = "PluginLoader" + + if ON_ARM64 and ON_LINUX: + binary_name += "-arm64" + + if ON_WINDOWS: + binary_name += ".exe" + + return binary_name + + def get_local_binary_name(self): + binary_name = "PluginLoader" + + if ON_WINDOWS: + binary_name += ".exe" + + return binary_name + async def download_decky_binary(self, download_url: str, version: str, is_zip: bool = False, size_in_bytes: int | None = None): - download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe" + download_filename = self.get_local_binary_name() download_temp_filename = download_filename + ".new" if size_in_bytes == None: @@ -196,10 +223,10 @@ async def do_update(self): version = self.remoteVer["tag_name"] download_url = None size_in_bytes = None - download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe" + remote_binary_name = self.get_remote_binary_name() for x in self.remoteVer["assets"]: - if x["name"] == download_filename: + if x["name"] == remote_binary_name: download_url = x["browser_download_url"] size_in_bytes = x["size"] break @@ -270,10 +297,16 @@ async def download_testing_version(self, pr_id: int, sha_id: str): 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': + # Windows x86_64 + if ON_WINDOWS and work['name'] == 'Builder (Windows)' or work['name'] == 'Builder Win': + down_id=work['id'] + break + # Linux arm64 + elif ON_LINUX and ON_ARM64 and work['name'] == 'Builder (Linux arm64)': down_id=work['id'] break - elif ON_LINUX and work['name'] == 'Builder': + # Linux x86_64 + elif ON_LINUX and work['name'] == 'Builder (Linux x86_64)' or work['name'] == 'Builder': down_id=work['id'] break if down_id != '': From 879f0d8d2d2cdec07df3634699d64ecbd217e5b0 Mon Sep 17 00:00:00 2001 From: virtudude Date: Mon, 21 Sep 2026 09:26:16 -0400 Subject: [PATCH 05/11] fix(updater): keep existing workflow names, pick PR test build per arch --- .github/workflows/build-linux-arm64.yml | 2 +- .github/workflows/build-linux-x86_64.yml | 2 +- .github/workflows/build-win.yml | 4 ++-- backend/decky_loader/updater.py | 9 +++------ 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-linux-arm64.yml b/.github/workflows/build-linux-arm64.yml index 86e37e69d..5692b3b3f 100644 --- a/.github/workflows/build-linux-arm64.yml +++ b/.github/workflows/build-linux-arm64.yml @@ -1,4 +1,4 @@ -name: Builder (Linux arm64) +name: Builder ARM64 on: push: diff --git a/.github/workflows/build-linux-x86_64.yml b/.github/workflows/build-linux-x86_64.yml index 115b49908..4671944b8 100644 --- a/.github/workflows/build-linux-x86_64.yml +++ b/.github/workflows/build-linux-x86_64.yml @@ -1,4 +1,4 @@ -name: Builder (Linux x86_64) +name: Builder on: push: diff --git a/.github/workflows/build-win.yml b/.github/workflows/build-win.yml index ecf8928ea..6cf504c4b 100644 --- a/.github/workflows/build-win.yml +++ b/.github/workflows/build-win.yml @@ -1,4 +1,4 @@ -name: Builder (Windows) +name: Builder Win on: push: @@ -9,7 +9,7 @@ permissions: jobs: build-win: - name: Build PluginLoader (Windows x86_64) + name: Build PluginLoader for Win runs-on: windows-2022 steps: diff --git a/backend/decky_loader/updater.py b/backend/decky_loader/updater.py index 108f6ea15..46df095a7 100644 --- a/backend/decky_loader/updater.py +++ b/backend/decky_loader/updater.py @@ -297,16 +297,13 @@ async def download_testing_version(self, pr_id: int, sha_id: str): works = await res.json() #Iterate over the workflow_run to get the two builds if they exists for work in works['workflow_runs']: - # Windows x86_64 - if ON_WINDOWS and work['name'] == 'Builder (Windows)' or work['name'] == 'Builder Win': + if ON_WINDOWS and work['name'] == 'Builder Win': down_id=work['id'] break - # Linux arm64 - elif ON_LINUX and ON_ARM64 and work['name'] == 'Builder (Linux arm64)': + elif ON_LINUX and ON_ARM64 and work['name'] == 'Builder ARM64': down_id=work['id'] break - # Linux x86_64 - elif ON_LINUX and work['name'] == 'Builder (Linux x86_64)' or work['name'] == 'Builder': + elif ON_LINUX and not ON_ARM64 and work['name'] == 'Builder': down_id=work['id'] break if down_id != '': From 0a79d2fbe02d62fd34ae7de55084881c5b6aa0de Mon Sep 17 00:00:00 2001 From: virtudude Date: Mon, 21 Sep 2026 09:27:16 -0400 Subject: [PATCH 06/11] fix(ci): keep artifact binary named PluginLoader, apply -arm64 suffix at release --- .github/workflows/build-linux-arm64.yml | 2 +- .github/workflows/build-linux-x86_64.yml | 2 +- .github/workflows/build-linux.yml | 16 ++++++---------- .github/workflows/release.yml | 18 +++++++++++++++--- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-linux-arm64.yml b/.github/workflows/build-linux-arm64.yml index 5692b3b3f..f5cae2994 100644 --- a/.github/workflows/build-linux-arm64.yml +++ b/.github/workflows/build-linux-arm64.yml @@ -10,4 +10,4 @@ jobs: uses: ./.github/workflows/build-linux.yml with: arch: arm64 - output_name: PluginLoader-arm64 \ No newline at end of file + artifact_name: PluginLoader-arm64 \ No newline at end of file diff --git a/.github/workflows/build-linux-x86_64.yml b/.github/workflows/build-linux-x86_64.yml index 4671944b8..54085a56d 100644 --- a/.github/workflows/build-linux-x86_64.yml +++ b/.github/workflows/build-linux-x86_64.yml @@ -10,4 +10,4 @@ jobs: uses: ./.github/workflows/build-linux.yml with: arch: x86_64 - output_name: PluginLoader \ No newline at end of file + artifact_name: PluginLoader \ No newline at end of file diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index c8c4ee42b..441531eac 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -7,14 +7,14 @@ on: description: Architecture to build for (x86_64, arm64) type: string default: x86_64 - output_name: - description: Output name + artifact_name: + description: Name of the uploaded artifact; the binary inside is always PluginLoader type: string default: PluginLoader jobs: build: - name: Build ${{ inputs.output_name }} (Linux ${{ inputs.arch }}) + name: Build ${{ inputs.artifact_name }} (Linux ${{ inputs.arch }}) runs-on: ${{ inputs.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-22.04' }} steps: @@ -60,19 +60,15 @@ jobs: poetry dynamic-versioning pyinstaller pyinstaller.spec - - name: Rename artifact 🛠️ - if: ${{ inputs.output_name != 'PluginLoader' }} - run: mv ./backend/dist/PluginLoader ./backend/dist/${{ inputs.output_name }} - - name: Upload package artifact ⬆️ if: ${{ !env.ACT }} uses: actions/upload-artifact@v4 with: - name: ${{ inputs.output_name }} - path: ./backend/dist/${{ inputs.output_name }} + name: ${{ inputs.artifact_name }} + path: ./backend/dist/PluginLoader - name: Download package artifact locally if: ${{ env.ACT }} uses: actions/upload-artifact@v4 with: - path: ./backend/dist/${{ inputs.output_name }} + path: ./backend/dist/PluginLoader diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bbf379564..3a59a1f86 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -136,11 +136,23 @@ jobs: runs-on: ubuntu-22.04 needs: [create_tag, build, build_arm64] steps: - - name: Fetch package artifacts ⬇️ + - name: Fetch package artifact (x86_64) ⬇️ uses: actions/download-artifact@v4 with: - pattern: PluginLoader* - merge-multiple: true + name: PluginLoader + path: ./artifacts/x86_64 + + - name: Fetch package artifact (arm64) ⬇️ + uses: actions/download-artifact@v4 + with: + name: PluginLoader-arm64 + path: ./artifacts/arm64 + + # Release assets need unique names; the artifacts both contain "PluginLoader" + - name: Name release assets 🛠️ + run: | + mv ./artifacts/x86_64/PluginLoader ./PluginLoader + mv ./artifacts/arm64/PluginLoader ./PluginLoader-arm64 - name: Pre-release 📦 if: github.event.inputs.release == 'prerelease' From d67fd16219e3056c7d70673e4fce0425f4c9d229 Mon Sep 17 00:00:00 2001 From: xXJsonDeruloXx Date: Fri, 25 Sep 2026 14:41:12 -0400 Subject: [PATCH 07/11] ARM64: delegate Decky updates to DroidDeck --- backend/decky_loader/updater.py | 21 +++++++++++++++++-- .../settings/pages/general/Updater.tsx | 6 ++++-- frontend/src/updater.ts | 1 + 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/backend/decky_loader/updater.py b/backend/decky_loader/updater.py index 46df095a7..dd179a9e9 100644 --- a/backend/decky_loader/updater.py +++ b/backend/decky_loader/updater.py @@ -92,15 +92,24 @@ def get_service_url(self): return str(url) async def get_version_info(self): + managed_externally = ON_LINUX and ON_ARM64 return { "current": self.localVer, "remote": self.remoteVer, - "all": self.allRemoteVers, - "updatable": self.localVer != "unknown" and self.localVer != "dev" + "all": None if managed_externally else self.allRemoteVers, + "updatable": self.localVer != "unknown" and self.localVer != "dev" and not managed_externally, + "managed_externally": managed_externally, } async def check_for_updates(self): logger.debug("checking for updates") + # DroidDeck installs and updates the ARM64 binary from its compatible release + # assets. The upstream updater only knows the regular Linux binary and could + # replace the working ARM64 loader with an incompatible executable. + if ON_LINUX and ON_ARM64: + self.remoteVer = None + self.allRemoteVers = [] + return await self.get_version_info() selectedBranch = self.get_branch(self.context.settings) async with ClientSession() as web: async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res: @@ -214,6 +223,9 @@ async def download_decky_binary(self, download_url: str, version: str, is_zip: b async def do_update(self): logger.debug("Starting update.") + if ON_LINUX and ON_ARM64: + logger.warning("ARM64 Decky updates are managed by DroidDeck.") + return try: assert self.remoteVer except AssertionError: @@ -274,6 +286,8 @@ async def do_shutdown(self): await service_stop("plugin_loader") async def get_testing_versions(self) -> List[TestingVersion]: + if ON_LINUX and ON_ARM64: + return [] result: List[TestingVersion] = [] async with ClientSession() as web: async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/pulls", @@ -289,6 +303,9 @@ async def get_testing_versions(self) -> List[TestingVersion]: return result async def download_testing_version(self, pr_id: int, sha_id: str): + if ON_LINUX and ON_ARM64: + logger.warning("ARM64 testing builds are managed by DroidDeck.") + return down_id = '' #Get all the associated workflow run for the given sha_id code hash async with ClientSession() as web: diff --git a/frontend/src/components/settings/pages/general/Updater.tsx b/frontend/src/components/settings/pages/general/Updater.tsx index 37aceb722..475698aac 100644 --- a/frontend/src/components/settings/pages/general/Updater.tsx +++ b/frontend/src/components/settings/pages/general/Updater.tsx @@ -125,7 +125,9 @@ export default function UpdaterSettings() { onOptionsButton={versionInfo?.all ? showPatchNotes : undefined} label={t('Updater.updates.label')} description={ - checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? ( + versionInfo?.managed_externally ? ( + Updates are managed in DroidDeck's Steam session settings. + ) : checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? ( '' ) : ( {t('Updater.updates.lat_version', { ver: versionInfo?.current })} @@ -139,7 +141,7 @@ export default function UpdaterSettings() { } childrenContainerWidth={'fixed'} > - {updateProgress == -1 && !isLoaderUpdating ? ( + {versionInfo?.managed_externally ? null : updateProgress == -1 && !isLoaderUpdating ? ( Date: Fri, 25 Sep 2026 16:20:20 -0400 Subject: [PATCH 08/11] fix: add more deps to pyinstaller spec (#970) --- backend/pyinstaller.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/pyinstaller.spec b/backend/pyinstaller.spec index 91ff4e5b3..13ac66a07 100644 --- a/backend/pyinstaller.spec +++ b/backend/pyinstaller.spec @@ -9,7 +9,7 @@ a = Analysis( ('decky_loader/locales', 'decky_loader/locales'), ('decky_loader/static', 'decky_loader/static'), ] + copy_metadata('decky_loader'), - hiddenimports=['logging.handlers', 'sqlite3', 'decky_plugin', 'decky'], + hiddenimports=['logging.handlers', 'sqlite3', 'http.server', 'socketserver', 'configparser', 'decky_plugin', 'decky'], ) pyz = PYZ(a.pure, a.zipped_data) From 84288115c8db0abe40061df0612567ab4ac81967 Mon Sep 17 00:00:00 2001 From: Nihaal Sangha Date: Fri, 25 Sep 2026 21:32:11 +0100 Subject: [PATCH 09/11] Add sort plugins setting (#859) --- backend/decky_loader/locales/en-US.json | 3 ++ frontend/src/components/DeckyState.tsx | 11 ++++++ frontend/src/components/PluginView.tsx | 9 +++-- .../settings/pages/general/index.tsx | 13 ++++++- .../settings/pages/plugin_list/index.tsx | 35 +++++++++++++++---- frontend/src/plugin-loader.tsx | 6 ++++ 6 files changed, 67 insertions(+), 10 deletions(-) diff --git a/backend/decky_loader/locales/en-US.json b/backend/decky_loader/locales/en-US.json index 1f87fe3bf..6b6553adb 100644 --- a/backend/decky_loader/locales/en-US.json +++ b/backend/decky_loader/locales/en-US.json @@ -217,6 +217,9 @@ "developer_mode": { "label": "Developer mode" }, + "sort_plugins": { + "label": "Sort plugins alphabetically" + }, "notifications": { "decky_updates_label": "Decky update available", "header": "Notifications", diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx index d1b558c15..db938a26b 100644 --- a/frontend/src/components/DeckyState.tsx +++ b/frontend/src/components/DeckyState.tsx @@ -10,6 +10,7 @@ interface PublicDeckyState { disabledPlugins: DisabledPlugin[]; installedPlugins: (Plugin | DisabledPlugin)[]; pluginOrder: string[]; + sortPlugins: boolean; frozenPlugins: string[]; hiddenPlugins: string[]; activePlugin: Plugin | null; @@ -31,6 +32,7 @@ export class DeckyState { private _disabledPlugins: DisabledPlugin[] = []; private _installedPlugins: (Plugin | DisabledPlugin)[] = []; private _pluginOrder: string[] = []; + private _sortPlugins: boolean = false; private _frozenPlugins: string[] = []; private _hiddenPlugins: string[] = []; private _activePlugin: Plugin | null = null; @@ -49,6 +51,7 @@ export class DeckyState { disabledPlugins: this._disabledPlugins, installedPlugins: this._installedPlugins, pluginOrder: this._pluginOrder, + sortPlugins: this._sortPlugins, frozenPlugins: this._frozenPlugins, hiddenPlugins: this._hiddenPlugins, activePlugin: this._activePlugin, @@ -83,6 +86,11 @@ export class DeckyState { this.notifyUpdate(); } + setSortPlugins(sortPlugins: boolean) { + this._sortPlugins = sortPlugins; + this.notifyUpdate(); + } + setFrozenPlugins(frozenPlugins: string[]) { this._frozenPlugins = frozenPlugins; this.notifyUpdate(); @@ -139,6 +147,7 @@ interface DeckyStateContext extends PublicDeckyState { setActivePlugin(name: string): void; setPluginOrder(pluginOrder: string[]): void; setDisabledPlugins(disabled: DisabledPlugin[]): void; + setSortPlugins(sortPlugins: boolean): void; closeActivePlugin(): void; } @@ -178,6 +187,7 @@ export const DeckyStateContextProvider: FC = ({ children, deckyState }) = const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState); const setPluginOrder = deckyState.setPluginOrder.bind(deckyState); const setDisabledPlugins = deckyState.setDisabledPlugins.bind(deckyState); + const setSortPlugins = deckyState.setSortPlugins.bind(deckyState); return ( = ({ children, deckyState }) = closeActivePlugin, setPluginOrder, setDisabledPlugins, + setSortPlugins, }} > {children} diff --git a/frontend/src/components/PluginView.tsx b/frontend/src/components/PluginView.tsx index ffaa176a8..fbafaf051 100644 --- a/frontend/src/components/PluginView.tsx +++ b/frontend/src/components/PluginView.tsx @@ -16,6 +16,7 @@ const PluginView: FC = () => { updates, activePlugin, pluginOrder, + sortPlugins, setActivePlugin, closeActivePlugin, } = useDeckyState(); @@ -26,10 +27,14 @@ const PluginView: FC = () => { console.log('updating PluginView after changes'); return [...plugins] - .sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)) + .sort((a, b) => + sortPlugins + ? a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) + : pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name), + ) .filter((p) => p.content) .filter(({ name }) => !hiddenPlugins.includes(name)); - }, [plugins, pluginOrder, hiddenPlugins]); + }, [plugins, pluginOrder, sortPlugins, hiddenPlugins]); const numberOfHidden = hiddenPlugins.filter((name) => !!plugins.find((p) => p.name === name)).length; diff --git a/frontend/src/components/settings/pages/general/index.tsx b/frontend/src/components/settings/pages/general/index.tsx index 84ad88f13..9bd5616d9 100644 --- a/frontend/src/components/settings/pages/general/index.tsx +++ b/frontend/src/components/settings/pages/general/index.tsx @@ -1,6 +1,7 @@ import { DialogBody, DialogControlsSection, DialogControlsSectionHeader, Field, Toggle } from '@decky/ui'; import { useTranslation } from 'react-i18next'; +import { useSetting } from '../../../../utils/hooks/useSetting'; import { useDeckyState } from '../../../DeckyState'; import BranchSelect from './BranchSelect'; import NotificationSettings from './NotificationSettings'; @@ -14,7 +15,8 @@ export default function GeneralSettings({ isDeveloper: boolean; setIsDeveloper: (val: boolean) => void; }) { - const { versionInfo } = useDeckyState(); + const { versionInfo, sortPlugins, setSortPlugins } = useDeckyState(); + const [_, setSortPluginsSetting] = useSetting('sortPlugins', false); const { t } = useTranslation(); return ( @@ -42,6 +44,15 @@ export default function GeneralSettings({ }} /> + + { + setSortPlugins(toggleValue); + setSortPluginsSetting(toggleValue); + }} + /> + {t('SettingsGeneralIndex.about.header')} diff --git a/frontend/src/components/settings/pages/plugin_list/index.tsx b/frontend/src/components/settings/pages/plugin_list/index.tsx index 75365ba5b..71a7df6c4 100644 --- a/frontend/src/components/settings/pages/plugin_list/index.tsx +++ b/frontend/src/components/settings/pages/plugin_list/index.tsx @@ -11,7 +11,7 @@ import { ReorderableList, showContextMenu, } from '@decky/ui'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa'; @@ -171,9 +171,16 @@ type PluginData = { }; export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) { - const { installedPlugins, disabledPlugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } = - useDeckyState(); - + const { + installedPlugins, + disabledPlugins, + updates, + pluginOrder, + setPluginOrder, + frozenPlugins, + hiddenPlugins, + sortPlugins, + } = useDeckyState(); const [_, setPluginOrderSetting] = useSetting( 'pluginOrder', installedPlugins.map((plugin) => plugin.name), @@ -188,6 +195,12 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) { const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService; const frozenPluginsService = DeckyPluginLoader.frozenPluginsService; + const sortedPlugins = useMemo(() => { + return sortPlugins + ? [...installedPlugins].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) + : installedPlugins; + }, [installedPlugins, sortPlugins]); + useEffect(() => { setPluginEntries( installedPlugins.map(({ name, version }) => { @@ -204,7 +217,7 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) { disabled={disabledPlugins.find((p) => p.name == name) !== undefined} /> ), - position: pluginOrder.indexOf(name), + position: sortPlugins ? sortedPlugins.findIndex((p) => p.name === name) : pluginOrder.indexOf(name), data: { name, disabled: disabledPlugins.some((disabledPlugin) => disabledPlugin.name === name), @@ -221,7 +234,7 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) { }; }), ); - }, [installedPlugins, updates, hiddenPlugins, disabledPlugins]); + }, [installedPlugins, updates, hiddenPlugins, disabledPlugins, sortPlugins, sortedPlugins]); if (installedPlugins.length === 0) { return ( @@ -232,6 +245,9 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) { } function onSave(entries: ReorderableEntry[]) { + if (sortPlugins) { + return; + } const newOrder = entries.map((entry) => entry.data!.name); console.log(newOrder); setPluginOrder(newOrder); @@ -265,7 +281,12 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) { )} - entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} /> + + entries={pluginEntries} + onSave={onSave} + interactables={PluginInteractables} + disableReordering={sortPlugins} + /> ); diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index f0a358ef6..31f4ec7e3 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -360,6 +360,12 @@ class PluginLoader extends Logger { this.deckyState.setPluginOrder(pluginOrder); }); + // Grab and set sort plugins setting + getSetting('sortPlugins', false).then((sortPlugins) => { + this.debug('sortPlugins: ', sortPlugins); + this.deckyState.setSortPlugins(sortPlugins); + }); + this.frozenPluginsService.init(); this.hiddenPluginsService.init(); this.notificationService.init(); From 75563316f9119ee7e36be7f43885be65e877fad0 Mon Sep 17 00:00:00 2001 From: pablosarm <47988099+pablosarm@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:36:21 -0300 Subject: [PATCH 10/11] Fix file picker with inaccessible directory entries (#969) Co-authored-by: Pablo Sarmiento --- backend/decky_loader/utilities.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/backend/decky_loader/utilities.py b/backend/decky_loader/utilities.py index c74ac5d14..99a1b0197 100644 --- a/backend/decky_loader/utilities.py +++ b/backend/decky_loader/utilities.py @@ -333,20 +333,24 @@ async def filepicker_ls(self, #Resolving all files/folders in the requested directory for file in path_obj.iterdir(): - if file.exists(): - filest = file.stat() - is_hidden = file.name.startswith('.') - if ON_WINDOWS and not is_hidden: - is_hidden = bool(filest.st_file_attributes & FILE_ATTRIBUTE_HIDDEN) # type: ignore - if include_folders and file.is_dir(): - if (is_hidden and include_hidden) or not is_hidden: - folders.append({"file": file, "filest": filest, "is_dir": True}) - elif include_files: - # Handle requested extensions if present - if include_ext == None or len(include_ext) == 0 or 'all_files' in include_ext \ - or splitext(file.name)[1].lstrip('.').upper() in (ext.upper() for ext in include_ext): + try: + if file.exists(): + filest = file.stat() + is_hidden = file.name.startswith('.') + if ON_WINDOWS and not is_hidden: + is_hidden = bool(filest.st_file_attributes & FILE_ATTRIBUTE_HIDDEN) # type: ignore + if include_folders and file.is_dir(): if (is_hidden and include_hidden) or not is_hidden: - files.append({"file": file, "filest": filest, "is_dir": False}) + folders.append({"file": file, "filest": filest, "is_dir": True}) + elif include_files: + # Handle requested extensions if present + if include_ext == None or len(include_ext) == 0 or 'all_files' in include_ext \ + or splitext(file.name)[1].lstrip('.').upper() in (ext.upper() for ext in include_ext): + if (is_hidden and include_hidden) or not is_hidden: + files.append({"file": file, "filest": filest, "is_dir": False}) + except (PermissionError, OSError): + self.logger.debug(f"Skipping inaccessible file picker entry: {file}") + continue # Filter logic if filter_for is not None: try: From 5dea9f843c7efafafcca345e730ce97961509b74 Mon Sep 17 00:00:00 2001 From: xXJsonDeruloXx Date: Thu, 8 Oct 2026 07:09:00 -0400 Subject: [PATCH 11/11] Support named DroidDeck Preview releases with checksums --- .github/workflows/release.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a59a1f86..c727053f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,10 @@ name: Release on: workflow_dispatch: inputs: + preview_tag: + type: string + description: Optional DroidDeck Preview tag (droiddeck-arm64-preview.N) + default: '' release: type: choice description: Release the asset @@ -51,7 +55,17 @@ jobs: - name: Prepare tag ⚙️ id: ready_tag + env: + PREVIEW_TAG: ${{ inputs.preview_tag }} run: | + if [[ -n "$PREVIEW_TAG" ]]; then + if [[ "${{ inputs.release }}" != "prerelease" || ! "$PREVIEW_TAG" =~ ^droiddeck-arm64-preview\.[1-9][0-9]*$ ]]; then + echo "DroidDeck Preview tags require prerelease and the format droiddeck-arm64-preview.N" + exit 1 + fi + echo "tag_name=$PREVIEW_TAG" >> "$GITHUB_OUTPUT" + exit 0 + fi export VERSION=${{ steps.latest_release.outputs.release }} echo "VERS: $VERSION" if [[ ${{github.event.inputs.release}} == "release" ]]; then @@ -153,6 +167,8 @@ jobs: run: | mv ./artifacts/x86_64/PluginLoader ./PluginLoader mv ./artifacts/arm64/PluginLoader ./PluginLoader-arm64 + sha256sum PluginLoader > PluginLoader.sha256 + sha256sum PluginLoader-arm64 > PluginLoader-arm64.sha256 - name: Pre-release 📦 if: github.event.inputs.release == 'prerelease' @@ -163,6 +179,8 @@ jobs: files: | ./PluginLoader ./PluginLoader-arm64 + ./PluginLoader.sha256 + ./PluginLoader-arm64.sha256 prerelease: true generate_release_notes: true - name: Release 📦 @@ -174,5 +192,7 @@ jobs: files: | ./PluginLoader ./PluginLoader-arm64 + ./PluginLoader.sha256 + ./PluginLoader-arm64.sha256 prerelease: false generate_release_notes: true