From 915997d1495cc47f60e148252df3e4878a1179a5 Mon Sep 17 00:00:00 2001 From: Party Wumpus <48649272+PartyWumpus@users.noreply.github.com> Date: Sat, 14 Jan 2023 15:49:23 +0000 Subject: [PATCH 01/15] [readme] change terminal commands to point at decky-installer (#342) * update readme to point at decky-installer for konsole scripts * test change to make dark/light theme work properly * Update README.md * Update README.md * revert changes as they didn't actually work --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 586b4f62..a3e6d1c7 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ For more information about Decky Loader as well as documentation and development Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://deckbrew.xyz/en/loader-dev/development). 1. Open the Return to Gaming Mode shortcut on your desktop. -- There is also a fast install for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_release.sh | sh` and type your password when prompted. +- There is also a fast install for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/install_release.sh | sh` and type your password when prompted. ### 👋 Uninstallation @@ -66,7 +66,7 @@ We are sorry to see you go! If you are considering uninstalling because you are 1. Press the button and open the Power menu. 1. Select "Switch to Desktop". 1. Run the installer file again, and select `uninstall decky loader` -- There is also a fast uninstall for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/uninstall.sh | sh` and type your password when prompted. +- There is also a fast uninstall for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/uninstall.sh | sh` and type your password when prompted. ## 🚀 Getting Started From 5fdcc56409a0bc75171df25ca50b6e09ffe7bf3a Mon Sep 17 00:00:00 2001 From: TrainDoctor Date: Sun, 15 Jan 2023 17:40:47 -0800 Subject: [PATCH 02/15] Aa/bump dfl navigation fix jan2023 (#341) * fix React DevTools * bump DFL to fix Navigation * Bump DFL and add shims * fix shims not applying due to timing issue Co-authored-by: AAGaming --- backend/injector.py | 13 ++++++++++++- backend/main.py | 9 ++------- backend/utilities.py | 6 ++++-- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 8 ++++---- frontend/src/index.tsx | 18 ++++++++++++++++++ 6 files changed, 41 insertions(+), 15 deletions(-) diff --git a/backend/injector.py b/backend/injector.py index f4dfd384..d77de13a 100644 --- a/backend/injector.py +++ b/backend/injector.py @@ -394,9 +394,12 @@ async def get_tab_lambda(test) -> Tab: raise ValueError(f"Tab not found by lambda") return tab +def tab_is_gamepadui(t: Tab) -> bool: + return "https://steamloopback.host/routes/" in t.url and (t.title == "Steam Shared Context presented by Valve™" or t.title == "Steam" or t.title == "SP") + async def get_gamepadui_tab() -> Tab: tabs = await get_tabs() - tab = next((i for i in tabs if ("https://steamloopback.host/routes/" in i.url and (i.title == "Steam Shared Context presented by Valve™" or i.title == "Steam" or i.title == "SP"))), None) + tab = next((i for i in tabs if tab_is_gamepadui(i)), None) if not tab: raise ValueError(f"GamepadUI Tab not found") return tab @@ -405,3 +408,11 @@ async def inject_to_tab(tab_name, js, run_async=False): tab = await get_tab(tab_name) return await tab.evaluate_js(js, run_async) + +async def close_old_tabs(): + tabs = await get_tabs() + for t in tabs: + if not t.title or (t.title != "Steam Shared Context presented by Valve™" and t.title != "Steam" and t.title != "SP"): + logger.debug("Closing tab: " + getattr(t, "title", "Untitled")) + await t.close() + await sleep(0.5) \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index b99e7df1..44311cf3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -22,7 +22,7 @@ from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token, get_home_path, get_homebrew_path, get_user, get_user_group, set_user, set_user_group, stop_systemd_unit, start_systemd_unit) -from injector import get_gamepadui_tab, Tab, get_tabs +from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs from loader import Loader from settings import SettingsManager from updater import Updater @@ -170,12 +170,7 @@ class PluginManager: try: if first: if await tab.has_global_var("deckyHasLoaded", False): - tabs = await get_tabs() - for t in tabs: - if not t.title or (t.title != "Steam" and t.title != "SP"): - logger.debug("Closing tab: " + getattr(t, "title", "Untitled")) - await t.close() - await sleep(0.5) + await close_old_tabs() await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => location.reload(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}", False, False, False) except: logger.info("Failed to inject JavaScript into tab\n" + format_exc()) diff --git a/backend/utilities.py b/backend/utilities.py index 0723402f..88e7d0bf 100644 --- a/backend/utilities.py +++ b/backend/utilities.py @@ -7,7 +7,7 @@ from asyncio import sleep, start_server, gather, open_connection from aiohttp import ClientSession, web from logging import getLogger -from injector import inject_to_tab, get_gamepadui_tab +from injector import inject_to_tab, get_gamepadui_tab, close_old_tabs import helpers import subprocess @@ -251,6 +251,7 @@ class Utilities: self.logger.info("Connected to React DevTools, loading script") tab = await get_gamepadui_tab() # RDT needs to load before React itself to work. + await close_old_tabs() result = await tab.reload_and_evaluate(script) self.logger.info(result) @@ -262,5 +263,6 @@ class Utilities: self.logger.info("Disabling React DevTools") tab = await get_gamepadui_tab() self.rdt_script_id = None - await tab.evaluate_js("SteamClient.User.StartRestart();", False, True, False) + await close_old_tabs() + await tab.evaluate_js("location.reload();", False, True, False) self.logger.info("React DevTools disabled") diff --git a/frontend/package.json b/frontend/package.json index 3e45e49a..d76289aa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,7 +41,7 @@ } }, "dependencies": { - "decky-frontend-lib": "^3.18.4", + "decky-frontend-lib": "^3.18.8", "react-file-icon": "^1.2.0", "react-icons": "^4.4.0", "react-markdown": "^8.0.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 58fa64ce..721c3a8d 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -10,7 +10,7 @@ specifiers: '@types/react-file-icon': ^1.0.1 '@types/react-router': 5.1.18 '@types/webpack': ^5.28.0 - decky-frontend-lib: ^3.18.4 + decky-frontend-lib: ^3.18.8 husky: ^8.0.1 import-sort-style-module: ^6.0.0 inquirer: ^8.2.4 @@ -30,7 +30,7 @@ specifiers: typescript: ^4.7.4 dependencies: - decky-frontend-lib: 3.18.4 + decky-frontend-lib: 3.18.8 react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty react-icons: 4.4.0_react@16.14.0 react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u @@ -944,8 +944,8 @@ packages: dependencies: ms: 2.1.2 - /decky-frontend-lib/3.18.4: - resolution: {integrity: sha512-i3TAe3RJtT1TK0rJgW9Ek5jxMWZRCYLDvqHDylGVieUvuyI7c8X+cogz30pP4cqeGOaA1d/MxBEbhlpD3JhVvg==} + /decky-frontend-lib/3.18.8: + resolution: {integrity: sha512-tgN35XgfsAePpmlxdCXnJ/TswmDoP2Dh9Ddl49bHbZMX2GV/H+7jT532Rrs/q8D4xX1LN1qA4alZ8Rr4q61PVg==} dev: false /decode-named-character-reference/1.0.2: diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 03010e13..86dd90e1 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,3 +1,5 @@ +import { Navigation, Router, sleep } from 'decky-frontend-lib'; + import PluginLoader from './plugin-loader'; import { DeckyUpdater } from './updater'; @@ -14,6 +16,22 @@ declare global { } } +(async () => { + try { + if (!Router.NavigateToAppProperties || !Router.NavigateToLibraryTab || !Router.NavigateToInvites) { + while (!Navigation.NavigateToAppProperties) await sleep(100); + const shims = { + NavigateToAppProperties: Navigation.NavigateToAppProperties, + NavigateToInvites: Navigation.NavigateToInvites, + NavigateToLibraryTab: Navigation.NavigateToLibraryTab, + }; + Object.assign(Router, shims); + } + } catch (e) { + console.error('[DECKY]: Error initializing Navigation interface shims', e); + } +})(); + (async () => { window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text()); From d695b90baf8bb5e62988d39e137604a4aa7b4ff0 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Sun, 15 Jan 2023 21:22:50 -0500 Subject: [PATCH 03/15] fix React DevTools again --- backend/utilities.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/utilities.py b/backend/utilities.py index 88e7d0bf..7b0a5c89 100644 --- a/backend/utilities.py +++ b/backend/utilities.py @@ -233,7 +233,7 @@ class Utilities: self.rdt_proxy_server.close() self.rdt_proxy_task.cancel() - async def enable_rdt(self): + async def _enable_rdt(self): # TODO un-hardcode port try: self.stop_rdt_proxy() @@ -243,11 +243,22 @@ class Utilities: self.logger.info("Connecting to React DevTools at " + ip) async with ClientSession() as web: res = await web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context()) + script = """ + if (!window.deckyHasConnectedRDT) { + window.deckyHasConnectedRDT = true; + // This fixes the overlay when hovering over an element in RDT + Object.defineProperty(window, '__REACT_DEVTOOLS_TARGET_WINDOW__', { + enumerable: true, + configurable: true, + get: function() { + return FocusNavController?.m_ActiveContext?.ActiveWindow || window; + } + }); + """ + await res.text() + "\n}" if res.status != 200: self.logger.error("Failed to connect to React DevTools at " + ip) return False self.start_rdt_proxy(ip, 8097) - script = "if(!window.deckyHasConnectedRDT){window.deckyHasConnectedRDT=true;\n" + await res.text() + "\n}" self.logger.info("Connected to React DevTools, loading script") tab = await get_gamepadui_tab() # RDT needs to load before React itself to work. @@ -259,6 +270,9 @@ class Utilities: self.logger.error("Failed to connect to React DevTools") self.logger.error(format_exc()) + async def enable_rdt(self): + self.context.loop.create_task(self._enable_rdt()) + async def disable_rdt(self): self.logger.info("Disabling React DevTools") tab = await get_gamepadui_tab() From 83680fffa2a73713f6af74752c0a20a2767717ea Mon Sep 17 00:00:00 2001 From: AAGaming Date: Mon, 16 Jan 2023 09:12:52 -0500 Subject: [PATCH 04/15] indicate to DFL that the router has shim applied --- frontend/src/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 86dd90e1..f8e1df31 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -25,6 +25,7 @@ declare global { NavigateToInvites: Navigation.NavigateToInvites, NavigateToLibraryTab: Navigation.NavigateToLibraryTab, }; + (Router as unknown as any).deckyShim = true; Object.assign(Router, shims); } } catch (e) { From 649eed89c9c5cd33d411db54939b7c86a64497db Mon Sep 17 00:00:00 2001 From: AAGaming Date: Mon, 16 Jan 2023 09:13:30 -0500 Subject: [PATCH 05/15] bump dfl --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index d76289aa..9001a472 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,7 +41,7 @@ } }, "dependencies": { - "decky-frontend-lib": "^3.18.8", + "decky-frontend-lib": "^3.18.9", "react-file-icon": "^1.2.0", "react-icons": "^4.4.0", "react-markdown": "^8.0.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 721c3a8d..4ba64804 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -10,7 +10,7 @@ specifiers: '@types/react-file-icon': ^1.0.1 '@types/react-router': 5.1.18 '@types/webpack': ^5.28.0 - decky-frontend-lib: ^3.18.8 + decky-frontend-lib: ^3.18.9 husky: ^8.0.1 import-sort-style-module: ^6.0.0 inquirer: ^8.2.4 @@ -30,7 +30,7 @@ specifiers: typescript: ^4.7.4 dependencies: - decky-frontend-lib: 3.18.8 + decky-frontend-lib: 3.18.9 react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty react-icons: 4.4.0_react@16.14.0 react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u @@ -944,8 +944,8 @@ packages: dependencies: ms: 2.1.2 - /decky-frontend-lib/3.18.8: - resolution: {integrity: sha512-tgN35XgfsAePpmlxdCXnJ/TswmDoP2Dh9Ddl49bHbZMX2GV/H+7jT532Rrs/q8D4xX1LN1qA4alZ8Rr4q61PVg==} + /decky-frontend-lib/3.18.9: + resolution: {integrity: sha512-QNMHDDAHfL+JpvVVte4Vj8iyOqvz/2iyFEknbJ1/Kz7aPTygFUsJp5mq1FDVvVNjfCYfF3fYAaZVqZu3d7pCEA==} dev: false /decode-named-character-reference/1.0.2: From a2716449f92c5f84000962143d517d20887b30b6 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Mon, 16 Jan 2023 14:17:27 -0500 Subject: [PATCH 06/15] fix missing await on chown_plugin_dir --- backend/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/main.py b/backend/main.py index 44311cf3..ad38d517 100644 --- a/backend/main.py +++ b/backend/main.py @@ -88,7 +88,7 @@ class PluginManager: else: self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT)) if CONFIG["chown_plugin_path"] == True: - chown_plugin_dir() + await chown_plugin_dir() self.loop.create_task(self.loader_reinjector()) self.loop.create_task(self.load_plugins()) From 0ad0016c620e89fecc46aad771b35a9a72749fb3 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Mon, 16 Jan 2023 14:44:16 -0500 Subject: [PATCH 07/15] move the chown --- backend/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index ad38d517..c48ad752 100644 --- a/backend/main.py +++ b/backend/main.py @@ -56,12 +56,15 @@ basicConfig( logger = getLogger("Main") -async def chown_plugin_dir(): +def chown_plugin_dir(): code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]]) code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]]) if code_chown != 0 or code_chmod != 0: logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})") +if CONFIG["chown_plugin_path"] == True: + chown_plugin_dir() + class PluginManager: def __init__(self, loop) -> None: self.loop = loop @@ -87,8 +90,6 @@ class PluginManager: self.loop.create_task(start_systemd_unit(REMOTE_DEBUGGER_UNIT)) else: self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT)) - if CONFIG["chown_plugin_path"] == True: - await chown_plugin_dir() self.loop.create_task(self.loader_reinjector()) self.loop.create_task(self.load_plugins()) From 1b6e18bcb351da250802817088b7a0507c5becf6 Mon Sep 17 00:00:00 2001 From: Nox <40637552+GoDev87@users.noreply.github.com> Date: Mon, 16 Jan 2023 23:43:16 +0100 Subject: [PATCH 08/15] Updated store CSS (#305) * PluginCard Store CSS Update * Fixing CSS * Updated * Removed padding --- frontend/src/components/store/PluginCard.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/components/store/PluginCard.tsx b/frontend/src/components/store/PluginCard.tsx index 27f98590..aa5fd1d6 100644 --- a/frontend/src/components/store/PluginCard.tsx +++ b/frontend/src/components/store/PluginCard.tsx @@ -68,6 +68,7 @@ const PluginCard: FC = ({ plugin }) => { style={{ display: 'flex', flexDirection: 'row', + margin: '0 0 0 10px', }} className="deckyStoreCardBody" > @@ -77,6 +78,7 @@ const PluginCard: FC = ({ plugin }) => { style={{ width: 'auto', height: '160px', + borderRadius: '5px', }} src={plugin.image_url} /> @@ -144,12 +146,14 @@ const PluginCard: FC = ({ plugin }) => { display: 'flex', flexDirection: 'row', width: '100%', + margin: '10px', }} >
Date: Tue, 17 Jan 2023 15:37:43 -0800 Subject: [PATCH 09/15] fix releases being called prereleases --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df8b03f6..d944416d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -159,7 +159,7 @@ jobs: uses: softprops/action-gh-release@v1 if: ${{ github.event_name == 'workflow_dispatch' && !env.ACT }} with: - name: Prerelease ${{ steps.ready_tag.outputs.tag_name }} + name: Release ${{ steps.ready_tag.outputs.tag_name }} tag_name: ${{ steps.ready_tag.outputs.tag_name }} files: ./dist/PluginLoader prerelease: false From cbbd56486070eab9a08253b2778fcd64877acd68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Jan 2023 17:54:50 -0800 Subject: [PATCH 10/15] Bump certifi from 2022.6.15 to 2022.12.7 (#345) Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.6.15 to 2022.12.7. - [Release notes](https://github.com/certifi/python-certifi/releases) - [Commits](https://github.com/certifi/python-certifi/compare/2022.06.15...2022.12.07) --- updated-dependencies: - dependency-name: certifi dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 520ec8e6..e7db9f1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ aiohttp==3.8.1 aiohttp-jinja2==1.5.0 aiohttp_cors==0.7.0 watchdog==2.1.7 -certifi==2022.6.15 \ No newline at end of file +certifi==2022.12.7 \ No newline at end of file From 3ebaac6752cb2b13ee5bfb6274cd7ae60b0d6bcb Mon Sep 17 00:00:00 2001 From: EMERALD Date: Thu, 19 Jan 2023 20:00:42 -0600 Subject: [PATCH 11/15] Store and plugin installation visual improvements (#343) * Redesign store, add comments for filtering * Improve installation/uninstallation modals * Fix store comment to be easier to fix * Add source code info to about page --- frontend/assets/plugin_store.png | Bin 0 -> 56506 bytes frontend/index.d.ts | 2 + frontend/package.json | 3 +- frontend/pnpm-lock.yaml | 44 ++- frontend/rollup.config.js | 2 + .../components/modals/PluginInstallModal.tsx | 17 +- frontend/src/components/store/PluginCard.tsx | 299 +++++++++--------- frontend/src/components/store/Store.tsx | 224 +++++++++++-- frontend/src/plugin-loader.tsx | 17 +- frontend/tsconfig.json | 2 +- 10 files changed, 408 insertions(+), 202 deletions(-) create mode 100644 frontend/assets/plugin_store.png create mode 100644 frontend/index.d.ts diff --git a/frontend/assets/plugin_store.png b/frontend/assets/plugin_store.png new file mode 100644 index 0000000000000000000000000000000000000000..17832cab86f96055902f94f3006ee1b19789af49 GIT binary patch literal 56506 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.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2_rollup@2.76.0 + mini-svg-data-uri: 1.4.4 + rollup: 2.76.0 + dev: true + /@rollup/plugin-inject/4.0.4_rollup@2.76.0: resolution: {integrity: sha512-4pbcU4J/nS+zuHk+c+OL3WtmEQhqxlZ9uqfjQMQDOHOPld7PsCd8k5LWs8h5wjwJN7MgnAn768F2sDxEP4eNFQ==} peerDependencies: @@ -422,6 +438,21 @@ packages: picomatch: 2.3.1 dev: true + /@rollup/pluginutils/5.0.2_rollup@2.76.0: + resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.0 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 2.76.0 + dev: true + /@types/debug/4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} dependencies: @@ -944,8 +975,8 @@ packages: dependencies: ms: 2.1.2 - /decky-frontend-lib/3.18.9: - resolution: {integrity: sha512-QNMHDDAHfL+JpvVVte4Vj8iyOqvz/2iyFEknbJ1/Kz7aPTygFUsJp5mq1FDVvVNjfCYfF3fYAaZVqZu3d7pCEA==} + /decky-frontend-lib/3.18.10: + resolution: {integrity: sha512-2mgbA3sSkuwQR/FnmhXVrcW6LyTS95IuL6muJAmQCruhBvXapDtjk1TcgxqMZxFZwGD1IPnemPYxHZll6IgnZw==} dev: false /decode-named-character-reference/1.0.2: @@ -1936,6 +1967,11 @@ packages: engines: {node: '>=6'} dev: true + /mini-svg-data-uri/1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + dev: true + /minimatch/3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js index fc924c36..46479295 100644 --- a/frontend/rollup.config.js +++ b/frontend/rollup.config.js @@ -1,4 +1,5 @@ import commonjs from '@rollup/plugin-commonjs'; +import image from '@rollup/plugin-image'; import json from '@rollup/plugin-json'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import replace from '@rollup/plugin-replace'; @@ -29,6 +30,7 @@ export default defineConfig({ preventAssignment: false, 'process.env.NODE_ENV': JSON.stringify('production'), }), + image(), ], preserveEntrySignatures: false, output: { diff --git a/frontend/src/components/modals/PluginInstallModal.tsx b/frontend/src/components/modals/PluginInstallModal.tsx index dfddc199..f2f13bbf 100644 --- a/frontend/src/components/modals/PluginInstallModal.tsx +++ b/frontend/src/components/modals/PluginInstallModal.tsx @@ -1,4 +1,4 @@ -import { ConfirmModal, Navigation, QuickAccessTab, Spinner, staticClasses } from 'decky-frontend-lib'; +import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib'; import { FC, useState } from 'react'; interface PluginInstallModalProps { @@ -26,15 +26,14 @@ const PluginInstallModal: FC = ({ artifact, version, ha onCancel={async () => { await onCancel(); }} + strTitle={`Install ${artifact}`} + strOKButtonText={loading ? 'Installing' : 'Install'} > -
- {hash == 'False' ?

!!!!NO HASH PROVIDED!!!!

: null} -
- {loading && } {loading ? 'Installing' : 'Install'} {artifact} - {version ? ' version ' + version : null} - {!loading && '?'} -
-
+ {hash == 'False' ? ( +

!!!!NO HASH PROVIDED!!!!

+ ) : ( + `Are you sure you want to install ${artifact} ${version}?` + )} ); }; diff --git a/frontend/src/components/store/PluginCard.tsx b/frontend/src/components/store/PluginCard.tsx index aa5fd1d6..828d3ae9 100644 --- a/frontend/src/components/store/PluginCard.tsx +++ b/frontend/src/components/store/PluginCard.tsx @@ -1,15 +1,12 @@ import { - DialogButton, + ButtonItem, Dropdown, Focusable, - Navigation, - QuickAccessTab, + PanelSectionRow, SingleDropdownOption, SuspensefulImage, - joinClassNames, - staticClasses, } from 'decky-frontend-lib'; -import { FC, useRef, useState } from 'react'; +import { FC, useState } from 'react'; import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store'; @@ -19,172 +16,162 @@ interface PluginCardProps { const PluginCard: FC = ({ plugin }) => { const [selectedOption, setSelectedOption] = useState(0); - const buttonRef = useRef(null); - const containerRef = useRef(null); + const root: boolean = plugin.tags.some((tag) => tag === 'root'); + return (
- {/* TODO: abstract this messy focus hackiness into a custom component in lib */} - { - buttonRef.current!.focus(); - }} - onCancel={(_: CustomEvent) => { - if (containerRef.current!.querySelectorAll('* :focus').length === 0) { - Navigation.NavigateBack(); - setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000); - } else { - containerRef.current!.focus(); - } - }} +
-
-
Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)} - > - {plugin.name} -
-
-
- -
-

