Merge remote-tracking branch 'origin/main' into i18n

This commit is contained in:
Marco Rodolfi
2023-01-23 11:21:12 +01:00
21 changed files with 554 additions and 282 deletions
+2 -4
View File
@@ -130,9 +130,7 @@ jobs:
OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT") OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT")
printf "OUT: ${OUT}\n" printf "OUT: ${OUT}\n"
else else
printf "no type selected, defaulting to patch.\n" printf "no type selected, not bumping for release.\n"
OUT=$(semver bump patch "$OUT")
printf "OUT: ${OUT}\n"
fi fi
elif [[ ! "$VERSION" =~ "-pre" ]]; then elif [[ ! "$VERSION" =~ "-pre" ]]; then
printf "previous tag is a release, bumping by selected type.\n" printf "previous tag is a release, bumping by selected type.\n"
@@ -159,7 +157,7 @@ jobs:
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: ${{ github.event_name == 'workflow_dispatch' && !env.ACT }} if: ${{ github.event_name == 'workflow_dispatch' && !env.ACT }}
with: 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 }} tag_name: ${{ steps.ready_tag.outputs.tag_name }}
files: ./dist/PluginLoader files: ./dist/PluginLoader
prerelease: false prerelease: false
+3 -3
View File
@@ -1,5 +1,5 @@
<h1 align="center"> <h1 align="center">
<a name="logo" href="https://deckbrew.xyz/"><img src="https://deckbrew.xyz/logo.png" alt="Deckbrew logo" width="200"></a> <a name="logo" href="https://deckbrew.xyz/"><img src="https://deckbrew.xyz/static/icon-45ca1f5aea376a9ad37e92db906f283e.png" alt="Deckbrew logo" width="200"></a>
<br> <br>
Decky Loader Decky Loader
<br> <br>
@@ -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). 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. 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 ### 👋 Uninstallation
@@ -66,7 +66,7 @@ We are sorry to see you go! If you are considering uninstalling because you are
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu. 1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
1. Select "Switch to Desktop". 1. Select "Switch to Desktop".
1. Run the installer file again, and select `uninstall decky loader` 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 ## 🚀 Getting Started
+64 -43
View File
@@ -5,6 +5,7 @@ import ssl
import subprocess import subprocess
import uuid import uuid
import os import os
import sys
from subprocess import check_output from subprocess import check_output
from time import sleep from time import sleep
from hashlib import sha256 from hashlib import sha256
@@ -19,8 +20,6 @@ REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
# global vars # global vars
csrf_token = str(uuid.uuid4()) csrf_token = str(uuid.uuid4())
ssl_ctx = ssl.create_default_context(cafile=certifi.where()) ssl_ctx = ssl.create_default_context(cafile=certifi.where())
user = None
group = None
assets_regex = re.compile("^/plugins/.*/assets/.*") assets_regex = re.compile("^/plugins/.*/assets/.*")
frontend_regex = re.compile("^/frontend/.*") frontend_regex = re.compile("^/frontend/.*")
@@ -37,65 +36,87 @@ async def csrf_middleware(request, handler):
return await handler(request) return await handler(request)
return Response(text='Forbidden', status='403') return Response(text='Forbidden', status='403')
# Get the user by checking for the first logged in user. As this is run # Deprecated
# 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.
def set_user(): def set_user():
global user pass
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)
# 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: def get_user() -> str:
global user return pwd.getpwuid(get_user_id()).pw_name
if user == None:
raise ValueError("helpers.get_user method called before user variable was set. Run helpers.set_user first.")
return user
#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: 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: 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: def set_user_group() -> str:
global group return get_user_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()
# 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: def get_user_group() -> str:
global group return grp.getgrgid(get_user_group_id()).gr_name
if group == None:
raise ValueError("helpers.get_user_group method called before group variable was set. Run helpers.set_user_group first.")
return group
# Get the default home path unless a user is specified # Get the default home path unless a user is specified
def get_home_path(username = None) -> str: def get_home_path(username = None) -> str:
if username == None: if username == None:
raise ValueError("Username not defined, no home path can be found.") username = get_user()
else: return pwd.getpwnam(username).pw_dir
return str("/home/"+username)
# 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: def get_homebrew_path(home_path = None) -> str:
if home_path == None: if home_path == None:
raise ValueError("Home path not defined, homebrew dir cannot be determined.") home_path = get_home_path()
else: return os.path.join(home_path, "homebrew")
return str(home_path+"/homebrew")
# return str(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 # Download Remote Binaries to local Plugin
async def download_remote_binary_to_path(url, binHash, path) -> bool: async def download_remote_binary_to_path(url, binHash, path) -> bool:
+1 -1
View File
@@ -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"): 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")) logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
await t.close() await t.close()
await sleep(0.5) await sleep(0.5)
+7 -14
View File
@@ -19,8 +19,7 @@ from aiohttp_jinja2 import setup as jinja_setup
# local modules # local modules
from browser import PluginBrowser from browser import PluginBrowser
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token, from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
get_home_path, get_homebrew_path, get_user, get_home_path, get_homebrew_path, get_user, get_user_group,
get_user_group, set_user, set_user_group,
stop_systemd_unit, start_systemd_unit) stop_systemd_unit, start_systemd_unit)
from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs
from loader import Loader from loader import Loader
@@ -28,18 +27,11 @@ from settings import SettingsManager
from updater import Updater from updater import Updater
from utilities import Utilities 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() USER = get_user()
GROUP = get_user_group() GROUP = get_user_group()
HOME_PATH = "/home/"+USER HOMEBREW_PATH = get_homebrew_path()
HOMEBREW_PATH = HOME_PATH+"/homebrew"
CONFIG = { 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", "chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
"server_host": getenv("SERVER_HOST", "127.0.0.1"), "server_host": getenv("SERVER_HOST", "127.0.0.1"),
"server_port": int(getenv("SERVER_PORT", "1337")), "server_port": int(getenv("SERVER_PORT", "1337")),
@@ -56,12 +48,15 @@ basicConfig(
logger = getLogger("Main") logger = getLogger("Main")
async def chown_plugin_dir(): def chown_plugin_dir():
code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]]) code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]])
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]]) code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
if code_chown != 0 or code_chmod != 0: 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})") 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: class PluginManager:
def __init__(self, loop) -> None: def __init__(self, loop) -> None:
self.loop = loop self.loop = loop
@@ -87,8 +82,6 @@ class PluginManager:
self.loop.create_task(start_systemd_unit(REMOTE_DEBUGGER_UNIT)) self.loop.create_task(start_systemd_unit(REMOTE_DEBUGGER_UNIT))
else: else:
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT)) 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.loader_reinjector())
self.loop.create_task(self.load_plugins()) self.loop.create_task(self.load_plugins())
+22 -3
View File
@@ -7,10 +7,12 @@ from importlib.util import module_from_spec, spec_from_file_location
from json import dumps, load, loads from json import dumps, load, loads
from logging import getLogger from logging import getLogger
from traceback import format_exc from traceback import format_exc
from os import path, setgid, setuid from os import path, setgid, setuid, environ
from signal import SIGINT, signal from signal import SIGINT, signal
from sys import exit from sys import exit
from time import time from time import time
import helpers
from updater import Updater
multiprocessing.set_start_method("fork") multiprocessing.set_start_method("fork")
@@ -19,6 +21,7 @@ BUFFER_LIMIT = 2 ** 20 # 1 MiB
class PluginWrapper: class PluginWrapper:
def __init__(self, file, plugin_directory, plugin_path) -> None: def __init__(self, file, plugin_directory, plugin_path) -> None:
self.file = file self.file = file
self.plugin_path = plugin_path
self.plugin_directory = plugin_directory self.plugin_directory = plugin_directory
self.reader = None self.reader = None
self.writer = None self.writer = None
@@ -56,8 +59,24 @@ class PluginWrapper:
set_event_loop(new_event_loop()) set_event_loop(new_event_loop())
if self.passive: if self.passive:
return return
setgid(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 1000) 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) spec = spec_from_file_location("_", self.file)
module = module_from_spec(spec) module = module_from_spec(spec)
spec.loader.exec_module(module) spec.loader.exec_module(module)
+5 -5
View File
@@ -2,14 +2,14 @@ from json import dump, load
from os import mkdir, path, listdir, rename from os import mkdir, path, listdir, rename
from shutil import chown 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: class SettingsManager:
def __init__(self, name, settings_directory = None) -> None: def __init__(self, name, settings_directory = None) -> None:
set_user()
USER = get_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: if settings_directory == None:
settings_directory = path.join(wrong_dir, "settings") settings_directory = path.join(wrong_dir, "settings")
@@ -18,7 +18,7 @@ class SettingsManager:
#Create the folder with the correct permission #Create the folder with the correct permission
if not path.exists(settings_directory): if not path.exists(settings_directory):
mkdir(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 #Copy all old settings file in the root directory to the correct folder
for file in listdir(wrong_dir): 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 the owner of the settings directory is not the user, then set it as the user:
if get_user_owner(settings_directory) != USER: if get_user_owner(settings_directory) != USER:
chown(settings_directory, USER, USER) chown(settings_directory, USER, GROUP)
self.settings = {} self.settings = {}
+2 -4
View File
@@ -31,9 +31,7 @@ class Updater:
self.remoteVer = None self.remoteVer = None
self.allRemoteVers = None self.allRemoteVers = None
try: try:
logger.info(getcwd()) self.localVer = helpers.get_loader_version()
with open(path.join(getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
self.localVer = version_file.readline().replace("\n", "")
except: except:
self.localVer = False self.localVer = False
@@ -161,7 +159,7 @@ class Updater:
logger.error(f"Error at %s", exc_info=e) logger.error(f"Error at %s", exc_info=e)
with open(path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8") as service_file: with open(path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8") as service_file:
service_data = service_file.read() 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: with open(path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8") as service_file:
service_file.write(service_data) service_file.write(service_data)
+16 -2
View File
@@ -233,7 +233,7 @@ class Utilities:
self.rdt_proxy_server.close() self.rdt_proxy_server.close()
self.rdt_proxy_task.cancel() self.rdt_proxy_task.cancel()
async def enable_rdt(self): async def _enable_rdt(self):
# TODO un-hardcode port # TODO un-hardcode port
try: try:
self.stop_rdt_proxy() self.stop_rdt_proxy()
@@ -243,11 +243,22 @@ class Utilities:
self.logger.info("Connecting to React DevTools at " + ip) self.logger.info("Connecting to React DevTools at " + ip)
async with ClientSession() as web: async with ClientSession() as web:
res = await web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context()) 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: if res.status != 200:
self.logger.error("Failed to connect to React DevTools at " + ip) self.logger.error("Failed to connect to React DevTools at " + ip)
return False return False
self.start_rdt_proxy(ip, 8097) 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") self.logger.info("Connected to React DevTools, loading script")
tab = await get_gamepadui_tab() tab = await get_gamepadui_tab()
# RDT needs to load before React itself to work. # 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("Failed to connect to React DevTools")
self.logger.error(format_exc()) self.logger.error(format_exc())
async def enable_rdt(self):
self.context.loop.create_task(self._enable_rdt())
async def disable_rdt(self): async def disable_rdt(self):
self.logger.info("Disabling React DevTools") self.logger.info("Disabling React DevTools")
tab = await get_gamepadui_tab() tab = await get_gamepadui_tab()
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+2
View File
@@ -0,0 +1,2 @@
declare module '*.png';
declare module '*.jpg';
+2 -1
View File
@@ -12,6 +12,7 @@
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^21.1.0", "@rollup/plugin-commonjs": "^21.1.0",
"@rollup/plugin-image": "^3.0.1",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0", "@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0", "@rollup/plugin-replace": "^4.0.0",
@@ -41,7 +42,7 @@
} }
}, },
"dependencies": { "dependencies": {
"decky-frontend-lib": "^3.18.6", "decky-frontend-lib": "^3.18.10",
"i18next": "^22.0.6", "i18next": "^22.0.6",
"i18next-fs-backend": "^2.0.0", "i18next-fs-backend": "^2.0.0",
"react-file-icon": "^1.2.0", "react-file-icon": "^1.2.0",
+43 -8
View File
@@ -2,6 +2,7 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@rollup/plugin-commonjs': ^21.1.0 '@rollup/plugin-commonjs': ^21.1.0
'@rollup/plugin-image': ^3.0.1
'@rollup/plugin-json': ^4.1.0 '@rollup/plugin-json': ^4.1.0
'@rollup/plugin-node-resolve': ^13.3.0 '@rollup/plugin-node-resolve': ^13.3.0
'@rollup/plugin-replace': ^4.0.0 '@rollup/plugin-replace': ^4.0.0
@@ -10,7 +11,7 @@ specifiers:
'@types/react-file-icon': ^1.0.1 '@types/react-file-icon': ^1.0.1
'@types/react-router': 5.1.18 '@types/react-router': 5.1.18
'@types/webpack': ^5.28.0 '@types/webpack': ^5.28.0
decky-frontend-lib: ^3.18.6 decky-frontend-lib: ^3.18.10
husky: ^8.0.1 husky: ^8.0.1
i18next: ^22.0.6 i18next: ^22.0.6
i18next-fs-backend: ^2.0.0 i18next-fs-backend: ^2.0.0
@@ -33,7 +34,7 @@ specifiers:
typescript: ^4.7.4 typescript: ^4.7.4
dependencies: dependencies:
decky-frontend-lib: 3.18.6 decky-frontend-lib: 3.18.10
i18next: 22.0.6 i18next: 22.0.6
i18next-fs-backend: 2.0.0 i18next-fs-backend: 2.0.0
react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
@@ -44,6 +45,7 @@ dependencies:
devDependencies: devDependencies:
'@rollup/plugin-commonjs': 21.1.0_rollup@2.76.0 '@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-json': 4.1.0_rollup@2.76.0
'@rollup/plugin-node-resolve': 13.3.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 '@rollup/plugin-replace': 4.0.0_rollup@2.76.0
@@ -384,6 +386,20 @@ packages:
rollup: 2.76.0 rollup: 2.76.0
dev: true 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: /@rollup/plugin-inject/4.0.4_rollup@2.76.0:
resolution: resolution:
{ integrity: sha512-4pbcU4J/nS+zuHk+c+OL3WtmEQhqxlZ9uqfjQMQDOHOPld7PsCd8k5LWs8h5wjwJN7MgnAn768F2sDxEP4eNFQ== } { integrity: sha512-4pbcU4J/nS+zuHk+c+OL3WtmEQhqxlZ9uqfjQMQDOHOPld7PsCd8k5LWs8h5wjwJN7MgnAn768F2sDxEP4eNFQ== }
@@ -474,6 +490,21 @@ packages:
picomatch: 2.3.1 picomatch: 2.3.1
dev: true 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: /@types/debug/4.1.7:
resolution: resolution:
{ integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== } { integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== }
@@ -1082,9 +1113,8 @@ packages:
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
/decky-frontend-lib/3.18.6: /decky-frontend-lib/3.18.10:
resolution: resolution: {integrity: sha512-2mgbA3sSkuwQR/FnmhXVrcW6LyTS95IuL6muJAmQCruhBvXapDtjk1TcgxqMZxFZwGD1IPnemPYxHZll6IgnZw==}
{ integrity: sha512-kM+kH/EuYCW+zsdYnNZa39EsU1bTF/iYn03zbeaTSIVwe/oxqOoEG2mwVaQdhk98jqY1uWZgayBQYnnTksMXFw== }
dev: false dev: false
/decode-named-character-reference/1.0.2: /decode-named-character-reference/1.0.2:
@@ -2234,9 +2264,14 @@ packages:
dev: true dev: true
/mimic-fn/2.1.0: /mimic-fn/2.1.0:
resolution: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
{ integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== } engines: {node: '>=6'}
engines: { node: '>=6' } dev: true
/mini-svg-data-uri/1.4.4:
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
hasBin: true
dev: true dev: true
/minimatch/3.1.2: /minimatch/3.1.2:
+2
View File
@@ -1,4 +1,5 @@
import commonjs from '@rollup/plugin-commonjs'; import commonjs from '@rollup/plugin-commonjs';
import image from '@rollup/plugin-image';
import json from '@rollup/plugin-json'; import json from '@rollup/plugin-json';
import { nodeResolve } from '@rollup/plugin-node-resolve'; import { nodeResolve } from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
@@ -29,6 +30,7 @@ export default defineConfig({
preventAssignment: false, preventAssignment: false,
'process.env.NODE_ENV': JSON.stringify('production'), 'process.env.NODE_ENV': JSON.stringify('production'),
}), }),
image(),
], ],
preserveEntrySignatures: false, preserveEntrySignatures: false,
output: { output: {
@@ -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'; import { FC, useState } from 'react';
interface PluginInstallModalProps { interface PluginInstallModalProps {
@@ -26,15 +26,14 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
onCancel={async () => { onCancel={async () => {
await onCancel(); await onCancel();
}} }}
strTitle={`Install ${artifact}`}
strOKButtonText={loading ? 'Installing' : 'Install'}
> >
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}> {hash == 'False' ? (
{hash == 'False' ? <h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3> : null} <h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3>
<div style={{ flexDirection: 'row' }}> ) : (
{loading && <Spinner style={{ width: '20px' }} />} {loading ? 'Installing' : 'Install'} {artifact} `Are you sure you want to install ${artifact} ${version}?`
{version ? ' version ' + version : null} )}
{!loading && '?'}
</div>
</div>
</ConfirmModal> </ConfirmModal>
); );
}; };
+143 -152
View File
@@ -1,15 +1,12 @@
import { import {
DialogButton, ButtonItem,
Dropdown, Dropdown,
Focusable, Focusable,
Navigation, PanelSectionRow,
QuickAccessTab,
SingleDropdownOption, SingleDropdownOption,
SuspensefulImage, SuspensefulImage,
joinClassNames,
staticClasses,
} from 'decky-frontend-lib'; } from 'decky-frontend-lib';
import { FC, useRef, useState } from 'react'; import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store'; import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store';
@@ -22,168 +19,162 @@ const { t } = useTranslation('PluginCard');
const PluginCard: FC<PluginCardProps> = ({ plugin }) => { const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
const [selectedOption, setSelectedOption] = useState<number>(0); const [selectedOption, setSelectedOption] = useState<number>(0);
const buttonRef = useRef<HTMLDivElement>(null); const root: boolean = plugin.tags.some((tag) => tag === 'root');
const containerRef = useRef<HTMLDivElement>(null);
return ( return (
<div <div
className="deckyStoreCard"
style={{ style={{
padding: '30px', marginLeft: '20px',
paddingTop: '10px', marginRight: '20px',
paddingBottom: '10px', marginBottom: '20px',
display: 'flex',
alignItems: 'center',
}} }}
> >
{/* TODO: abstract this messy focus hackiness into a custom component in lib */} <div
<Focusable className="deckyStoreCardImageContainer"
className="deckyStoreCard"
ref={containerRef}
onActivate={(_: CustomEvent) => {
buttonRef.current!.focus();
}}
onCancel={(_: CustomEvent) => {
if (containerRef.current!.querySelectorAll('* :focus').length === 0) {
Navigation.NavigateBack();
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
} else {
containerRef.current!.focus();
}
}}
style={{ style={{
display: 'flex', width: '320px',
flexDirection: 'column', height: '200px',
background: '#ACB2C924', position: 'relative',
height: 'unset',
marginBottom: 'unset',
// boxShadow: var(--gpShadow-Medium);
scrollSnapAlign: 'start',
boxSizing: 'border-box',
}} }}
> >
<div className="deckyStoreCardHeader" style={{ display: 'flex', alignItems: 'center' }}> <SuspensefulImage
<div className="deckyStoreCardImage"
style={{ fontSize: '18pt', padding: '10px' }} suspenseHeight="200px"
className={joinClassNames(staticClasses.Text)} suspenseWidth="320px"
// onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)}
>
{plugin.name}
</div>
</div>
<div
style={{ style={{
display: 'flex', width: '320px',
flexDirection: 'row', height: '200px',
objectFit: 'cover',
}} }}
className="deckyStoreCardBody" src={plugin.image_url}
> />
<SuspensefulImage </div>
className="deckyStoreCardImage" <div
suspenseWidth="256px" className="deckyStoreCardInfo"
style={{ style={{
width: 'auto', width: 'calc(100% - 320px)', // The calc is here so that the info section doesn't expand into the image
height: '160px', display: 'flex',
}} flexDirection: 'column',
src={plugin.image_url} height: '100%',
/> marginLeft: '1em',
<div justifyContent: 'center',
style={{ }}
display: 'flex', >
flexDirection: 'column', <span
}} className="deckyStoreCardTitle"
className="deckyStoreCardInfo"
>
<p
className={joinClassNames(staticClasses.PanelSectionRow)}
style={{ marginTop: '0px', marginLeft: '16px' }}
>
<span style={{ paddingLeft: '0px' }}>Author: {plugin.author}</span>
</p>
<p
className={joinClassNames(staticClasses.PanelSectionRow)}
style={{
marginLeft: '16px',
marginTop: '0px',
marginBottom: '0px',
marginRight: '16px',
}}
>
<span style={{ paddingLeft: '0px' }}>{plugin.description}</span>
</p>
<p
className={joinClassNames('deckyStoreCardTagsContainer', staticClasses.PanelSectionRow)}
style={{
padding: '0 16px',
display: 'flex',
flexWrap: 'wrap',
gap: '5px 10px',
}}
>
<span style={{ padding: '5px 0' }}>Tags:</span>
{plugin.tags.map((tag: string) => (
<span
className="deckyStoreCardTag"
style={{
padding: '5px',
borderRadius: '5px',
background: tag == 'root' ? '#842029' : '#ACB2C947',
}}
>
{tag == 'root' ? 'Requires root' : tag}
</span>
))}
</p>
</div>
</div>
<div
className="deckyStoreCardActionsContainer"
style={{ style={{
fontSize: '1.25em',
fontWeight: 'bold',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '90%',
}}
>
{plugin.name}
</span>
<span
className="deckyStoreCardAuthor"
style={{
marginRight: 'auto',
fontSize: '1em',
}}
>
{plugin.author}
</span>
<span
className="deckyStoreCardDescription"
style={{
fontSize: '13px',
color: '#969696',
WebkitLineClamp: root ? '2' : '3',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
display: '-webkit-box',
}}
>
{plugin.description ? (
plugin.description
) : (
<span>
<i style={{ color: '#666' }}>No description provided.</i>
</span>
)}
</span>
{root && (
<span
className="deckyStoreCardDescription deckyStoreCardDescriptionRoot"
style={{
fontSize: '13px',
color: '#fee75c',
}}
>
<i>This plugin has full access to your Steam Deck.</i>{' '}
<a
className="deckyStoreCardDescriptionRootLink"
href="https://deckbrew.xyz/root"
target="_blank"
style={{
color: '#fee75c',
textDecoration: 'none',
}}
>
deckbrew.xyz/root
</a>
</span>
)}
<div
className="deckyStoreCardButtonRow"
style={{
marginTop: '1em',
width: '100%', width: '100%',
alignSelf: 'flex-end', overflow: 'hidden',
display: 'flex',
flexDirection: 'row',
}} }}
> >
<Focusable <PanelSectionRow>
className="deckyStoreCardActions" <Focusable style={{ display: 'flex', maxWidth: '100%' }}>
style={{ <div
display: 'flex', className="deckyStoreCardInstallContainer"
flexDirection: 'row', style={{
width: '100%', paddingTop: '0px',
}} paddingBottom: '0px',
> width: '40%',
<div }}
className="deckyStoreCardInstallButtonContainer"
style={{
flex: '1',
}}
>
<DialogButton
className="deckyStoreCardInstallButton"
ref={buttonRef}
onClick={() => requestPluginInstall(plugin.name, plugin.versions[selectedOption])}
> >
Install <ButtonItem
</DialogButton> bottomSeparator="none"
</div> layout="below"
<div onClick={() => requestPluginInstall(plugin.name, plugin.versions[selectedOption])}
className="deckyStoreCardVersionDropdownContainer" >
style={{ <span className="deckyStoreCardInstallText">Install</span>
flex: '0.2', </ButtonItem>
}} </div>
> <div
<Dropdown className="deckyStoreCardVersionContainer"
rgOptions={ style={{
plugin.versions.map((version: StorePluginVersion, index) => ({ marginLeft: '5%',
data: index, width: '30%',
label: version.name, }}
})) as SingleDropdownOption[] >
} <Dropdown
strDefaultLabel={t('select_version') as string} rgOptions={
selectedOption={selectedOption} plugin.versions.map((version: StorePluginVersion, index) => ({
onChange={({ data }) => setSelectedOption(data)} data: index,
/> label: version.name,
</div> })) as SingleDropdownOption[]
</Focusable> }
menuLabel="Plugin Version"
selectedOption={selectedOption}
onChange={({ data }) => setSelectedOption(data)}
/>
</div>
</Focusable>
</PanelSectionRow>
</div> </div>
</Focusable> </div>
</div> </div>
); );
}; };
+206 -18
View File
@@ -1,6 +1,16 @@
import { SteamSpinner } from 'decky-frontend-lib'; import {
import { FC, useEffect, useState } from 'react'; 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 Logger from '../../logger';
import { StorePlugin, getPluginList } from '../../store'; import { StorePlugin, getPluginList } from '../../store';
import PluginCard from './PluginCard'; import PluginCard from './PluginCard';
@@ -8,7 +18,12 @@ import PluginCard from './PluginCard';
const logger = new Logger('FilePicker'); const logger = new Logger('FilePicker');
const StorePage: FC<{}> = () => { const StorePage: FC<{}> = () => {
const [currentTabRoute, setCurrentTabRoute] = useState<string>('browse');
const [data, setData] = useState<StorePlugin[] | null>(null); const [data, setData] = useState<StorePlugin[] | null>(null);
const { TabCount } = findModule((m) => {
if (m?.TabCount && m?.TabTitle) return true;
return false;
});
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -19,19 +34,12 @@ const StorePage: FC<{}> = () => {
}, []); }, []);
return ( return (
<div <>
style={{
marginTop: '40px',
height: 'calc( 100% - 40px )',
overflowY: 'scroll',
}}
>
<div <div
style={{ style={{
display: 'flex', marginTop: '40px',
flexWrap: 'nowrap', height: 'calc( 100% - 40px )',
flexDirection: 'column', background: '#0005',
height: '100%',
}} }}
> >
{!data ? ( {!data ? (
@@ -39,13 +47,193 @@ const StorePage: FC<{}> = () => {
<SteamSpinner /> <SteamSpinner />
</div> </div>
) : ( ) : (
<div> <Tabs
{data.map((plugin: StorePlugin) => ( activeTab={currentTabRoute}
<PluginCard plugin={plugin} /> onShowTab={(tabId: string) => {
))} setCurrentTabRoute(tabId);
</div> }}
tabs={[
{
title: 'Browse',
content: <BrowseTab children={{ data: data }} />,
id: 'browse',
renderTabAddon: () => <span className={TabCount}>{data.length}</span>,
},
{
title: 'About',
content: <AboutTab />,
id: 'about',
},
]}
/>
)} )}
</div> </div>
</>
);
};
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<number>(sortOptions[0].data);
// const [selectedFilter, setFilter] = useState<number>(filterOptions[0].data);
const [searchFieldValue, setSearchValue] = useState<string>('');
return (
<>
<style>{`
.deckyStoreCardInstallContainer > .Panel {
padding: 0;
}
`}</style>
{/* This should be used once filtering is added
<PanelSectionRow>
<Focusable style={{ display: 'flex', maxWidth: '100%' }}>
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '47.5%',
}}
>
<span className="DialogLabel">Sort</span>
<Dropdown
menuLabel="Sort"
rgOptions={sortOptions}
strDefaultLabel="Last Updated (Newest)"
selectedOption={selectedSort}
onChange={(e) => setSort(e.data)}
/>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '47.5%',
marginLeft: 'auto',
}}
>
<span className="DialogLabel">Filter</span>
<Dropdown
menuLabel="Filter"
rgOptions={filterOptions}
strDefaultLabel="All"
selectedOption={selectedFilter}
onChange={(e) => setFilter(e.data)}
/>
</div>
</Focusable>
</PanelSectionRow>
<div style={{ justifyContent: 'center', display: 'flex' }}>
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
<div style={{ width: '100%' }}>
<TextField label="Search" value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
</div>
</Focusable>
</div>
*/}
<PanelSectionRow>
<Focusable style={{ display: 'flex', maxWidth: '100%' }}>
<div
style={{
display: 'flex',
flexDirection: 'column',
minWidth: '100%',
maxWidth: '100%',
}}
>
<span className="DialogLabel">Sort</span>
<Dropdown
menuLabel="Sort"
rgOptions={sortOptions}
strDefaultLabel="Last Updated (Newest)"
selectedOption={selectedSort}
onChange={(e) => setSort(e.data)}
/>
</div>
</Focusable>
</PanelSectionRow>
<div style={{ justifyContent: 'center', display: 'flex' }}>
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
<div style={{ width: '100%' }}>
<TextField label="Search" value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
</div>
</Focusable>
</div>
<div>
{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) => (
<PluginCard plugin={plugin} />
))}
</div>
</>
);
};
const AboutTab: FC<{}> = () => {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
}}
>
<style>{`
.deckyStoreAboutHeader {
font-size: 24px;
font-weight: 600;
margin-top: 20px;
}
`}</style>
<img
src={logo}
style={{
width: '256px',
height: 'auto',
alignSelf: 'center',
}}
/>
<span className="deckyStoreAboutHeader">Testing</span>
<span>
Please consider testing new plugins to help the Decky Loader team!{' '}
<a
href="https://deckbrew.xyz/testing"
target="_blank"
style={{
textDecoration: 'none',
}}
>
deckbrew.xyz/testing
</a>
</span>
<span className="deckyStoreAboutHeader">Contributing</span>
<span>
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.
</span>
<span className="deckyStoreAboutHeader">Source Code</span>
<span>All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.</span>
</div> </div>
); );
}; };
+19
View File
@@ -1,5 +1,7 @@
import './i18n'; import './i18n';
import { Navigation, Router, sleep } from 'decky-frontend-lib';
import PluginLoader from './plugin-loader'; import PluginLoader from './plugin-loader';
import { DeckyUpdater } from './updater'; 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 () => { (async () => {
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text()); window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
+5 -13
View File
@@ -1,13 +1,4 @@
import { import { ConfirmModal, ModalRoot, Patch, QuickAccessTab, Router, showModal, sleep } from 'decky-frontend-lib';
ConfirmModal,
ModalRoot,
Patch,
QuickAccessTab,
Router,
showModal,
sleep,
staticClasses,
} from 'decky-frontend-lib';
import { FC, lazy } from 'react'; import { FC, lazy } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaCog, FaExclamationCircle, FaPlug } from 'react-icons/fa'; import { FaCog, FaExclamationCircle, FaPlug } from 'react-icons/fa';
@@ -163,10 +154,10 @@ class PluginLoader extends Logger {
onCancel={() => { onCancel={() => {
// do nothing // do nothing
}} }}
strTitle={`Uninstall ${name}`}
strOKButtonText={'Uninstall'}
> >
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}> {t('plugin_uninstall', name)}
{t('plugin_uninstall', name)}
</div>
</ConfirmModal>, </ConfirmModal>,
); );
} }
@@ -353,6 +344,7 @@ class PluginLoader extends Logger {
fetchNoCors(url: string, request: any = {}) { fetchNoCors(url: string, request: any = {}) {
let args = { method: 'POST', headers: {} }; let args = { method: 'POST', headers: {} };
const req = { ...args, ...request, url, data: request.body }; const req = { ...args, ...request, url, data: request.body };
req?.body && delete req.body
return this.callServerMethod('http_request', req); return this.callServerMethod('http_request', req);
}, },
executeInTab(tab: string, runAsync: boolean, code: string) { executeInTab(tab: string, runAsync: boolean, code: string) {
+1 -1
View File
@@ -18,6 +18,6 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["src"], "include": ["src", "index.d.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }
+1 -1
View File
@@ -2,4 +2,4 @@ aiohttp==3.8.1
aiohttp-jinja2==1.5.0 aiohttp-jinja2==1.5.0
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
watchdog==2.1.7 watchdog==2.1.7
certifi==2022.6.15 certifi==2022.12.7