diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index df8b03f6..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"
@@ -159,7 +157,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
diff --git a/README.md b/README.md
index 586b4f62..5ad9739b 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
Decky Loader
@@ -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
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/injector.py b/backend/injector.py
index c1f27b59..d77de13a 100644
--- a/backend/injector.py
+++ b/backend/injector.py
@@ -415,4 +415,4 @@ async def close_old_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)
+ await sleep(0.5)
\ No newline at end of file
diff --git a/backend/main.py b/backend/main.py
index 44311cf3..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")),
@@ -56,12 +48,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 +82,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:
- chown_plugin_dir()
self.loop.create_task(self.loader_reinjector())
self.loop.create_task(self.load_plugins())
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)
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()
diff --git a/frontend/assets/plugin_store.png b/frontend/assets/plugin_store.png
new file mode 100644
index 00000000..17832cab
Binary files /dev/null and b/frontend/assets/plugin_store.png differ
diff --git a/frontend/index.d.ts b/frontend/index.d.ts
new file mode 100644
index 00000000..3e1bef28
--- /dev/null
+++ b/frontend/index.d.ts
@@ -0,0 +1,2 @@
+declare module '*.png';
+declare module '*.jpg';
diff --git a/frontend/package.json b/frontend/package.json
index 434cb9d1..a51fca79 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,6 +12,7 @@
},
"devDependencies": {
"@rollup/plugin-commonjs": "^21.1.0",
+ "@rollup/plugin-image": "^3.0.1",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0",
@@ -41,7 +42,7 @@
}
},
"dependencies": {
- "decky-frontend-lib": "^3.18.6",
+ "decky-frontend-lib": "^3.18.10",
"i18next": "^22.0.6",
"i18next-fs-backend": "^2.0.0",
"react-file-icon": "^1.2.0",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 926345de..febcd933 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -2,6 +2,7 @@ lockfileVersion: 5.4
specifiers:
'@rollup/plugin-commonjs': ^21.1.0
+ '@rollup/plugin-image': ^3.0.1
'@rollup/plugin-json': ^4.1.0
'@rollup/plugin-node-resolve': ^13.3.0
'@rollup/plugin-replace': ^4.0.0
@@ -10,7 +11,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.6
+ decky-frontend-lib: ^3.18.10
husky: ^8.0.1
i18next: ^22.0.6
i18next-fs-backend: ^2.0.0
@@ -33,7 +34,7 @@ specifiers:
typescript: ^4.7.4
dependencies:
- decky-frontend-lib: 3.18.6
+ decky-frontend-lib: 3.18.10
i18next: 22.0.6
i18next-fs-backend: 2.0.0
react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
@@ -44,6 +45,7 @@ dependencies:
devDependencies:
'@rollup/plugin-commonjs': 21.1.0_rollup@2.76.0
+ '@rollup/plugin-image': 3.0.1_rollup@2.76.0
'@rollup/plugin-json': 4.1.0_rollup@2.76.0
'@rollup/plugin-node-resolve': 13.3.0_rollup@2.76.0
'@rollup/plugin-replace': 4.0.0_rollup@2.76.0
@@ -384,6 +386,20 @@ packages:
rollup: 2.76.0
dev: true
+ /@rollup/plugin-image/3.0.1_rollup@2.76.0:
+ resolution: {integrity: sha512-F50Sko4Xcc576x7HG9f3MvJKKnBfSmqfVFWJkJgyIEkI8YxZxux28lDbuy0+GsAK6BFl9Gn+TRXOUgHHJbFh3w==}
+ engines: {node: '>=14.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== }
@@ -474,6 +490,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== }
@@ -1082,9 +1113,8 @@ packages:
dependencies:
ms: 2.1.2
- /decky-frontend-lib/3.18.6:
- resolution:
- { integrity: sha512-kM+kH/EuYCW+zsdYnNZa39EsU1bTF/iYn03zbeaTSIVwe/oxqOoEG2mwVaQdhk98jqY1uWZgayBQYnnTksMXFw== }
+ /decky-frontend-lib/3.18.10:
+ resolution: {integrity: sha512-2mgbA3sSkuwQR/FnmhXVrcW6LyTS95IuL6muJAmQCruhBvXapDtjk1TcgxqMZxFZwGD1IPnemPYxHZll6IgnZw==}
dev: false
/decode-named-character-reference/1.0.2:
@@ -2234,9 +2264,14 @@ packages:
dev: true
/mimic-fn/2.1.0:
- resolution:
- { integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== }
- engines: { node: '>=6' }
+ resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
+ 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:
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 3dad0b38..fe1bfc89 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 { useTranslation } from 'react-i18next';
import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store';
@@ -22,168 +19,162 @@ const { t } = useTranslation('PluginCard');
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={t('select_version') as string}
- 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/index.tsx b/frontend/src/index.tsx
index 095b8d0f..4bd15e0f 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -1,5 +1,7 @@
import './i18n';
+import { Navigation, Router, sleep } from 'decky-frontend-lib';
+
import PluginLoader from './plugin-loader';
import { DeckyUpdater } from './updater';
@@ -16,6 +18,23 @@ 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,
+ };
+ (Router as unknown as any).deckyShim = true;
+ 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());
diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx
index 46694444..6350dcab 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 { useTranslation } from 'react-i18next';
import { FaCog, FaExclamationCircle, FaPlug } from 'react-icons/fa';
@@ -163,10 +154,10 @@ class PluginLoader extends Logger {
onCancel={() => {
// do nothing
}}
+ strTitle={`Uninstall ${name}`}
+ strOKButtonText={'Uninstall'}
>
-
- {t('plugin_uninstall', name)}
-
+ {t('plugin_uninstall', name)}
,
);
}
@@ -353,6 +344,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) {
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"]
}
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