- Author: {plugin.author} -

-

- {plugin.description} -

-

- Tags: - {plugin.tags.map((tag: string) => ( - - {tag == 'root' ? 'Requires root' : tag} - - ))} -

-
-
-
+
+
+ + {plugin.name} + + + {plugin.author} + + + {plugin.description ? ( + plugin.description + ) : ( + + No description provided. + + )} + + {root && ( + + This plugin has full access to your Steam Deck.{' '} + + deckbrew.xyz/root + + + )} +
- -
- requestPluginInstall(plugin.name, plugin.versions[selectedOption])} + + +
- Install - -
-
- ({ - data: index, - label: version.name, - })) as SingleDropdownOption[] - } - strDefaultLabel={'Select a version'} - selectedOption={selectedOption} - onChange={({ data }) => setSelectedOption(data)} - /> -
-
+ requestPluginInstall(plugin.name, plugin.versions[selectedOption])} + > + Install + +
+
+ ({ + data: index, + label: version.name, + })) as SingleDropdownOption[] + } + menuLabel="Plugin Version" + selectedOption={selectedOption} + onChange={({ data }) => setSelectedOption(data)} + /> +
+
+
- +
); }; diff --git a/frontend/src/components/store/Store.tsx b/frontend/src/components/store/Store.tsx index 2ffd8efb..7a9c0e33 100644 --- a/frontend/src/components/store/Store.tsx +++ b/frontend/src/components/store/Store.tsx @@ -1,6 +1,16 @@ -import { SteamSpinner } from 'decky-frontend-lib'; -import { FC, useEffect, useState } from 'react'; +import { + Dropdown, + DropdownOption, + Focusable, + PanelSectionRow, + SteamSpinner, + Tabs, + TextField, + findModule, +} from 'decky-frontend-lib'; +import { FC, useEffect, useMemo, useState } from 'react'; +import logo from '../../../assets/plugin_store.png'; import Logger from '../../logger'; import { StorePlugin, getPluginList } from '../../store'; import PluginCard from './PluginCard'; @@ -8,7 +18,12 @@ import PluginCard from './PluginCard'; const logger = new Logger('FilePicker'); const StorePage: FC<{}> = () => { + const [currentTabRoute, setCurrentTabRoute] = useState('browse'); const [data, setData] = useState(null); + const { TabCount } = findModule((m) => { + if (m?.TabCount && m?.TabTitle) return true; + return false; + }); useEffect(() => { (async () => { @@ -19,19 +34,12 @@ const StorePage: FC<{}> = () => { }, []); return ( -
+ <>
{!data ? ( @@ -39,13 +47,193 @@ const StorePage: FC<{}> = () => {
) : ( -
- {data.map((plugin: StorePlugin) => ( - - ))} -
+ { + setCurrentTabRoute(tabId); + }} + tabs={[ + { + title: 'Browse', + content: , + id: 'browse', + renderTabAddon: () => {data.length}, + }, + { + title: 'About', + content: , + id: 'about', + }, + ]} + /> )}
+ + ); +}; + +const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => { + const sortOptions = useMemo( + (): DropdownOption[] => [ + { data: 1, label: 'Alphabetical (A to Z)' }, + { data: 2, label: 'Alphabetical (Z to A)' }, + ], + [], + ); + + // const filterOptions = useMemo((): DropdownOption[] => [{ data: 1, label: 'All' }], []); + + const [selectedSort, setSort] = useState(sortOptions[0].data); + // const [selectedFilter, setFilter] = useState(filterOptions[0].data); + const [searchFieldValue, setSearchValue] = useState(''); + + return ( + <> + + {/* This should be used once filtering is added + + + +
+ Sort + setSort(e.data)} + /> +
+
+ Filter + setFilter(e.data)} + /> +
+
+
+
+ +
+ setSearchValue(e.target.value)} /> +
+
+
+ */} + + +
+ Sort + setSort(e.data)} + /> +
+
+
+
+ +
+ setSearchValue(e.target.value)} /> +
+
+
+
+ {data.children.data + .filter((plugin: StorePlugin) => { + return ( + plugin.name.toLowerCase().includes(searchFieldValue.toLowerCase()) || + plugin.description.toLowerCase().includes(searchFieldValue.toLowerCase()) || + plugin.author.toLowerCase().includes(searchFieldValue.toLowerCase()) || + plugin.tags.some((tag: string) => tag.toLowerCase().includes(searchFieldValue.toLowerCase())) + ); + }) + .sort((a, b) => { + if (selectedSort % 2 === 1) return a.name.localeCompare(b.name); + else return b.name.localeCompare(a.name); + }) + .map((plugin: StorePlugin) => ( + + ))} +
+ + ); +}; + +const AboutTab: FC<{}> = () => { + return ( +
+ + + Testing + + Please consider testing new plugins to help the Decky Loader team!{' '} + + deckbrew.xyz/testing + + + Contributing + + If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template + repository on GitHub. Information on development and distribution is available in the README. + + Source Code + All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.
); }; diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index c37e168c..bcd84b3f 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -1,13 +1,4 @@ -import { - ConfirmModal, - ModalRoot, - Patch, - QuickAccessTab, - Router, - showModal, - sleep, - staticClasses, -} from 'decky-frontend-lib'; +import { ConfirmModal, ModalRoot, Patch, QuickAccessTab, Router, showModal, sleep } from 'decky-frontend-lib'; import { FC, lazy } from 'react'; import { FaCog, FaExclamationCircle, FaPlug } from 'react-icons/fa'; @@ -155,10 +146,10 @@ class PluginLoader extends Logger { onCancel={() => { // do nothing }} + strTitle={`Uninstall ${name}`} + strOKButtonText={'Uninstall'} > -
- Uninstall {name}? -
+ Are you sure you want to uninstall {name}? , ); } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1e7159ee..6231d955 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -18,6 +18,6 @@ "allowSyntheticDefaultImports": true, "skipLibCheck": true }, - "include": ["src"], + "include": ["src", "index.d.ts"], "exclude": ["node_modules"] } From 6569f1b2684c7875e99f9a30802f5378e284e7eb Mon Sep 17 00:00:00 2001 From: Beebles <102569435+beebls@users.noreply.github.com> Date: Sun, 22 Jan 2023 15:33:26 -0700 Subject: [PATCH 12/15] Fix http_request not allowing bodys (#352) --- frontend/src/plugin-loader.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index bcd84b3f..7215b49a 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -335,6 +335,7 @@ class PluginLoader extends Logger { fetchNoCors(url: string, request: any = {}) { let args = { method: 'POST', headers: {} }; const req = { ...args, ...request, url, data: request.body }; + req?.body && delete req.body return this.callServerMethod('http_request', req); }, executeInTab(tab: string, runAsync: boolean, code: string) { From 2dce0646bd17df769672f7393fb9a9f2ba111bc0 Mon Sep 17 00:00:00 2001 From: TrainDoctor Date: Sun, 22 Jan 2023 16:29:27 -0800 Subject: [PATCH 13/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3e6d1c7..5ad9739b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Deckbrew logo + Deckbrew logo
Decky Loader
From c05e8f9ae0a38a58e73e98589133e9140eb8f46a Mon Sep 17 00:00:00 2001 From: TrainDoctor Date: Sun, 22 Jan 2023 16:33:06 -0800 Subject: [PATCH 14/15] Update build.yml --- .github/workflows/build.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d944416d..cb72821a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -130,9 +130,7 @@ jobs: OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT") printf "OUT: ${OUT}\n" else - printf "no type selected, defaulting to patch.\n" - OUT=$(semver bump patch "$OUT") - printf "OUT: ${OUT}\n" + printf "no type selected, not bumping for release.\n" fi elif [[ ! "$VERSION" =~ "-pre" ]]; then printf "previous tag is a release, bumping by selected type.\n" From c2b76d9099ac551f829f9cec821c23a9aff6b018 Mon Sep 17 00:00:00 2001 From: Philipp Richter Date: Mon, 23 Jan 2023 00:54:05 +0000 Subject: [PATCH 15/15] Expose useful env vars to plugin processes (#349) * recommended paths for storing data * improve helper functions --- backend/helpers.py | 107 ++++++++++++++++++++++++++------------------ backend/main.py | 14 ++---- backend/plugin.py | 25 +++++++++-- backend/settings.py | 10 ++--- backend/updater.py | 6 +-- 5 files changed, 96 insertions(+), 66 deletions(-) diff --git a/backend/helpers.py b/backend/helpers.py index b97352bd..7cab512b 100644 --- a/backend/helpers.py +++ b/backend/helpers.py @@ -5,6 +5,7 @@ import ssl import subprocess import uuid import os +import sys from subprocess import check_output from time import sleep from hashlib import sha256 @@ -19,8 +20,6 @@ REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service" # global vars csrf_token = str(uuid.uuid4()) ssl_ctx = ssl.create_default_context(cafile=certifi.where()) -user = None -group = None assets_regex = re.compile("^/plugins/.*/assets/.*") frontend_regex = re.compile("^/frontend/.*") @@ -37,65 +36,87 @@ async def csrf_middleware(request, handler): return await handler(request) return Response(text='Forbidden', status='403') -# Get the user by checking for the first logged in user. As this is run -# by systemd at startup the process is likely to start before the user -# logs in, so we will wait here until they are available. Note that -# other methods such as getenv wont work as there was no $SUDO_USER to -# start the systemd service. +# Deprecated def set_user(): - global user - cmd = "who | awk '{print $1}' | sort | head -1" - while user == None: - name = check_output(cmd, shell=True).decode().strip() - if name not in [None, '']: - user = name - sleep(0.1) + pass -# Get the global user. get_user must be called first. +# Get the user id hosting the plugin loader +def get_user_id() -> int: + proc_path = os.path.realpath(sys.argv[0]) + pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir)) + for pw in pws: + if proc_path.startswith(os.path.realpath(pw.pw_dir)): + return pw.pw_uid + raise PermissionError("The plugin loader does not seem to be hosted by any known user.") + +# Get the user hosting the plugin loader def get_user() -> str: - global user - if user == None: - raise ValueError("helpers.get_user method called before user variable was set. Run helpers.set_user first.") - return user + return pwd.getpwuid(get_user_id()).pw_name -#Get the user owner of the given file path. +# Get the effective user id of the running process +def get_effective_user_id() -> int: + return os.geteuid() + +# Get the effective user of the running process +def get_effective_user() -> str: + return pwd.getpwuid(get_effective_user_id()).pw_name + +# Get the effective user group id of the running process +def get_effective_user_group_id() -> int: + return os.getegid() + +# Get the effective user group of the running process +def get_effective_user_group() -> str: + return grp.getgrgid(get_effective_user_group_id()).gr_name + +# Get the user owner of the given file path. def get_user_owner(file_path) -> str: - return pwd.getpwuid(os.stat(file_path).st_uid)[0] + return pwd.getpwuid(os.stat(file_path).st_uid).pw_name -#Get the user group of the given file path. +# Get the user group of the given file path. def get_user_group(file_path) -> str: - return grp.getgrgid(os.stat(file_path).st_gid)[0] + return grp.getgrgid(os.stat(file_path).st_gid).gr_name -# Set the global user group. get_user must be called first +# Deprecated def set_user_group() -> str: - global group - global user - if user == None: - raise ValueError("helpers.set_user_dir method called before user variable was set. Run helpers.set_user first.") - if group == None: - group = check_output(["id", "-g", "-n", user]).decode().strip() + return get_user_group() -# Get the group of the global user. set_user_group must be called first. +# Get the group id of the user hosting the plugin loader +def get_user_group_id() -> int: + return pwd.getpwuid(get_user_id()).pw_gid + +# Get the group of the user hosting the plugin loader def get_user_group() -> str: - global group - if group == None: - raise ValueError("helpers.get_user_group method called before group variable was set. Run helpers.set_user_group first.") - return group + return grp.getgrgid(get_user_group_id()).gr_name # Get the default home path unless a user is specified def get_home_path(username = None) -> str: if username == None: - raise ValueError("Username not defined, no home path can be found.") - else: - return str("/home/"+username) + username = get_user() + return pwd.getpwnam(username).pw_dir -# Get the default homebrew path unless a user is specified +# Get the default homebrew path unless a home_path is specified def get_homebrew_path(home_path = None) -> str: if home_path == None: - raise ValueError("Home path not defined, homebrew dir cannot be determined.") - else: - return str(home_path+"/homebrew") - # return str(home_path+"/homebrew") + home_path = get_home_path() + return os.path.join(home_path, "homebrew") + +# Recursively create path and chown as user +def mkdir_as_user(path): + path = os.path.realpath(path) + os.makedirs(path, exist_ok=True) + chown_path = get_home_path() + parts = os.path.relpath(path, chown_path).split(os.sep) + uid = get_user_id() + gid = get_user_group_id() + for p in parts: + chown_path = os.path.join(chown_path, p) + os.chown(chown_path, uid, gid) + +# Fetches the version of loader +def get_loader_version() -> str: + with open(os.path.join(os.path.dirname(sys.argv[0]), ".loader.version"), "r", encoding="utf-8") as version_file: + return version_file.readline().replace("\n", "") # Download Remote Binaries to local Plugin async def download_remote_binary_to_path(url, binHash, path) -> bool: diff --git a/backend/main.py b/backend/main.py index c48ad752..a2ac008a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -19,8 +19,7 @@ from aiohttp_jinja2 import setup as jinja_setup # local modules from browser import PluginBrowser from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token, - get_home_path, get_homebrew_path, get_user, - get_user_group, set_user, set_user_group, + get_home_path, get_homebrew_path, get_user, get_user_group, stop_systemd_unit, start_systemd_unit) from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs from loader import Loader @@ -28,18 +27,11 @@ from settings import SettingsManager from updater import Updater from utilities import Utilities -# Ensure USER and GROUP vars are set first. -# TODO: This isn't the best way to do this but supports the current -# implementation. All the config load and environment setting eventually be -# moved into init or a config/loader method. -set_user() -set_user_group() USER = get_user() GROUP = get_user_group() -HOME_PATH = "/home/"+USER -HOMEBREW_PATH = HOME_PATH+"/homebrew" +HOMEBREW_PATH = get_homebrew_path() CONFIG = { - "plugin_path": getenv("PLUGIN_PATH", HOMEBREW_PATH+"/plugins"), + "plugin_path": getenv("PLUGIN_PATH", path.join(HOMEBREW_PATH, "plugins")), "chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1", "server_host": getenv("SERVER_HOST", "127.0.0.1"), "server_port": int(getenv("SERVER_PORT", "1337")), diff --git a/backend/plugin.py b/backend/plugin.py index e21d5bde..df0efe16 100644 --- a/backend/plugin.py +++ b/backend/plugin.py @@ -7,10 +7,12 @@ from importlib.util import module_from_spec, spec_from_file_location from json import dumps, load, loads from logging import getLogger from traceback import format_exc -from os import path, setgid, setuid +from os import path, setgid, setuid, environ from signal import SIGINT, signal from sys import exit from time import time +import helpers +from updater import Updater multiprocessing.set_start_method("fork") @@ -19,6 +21,7 @@ BUFFER_LIMIT = 2 ** 20 # 1 MiB class PluginWrapper: def __init__(self, file, plugin_directory, plugin_path) -> None: self.file = file + self.plugin_path = plugin_path self.plugin_directory = plugin_directory self.reader = None self.writer = None @@ -56,8 +59,24 @@ class PluginWrapper: set_event_loop(new_event_loop()) if self.passive: return - setgid(0 if "root" in self.flags else 1000) - setuid(0 if "root" in self.flags else 1000) + setgid(0 if "root" in self.flags else helpers.get_user_group_id()) + setuid(0 if "root" in self.flags else helpers.get_user_id()) + # export a bunch of environment variables to help plugin developers + environ["HOME"] = helpers.get_home_path("root" if "root" in self.flags else helpers.get_user()) + environ["USER"] = "root" if "root" in self.flags else helpers.get_user() + environ["DECKY_VERSION"] = helpers.get_loader_version() + environ["DECKY_USER"] = helpers.get_user() + environ["DECKY_HOME"] = helpers.get_homebrew_path() + environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory) + helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"]) + environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory) + helpers.mkdir_as_user(environ["DECKY_PLUGIN_RUNTIME_DIR"]) + environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory) + helpers.mkdir_as_user(environ["DECKY_PLUGIN_LOG_DIR"]) + environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory) + environ["DECKY_PLUGIN_NAME"] = self.name + environ["DECKY_PLUGIN_VERSION"] = self.version + environ["DECKY_PLUGIN_AUTHOR"] = self.author spec = spec_from_file_location("_", self.file) module = module_from_spec(spec) spec.loader.exec_module(module) diff --git a/backend/settings.py b/backend/settings.py index 6dedcbbe..64b04c60 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -2,14 +2,14 @@ from json import dump, load from os import mkdir, path, listdir, rename from shutil import chown -from helpers import get_home_path, get_homebrew_path, get_user, set_user, get_user_owner +from helpers import get_home_path, get_homebrew_path, get_user, get_user_group, get_user_owner class SettingsManager: def __init__(self, name, settings_directory = None) -> None: - set_user() USER = get_user() - wrong_dir = get_homebrew_path(get_home_path(USER)) + GROUP = get_user_group() + wrong_dir = get_homebrew_path() if settings_directory == None: settings_directory = path.join(wrong_dir, "settings") @@ -18,7 +18,7 @@ class SettingsManager: #Create the folder with the correct permission if not path.exists(settings_directory): mkdir(settings_directory) - chown(settings_directory, USER, USER) + chown(settings_directory, USER, GROUP) #Copy all old settings file in the root directory to the correct folder for file in listdir(wrong_dir): @@ -30,7 +30,7 @@ class SettingsManager: #If the owner of the settings directory is not the user, then set it as the user: if get_user_owner(settings_directory) != USER: - chown(settings_directory, USER, USER) + chown(settings_directory, USER, GROUP) self.settings = {} diff --git a/backend/updater.py b/backend/updater.py index 15a93e8a..14fd2070 100644 --- a/backend/updater.py +++ b/backend/updater.py @@ -31,9 +31,7 @@ class Updater: self.remoteVer = None self.allRemoteVers = None try: - logger.info(getcwd()) - with open(path.join(getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file: - self.localVer = version_file.readline().replace("\n", "") + self.localVer = helpers.get_loader_version() except: self.localVer = False @@ -161,7 +159,7 @@ class Updater: logger.error(f"Error at %s", exc_info=e) with open(path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8") as service_file: service_data = service_file.read() - service_data = service_data.replace("${HOMEBREW_FOLDER}", "/home/"+helpers.get_user()+"/homebrew") + service_data = service_data.replace("${HOMEBREW_FOLDER}", helpers.get_homebrew_path()) with open(path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8") as service_file: service_file.write(service_data)