mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 20:25:04 +03:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc146b0349 | |||
| 0c56f1b689 | |||
| 86b5567d4e | |||
| 8f41eb93ef | |||
| 670ae7d8a7 | |||
| 01ec1663bc | |||
| b840d75cb8 | |||
| e22ba20d13 | |||
| bff410d98b | |||
| 7188db9877 | |||
| 02e8640ad4 | |||
| e63983dba9 | |||
| cbea1518ed | |||
| 414493eed2 | |||
| 078a9cf33d | |||
| 6e357ceecc | |||
| e4f7546f99 | |||
| 4e5468a353 | |||
| 0b73882012 | |||
| b15392b5f2 | |||
| e646168e31 | |||
| 5ec4a4645d | |||
| b47ad69557 |
@@ -41,7 +41,7 @@ jobs:
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
pnpm i --frozen-lockfile --dangerously-allow-all-builds
|
||||
|
||||
- name: Build JS Frontend 🛠️
|
||||
working-directory: ./frontend
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
pnpm i --frozen-lockfile --dangerously-allow-all-builds
|
||||
|
||||
- name: Build JS Frontend 🛠️
|
||||
working-directory: ./frontend
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
pnpm i --frozen-lockfile --dangerously-allow-all-builds
|
||||
|
||||
- name: Run prettier (TypeScript)
|
||||
working-directory: frontend
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
pnpm i --frozen-lockfile --dangerously-allow-all-builds
|
||||
|
||||
- name: Run pyright (Python)
|
||||
uses: jakebailey/pyright-action@v1
|
||||
|
||||
@@ -95,7 +95,7 @@ Please consult [the wiki page regarding development](https://wiki.deckbrew.xyz/e
|
||||
1. In your clone of the repository, run these commands.
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm i
|
||||
pnpm i # NOTE: you may need to approve esbuild's build script with pnpm approve-builds
|
||||
pnpm run build
|
||||
```
|
||||
1. If you are modifying the UI, these commands will need to be run before deploying the changes to your Steam Deck.
|
||||
|
||||
@@ -18,9 +18,10 @@ from enum import IntEnum
|
||||
from typing import Dict, List, TypedDict
|
||||
|
||||
# Local modules
|
||||
from .localplatform.localplatform import chown, chmod
|
||||
from .localplatform.localplatform import chown, chmod, get_chown_plugin_path
|
||||
from .loader import Loader, Plugins
|
||||
from .helpers import get_ssl_context, download_remote_binary_to_path
|
||||
from .enums import UserType
|
||||
from .settings import SettingsManager
|
||||
|
||||
logger = getLogger("Browser")
|
||||
@@ -60,13 +61,6 @@ class PluginBrowser:
|
||||
return False
|
||||
zip_file = ZipFile(zip)
|
||||
zip_file.extractall(self.plugin_path)
|
||||
plugin_folder = self.find_plugin_folder(name)
|
||||
assert plugin_folder is not None
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
|
||||
if not chown(plugin_dir) or not chmod(plugin_dir, 555):
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath: str):
|
||||
@@ -101,8 +95,6 @@ class PluginBrowser:
|
||||
rv = False
|
||||
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
|
||||
|
||||
chown(self.plugin_path)
|
||||
chmod(pluginBasePath, 555)
|
||||
else:
|
||||
rv = True
|
||||
logger.info(f"No Remote Binaries to Download")
|
||||
@@ -124,6 +116,25 @@ class PluginBrowser:
|
||||
return folder
|
||||
except:
|
||||
logger.debug(f"skipping {folder}")
|
||||
|
||||
def set_plugin_dir_permissions(self, plugin_dir: str) -> bool:
|
||||
plugin_json_path = path.join(plugin_dir, 'plugin.json')
|
||||
logger.debug(f"Checking plugin.json at {plugin_json_path}")
|
||||
|
||||
root_plugin = False
|
||||
|
||||
if access(plugin_json_path, R_OK):
|
||||
with open(plugin_json_path, "r", encoding="utf-8") as f:
|
||||
plugin_json = json.load(f)
|
||||
if "flags" in plugin_json and "root" in plugin_json["flags"]:
|
||||
root_plugin = True
|
||||
|
||||
logger.debug("root_plugin %d, dir %s", root_plugin, plugin_dir)
|
||||
if get_chown_plugin_path():
|
||||
return chown(plugin_dir, UserType.EFFECTIVE_USER if root_plugin else UserType.HOST_USER, True) and chown(plugin_dir, UserType.EFFECTIVE_USER, False) and chmod(plugin_dir, 755) and chown(plugin_json_path, UserType.EFFECTIVE_USER, False) and chmod(plugin_json_path, 755)
|
||||
else:
|
||||
logger.debug("chown disabled by environment")
|
||||
return True
|
||||
|
||||
async def uninstall_plugin(self, name: str):
|
||||
if self.loader.watcher:
|
||||
@@ -266,6 +277,7 @@ class PluginBrowser:
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 95, "Store.download_progress_info.download_remote")
|
||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||
chown_ret = self.set_plugin_dir_permissions(plugin_dir)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
if name in self.loader.plugins:
|
||||
@@ -278,6 +290,9 @@ class PluginBrowser:
|
||||
self.settings.setSetting("pluginOrder", current_plugin_order)
|
||||
logger.debug("Plugin %s was added to the pluginOrder setting", name)
|
||||
await self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
|
||||
elif not chown_ret:
|
||||
logger.error("Could not chown plugin")
|
||||
return
|
||||
else:
|
||||
logger.error("Could not download remote binaries")
|
||||
return
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from enum import IntEnum
|
||||
|
||||
class UserType(IntEnum):
|
||||
HOST_USER = 1
|
||||
EFFECTIVE_USER = 2
|
||||
ROOT = 3
|
||||
HOST_USER = 1 # usually deck
|
||||
EFFECTIVE_USER = 2 # usually root
|
||||
|
||||
class PluginLoadType(IntEnum):
|
||||
LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI
|
||||
|
||||
@@ -23,6 +23,7 @@ csrf_token = str(uuid.uuid4())
|
||||
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
||||
data_regex = re.compile("^/plugins/.*/data/.*")
|
||||
dist_regex = re.compile("^/plugins/.*/dist/.*")
|
||||
frontend_regex = re.compile("^/frontend/.*")
|
||||
logger = getLogger("Main")
|
||||
@@ -45,6 +46,7 @@ async def csrf_middleware(request: Request, handler: Handler):
|
||||
str(request.rel_url.path) == "/fetch" or \
|
||||
str(request.rel_url.path) == "/ws" or \
|
||||
assets_regex.match(str(request.rel_url)) or \
|
||||
data_regex.match(str(request.rel_url)) or \
|
||||
dist_regex.match(str(request.rel_url)) or \
|
||||
frontend_regex.match(str(request.rel_url)):
|
||||
|
||||
@@ -179,7 +181,8 @@ def get_user_group_id() -> int:
|
||||
|
||||
# Get the default home path unless a user is specified
|
||||
def get_home_path(username: str | None = None) -> str:
|
||||
return localplatform.get_home_path(UserType.ROOT if username == "root" else UserType.HOST_USER)
|
||||
# TODO hardcoded root is kinda a hack
|
||||
return localplatform.get_home_path(UserType.EFFECTIVE_USER if username == "root" else UserType.HOST_USER)
|
||||
|
||||
async def is_systemd_unit_active(unit_name: str) -> bool:
|
||||
return await localplatform.service_active(unit_name)
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any, Tuple, Dict, cast
|
||||
|
||||
from aiohttp import web
|
||||
from os.path import exists
|
||||
from decky_loader.helpers import get_homebrew_path
|
||||
from watchdog.events import RegexMatchingEventHandler, FileSystemEvent
|
||||
from watchdog.observers import Observer
|
||||
|
||||
@@ -91,6 +92,7 @@ class Loader:
|
||||
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
|
||||
web.get("/plugins/{plugin_name}/dist/{path:.*}", self.handle_plugin_dist),
|
||||
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
|
||||
web.get("/plugins/{plugin_name}/data/{path:.*}", self.handle_plugin_frontend_assets_from_data),
|
||||
])
|
||||
|
||||
server_instance.ws.add_route("loader/get_plugins", self.get_plugins)
|
||||
@@ -142,6 +144,13 @@ class Loader:
|
||||
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def handle_plugin_frontend_assets_from_data(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
home = get_homebrew_path()
|
||||
file = path.join(home, "data", plugin.plugin_directory, request.match_info["path"])
|
||||
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def handle_frontend_bundle(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
|
||||
@@ -216,4 +225,4 @@ class Loader:
|
||||
async def handle_plugin_backend_reload(self, plugin_name: str):
|
||||
plugin = self.plugins[plugin_name]
|
||||
|
||||
await self.reload_queue.put((plugin.file, plugin.plugin_directory))
|
||||
await self.reload_queue.put((plugin.file, plugin.plugin_directory))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Canal de mise à jour",
|
||||
"prerelease": "Avant-première",
|
||||
"prerelease": "Préliminaire",
|
||||
"stable": "Stable",
|
||||
"testing": "Test"
|
||||
}
|
||||
@@ -52,21 +52,29 @@
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Êtes-vous sûr de vouloir apporter les modifications suivantes ?",
|
||||
"description": {
|
||||
"downgrade": "Rétrograder {{name}} en {{version}}",
|
||||
"install": "Installer {{name}} {{version}}",
|
||||
"overwrite": "Écraser {{name}} avec {{version}}",
|
||||
"reinstall": "Réinstaller {{name}} {{version}}",
|
||||
"update": "Mettre à jour {{name}} à {{version}}"
|
||||
"update": "Mettre à jour {{name}} en {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Confirmer",
|
||||
"loading": "En cours"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_many": "Rétrograder {{count}} plugins",
|
||||
"downgrade_one": "Rétrograder 1 plugin",
|
||||
"downgrade_other": "Rétrograder {{count}} plugins",
|
||||
"install_many": "Installer {{count}} plugins",
|
||||
"install_one": "Installer 1 plugin",
|
||||
"install_other": "Installer {{count}} plugins",
|
||||
"mixed_many": "Modifier {{count}} plugins",
|
||||
"mixed_one": "Modifier {{count}} plugin",
|
||||
"mixed_other": "Modifier {{count}} plugins",
|
||||
"overwrite_many": "Écraser {{count}} plugins",
|
||||
"overwrite_one": "Écraser 1 plugin",
|
||||
"overwrite_other": "Écraser {{count}} plugins",
|
||||
"reinstall_many": "Réinstaller {{count}} plugins",
|
||||
"reinstall_one": "Réinstaller 1 plugin",
|
||||
"reinstall_other": "Réinstaller {{count}} plugins",
|
||||
@@ -76,12 +84,22 @@
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "Rétrograder",
|
||||
"plugin_full_access": "Ce plugin a un accès complet à votre Steam Deck.",
|
||||
"plugin_install": "Installer",
|
||||
"plugin_no_desc": "Aucune description fournie.",
|
||||
"plugin_overwrite": "Écraser",
|
||||
"plugin_reinstall": "Réinstaller",
|
||||
"plugin_update": "Mettre à jour",
|
||||
"plugin_version_label": "Version du plugin"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "Rétrograder",
|
||||
"button_processing": "Rétrogradation",
|
||||
"desc": "Êtes-vous sûr de vouloir rétrograder {{artifact}} vers la version {{version}} ?",
|
||||
"title": "Rétrograder {{artifact}}"
|
||||
},
|
||||
"install": {
|
||||
"button_idle": "Installer",
|
||||
"button_processing": "Installation en cours",
|
||||
@@ -89,6 +107,13 @@
|
||||
"title": "Installer {{artifact}}"
|
||||
},
|
||||
"no_hash": "Ce plugin n'a pas de somme de contrôle, vous l'installez à vos risques et périls.",
|
||||
"not_installed": "(non installé)",
|
||||
"overwrite": {
|
||||
"button_idle": "Écraser",
|
||||
"button_processing": "Écrasement",
|
||||
"desc": "Êtes-vous sûr de vouloir remplacer {{artifact}} par la version {{version}} ?",
|
||||
"title": "Écraser {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Réinstaller",
|
||||
"button_processing": "Réinstallation en cours",
|
||||
@@ -97,8 +122,8 @@
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Mettre à jour",
|
||||
"button_processing": "Mise à jour",
|
||||
"desc": "Êtes-vous sûr de vouloir mettre à jour {{artifact}} {{version}} ?",
|
||||
"button_processing": "Mise à jour en cours",
|
||||
"desc": "Êtes-vous sûr de vouloir mettre à jour {{artifact}} vers la version {{version}} ?",
|
||||
"title": "Mettre à jour {{artifact}}"
|
||||
}
|
||||
},
|
||||
@@ -124,7 +149,7 @@
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Mise à jour vers {{tag_name}} disponible !",
|
||||
"error": "Erreur",
|
||||
"plugin_error_uninstall": "Allez sur {{name}} dans le menu de Decky si vous voulez désinstaller ce plugin.",
|
||||
"plugin_error_uninstall": "Le chargement de {{name}} a provoqué une exception comme indiqué ci-dessus. Cela signifie généralement que le plugin nécessite une mise à jour pour la nouvelle version de SteamUI. Vérifiez si une mise à jour est présente ou évaluez sa suppression dans les paramètres de Decky, dans la section Plugins.",
|
||||
"plugin_load_error": {
|
||||
"message": "Erreur lors du chargement du plugin {{name}}",
|
||||
"toast": "Erreur lors du chargement de {{name}}"
|
||||
@@ -153,7 +178,7 @@
|
||||
"cef_console": {
|
||||
"button": "Ouvrir la console",
|
||||
"desc": "Ouvre la console CEF. Utile uniquement à des fins de débogage. Les éléments présentés ici sont potentiellement dangereux et ne doivent être utilisés que si vous êtes un développeur de plugins ou si vous êtes dirigé ici par un de ces développeurs.",
|
||||
"label": "CEF Console"
|
||||
"label": "Console CEF"
|
||||
},
|
||||
"header": "Autre",
|
||||
"react_devtools": {
|
||||
@@ -171,7 +196,7 @@
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Active le menu développeur interne de Valve.",
|
||||
"desc2": "Ne touchez à rien dans ce menu à moins que vous ne sachiez ce qu'il fait.",
|
||||
"desc2": "Ne touchez à rien dans ce menu à moins que vous ne sachiez ce que ça fait.",
|
||||
"label": "Activer Valve Internal"
|
||||
}
|
||||
},
|
||||
@@ -187,9 +212,9 @@
|
||||
"label": "Mode développeur"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Mise à jour Decky disponible",
|
||||
"decky_updates_label": "Mise à jour de Decky disponible",
|
||||
"header": "Notifications",
|
||||
"plugin_updates_label": "Mises à jour du plugin disponibles"
|
||||
"plugin_updates_label": "Mises à jour des plugins disponibles"
|
||||
},
|
||||
"other": {
|
||||
"header": "Autre"
|
||||
@@ -202,9 +227,19 @@
|
||||
"developer_title": "Développeur",
|
||||
"general_title": "Général",
|
||||
"plugins_title": "Plugins",
|
||||
"testing_title": "Essai"
|
||||
"testing_title": "Expérimentations"
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_remote": "Téléchargement des binaires externes",
|
||||
"download_zip": "Téléchargement du plugin",
|
||||
"increment_count": "Incrémentation du nombre de téléchargements",
|
||||
"installing_plugin": "Installation du plugin",
|
||||
"open_zip": "Ouverture du fichier zip",
|
||||
"parse_zip": "Analyse du fichier zip",
|
||||
"start": "Initialisation",
|
||||
"uninstalling_previous": "Désinstallation de la copie précédente"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Si vous souhaitez contribuer au Decky Plugin Store, consultez le dépôt SteamDeckHomebrew/decky-plugin-template sur GitHub. Des informations sur le développement et la distribution sont disponibles dans le fichier README.",
|
||||
"label": "Contributions"
|
||||
@@ -237,23 +272,27 @@
|
||||
"store_testing_cta": "Pensez à tester de nouveaux plugins pour aider l'équipe Decky Loader !",
|
||||
"store_testing_warning": {
|
||||
"desc": "Vous pouvez utiliser cette chaîne de magasin pour tester des versions de plugins. Assurez-vous de laisser des commentaires sur GitHub afin que le plugin puisse être mis à jour pour tous les utilisateurs.",
|
||||
"label": "Bienvenue sur la chaîne du magasin de tests"
|
||||
"label": "Bienvenue sur le canal test de la boutique"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Plugin Store personnalisé",
|
||||
"label": "Magasin personnalisé",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Personnalisé",
|
||||
"default": "Par défaut",
|
||||
"label": "Canal du Plugin Store",
|
||||
"label": "Canal magasin",
|
||||
"testing": "Test"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Télécharger"
|
||||
"download": "Télécharger",
|
||||
"error": "Erreur d'installation de la PR",
|
||||
"header": "Les versions suivantes de Decky Loader sont construites à partir de Pull Requests ouvertes par des tiers. L'équipe de Decky Loader n'a pas vérifié leur fonctionnalité ou leur sécurité, et elles peuvent être obsolètes.",
|
||||
"loading": "Chargement des Pull Requests ouvertes...",
|
||||
"start_download_toast": "Téléchargement de la PR #{{id}}"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Ouvrir le magasin Decky",
|
||||
@@ -264,7 +303,7 @@
|
||||
"no_patch_notes_desc": "pas de notes de mise à jour pour cette version",
|
||||
"patch_notes_desc": "Notes de mise à jour",
|
||||
"updates": {
|
||||
"check_button": "Chercher les mises à jour",
|
||||
"check_button": "Vérifier les mises à jour",
|
||||
"checking": "Recherche",
|
||||
"cur_version": "Version actuelle: {{ver}}",
|
||||
"install_button": "Installer la mise à jour",
|
||||
|
||||
@@ -52,7 +52,9 @@
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "以下の変更を加えてもよろしいですか?",
|
||||
"description": {
|
||||
"downgrade": "ダウングレード {{name}} {{version}}",
|
||||
"install": "インストール {{name}} {{version}}",
|
||||
"overwrite": "上書き {{name}} {{version}}",
|
||||
"reinstall": "再インストール {{name}} {{version}}",
|
||||
"update": "アップデート {{name}} {{version}}"
|
||||
},
|
||||
@@ -61,19 +63,31 @@
|
||||
"loading": "作業中"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_other": "{{count}} 個のプラグインをダウングレード",
|
||||
"install_other": "{{count}} 個のプラグインをインストール",
|
||||
"mixed_other": "{{count}} 個のプラグインを修正",
|
||||
"overwrite_other": "{{count}} 個のプラグインを上書き",
|
||||
"reinstall_other": "{{count}} 個のプラグインを再インストール",
|
||||
"update_other": "{{count}} 個のプラグインをアップデート"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "ダウングレード",
|
||||
"plugin_full_access": "このプラグインはSteam Deckの全てのアクセス権を持ちます。",
|
||||
"plugin_install": "インストール",
|
||||
"plugin_no_desc": "説明はありません。",
|
||||
"plugin_overwrite": "上書き",
|
||||
"plugin_reinstall": "再インストール",
|
||||
"plugin_update": "アップデート",
|
||||
"plugin_version_label": "プラグインバージョン"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "ダウングレード",
|
||||
"button_processing": "ダウングレード中",
|
||||
"desc": "{{artifact}}をVer {{version}} にダウングレードしてもよろしいですか?",
|
||||
"title": "{{artifact}}をダウングレード"
|
||||
},
|
||||
"install": {
|
||||
"button_idle": "インストール",
|
||||
"button_processing": "インストール中",
|
||||
@@ -81,6 +95,13 @@
|
||||
"title": "{{artifact}} をインストール"
|
||||
},
|
||||
"no_hash": "このプラグインにはハッシュがありません。ご自身の責任でインストールしてください。",
|
||||
"not_installed": "(インストールされていません)",
|
||||
"overwrite": {
|
||||
"button_idle": "上書き",
|
||||
"button_processing": "上書き中",
|
||||
"desc": "{{artifact}}をVer {{version}} に上書きしてもよろしいですか?",
|
||||
"title": "{{artifact}}を上書き"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "再インストール",
|
||||
"button_processing": "再インストール中",
|
||||
@@ -90,7 +111,7 @@
|
||||
"update": {
|
||||
"button_idle": "アップデート",
|
||||
"button_processing": "アップデート中",
|
||||
"desc": "{{artifact}} {{version}} をアップデートしてもよろしいですか?",
|
||||
"desc": "{{artifact}}をVer {{version}} にアップデートしてもよろしいですか?",
|
||||
"title": "{{artifact}} をアップデート"
|
||||
}
|
||||
},
|
||||
@@ -192,6 +213,7 @@
|
||||
},
|
||||
"Store": {
|
||||
"download_progress_info": {
|
||||
"download_remote": "外部バイナリのダウンロード",
|
||||
"download_zip": "プラグインのダウンロード中",
|
||||
"increment_count": "ダウンロード数の増加",
|
||||
"installing_plugin": "プラグインのインストール中",
|
||||
|
||||
@@ -52,7 +52,9 @@
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Вы уверены, что хотите внести следующие изменения?",
|
||||
"description": {
|
||||
"downgrade": "Откатить {{name}} до {{version}}",
|
||||
"install": "Установить {{name}} {{version}}",
|
||||
"overwrite": "Заменить {{name}} на {{version}}",
|
||||
"reinstall": "Переустановить {{name}} {{version}}",
|
||||
"update": "Обновить с {{name}} на {{version}}"
|
||||
},
|
||||
@@ -61,6 +63,9 @@
|
||||
"loading": "В процессе"
|
||||
},
|
||||
"title": {
|
||||
"downgrade_few": "Откатить {{count}} плагинов",
|
||||
"downgrade_many": "Откатить {{count}} плагинов",
|
||||
"downgrade_one": "Откатить 1 плагин",
|
||||
"install_few": "Установить {{count}} плагинов",
|
||||
"install_many": "Установить {{count}} плагинов",
|
||||
"install_one": "Установить {{count}} плагин",
|
||||
@@ -76,12 +81,21 @@
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_downgrade": "Откат",
|
||||
"plugin_full_access": "Этот плагин имеет полный доступ к вашему Steam Deck.",
|
||||
"plugin_install": "Установить",
|
||||
"plugin_no_desc": "Нет описания.",
|
||||
"plugin_overwrite": "Замена",
|
||||
"plugin_reinstall": "Переустановка",
|
||||
"plugin_update": "Обновление",
|
||||
"plugin_version_label": "Версия плагина"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"downgrade": {
|
||||
"button_idle": "Откат",
|
||||
"desc": "Вы уверенны, что хотите откатить {{artifact}} до версии {{version}}?",
|
||||
"title": "Откатить {{artifact}}"
|
||||
},
|
||||
"install": {
|
||||
"button_idle": "Установить",
|
||||
"button_processing": "Установка",
|
||||
@@ -98,7 +112,7 @@
|
||||
"update": {
|
||||
"button_idle": "Обновить",
|
||||
"button_processing": "Обновление",
|
||||
"desc": "Вы уверены, что хотите обновить {{artifact}} {{version}}?",
|
||||
"desc": "Вы уверены, что хотите обновить {{artifact}} до версии {{version}}?",
|
||||
"title": "Обновить {{artifact}}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ logger = logging.getLogger("localplatform")
|
||||
# subprocess._ENV
|
||||
ENV = Mapping[str, str]
|
||||
ProcessIO = int | IO[Any] | None
|
||||
async def run(args: list[str], stdin: ProcessIO = DEVNULL, stdout: ProcessIO = PIPE, stderr: ProcessIO = PIPE, env: ENV | None = None) -> tuple[Process, bytes | None, bytes | None]:
|
||||
async def run(args: list[str], stdin: ProcessIO = DEVNULL, stdout: ProcessIO = PIPE, stderr: ProcessIO = PIPE, env: ENV | None = {"LD_LIBRARY_PATH": ""}) -> tuple[Process, bytes | None, bytes | None]:
|
||||
proc = await create_subprocess_exec(args[0], *(args[1:]), stdin=stdin, stdout=stdout, stderr=stderr, env=env)
|
||||
proc_stdout, proc_stderr = await proc.communicate()
|
||||
return (proc, proc_stdout, proc_stderr)
|
||||
@@ -59,8 +59,6 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
|
||||
user_str = _get_user()+":"+_get_user_group()
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
user_str = _get_effective_user()+":"+_get_effective_user_group()
|
||||
elif user == UserType.ROOT:
|
||||
user_str = "root:root"
|
||||
else:
|
||||
raise Exception("Unknown User Type")
|
||||
|
||||
@@ -87,7 +85,7 @@ def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
|
||||
|
||||
return True
|
||||
|
||||
def folder_owner(path : str) -> UserType|None:
|
||||
def file_owner(path : str) -> UserType|None:
|
||||
user_owner = _get_user_owner(path)
|
||||
|
||||
if (user_owner == _get_user()):
|
||||
@@ -106,13 +104,14 @@ def get_home_path(user : UserType = UserType.HOST_USER) -> str:
|
||||
user_name = _get_user()
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
user_name = _get_effective_user()
|
||||
elif user == UserType.ROOT:
|
||||
pass
|
||||
else:
|
||||
raise Exception("Unknown User Type")
|
||||
|
||||
return pwd.getpwnam(user_name).pw_dir
|
||||
|
||||
def get_effective_username() -> str:
|
||||
return _get_effective_user()
|
||||
|
||||
def get_username() -> str:
|
||||
return _get_user()
|
||||
|
||||
@@ -121,8 +120,8 @@ def setgid(user : UserType = UserType.HOST_USER):
|
||||
|
||||
if user == UserType.HOST_USER:
|
||||
user_id = _get_user_group_id()
|
||||
elif user == UserType.ROOT:
|
||||
pass
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
pass # we already are
|
||||
else:
|
||||
raise Exception("Unknown user type")
|
||||
|
||||
@@ -133,8 +132,8 @@ def setuid(user : UserType = UserType.HOST_USER):
|
||||
|
||||
if user == UserType.HOST_USER:
|
||||
user_id = _get_user_id()
|
||||
elif user == UserType.ROOT:
|
||||
pass
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
pass # we already are
|
||||
else:
|
||||
raise Exception("Unknown user type")
|
||||
|
||||
@@ -146,6 +145,7 @@ async def service_active(service_name : str) -> bool:
|
||||
|
||||
async def service_restart(service_name : str, block : bool = True) -> bool:
|
||||
await run(["systemctl", "daemon-reload"])
|
||||
logger.info("Systemd reload done.")
|
||||
cmd = ["systemctl", "restart", service_name]
|
||||
|
||||
if not block:
|
||||
@@ -272,7 +272,7 @@ async def close_cef_socket():
|
||||
logger.info(f"Closing CEF socket with PID {pid} and FD {fd}")
|
||||
|
||||
# Use gdb to inject a close() call for the socket fd into steamwebhelper
|
||||
gdb_ret, _, _ = await run(["gdb", "--nx", "-p", pid, "--batch", "--eval-command", f"call (int)close({fd})"], env={"LD_LIBRARY_PATH": ""})
|
||||
gdb_ret, _, _ = await run(["gdb", "--nx", "-p", pid, "--batch", "--eval-command", f"call (int)close({fd})"])
|
||||
|
||||
if gdb_ret.returncode != 0:
|
||||
logger.error(f"Failed to close CEF socket with gdb! return code: {str(gdb_ret.returncode)}", exc_info=True)
|
||||
|
||||
@@ -7,7 +7,7 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
|
||||
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
def folder_owner(path : str) -> UserType|None:
|
||||
def file_owner(path : str) -> UserType|None:
|
||||
return UserType.HOST_USER # Stubbed
|
||||
|
||||
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
|
||||
@@ -34,6 +34,9 @@ async def service_restart(service_name : str, block : bool = True) -> bool:
|
||||
|
||||
return True # Stubbed
|
||||
|
||||
def get_effective_username() -> str:
|
||||
return os.getlogin()
|
||||
|
||||
def get_username() -> str:
|
||||
return os.getlogin()
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ def chown_plugin_dir():
|
||||
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
|
||||
mkdir_as_user(plugin_path)
|
||||
|
||||
if not chown(plugin_path, UserType.HOST_USER) or not chmod(plugin_path, 555):
|
||||
if not chown(plugin_path, UserType.EFFECTIVE_USER, False) or not chmod(plugin_path, 755, False):
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code")
|
||||
|
||||
if get_chown_plugin_path() == True:
|
||||
|
||||
@@ -8,7 +8,8 @@ from traceback import format_exc
|
||||
|
||||
from .sandboxed_plugin import SandboxedPlugin
|
||||
from .messages import MethodCallRequest, SocketMessageType
|
||||
from ..enums import PluginLoadType
|
||||
from ..enums import PluginLoadType, UserType
|
||||
from ..localplatform.localplatform import file_owner, chown, chmod, get_chown_plugin_path
|
||||
from ..localplatform.localsocket import LocalSocket
|
||||
from ..helpers import get_homebrew_path, mkdir_as_user
|
||||
|
||||
@@ -26,9 +27,12 @@ class PluginWrapper:
|
||||
|
||||
self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value
|
||||
|
||||
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
|
||||
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
|
||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
|
||||
plugin_dir_path = path.join(plugin_path, plugin_directory)
|
||||
plugin_json_path = path.join(plugin_dir_path, "plugin.json")
|
||||
|
||||
json = load(open(plugin_json_path, "r", encoding="utf-8"))
|
||||
if path.isfile(path.join(plugin_dir_path, "package.json")):
|
||||
package_json = load(open(path.join(plugin_dir_path, "package.json"), "r", encoding="utf-8"))
|
||||
self.version = package_json["version"]
|
||||
if ("type" in package_json and package_json["type"] == "module"):
|
||||
self.load_type = PluginLoadType.ESMODULE_V1.value
|
||||
@@ -42,6 +46,17 @@ class PluginWrapper:
|
||||
|
||||
self.log = getLogger("plugin")
|
||||
|
||||
if get_chown_plugin_path():
|
||||
# ensure plugin folder ownership
|
||||
if file_owner(plugin_dir_path) != UserType.EFFECTIVE_USER:
|
||||
chown(plugin_dir_path, UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER, True)
|
||||
chown(plugin_dir_path, UserType.EFFECTIVE_USER, False)
|
||||
chmod(plugin_dir_path, 755, True)
|
||||
# fix plugin.json permissions
|
||||
if file_owner(plugin_json_path) != UserType.EFFECTIVE_USER:
|
||||
chown(plugin_json_path, UserType.EFFECTIVE_USER, False)
|
||||
chmod(plugin_json_path, 755, False)
|
||||
|
||||
self.sandboxed_plugin = SandboxedPlugin(self.name, self.passive, self.flags, self.file, self.plugin_directory, self.plugin_path, self.version, self.author, self.api_version)
|
||||
self.proc: Process | None = None
|
||||
self._socket = LocalSocket()
|
||||
|
||||
@@ -13,7 +13,7 @@ from .messages import SocketResponseDict, SocketMessageType
|
||||
from ..localplatform.localsocket import LocalSocket
|
||||
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX
|
||||
from ..enums import UserType
|
||||
from .. import helpers, settings, injector # pyright: ignore [reportUnusedImport]
|
||||
from .. import helpers
|
||||
|
||||
from typing import List, TypeVar, Any
|
||||
|
||||
@@ -61,10 +61,10 @@ class SandboxedPlugin:
|
||||
if self.passive:
|
||||
return
|
||||
|
||||
setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
setuid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
setgid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
|
||||
setuid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
|
||||
# export a bunch of environment variables to help plugin developers
|
||||
environ["HOME"] = get_home_path(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
environ["HOME"] = get_home_path(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
|
||||
environ["USER"] = "root" if "root" in self.flags else get_username()
|
||||
environ["DECKY_VERSION"] = helpers.get_loader_version()
|
||||
environ["DECKY_USER"] = get_username()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from json import dump, load
|
||||
from os import mkdir, path, listdir, rename
|
||||
from typing import Any, Dict
|
||||
from .localplatform.localplatform import chown, folder_owner, get_chown_plugin_path
|
||||
from .localplatform.localplatform import chown, file_owner, get_chown_plugin_path
|
||||
from .enums import UserType
|
||||
|
||||
from .helpers import get_homebrew_path
|
||||
@@ -28,8 +28,8 @@ class SettingsManager:
|
||||
|
||||
|
||||
#If the owner of the settings directory is not the user, then set it as the user:
|
||||
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.ROOT
|
||||
if folder_owner(settings_directory) != expected_user:
|
||||
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.EFFECTIVE_USER
|
||||
if file_owner(settings_directory) != expected_user:
|
||||
chown(settings_directory, expected_user, False)
|
||||
|
||||
self.settings: Dict[str, Any] = {}
|
||||
|
||||
@@ -14,7 +14,6 @@ import zipfile
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from . import helpers
|
||||
from .injector import get_gamepadui_tab
|
||||
from .settings import SettingsManager
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
@@ -142,8 +141,6 @@ class Updater:
|
||||
async def download_decky_binary(self, download_url: str, version: str, is_zip: bool = False, size_in_bytes: int | None = None):
|
||||
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
|
||||
download_temp_filename = download_filename + ".new"
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.open_websocket()
|
||||
|
||||
if size_in_bytes == None:
|
||||
size_in_bytes = 26214400 # 25MiB, a reasonable overestimate (19.6MiB as of 2024/02/25)
|
||||
@@ -186,7 +183,6 @@ class Updater:
|
||||
|
||||
logger.info("Updated loader installation.")
|
||||
await self.context.ws.emit("updater/finish_download")
|
||||
await tab.close_websocket()
|
||||
await self.do_restart()
|
||||
|
||||
async def do_update(self):
|
||||
@@ -244,6 +240,7 @@ class Updater:
|
||||
await self.download_decky_binary(download_url, version, size_in_bytes=size_in_bytes)
|
||||
|
||||
async def do_restart(self):
|
||||
logger.info("Restarting loader for update.")
|
||||
await service_restart("plugin_loader", block=False)
|
||||
|
||||
async def do_shutdown(self):
|
||||
|
||||
@@ -7,7 +7,7 @@ from aiohttp.web import Application, WebSocketResponse, Request, Response, get
|
||||
|
||||
from enum import IntEnum
|
||||
|
||||
from typing import Callable, Coroutine, Dict, Any, cast, TypeVar
|
||||
from typing import Callable, Coroutine, Dict, Any, cast
|
||||
|
||||
from traceback import format_exc
|
||||
|
||||
@@ -29,8 +29,6 @@ class WSMessageExtra(WSMessage):
|
||||
|
||||
# see wsrouter.ts for typings
|
||||
|
||||
DataType = TypeVar("DataType")
|
||||
|
||||
Route = Callable[..., Coroutine[Any, Any, Any]]
|
||||
|
||||
class WSRouter:
|
||||
|
||||
Generated
+746
-727
File diff suppressed because it is too large
Load Diff
@@ -7,4 +7,4 @@ export default {
|
||||
tabWidth: 2,
|
||||
endOfLine: 'auto',
|
||||
plugins: [importSort],
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-replace": "^5.0.7",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/react": "19.1.1",
|
||||
"@types/react-dom": "19.1.1",
|
||||
"@types/react-file-icon": "^1.0.4",
|
||||
"@types/react-router": "5.1.20",
|
||||
"husky": "^9.0.11",
|
||||
@@ -30,8 +30,8 @@
|
||||
"inquirer": "^9.2.23",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-import-sort": "^0.0.7",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-external-globals": "^0.10.0",
|
||||
@@ -47,13 +47,13 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@decky/ui": "^4.8.3",
|
||||
"@decky/ui": "^4.11.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
"filesize": "^10.1.2",
|
||||
"i18next": "^23.11.5",
|
||||
"i18next": "^25.6.0",
|
||||
"i18next-http-backend": "^2.5.2",
|
||||
"react-file-icon": "^1.5.0",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-file-icon": "^1.6.0",
|
||||
"react-i18next": "^16.0.1",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0"
|
||||
|
||||
Generated
+92
-77
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@decky/ui':
|
||||
specifier: ^4.8.3
|
||||
version: 4.8.3
|
||||
specifier: ^4.11.0
|
||||
version: 4.11.0
|
||||
compare-versions:
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1
|
||||
@@ -18,23 +18,23 @@ importers:
|
||||
specifier: ^10.1.2
|
||||
version: 10.1.2
|
||||
i18next:
|
||||
specifier: ^23.11.5
|
||||
version: 23.11.5
|
||||
specifier: ^25.6.0
|
||||
version: 25.6.0(typescript@5.4.5)
|
||||
i18next-http-backend:
|
||||
specifier: ^2.5.2
|
||||
version: 2.5.2
|
||||
react-file-icon:
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
react-i18next:
|
||||
specifier: ^14.1.2
|
||||
version: 14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
specifier: ^16.0.1
|
||||
version: 16.0.1(i18next@25.6.0(typescript@5.4.5))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.4.5)
|
||||
react-icons:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1(react@18.3.1)
|
||||
version: 5.2.1(react@19.1.1)
|
||||
react-markdown:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1(@types/react@18.3.3)(react@18.3.1)
|
||||
version: 9.0.1(@types/react@19.1.1)(react@19.1.1)
|
||||
remark-gfm:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
@@ -61,11 +61,11 @@ importers:
|
||||
specifier: ^11.1.6
|
||||
version: 11.1.6(rollup@4.22.4)(tslib@2.6.3)(typescript@5.4.5)
|
||||
'@types/react':
|
||||
specifier: 18.3.3
|
||||
version: 18.3.3
|
||||
specifier: 19.1.1
|
||||
version: 19.1.1
|
||||
'@types/react-dom':
|
||||
specifier: 18.3.0
|
||||
version: 18.3.0
|
||||
specifier: 19.1.1
|
||||
version: 19.1.1(@types/react@19.1.1)
|
||||
'@types/react-file-icon':
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4
|
||||
@@ -91,11 +91,11 @@ importers:
|
||||
specifier: ^0.0.7
|
||||
version: 0.0.7(prettier@3.3.2)
|
||||
react:
|
||||
specifier: 18.3.1
|
||||
version: 18.3.1
|
||||
specifier: 19.1.1
|
||||
version: 19.1.1
|
||||
react-dom:
|
||||
specifier: 18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
specifier: 19.1.1
|
||||
version: 19.1.1(react@19.1.1)
|
||||
rollup:
|
||||
specifier: ^4.22.4
|
||||
version: 4.22.4
|
||||
@@ -203,6 +203,10 @@ packages:
|
||||
resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@7.28.4':
|
||||
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.24.7':
|
||||
resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -218,8 +222,8 @@ packages:
|
||||
'@decky/api@1.1.1':
|
||||
resolution: {integrity: sha512-R5fkBRHBt5QIQY7Q0AlbVIhlIZ/nTzwBOoi8Rt4Go2fjFnoMKPInCJl6cPjXzimGwl2pyqKJgY6VnH6ar0XrHQ==}
|
||||
|
||||
'@decky/ui@4.8.3':
|
||||
resolution: {integrity: sha512-Y1KciazgvKqMEVBGrWFCTGOqgVi5sHbcQNoCZRMbPpcI0U3j7udl6mkfe/NBa16oRDZ03ljS41SmrAgKAAt/pA==}
|
||||
'@decky/ui@4.11.0':
|
||||
resolution: {integrity: sha512-l9PstFC+S8FE8M2kIM78L8cYW4vzJ/ZD30II0huarHLcCsKM4Q+rbmEnbWjlJ1/KLmGXVRXBdAbyD4X/FzfxnQ==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.20.2':
|
||||
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
|
||||
@@ -598,11 +602,10 @@ packages:
|
||||
'@types/node@20.14.2':
|
||||
resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==}
|
||||
|
||||
'@types/prop-types@15.7.12':
|
||||
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
|
||||
|
||||
'@types/react-dom@18.3.0':
|
||||
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
|
||||
'@types/react-dom@19.1.1':
|
||||
resolution: {integrity: sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w==}
|
||||
peerDependencies:
|
||||
'@types/react': ^19.0.0
|
||||
|
||||
'@types/react-file-icon@1.0.4':
|
||||
resolution: {integrity: sha512-c1mIklUDaxm9odxf8RTiy/EAxsblZliJ86EKIOAyuafP9eK3iudyn4ATv53DX6ZvgGymc7IttVNm97LTGnTiYA==}
|
||||
@@ -610,8 +613,8 @@ packages:
|
||||
'@types/react-router@5.1.20':
|
||||
resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
|
||||
|
||||
'@types/react@18.3.3':
|
||||
resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
|
||||
'@types/react@19.1.1':
|
||||
resolution: {integrity: sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==}
|
||||
|
||||
'@types/resolve@1.20.2':
|
||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||
@@ -1187,8 +1190,16 @@ packages:
|
||||
engines: {node: '>=18.0.0 || >=20.0.0 || >=22.0.0', npm: '>=6', yarn: '>=1'}
|
||||
hasBin: true
|
||||
|
||||
i18next@23.11.5:
|
||||
resolution: {integrity: sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==}
|
||||
i18next@23.16.8:
|
||||
resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==}
|
||||
|
||||
i18next@25.6.0:
|
||||
resolution: {integrity: sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==}
|
||||
peerDependencies:
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
@@ -1709,29 +1720,32 @@ packages:
|
||||
quick-temp@0.1.8:
|
||||
resolution: {integrity: sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==}
|
||||
|
||||
react-dom@18.3.1:
|
||||
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
||||
react-dom@19.1.1:
|
||||
resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
react: ^19.1.1
|
||||
|
||||
react-file-icon@1.5.0:
|
||||
resolution: {integrity: sha512-6K2/nAI69CS838HOS+4S95MLXwf1neWywek1FgqcTFPTYjnM8XT7aBLz4gkjoqQKY9qPhu3A2tu+lvxhmZYY9w==}
|
||||
react-file-icon@1.6.0:
|
||||
resolution: {integrity: sha512-Ba4Qa2ya/kvhcCd4LJja77sV7JD7u1ZXcI1DUz+TII3nGmglG6QY+NZeHizThokgct3qI0glwb9eV8NqRGs5lw==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^17.0.0 || ^16.2.0
|
||||
react-dom: ^18.0.0 || ^17.0.0 || ^16.2.0
|
||||
react: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.2.0
|
||||
react-dom: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.2.0
|
||||
|
||||
react-i18next@14.1.2:
|
||||
resolution: {integrity: sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==}
|
||||
react-i18next@16.0.1:
|
||||
resolution: {integrity: sha512-0S//bpYEkCPjzuVmxDf9Z6+Y+ArNvpAUk7eDL4qNCZXjDh6Z9j6MZ+NThU7kMCOsmYmDCun3GYEwkiOjjZo9Ug==}
|
||||
peerDependencies:
|
||||
i18next: '>= 23.2.3'
|
||||
i18next: '>= 25.5.2'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
react-icons@5.2.1:
|
||||
resolution: {integrity: sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==}
|
||||
@@ -1747,8 +1761,8 @@ packages:
|
||||
'@types/react': '>=18'
|
||||
react: '>=18'
|
||||
|
||||
react@18.3.1:
|
||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||
react@19.1.1:
|
||||
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
@@ -1873,8 +1887,8 @@ packages:
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
scheduler@0.23.2:
|
||||
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
|
||||
scheduler@0.26.0:
|
||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
@@ -2266,6 +2280,8 @@ snapshots:
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
'@babel/runtime@7.28.4': {}
|
||||
|
||||
'@babel/template@7.24.7':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.24.7
|
||||
@@ -2295,7 +2311,7 @@ snapshots:
|
||||
|
||||
'@decky/api@1.1.1': {}
|
||||
|
||||
'@decky/ui@4.8.3': {}
|
||||
'@decky/ui@4.11.0': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.20.2':
|
||||
optional: true
|
||||
@@ -2567,24 +2583,21 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/prop-types@15.7.12': {}
|
||||
|
||||
'@types/react-dom@18.3.0':
|
||||
'@types/react-dom@19.1.1(@types/react@19.1.1)':
|
||||
dependencies:
|
||||
'@types/react': 18.3.3
|
||||
'@types/react': 19.1.1
|
||||
|
||||
'@types/react-file-icon@1.0.4':
|
||||
dependencies:
|
||||
'@types/react': 18.3.3
|
||||
'@types/react': 19.1.1
|
||||
|
||||
'@types/react-router@5.1.20':
|
||||
dependencies:
|
||||
'@types/history': 4.7.11
|
||||
'@types/react': 18.3.3
|
||||
'@types/react': 19.1.1
|
||||
|
||||
'@types/react@18.3.3':
|
||||
'@types/react@19.1.1':
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.12
|
||||
csstype: 3.1.3
|
||||
|
||||
'@types/resolve@1.20.2': {}
|
||||
@@ -3229,7 +3242,7 @@ snapshots:
|
||||
esbuild: 0.20.2
|
||||
fs-extra: 11.2.0
|
||||
gulp-sort: 2.0.0
|
||||
i18next: 23.11.5
|
||||
i18next: 23.16.8
|
||||
js-yaml: 4.1.0
|
||||
lilconfig: 3.1.2
|
||||
rsvp: 4.8.5
|
||||
@@ -3240,10 +3253,16 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
i18next@23.11.5:
|
||||
i18next@23.16.8:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.7
|
||||
|
||||
i18next@25.6.0(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
optionalDependencies:
|
||||
typescript: 5.4.5
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -3960,43 +3979,43 @@ snapshots:
|
||||
rimraf: 2.7.1
|
||||
underscore.string: 3.3.6
|
||||
|
||||
react-dom@18.3.1(react@18.3.1):
|
||||
react-dom@19.1.1(react@19.1.1):
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
react: 19.1.1
|
||||
scheduler: 0.26.0
|
||||
|
||||
react-file-icon@1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
react-file-icon@1.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
|
||||
dependencies:
|
||||
colord: 2.9.3
|
||||
prop-types: 15.8.1
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
|
||||
react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
react-i18next@16.0.1(i18next@25.6.0(typescript@5.4.5))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.7
|
||||
'@babel/runtime': 7.28.4
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 23.11.5
|
||||
react: 18.3.1
|
||||
i18next: 25.6.0(typescript@5.4.5)
|
||||
react: 19.1.1
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
typescript: 5.4.5
|
||||
|
||||
react-icons@5.2.1(react@18.3.1):
|
||||
react-icons@5.2.1(react@19.1.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react: 19.1.1
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-markdown@9.0.1(@types/react@18.3.3)(react@18.3.1):
|
||||
react-markdown@9.0.1(@types/react@19.1.1)(react@19.1.1):
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/react': 18.3.3
|
||||
'@types/react': 19.1.1
|
||||
devlop: 1.1.0
|
||||
hast-util-to-jsx-runtime: 2.3.0
|
||||
html-url-attributes: 3.0.0
|
||||
mdast-util-to-hast: 13.2.0
|
||||
react: 18.3.1
|
||||
react: 19.1.1
|
||||
remark-parse: 11.0.0
|
||||
remark-rehype: 11.1.0
|
||||
unified: 11.0.4
|
||||
@@ -4005,9 +4024,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
react@18.3.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
react@19.1.1: {}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
@@ -4164,9 +4181,7 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
scheduler@0.23.2:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
scheduler@0.26.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export default defineConfig([
|
||||
}),
|
||||
externalGlobals({
|
||||
react: 'SP_REACT',
|
||||
'react/jsx-runtime': 'SP_JSX',
|
||||
'react-dom': 'SP_REACTDOM',
|
||||
// hack to shut up react-markdown
|
||||
process: '{cwd: () => {}}',
|
||||
|
||||
@@ -4,6 +4,8 @@ import { FunctionComponent, useEffect, useReducer, useState } from 'react';
|
||||
import { uninstallPlugin } from '../plugin';
|
||||
import { VerInfo, doRestart, doShutdown } from '../updater';
|
||||
import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
|
||||
import { useSetting } from '../utils/hooks/useSetting';
|
||||
import { UpdateBranch } from './settings/pages/general/BranchSelect';
|
||||
|
||||
interface DeckyErrorBoundaryProps {
|
||||
error: ValveReactErrorInfo;
|
||||
@@ -37,6 +39,27 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
|
||||
if (!shouldReportToValve) DeckyPluginLoader.errorBoundaryHook.temporarilyDisableReporting();
|
||||
DeckyPluginLoader.updateVersion().then(setVersionInfo);
|
||||
}, []);
|
||||
|
||||
const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Stable);
|
||||
const [isChecking, setIsChecking] = useState<boolean>(false);
|
||||
const [updateProgress, setUpdateProgress] = useState<number>(-1);
|
||||
const [versionToUpdateTo, setSetVersionToUpdateTo] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const a = DeckyBackend.addEventListener('updater/update_download_percentage', (percentage) => {
|
||||
setUpdateProgress(percentage);
|
||||
});
|
||||
|
||||
const b = DeckyBackend.addEventListener('updater/finish_download', () => {
|
||||
setUpdateProgress(-2);
|
||||
});
|
||||
|
||||
return () => {
|
||||
DeckyBackend.removeEventListener('updater/update_download_percentage', a);
|
||||
DeckyBackend.removeEventListener('updater/finish_download', b);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
@@ -149,6 +172,65 @@ const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error,
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||
{updateProgress > -1
|
||||
? 'Update in progress... ' + updateProgress + '%'
|
||||
: updateProgress == -2
|
||||
? 'Update complete. Restarting...'
|
||||
: 'Changing your Decky Loader branch and/or \n checking for updates might help!\n'}
|
||||
{updateProgress == -1 && (
|
||||
<div style={{ height: '30px' }}>
|
||||
<select
|
||||
style={{ height: '100%' }}
|
||||
onChange={async (e) => {
|
||||
const branch = parseInt(e.target.value);
|
||||
setSelectedBranch(branch);
|
||||
setSetVersionToUpdateTo('');
|
||||
}}
|
||||
>
|
||||
<option value="0" selected={selectedBranch == UpdateBranch.Stable}>
|
||||
Stable
|
||||
</option>
|
||||
<option value="1" selected={selectedBranch == UpdateBranch.Prerelease}>
|
||||
Pre-Release
|
||||
</option>
|
||||
<option value="2" selected={selectedBranch == UpdateBranch.Testing}>
|
||||
Testing
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
style={{ height: '100%' }}
|
||||
disabled={updateProgress != -1 || isChecking}
|
||||
onClick={async () => {
|
||||
if (versionToUpdateTo == '') {
|
||||
setIsChecking(true);
|
||||
const versionInfo = (await DeckyBackend.callable(
|
||||
'updater/check_for_updates',
|
||||
)()) as unknown as VerInfo;
|
||||
setIsChecking(false);
|
||||
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
|
||||
setSetVersionToUpdateTo(versionInfo.remote.tag_name);
|
||||
} else {
|
||||
setSetVersionToUpdateTo('');
|
||||
}
|
||||
} else {
|
||||
DeckyBackend.callable('updater/do_update')();
|
||||
setUpdateProgress(0);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{isChecking
|
||||
? 'Checking for updates...'
|
||||
: versionToUpdateTo != ''
|
||||
? 'Update to ' + versionToUpdateTo
|
||||
: 'Check for updates'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
{wasCausedByPlugin && (
|
||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||
{'\n'}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Focusable, Navigation } from '@decky/ui';
|
||||
import { Focusable, Navigation, findClass, findClassByName } from '@decky/ui';
|
||||
import { FunctionComponent, useRef } from 'react';
|
||||
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@@ -8,6 +8,9 @@ interface MarkdownProps extends ReactMarkdownOptions {
|
||||
}
|
||||
|
||||
const Markdown: FunctionComponent<MarkdownProps> = (props) => {
|
||||
const eventDetailsBodyClassName = findClassByName('EventDetailsBody') || undefined;
|
||||
const eventLinkClassName = findClass('43088', 'Link');
|
||||
|
||||
return (
|
||||
<Focusable>
|
||||
<ReactMarkdown
|
||||
@@ -25,8 +28,10 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
|
||||
Navigation.NavigateToExternalWeb(aRef.current!.href);
|
||||
}}
|
||||
style={{ display: 'inline' }}
|
||||
focusClassName="steam-focus"
|
||||
className={eventDetailsBodyClassName}
|
||||
>
|
||||
<a ref={aRef} {...nodeProps.node.properties}>
|
||||
<a ref={aRef} {...nodeProps.node.properties} className={eventLinkClassName}>
|
||||
{nodeProps.children}
|
||||
</a>
|
||||
</Focusable>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FC, ReactNode, createContext, useContext, useState } from 'react';
|
||||
import { FC, PropsWithChildren, createContext, useContext, useState } from 'react';
|
||||
|
||||
const QuickAccessVisibleState = createContext<boolean>(false);
|
||||
|
||||
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
|
||||
|
||||
export const QuickAccessVisibleStateProvider: FC<{ tab: any; children: ReactNode }> = ({ children, tab }) => {
|
||||
export const QuickAccessVisibleStateProvider: FC<PropsWithChildren<{ tab: any }>> = ({ children, tab }) => {
|
||||
const initial = tab.initialVisibility;
|
||||
const [visible, setVisible] = useState<boolean>(initial);
|
||||
// HACK but i can't think of a better way to do this
|
||||
|
||||
@@ -10,7 +10,7 @@ interface WithSuspenseProps {
|
||||
const WithSuspense: FunctionComponent<WithSuspenseProps> = (props) => {
|
||||
const propsCopy = { ...props };
|
||||
delete propsCopy.children;
|
||||
(props.children as ReactElement)?.props && Object.assign((props.children as ReactElement).props, propsCopy); // There is probably a better way to do this but valve does it this way so ¯\_(ツ)_/¯
|
||||
(props.children as ReactElement<any>)?.props && Object.assign((props.children as ReactElement<any>).props, propsCopy); // There is probably a better way to do this but valve does it this way so ¯\_(ツ)_/¯
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FC, JSX, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconContext } from 'react-icons';
|
||||
import { FaExclamationTriangle, FaQuestionCircle, FaUserSlash } from 'react-icons/fa';
|
||||
|
||||
@@ -47,7 +47,7 @@ export default async function libraryPatch() {
|
||||
}
|
||||
|
||||
const unlisten = History.listen(() => {
|
||||
if (window.SteamClient.Apps.PromptToChangeShortcut !== patch.patchedFunction) {
|
||||
if ((window.SteamClient.Apps as any).PromptToChangeShortcut !== patch.patchedFunction) {
|
||||
rePatch();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,7 +72,16 @@ export default function DeveloperSettings() {
|
||||
}
|
||||
icon={<FaLink style={{ display: 'block' }} />}
|
||||
>
|
||||
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
||||
<DialogButton
|
||||
disabled={pluginURL.length == 0}
|
||||
onClick={() => {
|
||||
if (/^https?:\/\//.test(pluginURL)) {
|
||||
installFromURL(pluginURL);
|
||||
} else {
|
||||
installFromURL('https://' + pluginURL);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('SettingsDeveloperIndex.third_party_plugins.button_install')}
|
||||
</DialogButton>
|
||||
</Field>
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
import {
|
||||
Carousel,
|
||||
DialogButton,
|
||||
Field,
|
||||
FocusRing,
|
||||
Focusable,
|
||||
ProgressBarWithInfo,
|
||||
Spinner,
|
||||
findSP,
|
||||
showModal,
|
||||
} from '@decky/ui';
|
||||
import { Carousel, DialogButton, Field, Focusable, ProgressBarWithInfo, Spinner, findSP, showModal } from '@decky/ui';
|
||||
import { Suspense, lazy, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaExclamation } from 'react-icons/fa';
|
||||
@@ -23,9 +13,31 @@ const MarkdownRenderer = lazy(() => import('../../../Markdown'));
|
||||
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
|
||||
const SP = findSP();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Focusable onCancelButton={closeModal}>
|
||||
<FocusRing>
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
.steam-focus {
|
||||
outline-offset: 3px;
|
||||
outline: 2px solid rgba(255, 255, 255, 0.6);
|
||||
animation: pulseOutline 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulseOutline {
|
||||
0% {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
50% {
|
||||
outline: 2px solid rgba(255, 255, 255, 1);
|
||||
}
|
||||
100% {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}`}
|
||||
</style>
|
||||
|
||||
<Focusable onCancelButton={closeModal}>
|
||||
<Carousel
|
||||
fnItemRenderer={(id: number) => (
|
||||
<Focusable
|
||||
@@ -35,7 +47,9 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
|
||||
overflowY: 'scroll',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
margin: '40px',
|
||||
margin: '30px',
|
||||
padding: '0 15px',
|
||||
backgroundColor: 'rgba(37, 40, 46, 0.5)',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
@@ -57,11 +71,11 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
|
||||
nItemMarginX={0}
|
||||
initialColumn={0}
|
||||
autoFocus={true}
|
||||
fnGetColumnWidth={() => SP.innerWidth}
|
||||
fnGetColumnWidth={() => SP.innerWidth - SP.innerWidth * (10 / 100)}
|
||||
name={t('Updater.decky_updates') as string}
|
||||
/>
|
||||
</FocusRing>
|
||||
</Focusable>
|
||||
</Focusable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+13
-5
@@ -1,8 +1,4 @@
|
||||
// Sets up DFL, then loads start.ts which starts up the loader
|
||||
interface Window {
|
||||
// Shut up TS
|
||||
SP_REACTDOM: any;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.debug('[Decky:Boot] Frontend init');
|
||||
@@ -21,7 +17,19 @@ interface Window {
|
||||
// deliberate partial import
|
||||
const DFLWebpack = await import('@decky/ui/dist/webpack');
|
||||
window.SP_REACT = DFLWebpack.findModule((m) => m.Component && m.PureComponent && m.useLayoutEffect);
|
||||
window.SP_REACTDOM = DFLWebpack.findModule((m) => m.createPortal && m.createRoot);
|
||||
window.SP_REACTDOM =
|
||||
DFLWebpack.findModule((m) => m.createPortal && m.createRoot) ||
|
||||
DFLWebpack.findModule((m) => m.createPortal && m.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE);
|
||||
|
||||
console.debug('[Decky:Boot] Setting up JSX internals...');
|
||||
const jsx = DFLWebpack.findModule((m) => m.jsx && Object.keys(m).length == 1)?.jsx;
|
||||
if (jsx) {
|
||||
window.SP_JSX = {
|
||||
jsx,
|
||||
jsxs: jsx,
|
||||
Fragment: window.SP_REACT.Fragment,
|
||||
};
|
||||
}
|
||||
}
|
||||
console.debug('[Decky:Boot] Setting up @decky/ui...');
|
||||
window.DFL = await import('@decky/ui');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ToastNotification } from '@decky/api';
|
||||
import {
|
||||
EUIMode,
|
||||
ModalRoot,
|
||||
Navigation,
|
||||
PanelSection,
|
||||
@@ -30,7 +31,7 @@ import { HiddenPluginsService } from './hidden-plugins-service';
|
||||
import Logger from './logger';
|
||||
import { NotificationService } from './notification-service';
|
||||
import { InstallType, Plugin, PluginLoadType } from './plugin';
|
||||
import RouterHook, { UIMode } from './router-hook';
|
||||
import RouterHook from './router-hook';
|
||||
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
|
||||
import { checkForPluginUpdates } from './store';
|
||||
import TabsHook from './tabs-hook';
|
||||
@@ -119,28 +120,6 @@ class PluginLoader extends Logger {
|
||||
<DeckyStateContextProvider deckyState={this.deckyState}>
|
||||
<FaPlug />
|
||||
<TabBadge />
|
||||
<style>
|
||||
{`
|
||||
/* fixes random overscrolling in QAM */
|
||||
.${quickAccessMenuClasses?.TabContentColumn} {
|
||||
flex-grow: 1 !important;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
.${quickAccessMenuClasses?.Tab} {
|
||||
flex-grow: 1 !important;
|
||||
height: unset !important;
|
||||
--decky-qam-tab-max-height: 64px; /* make things a little easier for themers */
|
||||
max-height: var(--decky-qam-tab-max-height) !important;
|
||||
}
|
||||
/* they broke the footer a while ago and forgot to update the styles LOL */
|
||||
.${quickAccessMenuClasses?.Tabs}.${quickAccessMenuClasses.TabsWithFooter} {
|
||||
margin-bottom: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</DeckyStateContextProvider>
|
||||
),
|
||||
});
|
||||
@@ -205,12 +184,12 @@ class PluginLoader extends Logger {
|
||||
let registration: any;
|
||||
const uiMode = await new Promise(
|
||||
(r) =>
|
||||
(registration = SteamClient.UI.RegisterForUIModeChanged((mode: UIMode) => {
|
||||
(registration = SteamClient.UI.RegisterForUIModeChanged((mode: EUIMode) => {
|
||||
r(mode);
|
||||
registration.unregister();
|
||||
})),
|
||||
);
|
||||
if (uiMode == UIMode.BigPicture) {
|
||||
if (uiMode == EUIMode.GamePad) {
|
||||
// wait for SP window to exist before loading plugins
|
||||
while (!findSP()) {
|
||||
await sleep(100);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JSX } from 'react';
|
||||
export enum PluginLoadType {
|
||||
LEGACY_EVAL_IIFE = 0, // legacy, uses legacy serverAPI
|
||||
ESMODULE_V1 = 1, // esmodule loading with modern @decky/backend apis
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
EUIMode,
|
||||
ErrorBoundary,
|
||||
Patch,
|
||||
afterPatch,
|
||||
@@ -8,7 +9,7 @@ import {
|
||||
getReactRoot,
|
||||
sleep,
|
||||
} from '@decky/ui';
|
||||
import { FC, ReactElement, ReactNode, cloneElement, createElement } from 'react';
|
||||
import { FC, JSX, ReactElement, ReactNode, cloneElement, createElement } from 'react';
|
||||
import type { Route } from 'react-router';
|
||||
|
||||
import {
|
||||
@@ -31,17 +32,12 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export enum UIMode {
|
||||
BigPicture = 4,
|
||||
Desktop = 7,
|
||||
}
|
||||
|
||||
const isPatched = Symbol('is patched');
|
||||
|
||||
class RouterHook extends Logger {
|
||||
private routerState: DeckyRouterState = new DeckyRouterState();
|
||||
private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
|
||||
private renderedComponents: ReactElement[] = [];
|
||||
private renderedComponents: ReactElement<any>[] = [];
|
||||
private Route: any;
|
||||
private DeckyGamepadRouterWrapper = this.gamepadRouterWrapper.bind(this);
|
||||
private DeckyDesktopRouterWrapper = this.desktopRouterWrapper.bind(this);
|
||||
@@ -76,13 +72,13 @@ class RouterHook extends Logger {
|
||||
this.error('Failed to find router stack module');
|
||||
}
|
||||
|
||||
this.modeChangeRegistration = SteamClient.UI.RegisterForUIModeChanged((mode: UIMode) => {
|
||||
this.modeChangeRegistration = SteamClient.UI.RegisterForUIModeChanged((mode: EUIMode) => {
|
||||
this.debug(`UI mode changed to ${mode}`);
|
||||
if (this.patchedModes.has(mode)) return;
|
||||
this.patchedModes.add(mode);
|
||||
this.debug(`Patching router for UI mode ${mode}`);
|
||||
switch (mode) {
|
||||
case UIMode.BigPicture:
|
||||
case EUIMode.GamePad:
|
||||
this.debug('Patching gamepad router');
|
||||
this.patchGamepadRouter();
|
||||
break;
|
||||
@@ -237,7 +233,7 @@ class RouterHook extends Logger {
|
||||
return <>{this.renderedComponents}</>;
|
||||
}
|
||||
|
||||
private gamepadRouterWrapper({ children }: { children: ReactElement }) {
|
||||
private gamepadRouterWrapper({ children }: { children: ReactElement<any> }) {
|
||||
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
||||
|
||||
const { routes, routePatches } = useDeckyRouterState();
|
||||
@@ -255,7 +251,7 @@ class RouterHook extends Logger {
|
||||
return children;
|
||||
}
|
||||
|
||||
private desktopRouterWrapper({ children }: { children: ReactElement }) {
|
||||
private desktopRouterWrapper({ children }: { children: ReactElement<any> }) {
|
||||
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
||||
this.debug('desktop router wrapper render', children);
|
||||
const { routes, routePatches } = useDeckyRouterState();
|
||||
@@ -291,7 +287,7 @@ class RouterHook extends Logger {
|
||||
if (routes) {
|
||||
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
|
||||
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
|
||||
const newRouterArray: (ReactElement | JSX.Element)[] = [];
|
||||
const newRouterArray: (ReactElement<any> | JSX.Element)[] = [];
|
||||
routes.forEach(({ component, props }, path) => {
|
||||
newRouterArray.push(
|
||||
<Route path={path} {...props}>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"target": "ES2021",
|
||||
"jsx": "react",
|
||||
"jsxFactory": "window.SP_REACT.createElement",
|
||||
"jsxFragmentFactory": "window.SP_REACT.Fragment",
|
||||
"jsx": "react-jsx",
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"noUnusedLocals": true,
|
||||
@@ -15,7 +13,8 @@
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src", "index.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
Regular → Executable
+29
-5
@@ -2,6 +2,13 @@
|
||||
# Usage: deckdebug.sh DECKIP:8081
|
||||
# Dependencies: websocat jq curl chromium
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Error: Missing or incorrect argument." >&2
|
||||
echo "Usage: deckdebug.sh DECKIP:8081" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# https://jackson.dev/post/a-portable-nix-shell-shebang/
|
||||
if [ -z "$INSIDE_NIX_RANDOMSTRING" ] && command -v nix &> /dev/null; then
|
||||
# If the user has nix, relaunch in nix shell with dependencies added
|
||||
@@ -13,7 +20,20 @@ if [ -z "$INSIDE_NIX_RANDOMSTRING" ] && command -v nix &> /dev/null; then
|
||||
exit $?
|
||||
fi
|
||||
|
||||
chromium --remote-debugging-port=9222 &
|
||||
[[ -f "$HOME/.config/deckdebug/config.sh" ]] && source "$HOME/.config/deckdebug/config.sh"
|
||||
CHROMIUM="${CHROMIUM:-chromium}"
|
||||
|
||||
required_dependencies=(websocat jq curl $CHROMIUM)
|
||||
|
||||
# Check if the dependencies are installed
|
||||
for cmd in "${required_dependencies[@]}"; do
|
||||
if ! command -v "$cmd" &> /dev/null; then
|
||||
echo "Error: '$cmd' is not installed. Please install it and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
$CHROMIUM --remote-debugging-port=9222 &
|
||||
sleep 2
|
||||
|
||||
ADDR=$1
|
||||
@@ -32,13 +52,17 @@ while :; do
|
||||
TARGET=$NEWTARGET
|
||||
TARGETURL="http://$ADDR/devtools/inspector.html?ws=$ADDR/devtools/page/$TARGET"
|
||||
|
||||
LOCALTARGET=$(echo '{"id": 1, "method": "Target.createTarget", "params": {"background": true, "url": "'$TARGETURL'"}}
|
||||
echo '{"id": 1, "method": "Target.createTarget", "params": {"background": true, "url": "'$TARGETURL'"}}
|
||||
{"id": 2, "method": "Target.closeTarget", "params": {"targetId": "'$LOCALTARGET'"}}' \
|
||||
| websocat ws://$LOCAL/devtools/page/$LOCALTARGET \
|
||||
| jq -r '.result.targetId')
|
||||
| websocat -t ws://$LOCAL/devtools/page/$LOCALTARGET
|
||||
|
||||
sleep 2
|
||||
|
||||
LOCALTARGETS=$(curl -s http://$LOCAL/json/list)
|
||||
LOCALTARGET=$(jq -r '.[] | select(.title | startswith("DevTools")) | .id' <<< "$LOCALTARGETS")
|
||||
|
||||
echo started devtools at $LOCALTARGET
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
done
|
||||
done
|
||||
|
||||
Regular → Executable
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# ./script/task.sh: Run a VSCode task from tasks.json including its dependencies.
|
||||
#
|
||||
# Usage: ./scripts/task.sh TASK_LABEL
|
||||
#
|
||||
# This script looks for .vscode/tasks.json in your workspace folder (or current directory)
|
||||
# and executes the command associated with the given task label.
|
||||
#
|
||||
# It also handles the "dependsOn" field recursively.
|
||||
#
|
||||
# Requirements: jq sed
|
||||
|
||||
# https://jackson.dev/post/a-portable-nix-shell-shebang/
|
||||
if [ -z "$INSIDE_NIX_RANDOMSTRING" ] && command -v nix &> /dev/null; then
|
||||
# If the user has nix, relaunch in nix shell with dependencies added
|
||||
INSIDE_NIX_RANDOMSTRING=1 nix shell \
|
||||
nixpkgs#jq \
|
||||
nixpkgs#gnused \
|
||||
--command "$0" "$@"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
required_dependencies=(jq sed)
|
||||
|
||||
# Check if the dependencies are installed
|
||||
for cmd in "${required_dependencies[@]}"; do
|
||||
if ! command -v "$cmd" &> /dev/null; then
|
||||
echo "Error: '$cmd' is not installed. Please install it and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Use WORKSPACE_FOLDER if set; otherwise, assume current directory.
|
||||
WORKSPACE_FOLDER="${WORKSPACE_FOLDER:-$(pwd)}"
|
||||
TASKS_FILE="$WORKSPACE_FOLDER/.vscode/tasks.json"
|
||||
|
||||
if [ ! -f "$TASKS_FILE" ]; then
|
||||
echo "Error: tasks.json not found at $TASKS_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 TASK_LABEL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove comment lines (lines starting with //) from the tasks file to be compliant with the JSON format.
|
||||
TASKS_JSON=$(sed '/^[[:space:]]*\/\//d' "$TASKS_FILE")
|
||||
|
||||
TASK_LABEL="$1"
|
||||
shift
|
||||
|
||||
# run_task recursively looks up the task by label,
|
||||
# runs any dependencies first, then executes its command.
|
||||
run_task() {
|
||||
local label="$1"
|
||||
echo "Looking up task: $label"
|
||||
|
||||
# Get the task object from the cleaned JSON.
|
||||
local task
|
||||
task=$(echo "$TASKS_JSON" | jq --arg label "$label" -r '.tasks[] | select(.label == $label)')
|
||||
if [ -z "$task" ]; then
|
||||
echo "Error: Task with label '$label' not found in $TASKS_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If the task has dependencies, run them first.
|
||||
local depends
|
||||
depends=$(echo "$task" | jq -r '.dependsOn? // empty')
|
||||
if [ -n "$depends" ] && [ "$depends" != "null" ]; then
|
||||
# "dependsOn" can be an array or a string.
|
||||
if echo "$depends" | jq -e 'if type=="array" then . else empty end' >/dev/null; then
|
||||
for dep in $(echo "$depends" | jq -r '.[]'); do
|
||||
run_task "$dep"
|
||||
done
|
||||
else
|
||||
run_task "$depends"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if the task has either a command or script.
|
||||
local has_command has_script
|
||||
has_command=$(echo "$task" | jq -r 'has("command")')
|
||||
has_script=$(echo "$task" | jq -r 'has("script")')
|
||||
if [[ "$has_command" != "true" && "$has_script" != "true" ]]; then
|
||||
echo "Task '$label' has no command or script; skipping execution."
|
||||
return
|
||||
fi
|
||||
|
||||
# Determine the command to run:
|
||||
local cmd=""
|
||||
if echo "$task" | jq 'has("command")' | grep -q "true"; then
|
||||
cmd=$(echo "$task" | jq -r '.command')
|
||||
elif echo "$task" | jq 'has("script")' | grep -q "true"; then
|
||||
local script
|
||||
script=$(echo "$task" | jq -r '.script')
|
||||
local path
|
||||
path=$(echo "$task" | jq -r '.path // empty')
|
||||
if [ -n "$path" ]; then
|
||||
cmd="cd $path && npm run $script"
|
||||
else
|
||||
cmd="npm run $script"
|
||||
fi
|
||||
else
|
||||
echo "Error: Task '$label' does not have a command or script." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Substitute ${workspaceFolder} with the actual folder path.
|
||||
cmd="${cmd//\$\{workspaceFolder\}/$WORKSPACE_FOLDER}"
|
||||
|
||||
echo "Running task '$label': $cmd"
|
||||
# Run the task in a subshell so that directory changes don't persist.
|
||||
( eval "$cmd" )
|
||||
}
|
||||
|
||||
run_task "$TASK_LABEL"
|
||||
Reference in New Issue
Block a user