mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 064c897c86 | |||
| 2f46e0dc3e | |||
| e363c677a0 | |||
| f94a1f97df | |||
| e0592f0959 | |||
| 0736a6f995 | |||
| d454a404a0 | |||
| ffbc79d919 | |||
| a2312256b3 | |||
| 2cdb49168c | |||
| 944e0e6e07 | |||
| f53a3f383d | |||
| 407e647993 | |||
| 4a17474133 | |||
| ade7cb7640 | |||
| e8cbeb1805 | |||
| 4b89fc1f9d | |||
| 00e10be93f | |||
| b7043655b3 | |||
| 75ae7dfe69 | |||
| a0d50baaca | |||
| 0c2079fa85 | |||
| 3960d28b06 | |||
| 2d68809c1b | |||
| b81c41f667 | |||
| e22cc6269d | |||
| 300885f724 | |||
| 5838ddca56 | |||
| f6401f4995 | |||
| 75fbc7524f | |||
| ecc5f5c2fa | |||
| ae399b8c0e | |||
| 22d579512d | |||
| caf4d75a06 | |||
| a43e4328df | |||
| 0ede024771 | |||
| 193f97d9fe | |||
| 38c96ea96a | |||
| dd130dbbd7 | |||
| 9233495cac | |||
| e4001966e8 | |||
| c52f1cd038 | |||
| 2ba9bce3de | |||
| d4a76da78c | |||
| c7e4eb1b3f | |||
| 5460f95eac | |||
| 3ae4ceb431 | |||
| 7a725935fc | |||
| 9437d7ed99 | |||
| 34cf24f7c0 | |||
| 5a9959f70f | |||
| 96069d3299 | |||
| b4c90683aa | |||
| 6993516ccb | |||
| 37c1a0e964 | |||
| 6d086fb5d5 | |||
| 7c805e9b80 | |||
| 6b3f9e4a9e | |||
| dea08868d3 |
@@ -12,7 +12,7 @@ body:
|
||||
- label: I have searched existing issues
|
||||
- label: This issue is not a duplicate of an existing one
|
||||
- label: I have checked the [common issues section in the readme file](https://github.com/SteamDeckHomebrew/decky-loader#-common-issues)
|
||||
- label: I have attached logs to this bug report (failure to include logs will mean your issue will not be responded too).
|
||||
- label: I have attached logs to this bug report (failure to include logs will mean your issue may not be responded to).
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Steam Deck Homebrew Discord Server
|
||||
url: https://discord.gg/ZU74G2NJzk
|
||||
url: https://decky.xyz/discord
|
||||
about: Please ask and answer questions here.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Builder Win
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-win:
|
||||
name: Build PluginLoader for Win
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up NodeJS 18 💎
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Set up Python 3.11.4 🐍
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11.4"
|
||||
|
||||
- name: Install Python dependencies ⬇️
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pyinstaller==5.13.0
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Install JS dependencies ⬇️
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
|
||||
- name: Build JS Frontend 🛠️
|
||||
working-directory: ./frontend
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build Python Backend 🛠️
|
||||
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data "./backend/static;/static" --add-data "./backend/locales;/locales" --add-data "./backend/src/legacy;/src/legacy" --add-data "./plugin/*;/" --hidden-import=sqlite3 ./backend/main.py
|
||||
|
||||
- name: Build Python Backend (noconsole) 🛠️
|
||||
run: pyinstaller --noconfirm --noconsole --onefile --name "PluginLoader_noconsole" --add-data "./backend/static;/static" --add-data "./backend/locales;/locales" --add-data "./backend/src/legacy;/src/legacy" --add-data "./plugin/*;/" --hidden-import=sqlite3 ./backend/main.py
|
||||
|
||||
- name: Upload package artifact ⬆️
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: PluginLoader Win
|
||||
path: |
|
||||
./dist/PluginLoader.exe
|
||||
./dist/PluginLoader_noconsole.exe
|
||||
|
||||
+35
-60
@@ -31,7 +31,7 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Build PluginLoader
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Print input
|
||||
@@ -47,65 +47,33 @@ jobs:
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Set up Python 3.10.2 🐍
|
||||
- name: Set up Python 3.10.6 🐍
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10.2"
|
||||
|
||||
python-version: "3.10.6"
|
||||
|
||||
- name: Upgrade SQLite 3 binary version to 3.42.0 🧑💻
|
||||
run: >
|
||||
cd /tmp &&
|
||||
wget "https://www.sqlite.org/2023/sqlite-autoconf-3420000.tar.gz" &&
|
||||
tar -xvzf sqlite-autoconf-3420000.tar.gz &&
|
||||
cd /tmp/sqlite-autoconf-3420000 &&
|
||||
./configure --prefix=/usr --disable-static CFLAGS="-g" CPPFLAGS="$CPPFLAGS -DSQLITE_ENABLE_COLUMN_METADATA=1 \
|
||||
-DSQLITE_ENABLE_UNLOCK_NOTIFY -DSQLITE_ENABLE_DBSTAT_VTAB=1 -DSQLITE_ENABLE_FTS3_TOKENIZER=1 \
|
||||
-DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_SECURE_DELETE -DSQLITE_ENABLE_STMTVTAB -DSQLITE_MAX_VARIABLE_NUMBER=250000 \
|
||||
-DSQLITE_MAX_EXPR_DEPTH=10000 -DSQLITE_ENABLE_MATH_FUNCTIONS" &&
|
||||
make -j$(nproc) &&
|
||||
sudo make install &&
|
||||
sudo cp /usr/lib/libsqlite3.so /usr/lib/x86_64-linux-gnu/ &&
|
||||
sudo cp /usr/lib/libsqlite3.so.0 /usr/lib/x86_64-linux-gnu/ &&
|
||||
sudo cp /usr/lib/libsqlite3.so.0.8.6 /usr/lib/x86_64-linux-gnu/ &&
|
||||
rm -r /tmp/sqlite-autoconf-3420000
|
||||
|
||||
- name: Install Python dependencies ⬇️
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pyinstaller==5.5
|
||||
[ -f requirements.txt ] && pip install -r requirements.txt
|
||||
|
||||
- name: Install JS dependencies ⬇️
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
|
||||
- name: Build JS Frontend 🛠️
|
||||
working-directory: ./frontend
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build Python Backend 🛠️
|
||||
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/locales:/locales --add-data ./backend/legacy:/legacy --add-data ./plugin:/plugin ./backend/*.py
|
||||
|
||||
- name: Upload package artifact ⬆️
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: PluginLoader
|
||||
path: ./dist/PluginLoader
|
||||
|
||||
- name: Download package artifact locally
|
||||
if: ${{ env.ACT }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: ./dist/PluginLoader
|
||||
|
||||
build-win:
|
||||
name: Build PluginLoader for Win
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up NodeJS 18 💎
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Set up Python 3.10.2 🐍
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10.2"
|
||||
|
||||
- name: Install Python dependencies ⬇️
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pyinstaller==5.5
|
||||
pip install pyinstaller==5.13.0
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Install JS dependencies ⬇️
|
||||
@@ -119,13 +87,20 @@ jobs:
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build Python Backend 🛠️
|
||||
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data "./backend/static;/static" --add-data "./backend/locales;/locales" --add-data "./backend/legacy;/legacy" --add-data "./plugin;/plugin" ./backend/main.py
|
||||
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/locales:/locales --add-data ./backend/src/legacy:/src/legacy --add-data ./plugin/*:/ --hidden-import=sqlite3 ./backend/main.py
|
||||
|
||||
- name: Upload package artifact ⬆️
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: PluginLoader Win
|
||||
path: ./dist/PluginLoader.exe
|
||||
name: PluginLoader
|
||||
path: ./dist/PluginLoader
|
||||
|
||||
- name: Download package artifact locally
|
||||
if: ${{ env.ACT }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: ./dist/PluginLoader
|
||||
|
||||
release:
|
||||
name: Release stable version of the package
|
||||
@@ -153,7 +128,7 @@ jobs:
|
||||
- name: Get latest release
|
||||
uses: rez0n/actions-github-release@main
|
||||
id: latest_release
|
||||
env:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: "SteamDeckHomebrew/decky-loader"
|
||||
type: "nodraft"
|
||||
@@ -232,7 +207,7 @@ jobs:
|
||||
- name: Get latest release
|
||||
uses: rez0n/actions-github-release@main
|
||||
id: latest_release
|
||||
env:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: "SteamDeckHomebrew/decky-loader"
|
||||
type: "nodraft"
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
|
||||
@@ -2,6 +2,7 @@ name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -9,9 +10,14 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2 # Check out the repository first.
|
||||
- name: Run prettier (JavaScript & TypeScript)
|
||||
- uses: actions/checkout@v3 # Check out the repository first.
|
||||
|
||||
- name: Install TypeScript dependencies
|
||||
working-directory: frontend
|
||||
run: |
|
||||
pushd frontend
|
||||
npm install
|
||||
npm run lint
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
|
||||
- name: Run prettier (TypeScript)
|
||||
working-directory: frontend
|
||||
run: pnpm run lint
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Type Check
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: Run type checkers
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2 # Check out the repository first.
|
||||
|
||||
- name: Install Python dependencies
|
||||
working-directory: backend
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
[ -f requirements.txt ] && pip install -r requirements.txt
|
||||
|
||||
- name: Install TypeScript dependencies
|
||||
working-directory: frontend
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i --frozen-lockfile
|
||||
|
||||
- name: Run pyright (Python)
|
||||
uses: jakebailey/pyright-action@v1
|
||||
with:
|
||||
python-version: "3.10.6"
|
||||
no-comments: true
|
||||
working-directory: backend
|
||||
|
||||
- name: Run tsc (TypeScript)
|
||||
working-directory: frontend
|
||||
run: $(pnpm bin)/tsc --noEmit
|
||||
@@ -35,6 +35,7 @@ For more information about Decky Loader as well as documentation and development
|
||||
### 🤔 Common Issues
|
||||
|
||||
- Syncthing may use port 8080 on Steam Deck, which Decky Loader needs to function. If you are using Syncthing as a service, please change its port to something else.
|
||||
- 8384 is the recommended port for Syncthing.
|
||||
- If you are using any software that uses port 1337 or 8080, please change its port to something else or uninstall it.
|
||||
- Sometimes Decky will disappear on SteamOS updates. This can easily be fixed by just re-running the installer and installing the stable branch again. If this doesn't work, try installing the prerelease instead. If that doesn't work, then [check the existing issues](https://github.com/SteamDeckHomebrew/decky-loader/issues) and if there isn't one then you can [file a new issue](https://github.com/SteamDeckHomebrew/decky-loader/issues/new?assignees=&labels=bug&template=bug_report.yml&title=%5BBUG%5D+%3Ctitle%3E).
|
||||
|
||||
|
||||
@@ -14,7 +14,27 @@
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Použít tuto složku"
|
||||
"select": "Použít tuto složku",
|
||||
"label": "Složka",
|
||||
"show_more": "Zobrazit více souborů"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Vytvořeno (Nejstarší)",
|
||||
"created_desc": "Vytvořeno (Nejnovější)",
|
||||
"modified_asce": "Upraveno (Nejstarší)",
|
||||
"modified_desc": "Upraveno (Nejnovější)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Velikost (Nejmenší)",
|
||||
"size_desc": "Velikost (Největší)"
|
||||
},
|
||||
"files": {
|
||||
"show_hidden": "Zobrazit skryté soubory",
|
||||
"all_files": "Všechny soubory",
|
||||
"file_type": "Typ souboru"
|
||||
},
|
||||
"file": {
|
||||
"select": "Vybrat tento soubor"
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
@@ -54,7 +74,7 @@
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_one": "Upravit 1 plugin",
|
||||
"mixed_one": "Upravit {{count}} plugin",
|
||||
"mixed_few": "Upravit {{count}} pluginů",
|
||||
"mixed_other": "Upravit {{count}} pluginů",
|
||||
"reinstall_one": "Přeinstalovat 1 plugin",
|
||||
@@ -129,9 +149,6 @@
|
||||
"label_url": "Instalovat plugin z URL",
|
||||
"label_zip": "Instalovat plugin ze ZIP souboru"
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Zapíná interní vývojářské menu Valve.",
|
||||
"desc2": "Nedotýkejte se ničeho v této nabídce, pokud nevíte, co děláte.",
|
||||
@@ -160,6 +177,11 @@
|
||||
},
|
||||
"updates": {
|
||||
"header": "Aktualizace"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Dostupná aktualizace Decky",
|
||||
"header": "Notifikace",
|
||||
"plugin_updates_label": "Dostupná aktualizace pluginu"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
@@ -193,7 +215,11 @@
|
||||
"alph_desc": "Abecedně (A do Z)",
|
||||
"title": "Procházet"
|
||||
},
|
||||
"store_testing_cta": "Zvažte prosím testování nových pluginů, pomůžete tím týmu Decky Loader!"
|
||||
"store_testing_cta": "Zvažte prosím testování nových pluginů, pomůžete tím týmu Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Tento kanál obchodu můžete použít k testování nejnovějších verzí pluginů. Nezapomeňte zanechat zpětnou vazbu na GitHubu, aby bylo možné plugin aktualizovat pro všechny uživatele.",
|
||||
"label": "Vítejte na testovacím kanálu obchodu"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
@@ -221,5 +247,21 @@
|
||||
"decky_updates": "Aktualizace Decky",
|
||||
"patch_notes_desc": "Poznámky k verzi",
|
||||
"no_patch_notes_desc": "žádné poznámky pro tuto verzi"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Zpět"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Zadaná cesta není platná. Zkontrolujte ji a zadejte znovu správně.",
|
||||
"unknown": "Nastala neznámá chyba. Nezpracovaná chyba je: {{raw_error}}",
|
||||
"perm_denied": "Nemáte přístup k zadanému adresáři. Zkontrolujte, zda jako uživatel (deck na Steam Decku) máte odpovídající oprávnění pro přístup k dané složce/souboru."
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"settings_desc": "Otevřít nastavení Decky",
|
||||
"decky_store_desc": "Otevřít obchod Decky"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,6 @@
|
||||
"button_install": "Installieren",
|
||||
"label_url": "Installiere Erweiterung via URL"
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc2": "Fasse in diesem Menü nichts an, es sei denn, du weißt was du tust.",
|
||||
"label": "Aktiviere Valve-internes Menü",
|
||||
|
||||
+87
-24
@@ -13,14 +13,17 @@
|
||||
"label_url": "Εγκατάσταση επέκτασης απο URL",
|
||||
"label_zip": "Εγκατάσταση επέκτασης από αρχείο ZIP"
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "Η εγκατάσταση απέτυχε. Μόνο αρχεία ZIP επιτρέπονται."
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Ενεργοποιεί το μενού προγραμματιστή της Valve.",
|
||||
"desc2": "Μην αγγίξετε τίποτα σε αυτό το μενού εκτός και αν ξέρετε τι κάνει.",
|
||||
"label": "Ενεργοποιήση εσωτερικού μενού Valve"
|
||||
}
|
||||
},
|
||||
"cef_console": {
|
||||
"button": "Άνοιγμα Κονσόλας",
|
||||
"desc": "Ανοίγει την Κονσόλα CEF. Χρήσιμο μόνο για εντοπισμό σφαλμάτων. Τα πράγματα εδώ είναι δυνητικά επικίνδυνα και θα πρέπει να χρησιμοποιηθεί μόνο εάν είστε προγραμματιστής επεκτάσεων, ή κατευθυνθήκατε εδώ από έναν προγραμματιστή.",
|
||||
"label": "Κονσόλα CEF"
|
||||
},
|
||||
"header": "Άλλα"
|
||||
},
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
@@ -32,8 +35,8 @@
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Γίνεται επαναφόρτωση σε 5 δευτερόλεπτα",
|
||||
"disabling": "Γίνεται απενεργοποίηση",
|
||||
"enabling": "Γίνεται ενεργοποίηση"
|
||||
"disabling": "Γίνεται απενεργοποίηση των React DevTools",
|
||||
"enabling": "Γίνεται ενεργοποίηση των React DevTools"
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_no_desc": "Δεν υπάρχει περιγραφή.",
|
||||
@@ -69,14 +72,16 @@
|
||||
"reload": "Επαναφόρτωση",
|
||||
"uninstall": "Απεγκατάσταση",
|
||||
"update_to": "Ενημέρωση σε {{name}}",
|
||||
"update_all_one": "",
|
||||
"update_all_other": ""
|
||||
"update_all_one": "Ενημέρωση 1 επέκτασης",
|
||||
"update_all_other": "Ενημέρωση {{count}} επεκτάσεων",
|
||||
"show": "Γρήγορη πρόσβαση: Εμφάνιση",
|
||||
"hide": "Γρήγορη πρόσβαση: Απόκρυψη"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Ενημέρωση σε {{tag_name}} διαθέσιμη!",
|
||||
"error": "Σφάλμα",
|
||||
"plugin_error_uninstall": "Πηγαίντε στο <0></0> στο μενού του Decky για να απεγκαταστήσετε αυτή την επέκταση.",
|
||||
"plugin_error_uninstall": "Η φόρτωση του {{name}} προκάλεσε το παραπάνω σφάλμα. Αυτό συνήθως σημαίνει ότι η επέκταση απαιτεί ενημέρωση για τη νέα έκδοση του SteamUI. Ελέγξτε εάν υπάρχει ενημέρωση ή αξιολογήστε την απεγκαταστήσετε της επέκτασης στις ρυθμίσεις του Decky, στην ενότητα Επεκτάσεις.",
|
||||
"plugin_load_error": {
|
||||
"message": "Σφάλμα στη φόρτωση της επέκτασης {{name}}",
|
||||
"toast": "Σφάλμα φόρτωσης {{name}}"
|
||||
@@ -86,8 +91,8 @@
|
||||
"desc": "Σίγουρα θέλετε να απεγκαταστήσετε το {{name}};",
|
||||
"title": "Απεγκατάσταση {{name}}"
|
||||
},
|
||||
"plugin_update_one": "",
|
||||
"plugin_update_other": ""
|
||||
"plugin_update_one": "Διαθέσιμη ενημέρωση για 1 επέκταση!",
|
||||
"plugin_update_other": "Διαθέσιμες ενημερώσεις για {{count}} επεκτάσεις!"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
@@ -111,6 +116,11 @@
|
||||
},
|
||||
"beta": {
|
||||
"header": "Συμμετοχή στη Beta"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Διαθέσιμη ενημέρωση του Decky",
|
||||
"header": "Ειδοποιήσεις",
|
||||
"plugin_updates_label": "Διαθέσιμες ενημερώσεις επεκτάσεων"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
@@ -121,7 +131,7 @@
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "Συνεισφέροντας",
|
||||
"desc": "Αν θέλετε να συνεισφέρετε στο κατάστημα επεκτάσεων του Decky, τσεκάρετε το SteamDeckHomebrew/decky-plugin-template repository στο GitHub. Πληροφοριές σχετικά με τη δημιουργία και τη διανομή επεκτάσεων είναι διαθέσιμες στο README."
|
||||
"desc": "Αν θέλετε να συνεισφέρετε στο κατάστημα επεκτάσεων του Decky, τσεκάρετε το SteamDeckHomebrew/decky-plugin-template repository στο GitHub. Πληροφορίες σχετικά με τη δημιουργία και τη διανομή επεκτάσεων είναι διαθέσιμες στο README."
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Φίλτρο",
|
||||
@@ -144,7 +154,11 @@
|
||||
"alph_desc": "Αλφαβητικά (Α σε Ζ)",
|
||||
"title": "Περιήγηση"
|
||||
},
|
||||
"store_testing_cta": "Παρακαλώ σκεφτείτε να τεστάρετε νέες επεκτάσεις για να βοηθήσετε την ομάδα του Decky Loader!"
|
||||
"store_testing_cta": "Παρακαλώ σκεφτείτε να τεστάρετε νέες επεκτάσεις για να βοηθήσετε την ομάδα του Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"desc": "Μπορείτε να χρησιμοποιήσετε αυτό το κανάλι του καταστήματος για να δοκιμάσετε τις νεότερες εκδόσεις των επεκτάσεων. Φροντίστε να αφήσετε σχόλια στο GitHub, ώστε να βοηθήσετε στην ενημέρωση της εκάστοτε επέκταση για όλους τους χρήστες.",
|
||||
"label": "Καλώς ήρθατε στο Δοκιμαστικό Κανάλι τους Καταστήματος"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
@@ -175,23 +189,72 @@
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Χρησιμοποιήστε αυτό το φάκελο"
|
||||
"select": "Χρησιμοποιήστε αυτό το φάκελο",
|
||||
"label": "Φάκελος",
|
||||
"show_more": "Εμφάνιση περισσότερων αρχείων"
|
||||
},
|
||||
"filter": {
|
||||
"modified_asce": "Τροποποιήθηκε (Παλαιότερο)",
|
||||
"modified_desc": "Τροποποιήθηκε (Νεότερο)",
|
||||
"created_desc": "Δημιουργήθηκε (Νεότερο)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"created_asce": "Δημιουργήθηκε (Παλαιότερο)",
|
||||
"size_asce": "Μέγεθος (Μικρότερο)",
|
||||
"size_desc": "Μέγεθος (Μεγαλύτερο)"
|
||||
},
|
||||
"file": {
|
||||
"select": "Επιλογή αυτού του αρχείου"
|
||||
},
|
||||
"files": {
|
||||
"show_hidden": "Εμφάνιση Κρυφών Αρχείων",
|
||||
"all_files": "Όλα Τα Αρχεία",
|
||||
"file_type": "Τύπος Αρχείου"
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "",
|
||||
"hidden_other": ""
|
||||
"hidden_one": "1 επέκταση είναι κρυμμένη σε αυτήν τη λίστα",
|
||||
"hidden_other": "{{count}} επεκτάσεις είναι κρυμμένες σε αυτήν τη λίστα"
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_one": "",
|
||||
"mixed_other": "",
|
||||
"update_one": "",
|
||||
"update_other": "",
|
||||
"reinstall_one": "",
|
||||
"reinstall_other": "",
|
||||
"install_one": "",
|
||||
"install_other": ""
|
||||
"mixed_one": "Τροποποίηση 1 επέκτασης",
|
||||
"mixed_other": "Τροποποίηση {{count}} επεκτάσεων",
|
||||
"update_one": "Ενημέρωση 1 επέκτασης",
|
||||
"update_other": "Ενημέρωση {{count}} επεκτάσεων",
|
||||
"reinstall_one": "Επανεγκατάσταση 1 επέκτασης",
|
||||
"reinstall_other": "Επανεγκατάσταση {{count}} επεκτάσεων",
|
||||
"install_one": "Εγκατάσταση 1 επέκτασης",
|
||||
"install_other": "Εγκατάσταση {{count}} επεκτάσεων"
|
||||
},
|
||||
"confirm": "Είστε βέβαιοι ότι θέλετε να κάνετε τις ακόλουθες τροποποιήσεις;",
|
||||
"description": {
|
||||
"reinstall": "Επανεγκατάσταση {{name}} {{version}}",
|
||||
"update": "Ενημέρωση {{name}} to {{version}}",
|
||||
"install": "Εγκατάσταση {{name}} {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Επιβεβαίωση",
|
||||
"loading": "Φόρτωση"
|
||||
}
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Κρυφό στο μενού γρήγορης πρόσβασης"
|
||||
},
|
||||
"TitleView": {
|
||||
"settings_desc": "Άνοιγμα Ρυθμίσεων Decky",
|
||||
"decky_store_desc": "Άνοιγμα Καταστήματος Decky"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Πίσω"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Η καθορισμένη διαδρομή δεν είναι έγκυρη. Παρακαλούμε ελέγξτε τη και εισάγετέ τη ξανά σωστά.",
|
||||
"perm_denied": "Δεν έχετε πρόσβαση στην καθορισμένη διαδρομή. Ελέγξτε εάν ο χρήστης σας (deck στο Steam Deck) έχει τα αντίστοιχα δικαιώματα πρόσβασης στον καθορισμένο φάκελο/αρχείο.",
|
||||
"unknown": "Παρουσιάστηκε άγνωστο σφάλμα. Το σφάλμα είναι: {{raw_error}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +220,11 @@
|
||||
"alph_desc": "Alphabetical (A to Z)",
|
||||
"title": "Browse"
|
||||
},
|
||||
"store_testing_cta": "Please consider testing new plugins to help the Decky Loader team!"
|
||||
"store_testing_cta": "Please consider testing new plugins to help the Decky Loader team!",
|
||||
"store_testing_warning": {
|
||||
"desc": "You can use this store channel to test bleeding-edge plugin versions. Be sure to leave feedback on GitHub so the plugin can be updated for all users.",
|
||||
"label": "Welcome to the Testing Store Channel"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
@@ -234,6 +238,10 @@
|
||||
"testing": "Testing"
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Open Decky Store",
|
||||
"settings_desc": "Open Decky Settings"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky Updates",
|
||||
"no_patch_notes_desc": "no patch notes for this version",
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
"label": "Activar menú interno de Valve",
|
||||
"desc1": "Activa el menú interno de desarrollo de Valve."
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "¡Ha fallado la instalación! Solo se permiten archivos ZIP."
|
||||
},
|
||||
"cef_console": {
|
||||
"button": "Abrir consola",
|
||||
"label": "Consola CEF",
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"prerelease": "Esijulkaisu",
|
||||
"testing": "Testiversio",
|
||||
"stable": "Vakaa versio",
|
||||
"label": "Päivityskanava"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Uudelleenladataan 5 sekunin kuluttua",
|
||||
"disabling": "Poistetaan React DevTools käytöstä",
|
||||
"enabling": "Otetaan React DevTools käyttöön"
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"perm_denied": "Sinulla ei ole käyttöoikeutta määritettyyn hakemistoon. Tarkista, onko käyttäjälläsi (käyttäjä 'deck' Steam Deckillä) vastaavat oikeudet käyttää määritettyä kansiota/tiedostoa.",
|
||||
"unknown": "Tapahtui tuntematon virhe. Raaka virhe on: {{raw_error}}",
|
||||
"file_not_found": "Määritetty polku ei kelpaa. Tarkista se ja kirjoita se uudelleen oikein."
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Valitse tämä tiedosto"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Kaikki tiedostot",
|
||||
"file_type": "Tiedostotyyppi",
|
||||
"show_hidden": "Näytä piilotetut tiedostot"
|
||||
},
|
||||
"filter": {
|
||||
"created_desc": "Luotu (uusin ensin)",
|
||||
"modified_asce": "Muokattu (vanhin)",
|
||||
"modified_desc": "Muokattu (uusin)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Koko (pienin ensin)",
|
||||
"size_desc": "Koko (suurin ensin)",
|
||||
"created_asce": "Luotu (vanhin ensin)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Kansio",
|
||||
"select": "Käytä tätä kansiota",
|
||||
"show_more": "Näytä lisää tiedostoja"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Haluatko varmasti tehdä seuraavat muutokset?",
|
||||
"description": {
|
||||
"reinstall": "Uudelleenasenna {{name}} {{version}}",
|
||||
"update": "Päivitä {{name}} versioon {{version}}",
|
||||
"install": "Asenna {{name}} {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Vahvista",
|
||||
"loading": "Ladataan"
|
||||
},
|
||||
"title": {
|
||||
"install_one": "Asenna yksi laajennus",
|
||||
"install_other": "Asenna {{count}} laajennusta",
|
||||
"update_one": "Päivitä yksi laajennus",
|
||||
"update_other": "Päivitä {{count}} laajennusta",
|
||||
"mixed_one": "Muuta yhtä laajennusta",
|
||||
"mixed_other": "Muuta {{count}} laajennusta",
|
||||
"reinstall_one": "Uudelleenasenna yksi laajennus",
|
||||
"reinstall_other": "Uudelleenasenna {{count}} laajennusta"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_install": "Asenna",
|
||||
"plugin_no_desc": "Ei kuvausta.",
|
||||
"plugin_version_label": "Laajennuksen versio",
|
||||
"plugin_full_access": "Tällä laajennuksella on täysi pääsy Steam Deckkiisi."
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Asenna",
|
||||
"button_processing": "Asennetaan",
|
||||
"desc": "Haluatko varmasti asentaa {{artifact}} {{version}}?",
|
||||
"title": "Asenna {{artifact}}"
|
||||
},
|
||||
"no_hash": "Tällä laajennuksella ei ole hashia, asennat sen omalla vastuullasi.",
|
||||
"reinstall": {
|
||||
"button_idle": "Uudelleenasenna",
|
||||
"button_processing": "Uudelleenasennetaan",
|
||||
"desc": "Haluatko varmasti uudelleenasentaa {{artifact}} {{version}}?",
|
||||
"title": "Uudelleenasenna {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Päivitä",
|
||||
"button_processing": "Päivitetään",
|
||||
"desc": "Haluatko varmasti päivittää {{artifact}} {{version}}?",
|
||||
"title": "Päivitä {{artifact}}"
|
||||
}
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Takaisin"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "Ei asennettuja laajennuksia!",
|
||||
"plugin_actions": "Laajennustoiminnot",
|
||||
"reinstall": "Uudelleenasenna",
|
||||
"reload": "Lataa uudelleen",
|
||||
"uninstall": "Poista asennus",
|
||||
"update_all_one": "Päivitä yksi laajennus",
|
||||
"update_all_other": "Päivitä {{count}} laajennusta",
|
||||
"update_to": "Päivitä versioon {{name}}",
|
||||
"hide": "Pikavalikko: Piilota",
|
||||
"show": "Pikavalikko: Näytä"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Piilotettu pikavalikosta"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Päivitys versioon {{tag_name}} on saatavilla!",
|
||||
"error": "Virhe",
|
||||
"plugin_load_error": {
|
||||
"message": "Virhe ladattaessa {{name}}-laajennusta",
|
||||
"toast": "Virhe ladattaessa {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Poista asennus",
|
||||
"desc": "Haluatko varmasti poistaa {{name}} asennuksen?",
|
||||
"title": "Poista {{name}}"
|
||||
},
|
||||
"plugin_update_one": "Päivityksiä saatavilla yhdelle laajennukselle!",
|
||||
"plugin_update_other": "Päivityksiä saatavilla {{count}} laajennukselle!",
|
||||
"plugin_error_uninstall": "{{name}} lataaminen aiheutti yllä olevan poikkeuksen. Tämä tarkoittaa yleensä sitä, että laajennus vaatii päivityksen uudelle SteamUI-versiolle. Tarkista, onko päivitystä saatavilla, tai harkitse laajennuksen poistoa Decky-asetuksista, laajennukset-osiosta."
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Salli todentamaton pääsy CEF-debuggeriin kenelle tahansa verkossasi",
|
||||
"label": "Salli CEF-etädebugaus"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Avaa konsoli",
|
||||
"desc": "Avaa CEF-konsolin. Hyödyllinen vain debugaustarkoituksiin. Täällä olevat jutut ovat mahdollisesti vaarallisia, ja niitä tulisi käyttää vain, jos olet laajennuksen kehittäjä tai jos kehittäjä on ohjannut sinut tänne.",
|
||||
"label": "CEF-konsoli"
|
||||
},
|
||||
"header": "Muu",
|
||||
"react_devtools": {
|
||||
"desc": "Mahdollistaa yhteyden tietokoneeseen, jossa on käytössä React DevTools. Tämän asetuksen muuttaminen lataa Steamin uudelleen. Aseta IP-osoite ennen käyttöönottoa.",
|
||||
"ip_label": "IP-osoite",
|
||||
"label": "Ota React DevTools käyttöön"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Asenna",
|
||||
"button_zip": "Selaa",
|
||||
"header": "Kolmannen osapuolen laajennukset",
|
||||
"label_desc": "URL-osoite",
|
||||
"label_zip": "Asenna laajennus ZIP-tiedostosta",
|
||||
"label_url": "Asenna laajennus URL-osoitteesta"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc2": "Älä koske mihinkään tässä valikossa, ellet tiedä mitä se tekee.",
|
||||
"label": "Ota Valve Internal käyttöön",
|
||||
"desc1": "Ottaa käyttöön Valven sisäisen kehittäjävalikon."
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky-versio",
|
||||
"header": "Tietoja"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Beta-osallistuminen"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Kehittäjätila"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Decky-päivitys saatavilla",
|
||||
"header": "Ilmoitukset",
|
||||
"plugin_updates_label": "Laajennuspäivityksiä saatavilla"
|
||||
},
|
||||
"other": {
|
||||
"header": "Muu"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Päivitykset"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Kehittäjä",
|
||||
"general_title": "Yleinen",
|
||||
"plugins_title": "Laajennukset"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "Osallistuminen",
|
||||
"desc": "Mikäli haluat julkaista Decky Plugin Storeen, tarkista GitHubin SteamDeckHomebrew/decky-plugin-template -esimerkkitietovarasto. Tietoa kehityksestä ja jakelusta löytyy README:stä."
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Suodin",
|
||||
"label_def": "Kaikki"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Hae"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Järjestä",
|
||||
"label_def": "Viimeksi päivitetty (uusin ensin)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Kaikken laajennusten lähdekoodit ovat saatavilla SteamDeckHomebrew/decky-plugin-database -arkistosta GitHubissa.",
|
||||
"label": "Lähdekoodi"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Tietoja",
|
||||
"alph_asce": "Aakkosjärjestyksessä (Z–A)",
|
||||
"alph_desc": "Aakkosjärjestyksessä (A–Z)",
|
||||
"title": "Selaa"
|
||||
},
|
||||
"store_testing_cta": "Harkitse uusien lisäosien testaamista auttaaksesi Decky Loader -tiimiä!",
|
||||
"store_testing_warning": {
|
||||
"label": "Tervetuloa testausmyymälä-kanavalle",
|
||||
"desc": "Voit käyttää tätä myymäläkanavaa testataksesi uusimpia laajennusversioita. Muista jättää palautetta GitHubissa, jotta laajennus voidaan päivittää kaikille käyttäjille."
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Mukautettu myymälä",
|
||||
"url_label": "URL-osoite"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Mukautettu",
|
||||
"default": "Oletus",
|
||||
"label": "Myymäläkanava",
|
||||
"testing": "Testaus"
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Avaa Decky-myymälä",
|
||||
"settings_desc": "Avaa Decky-asetukset"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky-päivitykset",
|
||||
"no_patch_notes_desc": "tälle versiolle ei ole korjausmerkintöjä",
|
||||
"patch_notes_desc": "Korjausmerkinnät",
|
||||
"updates": {
|
||||
"check_button": "Tarkista päivitykset",
|
||||
"checking": "Tarkistetaan",
|
||||
"cur_version": "Nykyinen versio: {{ver}}",
|
||||
"install_button": "Asenna päivitys",
|
||||
"label": "Päivitykset",
|
||||
"lat_version": "Ajan tasalla: versio {{ver}}",
|
||||
"reloading": "Uudelleenladataan",
|
||||
"updating": "Päivitetään"
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "Yksi laajennus on piilotettu tästä luettelosta",
|
||||
"hidden_other": "{{count}} laajennusta on piilotettu tästä luettelosta"
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,6 @@
|
||||
"label_url": "Installer le plugin à partir d'un URL",
|
||||
"label_zip": "Installer le plugin à partir d'un fichier ZIP"
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky"
|
||||
},
|
||||
"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.",
|
||||
|
||||
@@ -141,9 +141,6 @@
|
||||
"hidden_many": "Sono nascosti {{count}} plugin dalla lista",
|
||||
"hidden_other": "Sono nascosti {{count}} plugin dalla lista"
|
||||
},
|
||||
"notifications": {
|
||||
"plugin_updates_label": "Aggiornamenti dei plugins"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Permetti l'accesso non autenticato al debugger di CEF da tutti gli indirizzi sulla tua rete locale",
|
||||
@@ -230,7 +227,11 @@
|
||||
"alph_desc": "Alfabetico (A a Z)",
|
||||
"title": "Sfoglia"
|
||||
},
|
||||
"store_testing_cta": "Valuta la possibilità di testare nuovi plugin per aiutare il team di Decky Loader!"
|
||||
"store_testing_cta": "Valuta la possibilità di testare nuovi plugin per aiutare il team di Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"label": "Benvenuto nel Negozio di Test dei Plugins",
|
||||
"desc": "Puoi usare questo canale del negozio per testare versioni di plugin sperimentali. Assicurati di lasciare un feedback su Github dopo averlo testato in modo che il plugin possa essere promosso a stabile per tutti gli altri utenti o per permettere allo sviluppatore di plugin di correggere eventuali errori."
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
@@ -258,5 +259,9 @@
|
||||
"reloading": "Ricaricando",
|
||||
"updating": "Aggiornando"
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"settings_desc": "Apri le impostazioni di Decky",
|
||||
"decky_store_desc": "Apri lo store di Decky"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "업데이트 배포 채널",
|
||||
"stable": "안정판",
|
||||
"testing": "시험판",
|
||||
"prerelease": "사전 출시판"
|
||||
"stable": "안정",
|
||||
"testing": "테스트",
|
||||
"prerelease": "사전 출시"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
@@ -201,7 +201,11 @@
|
||||
"alph_desc": "알파벳순 (A-Z)",
|
||||
"title": "검색"
|
||||
},
|
||||
"store_testing_cta": "새로운 플러그인을 테스트하여 Decky Loader 팀을 도와주세요!"
|
||||
"store_testing_cta": "새로운 플러그인을 테스트하여 Decky Loader 팀을 도와주세요!",
|
||||
"store_testing_warning": {
|
||||
"desc": "이 스토어 채널을 사용하여 가장 최신 버전의 플러그인을 테스트할 수 있습니다. GitHub에 피드백을 남겨서 모든 사용자가 업데이트 할 수 있게 해주세요.",
|
||||
"label": "테스트 스토어 채널에 오신 것을 환영합니다"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
@@ -212,7 +216,7 @@
|
||||
"custom": "사용자 지정",
|
||||
"label": "스토어 배포 채널",
|
||||
"default": "기본",
|
||||
"testing": "시험"
|
||||
"testing": "테스트"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
@@ -241,5 +245,9 @@
|
||||
"button": {
|
||||
"back": "뒤로"
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"settings_desc": "Decky 설정 열기",
|
||||
"decky_store_desc": "Decky 스토어 열기"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,9 +156,6 @@
|
||||
"button_install": "Installeren",
|
||||
"button_zip": "Bladeren"
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "Installatie mislukt! Alleen ZIP-bestanden worden ondersteund."
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Schakelt het interne ontwikkelaarsmenu van Valve in.",
|
||||
"desc2": "Raak niets in dit menu aan tenzij u weet wat het doet.",
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"testing": "Testowy",
|
||||
"label": "Kanał aktualizacji",
|
||||
"stable": "Stabilny",
|
||||
"prerelease": "Przedpremierowy"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"enabling": "Włączanie React DevTools",
|
||||
"5secreload": "Ponowne załadowanie za 5 sekund",
|
||||
"disabling": "Wyłączanie React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Powrót"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"perm_denied": "Nie masz dostępu do podanego katalogu. Sprawdź, czy twój użytkownik (deck na Steam Deck) ma odpowiednie uprawnienia dostępu do określonego katalogu/pliku.",
|
||||
"unknown": "Wystąpił nieznany błąd. Surowy błąd to {{raw_error}}",
|
||||
"file_not_found": "Podana ścieżka jest nieprawidłowa. Sprawdź ją i wprowadź ponownie poprawnie."
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Wybierz ten plik"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Wszystkie pliki",
|
||||
"file_type": "Typ pliku",
|
||||
"show_hidden": "Pokaż ukryte pliki"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Utworzono (najstarszy)",
|
||||
"created_desc": "Utworzono (najnowszy)",
|
||||
"modified_asce": "Zmodyfikowany (najstarszy)",
|
||||
"modified_desc": "Zmodyfikowany (najnowszy)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Rozmiar (najmniejszy)",
|
||||
"size_desc": "Rozmiar (największy)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Katalog",
|
||||
"select": "Użyj tego katalogu",
|
||||
"show_more": "Pokaż więcej plików"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_one": "Zmodyfikuj {{count}} plugin",
|
||||
"mixed_few": "Zmodyfikuj {{count}} pluginy",
|
||||
"mixed_many": "Zmodyfikuj {{count}} pluginów",
|
||||
"reinstall_one": "Reinstaluj 1 plugin",
|
||||
"reinstall_few": "Reinstaluj {{count}} pluginy",
|
||||
"reinstall_many": "Reinstaluj {{count}} pluginów",
|
||||
"install_one": "Zainstaluj 1 plugin",
|
||||
"install_few": "Zainstaluj {{count}} pluginy",
|
||||
"install_many": "Zainstaluj {{count}} pluginów",
|
||||
"update_one": "Zaktualizuj 1 plugin",
|
||||
"update_few": "Zaktualizuj {{count}} pluginy",
|
||||
"update_many": "Zaktualizuj {{count}} pluginów"
|
||||
},
|
||||
"confirm": "Czy na pewno chcesz wprowadzić następujące modyfikacje?",
|
||||
"description": {
|
||||
"install": "Zainstaluj {{name}} {{version}}",
|
||||
"reinstall": "Reinstaluj {{name}} {{version}}",
|
||||
"update": "Zaktualizuj {{name}} do {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Potwierdź",
|
||||
"loading": "W toku"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_install": "Zainstaluj",
|
||||
"plugin_no_desc": "Brak opisu.",
|
||||
"plugin_version_label": "Wersja pluginu",
|
||||
"plugin_full_access": "Ten plugin ma pełny dostęp do twojego Steam Decka."
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Zainstaluj",
|
||||
"button_processing": "Instalowanie",
|
||||
"desc": "Czy na pewno chcesz zainstalować {{artifact}} {{version}}?",
|
||||
"title": "Zainstaluj {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstaluj",
|
||||
"button_processing": "Reinstalowanie",
|
||||
"desc": "Czy na pewno chcesz ponownie zainstalować {{artifact}} {{version}}?",
|
||||
"title": "Reinstaluj {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Aktualizacja",
|
||||
"button_processing": "Aktualizowanie",
|
||||
"desc": "Czy na pewno chcesz zaktualizować {{artifact}} {{version}}?",
|
||||
"title": "Zaktualizuj {{artifact}}"
|
||||
},
|
||||
"no_hash": "Ten plugin nie ma hasha, instalujesz go na własne ryzyko."
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"hide": "Szybki dostęp: Ukryj",
|
||||
"no_plugin": "Brak zainstalowanych pluginów!",
|
||||
"reload": "Załaduj ponownie",
|
||||
"update_all_one": "Zaktualizuj 1 plugin",
|
||||
"update_all_few": "Zaktualizuj {{count}} pluginy",
|
||||
"update_all_many": "Zaktualizuj {{count}} pluginów",
|
||||
"plugin_actions": "Akcje pluginów",
|
||||
"reinstall": "Reinstalacja",
|
||||
"show": "Szybki dostęp: Pokaż",
|
||||
"uninstall": "Odinstaluj",
|
||||
"update_to": "Zaktualizuj do {{name}}"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Dostępna aktualizacja do {{tag_name}}!",
|
||||
"error": "Błąd",
|
||||
"plugin_error_uninstall": "Ładowanie {{name}} spowodowało wyjątek, jak pokazano powyżej. Zwykle oznacza to, że plugin wymaga aktualizacji do nowej wersji SteamUI. Sprawdź, czy aktualizacja jest obecna lub rozważ usunięcie go w ustawieniach Decky, w sekcji Pluginy.",
|
||||
"plugin_load_error": {
|
||||
"message": "Błąd ładowania plugin {{name}}",
|
||||
"toast": "Błąd ładowania {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Odinstaluj",
|
||||
"title": "Odinstaluj {{name}}",
|
||||
"desc": "Czy na pewno chcesz odinstalować {{name}}?"
|
||||
},
|
||||
"plugin_update_one": "Aktualizacje dostępne dla 1 pluginu!",
|
||||
"plugin_update_few": "Aktualizacje dostępne dla {{count}} pluginów!",
|
||||
"plugin_update_many": "Aktualizacje dostępne dla {{count}} pluginów!"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Ukryty w menu szybkiego dostępu"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 plugin jest ukryty na tej liście",
|
||||
"hidden_few": "{{count}} pluginy jest ukryty na tej liście",
|
||||
"hidden_many": "{{count}} pluginów jest ukryty na tej liście"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Zezwalaj na nieuwierzytelniony dostęp do debugera CEF wszystkim osobom w Twojej sieci",
|
||||
"label": "Zezwól na zdalne debugowanie CEF"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Otwórz konsolę",
|
||||
"desc": "Otwiera konsolę CEF. Przydatne tylko do celów debugowania. Rzeczy tutaj są potencjalnie niebezpieczne i powinny być używane tylko wtedy, gdy jesteś twórcą wtyczek lub zostałeś tu przez kogoś skierowany.",
|
||||
"label": "Konsola CEF"
|
||||
},
|
||||
"header": "Inne",
|
||||
"react_devtools": {
|
||||
"desc": "Umożliwia połączenie z komputerem z uruchomionym React DevTools. Zmiana tego ustawienia spowoduje ponowne załadowanie Steam. Ustaw adres IP przed włączeniem.",
|
||||
"ip_label": "IP",
|
||||
"label": "Włącz React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Zainstaluj",
|
||||
"button_zip": "Przeglądaj",
|
||||
"header": "Pluginy zewnętrzne",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Zainstaluj plugin z adresu URL",
|
||||
"label_zip": "Zainstaluj plugin z pliku ZIP"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Włącza wewnętrzne menu programisty Valve.",
|
||||
"desc2": "Nie dotykaj niczego w tym menu, chyba że wiesz, co robi.",
|
||||
"label": "Włącz Valve Internal"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"notifications": {
|
||||
"decky_updates_label": "Dostępna aktualizacja Decky",
|
||||
"header": "Powiadomienia",
|
||||
"plugin_updates_label": "Dostępne aktualizacje pluginów"
|
||||
},
|
||||
"other": {
|
||||
"header": "Inne"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Aktualizacje"
|
||||
},
|
||||
"about": {
|
||||
"header": "Informacje",
|
||||
"decky_version": "Wersja Decky"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Udział w becie"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Tryb dewelopera"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Deweloper",
|
||||
"general_title": "Ogólne",
|
||||
"plugins_title": "Pluginy"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"desc": "Jeśli chcesz przyczynić się do rozwoju Decky Plugin Store, sprawdź repozytorium SteamDeckHomebrew/decky-plugin-template na GitHub. Informacje na temat rozwoju i dystrybucji są dostępne w pliku README.",
|
||||
"label": "Współtworzenie"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtr",
|
||||
"label_def": "Wszystko"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Szukaj"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Sortowanie",
|
||||
"label_def": "Ostatnia aktualizacja (najnowsza)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Cały kod źródłowy pluginów jest dostępny w repozytorium SteamDeckHomebrew/decky-plugin-database na GitHub.",
|
||||
"label": "Kod źródłowy"
|
||||
},
|
||||
"store_tabs": {
|
||||
"alph_asce": "Alfabetycznie (od Z do A)",
|
||||
"alph_desc": "Alfabetycznie (od A do Z)",
|
||||
"title": "Przeglądaj",
|
||||
"about": "Informacje"
|
||||
},
|
||||
"store_testing_cta": "Rozważ przetestowanie nowych pluginów, aby pomóc zespołowi Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
"label": "Witamy w Testowym Kanale Sklepu",
|
||||
"desc": "Możesz użyć tego kanału sklepu do testowania najnowszych wersji pluginów. Pamiętaj, aby zostawić opinię na GitHub, aby plugin mogła zostać zaktualizowana dla wszystkich użytkowników."
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Niestandardowy sklep",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Niestandardowy",
|
||||
"default": "Domyślny",
|
||||
"label": "Kanał sklepu",
|
||||
"testing": "Testowy"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Aktualizacje Decky",
|
||||
"no_patch_notes_desc": "Brak informacji o poprawkach dla tej wersji",
|
||||
"patch_notes_desc": "Opis zmian",
|
||||
"updates": {
|
||||
"check_button": "Sprawdź aktualizacje",
|
||||
"checking": "Sprawdzanie",
|
||||
"cur_version": "Aktualna wersja: {{ver}}",
|
||||
"install_button": "Zainstaluj aktualizację",
|
||||
"label": "Aktualizacje",
|
||||
"lat_version": "Aktualizacje zainstalowane. Aktualna wersja: {{ver}}",
|
||||
"reloading": "Ponowne ładowanie",
|
||||
"updating": "Aktualizowanie"
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"settings_desc": "Otwórz ustawienia Decky",
|
||||
"decky_store_desc": "Otwórz sklep Decky"
|
||||
}
|
||||
}
|
||||
@@ -120,7 +120,7 @@
|
||||
},
|
||||
"decky_update_available": "Atualização para {{tag_name}} disponível!",
|
||||
"plugin_error_uninstall": "Um erro aconteceu ao carregar {{name}}, como mostrado acima. Isso normalmente significa que o plugin precisa de uma atualização para a nova versão do SteamUI. Confira se existe uma atualização ou avalie a remoção do plugin nas configurações do Decky, na sessão de plugins.",
|
||||
"plugin_update_one": "Atualizações disponível para 1 plugin!",
|
||||
"plugin_update_one": "Atualização disponível para 1 plugin!",
|
||||
"plugin_update_many": "Atualizações disponíveis para {{count}} plugins!",
|
||||
"plugin_update_other": "Atualizações disponíveis para {{count}} plugins!"
|
||||
},
|
||||
@@ -150,9 +150,6 @@
|
||||
"label_zip": "Instalar Plugin a partir de um arquivo ZIP",
|
||||
"label_desc": "URL"
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "Falha na instalação! Somente arquivos ZIP são suportados."
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Habilita o menu interno de desenvolvedor da Valve.",
|
||||
"desc2": "Não toque em nada neste menu, a não ser que você saiba o que está fazendo.",
|
||||
@@ -175,6 +172,11 @@
|
||||
},
|
||||
"beta": {
|
||||
"header": "Participação no Beta"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Atualização do Decky disponível",
|
||||
"header": "Noificações",
|
||||
"plugin_updates_label": "Atualizações de Plugin disponíveis"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
@@ -250,7 +252,8 @@
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "O caminho especificado não é válido. Por favor, confira e reinsira corretamente.",
|
||||
"unknown": "Ocorreu um erro desconhecido. O erro completo é: {{raw_error}}"
|
||||
"unknown": "Ocorreu um erro desconhecido. O erro completo é: {{raw_error}}",
|
||||
"perm_denied": "Você não tem acesso à este diretório. Por favor, verifiquei se seu usuário (deck no Steam Deck) tem as permissões necessárias para acessar este arquivo/pasta."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,9 +129,6 @@
|
||||
"label_url": "Instalar plugin a partir dum URL",
|
||||
"label_zip": "Instalar plugin a partir dum ficheiro ZIP"
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "A instalação falhou! Só ficheiros ZIP são suportados."
|
||||
},
|
||||
"valve_internal": {
|
||||
"label": "Activar menu interno da Valve",
|
||||
"desc1": "Activa o menu interno de programador da Valve.",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Удалить",
|
||||
"desc": "Вы уверенны, что хотите удалить {{name}}?",
|
||||
"desc": "Вы уверены, что хотите удалить {{name}}?",
|
||||
"title": "Удалить {{name}}"
|
||||
},
|
||||
"decky_title": "Decky",
|
||||
@@ -97,19 +97,19 @@
|
||||
"button_processing": "Установка",
|
||||
"title": "Установить {{artifact}}",
|
||||
"button_idle": "Установить",
|
||||
"desc": "Вы уверенны, что хотите установить {{artifact}} {{version}}?"
|
||||
"desc": "Вы уверены, что хотите установить {{artifact}} {{version}}?"
|
||||
},
|
||||
"no_hash": "У данного плагина отсутствует хэш, устанавливайте на свой страх и риск.",
|
||||
"reinstall": {
|
||||
"title": "Переустановить {{artifact}}",
|
||||
"desc": "Вы уверенны, что хотите переустановить {{artifact}} {{version}}?",
|
||||
"desc": "Вы уверены, что хотите переустановить {{artifact}} {{version}}?",
|
||||
"button_idle": "Переустановить",
|
||||
"button_processing": "Переустановка"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Обновить",
|
||||
"button_processing": "Обновление",
|
||||
"desc": "Вы уверенны, что хотите обновить {{artifact}} {{version}}?",
|
||||
"desc": "Вы уверены, что хотите обновить {{artifact}} {{version}}?",
|
||||
"title": "Обновить {{artifact}}"
|
||||
}
|
||||
},
|
||||
@@ -197,6 +197,10 @@
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Поиск"
|
||||
},
|
||||
"store_testing_warning": {
|
||||
"label": "Добро пожаловать в тестовый канал магазина",
|
||||
"desc": "Вы можете использовать этот канал магазина для тестирования новейших версий плагинов. Не забудьте оставить отзыв на GitHub, чтобы плагин можно было обновить для всех пользователей."
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
@@ -208,7 +212,7 @@
|
||||
"custom": "Сторонний",
|
||||
"default": "По-умолчанию",
|
||||
"label": "Канал магазина",
|
||||
"testing": "Тестирование"
|
||||
"testing": "Тестовый"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
@@ -255,5 +259,9 @@
|
||||
"developer_title": "Разработчик",
|
||||
"general_title": "Общее",
|
||||
"plugins_title": "Плагины"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Открыть магазин Decky",
|
||||
"settings_desc": "Открыть настройки Decky"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,9 +124,6 @@
|
||||
"label_zip": "Встановити плагін з ZIP-файлу",
|
||||
"button_zip": "Огляд"
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "Помилка встановлення! Підтримуються лише ZIP-файли."
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Вмикає внутрішнє розробницьке меню Valve.",
|
||||
"label": "Увімкнути Valve Internal",
|
||||
|
||||
@@ -177,7 +177,11 @@
|
||||
"alph_desc": "字母排序 (A 到 Z)",
|
||||
"title": "浏览"
|
||||
},
|
||||
"store_testing_cta": "请考虑测试新插件以帮助 Decky Loader 团队!"
|
||||
"store_testing_cta": "请考虑测试新插件以帮助 Decky Loader 团队!",
|
||||
"store_testing_warning": {
|
||||
"desc": "你可以使用该商店频道以体验最新版本的插件。 请在插件 Github 页面留言以使插件可以正式面向所有用户。",
|
||||
"label": "欢迎来到商店测试频道"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"store_channel": {
|
||||
@@ -238,8 +242,12 @@
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "指定路径无效。请检查并输入正确的路径。",
|
||||
"unknown": "发生了一个为止错误。原始错误为:{{raw_error}}",
|
||||
"unknown": "发生了一个未知错误。原始错误为:{{raw_error}}",
|
||||
"perm_denied": "你没有访问特定目录的权限。请检查你的用户(Steam Deck 中的 deck 账户)有着相对应的权限以访问特定的文件夹或文件。"
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "打开 Decky 商店",
|
||||
"settings_desc": "打开 Decky 设置"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,27 @@
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "使用此資料夾"
|
||||
"select": "使用此資料夾",
|
||||
"show_more": "顯示更多檔案",
|
||||
"label": "資料夾"
|
||||
},
|
||||
"filter": {
|
||||
"modified_asce": "修改日期(舊到新)",
|
||||
"created_desc": "建立日期(新到舊)",
|
||||
"modified_desc": "修改日期(新到舊)",
|
||||
"name_desc": "子母排序(A到Z)",
|
||||
"name_asce": "子母排序(Z到A)",
|
||||
"size_asce": "檔案大小(小到大)",
|
||||
"size_desc": "檔案大小(大到小)",
|
||||
"created_asce": "建立日期(舊到新)"
|
||||
},
|
||||
"file": {
|
||||
"select": "選擇此檔案"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "所有檔案",
|
||||
"file_type": "檔案類型",
|
||||
"show_hidden": "顯示隱藏檔"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
@@ -86,9 +106,6 @@
|
||||
"button_install": "安裝",
|
||||
"header": "第三方外掛程式"
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc2": "除非您知道它的作用,否則不要碰這個選單中的任何東西。",
|
||||
"desc1": "啟用 Valve 內建開發人員選單。",
|
||||
@@ -103,7 +120,7 @@
|
||||
"cef_console": {
|
||||
"button": "開啟控制台",
|
||||
"label": "CEF 控制台",
|
||||
"desc": "開啟 CEF 控制台。僅用於偵錯。這裡的東西有潛在的風險,只有當你是一個外掛程式開發者或者被外掛程式開發者引導到這裡時,才應該使用。"
|
||||
"desc": "開啟 CEF 控制台。僅用於偵錯。這裡的東西有潛在的風險,只有當您是一個外掛程式開發者或者被外掛程式開發者引導到這裡時,才應該使用。"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
@@ -122,6 +139,11 @@
|
||||
},
|
||||
"updates": {
|
||||
"header": "更新"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Decky 可更新",
|
||||
"header": "通知",
|
||||
"plugin_updates_label": "外掛程式有更新"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
@@ -207,5 +229,17 @@
|
||||
"update": "更新 {{name}} 到 {{version}}",
|
||||
"reinstall": "重新安裝 {{name}} {{version}}"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"perm_denied": "您沒有瀏覽此目錄的權限。請檢查您的使用者(Steam Deck 中的 deck 帳號)有權限瀏覽特定的資料夾或檔案。",
|
||||
"unknown": "發生未知錯誤。錯誤詳細資料:{{raw_error}}",
|
||||
"file_not_found": "指定路徑無效。請檢查並輸入正確路徑。"
|
||||
}
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "返回"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-189
@@ -1,190 +1,4 @@
|
||||
# Change PyInstaller files permissions
|
||||
import sys
|
||||
from localplatform import (chmod, chown, service_stop, service_start,
|
||||
ON_WINDOWS, get_log_level, get_live_reload,
|
||||
get_server_port, get_server_host, get_chown_plugin_path,
|
||||
get_unprivileged_user, get_unprivileged_path,
|
||||
get_privileged_path)
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
chmod(sys._MEIPASS, 755)
|
||||
# Full imports
|
||||
from asyncio import new_event_loop, set_event_loop, sleep
|
||||
from json import dumps, loads
|
||||
from logging import DEBUG, INFO, basicConfig, getLogger
|
||||
from os import getenv, path
|
||||
from traceback import format_exc
|
||||
import multiprocessing
|
||||
|
||||
import aiohttp_cors
|
||||
# Partial imports
|
||||
from aiohttp import client_exceptions, WSMsgType
|
||||
from aiohttp.web import Application, Response, get, run_app, static
|
||||
from aiohttp_jinja2 import setup as jinja_setup
|
||||
|
||||
# local modules
|
||||
from browser import PluginBrowser
|
||||
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
|
||||
mkdir_as_user, get_system_pythonpaths)
|
||||
|
||||
from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs
|
||||
from loader import Loader
|
||||
from settings import SettingsManager
|
||||
from updater import Updater
|
||||
from utilities import Utilities
|
||||
from customtypes import UserType
|
||||
|
||||
|
||||
basicConfig(
|
||||
level=get_log_level(),
|
||||
format="[%(module)s][%(levelname)s]: %(message)s"
|
||||
)
|
||||
|
||||
logger = getLogger("Main")
|
||||
plugin_path = path.join(get_privileged_path(), "plugins")
|
||||
|
||||
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):
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code")
|
||||
|
||||
if get_chown_plugin_path() == True:
|
||||
chown_plugin_dir()
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, loop) -> None:
|
||||
self.loop = loop
|
||||
self.web_app = Application()
|
||||
self.web_app.middlewares.append(csrf_middleware)
|
||||
self.cors = aiohttp_cors.setup(self.web_app, defaults={
|
||||
"https://steamloopback.host": aiohttp_cors.ResourceOptions(
|
||||
expose_headers="*",
|
||||
allow_headers="*",
|
||||
allow_credentials=True
|
||||
)
|
||||
})
|
||||
self.plugin_loader = Loader(self.web_app, plugin_path, self.loop, get_live_reload())
|
||||
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
|
||||
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
|
||||
self.utilities = Utilities(self)
|
||||
self.updater = Updater(self)
|
||||
|
||||
jinja_setup(self.web_app)
|
||||
|
||||
async def startup(_):
|
||||
if self.settings.getSetting("cef_forward", False):
|
||||
self.loop.create_task(service_start(REMOTE_DEBUGGER_UNIT))
|
||||
else:
|
||||
self.loop.create_task(service_stop(REMOTE_DEBUGGER_UNIT))
|
||||
self.loop.create_task(self.loader_reinjector())
|
||||
self.loop.create_task(self.load_plugins())
|
||||
|
||||
self.web_app.on_startup.append(startup)
|
||||
|
||||
self.loop.set_exception_handler(self.exception_handler)
|
||||
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
|
||||
|
||||
for route in list(self.web_app.router.routes()):
|
||||
self.cors.add(route)
|
||||
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
|
||||
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
|
||||
|
||||
def exception_handler(self, loop, context):
|
||||
if context["message"] == "Unclosed connection":
|
||||
return
|
||||
loop.default_exception_handler(context)
|
||||
|
||||
async def get_auth_token(self, request):
|
||||
return Response(text=get_csrf_token())
|
||||
|
||||
async def load_plugins(self):
|
||||
# await self.wait_for_server()
|
||||
logger.debug("Loading plugins")
|
||||
self.plugin_loader.import_plugins()
|
||||
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
|
||||
if self.settings.getSetting("pluginOrder", None) == None:
|
||||
self.settings.setSetting("pluginOrder", list(self.plugin_loader.plugins.keys()))
|
||||
logger.debug("Did not find pluginOrder setting, set it to default")
|
||||
|
||||
async def loader_reinjector(self):
|
||||
while True:
|
||||
tab = None
|
||||
nf = False
|
||||
dc = False
|
||||
while not tab:
|
||||
try:
|
||||
tab = await get_gamepadui_tab()
|
||||
except (client_exceptions.ClientConnectorError, client_exceptions.ServerDisconnectedError):
|
||||
if not dc:
|
||||
logger.debug("Couldn't connect to debugger, waiting...")
|
||||
dc = True
|
||||
pass
|
||||
except ValueError:
|
||||
if not nf:
|
||||
logger.debug("Couldn't find GamepadUI tab, waiting...")
|
||||
nf = True
|
||||
pass
|
||||
if not tab:
|
||||
await sleep(5)
|
||||
await tab.open_websocket()
|
||||
await tab.enable()
|
||||
await self.inject_javascript(tab, True)
|
||||
try:
|
||||
async for msg in tab.listen_for_message():
|
||||
# this gets spammed a lot
|
||||
if msg.get("method", None) != "Page.navigatedWithinDocument":
|
||||
logger.debug("Page event: " + str(msg.get("method", None)))
|
||||
if msg.get("method", None) == "Page.domContentEventFired":
|
||||
if not await tab.has_global_var("deckyHasLoaded", False):
|
||||
await self.inject_javascript(tab)
|
||||
if msg.get("method", None) == "Inspector.detached":
|
||||
logger.info("CEF has requested that we detach.")
|
||||
await tab.close_websocket()
|
||||
break
|
||||
# If this is a forceful disconnect the loop will just stop without any failure message. In this case, injector.py will handle this for us so we don't need to close the socket.
|
||||
# This is because of https://github.com/aio-libs/aiohttp/blob/3ee7091b40a1bc58a8d7846e7878a77640e96996/aiohttp/client_ws.py#L321
|
||||
logger.info("CEF has disconnected...")
|
||||
# At this point the loop starts again and we connect to the freshly started Steam client once it is ready.
|
||||
except Exception as e:
|
||||
logger.error("Exception while reading page events " + format_exc())
|
||||
await tab.close_websocket()
|
||||
pass
|
||||
# while True:
|
||||
# await sleep(5)
|
||||
# if not await tab.has_global_var("deckyHasLoaded", False):
|
||||
# logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
# await self.inject_javascript(tab)
|
||||
|
||||
async def inject_javascript(self, tab: Tab, first=False, request=None):
|
||||
logger.info("Loading Decky frontend!")
|
||||
try:
|
||||
if first:
|
||||
if await tab.has_global_var("deckyHasLoaded", False):
|
||||
await close_old_tabs()
|
||||
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => location.reload(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}", False, False, False)
|
||||
except:
|
||||
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
return run_app(self.web_app, host=get_server_host(), port=get_server_port(), loop=self.loop, access_log=None)
|
||||
|
||||
# This file is needed to make the relative imports in src/ work properly.
|
||||
if __name__ == "__main__":
|
||||
if ON_WINDOWS:
|
||||
# Fix windows/flask not recognising that .js means 'application/javascript'
|
||||
import mimetypes
|
||||
mimetypes.add_type('application/javascript', '.js')
|
||||
|
||||
# Required for multiprocessing support in frozen files
|
||||
multiprocessing.freeze_support()
|
||||
|
||||
# Append the loader's plugin path to the recognized python paths
|
||||
sys.path.append(path.join(path.dirname(__file__), "plugin"))
|
||||
|
||||
# Append the system and user python paths
|
||||
sys.path.extend(get_system_pythonpaths())
|
||||
|
||||
loop = new_event_loop()
|
||||
set_event_loop(loop)
|
||||
PluginManager(loop).run()
|
||||
from src.main import main
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"strict": ["*"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
aiohttp==3.8.4
|
||||
aiohttp-jinja2==1.5.1
|
||||
aiohttp_cors==0.7.0
|
||||
watchdog==2.1.7
|
||||
certifi==2023.7.22
|
||||
@@ -4,53 +4,70 @@ import json
|
||||
# from pprint import pformat
|
||||
|
||||
# Partial imports
|
||||
from aiohttp import ClientSession, web
|
||||
from asyncio import get_event_loop, sleep
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from aiohttp import ClientSession
|
||||
from asyncio import sleep
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
from logging import getLogger
|
||||
from os import R_OK, W_OK, path, rename, listdir, access, mkdir
|
||||
from os import R_OK, W_OK, path, listdir, access, mkdir
|
||||
from shutil import rmtree
|
||||
from time import time
|
||||
from zipfile import ZipFile
|
||||
from localplatform import chown, chmod
|
||||
from enum import IntEnum
|
||||
from typing import Dict, List, TypedDict
|
||||
|
||||
# Local modules
|
||||
from helpers import get_ssl_context, download_remote_binary_to_path
|
||||
from injector import get_gamepadui_tab
|
||||
from .localplatform import chown, chmod
|
||||
from .loader import Loader, Plugins
|
||||
from .helpers import get_ssl_context, download_remote_binary_to_path
|
||||
from .settings import SettingsManager
|
||||
from .injector import get_gamepadui_tab
|
||||
|
||||
logger = getLogger("Browser")
|
||||
|
||||
class PluginInstallType(IntEnum):
|
||||
INSTALL = 0
|
||||
REINSTALL = 1
|
||||
UPDATE = 2
|
||||
|
||||
class PluginInstallRequest(TypedDict):
|
||||
name: str
|
||||
artifact: str
|
||||
version: str
|
||||
hash: str
|
||||
install_type: PluginInstallType
|
||||
|
||||
class PluginInstallContext:
|
||||
def __init__(self, artifact, name, version, hash) -> None:
|
||||
def __init__(self, artifact: str, name: str, version: str, hash: str) -> None:
|
||||
self.artifact = artifact
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.hash = hash
|
||||
|
||||
class PluginBrowser:
|
||||
def __init__(self, plugin_path, plugins, loader, settings) -> None:
|
||||
def __init__(self, plugin_path: str, plugins: Plugins, loader: Loader, settings: SettingsManager) -> None:
|
||||
self.plugin_path = plugin_path
|
||||
self.plugins = plugins
|
||||
self.loader = loader
|
||||
self.settings = settings
|
||||
self.install_requests = {}
|
||||
self.install_requests: Dict[str, PluginInstallContext | List[PluginInstallContext]] = {}
|
||||
|
||||
def _unzip_to_plugin_dir(self, zip, name, hash):
|
||||
def _unzip_to_plugin_dir(self, zip: BytesIO, name: str, hash: str):
|
||||
zip_hash = sha256(zip.getbuffer()).hexdigest()
|
||||
if hash and (zip_hash != hash):
|
||||
return False
|
||||
zip_file = ZipFile(zip)
|
||||
zip_file.extractall(self.plugin_path)
|
||||
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
|
||||
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):
|
||||
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath: str):
|
||||
rv = False
|
||||
try:
|
||||
packageJsonPath = path.join(pluginBasePath, 'package.json')
|
||||
@@ -91,7 +108,7 @@ class PluginBrowser:
|
||||
return rv
|
||||
|
||||
"""Return the filename (only) for the specified plugin"""
|
||||
def find_plugin_folder(self, name):
|
||||
def find_plugin_folder(self, name: str) -> str | None:
|
||||
for folder in listdir(self.plugin_path):
|
||||
try:
|
||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), "r", encoding="utf-8") as f:
|
||||
@@ -102,11 +119,13 @@ class PluginBrowser:
|
||||
except:
|
||||
logger.debug(f"skipping {folder}")
|
||||
|
||||
async def uninstall_plugin(self, name):
|
||||
async def uninstall_plugin(self, name: str):
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = True
|
||||
tab = await get_gamepadui_tab()
|
||||
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
|
||||
plugin_folder = self.find_plugin_folder(name)
|
||||
assert plugin_folder is not None
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
try:
|
||||
logger.info("uninstalling " + name)
|
||||
logger.info(" at dir " + plugin_dir)
|
||||
@@ -133,12 +152,14 @@ class PluginBrowser:
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
|
||||
async def _install(self, artifact, name, version, hash):
|
||||
async def _install(self, artifact: str, name: str, version: str, hash: str):
|
||||
# Will be set later in code
|
||||
res_zip = None
|
||||
|
||||
# Check if plugin is installed
|
||||
isInstalled = False
|
||||
# Preserve plugin order before removing plugin (uninstall alters the order and removes the plugin from the list)
|
||||
current_plugin_order = self.settings.getSetting("pluginOrder")[:]
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = True
|
||||
try:
|
||||
@@ -183,6 +204,7 @@ class PluginBrowser:
|
||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||
if ret:
|
||||
plugin_folder = self.find_plugin_folder(name)
|
||||
assert plugin_folder is not None
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||
if ret:
|
||||
@@ -191,27 +213,27 @@ class PluginBrowser:
|
||||
self.loader.plugins[name].stop()
|
||||
self.loader.plugins.pop(name, None)
|
||||
await sleep(1)
|
||||
|
||||
current_plugin_order = self.settings.getSetting("pluginOrder")
|
||||
current_plugin_order.append(name)
|
||||
if not isInstalled:
|
||||
current_plugin_order = self.settings.getSetting("pluginOrder")
|
||||
current_plugin_order.append(name)
|
||||
self.settings.setSetting("pluginOrder", current_plugin_order)
|
||||
logger.debug("Plugin %s was added to the pluginOrder setting", name)
|
||||
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
|
||||
else:
|
||||
logger.fatal(f"Failed Downloading Remote Binaries")
|
||||
else:
|
||||
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||
logger.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
|
||||
async def request_plugin_install(self, artifact, name, version, hash, install_type):
|
||||
async def request_plugin_install(self, artifact: str, name: str, version: str, hash: str, install_type: PluginInstallType):
|
||||
request_id = str(time())
|
||||
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.open_websocket()
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}', {install_type})")
|
||||
|
||||
async def request_multiple_plugin_installs(self, requests):
|
||||
async def request_multiple_plugin_installs(self, requests: List[PluginInstallRequest]):
|
||||
request_id = str(time())
|
||||
self.install_requests[request_id] = [PluginInstallContext(req['artifact'], req['name'], req['version'], req['hash']) for req in requests]
|
||||
js_requests_parameter = ','.join([
|
||||
@@ -222,17 +244,17 @@ class PluginBrowser:
|
||||
await tab.open_websocket()
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.addMultiplePluginsInstallPrompt('{request_id}', [{js_requests_parameter}])")
|
||||
|
||||
async def confirm_plugin_install(self, request_id):
|
||||
async def confirm_plugin_install(self, request_id: str):
|
||||
requestOrRequests = self.install_requests.pop(request_id)
|
||||
if isinstance(requestOrRequests, list):
|
||||
[await self._install(req.artifact, req.name, req.version, req.hash) for req in requestOrRequests]
|
||||
else:
|
||||
await self._install(requestOrRequests.artifact, requestOrRequests.name, requestOrRequests.version, requestOrRequests.hash)
|
||||
|
||||
def cancel_plugin_install(self, request_id):
|
||||
def cancel_plugin_install(self, request_id: str):
|
||||
self.install_requests.pop(request_id)
|
||||
|
||||
def cleanup_plugin_settings(self, name):
|
||||
def cleanup_plugin_settings(self, name: str):
|
||||
"""Removes any settings related to a plugin. Propably called when a plugin is uninstalled.
|
||||
|
||||
Args:
|
||||
@@ -2,16 +2,16 @@ import re
|
||||
import ssl
|
||||
import uuid
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
|
||||
import certifi
|
||||
from aiohttp.web import Response, middleware
|
||||
from aiohttp.web import Request, Response, middleware
|
||||
from aiohttp.typedefs import Handler
|
||||
from aiohttp import ClientSession
|
||||
import localplatform
|
||||
from customtypes import UserType
|
||||
from . import localplatform
|
||||
from .customtypes import UserType
|
||||
from logging import getLogger
|
||||
|
||||
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
||||
@@ -31,17 +31,17 @@ def get_csrf_token():
|
||||
return csrf_token
|
||||
|
||||
@middleware
|
||||
async def csrf_middleware(request, handler):
|
||||
async def csrf_middleware(request: Request, handler: Handler):
|
||||
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or str(request.rel_url).startswith("/frontend/") or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
|
||||
return await handler(request)
|
||||
return Response(text='Forbidden', status='403')
|
||||
return Response(text='Forbidden', status=403)
|
||||
|
||||
# Get the default homebrew path unless a home_path is specified. home_path argument is deprecated
|
||||
def get_homebrew_path(home_path = None) -> str:
|
||||
def get_homebrew_path() -> str:
|
||||
return localplatform.get_unprivileged_path()
|
||||
|
||||
# Recursively create path and chown as user
|
||||
def mkdir_as_user(path):
|
||||
def mkdir_as_user(path: str):
|
||||
path = os.path.realpath(path)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
localplatform.chown(path)
|
||||
@@ -57,23 +57,18 @@ def get_loader_version() -> str:
|
||||
|
||||
# returns the appropriate system python paths
|
||||
def get_system_pythonpaths() -> list[str]:
|
||||
extra_args = {}
|
||||
|
||||
if localplatform.ON_LINUX:
|
||||
# run as normal normal user to also include user python paths
|
||||
extra_args["user"] = localplatform.localplatform._get_user_id()
|
||||
extra_args["env"] = {}
|
||||
|
||||
try:
|
||||
# run as normal normal user if on linux to also include user python paths
|
||||
proc = subprocess.run(["python3" if localplatform.ON_LINUX else "python", "-c", "import sys; print('\\n'.join(x for x in sys.path if x))"],
|
||||
capture_output=True, **extra_args)
|
||||
# TODO make this less insane
|
||||
capture_output=True, user=localplatform.localplatform._get_user_id() if localplatform.ON_LINUX else None, env={} if localplatform.ON_LINUX else None) # type: ignore
|
||||
return [x.strip() for x in proc.stdout.decode().strip().split("\n")]
|
||||
except Exception as e:
|
||||
logger.warn(f"Failed to execute get_system_pythonpaths(): {str(e)}")
|
||||
return []
|
||||
|
||||
# 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: str, binHash: str, path: str) -> bool:
|
||||
rv = False
|
||||
try:
|
||||
if os.access(os.path.dirname(path), os.W_OK):
|
||||
@@ -110,46 +105,42 @@ def set_user_group() -> str:
|
||||
|
||||
# Get the user id hosting the plugin loader
|
||||
def get_user_id() -> int:
|
||||
return localplatform.localplatform._get_user_id()
|
||||
return localplatform.localplatform._get_user_id() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the user hosting the plugin loader
|
||||
def get_user() -> str:
|
||||
return localplatform.localplatform._get_user()
|
||||
return localplatform.localplatform._get_user() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the effective user id of the running process
|
||||
def get_effective_user_id() -> int:
|
||||
return localplatform.localplatform._get_effective_user_id()
|
||||
return localplatform.localplatform._get_effective_user_id() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the effective user of the running process
|
||||
def get_effective_user() -> str:
|
||||
return localplatform.localplatform._get_effective_user()
|
||||
return localplatform.localplatform._get_effective_user() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the effective user group id of the running process
|
||||
def get_effective_user_group_id() -> int:
|
||||
return localplatform.localplatform._get_effective_user_group_id()
|
||||
return localplatform.localplatform._get_effective_user_group_id() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the effective user group of the running process
|
||||
def get_effective_user_group() -> str:
|
||||
return localplatform.localplatform._get_effective_user_group()
|
||||
return localplatform.localplatform._get_effective_user_group() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the user owner of the given file path.
|
||||
def get_user_owner(file_path) -> str:
|
||||
return localplatform.localplatform._get_user_owner(file_path)
|
||||
def get_user_owner(file_path: str) -> str:
|
||||
return localplatform.localplatform._get_user_owner(file_path) # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the user group of the given file path.
|
||||
def get_user_group(file_path) -> str:
|
||||
return localplatform.localplatform._get_user_group(file_path)
|
||||
# Get the user group of the given file path, or the user group hosting the plugin loader
|
||||
def get_user_group(file_path: str | None = None) -> str:
|
||||
return localplatform.localplatform._get_user_group(file_path) # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the group id of the user hosting the plugin loader
|
||||
def get_user_group_id() -> int:
|
||||
return localplatform.localplatform._get_user_group_id()
|
||||
|
||||
# Get the group of the user hosting the plugin loader
|
||||
def get_user_group() -> str:
|
||||
return localplatform.localplatform._get_user_group()
|
||||
return localplatform.localplatform._get_user_group_id() # pyright: ignore [reportPrivateUsage]
|
||||
|
||||
# Get the default home path unless a user is specified
|
||||
def get_home_path(username = None) -> str:
|
||||
def get_home_path(username: str | None = None) -> str:
|
||||
return localplatform.get_home_path(UserType.ROOT if username == "root" else UserType.HOST_USER)
|
||||
|
||||
async def is_systemd_unit_active(unit_name: str) -> bool:
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
from asyncio import sleep
|
||||
from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from typing import List
|
||||
from typing import Any, Callable, List, TypedDict, Dict
|
||||
|
||||
from aiohttp import ClientSession, WSMsgType
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
|
||||
from asyncio.exceptions import TimeoutError
|
||||
import uuid
|
||||
@@ -14,35 +13,43 @@ BASE_ADDRESS = "http://localhost:8080"
|
||||
|
||||
logger = getLogger("Injector")
|
||||
|
||||
class _TabResponse(TypedDict):
|
||||
title: str
|
||||
id: str
|
||||
url: str
|
||||
webSocketDebuggerUrl: str
|
||||
|
||||
class Tab:
|
||||
cmd_id = 0
|
||||
|
||||
def __init__(self, res) -> None:
|
||||
self.title = res["title"]
|
||||
self.id = res["id"]
|
||||
self.url = res["url"]
|
||||
self.ws_url = res["webSocketDebuggerUrl"]
|
||||
def __init__(self, res: _TabResponse) -> None:
|
||||
self.title: str = res["title"]
|
||||
self.id: str = res["id"]
|
||||
self.url: str = res["url"]
|
||||
self.ws_url: str = res["webSocketDebuggerUrl"]
|
||||
|
||||
self.websocket = None
|
||||
self.client = None
|
||||
|
||||
async def open_websocket(self):
|
||||
self.client = ClientSession()
|
||||
self.websocket = await self.client.ws_connect(self.ws_url)
|
||||
self.websocket = await self.client.ws_connect(self.ws_url) # type: ignore
|
||||
|
||||
async def close_websocket(self):
|
||||
await self.websocket.close()
|
||||
await self.client.close()
|
||||
if self.websocket:
|
||||
await self.websocket.close()
|
||||
if self.client:
|
||||
await self.client.close()
|
||||
|
||||
async def listen_for_message(self):
|
||||
async for message in self.websocket:
|
||||
data = message.json()
|
||||
yield data
|
||||
logger.warn(f"The Tab {self.title} socket has been disconnected while listening for messages.")
|
||||
await self.close_websocket()
|
||||
if self.websocket:
|
||||
async for message in self.websocket:
|
||||
data = message.json()
|
||||
yield data
|
||||
logger.warn(f"The Tab {self.title} socket has been disconnected while listening for messages.")
|
||||
await self.close_websocket()
|
||||
|
||||
async def _send_devtools_cmd(self, dc, receive=True):
|
||||
async def _send_devtools_cmd(self, dc: Dict[str, Any], receive: bool = True):
|
||||
if self.websocket:
|
||||
self.cmd_id += 1
|
||||
dc["id"] = self.cmd_id
|
||||
@@ -54,7 +61,7 @@ class Tab:
|
||||
return None
|
||||
raise RuntimeError("Websocket not opened")
|
||||
|
||||
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
|
||||
async def evaluate_js(self, js: str, run_async: bool | None = False, manage_socket: bool | None = True, get_result: bool = True):
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
@@ -73,15 +80,16 @@ class Tab:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def has_global_var(self, var_name, manage_socket=True):
|
||||
async def has_global_var(self, var_name: str, manage_socket: bool = True):
|
||||
res = await self.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False, manage_socket)
|
||||
assert res is not None
|
||||
|
||||
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
||||
return False
|
||||
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
async def close(self, manage_socket=True):
|
||||
async def close(self, manage_socket: bool = True):
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
@@ -111,7 +119,7 @@ class Tab:
|
||||
"method": "Page.disable",
|
||||
}, False)
|
||||
|
||||
async def refresh(self, manage_socket=True):
|
||||
async def refresh(self, manage_socket: bool = True):
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
@@ -125,7 +133,7 @@ class Tab:
|
||||
await self.close_websocket()
|
||||
|
||||
return
|
||||
async def reload_and_evaluate(self, js, manage_socket=True):
|
||||
async def reload_and_evaluate(self, js: str, manage_socket: bool = True):
|
||||
"""
|
||||
Reloads the current tab, with JS to run on load via debugger
|
||||
"""
|
||||
@@ -153,11 +161,13 @@ class Tab:
|
||||
}
|
||||
}, True)
|
||||
|
||||
assert breakpoint_res is not None
|
||||
|
||||
logger.info(breakpoint_res)
|
||||
|
||||
# Page finishes loading when breakpoint hits
|
||||
|
||||
for x in range(20):
|
||||
for _ in range(20):
|
||||
# this works around 1/5 of the time, so just send it 8 times.
|
||||
# the js accounts for being injected multiple times allowing only one instance to run at a time anyway
|
||||
await self._send_devtools_cmd({
|
||||
@@ -176,7 +186,7 @@ class Tab:
|
||||
}
|
||||
}, False)
|
||||
|
||||
for x in range(4):
|
||||
for _ in range(4):
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.resume"
|
||||
}, False)
|
||||
@@ -190,7 +200,7 @@ class Tab:
|
||||
await self.close_websocket()
|
||||
return
|
||||
|
||||
async def add_script_to_evaluate_on_new_document(self, js, add_dom_wrapper=True, manage_socket=True, get_result=True):
|
||||
async def add_script_to_evaluate_on_new_document(self, js: str, add_dom_wrapper: bool = True, manage_socket: bool = True, get_result: bool = True):
|
||||
"""
|
||||
How the underlying call functions is not particularly clear from the devtools docs, so stealing puppeteer's description:
|
||||
|
||||
@@ -253,7 +263,7 @@ class Tab:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def remove_script_to_evaluate_on_new_document(self, script_id, manage_socket=True):
|
||||
async def remove_script_to_evaluate_on_new_document(self, script_id: str, manage_socket: bool = True):
|
||||
"""
|
||||
Removes a script from a page that was added with `add_script_to_evaluate_on_new_document`
|
||||
|
||||
@@ -267,7 +277,7 @@ class Tab:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.removeScriptToEvaluateOnNewDocument",
|
||||
"params": {
|
||||
"identifier": script_id
|
||||
@@ -278,15 +288,16 @@ class Tab:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
|
||||
async def has_element(self, element_name, manage_socket=True):
|
||||
async def has_element(self, element_name: str, manage_socket: bool = True):
|
||||
res = await self.evaluate_js(f"document.getElementById('{element_name}') != null", False, manage_socket)
|
||||
assert res is not None
|
||||
|
||||
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
||||
return False
|
||||
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
async def inject_css(self, style, manage_socket=True):
|
||||
async def inject_css(self, style: str, manage_socket: bool = True):
|
||||
try:
|
||||
css_id = str(uuid.uuid4())
|
||||
|
||||
@@ -300,6 +311,8 @@ class Tab:
|
||||
}})()
|
||||
""", False, manage_socket)
|
||||
|
||||
assert result is not None
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
@@ -316,7 +329,7 @@ class Tab:
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def remove_css(self, css_id, manage_socket=True):
|
||||
async def remove_css(self, css_id: str, manage_socket: bool = True):
|
||||
try:
|
||||
result = await self.evaluate_js(
|
||||
f"""
|
||||
@@ -328,6 +341,8 @@ class Tab:
|
||||
}})()
|
||||
""", False, manage_socket)
|
||||
|
||||
assert result is not None
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
@@ -343,8 +358,9 @@ class Tab:
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def get_steam_resource(self, url):
|
||||
async def get_steam_resource(self, url: str):
|
||||
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
|
||||
assert res is not None
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
def __repr__(self):
|
||||
@@ -380,14 +396,14 @@ async def get_tabs() -> List[Tab]:
|
||||
raise Exception(f"/json did not return 200. {await res.text()}")
|
||||
|
||||
|
||||
async def get_tab(tab_name) -> Tab:
|
||||
async def get_tab(tab_name: str) -> Tab:
|
||||
tabs = await get_tabs()
|
||||
tab = next((i for i in tabs if i.title == tab_name), None)
|
||||
if not tab:
|
||||
raise ValueError(f"Tab {tab_name} not found")
|
||||
return tab
|
||||
|
||||
async def get_tab_lambda(test) -> Tab:
|
||||
async def get_tab_lambda(test: Callable[[Tab], bool]) -> Tab:
|
||||
tabs = await get_tabs()
|
||||
tab = next((i for i in tabs if test(i)), None)
|
||||
if not tab:
|
||||
@@ -408,7 +424,7 @@ async def get_gamepadui_tab() -> Tab:
|
||||
raise ValueError(f"GamepadUI Tab not found")
|
||||
return tab
|
||||
|
||||
async def inject_to_tab(tab_name, js, run_async=False):
|
||||
async def inject_to_tab(tab_name: str, js: str, run_async: bool = False):
|
||||
tab = await get_tab(tab_name)
|
||||
|
||||
return await tab.evaluate_js(js, run_async)
|
||||
@@ -1,34 +1,43 @@
|
||||
from asyncio import Queue, sleep
|
||||
from __future__ import annotations
|
||||
from asyncio import AbstractEventLoop, Queue, sleep
|
||||
from json.decoder import JSONDecodeError
|
||||
from logging import getLogger
|
||||
from os import listdir, path
|
||||
from pathlib import Path
|
||||
from traceback import print_exc
|
||||
from typing import Any, Tuple
|
||||
|
||||
from aiohttp import web
|
||||
from os.path import exists
|
||||
from watchdog.events import RegexMatchingEventHandler
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import RegexMatchingEventHandler, DirCreatedEvent, DirModifiedEvent, FileCreatedEvent, FileModifiedEvent # type: ignore
|
||||
from watchdog.observers import Observer # type: ignore
|
||||
|
||||
from injector import get_tab, get_gamepadui_tab
|
||||
from plugin import PluginWrapper
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
|
||||
from .injector import get_tab, get_gamepadui_tab
|
||||
from .plugin import PluginWrapper
|
||||
|
||||
Plugins = dict[str, PluginWrapper]
|
||||
ReloadQueue = Queue[Tuple[str, str, bool | None] | Tuple[str, str]]
|
||||
|
||||
class FileChangeHandler(RegexMatchingEventHandler):
|
||||
def __init__(self, queue, plugin_path) -> None:
|
||||
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$'])
|
||||
def __init__(self, queue: ReloadQueue, plugin_path: str) -> None:
|
||||
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$']) # type: ignore
|
||||
self.logger = getLogger("file-watcher")
|
||||
self.plugin_path = plugin_path
|
||||
self.queue = queue
|
||||
self.disabled = True
|
||||
|
||||
def maybe_reload(self, src_path):
|
||||
def maybe_reload(self, src_path: str):
|
||||
if self.disabled:
|
||||
return
|
||||
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
|
||||
if exists(path.join(self.plugin_path, plugin_dir, "plugin.json")):
|
||||
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
|
||||
|
||||
def on_created(self, event):
|
||||
def on_created(self, event: DirCreatedEvent | FileCreatedEvent):
|
||||
src_path = event.src_path
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
@@ -42,7 +51,7 @@ class FileChangeHandler(RegexMatchingEventHandler):
|
||||
self.logger.debug(f"file created: {src_path}")
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
def on_modified(self, event):
|
||||
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent):
|
||||
src_path = event.src_path
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
@@ -57,25 +66,25 @@ class FileChangeHandler(RegexMatchingEventHandler):
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
class Loader:
|
||||
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
|
||||
def __init__(self, server_instance: PluginManager, plugin_path: str, loop: AbstractEventLoop, live_reload: bool = False) -> None:
|
||||
self.loop = loop
|
||||
self.logger = getLogger("Loader")
|
||||
self.plugin_path = plugin_path
|
||||
self.logger.info(f"plugin_path: {self.plugin_path}")
|
||||
self.plugins : dict[str, PluginWrapper] = {}
|
||||
self.plugins: Plugins = {}
|
||||
self.watcher = None
|
||||
self.live_reload = live_reload
|
||||
self.reload_queue = Queue()
|
||||
self.reload_queue: ReloadQueue = Queue()
|
||||
self.loop.create_task(self.handle_reloads())
|
||||
|
||||
if live_reload:
|
||||
self.observer = Observer()
|
||||
self.watcher = FileChangeHandler(self.reload_queue, plugin_path)
|
||||
self.observer.schedule(self.watcher, self.plugin_path, recursive=True)
|
||||
self.observer.schedule(self.watcher, self.plugin_path, recursive=True) # type: ignore
|
||||
self.observer.start()
|
||||
self.loop.create_task(self.enable_reload_wait())
|
||||
|
||||
server_instance.add_routes([
|
||||
server_instance.web_app.add_routes([
|
||||
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
||||
web.get("/locales/{path:.*}", self.handle_frontend_locales),
|
||||
web.get("/plugins", self.get_plugins),
|
||||
@@ -93,40 +102,41 @@ class Loader:
|
||||
async def enable_reload_wait(self):
|
||||
if self.live_reload:
|
||||
await sleep(10)
|
||||
self.logger.info("Hot reload enabled")
|
||||
self.watcher.disabled = False
|
||||
if self.watcher:
|
||||
self.logger.info("Hot reload enabled")
|
||||
self.watcher.disabled = False
|
||||
|
||||
async def handle_frontend_assets(self, request):
|
||||
file = path.join(path.dirname(__file__), "static", request.match_info["path"])
|
||||
async def handle_frontend_assets(self, request: web.Request):
|
||||
file = path.join(path.dirname(__file__), "..", "static", request.match_info["path"])
|
||||
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def handle_frontend_locales(self, request):
|
||||
async def handle_frontend_locales(self, request: web.Request):
|
||||
req_lang = request.match_info["path"]
|
||||
file = path.join(path.dirname(__file__), "locales", req_lang)
|
||||
file = path.join(path.dirname(__file__), "..", "locales", req_lang)
|
||||
if exists(file):
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache", "Content-Type": "application/json"})
|
||||
else:
|
||||
self.logger.info(f"Language {req_lang} not available, returning an empty dictionary")
|
||||
return web.json_response(data={}, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def get_plugins(self, request):
|
||||
async def get_plugins(self, request: web.Request):
|
||||
plugins = list(self.plugins.values())
|
||||
return web.json_response([{"name": str(i) if not i.legacy else "$LEGACY_"+str(i), "version": i.version} for i in plugins])
|
||||
|
||||
def handle_plugin_frontend_assets(self, request):
|
||||
async def handle_plugin_frontend_assets(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
file = path.join(self.plugin_path, plugin.plugin_directory, "dist/assets", request.match_info["path"])
|
||||
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
def handle_frontend_bundle(self, request):
|
||||
async def handle_frontend_bundle(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), "r", encoding="utf-8") as bundle:
|
||||
return web.Response(text=bundle.read(), content_type="application/javascript")
|
||||
|
||||
def import_plugin(self, file, plugin_directory, refresh=False, batch=False):
|
||||
def import_plugin(self, file: str, plugin_directory: str, refresh: bool | None = False, batch: bool | None = False):
|
||||
try:
|
||||
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
|
||||
if plugin.name in self.plugins:
|
||||
@@ -146,7 +156,7 @@ class Loader:
|
||||
self.logger.error(f"Could not load {file}. {e}")
|
||||
print_exc()
|
||||
|
||||
async def dispatch_plugin(self, name, version):
|
||||
async def dispatch_plugin(self, name: str, version: str | None):
|
||||
gpui_tab = await get_gamepadui_tab()
|
||||
await gpui_tab.evaluate_js(f"window.importDeckyPlugin('{name}', '{version}')")
|
||||
|
||||
@@ -161,15 +171,15 @@ class Loader:
|
||||
async def handle_reloads(self):
|
||||
while True:
|
||||
args = await self.reload_queue.get()
|
||||
self.import_plugin(*args)
|
||||
self.import_plugin(*args) # type: ignore
|
||||
|
||||
async def handle_plugin_method_call(self, request):
|
||||
async def handle_plugin_method_call(self, request: web.Request):
|
||||
res = {}
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
method_name = request.match_info["method_name"]
|
||||
try:
|
||||
method_info = await request.json()
|
||||
args = method_info["args"]
|
||||
args: Any = method_info["args"]
|
||||
except JSONDecodeError:
|
||||
args = {}
|
||||
try:
|
||||
@@ -189,7 +199,7 @@ class Loader:
|
||||
can introduce it more smoothly and give people the chance to sample the new features even
|
||||
without plugin support. They will be removed once legacy plugins are no longer relevant.
|
||||
"""
|
||||
async def load_plugin_main_view(self, request):
|
||||
async def load_plugin_main_view(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["name"]]
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), "r", encoding="utf-8") as template:
|
||||
template_data = template.read()
|
||||
@@ -201,7 +211,7 @@ class Loader:
|
||||
"""
|
||||
return web.Response(text=ret, content_type="text/html")
|
||||
|
||||
async def handle_sub_route(self, request):
|
||||
async def handle_sub_route(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["name"]]
|
||||
route_path = request.match_info["path"]
|
||||
self.logger.info(path)
|
||||
@@ -212,14 +222,14 @@ class Loader:
|
||||
|
||||
return web.Response(text=ret)
|
||||
|
||||
async def get_steam_resource(self, request):
|
||||
async def get_steam_resource(self, request: web.Request):
|
||||
tab = await get_tab("SP")
|
||||
try:
|
||||
return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html")
|
||||
except Exception as e:
|
||||
return web.Response(text=str(e), status=400)
|
||||
|
||||
async def handle_backend_reload_request(self, request):
|
||||
async def handle_backend_reload_request(self, request: web.Request):
|
||||
plugin_name : str = request.match_info["plugin_name"]
|
||||
plugin = self.plugins[plugin_name]
|
||||
|
||||
@@ -4,11 +4,11 @@ ON_WINDOWS = platform.system() == "Windows"
|
||||
ON_LINUX = not ON_WINDOWS
|
||||
|
||||
if ON_WINDOWS:
|
||||
from localplatformwin import *
|
||||
import localplatformwin as localplatform
|
||||
from .localplatformwin import *
|
||||
from . import localplatformwin as localplatform
|
||||
else:
|
||||
from localplatformlinux import *
|
||||
import localplatformlinux as localplatform
|
||||
from .localplatformlinux import *
|
||||
from . import localplatformlinux as localplatform
|
||||
|
||||
def get_privileged_path() -> str:
|
||||
'''Get path accessible by elevated user. Holds plugins, decky loader and decky loader configs'''
|
||||
@@ -40,4 +40,13 @@ def get_keep_systemd_service() -> bool:
|
||||
def get_log_level() -> int:
|
||||
return {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
|
||||
os.getenv("LOG_LEVEL", "INFO")
|
||||
]
|
||||
]
|
||||
|
||||
def get_selinux() -> bool:
|
||||
if ON_LINUX:
|
||||
from subprocess import check_output
|
||||
try:
|
||||
if (check_output("getenforce").decode("ascii").strip("\n") == "Enforcing"): return True
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return False
|
||||
@@ -1,6 +1,6 @@
|
||||
import os, pwd, grp, sys, logging
|
||||
from subprocess import call, run, DEVNULL, PIPE, STDOUT
|
||||
from customtypes import UserType
|
||||
from .customtypes import UserType
|
||||
|
||||
logger = logging.getLogger("localplatform")
|
||||
|
||||
@@ -29,21 +29,17 @@ 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) -> str:
|
||||
return pwd.getpwuid(os.stat(file_path).st_uid).pw_name
|
||||
|
||||
# Get the user group of the given file path.
|
||||
def _get_user_group(file_path) -> str:
|
||||
return grp.getgrgid(os.stat(file_path).st_gid).gr_name
|
||||
# Get the user group of the given file path, or the user group hosting the plugin loader
|
||||
def _get_user_group(file_path: str | None = None) -> str:
|
||||
return grp.getgrgid(os.stat(file_path).st_gid if file_path is not None else _get_user_group_id()).gr_name
|
||||
|
||||
# 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:
|
||||
return grp.getgrgid(_get_user_group_id()).gr_name
|
||||
|
||||
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
|
||||
user_str = ""
|
||||
|
||||
@@ -60,6 +56,8 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
|
||||
return result == 0
|
||||
|
||||
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
|
||||
if _get_effective_user_id() != 0:
|
||||
return True
|
||||
result = call(["chmod", "-R", str(permissions), path] if recursive else ["chmod", str(permissions), path])
|
||||
return result == 0
|
||||
|
||||
@@ -144,7 +142,7 @@ def get_privileged_path() -> str:
|
||||
|
||||
return path
|
||||
|
||||
def _parent_dir(path : str) -> str:
|
||||
def _parent_dir(path : str | None) -> str | None:
|
||||
if path == None:
|
||||
return None
|
||||
|
||||
@@ -164,7 +162,7 @@ def get_unprivileged_path() -> str:
|
||||
# Expected path of loader binary is /home/deck/homebrew/service/PluginLoader
|
||||
path = _parent_dir(_parent_dir(os.path.realpath(sys.argv[0])))
|
||||
|
||||
if not os.path.exists(path):
|
||||
if path != None and not os.path.exists(path):
|
||||
path = None
|
||||
|
||||
if path == None:
|
||||
@@ -191,4 +189,4 @@ def get_unprivileged_user() -> str:
|
||||
logger.warn("Unprivileged user is not properly configured. Defaulting to 'deck'")
|
||||
user = 'deck'
|
||||
|
||||
return user
|
||||
return user
|
||||
@@ -1,4 +1,4 @@
|
||||
from customtypes import UserType
|
||||
from .customtypes import UserType
|
||||
import os, sys
|
||||
|
||||
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
|
||||
@@ -1,10 +1,13 @@
|
||||
import asyncio, time, random
|
||||
from localplatform import ON_WINDOWS
|
||||
import asyncio, time
|
||||
from typing import Awaitable, Callable
|
||||
import random
|
||||
|
||||
from .localplatform import ON_WINDOWS
|
||||
|
||||
BUFFER_LIMIT = 2 ** 20 # 1 MiB
|
||||
|
||||
class UnixSocket:
|
||||
def __init__(self, on_new_message):
|
||||
def __init__(self, on_new_message: Callable[[str], Awaitable[str|None]]):
|
||||
'''
|
||||
on_new_message takes 1 string argument.
|
||||
It's return value gets used, if not None, to write data to the socket.
|
||||
@@ -46,28 +49,32 @@ class UnixSocket:
|
||||
self.reader = None
|
||||
|
||||
async def read_single_line(self) -> str|None:
|
||||
reader, writer = await self.get_socket_connection()
|
||||
reader, _ = await self.get_socket_connection()
|
||||
|
||||
if self.reader == None:
|
||||
return None
|
||||
try:
|
||||
assert reader
|
||||
except AssertionError:
|
||||
return
|
||||
|
||||
return await self._read_single_line(reader)
|
||||
|
||||
async def write_single_line(self, message : str):
|
||||
reader, writer = await self.get_socket_connection()
|
||||
_, writer = await self.get_socket_connection()
|
||||
|
||||
if self.writer == None:
|
||||
return;
|
||||
try:
|
||||
assert writer
|
||||
except AssertionError:
|
||||
return
|
||||
|
||||
await self._write_single_line(writer, message)
|
||||
|
||||
async def _read_single_line(self, reader) -> str:
|
||||
async def _read_single_line(self, reader: asyncio.StreamReader) -> str:
|
||||
line = bytearray()
|
||||
while True:
|
||||
try:
|
||||
line.extend(await reader.readuntil())
|
||||
except asyncio.LimitOverrunError:
|
||||
line.extend(await reader.read(reader._limit))
|
||||
line.extend(await reader.read(reader._limit)) # type: ignore
|
||||
continue
|
||||
except asyncio.IncompleteReadError as err:
|
||||
line.extend(err.partial)
|
||||
@@ -77,27 +84,27 @@ class UnixSocket:
|
||||
|
||||
return line.decode("utf-8")
|
||||
|
||||
async def _write_single_line(self, writer, message : str):
|
||||
async def _write_single_line(self, writer: asyncio.StreamWriter, message : str):
|
||||
if not message.endswith("\n"):
|
||||
message += "\n"
|
||||
|
||||
writer.write(message.encode("utf-8"))
|
||||
await writer.drain()
|
||||
|
||||
async def _listen_for_method_call(self, reader, writer):
|
||||
async def _listen_for_method_call(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||
while True:
|
||||
line = await self._read_single_line(reader)
|
||||
|
||||
try:
|
||||
res = await self.on_new_message(line)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if res != None:
|
||||
await self._write_single_line(writer, res)
|
||||
|
||||
class PortSocket (UnixSocket):
|
||||
def __init__(self, on_new_message):
|
||||
def __init__(self, on_new_message: Callable[[str], Awaitable[str|None]]):
|
||||
'''
|
||||
on_new_message takes 1 string argument.
|
||||
It's return value gets used, if not None, to write data to the socket.
|
||||
@@ -125,7 +132,7 @@ class PortSocket (UnixSocket):
|
||||
return True
|
||||
|
||||
if ON_WINDOWS:
|
||||
class LocalSocket (PortSocket):
|
||||
class LocalSocket (PortSocket): # type: ignore
|
||||
pass
|
||||
else:
|
||||
class LocalSocket (UnixSocket):
|
||||
@@ -0,0 +1,192 @@
|
||||
# Change PyInstaller files permissions
|
||||
import sys
|
||||
from typing import Dict
|
||||
from .localplatform import (chmod, chown, service_stop, service_start,
|
||||
ON_WINDOWS, get_log_level, get_live_reload,
|
||||
get_server_port, get_server_host, get_chown_plugin_path,
|
||||
get_privileged_path)
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
chmod(sys._MEIPASS, 755) # type: ignore
|
||||
# Full imports
|
||||
from asyncio import AbstractEventLoop, new_event_loop, set_event_loop, sleep
|
||||
from logging import basicConfig, getLogger
|
||||
from os import path
|
||||
from traceback import format_exc
|
||||
import multiprocessing
|
||||
|
||||
import aiohttp_cors # type: ignore
|
||||
# Partial imports
|
||||
from aiohttp import client_exceptions
|
||||
from aiohttp.web import Application, Response, Request, get, run_app, static # type: ignore
|
||||
from aiohttp_jinja2 import setup as jinja_setup
|
||||
|
||||
# local modules
|
||||
from .browser import PluginBrowser
|
||||
from .helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
|
||||
mkdir_as_user, get_system_pythonpaths, get_effective_user_id)
|
||||
|
||||
from .injector import get_gamepadui_tab, Tab, close_old_tabs
|
||||
from .loader import Loader
|
||||
from .settings import SettingsManager
|
||||
from .updater import Updater
|
||||
from .utilities import Utilities
|
||||
from .customtypes import UserType
|
||||
|
||||
|
||||
basicConfig(
|
||||
level=get_log_level(),
|
||||
format="[%(module)s][%(levelname)s]: %(message)s"
|
||||
)
|
||||
|
||||
logger = getLogger("Main")
|
||||
plugin_path = path.join(get_privileged_path(), "plugins")
|
||||
|
||||
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):
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code")
|
||||
|
||||
if get_chown_plugin_path() == True:
|
||||
chown_plugin_dir()
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, loop: AbstractEventLoop) -> None:
|
||||
self.loop = loop
|
||||
self.web_app = Application()
|
||||
self.web_app.middlewares.append(csrf_middleware)
|
||||
self.cors = aiohttp_cors.setup(self.web_app, defaults={
|
||||
"https://steamloopback.host": aiohttp_cors.ResourceOptions(
|
||||
expose_headers="*",
|
||||
allow_headers="*",
|
||||
allow_credentials=True
|
||||
)
|
||||
})
|
||||
self.plugin_loader = Loader(self, plugin_path, self.loop, get_live_reload())
|
||||
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
|
||||
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
|
||||
self.utilities = Utilities(self)
|
||||
self.updater = Updater(self)
|
||||
|
||||
jinja_setup(self.web_app)
|
||||
|
||||
async def startup(_: Application):
|
||||
if self.settings.getSetting("cef_forward", False):
|
||||
self.loop.create_task(service_start(REMOTE_DEBUGGER_UNIT))
|
||||
else:
|
||||
self.loop.create_task(service_stop(REMOTE_DEBUGGER_UNIT))
|
||||
self.loop.create_task(self.loader_reinjector())
|
||||
self.loop.create_task(self.load_plugins())
|
||||
|
||||
self.web_app.on_startup.append(startup)
|
||||
|
||||
self.loop.set_exception_handler(self.exception_handler)
|
||||
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
|
||||
|
||||
for route in list(self.web_app.router.routes()):
|
||||
self.cors.add(route) # type: ignore
|
||||
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), '..', 'static'))])
|
||||
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
|
||||
|
||||
def exception_handler(self, loop: AbstractEventLoop, context: Dict[str, str]):
|
||||
if context["message"] == "Unclosed connection":
|
||||
return
|
||||
loop.default_exception_handler(context)
|
||||
|
||||
async def get_auth_token(self, request: Request):
|
||||
return Response(text=get_csrf_token())
|
||||
|
||||
async def load_plugins(self):
|
||||
# await self.wait_for_server()
|
||||
logger.debug("Loading plugins")
|
||||
self.plugin_loader.import_plugins()
|
||||
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
|
||||
if self.settings.getSetting("pluginOrder", None) == None:
|
||||
self.settings.setSetting("pluginOrder", list(self.plugin_loader.plugins.keys()))
|
||||
logger.debug("Did not find pluginOrder setting, set it to default")
|
||||
|
||||
async def loader_reinjector(self):
|
||||
while True:
|
||||
tab = None
|
||||
nf = False
|
||||
dc = False
|
||||
while not tab:
|
||||
try:
|
||||
tab = await get_gamepadui_tab()
|
||||
except (client_exceptions.ClientConnectorError, client_exceptions.ServerDisconnectedError):
|
||||
if not dc:
|
||||
logger.debug("Couldn't connect to debugger, waiting...")
|
||||
dc = True
|
||||
pass
|
||||
except ValueError:
|
||||
if not nf:
|
||||
logger.debug("Couldn't find GamepadUI tab, waiting...")
|
||||
nf = True
|
||||
pass
|
||||
if not tab:
|
||||
await sleep(5)
|
||||
await tab.open_websocket()
|
||||
await tab.enable()
|
||||
await self.inject_javascript(tab, True)
|
||||
try:
|
||||
async for msg in tab.listen_for_message():
|
||||
# this gets spammed a lot
|
||||
if msg.get("method", None) != "Page.navigatedWithinDocument":
|
||||
logger.debug("Page event: " + str(msg.get("method", None)))
|
||||
if msg.get("method", None) == "Page.domContentEventFired":
|
||||
if not await tab.has_global_var("deckyHasLoaded", False):
|
||||
await self.inject_javascript(tab)
|
||||
if msg.get("method", None) == "Inspector.detached":
|
||||
logger.info("CEF has requested that we detach.")
|
||||
await tab.close_websocket()
|
||||
break
|
||||
# If this is a forceful disconnect the loop will just stop without any failure message. In this case, injector.py will handle this for us so we don't need to close the socket.
|
||||
# This is because of https://github.com/aio-libs/aiohttp/blob/3ee7091b40a1bc58a8d7846e7878a77640e96996/aiohttp/client_ws.py#L321
|
||||
logger.info("CEF has disconnected...")
|
||||
# At this point the loop starts again and we connect to the freshly started Steam client once it is ready.
|
||||
except Exception:
|
||||
logger.error("Exception while reading page events " + format_exc())
|
||||
await tab.close_websocket()
|
||||
pass
|
||||
# while True:
|
||||
# await sleep(5)
|
||||
# if not await tab.has_global_var("deckyHasLoaded", False):
|
||||
# logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
# await self.inject_javascript(tab)
|
||||
|
||||
async def inject_javascript(self, tab: Tab, first: bool=False, request: Request|None=None):
|
||||
logger.info("Loading Decky frontend!")
|
||||
try:
|
||||
if first:
|
||||
if await tab.has_global_var("deckyHasLoaded", False):
|
||||
await close_old_tabs()
|
||||
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => location.reload(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}", False, False, False)
|
||||
except:
|
||||
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
return run_app(self.web_app, host=get_server_host(), port=get_server_port(), loop=self.loop, access_log=None)
|
||||
|
||||
def main():
|
||||
if ON_WINDOWS:
|
||||
# Fix windows/flask not recognising that .js means 'application/javascript'
|
||||
import mimetypes
|
||||
mimetypes.add_type('application/javascript', '.js')
|
||||
|
||||
# Required for multiprocessing support in frozen files
|
||||
multiprocessing.freeze_support()
|
||||
else:
|
||||
if get_effective_user_id() != 0:
|
||||
logger.warning(f"decky is running as an unprivileged user, this is not officially supported and may cause issues")
|
||||
|
||||
# Append the loader's plugin path to the recognized python paths
|
||||
sys.path.append(path.join(path.dirname(__file__), "plugin"))
|
||||
|
||||
# Append the system and user python paths
|
||||
sys.path.extend(get_system_pythonpaths())
|
||||
|
||||
loop = new_event_loop()
|
||||
set_event_loop(loop)
|
||||
PluginManager(loop).run()
|
||||
@@ -1,7 +1,6 @@
|
||||
import multiprocessing
|
||||
from asyncio import (Lock, get_event_loop, new_event_loop,
|
||||
set_event_loop, sleep)
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from json import dumps, load, loads
|
||||
from logging import getLogger
|
||||
@@ -9,19 +8,19 @@ from traceback import format_exc
|
||||
from os import path, environ
|
||||
from signal import SIGINT, signal
|
||||
from sys import exit, path as syspath
|
||||
from time import time
|
||||
from localsocket import LocalSocket
|
||||
from localplatform import setgid, setuid, get_username, get_home_path
|
||||
from customtypes import UserType
|
||||
import helpers
|
||||
from typing import Any, Dict
|
||||
from .localsocket import LocalSocket
|
||||
from .localplatform import setgid, setuid, get_username, get_home_path
|
||||
from .customtypes import UserType
|
||||
from . import helpers
|
||||
|
||||
class PluginWrapper:
|
||||
def __init__(self, file, plugin_directory, plugin_path) -> None:
|
||||
def __init__(self, file: str, plugin_directory: str, plugin_path: str) -> None:
|
||||
self.file = file
|
||||
self.plugin_path = plugin_path
|
||||
self.plugin_directory = plugin_directory
|
||||
self.method_call_lock = Lock()
|
||||
self.socket = LocalSocket(self._on_new_message)
|
||||
self.socket: LocalSocket = LocalSocket(self._on_new_message)
|
||||
|
||||
self.version = None
|
||||
|
||||
@@ -73,14 +72,17 @@ class PluginWrapper:
|
||||
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
|
||||
if self.version:
|
||||
environ["DECKY_PLUGIN_VERSION"] = self.version
|
||||
environ["DECKY_PLUGIN_AUTHOR"] = self.author
|
||||
|
||||
# append the plugin's `py_modules` to the recognized python paths
|
||||
syspath.append(path.join(environ["DECKY_PLUGIN_DIR"], "py_modules"))
|
||||
|
||||
spec = spec_from_file_location("_", self.file)
|
||||
assert spec is not None
|
||||
module = module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
self.Plugin = module.Plugin
|
||||
|
||||
@@ -118,7 +120,8 @@ class PluginWrapper:
|
||||
get_event_loop().close()
|
||||
raise Exception("Closing message listener")
|
||||
|
||||
d = {"res": None, "success": True}
|
||||
# TODO there is definitely a better way to type this
|
||||
d: Dict[str, Any] = {"res": None, "success": True}
|
||||
try:
|
||||
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"])
|
||||
except Exception as e:
|
||||
@@ -137,17 +140,18 @@ class PluginWrapper:
|
||||
if self.passive:
|
||||
return
|
||||
|
||||
async def _(self):
|
||||
async def _(self: PluginWrapper):
|
||||
await self.socket.write_single_line(dumps({ "stop": True }, ensure_ascii=False))
|
||||
await self.socket.close_socket_connection()
|
||||
|
||||
get_event_loop().create_task(_(self))
|
||||
|
||||
async def execute_method(self, method_name, kwargs):
|
||||
async def execute_method(self, method_name: str, kwargs: Dict[Any, Any]):
|
||||
if self.passive:
|
||||
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
|
||||
async with self.method_call_lock:
|
||||
reader, writer = await self.socket.get_socket_connection()
|
||||
# reader, writer =
|
||||
await self.socket.get_socket_connection()
|
||||
|
||||
await self.socket.write_single_line(dumps({ "method": method_name, "args": kwargs }, ensure_ascii=False))
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from json import dump, load
|
||||
from os import mkdir, path, listdir, rename
|
||||
from localplatform import chown, folder_owner, get_chown_plugin_path
|
||||
from customtypes import UserType
|
||||
from typing import Any, Dict
|
||||
from .localplatform import chown, folder_owner, get_chown_plugin_path
|
||||
from .customtypes import UserType
|
||||
|
||||
from helpers import get_homebrew_path
|
||||
from .helpers import get_homebrew_path
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
def __init__(self, name, settings_directory = None) -> None:
|
||||
def __init__(self, name: str, settings_directory: str | None = None) -> None:
|
||||
wrong_dir = get_homebrew_path()
|
||||
if settings_directory == None:
|
||||
settings_directory = path.join(wrong_dir, "settings")
|
||||
@@ -31,11 +32,11 @@ class SettingsManager:
|
||||
if folder_owner(settings_directory) != expected_user:
|
||||
chown(settings_directory, expected_user, False)
|
||||
|
||||
self.settings = {}
|
||||
self.settings: Dict[str, Any] = {}
|
||||
|
||||
try:
|
||||
open(self.path, "x", encoding="utf-8")
|
||||
except FileExistsError as e:
|
||||
except FileExistsError as _:
|
||||
self.read()
|
||||
pass
|
||||
|
||||
@@ -51,9 +52,9 @@ class SettingsManager:
|
||||
with open(self.path, "w+", encoding="utf-8") as file:
|
||||
dump(self.settings, file, indent=4, ensure_ascii=False)
|
||||
|
||||
def getSetting(self, key, default=None):
|
||||
def getSetting(self, key: str, default: Any = None) -> Any:
|
||||
return self.settings.get(key, default)
|
||||
|
||||
def setSetting(self, key, value):
|
||||
def setSetting(self, key: str, value: Any) -> Any:
|
||||
self.settings[key] = value
|
||||
self.commit()
|
||||
@@ -1,23 +1,33 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from asyncio import sleep
|
||||
from ensurepip import version
|
||||
from json.decoder import JSONDecodeError
|
||||
from logging import getLogger
|
||||
from os import getcwd, path, remove
|
||||
from localplatform import chmod, service_restart, ON_LINUX, get_keep_systemd_service
|
||||
from typing import TYPE_CHECKING, List, TypedDict
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
from .localplatform import chmod, service_restart, ON_LINUX, get_keep_systemd_service, get_selinux
|
||||
|
||||
from aiohttp import ClientSession, web
|
||||
|
||||
import helpers
|
||||
from injector import get_gamepadui_tab, inject_to_tab
|
||||
from settings import SettingsManager
|
||||
from . import helpers
|
||||
from .injector import get_gamepadui_tab
|
||||
from .settings import SettingsManager
|
||||
|
||||
logger = getLogger("Updater")
|
||||
|
||||
class RemoteVerAsset(TypedDict):
|
||||
name: str
|
||||
browser_download_url: str
|
||||
class RemoteVer(TypedDict):
|
||||
tag_name: str
|
||||
prerelease: bool
|
||||
assets: List[RemoteVerAsset]
|
||||
|
||||
class Updater:
|
||||
def __init__(self, context) -> None:
|
||||
def __init__(self, context: PluginManager) -> None:
|
||||
self.context = context
|
||||
self.settings = self.context.settings
|
||||
# Exposes updater methods to frontend
|
||||
@@ -28,8 +38,8 @@ class Updater:
|
||||
"do_restart": self.do_restart,
|
||||
"check_for_updates": self.check_for_updates
|
||||
}
|
||||
self.remoteVer = None
|
||||
self.allRemoteVers = None
|
||||
self.remoteVer: RemoteVer | None = None
|
||||
self.allRemoteVers: List[RemoteVer] = []
|
||||
self.localVer = helpers.get_loader_version()
|
||||
|
||||
try:
|
||||
@@ -44,7 +54,7 @@ class Updater:
|
||||
])
|
||||
context.loop.create_task(self.version_reloader())
|
||||
|
||||
async def _handle_server_method_call(self, request):
|
||||
async def _handle_server_method_call(self, request: web.Request):
|
||||
method_name = request.match_info["method_name"]
|
||||
try:
|
||||
args = await request.json()
|
||||
@@ -52,7 +62,7 @@ class Updater:
|
||||
args = {}
|
||||
res = {}
|
||||
try:
|
||||
r = await self.updater_methods[method_name](**args)
|
||||
r = await self.updater_methods[method_name](**args) # type: ignore
|
||||
res["result"] = r
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
@@ -105,7 +115,7 @@ class Updater:
|
||||
selectedBranch = self.get_branch(self.context.settings)
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
|
||||
remoteVersions = await res.json()
|
||||
remoteVersions: List[RemoteVer] = await res.json()
|
||||
if selectedBranch == 0:
|
||||
logger.debug("release type: release")
|
||||
remoteVersions = list(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and not ver["tag_name"].find("-pre") > 0 and ver["tag_name"], remoteVersions))
|
||||
@@ -142,6 +152,12 @@ class Updater:
|
||||
|
||||
async def do_update(self):
|
||||
logger.debug("Starting update.")
|
||||
try:
|
||||
assert self.remoteVer
|
||||
except AssertionError:
|
||||
logger.error("Unable to update as remoteVer is missing")
|
||||
return
|
||||
|
||||
version = self.remoteVer["tag_name"]
|
||||
download_url = None
|
||||
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
|
||||
@@ -208,6 +224,10 @@ class Updater:
|
||||
remove(path.join(getcwd(), download_filename))
|
||||
shutil.move(path.join(getcwd(), download_temp_filename), path.join(getcwd(), download_filename))
|
||||
chmod(path.join(getcwd(), download_filename), 777, False)
|
||||
if get_selinux():
|
||||
from asyncio.subprocess import create_subprocess_exec
|
||||
process = await create_subprocess_exec("chcon", "-t", "bin_t", path.join(getcwd(), download_filename))
|
||||
logger.info(f"Setting the executable flag with chcon returned {await process.wait()}")
|
||||
|
||||
logger.info("Updated loader installation.")
|
||||
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
|
||||
@@ -1,26 +1,36 @@
|
||||
from __future__ import annotations
|
||||
from os import stat_result
|
||||
import uuid
|
||||
import os
|
||||
from json.decoder import JSONDecodeError
|
||||
from os.path import splitext
|
||||
import re
|
||||
from traceback import format_exc
|
||||
from stat import FILE_ATTRIBUTE_HIDDEN
|
||||
from stat import FILE_ATTRIBUTE_HIDDEN # type: ignore
|
||||
|
||||
from asyncio import sleep, start_server, gather, open_connection
|
||||
from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection
|
||||
from aiohttp import ClientSession, web
|
||||
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
|
||||
|
||||
from logging import getLogger
|
||||
from injector import inject_to_tab, get_gamepadui_tab, close_old_tabs, get_tab
|
||||
from pathlib import Path
|
||||
from localplatform import ON_WINDOWS
|
||||
import helpers
|
||||
import subprocess
|
||||
from localplatform import service_stop, service_start, get_home_path, get_username
|
||||
|
||||
from .browser import PluginInstallRequest, PluginInstallType
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
from .injector import inject_to_tab, get_gamepadui_tab, close_old_tabs, get_tab
|
||||
from .localplatform import ON_WINDOWS
|
||||
from . import helpers
|
||||
from .localplatform import service_stop, service_start, get_home_path, get_username
|
||||
|
||||
class FilePickerObj(TypedDict):
|
||||
file: Path
|
||||
filest: stat_result
|
||||
is_dir: bool
|
||||
|
||||
class Utilities:
|
||||
def __init__(self, context) -> None:
|
||||
def __init__(self, context: PluginManager) -> None:
|
||||
self.context = context
|
||||
self.util_methods = {
|
||||
self.util_methods: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {
|
||||
"ping": self.ping,
|
||||
"http_request": self.http_request,
|
||||
"install_plugin": self.install_plugin,
|
||||
@@ -53,7 +63,7 @@ class Utilities:
|
||||
web.post("/methods/{method_name}", self._handle_server_method_call)
|
||||
])
|
||||
|
||||
async def _handle_server_method_call(self, request):
|
||||
async def _handle_server_method_call(self, request: web.Request):
|
||||
method_name = request.match_info["method_name"]
|
||||
try:
|
||||
args = await request.json()
|
||||
@@ -69,7 +79,7 @@ class Utilities:
|
||||
res["success"] = False
|
||||
return web.json_response(res)
|
||||
|
||||
async def install_plugin(self, artifact="", name="No name", version="dev", hash=False, install_type=0):
|
||||
async def install_plugin(self, artifact: str="", name: str="No name", version: str="dev", hash: str="", install_type: PluginInstallType=PluginInstallType.INSTALL):
|
||||
return await self.context.plugin_browser.request_plugin_install(
|
||||
artifact=artifact,
|
||||
name=name,
|
||||
@@ -78,21 +88,21 @@ class Utilities:
|
||||
install_type=install_type
|
||||
)
|
||||
|
||||
async def install_plugins(self, requests):
|
||||
async def install_plugins(self, requests: List[PluginInstallRequest]):
|
||||
return await self.context.plugin_browser.request_multiple_plugin_installs(
|
||||
requests=requests
|
||||
)
|
||||
|
||||
async def confirm_plugin_install(self, request_id):
|
||||
async def confirm_plugin_install(self, request_id: str):
|
||||
return await self.context.plugin_browser.confirm_plugin_install(request_id)
|
||||
|
||||
def cancel_plugin_install(self, request_id):
|
||||
async def cancel_plugin_install(self, request_id: str):
|
||||
return self.context.plugin_browser.cancel_plugin_install(request_id)
|
||||
|
||||
async def uninstall_plugin(self, name):
|
||||
async def uninstall_plugin(self, name: str):
|
||||
return await self.context.plugin_browser.uninstall_plugin(name)
|
||||
|
||||
async def http_request(self, method="", url="", **kwargs):
|
||||
async def http_request(self, method: str="", url: str="", **kwargs: Any):
|
||||
async with ClientSession() as web:
|
||||
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs)
|
||||
text = await res.text()
|
||||
@@ -102,12 +112,13 @@ class Utilities:
|
||||
"body": text
|
||||
}
|
||||
|
||||
async def ping(self, **kwargs):
|
||||
async def ping(self, **kwargs: Any):
|
||||
return "pong"
|
||||
|
||||
async def execute_in_tab(self, tab, run_async, code):
|
||||
async def execute_in_tab(self, tab: str, run_async: bool, code: str):
|
||||
try:
|
||||
result = await inject_to_tab(tab, code, run_async)
|
||||
assert result
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
@@ -124,7 +135,7 @@ class Utilities:
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def inject_css_into_tab(self, tab, style):
|
||||
async def inject_css_into_tab(self, tab: str, style: str):
|
||||
try:
|
||||
css_id = str(uuid.uuid4())
|
||||
|
||||
@@ -138,7 +149,7 @@ class Utilities:
|
||||
}})()
|
||||
""", False)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
if result and "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result["result"]
|
||||
@@ -154,7 +165,7 @@ class Utilities:
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def remove_css_from_tab(self, tab, css_id):
|
||||
async def remove_css_from_tab(self, tab: str, css_id: str):
|
||||
try:
|
||||
result = await inject_to_tab(tab,
|
||||
f"""
|
||||
@@ -166,7 +177,7 @@ class Utilities:
|
||||
}})()
|
||||
""", False)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
if result and "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result
|
||||
@@ -181,10 +192,10 @@ class Utilities:
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def get_setting(self, key, default):
|
||||
async def get_setting(self, key: str, default: Any):
|
||||
return self.context.settings.getSetting(key, default)
|
||||
|
||||
async def set_setting(self, key, value):
|
||||
async def set_setting(self, key: str, value: Any):
|
||||
return self.context.settings.setSetting(key, value)
|
||||
|
||||
async def allow_remote_debugging(self):
|
||||
@@ -209,17 +220,18 @@ class Utilities:
|
||||
if path == None:
|
||||
path = get_home_path()
|
||||
|
||||
path = Path(path).resolve()
|
||||
path_obj = Path(path).resolve()
|
||||
|
||||
files, folders = [], []
|
||||
files: List[FilePickerObj] = []
|
||||
folders: List[FilePickerObj] = []
|
||||
|
||||
#Resolving all files/folders in the requested directory
|
||||
for file in path.iterdir():
|
||||
for file in path_obj.iterdir():
|
||||
if file.exists():
|
||||
filest = file.stat()
|
||||
is_hidden = file.name.startswith('.')
|
||||
if ON_WINDOWS and not is_hidden:
|
||||
is_hidden = bool(filest.st_file_attributes & FILE_ATTRIBUTE_HIDDEN)
|
||||
is_hidden = bool(filest.st_file_attributes & FILE_ATTRIBUTE_HIDDEN) # type: ignore
|
||||
if include_folders and file.is_dir():
|
||||
if (is_hidden and include_hidden) or not is_hidden:
|
||||
folders.append({"file": file, "filest": filest, "is_dir": True})
|
||||
@@ -233,9 +245,9 @@ class Utilities:
|
||||
if filter_for is not None:
|
||||
try:
|
||||
if re.compile(filter_for):
|
||||
files = filter(lambda file: re.search(filter_for, file.name) != None, files)
|
||||
files = list(filter(lambda file: re.search(filter_for, file["file"].name) != None, files))
|
||||
except re.error:
|
||||
files = filter(lambda file: file.name.find(filter_for) != -1, files)
|
||||
files = list(filter(lambda file: file["file"].name.find(filter_for) != -1, files))
|
||||
|
||||
# Ordering logic
|
||||
ord_arg = order_by.split("_")
|
||||
@@ -255,6 +267,9 @@ class Utilities:
|
||||
files.sort(key=lambda x: x['filest'].st_size, reverse = not rev)
|
||||
# Folders has no file size, order by name instead
|
||||
folders.sort(key=lambda x: x['file'].name.casefold())
|
||||
case _:
|
||||
files.sort(key=lambda x: x['file'].name.casefold(), reverse = rev)
|
||||
folders.sort(key=lambda x: x['file'].name.casefold(), reverse = rev)
|
||||
|
||||
#Constructing the final file list, folders first
|
||||
all = [{
|
||||
@@ -274,14 +289,14 @@ class Utilities:
|
||||
|
||||
|
||||
# Based on https://stackoverflow.com/a/46422554/13174603
|
||||
def start_rdt_proxy(self, ip, port):
|
||||
async def pipe(reader, writer):
|
||||
def start_rdt_proxy(self, ip: str, port: int):
|
||||
async def pipe(reader: StreamReader, writer: StreamWriter):
|
||||
try:
|
||||
while not reader.at_eof():
|
||||
writer.write(await reader.read(2048))
|
||||
finally:
|
||||
writer.close()
|
||||
async def handle_client(local_reader, local_writer):
|
||||
async def handle_client(local_reader: StreamReader, local_writer: StreamWriter):
|
||||
try:
|
||||
remote_reader, remote_writer = await open_connection(
|
||||
ip, port)
|
||||
@@ -295,9 +310,10 @@ class Utilities:
|
||||
self.rdt_proxy_task = self.context.loop.create_task(self.rdt_proxy_server)
|
||||
|
||||
def stop_rdt_proxy(self):
|
||||
if self.rdt_proxy_server:
|
||||
if self.rdt_proxy_server != None:
|
||||
self.rdt_proxy_server.close()
|
||||
self.rdt_proxy_task.cancel()
|
||||
if self.rdt_proxy_task:
|
||||
self.rdt_proxy_task.cancel()
|
||||
|
||||
async def _enable_rdt(self):
|
||||
# TODO un-hardcode port
|
||||
@@ -347,11 +363,11 @@ class Utilities:
|
||||
await tab.evaluate_js("location.reload();", False, True, False)
|
||||
self.logger.info("React DevTools disabled")
|
||||
|
||||
async def get_user_info(self) -> dict:
|
||||
async def get_user_info(self) -> Dict[str, str]:
|
||||
return {
|
||||
"username": get_username(),
|
||||
"path": get_home_path()
|
||||
}
|
||||
|
||||
async def get_tab_id(self, name):
|
||||
async def get_tab_id(self, name: str):
|
||||
return (await get_tab(name)).id
|
||||
Generated
+89
-19
@@ -1,4 +1,4 @@
|
||||
lockfileVersion: '6.1'
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
@@ -120,6 +120,14 @@ packages:
|
||||
'@jridgewell/trace-mapping': 0.3.18
|
||||
dev: true
|
||||
|
||||
/@babel/code-frame@7.22.13:
|
||||
resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/highlight': 7.22.20
|
||||
chalk: 2.4.2
|
||||
dev: true
|
||||
|
||||
/@babel/code-frame@7.22.5:
|
||||
resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -144,7 +152,7 @@ packages:
|
||||
'@babel/helpers': 7.22.5
|
||||
'@babel/parser': 7.22.5
|
||||
'@babel/template': 7.22.5
|
||||
'@babel/traverse': 7.22.5
|
||||
'@babel/traverse': 7.23.2
|
||||
'@babel/types': 7.22.5
|
||||
convert-source-map: 1.9.0
|
||||
debug: 4.3.4
|
||||
@@ -165,6 +173,16 @@ packages:
|
||||
jsesc: 2.5.2
|
||||
dev: true
|
||||
|
||||
/@babel/generator@7.23.0:
|
||||
resolution: {integrity: sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
'@jridgewell/gen-mapping': 0.3.3
|
||||
'@jridgewell/trace-mapping': 0.3.18
|
||||
jsesc: 2.5.2
|
||||
dev: true
|
||||
|
||||
/@babel/helper-compilation-targets@7.22.5(@babel/core@7.22.5):
|
||||
resolution: {integrity: sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -179,24 +197,29 @@ packages:
|
||||
semver: 6.3.0
|
||||
dev: true
|
||||
|
||||
/@babel/helper-environment-visitor@7.22.20:
|
||||
resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-environment-visitor@7.22.5:
|
||||
resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-function-name@7.22.5:
|
||||
resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==}
|
||||
/@babel/helper-function-name@7.23.0:
|
||||
resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/template': 7.22.5
|
||||
'@babel/types': 7.22.5
|
||||
'@babel/template': 7.22.15
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/helper-hoist-variables@7.22.5:
|
||||
resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.22.5
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/helper-module-imports@7.22.5:
|
||||
@@ -216,7 +239,7 @@ packages:
|
||||
'@babel/helper-split-export-declaration': 7.22.5
|
||||
'@babel/helper-validator-identifier': 7.22.5
|
||||
'@babel/template': 7.22.5
|
||||
'@babel/traverse': 7.22.5
|
||||
'@babel/traverse': 7.23.2
|
||||
'@babel/types': 7.22.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -236,11 +259,23 @@ packages:
|
||||
'@babel/types': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/helper-split-export-declaration@7.22.6:
|
||||
resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/helper-string-parser@7.22.5:
|
||||
resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-validator-identifier@7.22.20:
|
||||
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/@babel/helper-validator-identifier@7.22.5:
|
||||
resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -256,12 +291,21 @@ packages:
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/template': 7.22.5
|
||||
'@babel/traverse': 7.22.5
|
||||
'@babel/traverse': 7.23.2
|
||||
'@babel/types': 7.22.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@babel/highlight@7.22.20:
|
||||
resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.22.20
|
||||
chalk: 2.4.2
|
||||
js-tokens: 4.0.0
|
||||
dev: true
|
||||
|
||||
/@babel/highlight@7.22.5:
|
||||
resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -279,12 +323,29 @@ packages:
|
||||
'@babel/types': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/parser@7.23.0:
|
||||
resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/runtime@7.22.5:
|
||||
resolution: {integrity: sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.13.11
|
||||
|
||||
/@babel/template@7.22.15:
|
||||
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.22.13
|
||||
'@babel/parser': 7.23.0
|
||||
'@babel/types': 7.23.0
|
||||
dev: true
|
||||
|
||||
/@babel/template@7.22.5:
|
||||
resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -294,18 +355,18 @@ packages:
|
||||
'@babel/types': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/traverse@7.22.5:
|
||||
resolution: {integrity: sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==}
|
||||
/@babel/traverse@7.23.2:
|
||||
resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.22.5
|
||||
'@babel/generator': 7.22.5
|
||||
'@babel/helper-environment-visitor': 7.22.5
|
||||
'@babel/helper-function-name': 7.22.5
|
||||
'@babel/code-frame': 7.22.13
|
||||
'@babel/generator': 7.23.0
|
||||
'@babel/helper-environment-visitor': 7.22.20
|
||||
'@babel/helper-function-name': 7.23.0
|
||||
'@babel/helper-hoist-variables': 7.22.5
|
||||
'@babel/helper-split-export-declaration': 7.22.5
|
||||
'@babel/parser': 7.22.5
|
||||
'@babel/types': 7.22.5
|
||||
'@babel/helper-split-export-declaration': 7.22.6
|
||||
'@babel/parser': 7.23.0
|
||||
'@babel/types': 7.23.0
|
||||
debug: 4.3.4
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
@@ -321,6 +382,15 @@ packages:
|
||||
to-fast-properties: 2.0.0
|
||||
dev: true
|
||||
|
||||
/@babel/types@7.23.0:
|
||||
resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.22.5
|
||||
'@babel/helper-validator-identifier': 7.22.20
|
||||
to-fast-properties: 2.0.0
|
||||
dev: true
|
||||
|
||||
/@esbuild/android-arm64@0.17.19:
|
||||
resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2052,7 +2122,7 @@ packages:
|
||||
dependencies:
|
||||
'@babel/core': 7.22.5
|
||||
'@babel/parser': 7.22.5
|
||||
'@babel/traverse': 7.22.5
|
||||
'@babel/traverse': 7.23.2
|
||||
'@babel/types': 7.22.5
|
||||
find-line-column: 0.5.2
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
|
||||
import { CSSProperties, VFC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BsGearFill } from 'react-icons/bs';
|
||||
import { FaArrowLeft, FaStore } from 'react-icons/fa';
|
||||
|
||||
@@ -13,6 +14,7 @@ const titleStyles: CSSProperties = {
|
||||
|
||||
const TitleView: VFC = () => {
|
||||
const { activePlugin, closeActivePlugin } = useDeckyState();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onSettingsClick = () => {
|
||||
Router.CloseSideMenus();
|
||||
@@ -31,12 +33,14 @@ const TitleView: VFC = () => {
|
||||
<DialogButton
|
||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||
onClick={onStoreClick}
|
||||
onOKActionDescription={t('TitleView.decky_store_desc')}
|
||||
>
|
||||
<FaStore style={{ marginTop: '-4px', display: 'block' }} />
|
||||
</DialogButton>
|
||||
<DialogButton
|
||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||
onClick={onSettingsClick}
|
||||
onOKActionDescription={t('TitleView.settings_desc')}
|
||||
>
|
||||
<BsGearFill style={{ marginTop: '-4px', display: 'block' }} />
|
||||
</DialogButton>
|
||||
@@ -45,15 +49,15 @@ const TitleView: VFC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={staticClasses.Title} style={titleStyles}>
|
||||
<Focusable className={staticClasses.Title} style={titleStyles}>
|
||||
<DialogButton
|
||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||
onClick={closeActivePlugin}
|
||||
>
|
||||
<FaArrowLeft style={{ marginTop: '-4px', display: 'block' }} />
|
||||
</DialogButton>
|
||||
<div style={{ flex: 0.9 }}>{activePlugin.name}</div>
|
||||
</div>
|
||||
{activePlugin?.titleView || <div style={{ flex: 0.9 }}>{activePlugin.name}</div>}
|
||||
</Focusable>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from 'decky-frontend-lib';
|
||||
import { filesize } from 'filesize';
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FileIcon, defaultStyles } from 'react-file-icon';
|
||||
import { DefaultExtensionType, FileIcon, defaultStyles } from 'react-file-icon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaArrowUp, FaFolder } from 'react-icons/fa';
|
||||
|
||||
@@ -316,7 +316,12 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
|
||||
) : (
|
||||
<div style={iconStyles}>
|
||||
{file.realpath.includes('.') ? (
|
||||
<FileIcon {...defaultStyles[extension]} {...styleDefObj[extension]} extension={''} />
|
||||
<FileIcon
|
||||
{...defaultStyles[extension as DefaultExtensionType]}
|
||||
// @ts-expect-error
|
||||
{...styleDefObj[extension]}
|
||||
extension={''}
|
||||
/>
|
||||
) : (
|
||||
<FileIcon />
|
||||
)}
|
||||
|
||||
@@ -29,10 +29,10 @@ const BranchSelect: FunctionComponent<{}> = () => {
|
||||
<Field label={t('BranchSelect.update_channel.label')} childrenContainerWidth={'fixed'}>
|
||||
<Dropdown
|
||||
rgOptions={Object.values(UpdateBranch)
|
||||
.filter((branch) => typeof branch == 'string')
|
||||
.filter((branch) => typeof branch == 'number')
|
||||
.map((branch) => ({
|
||||
label: tBranches[UpdateBranch[branch]],
|
||||
data: UpdateBranch[branch],
|
||||
label: tBranches[branch as number],
|
||||
data: branch,
|
||||
}))}
|
||||
selectedOption={selectedBranch}
|
||||
onChange={async (newVal) => {
|
||||
|
||||
@@ -26,10 +26,10 @@ const StoreSelect: FunctionComponent<{}> = () => {
|
||||
<Field label={t('StoreSelect.store_channel.label')} childrenContainerWidth={'fixed'}>
|
||||
<Dropdown
|
||||
rgOptions={Object.values(Store)
|
||||
.filter((store) => typeof store == 'string')
|
||||
.filter((store) => typeof store == 'number')
|
||||
.map((store) => ({
|
||||
label: tStores[Store[store]],
|
||||
data: Store[store],
|
||||
label: tStores[store as number],
|
||||
data: store,
|
||||
}))}
|
||||
selectedOption={selectedStore}
|
||||
onChange={async (newVal) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import logo from '../../../assets/plugin_store.png';
|
||||
import Logger from '../../logger';
|
||||
import { StorePlugin, getPluginList } from '../../store';
|
||||
import { Store, StorePlugin, getPluginList, getStore } from '../../store';
|
||||
import PluginCard from './PluginCard';
|
||||
|
||||
const logger = new Logger('Store');
|
||||
@@ -21,6 +21,7 @@ const logger = new Logger('Store');
|
||||
const StorePage: FC<{}> = () => {
|
||||
const [currentTabRoute, setCurrentTabRoute] = useState<string>('browse');
|
||||
const [data, setData] = useState<StorePlugin[] | null>(null);
|
||||
const [isTesting, setIsTesting] = useState<boolean>(false);
|
||||
const { TabCount } = findModule((m) => {
|
||||
if (m?.TabCount && m?.TabTitle) return true;
|
||||
return false;
|
||||
@@ -33,6 +34,9 @@ const StorePage: FC<{}> = () => {
|
||||
const res = await getPluginList();
|
||||
logger.log('got data!', res);
|
||||
setData(res);
|
||||
const storeRes = await getStore();
|
||||
logger.log(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`);
|
||||
setIsTesting(storeRes === Store.Testing);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
@@ -58,7 +62,7 @@ const StorePage: FC<{}> = () => {
|
||||
tabs={[
|
||||
{
|
||||
title: t('Store.store_tabs.title'),
|
||||
content: <BrowseTab children={{ data: data }} />,
|
||||
content: <BrowseTab children={{ data: data, isTesting: isTesting }} />,
|
||||
id: 'browse',
|
||||
renderTabAddon: () => <span className={TabCount}>{data.length}</span>,
|
||||
},
|
||||
@@ -75,7 +79,7 @@ const StorePage: FC<{}> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
const BrowseTab: FC<{ children: { data: StorePlugin[]; isTesting: boolean } }> = (data) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sortOptions = useMemo(
|
||||
@@ -178,6 +182,36 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
</div>
|
||||
</Focusable>
|
||||
</div>
|
||||
{data.children.isTesting && (
|
||||
<div
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginLeft: '20px',
|
||||
marginRight: '20px',
|
||||
marginBottom: '20px',
|
||||
padding: '8px 36px',
|
||||
background: 'rgba(255, 255, 0, 0.067)',
|
||||
textAlign: 'center',
|
||||
border: '2px solid rgba(255, 255, 0, 0.467)',
|
||||
}}
|
||||
>
|
||||
<h2 style={{ margin: 0 }}>{t('Store.store_testing_warning.label')}</h2>
|
||||
<span>
|
||||
{`${t('Store.store_testing_warning.desc')} `}
|
||||
<a
|
||||
href="https://decky.xyz/testing"
|
||||
target="_blank"
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
decky.xyz/testing
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{data.children.data
|
||||
.filter((plugin: StorePlugin) => {
|
||||
@@ -229,13 +263,13 @@ const AboutTab: FC<{}> = () => {
|
||||
<span>
|
||||
{t('Store.store_testing_cta')}{' '}
|
||||
<a
|
||||
href="https://deckbrew.xyz/testing"
|
||||
href="https://decky.xyz/testing"
|
||||
target="_blank"
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
deckbrew.xyz/testing
|
||||
decky.xyz/testing
|
||||
</a>
|
||||
</span>
|
||||
<span className="deckyStoreAboutHeader">{t('Store.store_contrib.label')}</span>
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface Plugin {
|
||||
content?: JSX.Element;
|
||||
onDismount?(): void;
|
||||
alwaysRender?: boolean;
|
||||
titleView?: JSX.Element;
|
||||
}
|
||||
|
||||
export enum InstallType {
|
||||
|
||||
@@ -22,6 +22,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const isPatched = Symbol('is patched');
|
||||
|
||||
class RouterHook extends Logger {
|
||||
private router: any;
|
||||
private memoizedRouter: any;
|
||||
@@ -90,9 +92,10 @@ class RouterHook extends Logger {
|
||||
...routeList[index].props,
|
||||
children: {
|
||||
...cloneElement(routeList[index].props.children),
|
||||
type: (props) => createElement(oType, props),
|
||||
type: routeList[index].props.children[isPatched] ? oType : (props) => createElement(oType, props),
|
||||
},
|
||||
}).children;
|
||||
routeList[index].props.children[isPatched] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+31
-32
@@ -32,43 +32,42 @@ export interface PluginInstallRequest {
|
||||
// name: version
|
||||
export type PluginUpdateMapping = Map<string, StorePluginVersion>;
|
||||
|
||||
export async function getStore(): Promise<Store> {
|
||||
return await getSetting<Store>('store', Store.Default);
|
||||
}
|
||||
|
||||
export async function getPluginList(): Promise<StorePlugin[]> {
|
||||
let version = await window.DeckyPluginLoader.updateVersion();
|
||||
let store = await getSetting<Store>('store', Store.Default);
|
||||
let store = await getSetting<Store | null>('store', null);
|
||||
|
||||
let customURL = await getSetting<string>('store-url', 'https://plugins.deckbrew.xyz/plugins');
|
||||
let storeURL;
|
||||
if (!store) {
|
||||
console.log('Could not get a default store, using Default.');
|
||||
await setSetting('store-url', Store.Default);
|
||||
return fetch('https://plugins.deckbrew.xyz/plugins', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Decky-Version': version.current,
|
||||
},
|
||||
}).then((r) => r.json());
|
||||
} else {
|
||||
switch (+store) {
|
||||
case Store.Default:
|
||||
storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
||||
break;
|
||||
case Store.Testing:
|
||||
storeURL = 'https://testing.deckbrew.xyz/plugins';
|
||||
break;
|
||||
case Store.Custom:
|
||||
storeURL = customURL;
|
||||
break;
|
||||
default:
|
||||
console.error('Somehow you ended up without a standard URL, using the default URL.');
|
||||
storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
||||
break;
|
||||
}
|
||||
return fetch(storeURL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Decky-Version': version.current,
|
||||
},
|
||||
}).then((r) => r.json());
|
||||
if (store === null) {
|
||||
console.log('Could not get store, using Default.');
|
||||
await setSetting('store', Store.Default);
|
||||
store = Store.Default;
|
||||
}
|
||||
switch (+store) {
|
||||
case Store.Default:
|
||||
storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
||||
break;
|
||||
case Store.Testing:
|
||||
storeURL = 'https://testing.deckbrew.xyz/plugins';
|
||||
break;
|
||||
case Store.Custom:
|
||||
storeURL = customURL;
|
||||
break;
|
||||
default:
|
||||
console.error('Somehow you ended up without a standard URL, using the default URL.');
|
||||
storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
||||
break;
|
||||
}
|
||||
return fetch(storeURL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Decky-Version': version.current,
|
||||
},
|
||||
}).then((r) => r.json());
|
||||
}
|
||||
|
||||
export async function installFromURL(url: string) {
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
|
||||
@@ -17,6 +17,7 @@ __version__ = '0.1.0'
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
import time
|
||||
|
||||
"""
|
||||
Constants
|
||||
@@ -117,7 +118,8 @@ Environment variable: `DECKY_PLUGIN_AUTHOR`.
|
||||
e.g.: `John Doe`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_LOG: str = os.path.join(DECKY_PLUGIN_LOG_DIR, "plugin.log")
|
||||
__cur_time = time.strftime("%Y-%m-%d %H.%M.%S")
|
||||
DECKY_PLUGIN_LOG: str = os.path.join(DECKY_PLUGIN_LOG_DIR, f"{__cur_time}.log")
|
||||
"""
|
||||
The path to the plugin's main logfile.
|
||||
Environment variable: `DECKY_PLUGIN_LOG`.
|
||||
@@ -192,6 +194,12 @@ def migrate_logs(*files_or_directories: str) -> dict[str, str]:
|
||||
Logging
|
||||
"""
|
||||
|
||||
try:
|
||||
for x in [entry.name for entry in sorted(os.scandir(DECKY_PLUGIN_LOG_DIR),key=lambda x: x.stat().st_mtime, reverse=True) if entry.name.endswith(".log")][4:]:
|
||||
os.unlink(os.path.join(DECKY_PLUGIN_LOG_DIR, x))
|
||||
except Exception as e:
|
||||
print(f"Failed to delete old logs: {str(e)}")
|
||||
|
||||
logging.basicConfig(filename=DECKY_PLUGIN_LOG,
|
||||
format='[%(asctime)s][%(levelname)s]: %(message)s',
|
||||
force=True)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
aiohttp==3.8.1
|
||||
aiohttp-jinja2==1.5.0
|
||||
aiohttp_cors==0.7.0
|
||||
watchdog==2.1.7
|
||||
certifi==2022.12.7
|
||||
Reference in New Issue
Block a user