mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Compare commits
307 Commits
v2.2.7
...
v2.10.3-pre2
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d6d69568d | |||
| 62a2107c06 | |||
| cf2f419942 | |||
| f657529ab5 | |||
| 8eae2d60e5 | |||
| 3e64e53cd7 | |||
| ef9afa8cbc | |||
| 143461d597 | |||
| ae887e10d6 | |||
| 186c70591a | |||
| 777e7893e3 | |||
| b2a1b172e2 | |||
| fc72ac5c63 | |||
| f1576c7798 | |||
| 388526d02d | |||
| cb65fb4b11 | |||
| b82c9cf6e6 | |||
| 04fff476d5 | |||
| 66bcdfd84e | |||
| 8494f2ec3e | |||
| 52f25708ad | |||
| 24b02114cb | |||
| 651215723d | |||
| ca9831be05 | |||
| f7fd7b712f | |||
| 3859c0e483 | |||
| 2db92536fd | |||
| a6ffc8f04d | |||
| 4df8cb5026 | |||
| a86d473280 | |||
| 4d226b00fc | |||
| 8b16b156ed | |||
| 4029a43637 | |||
| 57f4555350 | |||
| bd87cc852b | |||
| 30d7c9bb81 | |||
| 890599c7bb | |||
| b8e48d2146 | |||
| da1db5a053 | |||
| 60353df7ed | |||
| e8dfe5a87d | |||
| d0b7d1a4a6 | |||
| 13b6ed5ad9 | |||
| 89bbbf6fe4 | |||
| 9a05c228a0 | |||
| 00d9b03322 | |||
| 47bc910a84 | |||
| fdc556edee | |||
| 1c6270ccd6 | |||
| f87e794c3f | |||
| 8f17f2b0fe | |||
| 81b601c0e7 | |||
| 83e8c89c97 | |||
| ca107feb25 | |||
| 56719df827 | |||
| 3c11bb71f6 | |||
| 413cec9244 | |||
| e5277190ed | |||
| 935de1ad0c | |||
| 2e8e0fc7c1 | |||
| 8049417e03 | |||
| f4c0a8b5aa | |||
| d3584a9931 | |||
| 9542708c92 | |||
| b3c363c89f | |||
| 5f5e8ad5d7 | |||
| 6b6de2fcd5 | |||
| b27b625921 | |||
| c5229c6a62 | |||
| c631d40aa3 | |||
| d21b221575 | |||
| 010feddf36 | |||
| 5114bb5711 | |||
| 4e7001efd6 | |||
| 0c9d90df10 | |||
| 09963ef4bb | |||
| 2f24454b1e | |||
| 177ed35522 | |||
| c5aeb018db | |||
| 671120a517 | |||
| f306239b5f | |||
| 44859be657 | |||
| ee4c706529 | |||
| 5ca3015609 | |||
| 8b05fb6943 | |||
| e4ebbed477 | |||
| d13536955e | |||
| 37462548b3 | |||
| 74d06aaca6 | |||
| 1934d12aac | |||
| 70bd5adad3 | |||
| f9d5c4ba2a | |||
| cc5e6ac24d | |||
| d44ce0f74b | |||
| 687f7bf5db | |||
| 9cd219fab7 | |||
| 6e6f8caca8 | |||
| 3a83062438 | |||
| dfdad14ede | |||
| 852897c502 | |||
| 9e502f85fa | |||
| 368a1044da | |||
| 578b8b3ee2 | |||
| ede1067bb3 | |||
| bec1c61366 | |||
| 5d2cc1c133 | |||
| ef97921e30 | |||
| 0009a53800 | |||
| 3eff60e8aa | |||
| cbfa548f98 | |||
| 3f2d54ddbd | |||
| 0ecc9bf579 | |||
| 88b9984b0f | |||
| d6c025da1c | |||
| 7ba864136f | |||
| d5dda39add | |||
| 320f392ad3 | |||
| 47388b1083 | |||
| c1edb9f2e9 | |||
| d184e1c4af | |||
| 88a4c0c361 | |||
| ff1f902c91 | |||
| c1215072a9 | |||
| ad3fc990f5 | |||
| d3038efd45 | |||
| 92b953a22d | |||
| 9accb676e6 | |||
| ba9f12ffe7 | |||
| a4f8f5fcf5 | |||
| 4400226c2d | |||
| 6e6ef81e66 | |||
| c4a4249440 | |||
| a4e02b8201 | |||
| ea56b03b38 | |||
| 3fe1d44515 | |||
| 3932c69ad6 | |||
| 1b8fe4f82f | |||
| 67dc7e7893 | |||
| c14f7043bc | |||
| 6d47a2111b | |||
| aceeaeee07 | |||
| a77ad33ea0 | |||
| e2691592ba | |||
| 3d8629b803 | |||
| 4c5468ae97 | |||
| 81cb3dd0ec | |||
| f94866f473 | |||
| d4a4d2287a | |||
| 4cfeb8ef3e | |||
| 826e014456 | |||
| 9fb211316d | |||
| ba01ad6e13 | |||
| d0b897ff7f | |||
| dd3d313517 | |||
| 83faf6697b | |||
| 7bc2187c8c | |||
| 8a61ecc71a | |||
| 579d52982a | |||
| d895b7c0ef | |||
| a3f6004fd9 | |||
| 73c5a890ce | |||
| 0cb0fb7165 | |||
| 5376478b2d | |||
| c74cfc51e7 | |||
| 6b10c87648 | |||
| 8ab0b34a2e | |||
| b5aeee505a | |||
| d7f343aac4 | |||
| 1eacdc4bce | |||
| a2e2335dd9 | |||
| 6882de6027 | |||
| fccadefe47 | |||
| 70a4b26984 | |||
| 39206b782e | |||
| 3ca625d838 | |||
| 1042655eb9 | |||
| cad2babbca | |||
| dbd1ea9543 | |||
| 313f6db5fa | |||
| 3b58001abe | |||
| bf99bce579 | |||
| 9c02ccc537 | |||
| fedbfcb041 | |||
| 3c52b33e18 | |||
| d99f332523 | |||
| 0c83c9a2b5 | |||
| 6b14f08d59 | |||
| 089e6b086c | |||
| 08d5c942a4 | |||
| 35e7c80835 | |||
| caf37d681f | |||
| 93151e4e5e | |||
| d6f336d84b | |||
| 4777963b65 | |||
| fc193f98db | |||
| a07e4d6fe6 | |||
| 4ab7d97ab2 | |||
| 15a6f7fdb8 | |||
| 7d2cff8745 | |||
| ee5ed3faf0 | |||
| 0f36e87cce | |||
| fd325ef1cc | |||
| faf46ba533 | |||
| 94ec434eae | |||
| a223efd6f5 | |||
| 395e45167d | |||
| 0dd0d9f4bd | |||
| 3e5404abdd | |||
| 46abc5a266 | |||
| 88e1e9b869 | |||
| fc0089f7a5 | |||
| d335562328 | |||
| f9624a0859 | |||
| 97bb3fa4c8 | |||
| 611245aec9 | |||
| e1807e8c75 | |||
| b94cfe32d9 | |||
| f1e679c3fb | |||
| e1b138bcbd | |||
| c6be8f6c14 | |||
| ac086cf59e | |||
| 3e120ea312 | |||
| 0b718daa47 | |||
| 0929b9c5cb | |||
| 43b2269ea7 | |||
| 0c4e27cd34 | |||
| 36cf85b08a | |||
| 994da868af | |||
| 2e53fb217a | |||
| c2b76d9099 | |||
| c05e8f9ae0 | |||
| 2dce0646bd | |||
| 6569f1b268 | |||
| 3ebaac6752 | |||
| cbbd564860 | |||
| 635edf7f5b | |||
| 1b6e18bcb3 | |||
| 0ad0016c62 | |||
| a2716449f9 | |||
| 649eed89c9 | |||
| 83680fffa2 | |||
| d695b90baf | |||
| 5fdcc56409 | |||
| 915997d149 | |||
| e8b4c4a307 | |||
| e92b66068a | |||
| b72b327610 | |||
| b8fdff8093 | |||
| 880b4c2f8f | |||
| 34af340009 | |||
| 80b6115f6f | |||
| 3bed83697e | |||
| 0ffef6e4bf | |||
| 8810a014f3 | |||
| 385552451b | |||
| c2c9d11c66 | |||
| 0474095a40 | |||
| 346f80beb3 | |||
| 2a6bf75f02 | |||
| f73918c902 | |||
| ea35af2050 | |||
| 6232e3da58 | |||
| 35e46f9ccb | |||
| 2b9a80c151 | |||
| a90ed38c89 | |||
| 3653cf5640 | |||
| 0db45ca71e | |||
| 16681fabb5 | |||
| c210523a22 | |||
| 5d8601347a | |||
| 1e02fcf394 | |||
| f923306a7f | |||
| 478fe32527 | |||
| 50764600c8 | |||
| aec7063139 | |||
| c9ee98e0c0 | |||
| 093b064a4e | |||
| 2955681975 | |||
| de42639726 | |||
| 17742e947a | |||
| 898271b33d | |||
| b44896524f | |||
| db7bb236d8 | |||
| 5e3de747d3 | |||
| d389b403b5 | |||
| bace5143d2 | |||
| f5fc205384 | |||
| 4d30339c34 | |||
| 5996a3f88b | |||
| 1b635c74b1 | |||
| a9bd5079de | |||
| c1fabe5b35 | |||
| ed82f51bb7 | |||
| df1524e15f | |||
| 2edd910df3 | |||
| 1cd69097ad | |||
| 84c3b039c3 | |||
| 2e6b3834da | |||
| 6749c78ed7 | |||
| 4ad15568cd | |||
| 58849b3002 | |||
| 6346da6fe5 | |||
| af51a29055 | |||
| c546a818f1 | |||
| 0226bd2bf8 | |||
| 7b16b623c8 | |||
| 6e3c05072c |
@@ -0,0 +1,74 @@
|
||||
name: Bug report
|
||||
description: File a bug/issue
|
||||
title: "[BUG] <title>"
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: low-effort-checks
|
||||
attributes:
|
||||
label: Please confirm
|
||||
description: Issues without all checks may be ignored/closed.
|
||||
options:
|
||||
- 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).
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug Report Description
|
||||
description: A clear and concise description of what the bug is and if possible, the steps you used to get to the bug. If appropriate, include screenshots or videos.
|
||||
placeholder: |
|
||||
When I try to use ...
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behaviour
|
||||
description: A brief description of the expected behavior.
|
||||
placeholder: It should be ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: SteamOS version
|
||||
# description: Can be found with `uname -a`
|
||||
# placeholder: "Linux steamdeck 5.13.0-valve36-1-neptune #1 SMP PREEMPT Mon, 19 Dec 2022 23:39:41 +0000 x86_64 GNU/Linux"
|
||||
placeholder: "SteamOS 3.4.3 Stable"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Selected Update Channel
|
||||
description: Which branch of Decky are you on?
|
||||
multiple: false
|
||||
options:
|
||||
- Stable
|
||||
- Prerelease
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Have you modified the read-only filesystem at any point?
|
||||
description: Describe how here, if you haven't done anything you can leave this blank
|
||||
placeholder: Yes, I've installed neofetch via pacman.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Logs
|
||||
description: Please reboot your deck (if possible) when attempting to recreate the issue, then run ``cd ~ && journalctl -b0 -u plugin_loader.service > deckylog.txt``. This will save the log file to ``~`` aka ``/home/deck``. Please upload the file here
|
||||
placeholder: deckylog.txt
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Steam Deck Homebrew Discord Server
|
||||
url: https://discord.gg/ZU74G2NJzk
|
||||
about: Please ask and answer questions here.
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Feature request
|
||||
description: Request a new feature (NOT A PLUGIN)
|
||||
title: "[Request] <title>"
|
||||
labels: [feature request]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: low-effort-checks
|
||||
attributes:
|
||||
label: Please confirm
|
||||
description: Issues without all checks may be ignored/closed.
|
||||
options:
|
||||
- label: I have searched existing issues
|
||||
- label: This issue is not a duplicate of an existing one
|
||||
- label: This is not a request for a plugin
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature Request Description
|
||||
description: A clear and concise description of what the new feature.
|
||||
placeholder: |
|
||||
Decky plugins should be sortable in the quick access menu
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Further Description
|
||||
description: A further explanation of the feature. If appropriate, include screenshots or videos.
|
||||
placeholder: |
|
||||
This would help make the UI clearer and easier to use as there is less clutter in the QAM.
|
||||
It would also make it faster to access plugins that are used more.
|
||||
|
||||
This could be implemented by adding ...
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,13 @@
|
||||
Please tick as appropriate:
|
||||
- [ ] I have tested this code on a steam deck or on a PC
|
||||
- [ ] My changes generate no new errors/warnings
|
||||
- [ ] This is a bugfix/hotfix
|
||||
- [ ] This is a new feature
|
||||
|
||||
If you're wanting to update a translation or add a new one, please use the weblate page: https://weblate.werwolv.net/projects/decky/
|
||||
|
||||
# Description
|
||||
|
||||
This fixes issue: #
|
||||
|
||||
Please provide a clear and concise description of what the new feature is. If appropriate, include screenshots or videos.
|
||||
+51
-10
@@ -31,7 +31,7 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Build PluginLoader
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Print input
|
||||
@@ -47,15 +47,15 @@ jobs:
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Set up Python 3.10 🐍
|
||||
uses: actions/setup-python@v3
|
||||
- name: Set up Python 3.10.2 🐍
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: "3.10.2"
|
||||
|
||||
- name: Install Python dependencies ⬇️
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pyinstaller
|
||||
pip install pyinstaller==5.5
|
||||
[ -f requirements.txt ] && pip install -r requirements.txt
|
||||
|
||||
- name: Install JS dependencies ⬇️
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build Python Backend 🛠️
|
||||
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/legacy:/legacy ./backend/*.py
|
||||
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 }}
|
||||
@@ -84,6 +84,49 @@ jobs:
|
||||
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 -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/main.py
|
||||
|
||||
- name: Upload package artifact ⬆️
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: PluginLoader Win
|
||||
path: ./dist/PluginLoader.exe
|
||||
|
||||
release:
|
||||
name: Release stable version of the package
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'release' }}
|
||||
@@ -130,9 +173,7 @@ jobs:
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "no type selected, defaulting to patch.\n"
|
||||
OUT=$(semver bump patch "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
printf "no type selected, not bumping for release.\n"
|
||||
fi
|
||||
elif [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
printf "previous tag is a release, bumping by selected type.\n"
|
||||
@@ -159,7 +200,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && !env.ACT }}
|
||||
with:
|
||||
name: Prerelease ${{ steps.ready_tag.outputs.tag_name }}
|
||||
name: Release ${{ steps.ready_tag.outputs.tag_name }}
|
||||
tag_name: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
files: ./dist/PluginLoader
|
||||
prerelease: false
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Push Updated Plugin Stub to Template
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
copy-stub:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8230315d06ad95c617244d2f265d237a1682d445
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v35.6.3
|
||||
with:
|
||||
separator: ","
|
||||
files: |
|
||||
plugin/*
|
||||
|
||||
- name: Is stub changed
|
||||
id: changed-stub
|
||||
run: |
|
||||
STUB_CHANGED="false"
|
||||
PATHS=(plugin plugin/decky_plugin.pyi)
|
||||
SHA=${{ github.sha }}
|
||||
SHA_PREV=HEAD^
|
||||
FILES=$(git diff $SHA_PREV..$SHA --name-only -- ${PATHS[@]} | jq -Rsc 'split("\n")[:-1] | join (",")')
|
||||
if [[ "$FILES" == *"plugin/decky_plugin.pyi"* ]]; then
|
||||
$STUB_CHANGED="true"
|
||||
echo "Stub has changed, pushing updated stub"
|
||||
else
|
||||
echo "Stub has not changed, exiting."
|
||||
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Push updated stub
|
||||
if: steps.changed-stub.outputs.has_changed == true
|
||||
uses: dmnemec/copy_file_to_another_repo_action@bbebd3da22e4a37d04dca5f782edd5201cb97083
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
source_file: 'plugin/decky_plugin.pyi'
|
||||
destination_repo: 'SteamDeckHomebrew/decky-plugin-template'
|
||||
user_email: '11465594+TrainDoctor@users.noreply.github.com'
|
||||
user_name: 'TrainDoctor'
|
||||
commit_message: 'Updated template with latest plugin stub changes'
|
||||
@@ -0,0 +1,17 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Run linters
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2 # Check out the repository first.
|
||||
- name: Run prettier (JavaScript & TypeScript)
|
||||
run: |
|
||||
pushd frontend
|
||||
npm install
|
||||
npm run lint
|
||||
Vendored
+1
-1
@@ -4,4 +4,4 @@
|
||||
"deckpass" : "ssap",
|
||||
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
|
||||
"deckdir" : "/home/deck"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+42
-7
@@ -14,7 +14,9 @@
|
||||
"label": "localrun",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"dependsOn" : ["buildall"],
|
||||
"dependsOn": [
|
||||
"buildall"
|
||||
],
|
||||
"detail": "Check for local runs, create a plugins folder",
|
||||
"command": "mkdir -p plugins",
|
||||
"problemMatcher": []
|
||||
@@ -39,6 +41,14 @@
|
||||
"command": "rsync -azp --rsh='ssh -p ${config:deckport} ${config:deckkey}' requirements.txt deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader/requirements.txt && ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip && python -m pip install --upgrade pip && python -m pip install --upgrade setuptools && python -m pip install -r ${config:deckdir}/homebrew/dev/pluginloader/requirements.txt'",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "extracttext",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"detail": "Check for new strings in the frontend source code and extract it into the corresponding json language files",
|
||||
"command": "cd frontend && ./node_modules/.bin/i18next --config ./i18next-parser.config.mjs",
|
||||
"problemMatcher": []
|
||||
},
|
||||
// BUILD
|
||||
{
|
||||
"label": "pnpmsetup",
|
||||
@@ -48,6 +58,16 @@
|
||||
"command": "cd frontend && pnpm i",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"script": "watch",
|
||||
"type": "npm",
|
||||
"path": "frontend",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"label": "watchfrontend",
|
||||
"detail": "rollup -c -w",
|
||||
"isBackground": true
|
||||
},
|
||||
{
|
||||
"label": "buildfrontend",
|
||||
"type": "npm",
|
||||
@@ -55,8 +75,7 @@
|
||||
"detail": "rollup -c",
|
||||
"script": "build",
|
||||
"path": "frontend",
|
||||
"problemMatcher": [],
|
||||
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "buildall",
|
||||
@@ -95,7 +114,9 @@
|
||||
"detail": "Run indev PluginLoader on Deck",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"dependsOn" : ["checkforsettings"],
|
||||
"dependsOn": [
|
||||
"checkforsettings"
|
||||
],
|
||||
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PLUGIN_PATH=${config:deckdir}/homebrew/dev/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/services; echo '${config:deckpass}' | sudo -SE python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py'",
|
||||
"problemMatcher": []
|
||||
},
|
||||
@@ -108,6 +129,20 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
// ALL-IN-ONES
|
||||
{
|
||||
"label": "deployandrun",
|
||||
"detail": "Deploy and run, skipping JS build. Useful when combined with npm:watch",
|
||||
"dependsOrder": "sequence",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"dependsOn": [
|
||||
"deploy",
|
||||
"runpydeck"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "updateremote",
|
||||
"detail": "Build and deploy",
|
||||
@@ -115,7 +150,7 @@
|
||||
"group": "none",
|
||||
"dependsOn": [
|
||||
"buildall",
|
||||
"deploy",
|
||||
"deploy"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
@@ -128,7 +163,7 @@
|
||||
"isDefault": true
|
||||
},
|
||||
"dependsOn": [
|
||||
"buildall",
|
||||
"buildfrontend",
|
||||
"deploy",
|
||||
"runpydeck"
|
||||
],
|
||||
@@ -152,4 +187,4 @@
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
<h1 align="center">
|
||||
<a name="logo" href="https://deckbrew.xyz/"><img src="https://deckbrew.xyz/logo.png" alt="Deckbrew logo" width="200"></a>
|
||||
<a name="logo" href="https://deckbrew.xyz/"><img src="https://deckbrew.xyz/static/icon-45ca1f5aea376a9ad37e92db906f283e.png" alt="Deckbrew logo" width="200"></a>
|
||||
<br>
|
||||
Decky Loader
|
||||
<br>
|
||||
<a name="download button" href="https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop"><img src="./docs/images/download_button.svg" alt="Download decky" width="350px" style="padding-top: 15px;"></a>
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/SteamDeckHomebrew/decky-loader/releases"><img src="https://img.shields.io/github/downloads/SteamDeckHomebrew/decky-loader/total" /></a>
|
||||
<a href="https://github.com/SteamDeckHomebrew/decky-loader/stargazers"><img src="https://img.shields.io/github/stars/SteamDeckHomebrew/decky-loader" /></a>
|
||||
<a href="https://github.com/SteamDeckHomebrew/decky-loader/commits/main"><img src="https://img.shields.io/github/last-commit/SteamDeckHomebrew/decky-loader.svg" /></a>
|
||||
<a href="https://weblate.werwolv.net/engage/decky/"><img src="https://weblate.werwolv.net/widgets/decky/-/decky/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="https://github.com/SteamDeckHomebrew/decky-loader/blob/main/LICENSE"><img src="https://img.shields.io/github/license/SteamDeckHomebrew/decky-loader" /></a>
|
||||
<a href="https://discord.gg/ZU74G2NJzk"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
|
||||
<a href="https://deckbrew.xyz/discord"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
|
||||
<br>
|
||||
<br>
|
||||
<img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">
|
||||
@@ -17,7 +20,7 @@
|
||||
|
||||
## 📖 About
|
||||
|
||||
Decky Loader is a homebrew plugin launcher for the Steam Deck. It can be used to [stylize your menus](https://github.com/suchmememanyskill/SDH-CssLoader), [change system sounds](https://github.com/EMERALD0874/SDH-AudioLoader), [adjust your screen saturation](https://github.com/libvibrant/vibrantDeck), [change additional system settings](https://github.com/NGnius/PowerTools), and [more](https://beta.deckbrew.xyz/).
|
||||
Decky Loader is a homebrew plugin launcher for the Steam Deck. It can be used to [stylize your menus](https://github.com/suchmememanyskill/SDH-CssLoader), [change system sounds](https://github.com/EMERALD0874/SDH-AudioLoader), [adjust your screen saturation](https://github.com/libvibrant/vibrantDeck), [change additional system settings](https://github.com/NGnius/PowerTools), and [more](https://plugins.deckbrew.xyz/).
|
||||
|
||||
For more information about Decky Loader as well as documentation and development tools, please visit [our wiki](https://deckbrew.xyz).
|
||||
|
||||
@@ -31,41 +34,40 @@ For more information about Decky Loader as well as documentation and development
|
||||
|
||||
### 🤔 Common Issues
|
||||
|
||||
- Crankshaft is incompatible with Decky Loader. If you are using Crankshaft, please uninstall it before installing Decky Loader.
|
||||
- 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.
|
||||
- If you are using any software that uses port 1337, please change its port to something else or uninstall it.
|
||||
- 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).
|
||||
|
||||
## 💾 Installation
|
||||
|
||||
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Settings menu.
|
||||
1. Navigate to the System menu and scroll to the System Settings. Toggle "Enable Developer Mode" so it is enabled.
|
||||
1. Navigate to the Developer menu and scroll to Miscellaneous. Toggle "CEF Remote Debugging" so it is enabled.
|
||||
1. Select "Restart Now" to apply your changes.
|
||||
- This installation can be done without an admin/sudo password set.
|
||||
1. Prepare a mouse and keyboard if possible.
|
||||
- Keyboards and mice can be connected to the Steam Deck via USB-C or Bluetooth.
|
||||
- Many Bluetooth keyboard and mouse apps are available for iOS and Android.
|
||||
- Many Bluetooth keyboard and mouse apps are available for iOS and Android. (KDE connect is preinstalled on the steam deck)
|
||||
- The Steam Link app is available on [Windows](https://media.steampowered.com/steamlink/windows/latest/SteamLink.zip), [macOS](https://apps.apple.com/us/app/steam-link/id1246969117), and [Linux](https://flathub.org/apps/details/com.valvesoftware.SteamLink). It works well as a remote desktop substitute.
|
||||
- If you have no other options, use the right trackpad as a mouse and press <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16>+<img src="./docs/images/light/x.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/x.svg#gh-light-mode-only" height=16> to open the on-screen keyboard as needed.
|
||||
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
|
||||
1. Select "Switch to Desktop".
|
||||
1. Open the Konsole app and enter the command `passwd`. You can skip this step if you have already created a sudo password using this command. ([YouTube Guide](https://www.youtube.com/watch?v=1vOMYGj22rQ))
|
||||
1. You will be prompted to create a password. Your text will not be visible. After you press enter, you will need to type your password again to confirm.
|
||||
1. Choose the version of Decky Loader you want to install and paste the following command into the Konsole app.
|
||||
1. Navigate to this Github page on a browser of your choice.
|
||||
1. Download the [installer file](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop). (If using firefox, it will be named `decky_installer.desktop.download`. Rename it to `decky_installer.desktop` before running it)
|
||||
1. Drag the file onto your desktop and double click it to run it.
|
||||
1. Either type your admin password or allow Decky to temporarily set your admin password to `Decky!` (this password will be removed after the installer finishes)
|
||||
1. Choose the version of Decky Loader you want to install.
|
||||
- **Latest Release**
|
||||
Intended for most users. This is the latest stable version of Decky Loader.
|
||||
`curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_release.sh | sh`
|
||||
- **Latest Pre-Release**
|
||||
Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://deckbrew.xyz/en/loader-dev/development).
|
||||
`curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_prerelease.sh | sh`
|
||||
Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://wiki.deckbrew.xyz/en/loader-dev/development).
|
||||
1. Open the Return to Gaming Mode shortcut on your desktop.
|
||||
|
||||
- There is also a fast install for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/install_release.sh | sh` and type your password when prompted.
|
||||
|
||||
### 👋 Uninstallation
|
||||
|
||||
We are sorry to see you go! If you are considering uninstalling because you are having issues, please consider [opening an issue](https://github.com/SteamDeckHomebrew/decky-loader/issues) or [joining our Discord](https://discord.gg/ZU74G2NJzk) so we can help you and other users.
|
||||
We are sorry to see you go! If you are considering uninstalling because you are having issues, please consider [opening an issue](https://github.com/SteamDeckHomebrew/decky-loader/issues) or [joining our Discord](https://deckbrew.xyz/discord) so we can help you and other users.
|
||||
|
||||
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
|
||||
1. Select "Switch to Desktop".
|
||||
1. Open the Konsole app and run `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/uninstall.sh | sh`.
|
||||
1. Run the installer file again, and select `uninstall decky loader`.
|
||||
- There is also a fast uninstall for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/uninstall.sh | sh` and type your password when prompted.
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
@@ -82,15 +84,16 @@ Now that you have Decky Loader installed, you can start using plugins. Each plug
|
||||
|
||||
### 🛠️ Plugin Development
|
||||
|
||||
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://discord.gg/ZU74G2NJzk) if you have any questions.
|
||||
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://deckbrew.xyz/discord) if you have any questions.
|
||||
|
||||
### 🤝 Contributing
|
||||
|
||||
Please consult [the wiki page regarding development](https://deckbrew.xyz/en/loader-dev/development) for more information on installing development versions of Decky Loader. You can also install the Steam Deck UI on a Windows or Linux computer for testing by following [this YouTube guide](https://youtu.be/1IAbZte8e7E?t=112).
|
||||
Please consult [the wiki page regarding development](https://wiki.deckbrew.xyz/en/loader-dev/development) for more information on installing development versions of Decky Loader. You can also install the Steam Deck UI on a Windows or Linux computer for testing by following [this YouTube guide](https://youtu.be/1IAbZte8e7E?t=112).
|
||||
|
||||
1. Clone the repository using the latest commit to main before starting your PR.
|
||||
1. In your clone of the repository, run these commands.
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm i
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
+2
-2
@@ -26,10 +26,10 @@ cd ..
|
||||
|
||||
if [[ "$type" == "release" ]]; then
|
||||
printf "release!\n"
|
||||
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts
|
||||
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts --container-architecture linux/amd64
|
||||
elif [[ "$type" == "prerelease" ]]; then
|
||||
printf "prerelease!\n"
|
||||
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts
|
||||
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts --container-architecture linux/amd64
|
||||
else
|
||||
printf "Release type unspecified/badly specified.\n"
|
||||
printf "Options: 'release' or 'prerelease'\n"
|
||||
|
||||
+143
-66
@@ -1,5 +1,7 @@
|
||||
# Full imports
|
||||
import json
|
||||
# import pprint
|
||||
# from pprint import pformat
|
||||
|
||||
# Partial imports
|
||||
from aiohttp import ClientSession, web
|
||||
@@ -10,13 +12,13 @@ from io import BytesIO
|
||||
from logging import getLogger
|
||||
from os import R_OK, W_OK, path, rename, listdir, access, mkdir
|
||||
from shutil import rmtree
|
||||
from subprocess import call
|
||||
from time import time
|
||||
from zipfile import ZipFile
|
||||
from localplatform import chown, chmod
|
||||
|
||||
# Local modules
|
||||
from helpers import get_ssl_context, get_user, get_user_group, download_remote_binary_to_path
|
||||
from injector import get_tab, inject_to_tab
|
||||
from helpers import get_ssl_context, download_remote_binary_to_path
|
||||
from injector import get_gamepadui_tab
|
||||
|
||||
logger = getLogger("Browser")
|
||||
|
||||
@@ -28,10 +30,11 @@ class PluginInstallContext:
|
||||
self.hash = hash
|
||||
|
||||
class PluginBrowser:
|
||||
def __init__(self, plugin_path, plugins, loader) -> None:
|
||||
def __init__(self, plugin_path, plugins, loader, settings) -> None:
|
||||
self.plugin_path = plugin_path
|
||||
self.plugins = plugins
|
||||
self.loader = loader
|
||||
self.settings = settings
|
||||
self.install_requests = {}
|
||||
|
||||
def _unzip_to_plugin_dir(self, zip, name, hash):
|
||||
@@ -40,14 +43,13 @@ class PluginBrowser:
|
||||
return False
|
||||
zip_file = ZipFile(zip)
|
||||
zip_file.extractall(self.plugin_path)
|
||||
plugin_dir = self.find_plugin_folder(name)
|
||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), plugin_dir])
|
||||
code_chmod = call(["chmod", "-R", "555", plugin_dir])
|
||||
if code_chown != 0 or code_chmod != 0:
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
|
||||
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
|
||||
|
||||
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):
|
||||
rv = False
|
||||
try:
|
||||
@@ -55,18 +57,16 @@ class PluginBrowser:
|
||||
pluginBinPath = path.join(pluginBasePath, 'bin')
|
||||
|
||||
if access(packageJsonPath, R_OK):
|
||||
with open(packageJsonPath, 'r') as f:
|
||||
with open(packageJsonPath, "r", encoding="utf-8") as f:
|
||||
packageJson = json.load(f)
|
||||
if len(packageJson["remote_binary"]) > 0:
|
||||
if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0:
|
||||
# create bin directory if needed.
|
||||
rc=call(["chmod", "-R", "777", pluginBasePath])
|
||||
chmod(pluginBasePath, 777)
|
||||
if access(pluginBasePath, W_OK):
|
||||
|
||||
if not path.exists(pluginBinPath):
|
||||
mkdir(pluginBinPath)
|
||||
|
||||
if not access(pluginBinPath, W_OK):
|
||||
rc=call(["chmod", "-R", "777", pluginBinPath])
|
||||
chmod(pluginBinPath, 777)
|
||||
|
||||
rv = True
|
||||
for remoteBinary in packageJson["remote_binary"]:
|
||||
@@ -78,99 +78,176 @@ class PluginBrowser:
|
||||
rv = False
|
||||
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
|
||||
|
||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
|
||||
rc=call(["chmod", "-R", "555", pluginBasePath])
|
||||
chown(self.plugin_path)
|
||||
chmod(pluginBasePath, 555)
|
||||
else:
|
||||
rv = True
|
||||
logger.debug(f"No Remote Binaries to Download")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
rv = False
|
||||
logger.debug(str(e))
|
||||
|
||||
return rv
|
||||
|
||||
"""Return the filename (only) for the specified plugin"""
|
||||
def find_plugin_folder(self, name):
|
||||
for folder in listdir(self.plugin_path):
|
||||
try:
|
||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), 'r') as f:
|
||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), "r", encoding="utf-8") as f:
|
||||
plugin = json.load(f)
|
||||
|
||||
if plugin['name'] == name:
|
||||
return path.join(self.plugin_path, folder)
|
||||
return folder
|
||||
except:
|
||||
logger.debug(f"skipping {folder}")
|
||||
|
||||
async def uninstall_plugin(self, name):
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = True
|
||||
tab = await get_tab("SP")
|
||||
tab = await get_gamepadui_tab()
|
||||
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
|
||||
try:
|
||||
logger.info("uninstalling " + name)
|
||||
logger.info(" at dir " + self.find_plugin_folder(name))
|
||||
logger.debug("unloading %s" % str(name))
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
||||
if self.plugins[name]:
|
||||
logger.info(" at dir " + plugin_dir)
|
||||
logger.debug("calling frontend unload for %s" % str(name))
|
||||
res = await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
||||
logger.debug("result of unload from UI: %s", res)
|
||||
# plugins_snapshot = self.plugins.copy()
|
||||
# snapshot_string = pformat(plugins_snapshot)
|
||||
# logger.debug("current plugins: %s", snapshot_string)
|
||||
if name in self.plugins:
|
||||
logger.debug("Plugin %s was found", name)
|
||||
self.plugins[name].stop()
|
||||
logger.debug("Plugin %s was stopped", name)
|
||||
del self.plugins[name]
|
||||
logger.debug("Plugin %s was removed from the dictionary", name)
|
||||
self.cleanup_plugin_settings(name)
|
||||
logger.debug("removing files %s" % str(name))
|
||||
rmtree(self.find_plugin_folder(name))
|
||||
rmtree(plugin_dir)
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"Plugin {name} not installed, skipping uninstallation")
|
||||
except Exception as e:
|
||||
logger.error(f"Plugin {name} in {self.find_plugin_folder(name)} was not uninstalled")
|
||||
logger.error(f"Error at %s", exc_info=e)
|
||||
logger.error(f"Plugin {name} in {plugin_dir} was not uninstalled")
|
||||
logger.error(f"Error at {str(e)}", exc_info=e)
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
|
||||
async def _install(self, artifact, name, version, hash):
|
||||
# Will be set later in code
|
||||
res_zip = None
|
||||
|
||||
# Check if plugin is installed
|
||||
isInstalled = False
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = True
|
||||
try:
|
||||
await self.uninstall_plugin(name)
|
||||
pluginFolderPath = self.find_plugin_folder(name)
|
||||
if pluginFolderPath:
|
||||
isInstalled = True
|
||||
except:
|
||||
logger.error(f"Plugin {name} not installed, skipping uninstallation")
|
||||
logger.info(f"Installing {name} (Version: {version})")
|
||||
async with ClientSession() as client:
|
||||
logger.debug(f"Fetching {artifact}")
|
||||
res = await client.get(artifact, ssl=get_ssl_context())
|
||||
if res.status == 200:
|
||||
logger.debug("Got 200. Reading...")
|
||||
data = await res.read()
|
||||
logger.debug(f"Read {len(data)} bytes")
|
||||
res_zip = BytesIO(data)
|
||||
logger.debug("Unzipping...")
|
||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||
if ret:
|
||||
plugin_dir = self.find_plugin_folder(name)
|
||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
if name in self.loader.plugins:
|
||||
self.loader.plugins[name].stop()
|
||||
self.loader.plugins.pop(name, None)
|
||||
await sleep(1)
|
||||
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_dir)
|
||||
# await inject_to_tab("SP", "window.syncDeckyPlugins()")
|
||||
else:
|
||||
logger.fatal(f"Failed Downloading Remote Binaries")
|
||||
else:
|
||||
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
else:
|
||||
logger.fatal(f"Could not fetch from URL. {await res.text()}")
|
||||
logger.error(f"Failed to determine if {name} is already installed, continuing anyway.")
|
||||
|
||||
async def request_plugin_install(self, artifact, name, version, hash):
|
||||
# Check if the file is a local file or a URL
|
||||
if artifact.startswith("file://"):
|
||||
logger.info(f"Installing {name} from local ZIP file (Version: {version})")
|
||||
res_zip = BytesIO(open(artifact[7:], "rb").read())
|
||||
else:
|
||||
logger.info(f"Installing {name} from URL (Version: {version})")
|
||||
async with ClientSession() as client:
|
||||
logger.debug(f"Fetching {artifact}")
|
||||
res = await client.get(artifact, ssl=get_ssl_context())
|
||||
if res.status == 200:
|
||||
logger.debug("Got 200. Reading...")
|
||||
data = await res.read()
|
||||
logger.debug(f"Read {len(data)} bytes")
|
||||
res_zip = BytesIO(data)
|
||||
else:
|
||||
logger.fatal(f"Could not fetch from URL. {await res.text()}")
|
||||
|
||||
# Check to make sure we got the file
|
||||
if res_zip is None:
|
||||
logger.fatal(f"Could not fetch {artifact}")
|
||||
return
|
||||
|
||||
# If plugin is installed, uninstall it
|
||||
if isInstalled:
|
||||
try:
|
||||
logger.debug("Uninstalling existing plugin...")
|
||||
await self.uninstall_plugin(name)
|
||||
except:
|
||||
logger.error(f"Plugin {name} could not be uninstalled.")
|
||||
|
||||
# Install the plugin
|
||||
logger.debug("Unzipping...")
|
||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||
if ret:
|
||||
plugin_folder = self.find_plugin_folder(name)
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
if name in self.loader.plugins:
|
||||
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)
|
||||
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})")
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
|
||||
async def request_plugin_install(self, artifact, name, version, hash, install_type):
|
||||
request_id = str(time())
|
||||
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
|
||||
tab = await get_tab("SP")
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.open_websocket()
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}')")
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}', {install_type})")
|
||||
|
||||
async def request_multiple_plugin_installs(self, requests):
|
||||
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([
|
||||
f"{{ name: '{req['name']}', version: '{req['version']}', hash: '{req['hash']}', install_type: {req['install_type']}}}" for req in requests
|
||||
])
|
||||
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.open_websocket()
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.addMultiplePluginsInstallPrompt('{request_id}', [{js_requests_parameter}])")
|
||||
|
||||
async def confirm_plugin_install(self, request_id):
|
||||
request = self.install_requests.pop(request_id)
|
||||
await self._install(request.artifact, request.name, request.version, request.hash)
|
||||
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):
|
||||
self.install_requests.pop(request_id)
|
||||
|
||||
def cleanup_plugin_settings(self, name):
|
||||
"""Removes any settings related to a plugin. Propably called when a plugin is uninstalled.
|
||||
|
||||
Args:
|
||||
name (string): The name of the plugin
|
||||
"""
|
||||
hidden_plugins = self.settings.getSetting("hiddenPlugins", [])
|
||||
if name in hidden_plugins:
|
||||
hidden_plugins.remove(name)
|
||||
self.settings.setSetting("hiddenPlugins", hidden_plugins)
|
||||
|
||||
|
||||
plugin_order = self.settings.getSetting("pluginOrder", [])
|
||||
|
||||
if name in plugin_order:
|
||||
plugin_order.remove(name)
|
||||
self.settings.setSetting("pluginOrder", plugin_order)
|
||||
|
||||
logger.debug("Removed any settings for plugin %s", name)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from enum import Enum
|
||||
|
||||
class UserType(Enum):
|
||||
HOST_USER = 1
|
||||
EFFECTIVE_USER = 2
|
||||
ROOT = 3
|
||||
+112
-76
@@ -1,27 +1,28 @@
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import uuid
|
||||
import os
|
||||
from subprocess import check_output
|
||||
from time import sleep
|
||||
import sys
|
||||
import subprocess
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
|
||||
import certifi
|
||||
from aiohttp.web import Response, middleware
|
||||
from aiohttp import ClientSession
|
||||
import localplatform
|
||||
from customtypes import UserType
|
||||
from logging import getLogger
|
||||
|
||||
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
||||
|
||||
# global vars
|
||||
csrf_token = str(uuid.uuid4())
|
||||
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
user = None
|
||||
group = None
|
||||
|
||||
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
||||
frontend_regex = re.compile("^/frontend/.*")
|
||||
logger = getLogger("Main")
|
||||
|
||||
def get_ssl_context():
|
||||
return ssl_ctx
|
||||
@@ -35,57 +36,41 @@ async def csrf_middleware(request, handler):
|
||||
return await handler(request)
|
||||
return Response(text='Forbidden', status='403')
|
||||
|
||||
# Get the user by checking for the first logged in user. As this is run
|
||||
# by systemd at startup the process is likely to start before the user
|
||||
# logs in, so we will wait here until they are available. Note that
|
||||
# other methods such as getenv wont work as there was no $SUDO_USER to
|
||||
# start the systemd service.
|
||||
def set_user():
|
||||
global user
|
||||
cmd = "who | awk '{print $1}' | sort | head -1"
|
||||
while user == None:
|
||||
name = check_output(cmd, shell=True).decode().strip()
|
||||
if name not in [None, '']:
|
||||
user = name
|
||||
sleep(0.1)
|
||||
|
||||
# Get the global user. get_user must be called first.
|
||||
def get_user() -> str:
|
||||
global user
|
||||
if user == None:
|
||||
raise ValueError("helpers.get_user method called before user variable was set. Run helpers.set_user first.")
|
||||
return user
|
||||
|
||||
# Set the global user group. get_user must be called first
|
||||
def set_user_group() -> str:
|
||||
global group
|
||||
global user
|
||||
if user == None:
|
||||
raise ValueError("helpers.set_user_dir method called before user variable was set. Run helpers.set_user first.")
|
||||
if group == None:
|
||||
group = check_output(["id", "-g", "-n", user]).decode().strip()
|
||||
|
||||
# Get the group of the global user. set_user_group must be called first.
|
||||
def get_user_group() -> str:
|
||||
global group
|
||||
if group == None:
|
||||
raise ValueError("helpers.get_user_group method called before group variable was set. Run helpers.set_user_group first.")
|
||||
return group
|
||||
|
||||
# Get the default home path unless a user is specified
|
||||
def get_home_path(username = None) -> str:
|
||||
if username == None:
|
||||
raise ValueError("Username not defined, no home path can be found.")
|
||||
else:
|
||||
return str("/home/"+username)
|
||||
|
||||
# Get the default homebrew path unless a user is specified
|
||||
# Get the default homebrew path unless a home_path is specified. home_path argument is deprecated
|
||||
def get_homebrew_path(home_path = None) -> str:
|
||||
if home_path == None:
|
||||
raise ValueError("Home path not defined, homebrew dir cannot be determined.")
|
||||
else:
|
||||
return str(home_path+"/homebrew")
|
||||
# return str(home_path+"/homebrew")
|
||||
return localplatform.get_unprivileged_path()
|
||||
|
||||
# Recursively create path and chown as user
|
||||
def mkdir_as_user(path):
|
||||
path = os.path.realpath(path)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
localplatform.chown(path)
|
||||
|
||||
# Fetches the version of loader
|
||||
def get_loader_version() -> str:
|
||||
try:
|
||||
with open(os.path.join(os.getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
|
||||
return version_file.readline().strip()
|
||||
except Exception as e:
|
||||
logger.warn(f"Failed to execute get_loader_version(): {str(e)}")
|
||||
return "unknown"
|
||||
|
||||
# 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:
|
||||
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)
|
||||
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:
|
||||
@@ -94,33 +79,84 @@ async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
||||
if os.access(os.path.dirname(path), os.W_OK):
|
||||
async with ClientSession() as client:
|
||||
res = await client.get(url, ssl=get_ssl_context())
|
||||
if res.status == 200:
|
||||
data = BytesIO(await res.read())
|
||||
remoteHash = sha256(data.getbuffer()).hexdigest()
|
||||
if binHash == remoteHash:
|
||||
data.seek(0)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data.getbuffer())
|
||||
rv = True
|
||||
else:
|
||||
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
|
||||
if res.status == 200:
|
||||
data = BytesIO(await res.read())
|
||||
remoteHash = sha256(data.getbuffer()).hexdigest()
|
||||
if binHash == remoteHash:
|
||||
data.seek(0)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data.getbuffer())
|
||||
rv = True
|
||||
else:
|
||||
rv = False
|
||||
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
|
||||
else:
|
||||
rv = False
|
||||
except:
|
||||
rv = False
|
||||
|
||||
return rv
|
||||
|
||||
# Deprecated
|
||||
def set_user():
|
||||
pass
|
||||
|
||||
# Deprecated
|
||||
def set_user_group() -> str:
|
||||
return get_user_group()
|
||||
|
||||
#########
|
||||
# Below is legacy code, provided for backwards compatibility. This will break on windows
|
||||
#########
|
||||
|
||||
# Get the user id hosting the plugin loader
|
||||
def get_user_id() -> int:
|
||||
return localplatform.localplatform._get_user_id()
|
||||
|
||||
# Get the user hosting the plugin loader
|
||||
def get_user() -> str:
|
||||
return localplatform.localplatform._get_user()
|
||||
|
||||
# Get the effective user id of the running process
|
||||
def get_effective_user_id() -> int:
|
||||
return localplatform.localplatform._get_effective_user_id()
|
||||
|
||||
# Get the effective user of the running process
|
||||
def get_effective_user() -> str:
|
||||
return localplatform.localplatform._get_effective_user()
|
||||
|
||||
# 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()
|
||||
|
||||
# Get the effective user group of the running process
|
||||
def get_effective_user_group() -> str:
|
||||
return localplatform.localplatform._get_effective_user_group()
|
||||
|
||||
# Get the user owner of the given file path.
|
||||
def get_user_owner(file_path) -> str:
|
||||
return localplatform.localplatform._get_user_owner(file_path)
|
||||
|
||||
# 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 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()
|
||||
|
||||
# Get the default home path unless a user is specified
|
||||
def get_home_path(username = 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:
|
||||
res = subprocess.run(["systemctl", "is-active", unit_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
return res.returncode == 0
|
||||
return await localplatform.service_active(unit_name)
|
||||
|
||||
async def stop_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
||||
cmd = ["systemctl", "stop", unit_name]
|
||||
async def stop_systemd_unit(unit_name: str) -> bool:
|
||||
return await localplatform.service_stop(unit_name)
|
||||
|
||||
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
async def start_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
||||
cmd = ["systemctl", "start", unit_name]
|
||||
|
||||
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
async def start_systemd_unit(unit_name: str) -> bool:
|
||||
return await localplatform.service_start(unit_name)
|
||||
|
||||
+366
-61
@@ -1,20 +1,27 @@
|
||||
#Injector code from https://github.com/SteamDeckHomebrew/steamdeck-ui-inject. More info on how it works there.
|
||||
# Injector code from https://github.com/SteamDeckHomebrew/steamdeck-ui-inject. More info on how it works there.
|
||||
|
||||
from asyncio import sleep
|
||||
from logging import debug, getLogger
|
||||
from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from typing import List
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from aiohttp import ClientSession, WSMsgType
|
||||
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
|
||||
from asyncio.exceptions import TimeoutError
|
||||
import uuid
|
||||
|
||||
BASE_ADDRESS = "http://localhost:8080"
|
||||
|
||||
logger = getLogger("Injector")
|
||||
|
||||
|
||||
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"]
|
||||
|
||||
self.websocket = None
|
||||
@@ -24,34 +31,318 @@ class Tab:
|
||||
self.client = ClientSession()
|
||||
self.websocket = await self.client.ws_connect(self.ws_url)
|
||||
|
||||
async def close_websocket(self):
|
||||
await self.websocket.close()
|
||||
await self.client.close()
|
||||
|
||||
async def listen_for_message(self):
|
||||
async for message in self.websocket:
|
||||
yield message
|
||||
|
||||
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):
|
||||
if self.websocket:
|
||||
self.cmd_id += 1
|
||||
dc["id"] = self.cmd_id
|
||||
await self.websocket.send_json(dc)
|
||||
return (await self.websocket.receive_json()) if receive else None
|
||||
if receive:
|
||||
async for msg in self.listen_for_message():
|
||||
if "id" in msg and msg["id"] == dc["id"]:
|
||||
return msg
|
||||
return None
|
||||
raise RuntimeError("Websocket not opened")
|
||||
|
||||
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"id": 1,
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": js,
|
||||
"userGesture": True,
|
||||
"awaitPromise": run_async
|
||||
}
|
||||
}, get_result)
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": js,
|
||||
"userGesture": True,
|
||||
"awaitPromise": run_async
|
||||
}
|
||||
}, get_result)
|
||||
|
||||
if manage_socket:
|
||||
await self.client.close()
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def has_global_var(self, var_name, manage_socket=True):
|
||||
res = await self.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False, manage_socket)
|
||||
|
||||
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):
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.close",
|
||||
}, False)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def enable(self):
|
||||
"""
|
||||
Enables page domain notifications.
|
||||
"""
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.enable",
|
||||
}, False)
|
||||
|
||||
async def disable(self):
|
||||
"""
|
||||
Disables page domain notifications.
|
||||
"""
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.disable",
|
||||
}, False)
|
||||
|
||||
async def refresh(self, manage_socket=True):
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.reload",
|
||||
}, False)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
|
||||
return
|
||||
async def reload_and_evaluate(self, js, manage_socket=True):
|
||||
"""
|
||||
Reloads the current tab, with JS to run on load via debugger
|
||||
"""
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.enable"
|
||||
}, True)
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "location.reload();",
|
||||
"userGesture": True,
|
||||
"awaitPromise": False
|
||||
}
|
||||
}, False)
|
||||
|
||||
breakpoint_res = await self._send_devtools_cmd({
|
||||
"method": "Debugger.setInstrumentationBreakpoint",
|
||||
"params": {
|
||||
"instrumentation": "beforeScriptExecution"
|
||||
}
|
||||
}, True)
|
||||
|
||||
logger.info(breakpoint_res)
|
||||
|
||||
# Page finishes loading when breakpoint hits
|
||||
|
||||
for x 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({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": js,
|
||||
"userGesture": True,
|
||||
"awaitPromise": False
|
||||
}
|
||||
}, False)
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.removeBreakpoint",
|
||||
"params": {
|
||||
"breakpointId": breakpoint_res["result"]["breakpointId"]
|
||||
}
|
||||
}, False)
|
||||
|
||||
for x in range(4):
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.resume"
|
||||
}, False)
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.disable"
|
||||
}, True)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
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):
|
||||
"""
|
||||
How the underlying call functions is not particularly clear from the devtools docs, so stealing puppeteer's description:
|
||||
|
||||
Adds a function which would be invoked in one of the following scenarios:
|
||||
* whenever the page is navigated
|
||||
* whenever the child frame is attached or navigated. In this case, the
|
||||
function is invoked in the context of the newly attached frame.
|
||||
|
||||
The function is invoked after the document was created but before any of
|
||||
its scripts were run. This is useful to amend the JavaScript environment,
|
||||
e.g. to seed `Math.random`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
js : str
|
||||
The script to evaluate on new document
|
||||
add_dom_wrapper : bool
|
||||
True to wrap the script in a wait for the 'DOMContentLoaded' event.
|
||||
DOM will usually not exist when this execution happens,
|
||||
so it is necessary to delay til DOM is loaded if you are modifying it
|
||||
manage_socket : bool
|
||||
True to have this function handle opening/closing the websocket for this tab
|
||||
get_result : bool
|
||||
True to wait for the result of this call
|
||||
|
||||
Returns
|
||||
-------
|
||||
int or None
|
||||
The identifier of the script added, used to remove it later.
|
||||
(see remove_script_to_evaluate_on_new_document below)
|
||||
None is returned if `get_result` is False
|
||||
"""
|
||||
try:
|
||||
|
||||
wrappedjs = """
|
||||
function scriptFunc() {
|
||||
{js}
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
addEventListener('DOMContentLoaded', () => {
|
||||
scriptFunc();
|
||||
});
|
||||
} else {
|
||||
scriptFunc();
|
||||
}
|
||||
""".format(js=js) if add_dom_wrapper else js
|
||||
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.addScriptToEvaluateOnNewDocument",
|
||||
"params": {
|
||||
"source": wrappedjs
|
||||
}
|
||||
}, get_result)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def remove_script_to_evaluate_on_new_document(self, script_id, manage_socket=True):
|
||||
"""
|
||||
Removes a script from a page that was added with `add_script_to_evaluate_on_new_document`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
script_id : int
|
||||
The identifier of the script to remove (returned from `add_script_to_evaluate_on_new_document`)
|
||||
"""
|
||||
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.removeScriptToEvaluateOnNewDocument",
|
||||
"params": {
|
||||
"identifier": script_id
|
||||
}
|
||||
}, False)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
|
||||
async def has_element(self, element_name, manage_socket=True):
|
||||
res = await self.evaluate_js(f"document.getElementById('{element_name}') != null", False, manage_socket)
|
||||
|
||||
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):
|
||||
try:
|
||||
css_id = str(uuid.uuid4())
|
||||
|
||||
result = await self.evaluate_js(
|
||||
f"""
|
||||
(function() {{
|
||||
const style = document.createElement('style');
|
||||
style.id = "{css_id}";
|
||||
document.head.append(style);
|
||||
style.textContent = `{style}`;
|
||||
}})()
|
||||
""", False, manage_socket)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result["result"]
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": css_id
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def remove_css(self, css_id, manage_socket=True):
|
||||
try:
|
||||
result = await self.evaluate_js(
|
||||
f"""
|
||||
(function() {{
|
||||
let style = document.getElementById("{css_id}");
|
||||
|
||||
if (style.nodeName.toLowerCase() == 'style')
|
||||
style.parentNode.removeChild(style);
|
||||
}})()
|
||||
""", False, manage_socket)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def get_steam_resource(self, url):
|
||||
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
|
||||
return res["result"]["result"]["value"]
|
||||
@@ -59,59 +350,73 @@ class Tab:
|
||||
def __repr__(self):
|
||||
return self.title
|
||||
|
||||
async def get_tabs():
|
||||
async with ClientSession() as web:
|
||||
res = {}
|
||||
|
||||
while True:
|
||||
try:
|
||||
res = await web.get(f"{BASE_ADDRESS}/json")
|
||||
except ClientConnectorError:
|
||||
logger.debug("ClientConnectorError excepted.")
|
||||
async def get_tabs() -> List[Tab]:
|
||||
res = {}
|
||||
|
||||
na = False
|
||||
while True:
|
||||
try:
|
||||
async with ClientSession() as web:
|
||||
res = await web.get(f"{BASE_ADDRESS}/json", timeout=3)
|
||||
except ClientConnectorError:
|
||||
if not na:
|
||||
logger.debug("Steam isn't available yet. Wait for a moment...")
|
||||
logger.error(format_exc())
|
||||
await sleep(5)
|
||||
else:
|
||||
break
|
||||
|
||||
if res.status == 200:
|
||||
r = await res.json()
|
||||
return [Tab(i) for i in r]
|
||||
na = True
|
||||
await sleep(5)
|
||||
except ClientOSError:
|
||||
logger.warn(f"The request to {BASE_ADDRESS}/json was reset")
|
||||
await sleep(1)
|
||||
except TimeoutError:
|
||||
logger.warn(f"The request to {BASE_ADDRESS}/json timed out")
|
||||
await sleep(1)
|
||||
else:
|
||||
raise Exception(f"/json did not return 200. {await res.text()}")
|
||||
break
|
||||
|
||||
async def get_tab(tab_name):
|
||||
if res.status == 200:
|
||||
r = await res.json()
|
||||
return [Tab(i) for i in r]
|
||||
else:
|
||||
raise Exception(f"/json did not return 200. {await res.text()}")
|
||||
|
||||
|
||||
async def get_tab(tab_name) -> 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:
|
||||
tabs = await get_tabs()
|
||||
tab = next((i for i in tabs if test(i)), None)
|
||||
if not tab:
|
||||
raise ValueError(f"Tab not found by lambda")
|
||||
return tab
|
||||
|
||||
SHARED_CTX_NAMES = ["SharedJSContext", "Steam Shared Context presented by Valve™", "Steam", "SP"]
|
||||
CLOSEABLE_URLS = ["about:blank", "data:text/html,%3Cbody%3E%3C%2Fbody%3E"] # Closing anything other than these *really* likes to crash Steam
|
||||
DO_NOT_CLOSE_URL = "Valve Steam Gamepad/default" # Steam Big Picture Mode tab
|
||||
|
||||
def tab_is_gamepadui(t: Tab) -> bool:
|
||||
return "https://steamloopback.host/routes/" in t.url and t.title in SHARED_CTX_NAMES
|
||||
|
||||
async def get_gamepadui_tab() -> Tab:
|
||||
tabs = await get_tabs()
|
||||
tab = next((i for i in tabs if tab_is_gamepadui(i)), None)
|
||||
if not tab:
|
||||
raise ValueError(f"GamepadUI Tab not found")
|
||||
return tab
|
||||
|
||||
async def inject_to_tab(tab_name, js, run_async=False):
|
||||
tab = await get_tab(tab_name)
|
||||
|
||||
return await tab.evaluate_js(js, run_async)
|
||||
|
||||
async def tab_has_global_var(tab_name, var_name):
|
||||
try:
|
||||
tab = await get_tab(tab_name)
|
||||
except ValueError:
|
||||
return False
|
||||
res = await tab.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False)
|
||||
|
||||
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 tab_has_element(tab_name, element_name):
|
||||
try:
|
||||
tab = await get_tab(tab_name)
|
||||
except ValueError:
|
||||
return False
|
||||
res = await tab.evaluate_js(f"document.getElementById('{element_name}') != null", False)
|
||||
|
||||
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_old_tabs():
|
||||
tabs = await get_tabs()
|
||||
for t in tabs:
|
||||
if not t.title or (t.title not in SHARED_CTX_NAMES and any(url in t.url for url in CLOSEABLE_URLS) and DO_NOT_CLOSE_URL not in t.url):
|
||||
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
|
||||
await t.close()
|
||||
await sleep(0.5)
|
||||
|
||||
+36
-21
@@ -6,19 +6,13 @@ from pathlib import Path
|
||||
from traceback import print_exc
|
||||
|
||||
from aiohttp import web
|
||||
from genericpath import exists
|
||||
from os.path import exists
|
||||
from watchdog.events import RegexMatchingEventHandler
|
||||
from watchdog.utils import UnsupportedLibc
|
||||
from watchdog.observers import Observer
|
||||
|
||||
try:
|
||||
from watchdog.observers.inotify import InotifyObserver as Observer
|
||||
except UnsupportedLibc:
|
||||
from watchdog.observers.fsevents import FSEventsObserver as Observer
|
||||
|
||||
from injector import get_tab, inject_to_tab
|
||||
from injector import get_tab, get_gamepadui_tab
|
||||
from plugin import PluginWrapper
|
||||
|
||||
|
||||
class FileChangeHandler(RegexMatchingEventHandler):
|
||||
def __init__(self, queue, plugin_path) -> None:
|
||||
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$'])
|
||||
@@ -68,25 +62,27 @@ class Loader:
|
||||
self.logger = getLogger("Loader")
|
||||
self.plugin_path = plugin_path
|
||||
self.logger.info(f"plugin_path: {self.plugin_path}")
|
||||
self.plugins = {}
|
||||
self.plugins : dict[str, PluginWrapper] = {}
|
||||
self.watcher = None
|
||||
self.live_reload = live_reload
|
||||
self.reload_queue = Queue()
|
||||
self.loop.create_task(self.handle_reloads())
|
||||
|
||||
if live_reload:
|
||||
self.reload_queue = Queue()
|
||||
self.observer = Observer()
|
||||
self.watcher = FileChangeHandler(self.reload_queue, plugin_path)
|
||||
self.observer.schedule(self.watcher, self.plugin_path, recursive=True)
|
||||
self.observer.start()
|
||||
self.loop.create_task(self.handle_reloads())
|
||||
self.loop.create_task(self.enable_reload_wait())
|
||||
|
||||
|
||||
server_instance.add_routes([
|
||||
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
||||
web.get("/locales/{path:.*}", self.handle_frontend_locales),
|
||||
web.get("/plugins", self.get_plugins),
|
||||
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
|
||||
web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call),
|
||||
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
|
||||
web.post("/plugins/{plugin_name}/reload", self.handle_backend_reload_request),
|
||||
|
||||
# The following is legacy plugin code.
|
||||
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
|
||||
@@ -105,6 +101,15 @@ class Loader:
|
||||
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def handle_frontend_locales(self, request):
|
||||
req_lang = request.match_info["path"]
|
||||
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):
|
||||
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])
|
||||
@@ -118,10 +123,10 @@ class Loader:
|
||||
def handle_frontend_bundle(self, request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), 'r') as bundle:
|
||||
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):
|
||||
def import_plugin(self, file, plugin_directory, refresh=False, batch=False):
|
||||
try:
|
||||
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
|
||||
if plugin.name in self.plugins:
|
||||
@@ -135,13 +140,15 @@ class Loader:
|
||||
self.logger.info(f"Plugin {plugin.name} is passive")
|
||||
self.plugins[plugin.name] = plugin.start()
|
||||
self.logger.info(f"Loaded {plugin.name}")
|
||||
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name, plugin.version))
|
||||
if not batch:
|
||||
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name, plugin.version))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not load {file}. {e}")
|
||||
print_exc()
|
||||
|
||||
async def dispatch_plugin(self, name, version):
|
||||
await inject_to_tab("SP", f"window.importDeckyPlugin('{name}', '{version}')")
|
||||
gpui_tab = await get_gamepadui_tab()
|
||||
await gpui_tab.evaluate_js(f"window.importDeckyPlugin('{name}', '{version}')")
|
||||
|
||||
def import_plugins(self):
|
||||
self.logger.info(f"import plugins from {self.plugin_path}")
|
||||
@@ -149,7 +156,7 @@ class Loader:
|
||||
directories = [i for i in listdir(self.plugin_path) if path.isdir(path.join(self.plugin_path, i)) and path.isfile(path.join(self.plugin_path, i, "plugin.json"))]
|
||||
for directory in directories:
|
||||
self.logger.info(f"found plugin: {directory}")
|
||||
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory)
|
||||
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory, False, True)
|
||||
|
||||
async def handle_reloads(self):
|
||||
while True:
|
||||
@@ -184,7 +191,7 @@ class Loader:
|
||||
"""
|
||||
async def load_plugin_main_view(self, request):
|
||||
plugin = self.plugins[request.match_info["name"]]
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template:
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), "r", encoding="utf-8") as template:
|
||||
template_data = template.read()
|
||||
ret = f"""
|
||||
<script src="/legacy/library.js"></script>
|
||||
@@ -200,14 +207,22 @@ class Loader:
|
||||
self.logger.info(path)
|
||||
ret = ""
|
||||
file_path = path.join(self.plugin_path, plugin.plugin_directory, route_path)
|
||||
with open(file_path, 'r') as resource_data:
|
||||
with open(file_path, "r", encoding="utf-8") as resource_data:
|
||||
ret = resource_data.read()
|
||||
|
||||
return web.Response(text=ret)
|
||||
|
||||
async def get_steam_resource(self, request):
|
||||
tab = await get_tab("QuickAccess")
|
||||
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):
|
||||
plugin_name : str = request.match_info["plugin_name"]
|
||||
plugin = self.plugins[plugin_name]
|
||||
|
||||
await self.reload_queue.put((plugin.file, plugin.plugin_directory))
|
||||
|
||||
return web.Response(status=200)
|
||||
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Aktualizační kanál",
|
||||
"prerelease": "Předběžná vydání",
|
||||
"stable": "Stabilní",
|
||||
"testing": "Testování"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"disabling": "Vypínám React DevTools",
|
||||
"enabling": "Zapínám React DevTools",
|
||||
"5secreload": "Znovu načtení za 5 vteřin"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Použít tuto složku"
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 plugin je v tomto seznamu skrytý",
|
||||
"hidden_few": "{{count}} pluginů je v tomto seznamu skryto",
|
||||
"hidden_other": "{{count}} pluginů je v tomto seznamu skryto"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Skryto z nabídky rychlého přístupu"
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Tento plugin má plný přístup k vašemu Steam Decku.",
|
||||
"plugin_install": "Instalovat",
|
||||
"plugin_no_desc": "Nebyl uveden žádný popis.",
|
||||
"plugin_version_label": "Verze pluginu"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Instalovat",
|
||||
"button_processing": "Instalování",
|
||||
"title": "Instalovat {{artifact}}",
|
||||
"desc": "Jste si jisti, že chcete nainstalovat {{artifact}} {{version}}?"
|
||||
},
|
||||
"no_hash": "Tento plugin nemá hash, instalujete jej na vlastní nebezpečí.",
|
||||
"reinstall": {
|
||||
"button_idle": "Přeinstalovat",
|
||||
"button_processing": "Přeinstalování",
|
||||
"title": "Přeinstalovat {{artifact}}",
|
||||
"desc": "Jste si jisti, že chcete přeinstalovat {{artifact}} {{version}}?"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Aktualizovat",
|
||||
"button_processing": "Aktualizování",
|
||||
"desc": "Jste si jisti, že chcete aktualizovat {{artifact}} {{version}}?",
|
||||
"title": "Aktualizovat {{artifact}}"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_one": "Upravit 1 plugin",
|
||||
"mixed_few": "Upravit {{count}} pluginů",
|
||||
"mixed_other": "Upravit {{count}} pluginů",
|
||||
"reinstall_one": "Přeinstalovat 1 plugin",
|
||||
"reinstall_few": "Přeinstalovat {{count}} pluginů",
|
||||
"reinstall_other": "Přeinstalovat {{count}} pluginů",
|
||||
"install_one": "Instalovat 1 plugin",
|
||||
"install_few": "Instalovat {{count}} pluginů",
|
||||
"install_other": "Instalovat {{count}} pluginů",
|
||||
"update_one": "Aktualizovat 1 plugin",
|
||||
"update_few": "Aktualizovat {{count}} pluginů",
|
||||
"update_other": "Aktualizovat {{count}} pluginů"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Potvrdit",
|
||||
"loading": "Probíhá"
|
||||
},
|
||||
"description": {
|
||||
"install": "Instalovat {{name}} {{version}}",
|
||||
"update": "Aktualizovat {{name}} na {{version}}",
|
||||
"reinstall": "Přeinstalovat {{name}} {{version}}"
|
||||
},
|
||||
"confirm": "Jste si jisti, že chcete udělat následující úpravy?"
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "Nejsou nainstalovány žádné pluginy!",
|
||||
"plugin_actions": "Akce pluginu",
|
||||
"reinstall": "Přeinstalovat",
|
||||
"reload": "Znovu načíst",
|
||||
"uninstall": "Odinstalovat",
|
||||
"update_to": "Aktualizovat na {{name}}",
|
||||
"show": "Rychlý přístup: Zobrazit",
|
||||
"hide": "Rychlý přístup: Skrýt",
|
||||
"update_all_one": "Aktualizovat 1 plugin",
|
||||
"update_all_few": "Aktualizovat {{count}} pluginů",
|
||||
"update_all_other": "Aktualizovat {{count}} pluginů"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Aktualizace na {{tag_name}} dostupná!",
|
||||
"error": "Chyba",
|
||||
"plugin_load_error": {
|
||||
"message": "Chyba při načítání pluginu {{name}}",
|
||||
"toast": "Chyba při načítání {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Odinstalovat",
|
||||
"desc": "Opravdu chcete odinstalovat {{name}}?",
|
||||
"title": "Odinstalovat {{name}}"
|
||||
},
|
||||
"plugin_update_one": "Je dostupná aktualizace pro 1 plugin!",
|
||||
"plugin_update_few": "Jsou dostupné aktualizace pro {{count}} pluginů!",
|
||||
"plugin_update_other": "Jsou dostupné aktualizace pro {{count}} pluginů!",
|
||||
"plugin_error_uninstall": "Načítání {{name}} způsobilo chybu uvedenou výše. To obvykle znamená, že plugin vyžaduje aktualizaci SteamUI. Zkontrolujte, zda je aktualizace k dispozici, nebo zvažte odstranění pluginu v nastavení Decky v sekci Pluginy."
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Otevřít konzoli",
|
||||
"label": "CEF konzole",
|
||||
"desc": "Otevře CEF konzoli. Užitečné pouze pro účely ladění. Věci zde jsou potenciálně nebezpečné a měly by být používány pouze v případě, že jste vývojář pluginů, nebo vás sem nějaký nasměroval."
|
||||
},
|
||||
"header": "Ostatní",
|
||||
"react_devtools": {
|
||||
"desc": "Umožňuje připojení k počítači, na kterém běží React DevTools. Změnou tohoto nastavení se znovu načte Steam. Před povolením nastavte IP adresu.",
|
||||
"ip_label": "IP adresa",
|
||||
"label": "Zapnout React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Instalovat",
|
||||
"button_zip": "Procházet",
|
||||
"header": "Pluginy třetí strany",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Instalovat plugin z URL",
|
||||
"label_zip": "Instalovat plugin ze ZIP souboru"
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "Instalace selhala! Podporovány jsou pouze soubory 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.",
|
||||
"label": "Zapnout Valve Internal"
|
||||
}
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"label": "Povolit vzdálené CEF ladění",
|
||||
"desc": "Umožní neověřený přístup k CEF ladění komukoli ve vaší síti"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky verze",
|
||||
"header": "O Decky"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Účast v betě"
|
||||
},
|
||||
"developer_mode": {
|
||||
"desc": "Zapíná vývojářské nastavení Decky.",
|
||||
"label": "Vývojářský režim"
|
||||
},
|
||||
"other": {
|
||||
"header": "Ostatní"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Aktualizace"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Vývojář",
|
||||
"general_title": "Obecné",
|
||||
"navbar_settings": "Nastavení Decky",
|
||||
"plugins_title": "Pluginy"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "Přispívání",
|
||||
"desc": "Pokud byste chtěli přispět do obchodu Decky Plugin Store, podívejte se na repozitář SteamDeckHomebrew/decky-plugin-template na GitHubu. Informace o vývoji a distribuci jsou k dispozici v README."
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtr",
|
||||
"label_def": "Vše"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Hledat"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Seřadit",
|
||||
"label_def": "Naposledy aktualizováno (Nejnovější)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Veškerý zdrojový kód pluginu je dostupný v repozitáři SteamDeckHomebrew/decky-plugin-database na GitHubu.",
|
||||
"label": "Zdrojový kód"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "O Decky Plugin Store",
|
||||
"alph_asce": "Abecedně (Z do A)",
|
||||
"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!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Vlastní obchod",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Vlastní",
|
||||
"default": "Výchozí",
|
||||
"label": "Kanál obchodu",
|
||||
"testing": "Testování"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"updates": {
|
||||
"lat_version": "Aktuální: běží na verzi {{ver}}",
|
||||
"reloading": "Znovu načítání",
|
||||
"updating": "Aktualizování",
|
||||
"check_button": "Zkontrolovat aktualizace",
|
||||
"checking": "Kontrolování",
|
||||
"cur_version": "Aktuální verze: {{ver}}",
|
||||
"install_button": "Instalovat aktualizaci",
|
||||
"label": "Aktualizace"
|
||||
},
|
||||
"decky_updates": "Aktualizace Decky",
|
||||
"patch_notes_desc": "Poznámky k verzi",
|
||||
"no_patch_notes_desc": "žádné poznámky pro tuto verzi"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Updatekanal",
|
||||
"prerelease": "Vorabveröffentlichung",
|
||||
"stable": "Standard",
|
||||
"testing": "Test"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"disabling": "Deaktiviere",
|
||||
"enabling": "Aktiviere",
|
||||
"5secreload": "Neu laden in 5 Sekunden"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Diesen Ordner verwenden"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_install": "Installieren",
|
||||
"plugin_no_desc": "Keine Beschreibung angegeben.",
|
||||
"plugin_version_label": "Erweiterungs Version",
|
||||
"plugin_full_access": "Diese Erweiterung hat uneingeschränkten Zugriff auf dein Steam Deck."
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Installieren",
|
||||
"button_processing": "Wird installiert",
|
||||
"desc": "Bist du dir sicher, dass du {{artifact}} {{version}} installieren willst?",
|
||||
"title": "Installiere {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Neu installieren",
|
||||
"button_processing": "Wird neu installiert",
|
||||
"desc": "Bist du dir sicher, dass du {{artifact}} {{version}} neu installieren willst?",
|
||||
"title": "Neu installation {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Aktualisieren",
|
||||
"button_processing": "Wird aktualisiert",
|
||||
"title": "Aktualisiere {{artifact}}",
|
||||
"desc": "Bist du dir sicher, dass du {{artifact}} {{version}} aktualisieren willst?"
|
||||
},
|
||||
"no_hash": "Diese Erweiterung besitzt keine Prüfsumme, Installation auf eigene Gefahr."
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "Keine Erweiterungen installiert!",
|
||||
"plugin_actions": "Erweiterungs Aktionen",
|
||||
"reinstall": "Neu installieren",
|
||||
"reload": "Neu laden",
|
||||
"uninstall": "Deinstallieren",
|
||||
"update_to": "Aktualisieren zu {{name}}",
|
||||
"update_all_one": "",
|
||||
"update_all_other": ""
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Eine neue Version ({{tag_name}}) ist verfügbar!",
|
||||
"error": "Fehler",
|
||||
"plugin_load_error": {
|
||||
"toast": "Fehler beim Laden von {{name}}",
|
||||
"message": "Fehler beim Laden von {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Deinstallieren",
|
||||
"desc": "Bist du dir sicher, dass du {{name}} deinstallieren willst?",
|
||||
"title": "Deinstalliere {{name}}"
|
||||
},
|
||||
"plugin_error_uninstall": "Das Laden von {{name}} hat einen Fehler verursacht. Dies bedeutet normalerweise, dass die Erweiterung ein Update für die neue Version von SteamUI benötigt. Prüfe in den Decky-Einstellungen im Bereich Erweiterungen, ob ein Update vorhanden ist.",
|
||||
"plugin_update_one": "1 Erweiterung kann aktualisiert werden!",
|
||||
"plugin_update_other": "{{count}} Erweiterungen können aktualisiert werden!"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"label": "Remote CEF Debugging Zugriff",
|
||||
"desc": "Erlaubt jedem aus dem Neztwerk unautorisierten Zugriff auf den CEF Debugger"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"header": "Sonstiges",
|
||||
"react_devtools": {
|
||||
"ip_label": "IP",
|
||||
"label": "Aktiviere React DevTools",
|
||||
"desc": "Erlaubt die Verbindung mit einem anderen Rechner, auf welchem React DevTools läuft. Eine Änderung startet Steam neu. Die IP Adresse muss vor Aktivierung ausgefüllt sein."
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_zip": "Durchsuchen",
|
||||
"header": "Erweiterungen von Drittanbietern",
|
||||
"label_desc": "URL",
|
||||
"label_zip": "Installiere Erweiterung via ZIP Datei",
|
||||
"button_install": "Installieren",
|
||||
"label_url": "Installiere Erweiterung via URL"
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "Installation fehlgeschlagen! Nur ZIP Datein werden unterstützt.",
|
||||
"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ü",
|
||||
"desc1": "Aktiviert das Valve-interne Entwickler Menü."
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky Version",
|
||||
"header": "Über"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Beta Teilnahme"
|
||||
},
|
||||
"developer_mode": {
|
||||
"desc": "Aktiviere Deckys Entwickleroptionen.",
|
||||
"label": "Entwickleroptionen"
|
||||
},
|
||||
"other": {
|
||||
"header": "Sonstiges"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Aktualisierungen"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Entwickler",
|
||||
"general_title": "Allgemein",
|
||||
"plugins_title": "Erweiterungen",
|
||||
"navbar_settings": "Decky Einstellungen"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "Mitwirken",
|
||||
"desc": "Wenn du Erweiterungen im Decky Store veröffentlichen willst, besuche die SteamDeckHomebrew/decky-plugin-template Repository auf GitHub. Informationen rund um Entwicklung und Veröffentlichung findest du in der README."
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filter",
|
||||
"label_def": "Alle"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Suche"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Sortierung",
|
||||
"label_def": "Zuletzt aktualisiert"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Jeder Erweiterungs Quellcode ist in der SteamDeckHomebrew/decky-plugin-database Repository auf GitHub verfügbar.",
|
||||
"label": "Quellcode"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Über",
|
||||
"alph_asce": "Alphabetisch (Z zu A)",
|
||||
"alph_desc": "Alphabetisch (A zu Z)",
|
||||
"title": "Durchstöbern"
|
||||
},
|
||||
"store_testing_cta": "Unterstütze das Decky Loader Team mit dem Testen von neuen Erweiterungen!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Benutzerdefinierter Marktplatz",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Benutzerdefiniert",
|
||||
"default": "Standard",
|
||||
"label": "Marktplatz Kanal",
|
||||
"testing": "Test"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky Aktualisierungen",
|
||||
"patch_notes_desc": "Patchnotizen",
|
||||
"updates": {
|
||||
"check_button": "Auf Aktualisierungen prüfen",
|
||||
"checking": "Wird überprüft",
|
||||
"cur_version": "Aktualle Version: {{ver}}",
|
||||
"install_button": "Aktualisierung installieren",
|
||||
"label": "Aktualisierungen",
|
||||
"lat_version": "{{ver}} ist die aktuellste",
|
||||
"reloading": "Lade neu",
|
||||
"updating": "Aktualisiere"
|
||||
},
|
||||
"no_patch_notes_desc": "Für diese Version gibt es keine Patchnotizen"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "",
|
||||
"hidden_other": ""
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"install_one": "",
|
||||
"install_other": "",
|
||||
"mixed_one": "",
|
||||
"mixed_other": "",
|
||||
"update_one": "",
|
||||
"update_other": "",
|
||||
"reinstall_one": "",
|
||||
"reinstall_other": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
{
|
||||
"SettingsDeveloperIndex": {
|
||||
"react_devtools": {
|
||||
"desc": "Επιτρέπει την σύνδεση με υπολογιστή που τρέχει React DevTools. Η αλλαγή αυτής της ρύθμισης θα προκαλέσει επαναφόρτωση του Steam. Ωρίστε την διεύθυνση IP πριν την ενεργοποιήσετε.",
|
||||
"ip_label": "IP",
|
||||
"label": "Ενεργοποίηση React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Εγκατάσταση",
|
||||
"button_zip": "Περιήγηση",
|
||||
"header": "Επεκτάσεις τρίτων",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Εγκατάσταση επέκτασης απο URL",
|
||||
"label_zip": "Εγκατάσταση επέκτασης από αρχείο ZIP"
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky",
|
||||
"body": "Η εγκατάσταση απέτυχε. Μόνο αρχεία ZIP επιτρέπονται."
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Ενεργοποιεί το μενού προγραμματιστή της Valve.",
|
||||
"desc2": "Μην αγγίξετε τίποτα σε αυτό το μενού εκτός και αν ξέρετε τι κάνει.",
|
||||
"label": "Ενεργοποιήση εσωτερικού μενού Valve"
|
||||
}
|
||||
},
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"prerelease": "Προ-κυκλοφορία",
|
||||
"stable": "Σταθερό",
|
||||
"label": "Κανάλι ενημερώσεων",
|
||||
"testing": "Δοκιμαστικό"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Γίνεται επαναφόρτωση σε 5 δευτερόλεπτα",
|
||||
"disabling": "Γίνεται απενεργοποίηση",
|
||||
"enabling": "Γίνεται ενεργοποίηση"
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_no_desc": "Δεν υπάρχει περιγραφή.",
|
||||
"plugin_full_access": "Αυτή η επέκταση έχει πλήρη πρόσβαση στο Steam Deck σας.",
|
||||
"plugin_install": "Εγκατάσταση",
|
||||
"plugin_version_label": "Έκδοση επέκτασης"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"desc": "Σίγουρα θέλετε να εγκαταστήσετε το {{artifact}}{{version}};",
|
||||
"button_idle": "Εγκατάσταση",
|
||||
"button_processing": "Γίνεται εγκατάσταση",
|
||||
"title": "Εγκατάσταση {{artifact}}"
|
||||
},
|
||||
"no_hash": "Αυτή η επέκταση δεν έχει υπογραφή, την εγκαθηστάτε με δικό σας ρίσκο.",
|
||||
"reinstall": {
|
||||
"button_idle": "Επανεγκατάσταση",
|
||||
"button_processing": "Γίνεται επανεγκατάσταση",
|
||||
"desc": "Σίγουρα θέλετε να επανεγκαταστήσετε το {{artifact}}{{version}};",
|
||||
"title": "Επανεγκατάσταση {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Ενημέρωση",
|
||||
"desc": "Σίγουρα θέλετε να ενημερώσετε το {{artifact}} {{version}};",
|
||||
"title": "Ενημέρωση {{artifact}}",
|
||||
"button_processing": "Γίνεται ενημέρωση"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "Δεν υπάρχουν εγκατεστημένες επεκτάσεις!",
|
||||
"plugin_actions": "Ενέργειες επεκτάσεων",
|
||||
"reinstall": "Επανεγκατάσταση",
|
||||
"reload": "Επαναφόρτωση",
|
||||
"uninstall": "Απεγκατάσταση",
|
||||
"update_to": "Ενημέρωση σε {{name}}",
|
||||
"update_all_one": "",
|
||||
"update_all_other": ""
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Ενημέρωση σε {{tag_name}} διαθέσιμη!",
|
||||
"error": "Σφάλμα",
|
||||
"plugin_error_uninstall": "Πηγαίντε στο <0></0> στο μενού του Decky για να απεγκαταστήσετε αυτή την επέκταση.",
|
||||
"plugin_load_error": {
|
||||
"message": "Σφάλμα στη φόρτωση της επέκτασης {{name}}",
|
||||
"toast": "Σφάλμα φόρτωσης {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Απεγκατάσταση",
|
||||
"desc": "Σίγουρα θέλετε να απεγκαταστήσετε το {{name}};",
|
||||
"title": "Απεγκατάσταση {{name}}"
|
||||
},
|
||||
"plugin_update_one": "",
|
||||
"plugin_update_other": ""
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"label": "Να επιτρέπεται η απομακρυσμένη πρόσβαση στον CEF debugger",
|
||||
"desc": "Να επιτρέπεται η ανεξέλεγκτη πρόσβαση στον CEF debugger σε οποιονδήποτε στο τοπικό δίκτυο"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Έκδοση Decky",
|
||||
"header": "Σχετικά"
|
||||
},
|
||||
"developer_mode": {
|
||||
"desc": "Ενεργοποιεί το μενού προγραμματιστή του Decky.",
|
||||
"label": "Λειτουργία προγραμματιστή"
|
||||
},
|
||||
"other": {
|
||||
"header": "Άλλα"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Ενημερώσεις"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Συμμετοχή στη Beta"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"plugins_title": "Επεκτάσεις",
|
||||
"developer_title": "Προγραμματιστής",
|
||||
"general_title": "Γενικά",
|
||||
"navbar_settings": "Ρυθμίσεις Decky"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "Συνεισφέροντας",
|
||||
"desc": "Αν θέλετε να συνεισφέρετε στο κατάστημα επεκτάσεων του Decky, τσεκάρετε το SteamDeckHomebrew/decky-plugin-template repository στο GitHub. Πληροφοριές σχετικά με τη δημιουργία και τη διανομή επεκτάσεων είναι διαθέσιμες στο README."
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Φίλτρο",
|
||||
"label_def": "Όλα"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Αναζήτηση"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Ταξινόμηση",
|
||||
"label_def": "Τελευταία ενημέρωση (Νεότερα)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Ο πηγαίος κώδικας όλων των επεκτάσεων είναι διαθέσιμος στο SteamDeckHomebrew/decky-plugin-database repository στο GitHub.",
|
||||
"label": "Πηγαίος κώδικας"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Σχετικά",
|
||||
"alph_asce": "Αλφαβητικά (Ζ σε Α)",
|
||||
"alph_desc": "Αλφαβητικά (Α σε Ζ)",
|
||||
"title": "Περιήγηση"
|
||||
},
|
||||
"store_testing_cta": "Παρακαλώ σκεφτείτε να τεστάρετε νέες επεκτάσεις για να βοηθήσετε την ομάδα του Decky Loader!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Προσαρμοσμένο κατάστημα",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Προσαρμοσμένο",
|
||||
"default": "Προεπιλεγμένο",
|
||||
"label": "Κανάλι καταστήματος",
|
||||
"testing": "Δοκιμαστικό"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"no_patch_notes_desc": "Κανένα ενημερωτικό σημείωμα για αυτή την έκδοση",
|
||||
"patch_notes_desc": "Σημειώσεις ενημέρωσης",
|
||||
"updates": {
|
||||
"check_button": "Έλεγχος για ενημερώσεις",
|
||||
"checking": "Γίνεται έλεγχος",
|
||||
"cur_version": "Τρέχουσα έκδοση: {{ver}}",
|
||||
"install_button": "Εγκατάσταση ενημέρωσης",
|
||||
"label": "Ενημερώσεις",
|
||||
"updating": "Γίνεται ενημέρωση",
|
||||
"lat_version": "Ενημερωμένο: τρέχουσα έκδοση {{ver}}",
|
||||
"reloading": "Γίνεται επαναφόρτωση"
|
||||
},
|
||||
"decky_updates": "Ενημερώσεις Decky"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Χρησιμοποιήστε αυτό το φάκελο"
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "",
|
||||
"hidden_other": ""
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_one": "",
|
||||
"mixed_other": "",
|
||||
"update_one": "",
|
||||
"update_other": "",
|
||||
"reinstall_one": "",
|
||||
"reinstall_other": "",
|
||||
"install_one": "",
|
||||
"install_other": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Update Channel",
|
||||
"prerelease": "Prerelease",
|
||||
"stable": "Stable",
|
||||
"testing": "Testing"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Reloading in 5 seconds",
|
||||
"disabling": "Disabling React DevTools",
|
||||
"enabling": "Enabling React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Back"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "The path specified is not valid. Please check it and reenter it correctly.",
|
||||
"perm_denied": "You do not have access to the specified directory. Please check if your user (deck on Steam Deck) has the corresponding permission to access the specified folder/file.",
|
||||
"unknown": "An unknown error occurred. The raw error is: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Select this file"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "All Files",
|
||||
"file_type": "File Type",
|
||||
"show_hidden": "Show Hidden Files"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Created (Oldest)",
|
||||
"created_desc": "Created (Newest)",
|
||||
"modified_asce": "Modified (Oldest)",
|
||||
"modified_desc": "Modified (Newest)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Size (Smallest)",
|
||||
"size_desc": "Size (Largest)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Folder",
|
||||
"select": "Use this folder",
|
||||
"show_more": "Show more files"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Are you sure you want to make the following modifications?",
|
||||
"description": {
|
||||
"install": "Install {{name}} {{version}}",
|
||||
"reinstall": "Reinstall {{name}} {{version}}",
|
||||
"update": "Update {{name}} to {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Confirm",
|
||||
"loading": "Working"
|
||||
},
|
||||
"title": {
|
||||
"install_one": "Install 1 plugin",
|
||||
"install_other": "Install {{count}} plugins",
|
||||
"mixed_one": "Modify {{count}} plugin",
|
||||
"mixed_other": "Modify {{count}} plugins",
|
||||
"reinstall_one": "Reinstall 1 plugin",
|
||||
"reinstall_other": "Reinstall {{count}} plugins",
|
||||
"update_one": "Update 1 plugin",
|
||||
"update_other": "Update {{count}} plugins"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "This plugin has full access to your Steam Deck.",
|
||||
"plugin_install": "Install",
|
||||
"plugin_no_desc": "No description provided.",
|
||||
"plugin_version_label": "Plugin Version"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Install",
|
||||
"button_processing": "Installing",
|
||||
"desc": "Are you sure you want to install {{artifact}} {{version}}?",
|
||||
"title": "Install {{artifact}}"
|
||||
},
|
||||
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstall",
|
||||
"button_processing": "Reinstalling",
|
||||
"desc": "Are you sure you want to reinstall {{artifact}} {{version}}?",
|
||||
"title": "Reinstall {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Update",
|
||||
"button_processing": "Updating",
|
||||
"desc": "Are you sure you want to update {{artifact}} {{version}}?",
|
||||
"title": "Update {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"hide": "Quick access: Hide",
|
||||
"no_plugin": "No plugins installed!",
|
||||
"plugin_actions": "Plugin Actions",
|
||||
"reinstall": "Reinstall",
|
||||
"reload": "Reload",
|
||||
"show": "Quick access: Show",
|
||||
"uninstall": "Uninstall",
|
||||
"update_all_one": "Update 1 plugin",
|
||||
"update_all_other": "Update {{count}} plugins",
|
||||
"update_to": "Update to {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Hidden from the quick access menu"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Update to {{tag_name}} available!",
|
||||
"error": "Error",
|
||||
"plugin_error_uninstall": "Loading {{name}} caused an exception as shown above. This usually means that the plugin requires an update for the new version of SteamUI. Check if an update is present or evaluate its removal in the Decky settings, in the Plugins section.",
|
||||
"plugin_load_error": {
|
||||
"message": "Error loading plugin {{name}}",
|
||||
"toast": "Error loading {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Uninstall",
|
||||
"desc": "Are you sure you want to uninstall {{name}}?",
|
||||
"title": "Uninstall {{name}}"
|
||||
},
|
||||
"plugin_update_one": "Updates available for 1 plugin!",
|
||||
"plugin_update_other": "Updates available for {{count}} plugins!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 plugin is hidden from this list",
|
||||
"hidden_other": "{{count}} plugins are hidden from this list"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Allow unauthenticated access to the CEF debugger to anyone in your network",
|
||||
"label": "Allow Remote CEF Debugging"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Open Console",
|
||||
"desc": "Opens the CEF Console. Only useful for debugging purposes. Stuff here is potentially dangerous and should only be used if you are a plugin dev, or are directed here by one.",
|
||||
"label": "CEF Console"
|
||||
},
|
||||
"header": "Other",
|
||||
"react_devtools": {
|
||||
"desc": "Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the IP address before enabling.",
|
||||
"ip_label": "IP",
|
||||
"label": "Enable React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Install",
|
||||
"button_zip": "Browse",
|
||||
"header": "Third-Party Plugins",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Install Plugin from URL",
|
||||
"label_zip": "Install Plugin from ZIP File"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Enables the Valve internal developer menu.",
|
||||
"desc2": "Do not touch anything in this menu unless you know what it does.",
|
||||
"label": "Enable Valve Internal"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky Version",
|
||||
"header": "About"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Beta participation"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Developer mode"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Decky update available",
|
||||
"header": "Notifications",
|
||||
"plugin_updates_label": "Plugin updates available"
|
||||
},
|
||||
"other": {
|
||||
"header": "Other"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Updates"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Developer",
|
||||
"general_title": "General",
|
||||
"plugins_title": "Plugins"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"desc": "If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template repository on GitHub. Information on development and distribution is available in the README.",
|
||||
"label": "Contributing"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filter",
|
||||
"label_def": "All"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Search"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Sort",
|
||||
"label_def": "Last Updated (Newest)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.",
|
||||
"label": "Source Code"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "About",
|
||||
"alph_asce": "Alphabetical (Z to A)",
|
||||
"alph_desc": "Alphabetical (A to Z)",
|
||||
"title": "Browse"
|
||||
},
|
||||
"store_testing_cta": "Please consider testing new plugins to help the Decky Loader team!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Custom Store",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Custom",
|
||||
"default": "Default",
|
||||
"label": "Store Channel",
|
||||
"testing": "Testing"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky Updates",
|
||||
"no_patch_notes_desc": "no patch notes for this version",
|
||||
"patch_notes_desc": "Patch Notes",
|
||||
"updates": {
|
||||
"check_button": "Check For Updates",
|
||||
"checking": "Checking",
|
||||
"cur_version": "Current version: {{ver}}",
|
||||
"install_button": "Install Update",
|
||||
"label": "Updates",
|
||||
"lat_version": "Up to date: running {{ver}}",
|
||||
"reloading": "Reloading",
|
||||
"updating": "Updating"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"SettingsDeveloperIndex": {
|
||||
"third_party_plugins": {
|
||||
"button_install": "Instalar",
|
||||
"button_zip": "Navegar",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Instalar plugin desde URL",
|
||||
"label_zip": "Instalar plugin desde archivo ZIP",
|
||||
"header": "Plugins de terceros"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc2": "No toques nada en este menú a menos que sepas lo que haces.",
|
||||
"label": "Activar menú interno de Valve",
|
||||
"desc1": "Activa el menú interno de desarrollo de Valve."
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky",
|
||||
"body": "¡Ha fallado la instalación! Solo se permiten archivos ZIP."
|
||||
},
|
||||
"cef_console": {
|
||||
"button": "Abrir consola",
|
||||
"label": "Consola CEF",
|
||||
"desc": "Abre la consola del CEF. Solamente es útil para propósitos de depuración. Las cosas que hagas aquí pueden ser potencialmente peligrosas y solo se debería usar si eres un desarrollador de plugins, o uno te ha dirigido aquí."
|
||||
},
|
||||
"react_devtools": {
|
||||
"ip_label": "IP",
|
||||
"label": "Activar DevTools de React",
|
||||
"desc": "Permite la conexión a un ordenador ejecutando las DevTools de React. Cambiar este ajuste recargará Steam. Configura la dirección IP antes de activarlo."
|
||||
},
|
||||
"header": "Otros"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Instalar",
|
||||
"button_processing": "Instalando",
|
||||
"title": "Instalar {{artifact}}",
|
||||
"desc": "¿Estás seguro de que quieres instalar {{artifact}} {{version}}?"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstalar",
|
||||
"button_processing": "Reinstalando",
|
||||
"desc": "¿Estás seguro de que quieres reinstalar {{artifact}} {{version}}?",
|
||||
"title": "Reinstalar {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_processing": "Actualizando",
|
||||
"button_idle": "Actualizar",
|
||||
"desc": "¿Estás seguro de que quieres actualizar {{artifact}} {{version}}?",
|
||||
"title": "Actualizar {{artifact}}"
|
||||
},
|
||||
"no_hash": "Este plugin no tiene un hash, lo estás instalando bajo tu propia responsabilidad."
|
||||
},
|
||||
"Developer": {
|
||||
"disabling": "Desactivando DevTools de React",
|
||||
"enabling": "Activando DevTools de React",
|
||||
"5secreload": "Recargando en 5 segundos"
|
||||
},
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"prerelease": "Prelanzamiento",
|
||||
"stable": "Estable",
|
||||
"label": "Canal de actualización",
|
||||
"testing": "Pruebas"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Este plugin tiene acceso completo a su Steam Deck.",
|
||||
"plugin_install": "Instalar",
|
||||
"plugin_version_label": "Versión de Plugin",
|
||||
"plugin_no_desc": "No se proporcionó una descripción."
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Usar esta carpeta"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"uninstall": "Desinstalar",
|
||||
"reinstall": "Reinstalar",
|
||||
"reload": "Recargar",
|
||||
"plugin_actions": "Acciones de plugin",
|
||||
"no_plugin": "¡No hay plugins instalados!",
|
||||
"update_all_one": "Actualizar 1 plugin",
|
||||
"update_all_many": "Actualizar {{count}} plugins",
|
||||
"update_all_other": "Actualizar {{count}} plugins",
|
||||
"update_to": "Actualizar a {{name}}"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"error": "Error",
|
||||
"plugin_uninstall": {
|
||||
"button": "Desinstalar",
|
||||
"desc": "¿Estás seguro de que quieres desinstalar {{name}}?",
|
||||
"title": "Desinstalar {{name}}"
|
||||
},
|
||||
"decky_title": "Decky",
|
||||
"plugin_update_one": "¡Actualización disponible para 1 plugin!",
|
||||
"plugin_update_many": "¡Actualizaciones disponibles para {{count}} plugins!",
|
||||
"plugin_update_other": "¡Actualizaciones disponibles para {{count}} plugins!",
|
||||
"decky_update_available": "¡Actualización {{tag_name}} disponible!",
|
||||
"plugin_load_error": {
|
||||
"message": "Se ha producido un error al cargar el plugin {{name}}",
|
||||
"toast": "Se ha producido un error al cargar {{name}}"
|
||||
},
|
||||
"plugin_error_uninstall": "Al cargar {{name}} se ha producido una excepción como se muestra arriba. Esto suele significar que el plugin requiere una actualización para la nueva versión de SteamUI. Comprueba si hay una actualización disponible o valora eliminarlo en los ajustes de Decky, en la sección Plugins."
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Permitir acceso no autenticado al CEF debugger a cualquier persona en su red",
|
||||
"label": "Permitir depuración remota del CEF"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"updates": {
|
||||
"header": "Actualizaciones"
|
||||
},
|
||||
"about": {
|
||||
"header": "Acerca de",
|
||||
"decky_version": "Versión de Decky"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Modo desarrollador",
|
||||
"desc": "Activa los ajustes de desarrollador de Decky."
|
||||
},
|
||||
"beta": {
|
||||
"header": "Participación en la beta"
|
||||
},
|
||||
"other": {
|
||||
"header": "Otros"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Desarrollador",
|
||||
"general_title": "General",
|
||||
"navbar_settings": "Ajustes de Decky",
|
||||
"plugins_title": "Plugins"
|
||||
},
|
||||
"Store": {
|
||||
"store_search": {
|
||||
"label": "Buscar"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Ordenar",
|
||||
"label_def": "Actualizado por última vez (Nuevos)"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Si desea contribuir a la tienda de plugins de Decky, mira el repositorio SteamDeckHomebrew/decky-plugin-template en GitHub. Hay información acerca del desarrollo y distribución en el archivo README.",
|
||||
"label": "Contribuyendo"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Información",
|
||||
"title": "Navegar",
|
||||
"alph_asce": "Alfabéticamente (Z-A)",
|
||||
"alph_desc": "Alfabéticamente (A-Z)"
|
||||
},
|
||||
"store_testing_cta": "¡Por favor considera probar plugins nuevos para ayudar al equipo de Decky Loader!",
|
||||
"store_source": {
|
||||
"desc": "El código fuente de los plugins está disponible en el repositiorio SteamDeckHomebrew/decky-plugin-database en GitHub.",
|
||||
"label": "Código fuente"
|
||||
},
|
||||
"store_filter": {
|
||||
"label_def": "Todos",
|
||||
"label": "Filtrar"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"updates": {
|
||||
"reloading": "Recargando",
|
||||
"updating": "Actualizando",
|
||||
"checking": "Buscando",
|
||||
"check_button": "Buscar actualizaciones",
|
||||
"install_button": "Instalar actualización",
|
||||
"label": "Actualizaciones",
|
||||
"lat_version": "Actualizado: ejecutando {{ver}}",
|
||||
"cur_version": "Versión actual: {{ver}}"
|
||||
},
|
||||
"decky_updates": "Actualizaciones de Decky",
|
||||
"no_patch_notes_desc": "No hay notas de parche para esta versión",
|
||||
"patch_notes_desc": "Notas de parche"
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"reinstall_one": "Reinstalar 1 plugin",
|
||||
"reinstall_many": "Reinstalar {{count}} plugins",
|
||||
"reinstall_other": "Reinstalar {{count}} plugins",
|
||||
"update_one": "Actualizar 1 plugin",
|
||||
"update_many": "Actualizar {{count}} plugins",
|
||||
"update_other": "Actualizar {{count}} plugins",
|
||||
"mixed_one": "Modificar 1 plugin",
|
||||
"mixed_many": "Modificar {{count}} plugins",
|
||||
"mixed_other": "Modificar {{count}} plugins",
|
||||
"install_one": "Instalar 1 plugin",
|
||||
"install_many": "Instalar {{count}} plugins",
|
||||
"install_other": "Instalar {{count}} plugins"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Confirmar",
|
||||
"loading": "Trabajando"
|
||||
},
|
||||
"confirm": "¿Estás seguro de que quieres hacer las siguientes modificaciones?",
|
||||
"description": {
|
||||
"install": "Instalar {{name}} {{version}}",
|
||||
"update": "Actualizar {{name}} a {{version}}",
|
||||
"reinstall": "Reinstalar {{name}} {{version}}"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"url_label": "URL",
|
||||
"label": "Tienda personalizada"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Personalizada",
|
||||
"default": "Por defecto",
|
||||
"label": "Canál de la tienda",
|
||||
"testing": "Pruebas"
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "",
|
||||
"hidden_many": "",
|
||||
"hidden_other": ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"SettingsDeveloperIndex": {
|
||||
"react_devtools": {
|
||||
"desc": "Permet la connexion à un ordinateur exécutant React DevTools. Changer ce paramètre rechargera Steam. Définissez l'adresse IP avant l'activation.",
|
||||
"ip_label": "IP",
|
||||
"label": "Activer React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Installer",
|
||||
"button_zip": "Parcourir",
|
||||
"header": "Plugins tiers",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Installer le plugin à partir d'un URL",
|
||||
"label_zip": "Installer le plugin à partir d'un fichier ZIP"
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "Échec de l'installation! Seuls les fichiers ZIP sont pris en charge.",
|
||||
"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.",
|
||||
"label": "Activer Valve Internal"
|
||||
}
|
||||
},
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"prerelease": "Avant-première",
|
||||
"label": "Canal de mise à jour",
|
||||
"stable": "Stable",
|
||||
"testing": "Test"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"store_channel": {
|
||||
"label": "Canal du Plugin Store",
|
||||
"testing": "Test",
|
||||
"custom": "Personnalisé",
|
||||
"default": "Par défaut"
|
||||
},
|
||||
"custom_store": {
|
||||
"label": "Plugin Store personnalisé",
|
||||
"url_label": "URL"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Mises à jour de Decky",
|
||||
"no_patch_notes_desc": "pas de notes de mise à jour pour cette version",
|
||||
"patch_notes_desc": "Notes de mise à jour",
|
||||
"updates": {
|
||||
"check_button": "Chercher les mises à jour",
|
||||
"checking": "Recherche",
|
||||
"cur_version": "Version actuelle: {{ver}}",
|
||||
"install_button": "Installer la mise à jour",
|
||||
"label": "Mises à jour",
|
||||
"lat_version": "À jour: version {{ver}}",
|
||||
"reloading": "Rechargement",
|
||||
"updating": "Mise à jour en cours"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Rechargement dans 5 secondes",
|
||||
"disabling": "Désactivation",
|
||||
"enabling": "Activation"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Utiliser ce dossier"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Ce plugin a un accès complet à votre Steam Deck.",
|
||||
"plugin_install": "Installer",
|
||||
"plugin_no_desc": "Aucune description fournie.",
|
||||
"plugin_version_label": "Version du plugin"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Installer",
|
||||
"button_processing": "Installation en cours",
|
||||
"title": "Installer {{artifact}}",
|
||||
"desc": "Êtes-vous sûr de vouloir installer {{artifact}} {{version}} ?"
|
||||
},
|
||||
"no_hash": "Ce plugin n'a pas de somme de contrôle, vous l'installez à vos risques et périls.",
|
||||
"reinstall": {
|
||||
"button_idle": "Réinstaller",
|
||||
"button_processing": "Réinstallation en cours",
|
||||
"desc": "Êtes-vous sûr de vouloir réinstaller {{artifact}} {{version}} ?",
|
||||
"title": "Réinstaller {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Mettre à jour",
|
||||
"button_processing": "Mise à jour",
|
||||
"title": "Mettre à jour {{artifact}}",
|
||||
"desc": "Êtes-vous sûr de vouloir mettre à jour {{artifact}} {{version}} ?"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"plugin_actions": "Plugin Actions",
|
||||
"reinstall": "Réinstaller",
|
||||
"reload": "Recharger",
|
||||
"uninstall": "Désinstaller",
|
||||
"update_to": "Mettre à jour vers {{name}}",
|
||||
"no_plugin": "Aucun plugin installé !",
|
||||
"update_all_one": "",
|
||||
"update_all_many": "",
|
||||
"update_all_other": ""
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"error": "Erreur",
|
||||
"plugin_error_uninstall": "Allez sur {{name}} dans le menu de Decky si vous voulez désinstaller ce plugin.",
|
||||
"plugin_load_error": {
|
||||
"message": "Erreur lors du chargement du plugin {{name}}",
|
||||
"toast": "Erreur lors du chargement de {{name}}"
|
||||
},
|
||||
"decky_update_available": "Mise à jour vers {{tag_name}} disponible !",
|
||||
"plugin_uninstall": {
|
||||
"button": "Désinstaller",
|
||||
"title": "Désinstaller {{name}}",
|
||||
"desc": "Êtes-vous sûr.e de vouloir désinstaller {{name}} ?"
|
||||
},
|
||||
"plugin_update_one": "",
|
||||
"plugin_update_many": "",
|
||||
"plugin_update_other": ""
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Autoriser l'accès non authentifié au débogueur CEF à toute personne de votre réseau",
|
||||
"label": "Autoriser le débogage CEF à distance"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Version de Decky",
|
||||
"header": "À propos"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Participation à la Bêta"
|
||||
},
|
||||
"developer_mode": {
|
||||
"desc": "Active les paramètres de développeur de Decky.",
|
||||
"label": "Mode développeur"
|
||||
},
|
||||
"other": {
|
||||
"header": "Autre"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Mises à jour"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Développeur",
|
||||
"general_title": "Général",
|
||||
"navbar_settings": "Paramètres de Decky",
|
||||
"plugins_title": "Plugins"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"desc": "Si vous souhaitez contribuer au Decky Plugin Store, consultez le dépôt SteamDeckHomebrew/decky-plugin-template sur GitHub. Des informations sur le développement et la distribution sont disponibles dans le fichier README.",
|
||||
"label": "Contributions"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtrer",
|
||||
"label_def": "Tous"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Rechercher"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Trier",
|
||||
"label_def": "Mises à jour (Plus récentes)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Tout le code source des plugins est disponible sur le dépôt SteamDeckHomebrew/decky-plugin-database sur GitHub.",
|
||||
"label": "Code Source"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "À propos",
|
||||
"alph_asce": "Alphabétique (Z à A)",
|
||||
"alph_desc": "Alphabétique (A à Z)",
|
||||
"title": "Explorer"
|
||||
},
|
||||
"store_testing_cta": "Pensez à tester de nouveaux plugins pour aider l'équipe Decky Loader !"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "",
|
||||
"hidden_many": "",
|
||||
"hidden_other": ""
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"reinstall_one": "",
|
||||
"reinstall_many": "",
|
||||
"reinstall_other": "",
|
||||
"install_one": "",
|
||||
"install_many": "",
|
||||
"install_other": "",
|
||||
"mixed_one": "",
|
||||
"mixed_many": "",
|
||||
"mixed_other": "",
|
||||
"update_one": "",
|
||||
"update_many": "",
|
||||
"update_other": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Canale di aggiornamento",
|
||||
"prerelease": "Prerilascio",
|
||||
"stable": "Stabile",
|
||||
"testing": "In prova"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Ricarico tra 5 secondi",
|
||||
"disabling": "Disabilito i tools di React",
|
||||
"enabling": "Abilito i tools di React"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Indietro"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Il percorso specificato non è valido. Controllalo e prova a reinserirlo di nuovo.",
|
||||
"unknown": "È avvenuto un'errore sconosciuto. L'errore segnalato è {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Seleziona questo file"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Tutti i file",
|
||||
"file_type": "Tipo di file",
|
||||
"show_hidden": "Mostra nascosti"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Creazione (meno recente)",
|
||||
"created_desc": "Creazione (più recente)",
|
||||
"modified_asce": "Modifica (meno recente)",
|
||||
"modified_desc": "Modifica (più recente)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Dimensione (più piccolo)",
|
||||
"size_desc": "Dimensione (più grande)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Cartella",
|
||||
"select": "Usa questa cartella",
|
||||
"show_more": "Mostra più file"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Sei sicuro di voler effettuare le modifiche seguenti?",
|
||||
"description": {
|
||||
"install": "Installa {{name}} {{version}}",
|
||||
"reinstall": "Reinstalla {{name}} {{version}}",
|
||||
"update": "Aggiorna {{name}} alla versione {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Conferma",
|
||||
"loading": "Elaboro"
|
||||
},
|
||||
"title": {
|
||||
"install_one": "Installa un plugin",
|
||||
"install_many": "Installa {{count}} plugins",
|
||||
"install_other": "Installa {{count}} plugins",
|
||||
"mixed_one": "Modifica un plugin",
|
||||
"mixed_many": "Modifica {{count}} plugins",
|
||||
"mixed_other": "Modifica {{count}} plugins",
|
||||
"reinstall_one": "Reinstalla un plugin",
|
||||
"reinstall_many": "Reinstalla {{count}} plugins",
|
||||
"reinstall_other": "Reinstalla {{count}} plugins",
|
||||
"update_one": "Aggiorna un plugin",
|
||||
"update_many": "Aggiorna {{count}} plugins",
|
||||
"update_other": "Aggiorna {{count}} plugins"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Questo plugin ha accesso completo al tuo Steam Deck.",
|
||||
"plugin_install": "Installa",
|
||||
"plugin_no_desc": "Nessuna descrizione fornita.",
|
||||
"plugin_version_label": "Versione Plugin"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Installa",
|
||||
"button_processing": "Installando",
|
||||
"desc": "Sei sicuro di voler installare {{artifact}} {{version}}?",
|
||||
"title": "Installa {{artifact}}"
|
||||
},
|
||||
"no_hash": "Questo plugin non ha un hash associato, lo stai installando a tuo rischio e pericolo.",
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstalla",
|
||||
"button_processing": "Reinstallando",
|
||||
"desc": "Sei sicuro di voler reinstallare {{artifact}} {{version}}?",
|
||||
"title": "Reinstalla {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Aggiorna",
|
||||
"button_processing": "Aggiornando",
|
||||
"desc": "Sei sicuro di voler aggiornare {{artifact}} {{version}}?",
|
||||
"title": "Aggiorna {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"hide": "Accesso rapido: Nascondi",
|
||||
"no_plugin": "Nessun plugin installato!",
|
||||
"plugin_actions": "Operazioni sui plugins",
|
||||
"reinstall": "Reinstalla",
|
||||
"reload": "Ricarica",
|
||||
"show": "Accesso rapido: Mostra",
|
||||
"uninstall": "Rimuovi",
|
||||
"update_all_one": "Aggiorna un plugin",
|
||||
"update_all_many": "Aggiorna {{count}} plugins",
|
||||
"update_all_other": "Aggiorna {{count}} plugins",
|
||||
"update_to": "Aggiorna a {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Nascosto dal menu di accesso rapido"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Disponibile aggiornamento a {{tag_name}}!",
|
||||
"error": "Errore",
|
||||
"plugin_error_uninstall": "Il plugin {{name}} ha causato un'eccezione che è descritta sopra. Questo tipicamente significa che il plugin deve essere aggiornato per funzionare sulla nuova versione di SteamUI. Controlla se è disponibile un aggiornamento o valutane la rimozione andando nelle impostazioni di Decky nella sezione Plugins.",
|
||||
"plugin_load_error": {
|
||||
"message": "Errore caricando il plugin {{name}}",
|
||||
"toast": "Errore caricando {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Rimuovi",
|
||||
"desc": "Sei sicuro di voler rimuovere {{name}}?",
|
||||
"title": "Rimuovi {{name}}"
|
||||
},
|
||||
"plugin_update_one": "Aggiornamento disponibile per 1 plugin!",
|
||||
"plugin_update_many": "Aggiornamenti disponibili per {{count}} plugins!",
|
||||
"plugin_update_other": "Aggiornamenti disponibili per {{count}} plugins!"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "Un plugin è nascosto dalla lista",
|
||||
"hidden_many": "Sono nascosti {{count}} plugin dalla lista",
|
||||
"hidden_other": "Sono nascosti {{count}} plugin dalla lista"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Aggiornamenti"
|
||||
},
|
||||
"notifications": {
|
||||
"header": "Notifiche",
|
||||
"decky_updates_label": "Aggiornamenti di Decky",
|
||||
"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",
|
||||
"label": "Permetti il debug remoto di CEF"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Apri la console",
|
||||
"desc": "Apri la console di CEF. Utile solamente per ragioni di debug. Questa opzione deve essere usata solo se sei uno sviluppatore di plugin o se uno di questi ti ha chiesto di farlo, visto che questa feature potrebbe essere potenzialmente pericolosa.",
|
||||
"label": "Console CEF"
|
||||
},
|
||||
"header": "Altro",
|
||||
"react_devtools": {
|
||||
"desc": "Abilita la connessione ad un computer che esegue i DevTools di React. Steam verrà ricaricato se lo stato cambia. Imposta il tuo indirizzo IP prima di abilitarlo.",
|
||||
"ip_label": "IP",
|
||||
"label": "Abilita i DevTools di React"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Installa",
|
||||
"button_zip": "Seleziona",
|
||||
"header": "Plugin di terze parti",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Installa plugin da un'indirizzo web",
|
||||
"label_zip": "Installa plugin da un file ZIP"
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "Installazione non riuscita! Solo supportati solo file ZIP.",
|
||||
"title": "Decky"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Abilita il menu di sviluppo interno di Valve.",
|
||||
"desc2": "Non toccare nulla in questo menu se non sai quello che fa.",
|
||||
"label": "Abilita Menu Sviluppatore"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Versione di Decky",
|
||||
"header": "Riguardo a"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Partecipazione alla beta"
|
||||
},
|
||||
"developer_mode": {
|
||||
"desc": "Abilità le impostazioni di sviluppo di Decky.",
|
||||
"label": "Modalità sviluppatore"
|
||||
},
|
||||
"other": {
|
||||
"header": "Altro"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Aggiornamenti"
|
||||
},
|
||||
"notifications": {
|
||||
"header": "Notifiche",
|
||||
"decky_updates_label": "Aggiornamenti di Decky",
|
||||
"plugin_updates_label": "Aggiornamenti dei plugins"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Sviluppatore",
|
||||
"general_title": "Generali",
|
||||
"navbar_settings": "Impostazioni Decky",
|
||||
"plugins_title": "Plugins"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"desc": "Se desideri contribuire allo store di Decky, puoi trovare un template caricato su GitHub all'indirizzo SteamDeckHomebrew/decky-plugin-template. Informazioni riguardo sviluppo e distribuzione sono disponibili nel README.",
|
||||
"label": "Contribuisci"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtra",
|
||||
"label_def": "Tutto"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Cerca"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Ordina",
|
||||
"label_def": "Ultimo aggiornato (Più recente)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Tutto il codice sorgente dei plugin è disponibile su GitHub all'indirizzo SteamDeckHomebrew/decky-plugin-database.",
|
||||
"label": "Codice Sorgente"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Riguardo a",
|
||||
"alph_asce": "Alfabetico (Z a A)",
|
||||
"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!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Negozio custom",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Personalizzato",
|
||||
"default": "Default",
|
||||
"label": "Canale del negozio",
|
||||
"testing": "In prova"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Aggiornamento di Decky",
|
||||
"no_patch_notes_desc": "nessuna patch notes per questa versione",
|
||||
"patch_notes_desc": "Cambiamenti",
|
||||
"updates": {
|
||||
"check_button": "Cerca aggiornamenti",
|
||||
"checking": "Controllando",
|
||||
"cur_version": "Versione attuale: {{ver}}",
|
||||
"install_button": "Installa aggiornamento",
|
||||
"label": "Aggiornamenti",
|
||||
"lat_version": "Aggiornato. Eseguendo {{ver}}",
|
||||
"reloading": "Ricaricando",
|
||||
"updating": "Aggiornando"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "업데이트 배포 채널",
|
||||
"stable": "안정판",
|
||||
"testing": "시험판",
|
||||
"prerelease": "사전 출시판"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"disabling": "React DevTools 비활성화",
|
||||
"enabling": "React DevTools 활성화",
|
||||
"5secreload": "5초 내로 다시 로드 됩니다"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "이 폴더 사용",
|
||||
"label": "폴더",
|
||||
"show_more": "더 많은 파일 표시"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "만든 날짜 (오름차순)",
|
||||
"modified_asce": "수정한 날짜 (오름차순)",
|
||||
"created_desc": "만든 날짜 (내림차 순)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "크기 (오름차순)",
|
||||
"modified_desc": "수정한 날짜 (내림차순)",
|
||||
"size_desc": "크기 (내림차순)"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "모든 파일",
|
||||
"show_hidden": "숨김 파일 표시",
|
||||
"file_type": "파일 형식"
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_other": "플러그인 {{count}}개 숨김"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "빠른 액세스 메뉴에서 숨김"
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_install": "설치",
|
||||
"plugin_no_desc": "플러그인 설명이 제공되지 않았습니다.",
|
||||
"plugin_version_label": "플러그인 버전",
|
||||
"plugin_full_access": "이 플러그인은 Steam Deck의 모든 접근 권한을 가집니다."
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "설치",
|
||||
"button_processing": "설치 중",
|
||||
"desc": "{{artifact}} {{version}}을(를) 설치하겠습니까?",
|
||||
"title": "{{artifact}} 설치"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "재설치",
|
||||
"button_processing": "재설치 중",
|
||||
"desc": "{{artifact}} {{version}}을(를) 재설치하겠습니까?",
|
||||
"title": "{{artifact}} 재설치"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "업데이트",
|
||||
"button_processing": "업데이트 중",
|
||||
"title": "{{artifact}} 업데이트",
|
||||
"desc": "{{artifact}} {{version}} 업데이트를 설치하겠습니까?"
|
||||
},
|
||||
"no_hash": "이 플러그인은 해시 확인을 하지 않습니다, 설치에 따른 위험은 사용자가 감수해야 합니다."
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_other": "플러그인 {{count}}개 수정",
|
||||
"update_other": "플러그인 {{count}}개 업데이트",
|
||||
"reinstall_other": "플러그인 {{count}}개 재설치",
|
||||
"install_other": "플러그인 {{count}}개 설치"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "확인",
|
||||
"loading": "작업 중"
|
||||
},
|
||||
"confirm": "해당 수정을 적용하겠습니까?",
|
||||
"description": {
|
||||
"install": "{{name}} {{version}} 플러그인 설치",
|
||||
"update": "{{name}}의 {{version}} 업데이트 설치",
|
||||
"reinstall": "{{name}} {{version}} 재설치"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"plugin_actions": "플러그인 동작",
|
||||
"reinstall": "재설치",
|
||||
"reload": "다시 로드",
|
||||
"uninstall": "설치 제거",
|
||||
"show": "빠른 액세스 메뉴: 표시",
|
||||
"hide": "빠른 액세스 메뉴: 숨김",
|
||||
"update_all_other": "플러그인 {{count}}개 업데이트",
|
||||
"no_plugin": "설치된 플러그인이 없습니다!",
|
||||
"update_to": "{{name}}(으)로 업데이트"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "{{tag_name}} 업데이트를 설치할 수 있습니다!",
|
||||
"error": "오류",
|
||||
"plugin_load_error": {
|
||||
"message": "{{name}} 플러그인 불러오기 오류",
|
||||
"toast": "{{name}} 불러오기 오류"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "설치 제거",
|
||||
"desc": "{{name}}을(를) 설치 제거하겠습니까?",
|
||||
"title": "{{name}} 설치 제거"
|
||||
},
|
||||
"plugin_update_other": "플러그인 {{count}}개를 업데이트 할 수 있습니다!",
|
||||
"plugin_error_uninstall": "{{name}} 플러그인을 불러올 때 위와 같은 예외가 발생했습니다. 이는 보통 SteamUI 최신 버전에 맞는 플러그인 업데이트가 필요할 때 발생합니다. Decky 설정의 플러그인 탭에서 업데이트가 있는지 확인하거나 설치 제거를 시도 해 보세요."
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"label": "리모트 CEF 디버그 허용",
|
||||
"desc": "네트워크의 모든 사용자에게 CEF 디버거에 대한 인증되지 않은 액세스 허용"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "콘솔 열기",
|
||||
"label": "CEF 콘솔",
|
||||
"desc": "CEF 콘솔을 엽니다. 디버그 전용입니다. 이 항목들은 위험 가능성이 있으므로 플러그인 개발자이거나 개발자의 가이드를 따를 경우에만 사용해야 합니다."
|
||||
},
|
||||
"header": "기타",
|
||||
"react_devtools": {
|
||||
"ip_label": "IP",
|
||||
"label": "React DevTools 활성화",
|
||||
"desc": "React DevTools를 실행하고 있는 컴퓨터에 연결을 활성화합니다. 이 설정을 변경하면 Steam이 다시 로드됩니다. 활성화하기 전에 IP 주소를 설정하세요."
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "설치",
|
||||
"button_zip": "검색",
|
||||
"header": "서드파티 플러그인",
|
||||
"label_desc": "URL",
|
||||
"label_url": "URL에서 플러그인 설치",
|
||||
"label_zip": "ZIP 파일에서 플러그인 설치"
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "설치를 실패했습니다! ZIP 파일만이 지원됩니다.",
|
||||
"title": "Decky"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Valve 내부 개발자 메뉴를 활성화합니다.",
|
||||
"label": "Valve 내부 개발자 메뉴 활성화",
|
||||
"desc2": "이 메뉴의 기능을 모른다면 어떤 것도 건드리지 마세요."
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky 버전",
|
||||
"header": "정보"
|
||||
},
|
||||
"beta": {
|
||||
"header": "베타 참가"
|
||||
},
|
||||
"developer_mode": {
|
||||
"desc": "Decky의 개발자 설정을 활성화합니다.",
|
||||
"label": "개발자 모드"
|
||||
},
|
||||
"other": {
|
||||
"header": "기타"
|
||||
},
|
||||
"updates": {
|
||||
"header": "업데이트"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "개발자",
|
||||
"general_title": "일반",
|
||||
"navbar_settings": "Decky 설정",
|
||||
"plugins_title": "플러그인"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"desc": "Decky 플러그인 스토어에 기여하고 싶다면 SteamDeckHomebrew/decky-plugin-template Github 저장소를 확인하세요. 개발 및 배포에 대한 정보는 README에서 확인할 수 있습니다.",
|
||||
"label": "기여하기"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "필터",
|
||||
"label_def": "모두"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "검색"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "정렬",
|
||||
"label_def": "최근 업데이트 순"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "모든 플러그인 소스 코드는 SteamDeckHomebrew/decky-plugin-database Github 저장소에서 확인할 수 있습니다.",
|
||||
"label": "소스 코드"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "정보",
|
||||
"alph_asce": "알파벳순 (Z-A)",
|
||||
"alph_desc": "알파벳순 (A-Z)",
|
||||
"title": "검색"
|
||||
},
|
||||
"store_testing_cta": "새로운 플러그인을 테스트하여 Decky Loader 팀을 도와주세요!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "사용자 지정 스토어",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "사용자 지정",
|
||||
"label": "스토어 배포 채널",
|
||||
"default": "기본",
|
||||
"testing": "시험"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky 업데이트",
|
||||
"no_patch_notes_desc": "이 버전에는 패치 노트가 없습니다",
|
||||
"patch_notes_desc": "패치 노트",
|
||||
"updates": {
|
||||
"check_button": "업데이트 확인",
|
||||
"checking": "확인 중",
|
||||
"cur_version": "현재 버전: {{ver}}",
|
||||
"install_button": "업데이트 설치",
|
||||
"label": "업데이트",
|
||||
"lat_version": "최신 상태: {{ver}} 실행 중",
|
||||
"reloading": "다시 로드 중",
|
||||
"updating": "업데이트 중"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "지정된 경로가 잘못되었습니다. 확인 후에 다시 입력해 주세요.",
|
||||
"unknown": "알 수 없는 오류가 발생했습니다. Raw 오류: {{raw_error}}"
|
||||
}
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "뒤로"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"prerelease": "Vooruitgave",
|
||||
"stable": "Stabiel",
|
||||
"label": "Update Kanaal",
|
||||
"testing": "Test"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Herlaad in 5 seconden",
|
||||
"disabling": "Uitschakelen React DevTools",
|
||||
"enabling": "Inschakelen React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Terug"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"unknown": "Een onbekende fout is opgetreden. De ruwe fout is: {{raw_error}}",
|
||||
"file_not_found": "Het opgegeven pad is niet geldig. Controleer het en voer het opnieuw correct in."
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"files": {
|
||||
"all_files": "Alle bestanden",
|
||||
"file_type": "Bestandstype",
|
||||
"show_hidden": "Toon verborgen bestanden"
|
||||
},
|
||||
"filter": {
|
||||
"created_desc": "Gecreëerd ( Nieuwste)",
|
||||
"modified_asce": "Veranderd (Oudste)",
|
||||
"modified_desc": "Veranderd (Nieuwste)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Grootte (Kleinste)",
|
||||
"size_desc": "Grootte (Grootste)",
|
||||
"created_asce": "Gecreëerd (Oudste)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Map",
|
||||
"select": "Gebruik deze map",
|
||||
"show_more": "Toon meer bestanden"
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 plug-in is verborgen in deze lijst",
|
||||
"hidden_other": "{{count}} plug-ins zijn verborgen in deze lijst"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Verborgen in het snelmenu"
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_install": "Installeren",
|
||||
"plugin_no_desc": "Geen beschrijving gegeven.",
|
||||
"plugin_version_label": "Plugin Versie",
|
||||
"plugin_full_access": "Deze plug-in heeft volledige toegang tot je Steam Deck."
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Installeren",
|
||||
"button_processing": "Bezig met installeren",
|
||||
"title": "Installeer {{artifact}}",
|
||||
"desc": "Weet je zeker dat je {{artifact}} {{version}} wilt installeren?"
|
||||
},
|
||||
"no_hash": "Deze plug-in heeft geen hash, u installeert deze op eigen risico.",
|
||||
"reinstall": {
|
||||
"button_idle": "Herinstalleren",
|
||||
"button_processing": "Bezig te herinstalleren",
|
||||
"desc": "Weet je zeker dat je {{artifact}} {{version}} opnieuw wilt installeren?",
|
||||
"title": "Installeer {{artifact}} opnieuw"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Update",
|
||||
"button_processing": "Bezig met updaten",
|
||||
"title": "{{artifact}} bijwerken",
|
||||
"desc": "Weet je zeker dat je {{artifact}} {{version}} wilt updaten?"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_one": "Wijzig {{count}} plug-in",
|
||||
"mixed_other": "Pas {{count}} plug-ins aan",
|
||||
"update_one": "1 plugin bijwerken",
|
||||
"update_other": "{{count}} plug-ins bijwerken",
|
||||
"install_one": "Installeer 1 plugin",
|
||||
"install_other": "Installeer {{count}} plugins",
|
||||
"reinstall_one": "1 plugin opnieuw installeren",
|
||||
"reinstall_other": "{{count}} plugins opnieuw installeren"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Bevestigen",
|
||||
"loading": "Werkend"
|
||||
},
|
||||
"confirm": "Weet u zeker dat u de volgende wijzigingen wilt aanbrengen?",
|
||||
"description": {
|
||||
"install": "Installeer {{name}} {{version}}",
|
||||
"update": "Update {{name}} naar {{version}}",
|
||||
"reinstall": "Installeer opnieuw {{name}} {{version}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "Geen plugins geïnstalleerd!",
|
||||
"plugin_actions": "Plugin Acties",
|
||||
"reload": "Herladen",
|
||||
"uninstall": "Verwijderen",
|
||||
"update_to": "Update naar {{name}}",
|
||||
"hide": "Snelle toegang: Verberg",
|
||||
"update_all_one": "Update 1 plugin",
|
||||
"update_all_other": "Update {{count}} plugins",
|
||||
"reinstall": "Opnieuw installeren",
|
||||
"show": "Snelle toegang: Toon"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"error": "Fout",
|
||||
"plugin_load_error": {
|
||||
"message": "Fout bij het laden van plugin {{name}}",
|
||||
"toast": "Fout bij het laden van {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Verwijderen",
|
||||
"desc": "Weet je zeker dat je {{name}} wilt verwijderen?",
|
||||
"title": "Verwijder {{name}}"
|
||||
},
|
||||
"plugin_update_one": "Updates beschikbaar voor 1 plugin!",
|
||||
"plugin_update_other": "Updates beschikbaar voor {{count}} plugins!",
|
||||
"decky_update_available": "Update naar {{tag_name}} beschikbaar!",
|
||||
"plugin_error_uninstall": "Het laden van {{name}} veroorzaakte een uitzondering zoals hierboven weergegeven. Dit betekent meestal dat de plug-in een update vereist voor de nieuwe versie van SteamUI. Controleer of er een update aanwezig is of evalueer de verwijdering ervan in de Decky-instellingen, in het gedeelte Plug-ins."
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Sta ongeauthenticeerde toegang tot de CEF-foutopsporing toe aan iedereen in uw netwerk",
|
||||
"label": "Externe CEF-foutopsporing toestaan"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Console openen",
|
||||
"label": "CEF Bedieningsscherm",
|
||||
"desc": "Opent de CEF-console. Alleen nuttig voor foutopsporingsdoeleinden. Dingen hier zijn potentieel gevaarlijk en mogen alleen worden gebruikt als u een ontwikkelaar van plug-ins bent, of hier door een ontwikkelaar naartoe wordt geleid."
|
||||
},
|
||||
"header": "Andere",
|
||||
"react_devtools": {
|
||||
"ip_label": "IP",
|
||||
"label": "Aanzetten React DevTools",
|
||||
"desc": "Maakt verbinding met een computer met React DevTools mogelijk. Als je deze instelling wijzigt, wordt Steam opnieuw geladen. Stel het IP-adres in voordat u het inschakelt."
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"header": "Plug-ins van derden",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Installeer Plugin van URL",
|
||||
"label_zip": "Installeer Plugin van Zip bestand",
|
||||
"button_install": "Installeren",
|
||||
"button_zip": "Bladeren"
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky",
|
||||
"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.",
|
||||
"label": "Valve Internal inschakelen"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky versie",
|
||||
"header": "Over"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Beta deelname"
|
||||
},
|
||||
"developer_mode": {
|
||||
"desc": "Schakelt de ontwikkelaarsinstellingen van Decky in.",
|
||||
"label": "Ontwikkelaars modus"
|
||||
},
|
||||
"other": {
|
||||
"header": "Overige"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Nieuwe Versies"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Ontwikkelaar",
|
||||
"general_title": "Algemeen",
|
||||
"navbar_settings": "Decky instellingen",
|
||||
"plugins_title": "Plugins"
|
||||
},
|
||||
"Store": {
|
||||
"store_filter": {
|
||||
"label": "Filter",
|
||||
"label_def": "Alles"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Zoek"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Sorteren",
|
||||
"label_def": "Laatste Geupdate (Nieuwste)"
|
||||
},
|
||||
"store_source": {
|
||||
"label": "Bron Code",
|
||||
"desc": "Alle broncode van de plug-in is beschikbaar in de SteamDeckHomebrew/decky-plugin-database-repository op GitHub."
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Over",
|
||||
"alph_asce": "Alfabetisch (Z naar A)",
|
||||
"alph_desc": "Alfabetisch (A naar Z)",
|
||||
"title": "Bladeren"
|
||||
},
|
||||
"store_testing_cta": "Overweeg alsjeblieft om nieuwe plug-ins te testen om het Decky Loader-team te helpen!",
|
||||
"store_contrib": {
|
||||
"desc": "Als je wilt bijdragen aan de Decky Plugin winkel, kijk dan in de SteamDeckHomebrew/decky-plugin-template repository op GitHub. Informatie over ontwikkeling en distributie is beschikbaar in de README.",
|
||||
"label": "Bijdragende"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Aangepassingen winkel",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Aanpassingen",
|
||||
"default": "Standaard",
|
||||
"label": "Winkel Kanaal",
|
||||
"testing": "Testen"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"patch_notes_desc": "Correctie opmerkingen",
|
||||
"updates": {
|
||||
"check_button": "Controleer op updates",
|
||||
"checking": "Controleren",
|
||||
"cur_version": "Huidige versie: {{ver}}",
|
||||
"install_button": "Installeer Update",
|
||||
"label": "Update",
|
||||
"lat_version": "Up-to-date: loopt {{ver}}",
|
||||
"reloading": "Herstarten",
|
||||
"updating": "Aan het updaten"
|
||||
},
|
||||
"decky_updates": "Decky Nieuwe Versies",
|
||||
"no_patch_notes_desc": "geen correctie-opmerkingen voor deze versie"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"prerelease": "Pré-lançamento",
|
||||
"stable": "Estável",
|
||||
"testing": "Em Teste",
|
||||
"label": "Canal de Atualização"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Recarregando em 5 segundos",
|
||||
"enabling": "Habilitando React DevTools",
|
||||
"disabling": "Desabilitando React DevTools"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Use esta pasta",
|
||||
"label": "Pasta",
|
||||
"show_more": "Mostrar mais arquivos"
|
||||
},
|
||||
"files": {
|
||||
"show_hidden": "Mostrar Arquivos Ocultos",
|
||||
"all_files": "Todos os arquivos",
|
||||
"file_type": "Formato de arquivo"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Criado (Mais antigo)",
|
||||
"created_desc": "Criado (Mais recente)",
|
||||
"modified_asce": "Alterado (Mais antigo)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Tamanho (Menor)",
|
||||
"size_desc": "Tamanho (Maior)",
|
||||
"modified_desc": "Alterado (Mais recente)"
|
||||
},
|
||||
"file": {
|
||||
"select": "Selecione este arquivo"
|
||||
}
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Oculto no menu de acesso rápido"
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Este plugin tem acesso total ao seu Steam Deck.",
|
||||
"plugin_install": "Instalar",
|
||||
"plugin_no_desc": "Nenhuma descrição fornecida.",
|
||||
"plugin_version_label": "Versão do plugin"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Instalar",
|
||||
"button_processing": "Instalando",
|
||||
"desc": "Você tem certeza que deseja instalar {{artifact}} {{version}}?",
|
||||
"title": "Instalar {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstalar",
|
||||
"button_processing": "Reinstalando",
|
||||
"desc": "Tem certeza que voce deseja reinstalar {{artifact}} {{version}}?",
|
||||
"title": "Reinstalar {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Atualizar",
|
||||
"button_processing": "Atualizando",
|
||||
"desc": "Tem certeza que voce deseja atualizar {{artifact}} {{version}}?",
|
||||
"title": "Atualizar {{artifact}}"
|
||||
},
|
||||
"no_hash": "Este plugin não tem um hash, você o está instalando por sua conta em risco."
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_one": "Modificar {{count}} plugin",
|
||||
"mixed_many": "Modificar {{count}} plugins",
|
||||
"mixed_other": "Modificar {{count}} plugins",
|
||||
"update_one": "Atualizar 1 plugin",
|
||||
"update_many": "Atualizar {{count}} plugins",
|
||||
"update_other": "Atualizar {{count}} plugins",
|
||||
"install_one": "Instalar 1 plugin",
|
||||
"install_many": "Instalar {{count}} plugins",
|
||||
"install_other": "Instalar {{count}} plugins",
|
||||
"reinstall_one": "Reinstalar 1 plugin",
|
||||
"reinstall_many": "Reinstalar {{count}} plugins",
|
||||
"reinstall_other": "Reinstalar {{count}} plugins"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Confirmar",
|
||||
"loading": "Carregando"
|
||||
},
|
||||
"description": {
|
||||
"install": "Instalar {{name}} {{version}}",
|
||||
"update": "Atualizar {{name}} para {{version}}",
|
||||
"reinstall": "Reinstalar {{name}} {{version}}"
|
||||
},
|
||||
"confirm": "Tem certeza que deseja fazer as seguintes modificações?"
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "Nenhum plugin instalado!",
|
||||
"plugin_actions": "Ações do plugin",
|
||||
"reinstall": "Reinstalar",
|
||||
"reload": "Recarregar",
|
||||
"uninstall": "Desinstalar",
|
||||
"update_to": "Atualizar para {{name}}",
|
||||
"show": "Acesso Rápido: Mostrar",
|
||||
"update_all_one": "Atualizar 1 plugin",
|
||||
"update_all_many": "Atualizar {{count}} plugins",
|
||||
"update_all_other": "Atualizar {{count}} plugins",
|
||||
"hide": "Acesso Rápido: Ocultar"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"error": "Erro",
|
||||
"plugin_load_error": {
|
||||
"message": "Erro ao carregar o plugin {{name}}",
|
||||
"toast": "Erro ao carregar {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Desinstalar",
|
||||
"desc": "Você tem certeza que deseja desinstalar {{name}}?",
|
||||
"title": "Desinstalar {{name}}"
|
||||
},
|
||||
"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_many": "Atualizações disponíveis para {{count}} plugins!",
|
||||
"plugin_update_other": "Atualizações disponíveis para {{count}} plugins!"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"label": "Permitir Depuração CEF Demota",
|
||||
"desc": "Permitir acesso não autenticato ao depurador CEF para qualquer um na sua rede"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Abrir o Console",
|
||||
"label": "Console CEF",
|
||||
"desc": "Abre o Console CEF. Somente útil para fins de depuração. O material aqui é potencialmente perigoso e só deve ser usado se você for um desenvolvedor de plugin, ou direcionado até aqui por um."
|
||||
},
|
||||
"header": "Outros",
|
||||
"react_devtools": {
|
||||
"desc": "Habilita a conexão a um computador executando React DevTools. Alterar essa configuração irá recarregar a Steam. Defina o endereço IP antes de habilitar.",
|
||||
"ip_label": "IP",
|
||||
"label": "Habilitar React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Instalar",
|
||||
"button_zip": "Navegar",
|
||||
"header": "Plugins de terceiros",
|
||||
"label_url": "Instalar Plugin a partir da URL",
|
||||
"label_zip": "Instalar Plugin a partir de um arquivo ZIP",
|
||||
"label_desc": "URL"
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky",
|
||||
"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.",
|
||||
"label": "Habilitar Menu Interno da Valve"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Versão do Decky",
|
||||
"header": "Sobre"
|
||||
},
|
||||
"developer_mode": {
|
||||
"desc": "Habilita as configurações de desenvolvedor do Decky.",
|
||||
"label": "Modo Deselvolvedor"
|
||||
},
|
||||
"other": {
|
||||
"header": "Outros"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Atualizações"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Participação no Beta"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Desenvolvedor",
|
||||
"general_title": "Geral",
|
||||
"navbar_settings": "Configurações do Decky",
|
||||
"plugins_title": "Plugins"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "Contribuindo",
|
||||
"desc": "Se você deseja contribuir para a Loja de Plugins para o Decky, confira o repositório SteamDeckHomebrew/decky-plugin-template no GitHub. Informações sobre o desenvolvimento e distribuição estão disponíveis no README."
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtros",
|
||||
"label_def": "Todos"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Buscar"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Ordenar",
|
||||
"label_def": "Último atualizado (Mais recente)"
|
||||
},
|
||||
"store_source": {
|
||||
"desc": "Todos os códigos fonte dos plugins estão disponíveis no repositório SteamDeckHomebrew/decky-plugin-database no GitHub.",
|
||||
"label": "Código Fonte"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Sobre",
|
||||
"alph_desc": "Alfabética (A - Z)",
|
||||
"title": "Navegar",
|
||||
"alph_asce": "Alfabética (Z - A)"
|
||||
},
|
||||
"store_testing_cta": "Por favor, considere testar os novos plugins para ajudar o time do Decky Loader!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Loja Personalizada",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Personalizada",
|
||||
"default": "Padrão",
|
||||
"label": "Canal da Loja",
|
||||
"testing": "Em Teste"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"no_patch_notes_desc": "nenhuma nota de alteração para esta versão",
|
||||
"patch_notes_desc": "Notas de alteração",
|
||||
"updates": {
|
||||
"check_button": "Buscar Atualizações",
|
||||
"checking": "Buscando",
|
||||
"cur_version": "Versão atual: {{ver}}",
|
||||
"install_button": "Instalar Atualização",
|
||||
"label": "Atualizações",
|
||||
"lat_version": "Atualizado: rodando {{ver}}",
|
||||
"reloading": "Recarregando",
|
||||
"updating": "Atualizando"
|
||||
},
|
||||
"decky_updates": "Atualizações do Decky"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 plugin está oculto nesta lista",
|
||||
"hidden_many": "{{count}} plugins estão ocultos nesta lista",
|
||||
"hidden_other": "{{count}} plugins estão ocultos nesta lista"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Voltar"
|
||||
}
|
||||
},
|
||||
"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}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Usar esta pasta"
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 plugin está oculto desta lista",
|
||||
"hidden_many": "{{count}} plugins estão ocultos desta lista",
|
||||
"hidden_other": "{{count}} plugins estão ocultos desta lista"
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Este plugin tem acesso total à tua Steam Deck.",
|
||||
"plugin_install": "Instalar",
|
||||
"plugin_version_label": "Versão do plugin",
|
||||
"plugin_no_desc": "Não tem descrição."
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Instalar",
|
||||
"button_processing": "Instalação em curso",
|
||||
"title": "Instalar {{artifact}}",
|
||||
"desc": "De certeza que queres instalar {{artifact}} {{version}}?"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstalar",
|
||||
"button_processing": "Reinstalação em curso",
|
||||
"title": "Reinstalar {{artifact}}",
|
||||
"desc": "De certeza que queres reinstalar {{artifact}} {{version}}?"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Actualizar",
|
||||
"button_processing": "Actualização em curso",
|
||||
"title": "Actualizar {{artifact}}",
|
||||
"desc": "De certeza que queres actualizar {{artifact}} {{version}}?"
|
||||
},
|
||||
"no_hash": "Este plugin não tem um hash, estás a instalá-lo por tua conta e risco."
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_one": "Alterar 1 plugin",
|
||||
"mixed_many": "Alterar {{count}} plugins",
|
||||
"mixed_other": "Alterar {{count}} plugins",
|
||||
"update_one": "Actualizar 1 plugin",
|
||||
"update_many": "Actualizar {{count}} plugins",
|
||||
"update_other": "Actualizar {{count}} plugins",
|
||||
"reinstall_one": "Reinstalar 1 plugin",
|
||||
"reinstall_many": "Reinstalar {{count}} plugins",
|
||||
"reinstall_other": "Reinstalar {{count}} plugins",
|
||||
"install_one": "Instalar 1 plugin",
|
||||
"install_many": "Instalar {{count}} plugins",
|
||||
"install_other": "Instalar {{count}} plugins"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Confirmar",
|
||||
"loading": "Em curso"
|
||||
},
|
||||
"description": {
|
||||
"install": "Instalar {{name}} {{version}}",
|
||||
"update": "Actualizar {{name}} para {{version}}",
|
||||
"reinstall": "Reinstalar {{name}} {{version}}"
|
||||
},
|
||||
"confirm": "De certeza que queres fazer as seguintes alterações?"
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "Nenhum plugin instalado!",
|
||||
"reinstall": "Reinstalar",
|
||||
"uninstall": "Desinstalar",
|
||||
"update_to": "Actualizar para {{name}}",
|
||||
"update_all_one": "Actualizar 1 plugin",
|
||||
"update_all_many": "Actualizar {{count}} plugins",
|
||||
"update_all_other": "Actualizar {{count}} plugins",
|
||||
"plugin_actions": "Operações de plugin",
|
||||
"reload": "Recarregar",
|
||||
"show": "Acesso rápido: Mostrar",
|
||||
"hide": "Acesso rápido: Ocultar"
|
||||
},
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"stable": "Estável",
|
||||
"testing": "Em teste",
|
||||
"label": "Canal de actualização",
|
||||
"prerelease": "Pré-lançamento"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Vai recarregar em 5 segundos",
|
||||
"disabling": "Desactivando React DevTools",
|
||||
"enabling": "Activando React DevTools"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Oculto do menu de acesso rápido"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"error": "Erro",
|
||||
"plugin_load_error": {
|
||||
"message": "Erro ao carregar o plugin {{name}}",
|
||||
"toast": "Erro ao carregar {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Desinstalar",
|
||||
"title": "Desinstalar {{name}}",
|
||||
"desc": "De certeza que queres desinstalar {{name}}?"
|
||||
},
|
||||
"decky_update_available": "Está disponível uma nova versão de {{tag_name}} !",
|
||||
"plugin_update_one": "1 plugin tem actualizações disponíveis!",
|
||||
"plugin_update_many": "{{count}} plugins têm actualizações disponíveis!",
|
||||
"plugin_update_other": "{{count}} plugins têm actualizações disponíveis!",
|
||||
"plugin_error_uninstall": "Houve uma excepção ao carregar {{name}}, como mostrado em cima. Pode ter sido porque o plugin requere a última versão do SteamUI. Verifica se há uma actualização disponível ou desinstala o plugin nas definições do Decky."
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Abrir consola",
|
||||
"label": "Consola CEF",
|
||||
"desc": "Abre a consola do CEF. Só é útil para efeitos de debugging. Pode ser perigosa e só deve ser usada se és um desenvolvedor de plugins, ou se foste aqui indicado por um desenvolvedor."
|
||||
},
|
||||
"header": "Outros",
|
||||
"react_devtools": {
|
||||
"desc": "Permite a conecção a um computador que está a correr React DevTools. Mudar esta definição vai recarregar o Steam. Define o endereço de IP antes de activar.",
|
||||
"ip_label": "IP",
|
||||
"label": "Activar React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Instalar",
|
||||
"button_zip": "Navegar",
|
||||
"header": "Plugins de terceiros",
|
||||
"label_desc": "URl",
|
||||
"label_url": "Instalar plugin a partir dum URL",
|
||||
"label_zip": "Instalar plugin a partir dum ficheiro ZIP"
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky",
|
||||
"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.",
|
||||
"desc2": "Não toques em nada deste menu se não souberes a sua função."
|
||||
}
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Permitir acesso não autenticado ao debugger do CEF a qualquer pessoa na tua rede",
|
||||
"label": "Permitir debugging remoto do CEF"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Versão do Decky",
|
||||
"header": "Sobre"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Participação na versão Beta"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Modo de programador",
|
||||
"desc": "Activa as definições de programador do Decky."
|
||||
},
|
||||
"other": {
|
||||
"header": "Outros"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Actualizações"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Programador",
|
||||
"general_title": "Geral",
|
||||
"navbar_settings": "Definições do Decky",
|
||||
"plugins_title": "Plugins"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "Contribuir",
|
||||
"desc": "Se queres contribuir com um novo plugin, vai ao repositório SteamDeckHomebrew/decky-plugin-template no GitHub. No README, podes encontrar mais informação sobre desenvolvimento e distribuição."
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtro",
|
||||
"label_def": "Todos"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Procurar"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Ordenar",
|
||||
"label_def": "Última actualização (mais recente)"
|
||||
},
|
||||
"store_source": {
|
||||
"label": "Código fonte",
|
||||
"desc": "O código fonte de cada plugin está disponível no repositório SteamDeckHomebrew/decky-plugin-database no GitHub."
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Sobre",
|
||||
"alph_asce": "Alfabeticamente (Z-A)",
|
||||
"alph_desc": "Alfabeticamente (A-Z)",
|
||||
"title": "Navegar"
|
||||
},
|
||||
"store_testing_cta": "Testa novos plugins e ajuda a equipa do Decky Loader!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"url_label": "URL",
|
||||
"label": "Loja personalizada"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Personalizada",
|
||||
"default": "Standard",
|
||||
"testing": "Em teste",
|
||||
"label": "Canal de loja"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Actualizações do Decky",
|
||||
"no_patch_notes_desc": "sem registo de alterações desta versão",
|
||||
"patch_notes_desc": "Registo de alterações",
|
||||
"updates": {
|
||||
"check_button": "Procurar actualizações",
|
||||
"checking": "Busca de actualizações em curso",
|
||||
"cur_version": "Versão actual: {{ver}}",
|
||||
"label": "Actualizações",
|
||||
"lat_version": "Actualizado: a correr {{ver}}",
|
||||
"updating": "Actualização em curso",
|
||||
"reloading": "Recarregar",
|
||||
"install_button": "Instalar actualização"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"update_one": "",
|
||||
"update_few": "",
|
||||
"update_many": "",
|
||||
"reinstall_one": "",
|
||||
"reinstall_few": "",
|
||||
"reinstall_many": "",
|
||||
"install_one": "",
|
||||
"install_few": "",
|
||||
"install_many": "",
|
||||
"mixed_one": "",
|
||||
"mixed_few": "",
|
||||
"mixed_many": ""
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"update_all_one": "",
|
||||
"update_all_few": "",
|
||||
"update_all_many": ""
|
||||
},
|
||||
"PluginLoader": {
|
||||
"plugin_update_one": "",
|
||||
"plugin_update_few": "",
|
||||
"plugin_update_many": ""
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "",
|
||||
"hidden_few": "",
|
||||
"hidden_many": ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"SettingsDeveloperIndex": {
|
||||
"react_devtools": {
|
||||
"ip_label": "IP",
|
||||
"label": "Aktivizo React DevTools"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_zip": "Kërko",
|
||||
"header": "Shtesa të Huaj",
|
||||
"button_install": "Instalo",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Instalo Shtes Nga URL",
|
||||
"label_zip": "Instalo Shtes Nga ZIP"
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky"
|
||||
}
|
||||
},
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"stable": "Fiksuar",
|
||||
"label": "Kanali Përditësimet"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Përdore këtë folder"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_install": "Instalo",
|
||||
"plugin_version_label": "Versioni Shteses"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Instalo",
|
||||
"button_processing": "Instalohet",
|
||||
"desc": "Je i sigurt që don ta instalojsh {{artifact}} {{version}}?",
|
||||
"title": "Instalo {{artifact}}"
|
||||
},
|
||||
"no_hash": "Ky shtesë nuk ka hash, ti e instalon me rrezikun tuaj.",
|
||||
"reinstall": {
|
||||
"button_idle": "Riinstalo",
|
||||
"button_processing": "Riinstalohet",
|
||||
"desc": "Je i sigurt a don ta riinstalojsh {{artifact}} {{version}}?",
|
||||
"title": "Riinstalo {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_processing": "Përditësohet",
|
||||
"desc": "Je i sigurt a don ta përditësojsh {{artifact}} {{version}}?",
|
||||
"title": "Përditëso {{artifact}}"
|
||||
}
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"plugin_uninstall": {
|
||||
"title": "Çinstalo {{name}}",
|
||||
"button": "Çinstalo",
|
||||
"desc": "Je i sigurt që don ta çinstalojsh {{name}}?"
|
||||
},
|
||||
"error": "Gabim",
|
||||
"plugin_error_uninstall": "Ju lutem shko nga {{name}} në Decky menu nëse don ta çinstalojsh këtë shtese.",
|
||||
"plugin_update_one": "",
|
||||
"plugin_update_other": ""
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "Nuk ka shtesa të instaluar!",
|
||||
"uninstall": "Çinstalo",
|
||||
"update_all_one": "",
|
||||
"update_all_other": ""
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"other": {
|
||||
"header": "Të Tjera"
|
||||
},
|
||||
"about": {
|
||||
"decky_version": "Versioni Decky"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Përmirësimet"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Zhvillues",
|
||||
"general_title": "Gjeneral",
|
||||
"navbar_settings": "Cilësimet Decky"
|
||||
},
|
||||
"Store": {
|
||||
"store_sort": {
|
||||
"label": "Rendit"
|
||||
},
|
||||
"store_tabs": {
|
||||
"title": "Kërko"
|
||||
},
|
||||
"store_contrib": {
|
||||
"label": "Kontributi"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtro",
|
||||
"label_def": "Të Gjitha"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Kërko"
|
||||
},
|
||||
"store_source": {
|
||||
"label": "Kodin Burimor"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"store_channel": {
|
||||
"label": "Kanali Dyqanit"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"updates": {
|
||||
"cur_version": "Versioni e tanishëme: {{ver}}"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_one": "",
|
||||
"mixed_other": "",
|
||||
"update_one": "",
|
||||
"update_other": "",
|
||||
"reinstall_one": "",
|
||||
"reinstall_other": "",
|
||||
"install_one": "",
|
||||
"install_other": ""
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "",
|
||||
"hidden_other": ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"prerelease": "Передреліз",
|
||||
"testing": "Тестовий",
|
||||
"label": "Канал оновлень",
|
||||
"stable": "Стабільний"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Перезавантаження за 5 секунд",
|
||||
"enabling": "Увімкнення React DevTools",
|
||||
"disabling": "Вимкнення React DevTools"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Використовувати цю папку"
|
||||
}
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Приховано з меню швидкого доступу"
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Цей плагін має повний доступ до вашого Steam Deck.",
|
||||
"plugin_install": "Встановити",
|
||||
"plugin_no_desc": "Опис не надано.",
|
||||
"plugin_version_label": "Версія плагіна"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Встановити",
|
||||
"button_processing": "Встановлення",
|
||||
"title": "Встановити {{artifact}}",
|
||||
"desc": "Ви впевнені, що хочете встановити {{artifact}} {{version}}?"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Перевстановити",
|
||||
"desc": "Ви впевнені, що хочете перевстановити {{artifact}} {{version}}?",
|
||||
"title": "Перевстановити {{artifact}}",
|
||||
"button_processing": "Перевстановлення"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Оновити",
|
||||
"button_processing": "Оновлення",
|
||||
"title": "Оновити {{artifact}}",
|
||||
"desc": "Ви впевнені, що хочете оновити {{artifact}} {{version}}?"
|
||||
},
|
||||
"no_hash": "Цей плагін не має хешу, ви встановлюєте його на власний ризик."
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_one": "Модифікувати 1 плагін",
|
||||
"mixed_few": "Модифікувати {{count}} плагінів",
|
||||
"mixed_many": "",
|
||||
"reinstall_one": "Перевстановити 1 плагін",
|
||||
"reinstall_few": "Перевстановити {{count}} плагінів",
|
||||
"reinstall_many": "Перевстановити {{count}} плагінів",
|
||||
"update_one": "Оновити 1 плагін",
|
||||
"update_few": "Оновити {{count}} плагінів",
|
||||
"update_many": "Оновити {{count}} плагінів",
|
||||
"install_one": "Встановити 1 плагін",
|
||||
"install_few": "Встановити {{count}} плагінів",
|
||||
"install_many": "Встановити {{count}} плагінів"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Підтвердити",
|
||||
"loading": "Опрацювання"
|
||||
},
|
||||
"description": {
|
||||
"install": "Встановити {{name}} {{version}}",
|
||||
"update": "Оновити {{name}} до {{version}}",
|
||||
"reinstall": "Перевстановити {{name}} {{version}}"
|
||||
},
|
||||
"confirm": "Ви впевнені, що хочете застосувати такі модифікації?"
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "Плагінів не встановлено!",
|
||||
"plugin_actions": "Дії плагінів",
|
||||
"reinstall": "Перевстановити",
|
||||
"reload": "Перезавантажити",
|
||||
"update_to": "Оновити {{name}}",
|
||||
"show": "Швидкий доступ: Показати",
|
||||
"hide": "Швидкий доступ: Приховати",
|
||||
"uninstall": "Видалити",
|
||||
"update_all_one": "Оновити 1 плагін",
|
||||
"update_all_few": "Оновити {{count}} плагінів",
|
||||
"update_all_many": "Оновити {{count}} плагінів"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "Доступне оновлення до {{tag_name}}!",
|
||||
"error": "Помилка",
|
||||
"plugin_load_error": {
|
||||
"message": "Помилка завантаження плагіна {{name}}",
|
||||
"toast": "Помилка завантаження {{name}}"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"desc": "Ви впевнені, що хочете видалити {{name}}?",
|
||||
"title": "Видалити {{name}}",
|
||||
"button": "Видалення"
|
||||
},
|
||||
"plugin_error_uninstall": "Завантаження {{name}} спровокувало помилку показану вище. Зазвичай це означає, що плагін вимагає оновлення до нової версії SteamUI. Перевірте чи таке оновлення доступне або виконайте його видалення у налаштуваннях Decky, у секції Плагіни.",
|
||||
"plugin_update_one": "Доступне оновлення для 1 плагіна!",
|
||||
"plugin_update_few": "Доступне оновлення для {{count}} плагінів!",
|
||||
"plugin_update_many": "Доступне оновлення для {{count}} плагінів!"
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Відкрити консоль",
|
||||
"label": "CEF-консоль",
|
||||
"desc": "Відкрити CEF-консоль. Корисно тільки для дебагу. Ця штука потенційно небезпечна і повинна використовувати виключно якщо ви розробник плагіна, або якщо розробник спрямував вас сюди."
|
||||
},
|
||||
"header": "Інше",
|
||||
"react_devtools": {
|
||||
"desc": "Вмикає доступ до компʼютера із запущеним React DevTools. Зміна цього налаштування перезавантажить Steam. Вкажіть IP перед увімкненням.",
|
||||
"label": "Увімкнути React DevTools",
|
||||
"ip_label": "IP"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Встановити",
|
||||
"header": "Сторонні плагіни",
|
||||
"label_desc": "URL",
|
||||
"label_url": "Встановити плагін з URL",
|
||||
"label_zip": "Встановити плагін з ZIP-файлу",
|
||||
"button_zip": "Огляд"
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky",
|
||||
"body": "Помилка встановлення! Підтримуються лише ZIP-файли."
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "Вмикає внутрішнє розробницьке меню Valve.",
|
||||
"label": "Увімкнути Valve Internal",
|
||||
"desc2": "Нічого не торкайтесь у цьому меню, якщо не розумієте, що ви робите."
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Версія Decky",
|
||||
"header": "Про нас"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Участь у Beta"
|
||||
},
|
||||
"developer_mode": {
|
||||
"desc": "Вмикає розробницькі налаштування Decky.",
|
||||
"label": "Розробницький режим"
|
||||
},
|
||||
"other": {
|
||||
"header": "Інше"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Оновлення"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Розробник",
|
||||
"general_title": "Загальне",
|
||||
"navbar_settings": "Налаштування Decky",
|
||||
"plugins_title": "Плагіни"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "Зробити внесок",
|
||||
"desc": "Якщо ви бажаєте додати щось у Decky Plugin Store, завітайте у репозиторій SteamDeckHomebrew/decky-plugin-template на GitHub. Інформація про розробку та поширення доступна у README."
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Фільтр",
|
||||
"label_def": "Усе"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Пошук"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Сортування",
|
||||
"label_def": "Востаннє оновлені (Найновіші)"
|
||||
},
|
||||
"store_source": {
|
||||
"label": "Вихідний код",
|
||||
"desc": "Код усіх плагінів доступний у репозиторії SteamDeckHomebrew/decky-plugin-database на GitHub."
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Інформація",
|
||||
"alph_asce": "За алфавітом (Z до A)",
|
||||
"alph_desc": "За алфавітом (A до Z)",
|
||||
"title": "Огляд"
|
||||
},
|
||||
"store_testing_cta": "Розгляньте можливість тестування нових плагінів, щоб допомогти команді Decky Loader!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Власний магазин",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Власний",
|
||||
"default": "За замовчуванням",
|
||||
"testing": "Тестування",
|
||||
"label": "Канал магазину"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Оновлення Decky",
|
||||
"no_patch_notes_desc": "Немає нотаток до цієї версії",
|
||||
"patch_notes_desc": "Перелік змін",
|
||||
"updates": {
|
||||
"checking": "Перевірка",
|
||||
"cur_version": "Поточна версія: {{ver}}",
|
||||
"install_button": "Встановити оновлення",
|
||||
"label": "Оновлення",
|
||||
"reloading": "Перезавантаження",
|
||||
"updating": "Оновлення",
|
||||
"check_button": "Перевірити оновлення",
|
||||
"lat_version": "Оновлено: використовується {{ver}}"
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "{{count}} плагін приховано з цього списку",
|
||||
"hidden_few": "{{count}} плагінів приховано з цього списку",
|
||||
"hidden_many": "{{count}} плагінів приховано з цього списку"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Дозволити доступ до CEF-дебагера без аутентифікації для будь-кого у вашій мережі",
|
||||
"label": "Дозволити віддалений CEF-дебагінг"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"prerelease": "发布候选",
|
||||
"stable": "稳定",
|
||||
"testing": "测试",
|
||||
"label": "更新通道"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "5 秒钟后重新加载",
|
||||
"disabling": "正在禁用 React DevTools",
|
||||
"enabling": "正在启用 React DevTools"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "使用这个文件夹",
|
||||
"label": "文件夹",
|
||||
"show_more": "显示更多文件"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "创建日期(最旧)",
|
||||
"created_desc": "创建日期(最新)",
|
||||
"modified_asce": "修改日期(最旧)",
|
||||
"modified_desc": "修改日期(最新)",
|
||||
"name_asce": "字母降序",
|
||||
"name_desc": "字母升序",
|
||||
"size_asce": "大小(最小)",
|
||||
"size_desc": "大小(最大)"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "全部文件",
|
||||
"file_type": "文件类型",
|
||||
"show_hidden": "显示隐藏文件"
|
||||
},
|
||||
"file": {
|
||||
"select": "选择此文件"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_install": "安装",
|
||||
"plugin_no_desc": "无描述提供。",
|
||||
"plugin_version_label": "插件版本",
|
||||
"plugin_full_access": "此插件可以完全访问你的 Steam Deck。"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "安装",
|
||||
"button_processing": "安装中",
|
||||
"desc": "你确定要安装 {{artifact}} {{version}} 吗?",
|
||||
"title": "安装 {{artifact}}"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "重新安装",
|
||||
"button_processing": "正在重新安装",
|
||||
"desc": "你确定要重新安装 {{artifact}} {{version}} 吗?",
|
||||
"title": "重新安装 {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "更新",
|
||||
"button_processing": "正在更新",
|
||||
"desc": "你确定要更新 {{artifact}} {{version}} 吗?",
|
||||
"title": "更新 {{artifact}}"
|
||||
},
|
||||
"no_hash": "此插件没有哈希校验值,你需要自行承担安装风险。"
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "没有安装插件!",
|
||||
"plugin_actions": "插件操作",
|
||||
"reinstall": "重新安装",
|
||||
"reload": "重新加载",
|
||||
"uninstall": "卸载",
|
||||
"update_to": "更新 {{name}}",
|
||||
"update_all_other": "更新 {{count}} 个插件",
|
||||
"show": "在快速访问菜单中显示",
|
||||
"hide": "在快速访问菜单中隐藏"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"error": "错误",
|
||||
"plugin_error_uninstall": "加载 {{name}} 时引起了上述异常。这通常意味着插件需要更新以适应 SteamUI 的新版本。请检查插件是否有更新,或在 Decky 设置中的插件部分将其移除。",
|
||||
"plugin_load_error": {
|
||||
"message": "加载插件 {{name}} 错误",
|
||||
"toast": "加载插件 {{name}} 发生了错误"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "卸载",
|
||||
"title": "卸载 {{name}}",
|
||||
"desc": "你确定要卸载 {{name}} 吗?"
|
||||
},
|
||||
"decky_update_available": "新版本 {{tag_name}} 可用!",
|
||||
"plugin_update_other": "{{count}} 个插件有更新!"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "允许你网络中的任何人无需身份验证即可访问CEF调试器",
|
||||
"label": "允许远程访问CEF调试"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"react_devtools": {
|
||||
"ip_label": "IP",
|
||||
"label": "启用 React DevTools",
|
||||
"desc": "允许连接到运行着 React DevTools 的计算机,更改此设置将重新加载Steam,请在启用前设置IP地址。"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "安装",
|
||||
"button_zip": "浏览文件",
|
||||
"header": "第三方插件",
|
||||
"label_desc": "URL",
|
||||
"label_url": "从 URL 安装插件",
|
||||
"label_zip": "从 ZIP 压缩文件安装插件"
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky",
|
||||
"body": "安装失败!只有 ZIP 格式的插件被支持。"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc1": "启用 Valve 内部开发者菜单。",
|
||||
"desc2": "除非你知道你在干什么,否则请不要修改此菜单中的任何内容。",
|
||||
"label": "启用 Valve 内部开发者"
|
||||
},
|
||||
"cef_console": {
|
||||
"button": "打开控制台",
|
||||
"label": "CEF 控制台",
|
||||
"desc": "打开 CEF 控制台。仅在调试目的下使用。这列选项均有风险,请仅在您是插件开发者或是在插件开发者指导时访问使用。"
|
||||
},
|
||||
"header": "其他"
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky 版本",
|
||||
"header": "关于"
|
||||
},
|
||||
"beta": {
|
||||
"header": "参与测试"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "开发者模式",
|
||||
"desc": "启用 Decky 的开发者测试。"
|
||||
},
|
||||
"other": {
|
||||
"header": "其他"
|
||||
},
|
||||
"updates": {
|
||||
"header": "更新"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "开发者",
|
||||
"general_title": "通用",
|
||||
"navbar_settings": "Decky 设置",
|
||||
"plugins_title": "插件"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "贡献",
|
||||
"desc": "如果你想要提交你的插件到 Decky 插件商店,请访问 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 存储库,关于开发和分发的相关信息,请查看 README 文件。"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "过滤器",
|
||||
"label_def": "全部"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "搜索"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "排序",
|
||||
"label_def": "最后更新 (最新)"
|
||||
},
|
||||
"store_source": {
|
||||
"label": "源代码",
|
||||
"desc": "所有插件的源代码都可以在 GitHub 上的 SteamDeckHomebrew/decky-plugin-database 存储库中获得。"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "关于",
|
||||
"alph_asce": "字母排序 (Z 到 A)",
|
||||
"alph_desc": "字母排序 (A 到 Z)",
|
||||
"title": "浏览"
|
||||
},
|
||||
"store_testing_cta": "请考虑测试新插件以帮助 Decky Loader 团队!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"store_channel": {
|
||||
"default": "默认",
|
||||
"label": "商店通道",
|
||||
"testing": "测试",
|
||||
"custom": "自定义"
|
||||
},
|
||||
"custom_store": {
|
||||
"label": "自定义商店",
|
||||
"url_label": "URL"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky 更新",
|
||||
"no_patch_notes_desc": "此版本没有补丁说明",
|
||||
"patch_notes_desc": "补丁说明",
|
||||
"updates": {
|
||||
"check_button": "检查更新",
|
||||
"checking": "检查中",
|
||||
"cur_version": "当前版本: {{ver}}",
|
||||
"install_button": "安装更新",
|
||||
"label": "更新",
|
||||
"lat_version": "已是最新版本: {{ver}} 运行中",
|
||||
"reloading": "重新加载中",
|
||||
"updating": "更新中"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_other": "更改 {{count}} 个插件",
|
||||
"update_other": "更新 {{count}} 个插件",
|
||||
"reinstall_other": "重装 {{count}} 个插件",
|
||||
"install_other": "安装 {{count}} 个插件"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "确认",
|
||||
"loading": "工作中"
|
||||
},
|
||||
"confirm": "确定要进行以下修改吗?",
|
||||
"description": {
|
||||
"install": "安装 {{name}} {{version}}",
|
||||
"update": "更新 {{name}} to {{version}}",
|
||||
"reinstall": "重装 {{name}} {{version}}"
|
||||
}
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "在快速访问菜单中已隐藏"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_other": "此列表隐藏了 {{count}} 个插件"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "返回"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "指定路径无效。请检查并输入正确的路径。",
|
||||
"unknown": "发生了一个为止错误。原始错误为:{{raw_error}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"testing": "測試版",
|
||||
"label": "更新頻道",
|
||||
"prerelease": "預發佈",
|
||||
"stable": "穩定版"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "5 秒後重新載入",
|
||||
"disabling": "正在停用 React DevTools",
|
||||
"enabling": "正在啟用 React DevTools"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "使用此資料夾"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_install": "安裝",
|
||||
"plugin_no_desc": "未提示描述。",
|
||||
"plugin_version_label": "外掛程式版本",
|
||||
"plugin_full_access": "此外掛程式擁有您的 Steam Deck 的完整存取權。"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "安裝",
|
||||
"button_processing": "正在安裝",
|
||||
"title": "安裝 {{artifact}}",
|
||||
"desc": "您確定要安裝 {{artifact}} {{version}} 嗎?"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "重新安裝",
|
||||
"button_processing": "正在重新安裝",
|
||||
"desc": "您確定要重新安裝 {{artifact}} {{version}} 嗎?",
|
||||
"title": "重新安裝 {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "更新",
|
||||
"button_processing": "正在更新",
|
||||
"desc": "您確定要更新 {{artifact}} {{version}} 嗎?",
|
||||
"title": "更新 {{artifact}}"
|
||||
},
|
||||
"no_hash": "此外掛程式沒有提供 hash 驗證,安裝可能有風險。"
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "未安裝外掛程式!",
|
||||
"plugin_actions": "外掛程式操作",
|
||||
"uninstall": "解除安裝",
|
||||
"update_to": "更新到 {{name}}",
|
||||
"reinstall": "重新安裝",
|
||||
"reload": "重新載入",
|
||||
"show": "快速存取:顯示",
|
||||
"hide": "快速存取:隱藏",
|
||||
"update_all_other": "更新 {{count}} 個外掛程式"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"error": "錯誤",
|
||||
"plugin_error_uninstall": "載入 {{name}} 導致上述異常。這通常意味著該外掛程式需要針對新版本的 SteamUI 進行更新。在 Decky 設定中檢查是否存在更新,或評估刪除此外掛程式。",
|
||||
"plugin_load_error": {
|
||||
"message": "載入外掛程式 {{name}} 發生錯誤",
|
||||
"toast": "{{name}} 載入出錯"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "解除安裝",
|
||||
"title": "解除安裝 {{name}}",
|
||||
"desc": "您確定要解除安裝 {{name}} 嗎?"
|
||||
},
|
||||
"decky_update_available": "可更新至版本 {{tag_name}}!",
|
||||
"plugin_update_other": "可更新 {{count}} 個外掛程式!"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "允許您的網路中的任何人未經認證地存取 CEF 偵錯器",
|
||||
"label": "允許 CEF 遠端偵錯"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"third_party_plugins": {
|
||||
"button_zip": "開啟",
|
||||
"label_desc": "網址",
|
||||
"label_url": "從網址安裝外掛程式",
|
||||
"label_zip": "從 ZIP 檔案安裝外掛程式",
|
||||
"button_install": "安裝",
|
||||
"header": "第三方外掛程式"
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "安裝失敗!只支援 ZIP 檔案。",
|
||||
"title": "Decky"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc2": "除非您知道它的作用,否則不要碰這個選單中的任何東西。",
|
||||
"desc1": "啟用 Valve 內建開發人員選單。",
|
||||
"label": "啟用 Valve 內建"
|
||||
},
|
||||
"react_devtools": {
|
||||
"desc": "啟用與執行 React DevTools 的電腦的連接。改變這個設定將重新載入 Steam。啟用前必須設定 IP 位址。",
|
||||
"ip_label": "IP",
|
||||
"label": "啟用 React DevTools"
|
||||
},
|
||||
"header": "其他",
|
||||
"cef_console": {
|
||||
"button": "開啟控制台",
|
||||
"label": "CEF 控制台",
|
||||
"desc": "開啟 CEF 控制台。僅用於偵錯。這裡的東西有潛在的風險,只有當你是一個外掛程式開發者或者被外掛程式開發者引導到這裡時,才應該使用。"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"header": "關於",
|
||||
"decky_version": "Decky 版本"
|
||||
},
|
||||
"beta": {
|
||||
"header": "參與測試"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "開發人員模式",
|
||||
"desc": "啟用 Decky 的開發人員模式。"
|
||||
},
|
||||
"other": {
|
||||
"header": "其他"
|
||||
},
|
||||
"updates": {
|
||||
"header": "更新"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "開發人員",
|
||||
"general_title": "一般",
|
||||
"navbar_settings": "Decky 設定",
|
||||
"plugins_title": "外掛程式"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "貢獻",
|
||||
"desc": "如果您想為 Decky 外掛程式商店做貢獻,請查看 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 儲存庫。README 中提供了有關開發和發佈的資訊。"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "過濾",
|
||||
"label_def": "全部"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "搜尋"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "排序",
|
||||
"label_def": "最後更新 (最新)"
|
||||
},
|
||||
"store_source": {
|
||||
"label": "原始碼",
|
||||
"desc": "所有外掛程式原始碼可以在 GitHub 的 SteamDeckHomebrew/decky-plugin-database 儲存庫查看。"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "關於",
|
||||
"alph_asce": "依字母排序 (Z 到 A)",
|
||||
"alph_desc": "依字母排序 (A 到 Z)",
|
||||
"title": "瀏覽"
|
||||
},
|
||||
"store_testing_cta": "請考慮測試新的外掛程式來幫助 Decky Loader 團隊!"
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "自訂商店",
|
||||
"url_label": "網址"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "自訂",
|
||||
"default": "預設",
|
||||
"label": "商店頻道",
|
||||
"testing": "測試"
|
||||
}
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky 更新",
|
||||
"no_patch_notes_desc": "這個版本沒有更新日誌",
|
||||
"patch_notes_desc": "更新日誌",
|
||||
"updates": {
|
||||
"checking": "正在檢查",
|
||||
"install_button": "安裝更新",
|
||||
"label": "更新",
|
||||
"lat_version": "已是最新:執行 {{ver}}",
|
||||
"reloading": "正在重新載入",
|
||||
"check_button": "檢查更新",
|
||||
"cur_version": "目前版本:{{ver}}",
|
||||
"updating": "正在更新"
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_other": "{{count}} 個外掛程式已隱藏"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "已從快速存取選單中移除"
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_other": "修改 {{count}} 個外掛程式",
|
||||
"update_other": "更新 {{count}} 個外掛程式",
|
||||
"reinstall_other": "重新安裝 {{count}} 個外掛程式",
|
||||
"install_other": "安裝 {{count}} 個外掛程式"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "確定",
|
||||
"loading": "執行中"
|
||||
},
|
||||
"confirm": "您確定要進行以下的修改嗎?",
|
||||
"description": {
|
||||
"install": "安裝 {{name}} {{version}}",
|
||||
"update": "更新 {{name}} 到 {{version}}",
|
||||
"reinstall": "重新安裝 {{name}} {{version}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import platform, os
|
||||
|
||||
ON_WINDOWS = platform.system() == "Windows"
|
||||
ON_LINUX = not ON_WINDOWS
|
||||
|
||||
if ON_WINDOWS:
|
||||
from localplatformwin import *
|
||||
import localplatformwin as localplatform
|
||||
else:
|
||||
from localplatformlinux import *
|
||||
import localplatformlinux as localplatform
|
||||
|
||||
def get_privileged_path() -> str:
|
||||
'''Get path accessible by elevated user. Holds plugins, decky loader and decky loader configs'''
|
||||
return localplatform.get_privileged_path()
|
||||
|
||||
def get_unprivileged_path() -> str:
|
||||
'''Get path accessible by non-elevated user. Holds plugin configuration, plugin data and plugin logs. Externally referred to as the 'Homebrew' directory'''
|
||||
return localplatform.get_unprivileged_path()
|
||||
|
||||
def get_unprivileged_user() -> str:
|
||||
'''Get user that should own files made in unprivileged path'''
|
||||
return localplatform.get_unprivileged_user()
|
||||
|
||||
def get_chown_plugin_path() -> bool:
|
||||
return os.getenv("CHOWN_PLUGIN_PATH", "1") == "1"
|
||||
|
||||
def get_server_host() -> str:
|
||||
return os.getenv("SERVER_HOST", "127.0.0.1")
|
||||
|
||||
def get_server_port() -> int:
|
||||
return int(os.getenv("SERVER_PORT", "1337"))
|
||||
|
||||
def get_live_reload() -> bool:
|
||||
return os.getenv("LIVE_RELOAD", "1") == "1"
|
||||
|
||||
def get_keep_systemd_service() -> bool:
|
||||
return os.getenv("KEEP_SYSTEMD_SERVICE", "0") == "1"
|
||||
|
||||
def get_log_level() -> int:
|
||||
return {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
|
||||
os.getenv("LOG_LEVEL", "INFO")
|
||||
]
|
||||
@@ -0,0 +1,194 @@
|
||||
import os, pwd, grp, sys, logging
|
||||
from subprocess import call, run, DEVNULL, PIPE, STDOUT
|
||||
from customtypes import UserType
|
||||
|
||||
logger = logging.getLogger("localplatform")
|
||||
|
||||
# Get the user id hosting the plugin loader
|
||||
def _get_user_id() -> int:
|
||||
return pwd.getpwnam(_get_user()).pw_uid
|
||||
|
||||
# Get the user hosting the plugin loader
|
||||
def _get_user() -> str:
|
||||
return get_unprivileged_user()
|
||||
|
||||
# Get the effective user id of the running process
|
||||
def _get_effective_user_id() -> int:
|
||||
return os.geteuid()
|
||||
|
||||
# Get the effective user of the running process
|
||||
def _get_effective_user() -> str:
|
||||
return pwd.getpwuid(_get_effective_user_id()).pw_name
|
||||
|
||||
# Get the effective user group id of the running process
|
||||
def _get_effective_user_group_id() -> int:
|
||||
return os.getegid()
|
||||
|
||||
# Get the effective user group of the running process
|
||||
def _get_effective_user_group() -> str:
|
||||
return grp.getgrgid(_get_effective_user_group_id()).gr_name
|
||||
|
||||
# Get the user owner of the given file path.
|
||||
def _get_user_owner(file_path) -> str:
|
||||
return pwd.getpwuid(os.stat(file_path).st_uid).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 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 = ""
|
||||
|
||||
if user == UserType.HOST_USER:
|
||||
user_str = _get_user()+":"+_get_user_group()
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
user_str = _get_effective_user()+":"+_get_effective_user_group()
|
||||
elif user == UserType.ROOT:
|
||||
user_str = "root:root"
|
||||
else:
|
||||
raise Exception("Unknown User Type")
|
||||
|
||||
result = call(["chown", "-R", user_str, path] if recursive else ["chown", user_str, path])
|
||||
return result == 0
|
||||
|
||||
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
|
||||
result = call(["chmod", "-R", str(permissions), path] if recursive else ["chmod", str(permissions), path])
|
||||
return result == 0
|
||||
|
||||
def folder_owner(path : str) -> UserType|None:
|
||||
user_owner = _get_user_owner(path)
|
||||
|
||||
if (user_owner == _get_user()):
|
||||
return UserType.HOST_USER
|
||||
|
||||
elif (user_owner == _get_effective_user()):
|
||||
return UserType.EFFECTIVE_USER
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
|
||||
user_name = "root"
|
||||
|
||||
if user == UserType.HOST_USER:
|
||||
user_name = _get_user()
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
user_name = _get_effective_user()
|
||||
elif user == UserType.ROOT:
|
||||
pass
|
||||
else:
|
||||
raise Exception("Unknown User Type")
|
||||
|
||||
return pwd.getpwnam(user_name).pw_dir
|
||||
|
||||
def get_username() -> str:
|
||||
return _get_user()
|
||||
|
||||
def setgid(user : UserType = UserType.HOST_USER):
|
||||
user_id = 0
|
||||
|
||||
if user == UserType.HOST_USER:
|
||||
user_id = _get_user_group_id()
|
||||
elif user == UserType.ROOT:
|
||||
pass
|
||||
else:
|
||||
raise Exception("Unknown user type")
|
||||
|
||||
os.setgid(user_id)
|
||||
|
||||
def setuid(user : UserType = UserType.HOST_USER):
|
||||
user_id = 0
|
||||
|
||||
if user == UserType.HOST_USER:
|
||||
user_id = _get_user_id()
|
||||
elif user == UserType.ROOT:
|
||||
pass
|
||||
else:
|
||||
raise Exception("Unknown user type")
|
||||
|
||||
os.setuid(user_id)
|
||||
|
||||
async def service_active(service_name : str) -> bool:
|
||||
res = run(["systemctl", "is-active", service_name], stdout=DEVNULL, stderr=DEVNULL)
|
||||
return res.returncode == 0
|
||||
|
||||
async def service_restart(service_name : str) -> bool:
|
||||
call(["systemctl", "daemon-reload"])
|
||||
cmd = ["systemctl", "restart", service_name]
|
||||
res = run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
return res.returncode == 0
|
||||
|
||||
async def service_stop(service_name : str) -> bool:
|
||||
cmd = ["systemctl", "stop", service_name]
|
||||
res = run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
return res.returncode == 0
|
||||
|
||||
async def service_start(service_name : str) -> bool:
|
||||
cmd = ["systemctl", "start", service_name]
|
||||
res = run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
return res.returncode == 0
|
||||
|
||||
def get_privileged_path() -> str:
|
||||
path = os.getenv("PRIVILEGED_PATH")
|
||||
|
||||
if path == None:
|
||||
path = get_unprivileged_path()
|
||||
|
||||
return path
|
||||
|
||||
def _parent_dir(path : str) -> str:
|
||||
if path == None:
|
||||
return None
|
||||
|
||||
if path.endswith('/'):
|
||||
path = path[:-1]
|
||||
|
||||
return os.path.dirname(path)
|
||||
|
||||
def get_unprivileged_path() -> str:
|
||||
path = os.getenv("UNPRIVILEGED_PATH")
|
||||
|
||||
if path == None:
|
||||
path = _parent_dir(os.getenv("PLUGIN_PATH"))
|
||||
|
||||
if path == None:
|
||||
logger.debug("Unprivileged path is not properly configured. Making something up!")
|
||||
# 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):
|
||||
path = None
|
||||
|
||||
if path == None:
|
||||
logger.warn("Unprivileged path is not properly configured. Defaulting to /home/deck/homebrew")
|
||||
path = "/home/deck/homebrew" # We give up
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def get_unprivileged_user() -> str:
|
||||
user = os.getenv("UNPRIVILEGED_USER")
|
||||
|
||||
if user == None:
|
||||
# Lets hope we can extract it from the unprivileged dir
|
||||
dir = os.path.realpath(get_unprivileged_path())
|
||||
|
||||
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
|
||||
for pw in pws:
|
||||
if dir.startswith(os.path.realpath(pw.pw_dir)):
|
||||
user = pw.pw_name
|
||||
break
|
||||
|
||||
if user == None:
|
||||
logger.warn("Unprivileged user is not properly configured. Defaulting to 'deck'")
|
||||
user = 'deck'
|
||||
|
||||
return user
|
||||
@@ -0,0 +1,53 @@
|
||||
from customtypes import UserType
|
||||
import os, sys
|
||||
|
||||
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
def folder_owner(path : str) -> UserType|None:
|
||||
return UserType.HOST_USER # Stubbed
|
||||
|
||||
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
|
||||
return os.path.expanduser("~") # Mostly stubbed
|
||||
|
||||
def setgid(user : UserType = UserType.HOST_USER):
|
||||
pass # Stubbed
|
||||
|
||||
def setuid(user : UserType = UserType.HOST_USER):
|
||||
pass # Stubbed
|
||||
|
||||
async def service_active(service_name : str) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
async def service_stop(service_name : str) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
async def service_start(service_name : str) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
async def service_restart(service_name : str) -> bool:
|
||||
if service_name == "plugin_loader":
|
||||
sys.exit(42)
|
||||
|
||||
return True # Stubbed
|
||||
|
||||
def get_username() -> str:
|
||||
return os.getlogin()
|
||||
|
||||
def get_privileged_path() -> str:
|
||||
'''On windows, privileged_path is equal to unprivileged_path'''
|
||||
return get_unprivileged_path()
|
||||
|
||||
def get_unprivileged_path() -> str:
|
||||
path = os.getenv("UNPRIVILEGED_PATH")
|
||||
|
||||
if path == None:
|
||||
path = os.getenv("PRIVILEGED_PATH", os.path.join(os.path.expanduser("~"), "homebrew"))
|
||||
|
||||
return path
|
||||
|
||||
def get_unprivileged_user() -> str:
|
||||
return os.getenv("UNPRIVILEGED_USER", os.getlogin())
|
||||
@@ -0,0 +1,132 @@
|
||||
import asyncio, time, random
|
||||
from localplatform import ON_WINDOWS
|
||||
|
||||
BUFFER_LIMIT = 2 ** 20 # 1 MiB
|
||||
|
||||
class UnixSocket:
|
||||
def __init__(self, on_new_message):
|
||||
'''
|
||||
on_new_message takes 1 string argument.
|
||||
It's return value gets used, if not None, to write data to the socket.
|
||||
Method should be async
|
||||
'''
|
||||
self.socket_addr = f"/tmp/plugin_socket_{time.time()}"
|
||||
self.on_new_message = on_new_message
|
||||
self.socket = None
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
|
||||
async def setup_server(self):
|
||||
self.socket = await asyncio.start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
|
||||
|
||||
async def _open_socket_if_not_exists(self):
|
||||
if not self.reader:
|
||||
retries = 0
|
||||
while retries < 10:
|
||||
try:
|
||||
self.reader, self.writer = await asyncio.open_unix_connection(self.socket_addr, limit=BUFFER_LIMIT)
|
||||
return True
|
||||
except:
|
||||
await asyncio.sleep(2)
|
||||
retries += 1
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
async def get_socket_connection(self):
|
||||
if not await self._open_socket_if_not_exists():
|
||||
return None, None
|
||||
|
||||
return self.reader, self.writer
|
||||
|
||||
async def close_socket_connection(self):
|
||||
if self.writer != None:
|
||||
self.writer.close()
|
||||
|
||||
self.reader = None
|
||||
|
||||
async def read_single_line(self) -> str|None:
|
||||
reader, writer = await self.get_socket_connection()
|
||||
|
||||
if self.reader == None:
|
||||
return None
|
||||
|
||||
return await self._read_single_line(reader)
|
||||
|
||||
async def write_single_line(self, message : str):
|
||||
reader, writer = await self.get_socket_connection()
|
||||
|
||||
if self.writer == None:
|
||||
return;
|
||||
|
||||
await self._write_single_line(writer, message)
|
||||
|
||||
async def _read_single_line(self, reader) -> str:
|
||||
line = bytearray()
|
||||
while True:
|
||||
try:
|
||||
line.extend(await reader.readuntil())
|
||||
except asyncio.LimitOverrunError:
|
||||
line.extend(await reader.read(reader._limit))
|
||||
continue
|
||||
except asyncio.IncompleteReadError as err:
|
||||
line.extend(err.partial)
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
return line.decode("utf-8")
|
||||
|
||||
async def _write_single_line(self, writer, 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):
|
||||
while True:
|
||||
line = await self._read_single_line(reader)
|
||||
|
||||
try:
|
||||
res = await self.on_new_message(line)
|
||||
except Exception as e:
|
||||
return
|
||||
|
||||
if res != None:
|
||||
await self._write_single_line(writer, res)
|
||||
|
||||
class PortSocket (UnixSocket):
|
||||
def __init__(self, on_new_message):
|
||||
'''
|
||||
on_new_message takes 1 string argument.
|
||||
It's return value gets used, if not None, to write data to the socket.
|
||||
Method should be async
|
||||
'''
|
||||
super().__init__(on_new_message)
|
||||
self.host = "127.0.0.1"
|
||||
self.port = random.sample(range(40000, 60000), 1)[0]
|
||||
|
||||
async def setup_server(self):
|
||||
self.socket = await asyncio.start_server(self._listen_for_method_call, host=self.host, port=self.port, limit=BUFFER_LIMIT)
|
||||
|
||||
async def _open_socket_if_not_exists(self):
|
||||
if not self.reader:
|
||||
retries = 0
|
||||
while retries < 10:
|
||||
try:
|
||||
self.reader, self.writer = await asyncio.open_connection(host=self.host, port=self.port, limit=BUFFER_LIMIT)
|
||||
return True
|
||||
except:
|
||||
await asyncio.sleep(2)
|
||||
retries += 1
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
if ON_WINDOWS:
|
||||
class LocalSocket (PortSocket):
|
||||
pass
|
||||
else:
|
||||
class LocalSocket (UnixSocket):
|
||||
pass
|
||||
+116
-66
@@ -1,70 +1,60 @@
|
||||
# Change PyInstaller files permissions
|
||||
import sys
|
||||
from subprocess import call
|
||||
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'):
|
||||
call(['chmod', '-R', '755', sys._MEIPASS])
|
||||
chmod(sys._MEIPASS, 755)
|
||||
# Full imports
|
||||
from asyncio import get_event_loop, sleep
|
||||
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, chmod, path
|
||||
from os import getenv, path
|
||||
from traceback import format_exc
|
||||
import multiprocessing
|
||||
|
||||
import aiohttp_cors
|
||||
# Partial imports
|
||||
from aiohttp import ClientSession
|
||||
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,
|
||||
get_home_path, get_homebrew_path, get_user,
|
||||
get_user_group, set_user, set_user_group,
|
||||
stop_systemd_unit)
|
||||
from injector import inject_to_tab, tab_has_global_var
|
||||
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
|
||||
|
||||
# Ensure USER and GROUP vars are set first.
|
||||
# TODO: This isn't the best way to do this but supports the current
|
||||
# implementation. All the config load and environment setting eventually be
|
||||
# moved into init or a config/loader method.
|
||||
set_user()
|
||||
set_user_group()
|
||||
USER = get_user()
|
||||
GROUP = get_user_group()
|
||||
HOME_PATH = "/home/"+USER
|
||||
HOMEBREW_PATH = HOME_PATH+"/homebrew"
|
||||
CONFIG = {
|
||||
"plugin_path": getenv("PLUGIN_PATH", HOMEBREW_PATH+"/plugins"),
|
||||
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
|
||||
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
||||
"server_port": int(getenv("SERVER_PORT", "1337")),
|
||||
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
|
||||
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
|
||||
getenv("LOG_LEVEL", "INFO")
|
||||
],
|
||||
}
|
||||
|
||||
basicConfig(
|
||||
level=CONFIG["log_level"],
|
||||
level=get_log_level(),
|
||||
format="[%(module)s][%(levelname)s]: %(message)s"
|
||||
)
|
||||
|
||||
logger = getLogger("Main")
|
||||
plugin_path = path.join(get_privileged_path(), "plugins")
|
||||
|
||||
async def chown_plugin_dir(_):
|
||||
code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]])
|
||||
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
|
||||
if code_chown != 0 or code_chmod != 0:
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
|
||||
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) -> None:
|
||||
self.loop = get_event_loop()
|
||||
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={
|
||||
@@ -74,19 +64,24 @@ class PluginManager:
|
||||
allow_credentials=True
|
||||
)
|
||||
})
|
||||
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
|
||||
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.plugin_loader.plugins, self.plugin_loader)
|
||||
self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "settings"))
|
||||
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)
|
||||
if CONFIG["chown_plugin_path"] == True:
|
||||
self.web_app.on_startup.append(chown_plugin_dir)
|
||||
self.loop.create_task(self.loader_reinjector())
|
||||
self.loop.create_task(self.load_plugins())
|
||||
if not self.settings.getSetting("cef_forward", False):
|
||||
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT))
|
||||
|
||||
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)])
|
||||
|
||||
@@ -103,38 +98,93 @@ class PluginManager:
|
||||
async def get_auth_token(self, request):
|
||||
return Response(text=get_csrf_token())
|
||||
|
||||
async def wait_for_server(self):
|
||||
async with ClientSession() as web:
|
||||
while True:
|
||||
try:
|
||||
await web.get(f"http://{CONFIG['server_host']}:{CONFIG['server_port']}")
|
||||
return
|
||||
except Exception as e:
|
||||
await sleep(0.1)
|
||||
|
||||
async def load_plugins(self):
|
||||
await self.wait_for_server()
|
||||
# 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):
|
||||
await sleep(2)
|
||||
await self.inject_javascript()
|
||||
while True:
|
||||
await sleep(5)
|
||||
if not await tab_has_global_var("SP", "deckyHasLoaded"):
|
||||
logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||
await self.inject_javascript()
|
||||
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, request=None):
|
||||
async def inject_javascript(self, tab: Tab, first=False, request=None):
|
||||
logger.info("Loading Decky frontend!")
|
||||
try:
|
||||
await inject_to_tab("SP", "try{if (window.deckyHasLoaded) location.reload();window.deckyHasLoaded = true;(async()=>{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')})();}catch(e){console.error(e)}", True)
|
||||
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=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
|
||||
return run_app(self.web_app, host=get_server_host(), port=get_server_port(), loop=self.loop, access_log=None)
|
||||
|
||||
if __name__ == "__main__":
|
||||
PluginManager().run()
|
||||
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()
|
||||
|
||||
+82
-84
@@ -1,38 +1,35 @@
|
||||
import multiprocessing
|
||||
from asyncio import (Lock, get_event_loop, new_event_loop,
|
||||
open_unix_connection, set_event_loop, sleep,
|
||||
start_unix_server, IncompleteReadError, LimitOverrunError)
|
||||
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
|
||||
from traceback import format_exc
|
||||
from os import path, setgid, setuid
|
||||
from os import path, environ
|
||||
from signal import SIGINT, signal
|
||||
from sys import exit
|
||||
from sys import exit, path as syspath
|
||||
from time import time
|
||||
|
||||
multiprocessing.set_start_method("fork")
|
||||
|
||||
BUFFER_LIMIT = 2 ** 20 # 1 MiB
|
||||
from localsocket import LocalSocket
|
||||
from localplatform import setgid, setuid, get_username, get_home_path
|
||||
from customtypes import UserType
|
||||
import helpers
|
||||
|
||||
class PluginWrapper:
|
||||
def __init__(self, file, plugin_directory, plugin_path) -> None:
|
||||
self.file = file
|
||||
self.plugin_path = plugin_path
|
||||
self.plugin_directory = plugin_directory
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
self.socket_addr = f"/tmp/plugin_socket_{time()}"
|
||||
self.method_call_lock = Lock()
|
||||
self.socket = LocalSocket(self._on_new_message)
|
||||
|
||||
self.version = None
|
||||
|
||||
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r"))
|
||||
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
|
||||
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
|
||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r"))
|
||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
|
||||
self.version = package_json["version"]
|
||||
|
||||
|
||||
self.legacy = False
|
||||
self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
|
||||
self.tile_view_html = json["tile_view_html"] if "tile_view_html" in json else ""
|
||||
@@ -56,68 +53,79 @@ class PluginWrapper:
|
||||
set_event_loop(new_event_loop())
|
||||
if self.passive:
|
||||
return
|
||||
setgid(0 if "root" in self.flags else 1000)
|
||||
setuid(0 if "root" in self.flags else 1000)
|
||||
setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
setuid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
# export a bunch of environment variables to help plugin developers
|
||||
environ["HOME"] = get_home_path(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
environ["USER"] = "root" if "root" in self.flags else get_username()
|
||||
environ["DECKY_VERSION"] = helpers.get_loader_version()
|
||||
environ["DECKY_USER"] = get_username()
|
||||
environ["DECKY_USER_HOME"] = helpers.get_home_path()
|
||||
environ["DECKY_HOME"] = helpers.get_homebrew_path()
|
||||
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
|
||||
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "settings"))
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"])
|
||||
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
|
||||
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "data"))
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_RUNTIME_DIR"])
|
||||
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
|
||||
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "logs"))
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_LOG_DIR"])
|
||||
environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory)
|
||||
environ["DECKY_PLUGIN_NAME"] = self.name
|
||||
environ["DECKY_PLUGIN_VERSION"] = self.version
|
||||
environ["DECKY_PLUGIN_AUTHOR"] = self.author
|
||||
|
||||
# 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)
|
||||
module = module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
self.Plugin = module.Plugin
|
||||
|
||||
if hasattr(self.Plugin, "_migration"):
|
||||
get_event_loop().run_until_complete(self.Plugin._migration(self.Plugin))
|
||||
if hasattr(self.Plugin, "_main"):
|
||||
get_event_loop().create_task(self.Plugin._main(self.Plugin))
|
||||
get_event_loop().create_task(self._setup_socket())
|
||||
get_event_loop().create_task(self.socket.setup_server())
|
||||
get_event_loop().run_forever()
|
||||
except:
|
||||
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
|
||||
exit(0)
|
||||
|
||||
async def _setup_socket(self):
|
||||
self.socket = await start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
|
||||
async def _unload(self):
|
||||
try:
|
||||
self.log.info("Attempting to unload with plugin " + self.name + "'s \"_unload\" function.\n")
|
||||
if hasattr(self.Plugin, "_unload"):
|
||||
await self.Plugin._unload(self.Plugin)
|
||||
self.log.info("Unloaded " + self.name + "\n")
|
||||
else:
|
||||
self.log.info("Could not find \"_unload\" in " + self.name + "'s main.py" + "\n")
|
||||
except:
|
||||
self.log.error("Failed to unload " + self.name + "!\n" + format_exc())
|
||||
exit(0)
|
||||
|
||||
async def _listen_for_method_call(self, reader, writer):
|
||||
while True:
|
||||
line = bytearray()
|
||||
while True:
|
||||
try:
|
||||
line.extend(await reader.readuntil())
|
||||
except LimitOverrunError:
|
||||
line.extend(await reader.read(reader._limit))
|
||||
continue
|
||||
except IncompleteReadError as err:
|
||||
line.extend(err.partial)
|
||||
break
|
||||
else:
|
||||
break
|
||||
data = loads(line.decode("utf-8"))
|
||||
if "stop" in data:
|
||||
get_event_loop().stop()
|
||||
while get_event_loop().is_running():
|
||||
await sleep(0)
|
||||
get_event_loop().close()
|
||||
return
|
||||
d = {"res": None, "success": True}
|
||||
try:
|
||||
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"])
|
||||
except Exception as e:
|
||||
d["res"] = str(e)
|
||||
d["success"] = False
|
||||
finally:
|
||||
writer.write((dumps(d)+"\n").encode("utf-8"))
|
||||
await writer.drain()
|
||||
async def _on_new_message(self, message : str) -> str|None:
|
||||
data = loads(message)
|
||||
|
||||
async def _open_socket_if_not_exists(self):
|
||||
if not self.reader:
|
||||
retries = 0
|
||||
while retries < 10:
|
||||
try:
|
||||
self.reader, self.writer = await open_unix_connection(self.socket_addr, limit=BUFFER_LIMIT)
|
||||
return True
|
||||
except:
|
||||
await sleep(2)
|
||||
retries += 1
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
if "stop" in data:
|
||||
self.log.info("Calling Loader unload function.")
|
||||
await self._unload()
|
||||
get_event_loop().stop()
|
||||
while get_event_loop().is_running():
|
||||
await sleep(0)
|
||||
get_event_loop().close()
|
||||
raise Exception("Closing message listener")
|
||||
|
||||
d = {"res": None, "success": True}
|
||||
try:
|
||||
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"])
|
||||
except Exception as e:
|
||||
d["res"] = str(e)
|
||||
d["success"] = False
|
||||
finally:
|
||||
return dumps(d, ensure_ascii=False)
|
||||
|
||||
def start(self):
|
||||
if self.passive:
|
||||
@@ -128,34 +136,24 @@ class PluginWrapper:
|
||||
def stop(self):
|
||||
if self.passive:
|
||||
return
|
||||
|
||||
async def _(self):
|
||||
if await self._open_socket_if_not_exists():
|
||||
self.writer.write((dumps({"stop": True})+"\n").encode("utf-8"))
|
||||
await self.writer.drain()
|
||||
self.writer.close()
|
||||
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):
|
||||
if self.passive:
|
||||
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
|
||||
async with self.method_call_lock:
|
||||
if await self._open_socket_if_not_exists():
|
||||
self.writer.write(
|
||||
(dumps({"method": method_name, "args": kwargs})+"\n").encode("utf-8"))
|
||||
await self.writer.drain()
|
||||
line = bytearray()
|
||||
while True:
|
||||
try:
|
||||
line.extend(await self.reader.readuntil())
|
||||
except LimitOverrunError:
|
||||
line.extend(await self.reader.read(self.reader._limit))
|
||||
continue
|
||||
except IncompleteReadError as err:
|
||||
line.extend(err.partial)
|
||||
break
|
||||
else:
|
||||
break
|
||||
res = loads(line.decode("utf-8"))
|
||||
reader, writer = await self.socket.get_socket_connection()
|
||||
|
||||
await self.socket.write_single_line(dumps({ "method": method_name, "args": kwargs }, ensure_ascii=False))
|
||||
|
||||
line = await self.socket.read_single_line()
|
||||
if line != None:
|
||||
res = loads(line)
|
||||
if not res["success"]:
|
||||
raise Exception(res["res"])
|
||||
return res["res"]
|
||||
return res["res"]
|
||||
+26
-11
@@ -1,42 +1,57 @@
|
||||
import imp
|
||||
from json import dump, load
|
||||
from os import mkdir, path
|
||||
from os import mkdir, path, listdir, rename
|
||||
from localplatform import chown, folder_owner, get_chown_plugin_path
|
||||
from customtypes import UserType
|
||||
|
||||
from helpers import get_home_path, get_homebrew_path, get_user, set_user
|
||||
from helpers import get_homebrew_path
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
def __init__(self, name, settings_directory = None) -> None:
|
||||
set_user()
|
||||
USER = get_user()
|
||||
wrong_dir = get_homebrew_path()
|
||||
if settings_directory == None:
|
||||
settings_directory = get_homebrew_path(get_home_path(USER))
|
||||
settings_directory = path.join(wrong_dir, "settings")
|
||||
|
||||
self.path = path.join(settings_directory, name + ".json")
|
||||
|
||||
#Create the folder with the correct permission
|
||||
if not path.exists(settings_directory):
|
||||
mkdir(settings_directory)
|
||||
|
||||
#Copy all old settings file in the root directory to the correct folder
|
||||
for file in listdir(wrong_dir):
|
||||
if file.endswith(".json"):
|
||||
rename(path.join(wrong_dir,file),
|
||||
path.join(settings_directory, file))
|
||||
self.path = path.join(settings_directory, name + ".json")
|
||||
|
||||
|
||||
#If the owner of the settings directory is not the user, then set it as the user:
|
||||
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.ROOT
|
||||
if folder_owner(settings_directory) != expected_user:
|
||||
chown(settings_directory, expected_user, False)
|
||||
|
||||
self.settings = {}
|
||||
|
||||
try:
|
||||
open(self.path, "x")
|
||||
open(self.path, "x", encoding="utf-8")
|
||||
except FileExistsError as e:
|
||||
self.read()
|
||||
pass
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
with open(self.path, "r") as file:
|
||||
with open(self.path, "r", encoding="utf-8") as file:
|
||||
self.settings = load(file)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
def commit(self):
|
||||
with open(self.path, "w+") as file:
|
||||
dump(self.settings, file, indent=4)
|
||||
with open(self.path, "w+", encoding="utf-8") as file:
|
||||
dump(self.settings, file, indent=4, ensure_ascii=False)
|
||||
|
||||
def getSetting(self, key, default):
|
||||
def getSetting(self, key, default=None):
|
||||
return self.settings.get(key, default)
|
||||
|
||||
def setSetting(self, key, value):
|
||||
|
||||
+99
-45
@@ -1,15 +1,17 @@
|
||||
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 subprocess import call
|
||||
from localplatform import chmod, service_restart, ON_LINUX, get_keep_systemd_service
|
||||
|
||||
from aiohttp import ClientSession, web
|
||||
|
||||
import helpers
|
||||
from injector import get_tab, inject_to_tab
|
||||
from injector import get_gamepadui_tab, inject_to_tab
|
||||
from settings import SettingsManager
|
||||
|
||||
logger = getLogger("Updater")
|
||||
@@ -28,12 +30,7 @@ class Updater:
|
||||
}
|
||||
self.remoteVer = None
|
||||
self.allRemoteVers = None
|
||||
try:
|
||||
logger.info(getcwd())
|
||||
with open(path.join(getcwd(), ".loader.version"), 'r') as version_file:
|
||||
self.localVer = version_file.readline().replace("\n", "")
|
||||
except:
|
||||
self.localVer = False
|
||||
self.localVer = helpers.get_loader_version()
|
||||
|
||||
try:
|
||||
self.currentBranch = self.get_branch(self.context.settings)
|
||||
@@ -68,27 +65,40 @@ class Updater:
|
||||
logger.debug("current branch: %i" % ver)
|
||||
if ver == -1:
|
||||
logger.info("Current branch is not set, determining branch from version...")
|
||||
if self.localVer.startswith("v") and self.localVer.find("-pre"):
|
||||
if self.localVer.startswith("v") and "-pre" in self.localVer:
|
||||
logger.info("Current version determined to be pre-release")
|
||||
manager.setSetting('branch', 1)
|
||||
return 1
|
||||
else:
|
||||
logger.info("Current version determined to be stable")
|
||||
manager.setSetting('branch', 0)
|
||||
return 0
|
||||
return ver
|
||||
|
||||
async def _get_branch(self, manager: SettingsManager):
|
||||
return self.get_branch(manager)
|
||||
|
||||
# retrieve relevant service file's url for each branch
|
||||
def get_service_url(self):
|
||||
logger.debug("Getting service URL")
|
||||
branch = self.get_branch(self.context.settings)
|
||||
match branch:
|
||||
case 0:
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service"
|
||||
case 1 | 2:
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
|
||||
case _:
|
||||
logger.error("You have an invalid branch set... Defaulting to prerelease service, please send the logs to the devs!")
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
|
||||
return str(url)
|
||||
|
||||
async def get_version(self):
|
||||
if self.localVer:
|
||||
return {
|
||||
"current": self.localVer,
|
||||
"remote": self.remoteVer,
|
||||
"all": self.allRemoteVers,
|
||||
"updatable": self.localVer != None
|
||||
}
|
||||
else:
|
||||
return {"current": "unknown", "remote": self.remoteVer, "all": self.allRemoteVers, "updatable": False}
|
||||
return {
|
||||
"current": self.localVer,
|
||||
"remote": self.remoteVer,
|
||||
"all": self.allRemoteVers,
|
||||
"updatable": self.localVer != "unknown"
|
||||
}
|
||||
|
||||
async def check_for_updates(self):
|
||||
logger.debug("checking for updates")
|
||||
@@ -96,25 +106,29 @@ class Updater:
|
||||
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()
|
||||
self.allRemoteVers = remoteVersions
|
||||
logger.debug("determining release type to find, branch is %i" % selectedBranch)
|
||||
if selectedBranch == 0:
|
||||
logger.debug("release type: release")
|
||||
self.remoteVer = next(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and ver["tag_name"], remoteVersions), None)
|
||||
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))
|
||||
elif selectedBranch == 1:
|
||||
logger.debug("release type: pre-release")
|
||||
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].find("-pre"), remoteVersions), None)
|
||||
# elif selectedBranch == 2:
|
||||
# logger.debug("release type: nightly")
|
||||
# self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].find("nightly"), remoteVersions), None)
|
||||
remoteVersions = list(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions))
|
||||
else:
|
||||
logger.error("release type: NOT FOUND")
|
||||
raise ValueError("no valid branch found")
|
||||
# doesn't make it to this line below or farther
|
||||
# logger.debug("Remote Version: %s" % self.remoteVer.find("name"))
|
||||
logger.info("Updated remote version information")
|
||||
tab = await get_tab("SP")
|
||||
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
|
||||
self.allRemoteVers = remoteVersions
|
||||
logger.debug("determining release type to find, branch is %i" % selectedBranch)
|
||||
if selectedBranch == 0:
|
||||
logger.debug("release type: release")
|
||||
self.remoteVer = next(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), None)
|
||||
elif selectedBranch == 1:
|
||||
logger.debug("release type: pre-release")
|
||||
self.remoteVer = next(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions), None)
|
||||
else:
|
||||
logger.error("release type: NOT FOUND")
|
||||
raise ValueError("no valid branch found")
|
||||
logger.info("Updated remote version information")
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
|
||||
return await self.get_version()
|
||||
|
||||
async def version_reloader(self):
|
||||
@@ -127,19 +141,56 @@ class Updater:
|
||||
await sleep(60 * 60 * 6) # 6 hours
|
||||
|
||||
async def do_update(self):
|
||||
logger.debug("Starting update.")
|
||||
version = self.remoteVer["tag_name"]
|
||||
download_url = self.remoteVer["assets"][0]["browser_download_url"]
|
||||
download_url = None
|
||||
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
|
||||
download_temp_filename = download_filename + ".new"
|
||||
|
||||
tab = await get_tab("SP")
|
||||
for x in self.remoteVer["assets"]:
|
||||
if x["name"] == download_filename:
|
||||
download_url = x["browser_download_url"]
|
||||
break
|
||||
|
||||
if download_url == None:
|
||||
raise Exception("Download url not found")
|
||||
|
||||
service_url = self.get_service_url()
|
||||
logger.debug("Retrieved service URL")
|
||||
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.open_websocket()
|
||||
async with ClientSession() as web:
|
||||
if ON_LINUX and not get_keep_systemd_service():
|
||||
logger.debug("Downloading systemd service")
|
||||
# download the relevant systemd service depending upon branch
|
||||
async with web.request("GET", service_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
||||
logger.debug("Downloading service file")
|
||||
data = await res.content.read()
|
||||
logger.debug(str(data))
|
||||
service_file_path = path.join(getcwd(), "plugin_loader.service")
|
||||
try:
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out:
|
||||
out.write(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error at %s", exc_info=e)
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8") as service_file:
|
||||
service_data = service_file.read()
|
||||
service_data = service_data.replace("${HOMEBREW_FOLDER}", helpers.get_homebrew_path())
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8") as service_file:
|
||||
service_file.write(service_data)
|
||||
|
||||
logger.debug("Saved service file")
|
||||
logger.debug("Copying service file over current file.")
|
||||
shutil.copy(service_file_path, "/etc/systemd/system/plugin_loader.service")
|
||||
if not os.path.exists(path.join(getcwd(), ".systemd")):
|
||||
os.mkdir(path.join(getcwd(), ".systemd"))
|
||||
shutil.move(service_file_path, path.join(getcwd(), ".systemd")+"/plugin_loader.service")
|
||||
|
||||
logger.debug("Downloading binary")
|
||||
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
||||
total = int(res.headers.get('content-length', 0))
|
||||
try:
|
||||
remove(path.join(getcwd(), "PluginLoader"))
|
||||
except:
|
||||
pass
|
||||
with open(path.join(getcwd(), "PluginLoader"), "wb") as out:
|
||||
with open(path.join(getcwd(), download_temp_filename), "wb") as out:
|
||||
progress = 0
|
||||
raw = 0
|
||||
async for c in res.content.iter_chunked(512):
|
||||
@@ -150,15 +201,18 @@ class Updater:
|
||||
self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False))
|
||||
progress = new_progress
|
||||
|
||||
with open(path.join(getcwd(), ".loader.version"), "w") as out:
|
||||
out.write(version)
|
||||
with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
|
||||
out.write(version)
|
||||
|
||||
call(['chmod', '+x', path.join(getcwd(), "PluginLoader")])
|
||||
if ON_LINUX:
|
||||
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)
|
||||
|
||||
logger.info("Updated loader installation.")
|
||||
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
|
||||
await tab.client.close()
|
||||
logger.info("Updated loader installation.")
|
||||
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
|
||||
await self.do_restart()
|
||||
await tab.close_websocket()
|
||||
|
||||
async def do_restart(self):
|
||||
call(["systemctl", "daemon-reload"])
|
||||
call(["systemctl", "restart", "plugin_loader"])
|
||||
await service_restart("plugin_loader")
|
||||
|
||||
+193
-32
@@ -1,13 +1,21 @@
|
||||
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 asyncio import sleep, start_server, gather, open_connection
|
||||
from aiohttp import ClientSession, web
|
||||
|
||||
from injector import inject_to_tab
|
||||
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
|
||||
|
||||
class Utilities:
|
||||
def __init__(self, context) -> None:
|
||||
@@ -16,6 +24,7 @@ class Utilities:
|
||||
"ping": self.ping,
|
||||
"http_request": self.http_request,
|
||||
"install_plugin": self.install_plugin,
|
||||
"install_plugins": self.install_plugins,
|
||||
"cancel_plugin_install": self.cancel_plugin_install,
|
||||
"confirm_plugin_install": self.confirm_plugin_install,
|
||||
"uninstall_plugin": self.uninstall_plugin,
|
||||
@@ -26,9 +35,19 @@ class Utilities:
|
||||
"disallow_remote_debugging": self.disallow_remote_debugging,
|
||||
"set_setting": self.set_setting,
|
||||
"get_setting": self.get_setting,
|
||||
"filepicker_ls": self.filepicker_ls
|
||||
"filepicker_ls": self.filepicker_ls,
|
||||
"disable_rdt": self.disable_rdt,
|
||||
"enable_rdt": self.enable_rdt,
|
||||
"get_tab_id": self.get_tab_id,
|
||||
"get_user_info": self.get_user_info,
|
||||
}
|
||||
|
||||
self.logger = getLogger("Utilities")
|
||||
|
||||
self.rdt_proxy_server = None
|
||||
self.rdt_script_id = None
|
||||
self.rdt_proxy_task = None
|
||||
|
||||
if context:
|
||||
context.web_app.add_routes([
|
||||
web.post("/methods/{method_name}", self._handle_server_method_call)
|
||||
@@ -50,12 +69,18 @@ class Utilities:
|
||||
res["success"] = False
|
||||
return web.json_response(res)
|
||||
|
||||
async def install_plugin(self, artifact="", name="No name", version="dev", hash=False):
|
||||
async def install_plugin(self, artifact="", name="No name", version="dev", hash=False, install_type=0):
|
||||
return await self.context.plugin_browser.request_plugin_install(
|
||||
artifact=artifact,
|
||||
name=name,
|
||||
version=version,
|
||||
hash=hash
|
||||
hash=hash,
|
||||
install_type=install_type
|
||||
)
|
||||
|
||||
async def install_plugins(self, requests):
|
||||
return await self.context.plugin_browser.request_multiple_plugin_installs(
|
||||
requests=requests
|
||||
)
|
||||
|
||||
async def confirm_plugin_install(self, request_id):
|
||||
@@ -69,12 +94,13 @@ class Utilities:
|
||||
|
||||
async def http_request(self, method="", url="", **kwargs):
|
||||
async with ClientSession() as web:
|
||||
async with web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs) as res:
|
||||
return {
|
||||
"status": res.status,
|
||||
"headers": dict(res.headers),
|
||||
"body": await res.text()
|
||||
}
|
||||
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs)
|
||||
text = await res.text()
|
||||
return {
|
||||
"status": res.status,
|
||||
"headers": dict(res.headers),
|
||||
"body": text
|
||||
}
|
||||
|
||||
async def ping(self, **kwargs):
|
||||
return "pong"
|
||||
@@ -162,35 +188,170 @@ class Utilities:
|
||||
return self.context.settings.setSetting(key, value)
|
||||
|
||||
async def allow_remote_debugging(self):
|
||||
await helpers.start_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
|
||||
await service_start(helpers.REMOTE_DEBUGGER_UNIT)
|
||||
return True
|
||||
|
||||
async def disallow_remote_debugging(self):
|
||||
await helpers.stop_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
|
||||
await service_stop(helpers.REMOTE_DEBUGGER_UNIT)
|
||||
return True
|
||||
|
||||
async def filepicker_ls(self, path, include_files=True):
|
||||
# def sorter(file): # Modification time
|
||||
# if os.path.isdir(os.path.join(path, file)) or os.path.isfile(os.path.join(path, file)):
|
||||
# return os.path.getmtime(os.path.join(path, file))
|
||||
# return 0
|
||||
# file_names = sorted(os.listdir(path), key=sorter, reverse=True) # TODO provide more sort options
|
||||
file_names = sorted(os.listdir(path)) # Alphabetical
|
||||
async def filepicker_ls(self,
|
||||
path : str | None = None,
|
||||
include_files: bool = True,
|
||||
include_folders: bool = True,
|
||||
include_ext: list[str] = [],
|
||||
include_hidden: bool = False,
|
||||
order_by: str = "name_asc",
|
||||
filter_for: str | None = None,
|
||||
page: int = 1,
|
||||
max: int = 1000):
|
||||
|
||||
if path == None:
|
||||
path = get_home_path()
|
||||
|
||||
files = []
|
||||
path = Path(path).resolve()
|
||||
|
||||
for file in file_names:
|
||||
full_path = os.path.join(path, file)
|
||||
is_dir = os.path.isdir(full_path)
|
||||
files, folders = [], []
|
||||
|
||||
if is_dir or include_files:
|
||||
files.append({
|
||||
"isdir": is_dir,
|
||||
"name": file,
|
||||
"realpath": os.path.realpath(full_path)
|
||||
})
|
||||
#Resolving all files/folders in the requested directory
|
||||
for file in path.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)
|
||||
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})
|
||||
elif include_files:
|
||||
# Handle requested extensions if present
|
||||
if len(include_ext) == 0 or 'all_files' in include_ext \
|
||||
or splitext(file.name)[1].lstrip('.') in include_ext:
|
||||
if (is_hidden and include_hidden) or not is_hidden:
|
||||
files.append({"file": file, "filest": filest, "is_dir": False})
|
||||
# Filter logic
|
||||
if filter_for is not None:
|
||||
try:
|
||||
if re.compile(filter_for):
|
||||
files = filter(lambda file: re.search(filter_for, file.name) != None, files)
|
||||
except re.error:
|
||||
files = filter(lambda file: file.name.find(filter_for) != -1, files)
|
||||
|
||||
# Ordering logic
|
||||
ord_arg = order_by.split("_")
|
||||
ord = ord_arg[0]
|
||||
rev = True if ord_arg[1] == "asc" else False
|
||||
match ord:
|
||||
case 'name':
|
||||
files.sort(key=lambda x: x['file'].name.casefold(), reverse = rev)
|
||||
folders.sort(key=lambda x: x['file'].name.casefold(), reverse = rev)
|
||||
case 'modified':
|
||||
files.sort(key=lambda x: x['filest'].st_mtime, reverse = not rev)
|
||||
folders.sort(key=lambda x: x['filest'].st_mtime, reverse = not rev)
|
||||
case 'created':
|
||||
files.sort(key=lambda x: x['filest'].st_ctime, reverse = not rev)
|
||||
folders.sort(key=lambda x: x['filest'].st_ctime, reverse = not rev)
|
||||
case 'size':
|
||||
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())
|
||||
|
||||
#Constructing the final file list, folders first
|
||||
all = [{
|
||||
"isdir": x['is_dir'],
|
||||
"name": str(x['file'].name),
|
||||
"realpath": str(x['file']),
|
||||
"size": x['filest'].st_size,
|
||||
"modified": x['filest'].st_mtime,
|
||||
"created": x['filest'].st_ctime,
|
||||
} for x in folders + files ]
|
||||
|
||||
return {
|
||||
"realpath": os.path.realpath(path),
|
||||
"files": files
|
||||
"realpath": str(path),
|
||||
"files": all[(page-1)*max:(page)*max],
|
||||
"total": len(all),
|
||||
}
|
||||
|
||||
|
||||
# Based on https://stackoverflow.com/a/46422554/13174603
|
||||
def start_rdt_proxy(self, ip, port):
|
||||
async def pipe(reader, writer):
|
||||
try:
|
||||
while not reader.at_eof():
|
||||
writer.write(await reader.read(2048))
|
||||
finally:
|
||||
writer.close()
|
||||
async def handle_client(local_reader, local_writer):
|
||||
try:
|
||||
remote_reader, remote_writer = await open_connection(
|
||||
ip, port)
|
||||
pipe1 = pipe(local_reader, remote_writer)
|
||||
pipe2 = pipe(remote_reader, local_writer)
|
||||
await gather(pipe1, pipe2)
|
||||
finally:
|
||||
local_writer.close()
|
||||
|
||||
self.rdt_proxy_server = start_server(handle_client, "127.0.0.1", port)
|
||||
self.rdt_proxy_task = self.context.loop.create_task(self.rdt_proxy_server)
|
||||
|
||||
def stop_rdt_proxy(self):
|
||||
if self.rdt_proxy_server:
|
||||
self.rdt_proxy_server.close()
|
||||
self.rdt_proxy_task.cancel()
|
||||
|
||||
async def _enable_rdt(self):
|
||||
# TODO un-hardcode port
|
||||
try:
|
||||
self.stop_rdt_proxy()
|
||||
ip = self.context.settings.getSetting("developer.rdt.ip", None)
|
||||
|
||||
if ip != None:
|
||||
self.logger.info("Connecting to React DevTools at " + ip)
|
||||
async with ClientSession() as web:
|
||||
res = await web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context())
|
||||
script = """
|
||||
if (!window.deckyHasConnectedRDT) {
|
||||
window.deckyHasConnectedRDT = true;
|
||||
// This fixes the overlay when hovering over an element in RDT
|
||||
Object.defineProperty(window, '__REACT_DEVTOOLS_TARGET_WINDOW__', {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return (GamepadNavTree?.m_context?.m_controller || FocusNavController)?.m_ActiveContext?.ActiveWindow || window;
|
||||
}
|
||||
});
|
||||
""" + await res.text() + "\n}"
|
||||
if res.status != 200:
|
||||
self.logger.error("Failed to connect to React DevTools at " + ip)
|
||||
return False
|
||||
self.start_rdt_proxy(ip, 8097)
|
||||
self.logger.info("Connected to React DevTools, loading script")
|
||||
tab = await get_gamepadui_tab()
|
||||
# RDT needs to load before React itself to work.
|
||||
await close_old_tabs()
|
||||
result = await tab.reload_and_evaluate(script)
|
||||
self.logger.info(result)
|
||||
|
||||
except Exception:
|
||||
self.logger.error("Failed to connect to React DevTools")
|
||||
self.logger.error(format_exc())
|
||||
|
||||
async def enable_rdt(self):
|
||||
self.context.loop.create_task(self._enable_rdt())
|
||||
|
||||
async def disable_rdt(self):
|
||||
self.logger.info("Disabling React DevTools")
|
||||
tab = await get_gamepadui_tab()
|
||||
self.rdt_script_id = None
|
||||
await close_old_tabs()
|
||||
await tab.evaluate_js("location.reload();", False, True, False)
|
||||
self.logger.info("React DevTools disabled")
|
||||
|
||||
async def get_user_info(self) -> dict:
|
||||
return {
|
||||
"username": get_username(),
|
||||
"path": get_home_path()
|
||||
}
|
||||
|
||||
async def get_tab_id(self, name):
|
||||
return (await get_tab(name)).id
|
||||
|
||||
Vendored
-50
@@ -1,50 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
|
||||
|
||||
echo "Installing Steam Deck Plugin Loader nightly..."
|
||||
|
||||
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
|
||||
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
|
||||
|
||||
# Create folder structure
|
||||
rm -rf ${HOMEBREW_FOLDER}/services
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||
|
||||
# Download latest nightly build and install it
|
||||
rm -rf /tmp/plugin_loader
|
||||
mkdir -p /tmp/plugin_loader
|
||||
curl -L https://nightly.link/SteamDeckHomebrew/PluginLoader/workflows/build/main/Plugin%20Loader.zip --output /tmp/plugin_loader/PluginLoader.zip
|
||||
unzip /tmp/plugin_loader/PluginLoader.zip -d /tmp/plugin_loader
|
||||
cp /tmp/plugin_loader/PluginLoader ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
rm -rf /tmp/plugin_loader
|
||||
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
|
||||
systemctl --user stop plugin_loader 2> /dev/null
|
||||
systemctl --user disable plugin_loader 2> /dev/null
|
||||
rm -f ${USER_DIR}/.config/systemd/user/plugin_loader.service
|
||||
|
||||
systemctl stop plugin_loader 2> /dev/null
|
||||
systemctl disable plugin_loader 2> /dev/null
|
||||
rm -f /etc/systemd/system/plugin_loader.service
|
||||
|
||||
cat > /etc/systemd/system/plugin_loader.service <<- EOM
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOM
|
||||
systemctl daemon-reload
|
||||
systemctl start plugin_loader
|
||||
systemctl enable plugin_loader
|
||||
Vendored
+28
-5
@@ -7,14 +7,16 @@ echo "Installing Steam Deck Plugin Loader pre-release..."
|
||||
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
|
||||
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
|
||||
|
||||
# # Create folder structure
|
||||
rm -rf ${HOMEBREW_FOLDER}/services
|
||||
# Create folder structure
|
||||
rm -rf "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||
touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging"
|
||||
|
||||
# Download latest release and install it
|
||||
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))")
|
||||
read VERSION DOWNLOADURL < <(echo $(jq -r '.tag_name, .assets[].browser_download_url' <<< ${RELEASE}))
|
||||
VERSION=$(jq -r '.tag_name' <<< ${RELEASE} )
|
||||
DOWNLOADURL=$(jq -r '.assets[].browser_download_url | select(endswith("PluginLoader"))' <<< ${RELEASE})
|
||||
|
||||
printf "Installing version %s...\n" "${VERSION}"
|
||||
curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
@@ -26,21 +28,42 @@ systemctl --user disable plugin_loader 2> /dev/null
|
||||
|
||||
systemctl stop plugin_loader 2> /dev/null
|
||||
systemctl disable plugin_loader 2> /dev/null
|
||||
rm -f /etc/systemd/system/plugin_loader.service
|
||||
cat > /etc/systemd/system/plugin_loader.service <<- EOM
|
||||
|
||||
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service
|
||||
|
||||
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOM
|
||||
|
||||
if [[ -f "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service" ]]; then
|
||||
printf "Grabbed latest prerelease service.\n"
|
||||
sed -i -e "s|\${HOMEBREW_FOLDER}|${HOMEBREW_FOLDER}|" "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service"
|
||||
cp -f "${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service" "/etc/systemd/system/plugin_loader.service"
|
||||
else
|
||||
printf "Could not curl latest prerelease systemd service, using built-in service as a backup!\n"
|
||||
rm -f "/etc/systemd/system/plugin_loader.service"
|
||||
cp "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" "/etc/systemd/system/plugin_loader.service"
|
||||
fi
|
||||
|
||||
mkdir -p ${HOMEBREW_FOLDER}/services/.systemd
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-prerelease.service
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-backup.service
|
||||
rm ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl start plugin_loader
|
||||
systemctl enable plugin_loader
|
||||
|
||||
Vendored
+27
-3
@@ -11,10 +11,12 @@ HOMEBREW_FOLDER="${USER_DIR}/homebrew"
|
||||
rm -rf "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||
touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging"
|
||||
|
||||
# Download latest release and install it
|
||||
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "false"))")
|
||||
read VERSION DOWNLOADURL < <(echo $(jq -r '.tag_name, .assets[].browser_download_url' <<< ${RELEASE}))
|
||||
VERSION=$(jq -r '.tag_name' <<< ${RELEASE} )
|
||||
DOWNLOADURL=$(jq -r '.assets[].browser_download_url | select(endswith("PluginLoader"))' <<< ${RELEASE})
|
||||
|
||||
printf "Installing version %s...\n" "${VERSION}"
|
||||
curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
@@ -26,20 +28,42 @@ systemctl --user disable plugin_loader 2> /dev/null
|
||||
|
||||
systemctl stop plugin_loader 2> /dev/null
|
||||
systemctl disable plugin_loader 2> /dev/null
|
||||
rm -f "/etc/systemd/system/plugin_loader.service"
|
||||
cat > "/etc/systemd/system/plugin_loader.service" <<- EOM
|
||||
|
||||
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-release.service
|
||||
|
||||
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOM
|
||||
|
||||
if [[ -f "${HOMEBREW_FOLDER}/services/plugin_loader-release.service" ]]; then
|
||||
printf "Grabbed latest release service.\n"
|
||||
sed -i -e "s|\${HOMEBREW_FOLDER}|${HOMEBREW_FOLDER}|" "${HOMEBREW_FOLDER}/services/plugin_loader-release.service"
|
||||
cp -f "${HOMEBREW_FOLDER}/services/plugin_loader-release.service" "/etc/systemd/system/plugin_loader.service"
|
||||
else
|
||||
printf "Could not curl latest release systemd service, using built-in service as a backup!\n"
|
||||
rm -f "/etc/systemd/system/plugin_loader.service"
|
||||
cp "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" "/etc/systemd/system/plugin_loader.service"
|
||||
fi
|
||||
|
||||
mkdir -p ${HOMEBREW_FOLDER}/services/.systemd
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-release.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-release.service
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-backup.service
|
||||
rm ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/plugin_loader-release.service
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl start plugin_loader
|
||||
systemctl enable plugin_loader
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,162 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="81.700577mm"
|
||||
height="24.334814mm"
|
||||
viewBox="0 0 81.700577 24.334814"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="download.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="3.659624"
|
||||
inkscape:cx="115.44902"
|
||||
inkscape:cy="59.295709"
|
||||
inkscape:window-width="1827"
|
||||
inkscape:window-height="1233"
|
||||
inkscape:window-x="69"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient4494">
|
||||
<stop
|
||||
style="stop-color:#009fff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4490" />
|
||||
<stop
|
||||
style="stop-color:#ff1965;stop-opacity:1;"
|
||||
offset="0.79417855"
|
||||
id="stop4498" />
|
||||
<stop
|
||||
style="stop-color:#b9b500;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop4492" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4494"
|
||||
id="linearGradient4496"
|
||||
x1="49.131042"
|
||||
y1="118.6573"
|
||||
x2="150.29259"
|
||||
y2="138.74957"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
spreadMethod="pad"
|
||||
gradientTransform="matrix(1.0500324,0,0,1,-1.6155884,24.621921)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient4494"
|
||||
id="linearGradient13802"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(1.0500324,0,0,1,-1.6155884,24.621921)"
|
||||
x1="49.131042"
|
||||
y1="118.6573"
|
||||
x2="150.29259"
|
||||
y2="138.74957"
|
||||
spreadMethod="pad" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-64.149712,-136.3326)">
|
||||
<rect
|
||||
style="mix-blend-mode:normal;fill:url(#linearGradient13802);fill-opacity:1;stroke:none;stroke-width:0.271121"
|
||||
id="rect111"
|
||||
width="81.700577"
|
||||
height="24.334814"
|
||||
x="64.149712"
|
||||
y="136.3326"
|
||||
ry="8.1781616" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583"
|
||||
x="66.364288"
|
||||
y="124.84658"
|
||||
id="text10382"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan10380"
|
||||
style="stroke-width:0.264583"
|
||||
x="66.364288"
|
||||
y="124.84658" /></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15.1694px;font-family:sans-serif;-inkscape-font-specification:sans-serif;white-space:pre;inline-size:82.6483;display:inline;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264583"
|
||||
x="67.732498"
|
||||
y="126.05277"
|
||||
id="text10440"
|
||||
transform="translate(1.088576,28.135753)"><tspan
|
||||
x="67.732498"
|
||||
y="126.05277"
|
||||
id="tspan13872">Download</tspan></text>
|
||||
<rect
|
||||
style="mix-blend-mode:normal;fill:url(#linearGradient4496);fill-opacity:1;stroke:none;stroke-width:0.271121"
|
||||
id="rect13792"
|
||||
width="81.700577"
|
||||
height="24.334814"
|
||||
x="64.149712"
|
||||
y="136.3326"
|
||||
ry="8.1781616" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583"
|
||||
x="66.364288"
|
||||
y="124.84658"
|
||||
id="text13796"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan13794"
|
||||
style="stroke-width:0.264583"
|
||||
x="66.364288"
|
||||
y="124.84658" /></text>
|
||||
<g
|
||||
aria-label="Download"
|
||||
transform="translate(1.088576,28.135753)"
|
||||
id="text13800"
|
||||
style="font-size:15.1694px;-inkscape-font-specification:sans-serif;white-space:pre;inline-size:82.6483;display:inline;fill:#ffffff;stroke-width:0.264583">
|
||||
<path
|
||||
d="m 77.880751,120.53111 q 0,2.74566 -1.501771,4.14125 -1.486601,1.38041 -4.156416,1.38041 h -3.01871 v -10.83095 h 3.337268 q 1.638295,0 2.836678,0.60678 1.198382,0.60677 1.850666,1.78999 0.652285,1.16804 0.652285,2.91252 z m -1.441093,0.0455 q 0,-2.16923 -1.077028,-3.17041 -1.061858,-1.01635 -3.01871,-1.01635 H 70.5691 v 8.49487 h 1.471432 q 4.399126,0 4.399126,-4.30811 z"
|
||||
id="path13828" />
|
||||
<path
|
||||
d="m 87.164417,121.9722 q 0,2.01753 -1.03152,3.1249 -1.01635,1.10737 -2.760831,1.10737 -1.077027,0 -1.926513,-0.48542 -0.834317,-0.50059 -1.319738,-1.4411 -0.485421,-0.95567 -0.485421,-2.30575 0,-2.01753 1.01635,-3.10972 1.01635,-1.0922 2.760831,-1.0922 1.107366,0 1.941683,0.50059 0.849486,0.48542 1.319738,1.42592 0.485421,0.92534 0.485421,2.27541 z m -6.143608,0 q 0,1.4411 0.561268,2.29058 0.576437,0.83432 1.820328,0.83432 1.228722,0 1.805159,-0.83432 0.576437,-0.84948 0.576437,-2.29058 0,-1.44109 -0.576437,-2.26024 -0.576437,-0.81914 -1.820328,-0.81914 -1.243891,0 -1.805159,0.81914 -0.561268,0.81915 -0.561268,2.26024 z"
|
||||
id="path13830" />
|
||||
<path
|
||||
d="m 94.218174,121.45644 q -0.197202,-0.62194 -0.348896,-1.21355 -0.136525,-0.60677 -0.212372,-0.9405 h -0.06068 q -0.06068,0.33373 -0.197203,0.9405 -0.136524,0.59161 -0.348896,1.22872 l -1.456262,4.56599 h -1.51694 l -2.229902,-8.1308 h 1.380415 l 1.122536,4.33845 q 0.166863,0.65229 0.318557,1.31974 0.151694,0.66745 0.212372,1.10737 h 0.06068 q 0.06068,-0.25788 0.136525,-0.63712 0.09102,-0.37923 0.197202,-0.78881 0.106186,-0.42474 0.212372,-0.75847 l 1.441093,-4.58116 h 1.456262 l 1.395585,4.58116 q 0.166864,0.51576 0.318558,1.12254 0.166863,0.60678 0.227541,1.04669 h 0.06068 q 0.04551,-0.37924 0.197202,-1.04669 0.166864,-0.66745 0.348897,-1.36525 l 1.137705,-4.33845 h 1.365246 l -2.260241,8.1308 h -1.562448 z"
|
||||
id="path13832" />
|
||||
<path
|
||||
d="m 104.8064,117.77028 q 1.45627,0 2.19957,0.71296 0.7433,0.69779 0.7433,2.27541 v 5.29412 h -1.31974 v -5.2031 q 0,-1.95685 -1.82033,-1.95685 -1.35007,0 -1.86583,0.75847 -0.51576,0.75847 -0.51576,2.18439 v 4.21709 h -1.33491 v -8.1308 h 1.07703 l 0.1972,1.10737 h 0.0759 q 0.3944,-0.63711 1.09219,-0.9405 0.69779,-0.31856 1.47143,-0.31856 z"
|
||||
id="path13834" />
|
||||
<path
|
||||
d="m 111.6023,126.05277 h -1.33491 v -11.52874 h 1.33491 z"
|
||||
id="path13836" />
|
||||
<path
|
||||
d="m 121.25003,121.9722 q 0,2.01753 -1.03152,3.1249 -1.01635,1.10737 -2.76084,1.10737 -1.07702,0 -1.92651,-0.48542 -0.83432,-0.50059 -1.31974,-1.4411 -0.48542,-0.95567 -0.48542,-2.30575 0,-2.01753 1.01635,-3.10972 1.01635,-1.0922 2.76083,-1.0922 1.10737,0 1.94169,0.50059 0.84948,0.48542 1.31973,1.42592 0.48543,0.92534 0.48543,2.27541 z m -6.14361,0 q 0,1.4411 0.56127,2.29058 0.57643,0.83432 1.82032,0.83432 1.22873,0 1.80516,-0.83432 0.57644,-0.84948 0.57644,-2.29058 0,-1.44109 -0.57644,-2.26024 -0.57643,-0.81914 -1.82033,-0.81914 -1.24389,0 -1.80515,0.81914 -0.56127,0.81915 -0.56127,2.26024 z"
|
||||
id="path13838" />
|
||||
<path
|
||||
d="m 126.43796,117.78545 q 1.4866,0 2.19956,0.65228 0.71296,0.65229 0.71296,2.07821 v 5.53683 h -0.97084 l -0.25788,-1.15287 h -0.0607 q -0.53093,0.66745 -1.12253,0.98601 -0.57644,0.31856 -1.60796,0.31856 -1.10737,0 -1.8355,-0.57644 -0.72813,-0.59161 -0.72813,-1.8355 0,-1.21355 0.95567,-1.86583 0.95567,-0.66746 2.94287,-0.72814 l 1.38041,-0.0455 v -0.48542 q 0,-1.01635 -0.43991,-1.41076 -0.43991,-0.3944 -1.24389,-0.3944 -0.63712,0 -1.21355,0.1972 -0.57644,0.18203 -1.07703,0.42474 l -0.40957,-1.00118 q 0.53092,-0.28822 1.25906,-0.48542 0.72813,-0.21237 1.51694,-0.21237 z m 0.3944,4.33845 q -1.51694,0.0607 -2.10855,0.48542 -0.57643,0.42474 -0.57643,1.19838 0,0.68262 0.40957,1.00118 0.42474,0.31856 1.07703,0.31856 1.03152,0 1.71414,-0.56127 0.68262,-0.57644 0.68262,-1.75965 v -0.72813 z"
|
||||
id="path13840" />
|
||||
<path
|
||||
d="m 134.7508,126.20447 q -1.51694,0 -2.42711,-1.04669 -0.91016,-1.06186 -0.91016,-3.15524 0,-2.09337 0.91016,-3.15523 0.92534,-1.07703 2.44228,-1.07703 0.9405,0 1.53211,0.3489 0.60677,0.34889 0.98601,0.84948 h 0.091 q -0.0152,-0.1972 -0.0607,-0.57643 -0.0303,-0.39441 -0.0303,-0.62195 v -3.24625 h 1.3349 v 11.52874 h -1.07702 l -0.19721,-1.09219 h -0.0607 q -0.36407,0.51576 -0.97084,0.87982 -0.60678,0.36407 -1.56245,0.36407 z m 0.21237,-1.10737 q 1.2894,0 1.80516,-0.69779 0.53093,-0.71296 0.53093,-2.13889 v -0.24271 q 0,-1.51694 -0.50059,-2.32092 -0.50059,-0.81914 -1.85067,-0.81914 -1.07703,0 -1.62313,0.86465 -0.53093,0.84949 -0.53093,2.29058 0,1.45626 0.53093,2.26024 0.5461,0.80398 1.6383,0.80398 z"
|
||||
id="path13842" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.9 KiB |
@@ -2,3 +2,5 @@ node_modules/
|
||||
|
||||
.yalc
|
||||
yalc.lock
|
||||
|
||||
stats.html
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
@@ -0,0 +1,100 @@
|
||||
export default {
|
||||
contextSeparator: '_',
|
||||
// Key separator used in your translation keys
|
||||
|
||||
createOldCatalogs: false,
|
||||
// Save the \_old files
|
||||
|
||||
defaultNamespace: 'translation',
|
||||
// Default namespace used in your i18next config
|
||||
|
||||
defaultValue: '',
|
||||
// Default value to give to keys with no value
|
||||
// You may also specify a function accepting the locale, namespace, key, and value as arguments
|
||||
|
||||
indentation: 2,
|
||||
// Indentation of the catalog files
|
||||
|
||||
keepRemoved: true,
|
||||
// Keep keys from the catalog that are no longer in code
|
||||
|
||||
keySeparator: '.',
|
||||
// Key separator used in your translation keys
|
||||
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
|
||||
|
||||
// see below for more details
|
||||
lexers: {
|
||||
mjs: ['JavascriptLexer'],
|
||||
js: ['JavascriptLexer'], // if you're writing jsx inside .js files, change this to JsxLexer
|
||||
ts: ['JavascriptLexer'],
|
||||
jsx: ['JsxLexer'],
|
||||
tsx: ['JsxLexer'],
|
||||
|
||||
default: ['JavascriptLexer'],
|
||||
},
|
||||
|
||||
lineEnding: 'auto',
|
||||
// Control the line ending. See options at https://github.com/ryanve/eol
|
||||
|
||||
locales: ['en-US'],
|
||||
// An array of the locales in your applications
|
||||
|
||||
namespaceSeparator: false,
|
||||
// Namespace separator used in your translation keys
|
||||
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
|
||||
|
||||
output: '../backend/locales/$LOCALE.json',
|
||||
// Supports $LOCALE and $NAMESPACE injection
|
||||
// Supports JSON (.json) and YAML (.yml) file formats
|
||||
// Where to write the locale files relative to process.cwd()
|
||||
|
||||
pluralSeparator: '_',
|
||||
// Plural separator used in your translation keys
|
||||
// If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys.
|
||||
|
||||
input: './src/**/*.{ts,tsx}',
|
||||
// An array of globs that describe where to look for source files
|
||||
// relative to the location of the configuration file
|
||||
|
||||
sort: true,
|
||||
// Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters)
|
||||
|
||||
verbose: false,
|
||||
// Display info about the parsing including some stats
|
||||
|
||||
failOnWarnings: false,
|
||||
// Exit with an exit code of 1 on warnings
|
||||
|
||||
failOnUpdate: false,
|
||||
// Exit with an exit code of 1 when translations are updated (for CI purpose)
|
||||
|
||||
customValueTemplate: null,
|
||||
// If you wish to customize the value output the value as an object, you can set your own format.
|
||||
// ${defaultValue} is the default value you set in your translation function.
|
||||
// Any other custom property will be automatically extracted.
|
||||
//
|
||||
// Example:
|
||||
// {
|
||||
// message: "${defaultValue}",
|
||||
// description: "${maxLength}", // t('my-key', {maxLength: 150})
|
||||
// }
|
||||
|
||||
resetDefaultValueLocale: null,
|
||||
// The locale to compare with default values to determine whether a default value has been changed.
|
||||
// If this is set and a default value differs from a translation in the specified locale, all entries
|
||||
// for that key across locales are reset to the default value, and existing translations are moved to
|
||||
// the `_old` file.
|
||||
|
||||
i18nextOptions: null,
|
||||
// If you wish to customize options in internally used i18next instance, you can define an object with any
|
||||
// configuration property supported by i18next (https://www.i18next.com/overview/configuration-options).
|
||||
// { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals.
|
||||
|
||||
yamlOptions: null,
|
||||
// If you wish to customize options for yaml output, you can define an object here.
|
||||
// Configuration options are here (https://github.com/nodeca/js-yaml#dump-object---options-).
|
||||
// Example:
|
||||
// {
|
||||
// lineWidth: -1,
|
||||
// }
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
declare module '*.png';
|
||||
declare module '*.jpg';
|
||||
+19
-12
@@ -12,27 +12,30 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^21.1.0",
|
||||
"@rollup/plugin-image": "^3.0.2",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@rollup/plugin-typescript": "^8.3.3",
|
||||
"@rollup/plugin-typescript": "^8.5.0",
|
||||
"@types/react": "16.14.0",
|
||||
"@types/react-file-icon": "^1.0.1",
|
||||
"@types/react-router": "5.1.18",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"husky": "^8.0.1",
|
||||
"@types/webpack": "^5.28.1",
|
||||
"husky": "^8.0.3",
|
||||
"i18next-parser": "^8.0.0",
|
||||
"import-sort-style-module": "^6.0.0",
|
||||
"inquirer": "^8.2.4",
|
||||
"prettier": "^2.7.1",
|
||||
"inquirer": "^8.2.5",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-import-sort": "^0.0.7",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"rollup": "^2.76.0",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-external-globals": "^0.6.1",
|
||||
"rollup-plugin-polyfill-node": "^0.10.2",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.7.4"
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"importSort": {
|
||||
".js, .jsx, .ts, .tsx": {
|
||||
@@ -41,10 +44,14 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "^3.6.0",
|
||||
"react-file-icon": "^1.2.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"decky-frontend-lib": "3.21.1",
|
||||
"filesize": "^10.0.7",
|
||||
"i18next": "^23.2.1",
|
||||
"i18next-http-backend": "^2.2.1",
|
||||
"react-file-icon": "^1.3.0",
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-icons": "^4.9.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"remark-gfm": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2340
-961
File diff suppressed because it is too large
Load Diff
+23
-11
@@ -1,25 +1,31 @@
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import image from '@rollup/plugin-image';
|
||||
import json from '@rollup/plugin-json';
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import externalGlobals from "rollup-plugin-external-globals";
|
||||
import del from 'rollup-plugin-delete'
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import { defineConfig } from 'rollup';
|
||||
import del from 'rollup-plugin-delete';
|
||||
import externalGlobals from 'rollup-plugin-external-globals';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL'];
|
||||
|
||||
export default defineConfig({
|
||||
input: 'src/index.tsx',
|
||||
input: 'src/index.ts',
|
||||
plugins: [
|
||||
del({ targets: "../backend/static/*", force: true }),
|
||||
del({ targets: '../backend/static/*', force: true }),
|
||||
commonjs(),
|
||||
nodeResolve(),
|
||||
nodeResolve({
|
||||
browser: true,
|
||||
}),
|
||||
externalGlobals({
|
||||
react: 'SP_REACT',
|
||||
'react-dom': 'SP_REACTDOM',
|
||||
// hack to shut up react-markdown
|
||||
'process': '{cwd: () => {}}',
|
||||
'path': '{dirname: () => {}, join: () => {}, basename: () => {}, extname: () => {}}',
|
||||
'url': '{fileURLToPath: (f) => f}'
|
||||
process: '{cwd: () => {}}',
|
||||
path: '{dirname: () => {}, join: () => {}, basename: () => {}, extname: () => {}}',
|
||||
url: '{fileURLToPath: (f) => f}',
|
||||
}),
|
||||
typescript(),
|
||||
json(),
|
||||
@@ -27,13 +33,19 @@ export default defineConfig({
|
||||
preventAssignment: false,
|
||||
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||
}),
|
||||
image(),
|
||||
visualizer(),
|
||||
],
|
||||
preserveEntrySignatures: false,
|
||||
output: {
|
||||
dir: '../backend/static',
|
||||
format: 'esm',
|
||||
chunkFileNames: (chunkInfo) => {
|
||||
return 'chunk-[hash].js'
|
||||
}
|
||||
}
|
||||
return 'chunk-[hash].js';
|
||||
},
|
||||
},
|
||||
onwarn: function (message, handleWarning) {
|
||||
if (hiddenWarnings.some((warning) => message.code === warning)) return;
|
||||
handleWarning(message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
interface PublicDeckyGlobalComponentsState {
|
||||
components: Map<string, FC>;
|
||||
}
|
||||
|
||||
export class DeckyGlobalComponentsState {
|
||||
// TODO a set would be better
|
||||
private _components = new Map<string, FC>();
|
||||
|
||||
public eventBus = new EventTarget();
|
||||
|
||||
publicState(): PublicDeckyGlobalComponentsState {
|
||||
return { components: this._components };
|
||||
}
|
||||
|
||||
addComponent(path: string, component: FC) {
|
||||
this._components.set(path, component);
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
removeComponent(path: string) {
|
||||
this._components.delete(path);
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
private notifyUpdate() {
|
||||
this.eventBus.dispatchEvent(new Event('update'));
|
||||
}
|
||||
}
|
||||
|
||||
interface DeckyGlobalComponentsContext extends PublicDeckyGlobalComponentsState {
|
||||
addComponent(path: string, component: FC): void;
|
||||
removeComponent(path: string): void;
|
||||
}
|
||||
|
||||
const DeckyGlobalComponentsContext = createContext<DeckyGlobalComponentsContext>(null as any);
|
||||
|
||||
export const useDeckyGlobalComponentsState = () => useContext(DeckyGlobalComponentsContext);
|
||||
|
||||
interface Props {
|
||||
deckyGlobalComponentsState: DeckyGlobalComponentsState;
|
||||
}
|
||||
|
||||
export const DeckyGlobalComponentsStateContextProvider: FC<Props> = ({
|
||||
children,
|
||||
deckyGlobalComponentsState: deckyGlobalComponentsState,
|
||||
}) => {
|
||||
const [publicDeckyGlobalComponentsState, setPublicDeckyGlobalComponentsState] =
|
||||
useState<PublicDeckyGlobalComponentsState>({
|
||||
...deckyGlobalComponentsState.publicState(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function onUpdate() {
|
||||
setPublicDeckyGlobalComponentsState({ ...deckyGlobalComponentsState.publicState() });
|
||||
}
|
||||
|
||||
deckyGlobalComponentsState.eventBus.addEventListener('update', onUpdate);
|
||||
|
||||
return () => deckyGlobalComponentsState.eventBus.removeEventListener('update', onUpdate);
|
||||
}, []);
|
||||
|
||||
const addComponent = deckyGlobalComponentsState.addComponent.bind(deckyGlobalComponentsState);
|
||||
const removeComponent = deckyGlobalComponentsState.removeComponent.bind(deckyGlobalComponentsState);
|
||||
|
||||
return (
|
||||
<DeckyGlobalComponentsContext.Provider
|
||||
value={{ ...publicDeckyGlobalComponentsState, addComponent, removeComponent }}
|
||||
>
|
||||
{children}
|
||||
</DeckyGlobalComponentsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
export default function DeckyIcon() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 456" width="512" height="456">
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: 'none' }}
|
||||
d="M154.33,72.51v49.79c11.78-0.17,23.48,2,34.42,6.39c10.93,4.39,20.89,10.91,29.28,19.18
|
||||
c8.39,8.27,15.06,18.13,19.61,29c4.55,10.87,6.89,22.54,6.89,34.32c0,11.78-2.34,23.45-6.89,34.32
|
||||
c-4.55,10.87-11.21,20.73-19.61,29c-8.39,8.27-18.35,14.79-29.28,19.18c-10.94,4.39-22.63,6.56-34.42,6.39v49.77
|
||||
c36.78,0,72.05-14.61,98.05-40.62c26-26.01,40.61-61.28,40.61-98.05c0-36.78-14.61-72.05-40.61-98.05
|
||||
C226.38,87.12,191.11,72.51,154.33,72.51z"
|
||||
/>
|
||||
|
||||
<ellipse
|
||||
transform="matrix(0.982 -0.1891 0.1891 0.982 -37.1795 32.9988)"
|
||||
style={{ fill: 'none' }}
|
||||
cx="154.33"
|
||||
cy="211.33"
|
||||
rx="69.33"
|
||||
ry="69.33"
|
||||
/>
|
||||
<path style={{ fill: 'none' }} d="M430,97h-52v187h52c7.18,0,13-5.82,13-13V110C443,102.82,437.18,97,430,97z" />
|
||||
<path
|
||||
style={{ fill: 'currentColor' }}
|
||||
d="M432,27h-54V0H0v361c0,52.47,42.53,95,95,95h188c52.47,0,95-42.53,95-95v-7h54c44.18,0,80-35.82,80-80V107
|
||||
C512,62.82,476.18,27,432,27z M85,211.33c0-38.29,31.04-69.33,69.33-69.33c38.29,0,69.33,31.04,69.33,69.33
|
||||
c0,38.29-31.04,69.33-69.33,69.33C116.04,280.67,85,249.62,85,211.33z M252.39,309.23c-26.01,26-61.28,40.62-98.05,40.62v-49.77
|
||||
c11.78,0.17,23.48-2,34.42-6.39c10.93-4.39,20.89-10.91,29.28-19.18c8.39-8.27,15.06-18.13,19.61-29
|
||||
c4.55-10.87,6.89-22.53,6.89-34.32c0-11.78-2.34-23.45-6.89-34.32c-4.55-10.87-11.21-20.73-19.61-29
|
||||
c-8.39-8.27-18.35-14.79-29.28-19.18c-10.94-4.39-22.63-6.56-34.42-6.39V72.51c36.78,0,72.05,14.61,98.05,40.61
|
||||
c26,26.01,40.61,61.28,40.61,98.05C293,247.96,278.39,283.23,252.39,309.23z M443,271c0,7.18-5.82,13-13,13h-52V97h52
|
||||
c7.18,0,13,5.82,13,13V271z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +1,54 @@
|
||||
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
|
||||
import { Plugin } from '../plugin';
|
||||
import { PluginUpdateMapping } from '../store';
|
||||
import { VerInfo } from '../updater';
|
||||
|
||||
interface PublicDeckyState {
|
||||
plugins: Plugin[];
|
||||
pluginOrder: string[];
|
||||
hiddenPlugins: string[];
|
||||
activePlugin: Plugin | null;
|
||||
updates: PluginUpdateMapping | null;
|
||||
hasLoaderUpdate?: boolean;
|
||||
isLoaderUpdating: boolean;
|
||||
versionInfo: VerInfo | null;
|
||||
notificationSettings: NotificationSettings;
|
||||
userInfo: UserInfo | null;
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
username: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export class DeckyState {
|
||||
private _plugins: Plugin[] = [];
|
||||
private _pluginOrder: string[] = [];
|
||||
private _hiddenPlugins: string[] = [];
|
||||
private _activePlugin: Plugin | null = null;
|
||||
private _updates: PluginUpdateMapping | null = null;
|
||||
private _hasLoaderUpdate: boolean = false;
|
||||
private _isLoaderUpdating: boolean = false;
|
||||
private _versionInfo: VerInfo | null = null;
|
||||
private _notificationSettings = DEFAULT_NOTIFICATION_SETTINGS;
|
||||
private _userInfo: UserInfo | null = null;
|
||||
|
||||
public eventBus = new EventTarget();
|
||||
|
||||
publicState(): PublicDeckyState {
|
||||
return {
|
||||
plugins: this._plugins,
|
||||
pluginOrder: this._pluginOrder,
|
||||
hiddenPlugins: this._hiddenPlugins,
|
||||
activePlugin: this._activePlugin,
|
||||
updates: this._updates,
|
||||
hasLoaderUpdate: this._hasLoaderUpdate,
|
||||
isLoaderUpdating: this._isLoaderUpdating,
|
||||
versionInfo: this._versionInfo,
|
||||
notificationSettings: this._notificationSettings,
|
||||
userInfo: this._userInfo,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,6 +62,16 @@ export class DeckyState {
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setPluginOrder(pluginOrder: string[]) {
|
||||
this._pluginOrder = pluginOrder;
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setHiddenPlugins(hiddenPlugins: string[]) {
|
||||
this._hiddenPlugins = hiddenPlugins;
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setActivePlugin(name: string) {
|
||||
this._activePlugin = this._plugins.find((plugin) => plugin.name === name) ?? null;
|
||||
this.notifyUpdate();
|
||||
@@ -69,6 +97,16 @@ export class DeckyState {
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setNotificationSettings(notificationSettings: NotificationSettings) {
|
||||
this._notificationSettings = notificationSettings;
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setUserInfo(userInfo: UserInfo) {
|
||||
this._userInfo = userInfo;
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
private notifyUpdate() {
|
||||
this.eventBus.dispatchEvent(new Event('update'));
|
||||
}
|
||||
@@ -78,6 +116,7 @@ interface DeckyStateContext extends PublicDeckyState {
|
||||
setVersionInfo(versionInfo: VerInfo): void;
|
||||
setIsLoaderUpdating(hasUpdate: boolean): void;
|
||||
setActivePlugin(name: string): void;
|
||||
setPluginOrder(pluginOrder: string[]): void;
|
||||
closeActivePlugin(): void;
|
||||
}
|
||||
|
||||
@@ -102,14 +141,22 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
|
||||
return () => deckyState.eventBus.removeEventListener('update', onUpdate);
|
||||
}, []);
|
||||
|
||||
const setIsLoaderUpdating = (hasUpdate: boolean) => deckyState.setIsLoaderUpdating(hasUpdate);
|
||||
const setVersionInfo = (versionInfo: VerInfo) => deckyState.setVersionInfo(versionInfo);
|
||||
const setActivePlugin = (name: string) => deckyState.setActivePlugin(name);
|
||||
const closeActivePlugin = () => deckyState.closeActivePlugin();
|
||||
const setIsLoaderUpdating = deckyState.setIsLoaderUpdating.bind(deckyState);
|
||||
const setVersionInfo = deckyState.setVersionInfo.bind(deckyState);
|
||||
const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
|
||||
const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
|
||||
const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
|
||||
|
||||
return (
|
||||
<DeckyStateContext.Provider
|
||||
value={{ ...publicDeckyState, setIsLoaderUpdating, setVersionInfo, setActivePlugin, closeActivePlugin }}
|
||||
value={{
|
||||
...publicDeckyState,
|
||||
setIsLoaderUpdating,
|
||||
setVersionInfo,
|
||||
setActivePlugin,
|
||||
closeActivePlugin,
|
||||
setPluginOrder,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DeckyStateContext.Provider>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ToastData, joinClassNames } from 'decky-frontend-lib';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { ReactElement } from 'react-markdown/lib/react-markdown';
|
||||
|
||||
import { useDeckyToasterState } from './DeckyToasterState';
|
||||
import Toast, { toastClasses } from './Toast';
|
||||
|
||||
interface DeckyToasterProps {}
|
||||
|
||||
interface RenderedToast {
|
||||
component: ReactElement;
|
||||
data: ToastData;
|
||||
}
|
||||
|
||||
const DeckyToaster: FC<DeckyToasterProps> = () => {
|
||||
const { toasts, removeToast } = useDeckyToasterState();
|
||||
const [renderedToast, setRenderedToast] = useState<RenderedToast | null>(null);
|
||||
console.log(toasts);
|
||||
if (toasts.size > 0) {
|
||||
const [activeToast] = toasts;
|
||||
if (!renderedToast || activeToast != renderedToast.data) {
|
||||
// TODO play toast sound
|
||||
console.log('rendering toast', activeToast);
|
||||
setRenderedToast({ component: <Toast key={Math.random()} toast={activeToast} />, data: activeToast });
|
||||
}
|
||||
} else {
|
||||
if (renderedToast) setRenderedToast(null);
|
||||
}
|
||||
useEffect(() => {
|
||||
// not actually node but TS is shit
|
||||
let interval: NodeJS.Timer | null;
|
||||
if (renderedToast) {
|
||||
interval = setTimeout(() => {
|
||||
interval = null;
|
||||
console.log('clear toast', renderedToast.data);
|
||||
removeToast(renderedToast.data);
|
||||
}, (renderedToast.data.duration || 5e3) + 1000);
|
||||
console.log('set int', interval);
|
||||
}
|
||||
return () => {
|
||||
if (interval) {
|
||||
console.log('clearing int', interval);
|
||||
clearTimeout(interval);
|
||||
}
|
||||
};
|
||||
}, [renderedToast]);
|
||||
return (
|
||||
<div className={joinClassNames('deckyToaster', toastClasses.ToastPlaceholder)}>
|
||||
{renderedToast && renderedToast.component}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeckyToaster;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ToastData } from 'decky-frontend-lib';
|
||||
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
interface PublicDeckyToasterState {
|
||||
toasts: Set<ToastData>;
|
||||
}
|
||||
|
||||
export class DeckyToasterState {
|
||||
// TODO a set would be better
|
||||
private _toasts: Set<ToastData> = new Set();
|
||||
|
||||
public eventBus = new EventTarget();
|
||||
|
||||
publicState(): PublicDeckyToasterState {
|
||||
return { toasts: this._toasts };
|
||||
}
|
||||
|
||||
addToast(toast: ToastData) {
|
||||
this._toasts.add(toast);
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
removeToast(toast: ToastData) {
|
||||
this._toasts.delete(toast);
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
private notifyUpdate() {
|
||||
this.eventBus.dispatchEvent(new Event('update'));
|
||||
}
|
||||
}
|
||||
|
||||
interface DeckyToasterContext extends PublicDeckyToasterState {
|
||||
addToast(toast: ToastData): void;
|
||||
removeToast(toast: ToastData): void;
|
||||
}
|
||||
|
||||
const DeckyToasterContext = createContext<DeckyToasterContext>(null as any);
|
||||
|
||||
export const useDeckyToasterState = () => useContext(DeckyToasterContext);
|
||||
|
||||
interface Props {
|
||||
deckyToasterState: DeckyToasterState;
|
||||
}
|
||||
|
||||
export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {
|
||||
const [publicDeckyToasterState, setPublicDeckyToasterState] = useState<PublicDeckyToasterState>({
|
||||
...deckyToasterState.publicState(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function onUpdate() {
|
||||
setPublicDeckyToasterState({ ...deckyToasterState.publicState() });
|
||||
}
|
||||
|
||||
deckyToasterState.eventBus.addEventListener('update', onUpdate);
|
||||
|
||||
return () => deckyToasterState.eventBus.removeEventListener('update', onUpdate);
|
||||
}, []);
|
||||
|
||||
const addToast = deckyToasterState.addToast.bind(deckyToasterState);
|
||||
const removeToast = deckyToasterState.removeToast.bind(deckyToasterState);
|
||||
|
||||
return (
|
||||
<DeckyToasterContext.Provider value={{ ...publicDeckyToasterState, addToast, removeToast }}>
|
||||
{children}
|
||||
</DeckyToasterContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Focusable } from 'decky-frontend-lib';
|
||||
import { Focusable, Navigation } from 'decky-frontend-lib';
|
||||
import { FunctionComponent, useRef } from 'react';
|
||||
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@@ -21,8 +21,8 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
|
||||
<Focusable
|
||||
onActivate={() => {}}
|
||||
onOKButton={() => {
|
||||
aRef?.current?.click();
|
||||
props.onDismiss?.();
|
||||
Navigation.NavigateToExternalWeb(aRef.current!.href);
|
||||
}}
|
||||
style={{ display: 'inline' }}
|
||||
>
|
||||
|
||||
@@ -7,16 +7,30 @@ import {
|
||||
scrollClasses,
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { VFC } from 'react';
|
||||
import { VFC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaEyeSlash } from 'react-icons/fa';
|
||||
|
||||
import { Plugin } from '../plugin';
|
||||
import { useDeckyState } from './DeckyState';
|
||||
import NotificationBadge from './NotificationBadge';
|
||||
import { useQuickAccessVisible } from './QuickAccessVisibleState';
|
||||
import TitleView from './TitleView';
|
||||
|
||||
const PluginView: VFC = () => {
|
||||
const { plugins, updates, activePlugin, setActivePlugin, closeActivePlugin } = useDeckyState();
|
||||
const { hiddenPlugins } = useDeckyState();
|
||||
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
|
||||
const visible = useQuickAccessVisible();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [pluginList, setPluginList] = useState<Plugin[]>(
|
||||
plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setPluginList(plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)));
|
||||
console.log('updating PluginView after changes');
|
||||
}, [plugins, pluginOrder]);
|
||||
|
||||
if (activePlugin) {
|
||||
return (
|
||||
@@ -36,8 +50,9 @@ const PluginView: VFC = () => {
|
||||
<TitleView />
|
||||
<div className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}>
|
||||
<PanelSection>
|
||||
{plugins
|
||||
{pluginList
|
||||
.filter((p) => p.content)
|
||||
.filter(({ name }) => !hiddenPlugins.includes(name))
|
||||
.map(({ name, icon }) => (
|
||||
<PanelSectionRow key={name}>
|
||||
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
|
||||
@@ -49,6 +64,12 @@ const PluginView: VFC = () => {
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
))}
|
||||
{hiddenPlugins.length > 0 && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}>
|
||||
<FaEyeSlash />
|
||||
<div>{t('PluginView.hidden', { count: hiddenPlugins.length })}</div>
|
||||
</div>
|
||||
)}
|
||||
</PanelSection>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { FC, createContext, useContext } from 'react';
|
||||
import { FC, createContext, useContext, useState } from 'react';
|
||||
|
||||
const QuickAccessVisibleState = createContext<boolean>(true);
|
||||
const QuickAccessVisibleState = createContext<boolean>(false);
|
||||
|
||||
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
}
|
||||
export const QuickAccessVisibleStateProvider: FC<{ tab: any }> = ({ children, tab }) => {
|
||||
const initial = tab.initialVisibility;
|
||||
const [visible, setVisible] = useState<boolean>(initial);
|
||||
// HACK but i can't think of a better way to do this
|
||||
tab.qAMVisibilitySetter = (val: boolean) => {
|
||||
if (val != visible) setVisible(val);
|
||||
};
|
||||
|
||||
export const QuickAccessVisibleStateProvider: FC<Props> = ({ children, visible }) => {
|
||||
return <QuickAccessVisibleState.Provider value={visible}>{children}</QuickAccessVisibleState.Provider>;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
|
||||
import { CSSProperties, VFC } from 'react';
|
||||
import { FaArrowLeft, FaCog, FaStore } from 'react-icons/fa';
|
||||
import { BsGearFill } from 'react-icons/bs';
|
||||
import { FaArrowLeft, FaStore } from 'react-icons/fa';
|
||||
|
||||
import { useDeckyState } from './DeckyState';
|
||||
|
||||
@@ -26,12 +27,6 @@ const TitleView: VFC = () => {
|
||||
if (activePlugin === null) {
|
||||
return (
|
||||
<Focusable style={titleStyles} className={staticClasses.Title}>
|
||||
<DialogButton
|
||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||
onClick={onSettingsClick}
|
||||
>
|
||||
<FaCog style={{ marginTop: '-4px', display: 'block' }} />
|
||||
</DialogButton>
|
||||
<div style={{ marginRight: 'auto', flex: 0.9 }}>Decky</div>
|
||||
<DialogButton
|
||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||
@@ -39,6 +34,12 @@ const TitleView: VFC = () => {
|
||||
>
|
||||
<FaStore style={{ marginTop: '-4px', display: 'block' }} />
|
||||
</DialogButton>
|
||||
<DialogButton
|
||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||
onClick={onSettingsClick}
|
||||
>
|
||||
<BsGearFill style={{ marginTop: '-4px', display: 'block' }} />
|
||||
</DialogButton>
|
||||
</Focusable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,10 @@ import { ToastData, findModule, joinClassNames } from 'decky-frontend-lib';
|
||||
import { FunctionComponent } from 'react';
|
||||
|
||||
interface ToastProps {
|
||||
toast: {
|
||||
data: ToastData;
|
||||
nToastDurationMS: number;
|
||||
};
|
||||
toast: ToastData;
|
||||
}
|
||||
|
||||
const toastClasses = findModule((mod) => {
|
||||
export const toastClasses = findModule((mod) => {
|
||||
if (typeof mod !== 'object') return false;
|
||||
|
||||
if (mod.ToastPlaceholder) {
|
||||
@@ -31,21 +28,17 @@ const templateClasses = findModule((mod) => {
|
||||
const Toast: FunctionComponent<ToastProps> = ({ toast }) => {
|
||||
return (
|
||||
<div
|
||||
style={{ '--toast-duration': `${toast.nToastDurationMS}ms` } as React.CSSProperties}
|
||||
className={joinClassNames(toastClasses.ToastPopup, toastClasses.toastEnter)}
|
||||
style={{ '--toast-duration': `${toast.duration}ms` } as React.CSSProperties}
|
||||
onClick={toast.onClick}
|
||||
className={joinClassNames(templateClasses.ShortTemplate, toast.className || '')}
|
||||
>
|
||||
<div
|
||||
onClick={toast.data.onClick}
|
||||
className={joinClassNames(templateClasses.ShortTemplate, toast.data.className || '')}
|
||||
>
|
||||
{toast.data.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.data.logo}</div>}
|
||||
<div className={joinClassNames(templateClasses.Content, toast.data.contentClassName || '')}>
|
||||
<div className={templateClasses.Header}>
|
||||
{toast.data.icon && <div className={templateClasses.Icon}>{toast.data.icon}</div>}
|
||||
<div className={templateClasses.Title}>{toast.data.title}</div>
|
||||
</div>
|
||||
<div className={templateClasses.Body}>{toast.data.body}</div>
|
||||
{toast.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.logo}</div>}
|
||||
<div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}>
|
||||
<div className={templateClasses.Header}>
|
||||
{toast.icon && <div className={templateClasses.Icon}>{toast.icon}</div>}
|
||||
<div className={templateClasses.Title}>{toast.title}</div>
|
||||
</div>
|
||||
<div className={templateClasses.Body}>{toast.body}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
DialogButton,
|
||||
DialogCheckbox,
|
||||
DialogCheckboxProps,
|
||||
Marquee,
|
||||
Menu,
|
||||
MenuItem,
|
||||
findModuleChild,
|
||||
showContextMenu,
|
||||
} from 'decky-frontend-lib';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaChevronDown } from 'react-icons/fa';
|
||||
|
||||
const dropDownControlButtonClass = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (const prop in m) {
|
||||
if (m[prop]?.toString()?.includes('gamepaddropdown_DropDownControlButton')) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const DropdownMultiselectItem: FC<
|
||||
{
|
||||
value: any;
|
||||
onSelect: (checked: boolean, value: any) => void;
|
||||
checked: boolean;
|
||||
} & DialogCheckboxProps
|
||||
> = ({ value, onSelect, checked: defaultChecked, ...rest }) => {
|
||||
const [checked, setChecked] = useState(defaultChecked);
|
||||
|
||||
useEffect(() => {
|
||||
onSelect?.(checked, value);
|
||||
}, [checked, onSelect, value]);
|
||||
|
||||
return (
|
||||
<MenuItem bInteractableItem onClick={() => setChecked((x) => !x)}>
|
||||
<DialogCheckbox
|
||||
style={{ marginBottom: 0, padding: 0 }}
|
||||
className="decky_DropdownMultiselectItem_DialogCheckbox"
|
||||
bottomSeparator="none"
|
||||
{...rest}
|
||||
onClick={() => setChecked((x) => !x)}
|
||||
onChange={(checked) => setChecked(checked)}
|
||||
controlled
|
||||
checked={checked}
|
||||
/>
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownMultiselect: FC<{
|
||||
items: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
selected: string[];
|
||||
onSelect: (selected: any[]) => void;
|
||||
label: string;
|
||||
}> = ({ label, items, selected, onSelect }) => {
|
||||
const [itemsSelected, setItemsSelected] = useState<any>(selected);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleItemSelect = useCallback((checked, value) => {
|
||||
setItemsSelected((x: any) =>
|
||||
checked ? [...x.filter((y: any) => y !== value), value] : x.filter((y: any) => y !== value),
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onSelect(itemsSelected);
|
||||
}, [itemsSelected, onSelect]);
|
||||
|
||||
return (
|
||||
<DialogButton
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
className={dropDownControlButtonClass}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
showContextMenu(
|
||||
<Menu label={label} cancelText={t('DropdownMultiselect.button.back') as string}>
|
||||
<style>
|
||||
{`
|
||||
/* Inherit color from ".basiccontextmenu" */
|
||||
.decky_DropdownMultiselectItem_DialogCheckbox > .DialogToggle_Label {
|
||||
color: inherit;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div style={{ marginTop: '10px' }}>{/*FIXME: Hack for missing padding under label menu*/}</div>
|
||||
{items.map((x) => (
|
||||
<DropdownMultiselectItem
|
||||
key={x.value}
|
||||
label={x.label}
|
||||
value={x.value}
|
||||
checked={itemsSelected.includes(x.value)}
|
||||
onSelect={handleItemSelect}
|
||||
/>
|
||||
))}
|
||||
</Menu>,
|
||||
evt.currentTarget ?? window,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Marquee>
|
||||
{selected.length > 0
|
||||
? selected.map((x: any) => items[items.findIndex((v) => v.value === x)].label).join(', ')
|
||||
: '…'}
|
||||
</Marquee>
|
||||
<div style={{ flexGrow: 1, minWidth: '1ch' }} />
|
||||
<FaChevronDown style={{ height: '1em', flex: '0 0 1em' }} />
|
||||
</DialogButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMultiselect;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
|
||||
import { FC, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { InstallType } from '../../plugin';
|
||||
|
||||
interface MultiplePluginsInstallModalProps {
|
||||
requests: { name: string; version: string; hash: string; install_type: InstallType }[];
|
||||
onOK(): void | Promise<void>;
|
||||
onCancel(): void | Promise<void>;
|
||||
closeModal?(): void;
|
||||
}
|
||||
|
||||
// values are the JSON keys used in the translation file
|
||||
const InstallTypeTranslationMapping = {
|
||||
[InstallType.INSTALL]: 'install',
|
||||
[InstallType.REINSTALL]: 'reinstall',
|
||||
[InstallType.UPDATE]: 'update',
|
||||
} as const satisfies Record<InstallType, string>;
|
||||
|
||||
type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[InstallType];
|
||||
|
||||
const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
|
||||
requests,
|
||||
onOK,
|
||||
onCancel,
|
||||
closeModal,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// used as part of the title translation
|
||||
// if we know all operations are of a specific type, we can show so in the title to make decision easier
|
||||
const installTypeGrouped = useMemo((): TitleTranslationMapping => {
|
||||
if (requests.every(({ install_type }) => install_type === InstallType.INSTALL)) return 'install';
|
||||
if (requests.every(({ install_type }) => install_type === InstallType.REINSTALL)) return 'reinstall';
|
||||
if (requests.every(({ install_type }) => install_type === InstallType.UPDATE)) return 'update';
|
||||
return 'mixed';
|
||||
}, [requests]);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
bOKDisabled={loading}
|
||||
closeModal={closeModal}
|
||||
onOK={async () => {
|
||||
setLoading(true);
|
||||
await onOK();
|
||||
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
|
||||
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
|
||||
}}
|
||||
onCancel={async () => {
|
||||
await onCancel();
|
||||
}}
|
||||
strTitle={<div>{t(`MultiplePluginsInstallModal.title.${installTypeGrouped}`, { count: requests.length })}</div>}
|
||||
strOKButtonText={t(`MultiplePluginsInstallModal.ok_button.${loading ? 'loading' : 'idle'}`)}
|
||||
>
|
||||
<div>
|
||||
{t('MultiplePluginsInstallModal.confirm')}
|
||||
<ul style={{ listStyle: 'none', display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
{requests.map(({ name, version, install_type, hash }, i) => {
|
||||
const installTypeStr = InstallTypeTranslationMapping[install_type];
|
||||
const description = t(`MultiplePluginsInstallModal.description.${installTypeStr}`, {
|
||||
name,
|
||||
version,
|
||||
});
|
||||
|
||||
return (
|
||||
<li key={i} style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div>{description}</div>
|
||||
{hash === 'False' && (
|
||||
<div style={{ color: 'red', paddingLeft: '10px' }}>{t('PluginInstallModal.no_hash')}</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiplePluginsInstallModal;
|
||||
@@ -1,18 +1,31 @@
|
||||
import { ConfirmModal, QuickAccessTab, Router, Spinner, staticClasses } from 'decky-frontend-lib';
|
||||
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper';
|
||||
|
||||
interface PluginInstallModalProps {
|
||||
artifact: string;
|
||||
version: string;
|
||||
hash: string;
|
||||
// reinstall: boolean;
|
||||
installType: number;
|
||||
onOK(): void;
|
||||
onCancel(): void;
|
||||
closeModal?(): void;
|
||||
}
|
||||
|
||||
const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, hash, onOK, onCancel, closeModal }) => {
|
||||
const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
artifact,
|
||||
version,
|
||||
hash,
|
||||
installType,
|
||||
onOK,
|
||||
onCancel,
|
||||
closeModal,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
bOKDisabled={loading}
|
||||
@@ -20,21 +33,54 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
|
||||
onOK={async () => {
|
||||
setLoading(true);
|
||||
await onOK();
|
||||
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
|
||||
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
|
||||
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
|
||||
}}
|
||||
onCancel={async () => {
|
||||
await onCancel();
|
||||
}}
|
||||
>
|
||||
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
|
||||
{hash == 'False' ? <h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3> : null}
|
||||
<div style={{ flexDirection: 'row' }}>
|
||||
{loading && <Spinner style={{ width: '20px' }} />} {loading ? 'Installing' : 'Install'} {artifact}
|
||||
{version ? ' version ' + version : null}
|
||||
{!loading && '?'}
|
||||
strTitle={
|
||||
<div>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
trans_text="title"
|
||||
i18n_args={{ artifact: artifact }}
|
||||
install_type={installType}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
strOKButtonText={
|
||||
loading ? (
|
||||
<div>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
trans_text="button_processing"
|
||||
install_type={installType}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
trans_text="button_idle"
|
||||
install_type={installType}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
trans_text="desc"
|
||||
i18n_args={{
|
||||
artifact: artifact,
|
||||
version: version,
|
||||
}}
|
||||
install_type={installType}
|
||||
/>
|
||||
</div>
|
||||
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ConfirmModal } from 'decky-frontend-lib';
|
||||
import { FC } from 'react';
|
||||
|
||||
interface PluginUninstallModalProps {
|
||||
name: string;
|
||||
title: string;
|
||||
buttonText: string;
|
||||
description: string;
|
||||
closeModal?(): void;
|
||||
}
|
||||
|
||||
const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => {
|
||||
return (
|
||||
<ConfirmModal
|
||||
closeModal={closeModal}
|
||||
onOK={async () => {
|
||||
await window.DeckyPluginLoader.callServerMethod('uninstall_plugin', { name });
|
||||
// uninstalling a plugin resets the hidden setting for it server-side
|
||||
// we invalidate here so if you re-install it, you won't have an out-of-date hidden filter
|
||||
await window.DeckyPluginLoader.hiddenPluginsService.invalidate();
|
||||
}}
|
||||
strTitle={title}
|
||||
strOKButtonText={buttonText}
|
||||
>
|
||||
{description}
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginUninstallModal;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconContext } from 'react-icons';
|
||||
import { FaExclamationTriangle, FaQuestionCircle, FaUserSlash } from 'react-icons/fa';
|
||||
|
||||
export enum FileErrorTypes {
|
||||
FileNotFound,
|
||||
PermissionDenied,
|
||||
Unknown,
|
||||
None,
|
||||
}
|
||||
|
||||
interface FilePickerErrorProps {
|
||||
error: FileErrorTypes;
|
||||
rawError?: string;
|
||||
}
|
||||
|
||||
const FilePickerError: FC<FilePickerErrorProps> = ({ error, rawError = null }) => {
|
||||
const [icon, setIcon] = useState<JSX.Element>(<FaQuestionCircle />);
|
||||
const [text, setText] = useState<string | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
switch (error) {
|
||||
case FileErrorTypes.FileNotFound:
|
||||
setText(t('FilePickerError.errors.file_not_found'));
|
||||
setIcon(<FaExclamationTriangle />);
|
||||
break;
|
||||
case FileErrorTypes.PermissionDenied:
|
||||
setText(t('FilePickerError.errors.perm_denied'));
|
||||
setIcon(<FaUserSlash />);
|
||||
break;
|
||||
case FileErrorTypes.Unknown:
|
||||
setText(t('FilePickerError.errors.unknown', { raw_error: rawError }));
|
||||
setIcon(<FaQuestionCircle />);
|
||||
break;
|
||||
case FileErrorTypes.None:
|
||||
setText(null);
|
||||
setIcon(<div></div>);
|
||||
break;
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ paddingTop: '50px', textAlign: 'center', height: '100%' }}>
|
||||
<IconContext.Provider value={{ className: 'fileError', size: '128px' }}>
|
||||
<div style={{ alignSelf: 'center', alignContent: 'center' }}>{icon}</div>
|
||||
</IconContext.Provider>
|
||||
<p style={{ height: '32px', paddingTop: '25px', alignSelf: 'flex-start', textAlign: 'center' }}>{text}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilePickerError;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { FC } from 'react';
|
||||
import { Translation } from 'react-i18next';
|
||||
|
||||
export enum SortOptions {
|
||||
name_desc = 'name_desc',
|
||||
name_asc = 'name_asc',
|
||||
modified_desc = 'modified_desc',
|
||||
modified_asc = 'modified_asc',
|
||||
created_desc = 'created_desc',
|
||||
created_asc = 'created_asc',
|
||||
size_desc = 'size_desc',
|
||||
size_asc = 'size_asc',
|
||||
}
|
||||
|
||||
interface TSortOptionsProps {
|
||||
trans_part: SortOptions;
|
||||
}
|
||||
|
||||
const TSortOptions: FC<TSortOptionsProps> = ({ trans_part }) => {
|
||||
return (
|
||||
<Translation>
|
||||
{(t, {}) => {
|
||||
switch (trans_part) {
|
||||
case SortOptions.name_desc:
|
||||
return t('FilePickerIndex.filter.name_desc');
|
||||
case SortOptions.name_asc:
|
||||
return t('FilePickerIndex.filter.name_asce');
|
||||
case SortOptions.modified_desc:
|
||||
return t('FilePickerIndex.filter.modified_desc');
|
||||
case SortOptions.modified_asc:
|
||||
return t('FilePickerIndex.filter.modified_asce');
|
||||
case SortOptions.created_desc:
|
||||
return t('FilePickerIndex.filter.created_desc');
|
||||
case SortOptions.created_asc:
|
||||
return t('FilePickerIndex.filter.created_asce');
|
||||
case SortOptions.size_desc:
|
||||
return t('FilePickerIndex.filter.size_desc');
|
||||
case SortOptions.size_asc:
|
||||
return t('FilePickerIndex.filter.size_asce');
|
||||
}
|
||||
}}
|
||||
</Translation>
|
||||
);
|
||||
};
|
||||
|
||||
export default TSortOptions;
|
||||
@@ -38,7 +38,7 @@ const imageStyle = {
|
||||
color: '#d18f00',
|
||||
};
|
||||
|
||||
const imageExtList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff'];
|
||||
const imageExtList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff', 'apng', 'tga'];
|
||||
|
||||
styleDef.push([imageStyle, imageExtList]);
|
||||
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import { DialogButton, Focusable, SteamSpinner, TextField } from 'decky-frontend-lib';
|
||||
import { useEffect } from 'react';
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
import {
|
||||
ControlsList,
|
||||
DialogBody,
|
||||
DialogButton,
|
||||
DialogControlsSection,
|
||||
DialogFooter,
|
||||
Dropdown,
|
||||
Focusable,
|
||||
Marquee,
|
||||
SteamSpinner,
|
||||
TextField,
|
||||
ToggleField,
|
||||
} from 'decky-frontend-lib';
|
||||
import { filesize } from 'filesize';
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FileIcon, defaultStyles } from 'react-file-icon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaArrowUp, FaFolder } from 'react-icons/fa';
|
||||
|
||||
import Logger from '../../../logger';
|
||||
import DropdownMultiselect from '../DropdownMultiselect';
|
||||
import FilePickerError, { FileErrorTypes } from './FilePickerError';
|
||||
import TSortOption, { SortOptions } from './i18n/TSortOptions';
|
||||
import { styleDefObj } from './iconCustomizations';
|
||||
|
||||
const logger = new Logger('FilePicker');
|
||||
@@ -12,27 +28,95 @@ const logger = new Logger('FilePicker');
|
||||
export interface FilePickerProps {
|
||||
startPath: string;
|
||||
includeFiles?: boolean;
|
||||
regex?: RegExp;
|
||||
includeFolders?: boolean;
|
||||
filter?: RegExp | ((file: File) => boolean);
|
||||
validFileExtensions?: string[];
|
||||
allowAllFiles?: boolean;
|
||||
defaultHidden?: boolean;
|
||||
max?: number;
|
||||
fileSelType?: FileSelectionType;
|
||||
onSubmit: (val: { path: string; realpath: string }) => void;
|
||||
closeModal?: () => void;
|
||||
}
|
||||
|
||||
interface File {
|
||||
export interface File {
|
||||
isdir: boolean;
|
||||
ishidden: boolean;
|
||||
name: string;
|
||||
realpath: string;
|
||||
size: number;
|
||||
modified: number;
|
||||
created: number;
|
||||
}
|
||||
|
||||
export enum FileSelectionType {
|
||||
FILE,
|
||||
FOLDER,
|
||||
}
|
||||
|
||||
interface FileListing {
|
||||
realpath: string;
|
||||
files: File[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
const sortOptions = [
|
||||
{
|
||||
data: SortOptions.name_desc,
|
||||
label: <TSortOption trans_part={SortOptions.name_desc} />,
|
||||
},
|
||||
{
|
||||
data: SortOptions.name_asc,
|
||||
label: <TSortOption trans_part={SortOptions.name_asc} />,
|
||||
},
|
||||
{
|
||||
data: SortOptions.modified_desc,
|
||||
label: <TSortOption trans_part={SortOptions.modified_desc} />,
|
||||
},
|
||||
{
|
||||
data: SortOptions.modified_asc,
|
||||
label: <TSortOption trans_part={SortOptions.modified_asc} />,
|
||||
},
|
||||
{
|
||||
data: SortOptions.created_desc,
|
||||
label: <TSortOption trans_part={SortOptions.created_desc} />,
|
||||
},
|
||||
{
|
||||
data: SortOptions.created_asc,
|
||||
label: <TSortOption trans_part={SortOptions.created_asc} />,
|
||||
},
|
||||
{
|
||||
data: SortOptions.size_desc,
|
||||
label: <TSortOption trans_part={SortOptions.size_desc} />,
|
||||
},
|
||||
{
|
||||
data: SortOptions.size_asc,
|
||||
label: <TSortOption trans_part={SortOptions.size_asc} />,
|
||||
},
|
||||
];
|
||||
|
||||
function getList(
|
||||
path: string,
|
||||
includeFiles: boolean = true,
|
||||
includeFiles: boolean,
|
||||
includeFolders: boolean = true,
|
||||
includeExt: string[] | null = null,
|
||||
includeHidden: boolean = false,
|
||||
orderBy: SortOptions = SortOptions.name_desc,
|
||||
filterFor: RegExp | ((file: File) => boolean) | null = null,
|
||||
pageNumber: number = 1,
|
||||
max: number = 1000,
|
||||
): Promise<{ result: FileListing | string; success: boolean }> {
|
||||
return window.DeckyPluginLoader.callServerMethod('filepicker_ls', { path, include_files: includeFiles });
|
||||
return window.DeckyPluginLoader.callServerMethod('filepicker_ls', {
|
||||
path,
|
||||
include_files: includeFiles,
|
||||
include_folders: includeFolders,
|
||||
include_ext: includeExt ? includeExt : [],
|
||||
include_hidden: includeHidden,
|
||||
order_by: orderBy,
|
||||
filter_for: filterFor,
|
||||
page: pageNumber,
|
||||
max: max,
|
||||
});
|
||||
}
|
||||
|
||||
const iconStyles = {
|
||||
@@ -42,118 +126,254 @@ const iconStyles = {
|
||||
|
||||
const FilePicker: FunctionComponent<FilePickerProps> = ({
|
||||
startPath,
|
||||
//What are we allowing to show in the file picker
|
||||
includeFiles = true,
|
||||
regex,
|
||||
includeFolders = true,
|
||||
//Parameter for specifying a specific filename match
|
||||
filter = undefined,
|
||||
//Filter for specific extensions as an array
|
||||
validFileExtensions = undefined,
|
||||
//Allow to override the fixed extension above
|
||||
allowAllFiles = true,
|
||||
//If we need to show hidden files and folders (both Win and Linux should work)
|
||||
defaultHidden = false, // false by default makes sense for most users
|
||||
//How much files per page to show, default 1000
|
||||
max = 1000,
|
||||
//Which picking option to select by default
|
||||
fileSelType = FileSelectionType.FOLDER,
|
||||
onSubmit,
|
||||
closeModal,
|
||||
}) => {
|
||||
if (startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (startPath !== '/' && startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path
|
||||
const [path, setPath] = useState<string>(startPath);
|
||||
const [listing, setListing] = useState<FileListing>({ files: [], realpath: path });
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [listing, setListing] = useState<FileListing>({ files: [], realpath: path, total: 0 });
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [error, setError] = useState<FileErrorTypes>(FileErrorTypes.None);
|
||||
const [rawError, setRawError] = useState<string | null>(null);
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [showHidden, setShowHidden] = useState<boolean>(defaultHidden);
|
||||
const [sort, setSort] = useState<SortOptions>(SortOptions.name_desc);
|
||||
const [selectedExts, setSelectedExts] = useState<string[] | undefined>(validFileExtensions);
|
||||
|
||||
const validExtsOptions = useMemo(() => {
|
||||
let validExt: { label: string; value: string }[] = [];
|
||||
if (validFileExtensions) {
|
||||
if (allowAllFiles) {
|
||||
validExt.push({ label: t('FilePickerIndex.files.all_files'), value: 'all_files' });
|
||||
}
|
||||
validExt.push(...validFileExtensions.map((x) => ({ label: x, value: x })));
|
||||
}
|
||||
return validExt;
|
||||
}, [validFileExtensions, allowAllFiles]);
|
||||
|
||||
function isSelectionValid(validExts: string[], selection: string[]) {
|
||||
if (validExts.some((el) => selection.includes(el))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const handleExtsSelect = useCallback((val: any) => {
|
||||
// unselect other options if "All Files" is checked
|
||||
if (allowAllFiles && val.includes('all_files')) {
|
||||
setSelectedExts(['all_files']);
|
||||
} else if (validFileExtensions && isSelectionValid(validFileExtensions, val)) {
|
||||
// If at least one extension is still selected, then assign this selection to the selected values
|
||||
setSelectedExts(val);
|
||||
} else {
|
||||
// Else do nothing
|
||||
setSelectedExts(selectedExts);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (error) setError(null);
|
||||
setLoading(true);
|
||||
const listing = await getList(path, includeFiles);
|
||||
const listing = await getList(
|
||||
path,
|
||||
includeFiles,
|
||||
includeFolders,
|
||||
selectedExts,
|
||||
showHidden,
|
||||
sort,
|
||||
filter,
|
||||
page,
|
||||
max,
|
||||
);
|
||||
if (!listing.success) {
|
||||
setListing({ files: [], realpath: path });
|
||||
setListing({ files: [], realpath: path, total: 0 });
|
||||
setLoading(false);
|
||||
setError(listing.result as string);
|
||||
logger.error(listing.result);
|
||||
const theError = listing.result as string;
|
||||
switch (theError) {
|
||||
case theError.match(/\[Errno\s2.*/i)?.input:
|
||||
case theError.match(/\[WinError\s3.*/i)?.input:
|
||||
setError(FileErrorTypes.FileNotFound);
|
||||
break;
|
||||
case theError.match(/\[Errno\s13.*/i)?.input:
|
||||
setError(FileErrorTypes.PermissionDenied);
|
||||
break;
|
||||
default:
|
||||
setRawError(theError);
|
||||
setError(FileErrorTypes.Unknown);
|
||||
break;
|
||||
}
|
||||
logger.debug(theError);
|
||||
return;
|
||||
} else {
|
||||
setRawError(null);
|
||||
setError(FileErrorTypes.None);
|
||||
setFiles((listing.result as FileListing).files);
|
||||
}
|
||||
setLoading(false);
|
||||
setListing(listing.result as FileListing);
|
||||
logger.log('reloaded', path, listing);
|
||||
})();
|
||||
}, [path]);
|
||||
}, [error, path, includeFiles, includeFolders, showHidden, sort, selectedExts, page]);
|
||||
|
||||
return (
|
||||
<div className="deckyFilePicker">
|
||||
<Focusable style={{ display: 'flex', flexDirection: 'row', paddingBottom: '10px' }}>
|
||||
<DialogButton
|
||||
style={{
|
||||
minWidth: 'unset',
|
||||
width: '40px',
|
||||
flexGrow: '0',
|
||||
borderRadius: 'unset',
|
||||
margin: '0',
|
||||
padding: '10px',
|
||||
}}
|
||||
onClick={() => {
|
||||
const newPathArr = path.split('/');
|
||||
newPathArr.pop();
|
||||
let newPath = newPathArr.join('/');
|
||||
if (newPath == '') newPath = '/';
|
||||
setPath(newPath);
|
||||
}}
|
||||
>
|
||||
<FaArrowUp />
|
||||
</DialogButton>
|
||||
<div style={{ flexGrow: '1', width: '100%' }}>
|
||||
<TextField
|
||||
value={path}
|
||||
onChange={(e) => {
|
||||
e.target.value && setPath(e.target.value);
|
||||
<>
|
||||
<DialogBody className="deckyFilePicker">
|
||||
<DialogControlsSection>
|
||||
<Focusable flow-children="right" style={{ display: 'flex', marginBottom: '1em' }}>
|
||||
<DialogButton
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minWidth: 'unset',
|
||||
width: '40px',
|
||||
borderRadius: 'unset',
|
||||
margin: '0',
|
||||
padding: '10px',
|
||||
}}
|
||||
onClick={() => {
|
||||
const newPathArr = path.split('/');
|
||||
const lastPath = newPathArr.pop();
|
||||
//If I have a single / with spaces, pop the array twice
|
||||
if (lastPath?.match(/^\/\s*$/) != null) newPathArr.pop();
|
||||
let newPath = newPathArr.join('/');
|
||||
if (newPath == '') newPath = '/';
|
||||
setPath(newPath);
|
||||
}}
|
||||
>
|
||||
<FaArrowUp />
|
||||
</DialogButton>
|
||||
<div style={{ width: '100%' }}>
|
||||
<TextField
|
||||
value={path}
|
||||
onChange={(e) => {
|
||||
e.target.value && setPath(e.target.value);
|
||||
}}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</Focusable>
|
||||
<ControlsList alignItems="center" spacing="standard">
|
||||
<ToggleField
|
||||
highlightOnFocus={false}
|
||||
label={t('FilePickerIndex.files.show_hidden')}
|
||||
bottomSeparator="none"
|
||||
checked={showHidden}
|
||||
onChange={() => setShowHidden((x) => !x)}
|
||||
/>
|
||||
<Dropdown rgOptions={sortOptions} selectedOption={sort} onChange={(x) => setSort(x.data)} />
|
||||
{validFileExtensions && (
|
||||
<DropdownMultiselect
|
||||
label={t('FilePickerIndex.files.file_type')}
|
||||
items={validExtsOptions}
|
||||
selected={selectedExts ? selectedExts : []}
|
||||
onSelect={handleExtsSelect}
|
||||
/>
|
||||
)}
|
||||
</ControlsList>
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection style={{ marginTop: '1em' }}>
|
||||
<Focusable
|
||||
style={{ display: 'flex', gap: '.25em', flexDirection: 'column', height: '60vh', overflow: 'scroll' }}
|
||||
>
|
||||
{loading && error === FileErrorTypes.None && <SteamSpinner style={{ height: '100%' }} />}
|
||||
{!loading &&
|
||||
error === FileErrorTypes.None &&
|
||||
files.map((file) => {
|
||||
const extension = file.realpath.split('.').pop() as string;
|
||||
return (
|
||||
<DialogButton
|
||||
key={`${file.realpath}${file.name}`}
|
||||
style={{ borderRadius: 'unset', margin: '0', padding: '10px' }}
|
||||
onClick={() => {
|
||||
const fullPath = `${path}${path.endsWith('/') ? '' : '/'}${file.name}`;
|
||||
if (file.isdir) setPath(fullPath);
|
||||
else {
|
||||
onSubmit({ path: fullPath, realpath: file.realpath });
|
||||
closeModal?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-start' }}>
|
||||
{file.isdir ? (
|
||||
<FaFolder style={iconStyles} />
|
||||
) : (
|
||||
<div style={iconStyles}>
|
||||
{file.realpath.includes('.') ? (
|
||||
<FileIcon {...defaultStyles[extension]} {...styleDefObj[extension]} extension={''} />
|
||||
) : (
|
||||
<FileIcon />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Marquee>{file.name}</Marquee>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
opacity: 0.5,
|
||||
fontSize: '.6em',
|
||||
textAlign: 'left',
|
||||
lineHeight: 1,
|
||||
marginTop: '.5em',
|
||||
}}
|
||||
>
|
||||
{file.isdir ? t('FilePickerIndex.folder.label') : filesize(file.size, { standard: 'iec' })}
|
||||
<span style={{ marginLeft: 'auto' }}>{new Date(file.modified * 1000).toLocaleString()}</span>
|
||||
</div>
|
||||
</DialogButton>
|
||||
);
|
||||
})}
|
||||
{error !== FileErrorTypes.None && <FilePickerError error={error} rawError={rawError ? rawError : ''} />}
|
||||
</Focusable>
|
||||
</DialogControlsSection>
|
||||
</DialogBody>
|
||||
{!loading && error === FileErrorTypes.None && (
|
||||
<DialogFooter>
|
||||
<DialogButton
|
||||
className="Primary"
|
||||
style={{ marginTop: '10px', alignSelf: 'flex-end' }}
|
||||
onClick={() => {
|
||||
onSubmit({ path, realpath: listing.realpath });
|
||||
closeModal?.();
|
||||
}}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</Focusable>
|
||||
<Focusable style={{ display: 'flex', flexDirection: 'column', height: '60vh', overflow: 'scroll' }}>
|
||||
{loading && <SteamSpinner style={{ height: '100%' }} />}
|
||||
{!loading &&
|
||||
listing.files
|
||||
.filter((file) => (includeFiles || file.isdir) && (!regex || regex.test(file.name)))
|
||||
.map((file) => {
|
||||
let extension = file.realpath.split('.').pop() as string;
|
||||
return (
|
||||
<DialogButton
|
||||
style={{ borderRadius: 'unset', margin: '0', padding: '10px' }}
|
||||
onClick={() => {
|
||||
const fullPath = `${path}${path.endsWith('/') ? '' : '/'}${file.name}`;
|
||||
if (file.isdir) setPath(fullPath);
|
||||
else {
|
||||
onSubmit({ path: fullPath, realpath: file.realpath });
|
||||
closeModal?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-start' }}>
|
||||
{file.isdir ? (
|
||||
<FaFolder style={iconStyles} />
|
||||
) : (
|
||||
<div style={iconStyles}>
|
||||
{file.realpath.includes('.') ? (
|
||||
<FileIcon {...defaultStyles[extension]} {...styleDefObj[extension]} extension={''} />
|
||||
) : (
|
||||
<FileIcon />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{file.name}
|
||||
</div>
|
||||
</DialogButton>
|
||||
);
|
||||
})}
|
||||
{error}
|
||||
</Focusable>
|
||||
{!loading && !error && !includeFiles && (
|
||||
<DialogButton
|
||||
className="Primary"
|
||||
style={{ marginTop: '10px', alignSelf: 'flex-end' }}
|
||||
onClick={() => {
|
||||
onSubmit({ path, realpath: listing.realpath });
|
||||
closeModal?.();
|
||||
}}
|
||||
>
|
||||
Use this folder
|
||||
</DialogButton>
|
||||
>
|
||||
{fileSelType === FileSelectionType.FILE
|
||||
? t('FilePickerIndex.file.select')
|
||||
: t('FilePickerIndex.folder.select')}
|
||||
</DialogButton>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</div>
|
||||
{page * max < listing.total && (
|
||||
<DialogFooter>
|
||||
<DialogButton
|
||||
className="Primary"
|
||||
style={{ marginTop: '10px', alignSelf: 'flex-end' }}
|
||||
onClick={() => {
|
||||
setPage(page + 1);
|
||||
}}
|
||||
>
|
||||
{t('FilePickerIndex.folder.show_more')}
|
||||
</DialogButton>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,45 +1,52 @@
|
||||
import { Patch, findModuleChild, replacePatch } from 'decky-frontend-lib';
|
||||
import { Patch, findModuleChild, replacePatch, sleep } from 'decky-frontend-lib';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
SteamClient: any;
|
||||
appDetailsStore: any;
|
||||
}
|
||||
}
|
||||
import Logger from '../../../../logger';
|
||||
|
||||
const logger = new Logger('LibraryPatch');
|
||||
|
||||
let patch: Patch;
|
||||
|
||||
function rePatch() {
|
||||
// If you patch anything on SteamClient within the first few seconds of the client having loaded it will get redefined for some reason, so repatch any of these changes that occur within the first 20s of the last patch
|
||||
// If you patch anything on SteamClient within the first few seconds of the client having loaded it will get redefined for some reason, so repatch any of these changes that occur with History.listen or an interval
|
||||
patch = replacePatch(window.SteamClient.Apps, 'PromptToChangeShortcut', async ([appid]: number[]) => {
|
||||
try {
|
||||
const details = window.appDetailsStore.GetAppDetails(appid);
|
||||
console.log(details);
|
||||
logger.debug('game details', details);
|
||||
// strShortcutStartDir
|
||||
const file = await window.DeckyPluginLoader.openFilePicker(details.strShortcutStartDir.replaceAll('"', ''));
|
||||
console.log('user selected', file);
|
||||
const file = await window.DeckyPluginLoader.openFilePicker(
|
||||
details?.strShortcutStartDir.replaceAll('"', '') || '/',
|
||||
);
|
||||
logger.debug('user selected', file);
|
||||
window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
|
||||
const pathArr = file.path.split('/');
|
||||
pathArr.pop();
|
||||
const folder = pathArr.join('/');
|
||||
window.SteamClient.Apps.SetShortcutStartDir(appid, JSON.stringify(folder));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
logger.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO type and add to frontend-lib
|
||||
const History = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.m_history) return m[prop].m_history;
|
||||
}
|
||||
});
|
||||
|
||||
export default async function libraryPatch() {
|
||||
try {
|
||||
rePatch();
|
||||
// TODO type and add to frontend-lib
|
||||
let History: any;
|
||||
|
||||
while (!History) {
|
||||
History = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.m_history) return m[prop].m_history;
|
||||
}
|
||||
});
|
||||
if (!History) {
|
||||
logger.debug('Waiting 5s for history to become available.');
|
||||
await sleep(5000);
|
||||
}
|
||||
}
|
||||
|
||||
const unlisten = History.listen(() => {
|
||||
if (window.SteamClient.Apps.PromptToChangeShortcut !== patch.patchedFunction) {
|
||||
rePatch();
|
||||
@@ -47,11 +54,11 @@ export default async function libraryPatch() {
|
||||
});
|
||||
|
||||
return () => {
|
||||
patch.unpatch();
|
||||
unlisten();
|
||||
patch.unpatch();
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error patching library file picker', e);
|
||||
logger.error('Error patching library file picker', e);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
|
||||
@@ -1,25 +1,45 @@
|
||||
import { SidebarNavigation } from 'decky-frontend-lib';
|
||||
import { lazy } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaCode, FaPlug } from 'react-icons/fa';
|
||||
|
||||
import { useSetting } from '../../utils/hooks/useSetting';
|
||||
import DeckyIcon from '../DeckyIcon';
|
||||
import WithSuspense from '../WithSuspense';
|
||||
import GeneralSettings from './pages/general';
|
||||
import PluginList from './pages/plugin_list';
|
||||
|
||||
const DeveloperSettings = lazy(() => import('./pages/developer'));
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<SidebarNavigation
|
||||
title="Decky Settings"
|
||||
showTitle
|
||||
pages={[
|
||||
{
|
||||
title: 'General',
|
||||
content: <GeneralSettings />,
|
||||
route: '/decky/settings/general',
|
||||
},
|
||||
{
|
||||
title: 'Plugins',
|
||||
content: <PluginList />,
|
||||
route: '/decky/settings/plugins',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const [isDeveloper, setIsDeveloper] = useSetting<boolean>('developer.enabled', false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pages = [
|
||||
{
|
||||
title: t('SettingsIndex.general_title'),
|
||||
content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
|
||||
route: '/decky/settings/general',
|
||||
icon: <DeckyIcon />,
|
||||
},
|
||||
{
|
||||
title: t('SettingsIndex.plugins_title'),
|
||||
content: <PluginList />,
|
||||
route: '/decky/settings/plugins',
|
||||
icon: <FaPlug />,
|
||||
},
|
||||
{
|
||||
title: t('SettingsIndex.developer_title'),
|
||||
content: (
|
||||
<WithSuspense>
|
||||
<DeveloperSettings />
|
||||
</WithSuspense>
|
||||
),
|
||||
route: '/decky/settings/developer',
|
||||
icon: <FaCode />,
|
||||
visible: isDeveloper,
|
||||
},
|
||||
];
|
||||
|
||||
return <SidebarNavigation pages={pages} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
DialogBody,
|
||||
DialogButton,
|
||||
DialogControlsSection,
|
||||
DialogControlsSectionHeader,
|
||||
Field,
|
||||
Navigation,
|
||||
TextField,
|
||||
Toggle,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaFileArchive, FaLink, FaReact, FaSteamSymbol, FaTerminal } from 'react-icons/fa';
|
||||
|
||||
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
|
||||
import Logger from '../../../../logger';
|
||||
import { installFromURL } from '../../../../store';
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
import { getSetting } from '../../../../utils/settings';
|
||||
import { FileSelectionType } from '../../../modals/filepicker';
|
||||
import RemoteDebuggingSettings from '../general/RemoteDebugging';
|
||||
|
||||
const logger = new Logger('DeveloperIndex');
|
||||
|
||||
const installFromZip = async () => {
|
||||
const path = await getSetting<string>('user_info.user_home', '');
|
||||
if (path === '') {
|
||||
logger.error('The default path has not been found!');
|
||||
return;
|
||||
}
|
||||
window.DeckyPluginLoader.openFilePickerV2(
|
||||
FileSelectionType.FILE,
|
||||
path,
|
||||
true,
|
||||
true,
|
||||
undefined,
|
||||
['zip'],
|
||||
false,
|
||||
false,
|
||||
).then((val) => {
|
||||
const url = `file://${val.path}`;
|
||||
console.log(`Installing plugin locally from ${url}`);
|
||||
installFromURL(url);
|
||||
});
|
||||
};
|
||||
|
||||
export default function DeveloperSettings() {
|
||||
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
|
||||
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
|
||||
const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', '');
|
||||
const [pluginURL, setPluginURL] = useState('');
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DialogBody>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>
|
||||
{t('SettingsDeveloperIndex.third_party_plugins.header')}
|
||||
</DialogControlsSectionHeader>
|
||||
<Field
|
||||
label={t('SettingsDeveloperIndex.third_party_plugins.label_zip')}
|
||||
icon={<FaFileArchive style={{ display: 'block' }} />}
|
||||
>
|
||||
<DialogButton onClick={installFromZip}>
|
||||
{t('SettingsDeveloperIndex.third_party_plugins.button_zip')}
|
||||
</DialogButton>
|
||||
</Field>
|
||||
<Field
|
||||
label={t('SettingsDeveloperIndex.third_party_plugins.label_url')}
|
||||
description={
|
||||
<TextField
|
||||
label={t('SettingsDeveloperIndex.third_party_plugins.label_desc')}
|
||||
value={pluginURL}
|
||||
onChange={(e) => setPluginURL(e?.target.value)}
|
||||
/>
|
||||
}
|
||||
icon={<FaLink style={{ display: 'block' }} />}
|
||||
>
|
||||
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
||||
{t('SettingsDeveloperIndex.third_party_plugins.button_install')}
|
||||
</DialogButton>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>{t('SettingsDeveloperIndex.header')}</DialogControlsSectionHeader>
|
||||
<Field
|
||||
label={t('SettingsDeveloperIndex.cef_console.label')}
|
||||
description={<span style={{ whiteSpace: 'pre-line' }}>{t('SettingsDeveloperIndex.cef_console.desc')}</span>}
|
||||
icon={<FaTerminal style={{ display: 'block' }} />}
|
||||
>
|
||||
<DialogButton
|
||||
onClick={async () => {
|
||||
let res = await window.DeckyPluginLoader.callServerMethod('get_tab_id', { name: 'SharedJSContext' });
|
||||
if (res.success) {
|
||||
Navigation.NavigateToExternalWeb(
|
||||
'localhost:8080/devtools/inspector.html?ws=localhost:8080/devtools/page/' + res.result,
|
||||
);
|
||||
} else {
|
||||
console.error('Unable to find ID for SharedJSContext tab ', res.result);
|
||||
Navigation.NavigateToExternalWeb('localhost:8080');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('SettingsDeveloperIndex.cef_console.button')}
|
||||
</DialogButton>
|
||||
</Field>
|
||||
<RemoteDebuggingSettings />
|
||||
<Field
|
||||
label={t('SettingsDeveloperIndex.valve_internal.label')}
|
||||
description={
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
{t('SettingsDeveloperIndex.valve_internal.desc1')}{' '}
|
||||
<span style={{ color: 'red' }}>{t('SettingsDeveloperIndex.valve_internal.desc2')}</span>
|
||||
</span>
|
||||
}
|
||||
icon={<FaSteamSymbol style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={enableValveInternal}
|
||||
onChange={(toggleValue) => {
|
||||
setEnableValveInternal(toggleValue);
|
||||
setShowValveInternal(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={t('SettingsDeveloperIndex.react_devtools.label')}
|
||||
description={
|
||||
<>
|
||||
<span style={{ whiteSpace: 'pre-line' }}>{t('SettingsDeveloperIndex.react_devtools.desc')}</span>
|
||||
<br />
|
||||
<br />
|
||||
<div ref={textRef}>
|
||||
<TextField
|
||||
label={t('SettingsDeveloperIndex.react_devtools.ip_label')}
|
||||
value={reactDevtoolsIP}
|
||||
onChange={(e) => setReactDevtoolsIP(e?.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
icon={<FaReact style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={reactDevtoolsEnabled}
|
||||
// disabled={reactDevtoolsIP == ''}
|
||||
onChange={(toggleValue) => {
|
||||
setReactDevtoolsEnabled(toggleValue);
|
||||
setShouldConnectToReactDevTools(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dropdown, Field } from 'decky-frontend-lib';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Logger from '../../../../logger';
|
||||
import { callUpdaterMethod } from '../../../../updater';
|
||||
@@ -10,21 +11,27 @@ const logger = new Logger('BranchSelect');
|
||||
enum UpdateBranch {
|
||||
Stable,
|
||||
Prerelease,
|
||||
// Nightly,
|
||||
// Testing,
|
||||
}
|
||||
|
||||
const BranchSelect: FunctionComponent<{}> = () => {
|
||||
const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Prerelease);
|
||||
const { t } = useTranslation();
|
||||
const tBranches = [
|
||||
t('BranchSelect.update_channel.stable'),
|
||||
t('BranchSelect.update_channel.prerelease'),
|
||||
t('BranchSelect.update_channel.testing'),
|
||||
];
|
||||
const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Stable);
|
||||
|
||||
return (
|
||||
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
|
||||
// 0 being stable, 1 being pre-release and 2 being nightly
|
||||
<Field label="Update Channel">
|
||||
<Field label={t('BranchSelect.update_channel.label')} childrenContainerWidth={'fixed'}>
|
||||
<Dropdown
|
||||
rgOptions={Object.values(UpdateBranch)
|
||||
.filter((branch) => typeof branch == 'string')
|
||||
.map((branch) => ({
|
||||
label: branch,
|
||||
label: tBranches[UpdateBranch[branch]],
|
||||
data: UpdateBranch[branch],
|
||||
}))}
|
||||
selectedOption={selectedBranch}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Field, Toggle } from 'decky-frontend-lib';
|
||||
import { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
|
||||
const NotificationSettings: FC = () => {
|
||||
const { notificationSettings } = useDeckyState();
|
||||
const notificationService = window.DeckyPluginLoader.notificationService;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field label={t('SettingsGeneralIndex.notifications.decky_updates_label')}>
|
||||
<Toggle
|
||||
value={notificationSettings.deckyUpdates}
|
||||
onChange={(deckyUpdates) => {
|
||||
notificationService.update({ ...notificationSettings, deckyUpdates });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t('SettingsGeneralIndex.notifications.plugin_updates_label')}>
|
||||
<Toggle
|
||||
value={notificationSettings.pluginUpdates}
|
||||
onChange={(pluginUpdates) => {
|
||||
notificationService.update({ ...notificationSettings, pluginUpdates });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationSettings;
|
||||
@@ -1,20 +1,18 @@
|
||||
import { Field, Toggle } from 'decky-frontend-lib';
|
||||
import { FaBug } from 'react-icons/fa';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaChrome } from 'react-icons/fa';
|
||||
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
|
||||
export default function RemoteDebuggingSettings() {
|
||||
const [allowRemoteDebugging, setAllowRemoteDebugging] = useSetting<boolean>('cef_forward', false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Field
|
||||
label="Allow Remote CEF Debugging"
|
||||
description={
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
Allow unauthenticated access to the CEF debugger to anyone in your network
|
||||
</span>
|
||||
}
|
||||
icon={<FaBug style={{ display: 'block' }} />}
|
||||
label={t('RemoteDebugging.remote_cef.label')}
|
||||
description={<span style={{ whiteSpace: 'pre-line' }}>{t('RemoteDebugging.remote_cef.desc')}</span>}
|
||||
icon={<FaChrome style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={allowRemoteDebugging || false}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Dropdown, Field, TextField } from 'decky-frontend-lib';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaShapes } from 'react-icons/fa';
|
||||
|
||||
import Logger from '../../../../logger';
|
||||
import { Store } from '../../../../store';
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
|
||||
const logger = new Logger('StoreSelect');
|
||||
|
||||
const StoreSelect: FunctionComponent<{}> = () => {
|
||||
const [selectedStore, setSelectedStore] = useSetting<Store>('store', Store.Default);
|
||||
const [selectedStoreURL, setSelectedStoreURL] = useSetting<string | null>('store-url', null);
|
||||
const { t } = useTranslation();
|
||||
const tStores = [
|
||||
t('StoreSelect.store_channel.default'),
|
||||
t('StoreSelect.store_channel.testing'),
|
||||
t('StoreSelect.store_channel.custom'),
|
||||
];
|
||||
|
||||
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
|
||||
// 0 being Default, 1 being Testing and 2 being Custom
|
||||
return (
|
||||
<>
|
||||
<Field label={t('StoreSelect.store_channel.label')} childrenContainerWidth={'fixed'}>
|
||||
<Dropdown
|
||||
rgOptions={Object.values(Store)
|
||||
.filter((store) => typeof store == 'string')
|
||||
.map((store) => ({
|
||||
label: tStores[Store[store]],
|
||||
data: Store[store],
|
||||
}))}
|
||||
selectedOption={selectedStore}
|
||||
onChange={async (newVal) => {
|
||||
await setSelectedStore(newVal.data);
|
||||
logger.log('switching stores!');
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
{selectedStore == Store.Custom && (
|
||||
<Field
|
||||
label={t('StoreSelect.custom_store.label')}
|
||||
indentLevel={1}
|
||||
description={
|
||||
<TextField
|
||||
label={t('StoreSelect.custom_store.url_label')}
|
||||
value={selectedStoreURL || undefined}
|
||||
onChange={(e) => setSelectedStoreURL(e?.target.value || null)}
|
||||
/>
|
||||
}
|
||||
icon={<FaShapes style={{ display: 'block' }} />}
|
||||
></Field>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default StoreSelect;
|
||||
@@ -6,12 +6,14 @@ import {
|
||||
Focusable,
|
||||
ProgressBarWithInfo,
|
||||
Spinner,
|
||||
findSP,
|
||||
showModal,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useCallback } from 'react';
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaArrowDown } from 'react-icons/fa';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaExclamation } from 'react-icons/fa';
|
||||
|
||||
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
@@ -21,6 +23,8 @@ import WithSuspense from '../../../WithSuspense';
|
||||
const MarkdownRenderer = lazy(() => import('../../../Markdown'));
|
||||
|
||||
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
|
||||
const SP = findSP();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Focusable onCancelButton={closeModal}>
|
||||
<FocusRing>
|
||||
@@ -37,25 +41,26 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1>{versionInfo?.all?.[id]?.name}</h1>
|
||||
<h1>{versionInfo?.all?.[id]?.name || 'Invalid Update Name'}</h1>
|
||||
{versionInfo?.all?.[id]?.body ? (
|
||||
<WithSuspense>
|
||||
<MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer>
|
||||
</WithSuspense>
|
||||
) : (
|
||||
'no patch notes for this version'
|
||||
t('Updater.no_patch_notes_desc')
|
||||
)}
|
||||
</div>
|
||||
</Focusable>
|
||||
)}
|
||||
fnGetId={(id) => id}
|
||||
nNumItems={versionInfo?.all?.length}
|
||||
nHeight={window.innerHeight - 40}
|
||||
nItemHeight={window.innerHeight - 40}
|
||||
nHeight={SP.innerHeight - 40}
|
||||
nItemHeight={SP.innerHeight - 40}
|
||||
nItemMarginX={0}
|
||||
initialColumn={0}
|
||||
autoFocus={true}
|
||||
fnGetColumnWidth={() => window.innerWidth}
|
||||
fnGetColumnWidth={() => SP.innerWidth}
|
||||
name={t('Updater.decky_updates') as string}
|
||||
/>
|
||||
</FocusRing>
|
||||
</Focusable>
|
||||
@@ -69,6 +74,8 @@ export default function UpdaterSettings() {
|
||||
const [updateProgress, setUpdateProgress] = useState<number>(-1);
|
||||
const [reloading, setReloading] = useState<boolean>(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
window.DeckyUpdater = {
|
||||
updateProgress: (i) => {
|
||||
@@ -90,23 +97,23 @@ export default function UpdaterSettings() {
|
||||
return (
|
||||
<>
|
||||
<Field
|
||||
onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined}
|
||||
onOptionsActionDescription={versionInfo?.all ? t('Updater.patch_notes_desc') : undefined}
|
||||
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
|
||||
label="Updates"
|
||||
label={t('Updater.updates.label')}
|
||||
description={
|
||||
versionInfo && (
|
||||
<span style={{ whiteSpace: 'pre-line' }}>{`Current version: ${versionInfo.current}\n${
|
||||
versionInfo.updatable ? `Latest version: ${versionInfo.remote?.tag_name}` : ''
|
||||
}`}</span>
|
||||
checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? (
|
||||
''
|
||||
) : (
|
||||
<span>{t('Updater.updates.lat_version', { ver: versionInfo?.current })} </span>
|
||||
)
|
||||
}
|
||||
icon={
|
||||
!versionInfo ? (
|
||||
<Spinner style={{ width: '1em', height: 20, display: 'block' }} />
|
||||
) : (
|
||||
<FaArrowDown style={{ display: 'block' }} />
|
||||
versionInfo?.remote &&
|
||||
versionInfo?.remote?.tag_name != versionInfo?.current && (
|
||||
<FaExclamation color="var(--gpColor-Yellow)" style={{ display: 'block' }} />
|
||||
)
|
||||
}
|
||||
childrenContainerWidth={'fixed'}
|
||||
>
|
||||
{updateProgress == -1 && !isLoaderUpdating ? (
|
||||
<DialogButton
|
||||
@@ -126,10 +133,10 @@ export default function UpdaterSettings() {
|
||||
}
|
||||
>
|
||||
{checkingForUpdates
|
||||
? 'Checking'
|
||||
? t('Updater.updates.checking')
|
||||
: !versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
|
||||
? 'Check For Updates'
|
||||
: 'Install Update'}
|
||||
? t('Updater.updates.check_button')
|
||||
: t('Updater.updates.install_button')}
|
||||
</DialogButton>
|
||||
) : (
|
||||
<ProgressBarWithInfo
|
||||
@@ -137,11 +144,11 @@ export default function UpdaterSettings() {
|
||||
bottomSeparator="none"
|
||||
nProgress={updateProgress}
|
||||
indeterminate={reloading}
|
||||
sOperationText={reloading ? 'Reloading' : 'Updating'}
|
||||
sOperationText={reloading ? t('Updater.updates.reloading') : t('Updater.updates.updating')}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
{versionInfo?.remote && (
|
||||
{versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current && (
|
||||
<InlinePatchNotes
|
||||
title={versionInfo?.remote.name}
|
||||
date={new Intl.RelativeTimeFormat('en-US', {
|
||||
|
||||
@@ -1,38 +1,54 @@
|
||||
import { DialogButton, Field, TextField } from 'decky-frontend-lib';
|
||||
import { useState } from 'react';
|
||||
import { FaShapes } from 'react-icons/fa';
|
||||
import { DialogBody, DialogControlsSection, DialogControlsSectionHeader, Field, Toggle } from 'decky-frontend-lib';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { installFromURL } from '../../../../store';
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
import BranchSelect from './BranchSelect';
|
||||
import RemoteDebuggingSettings from './RemoteDebugging';
|
||||
import NotificationSettings from './NotificationSettings';
|
||||
import StoreSelect from './StoreSelect';
|
||||
import UpdaterSettings from './Updater';
|
||||
|
||||
export default function GeneralSettings() {
|
||||
const [pluginURL, setPluginURL] = useState('');
|
||||
// const [checked, setChecked] = useState(false); // store these in some kind of State instead
|
||||
export default function GeneralSettings({
|
||||
isDeveloper,
|
||||
setIsDeveloper,
|
||||
}: {
|
||||
isDeveloper: boolean;
|
||||
setIsDeveloper: (val: boolean) => void;
|
||||
}) {
|
||||
const { versionInfo } = useDeckyState();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* <Field
|
||||
label="A Toggle with an icon"
|
||||
icon={<FaShapes style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={checked}
|
||||
onChange={(e) => setChecked(e)}
|
||||
/>
|
||||
</Field> */}
|
||||
<UpdaterSettings />
|
||||
<BranchSelect />
|
||||
<RemoteDebuggingSettings />
|
||||
<Field
|
||||
label="Manual plugin install"
|
||||
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
||||
icon={<FaShapes style={{ display: 'block' }} />}
|
||||
>
|
||||
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
||||
Install
|
||||
</DialogButton>
|
||||
</Field>
|
||||
</div>
|
||||
<DialogBody>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.updates.header')}</DialogControlsSectionHeader>
|
||||
<UpdaterSettings />
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.beta.header')}</DialogControlsSectionHeader>
|
||||
<BranchSelect />
|
||||
<StoreSelect />
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.notifications.header')}</DialogControlsSectionHeader>
|
||||
<NotificationSettings />
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.other.header')}</DialogControlsSectionHeader>
|
||||
<Field label={t('SettingsGeneralIndex.developer_mode.label')}>
|
||||
<Toggle
|
||||
value={isDeveloper}
|
||||
onChange={(toggleValue) => {
|
||||
setIsDeveloper(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.about.header')}</DialogControlsSectionHeader>
|
||||
<Field label={t('SettingsGeneralIndex.about.decky_version')} focusable={true}>
|
||||
<div style={{ color: 'var(--gpSystemLighterGrey)' }}>{versionInfo?.current}</div>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaEyeSlash } from 'react-icons/fa';
|
||||
|
||||
interface PluginListLabelProps {
|
||||
hidden: boolean;
|
||||
name: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
const PluginListLabel: FC<PluginListLabelProps> = ({ name, hidden, version }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<div>{version ? `${name} - ${version}` : name}</div>
|
||||
{hidden && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '0.8rem',
|
||||
color: '#dcdedf',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<FaEyeSlash />
|
||||
{t('PluginListLabel.hidden')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginListLabel;
|
||||
@@ -1,66 +1,223 @@
|
||||
import { DialogButton, Focusable, Menu, MenuItem, showContextMenu } from 'decky-frontend-lib';
|
||||
import { useEffect } from 'react';
|
||||
import { FaDownload, FaEllipsisH } from 'react-icons/fa';
|
||||
import {
|
||||
DialogBody,
|
||||
DialogButton,
|
||||
DialogControlsSection,
|
||||
GamepadEvent,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ReorderableEntry,
|
||||
ReorderableList,
|
||||
showContextMenu,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
|
||||
|
||||
import { requestPluginInstall } from '../../../../store';
|
||||
import { InstallType } from '../../../../plugin';
|
||||
import {
|
||||
StorePluginVersion,
|
||||
getPluginList,
|
||||
requestMultiplePluginInstalls,
|
||||
requestPluginInstall,
|
||||
} from '../../../../store';
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
import PluginListLabel from './PluginListLabel';
|
||||
|
||||
async function reinstallPlugin(pluginName: string, currentVersion?: string) {
|
||||
const serverData = await getPluginList();
|
||||
const remotePlugin = serverData?.find((x) => x.name == pluginName);
|
||||
if (remotePlugin && remotePlugin.versions?.length > 0) {
|
||||
const currentVersionData = remotePlugin.versions.find((version) => version.name == currentVersion);
|
||||
if (currentVersionData) requestPluginInstall(pluginName, currentVersionData, InstallType.REINSTALL);
|
||||
}
|
||||
}
|
||||
|
||||
type PluginTableData = PluginData & { name: string; hidden: boolean; onHide(): void; onShow(): void };
|
||||
|
||||
function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// nothing to display without this data...
|
||||
if (!props.entry.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name, update, version, onHide, onShow, hidden } = props.entry.data;
|
||||
|
||||
const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
|
||||
showContextMenu(
|
||||
<Menu label={t('PluginListIndex.plugin_actions')}>
|
||||
<MenuItem
|
||||
onSelected={() => {
|
||||
try {
|
||||
fetch(`http://127.0.0.1:1337/plugins/${name}/reload`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error Reloading Plugin Backend', err);
|
||||
}
|
||||
|
||||
window.DeckyPluginLoader.importPlugin(name, version);
|
||||
}}
|
||||
>
|
||||
{t('PluginListIndex.reload')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelected={() =>
|
||||
window.DeckyPluginLoader.uninstallPlugin(
|
||||
name,
|
||||
t('PluginLoader.plugin_uninstall.title', { name }),
|
||||
t('PluginLoader.plugin_uninstall.button'),
|
||||
t('PluginLoader.plugin_uninstall.desc', { name }),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('PluginListIndex.uninstall')}
|
||||
</MenuItem>
|
||||
{hidden ? (
|
||||
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
|
||||
) : (
|
||||
<MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem>
|
||||
)}
|
||||
</Menu>,
|
||||
e.currentTarget ?? window,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{update ? (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => requestPluginInstall(name, update, InstallType.UPDATE)}
|
||||
onOKButton={() => requestPluginInstall(name, update, InstallType.UPDATE)}
|
||||
>
|
||||
<div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{t('PluginListIndex.update_to', { name: update.name })}
|
||||
<FaDownload style={{ paddingLeft: '1rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
) : (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => reinstallPlugin(name, version)}
|
||||
onOKButton={() => reinstallPlugin(name, version)}
|
||||
>
|
||||
<div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{t('PluginListIndex.reinstall')}
|
||||
<FaRecycle style={{ paddingLeft: '1rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
)}
|
||||
<DialogButton
|
||||
style={{
|
||||
height: '40px',
|
||||
width: '40px',
|
||||
padding: '10px 12px',
|
||||
minWidth: '40px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={showCtxMenu}
|
||||
onOKButton={showCtxMenu}
|
||||
>
|
||||
<FaEllipsisH />
|
||||
</DialogButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type PluginData = {
|
||||
update?: StorePluginVersion;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export default function PluginList() {
|
||||
const { plugins, updates } = useDeckyState();
|
||||
const { plugins, updates, pluginOrder, setPluginOrder, hiddenPlugins } = useDeckyState();
|
||||
const [_, setPluginOrderSetting] = useSetting<string[]>(
|
||||
'pluginOrder',
|
||||
plugins.map((plugin) => plugin.name),
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
window.DeckyPluginLoader.checkPluginUpdates();
|
||||
}, []);
|
||||
|
||||
const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]);
|
||||
const hiddenPluginsService = window.DeckyPluginLoader.hiddenPluginsService;
|
||||
|
||||
useEffect(() => {
|
||||
setPluginEntries(
|
||||
plugins.map(({ name, version }) => {
|
||||
const hidden = hiddenPlugins.includes(name);
|
||||
|
||||
return {
|
||||
label: <PluginListLabel name={name} hidden={hidden} version={version} />,
|
||||
position: pluginOrder.indexOf(name),
|
||||
data: {
|
||||
name,
|
||||
hidden,
|
||||
version,
|
||||
update: updates?.get(name),
|
||||
onHide: () => hiddenPluginsService.update([...hiddenPlugins, name]),
|
||||
onShow: () => hiddenPluginsService.update(hiddenPlugins.filter((pluginName) => name !== pluginName)),
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
}, [plugins, updates, hiddenPlugins]);
|
||||
|
||||
if (plugins.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<p>No plugins installed</p>
|
||||
<p>{t('PluginListIndex.no_plugin')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function onSave(entries: ReorderableEntry<PluginTableData>[]) {
|
||||
const newOrder = entries.map((entry) => entry.data!.name);
|
||||
console.log(newOrder);
|
||||
setPluginOrder(newOrder);
|
||||
setPluginOrderSetting(newOrder);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul style={{ listStyleType: 'none' }}>
|
||||
{plugins.map(({ name, version }) => {
|
||||
const update = updates?.get(name);
|
||||
return (
|
||||
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
|
||||
<span>
|
||||
{name} {version}
|
||||
</span>
|
||||
<Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
|
||||
{update && (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => requestPluginInstall(name, update)}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
Update to {update.name}
|
||||
<FaDownload style={{ paddingLeft: '2rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
)}
|
||||
<DialogButton
|
||||
style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }}
|
||||
onClick={(e: MouseEvent) =>
|
||||
showContextMenu(
|
||||
<Menu label="Plugin Actions">
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name, version)}>
|
||||
Reload
|
||||
</MenuItem>
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem>
|
||||
</Menu>,
|
||||
e.currentTarget ?? window,
|
||||
)
|
||||
}
|
||||
>
|
||||
<FaEllipsisH />
|
||||
</DialogButton>
|
||||
</Focusable>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<DialogBody>
|
||||
{updates && updates.size > 0 && (
|
||||
<DialogButton
|
||||
onClick={() =>
|
||||
requestMultiplePluginInstalls(
|
||||
[...updates.entries()].map(([plugin, selectedVer]) => ({
|
||||
installType: InstallType.UPDATE,
|
||||
plugin,
|
||||
selectedVer,
|
||||
})),
|
||||
)
|
||||
}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '57px',
|
||||
right: '2.8vw',
|
||||
width: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{t('PluginListIndex.update_all', { count: updates.size })}
|
||||
<FaDownload style={{ paddingLeft: '1rem' }} />
|
||||
</DialogButton>
|
||||
)}
|
||||
<DialogControlsSection style={{ marginTop: 0 }}>
|
||||
<ReorderableList<PluginTableData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} />
|
||||
</DialogControlsSection>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,234 +1,175 @@
|
||||
import {
|
||||
DialogButton,
|
||||
ButtonItem,
|
||||
Dropdown,
|
||||
Focusable,
|
||||
QuickAccessTab,
|
||||
Router,
|
||||
PanelSectionRow,
|
||||
SingleDropdownOption,
|
||||
SuspensefulImage,
|
||||
joinClassNames,
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { FC, useRef, useState } from 'react';
|
||||
import { CSSProperties, FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
LegacyStorePlugin,
|
||||
StorePlugin,
|
||||
StorePluginVersion,
|
||||
isLegacyPlugin,
|
||||
requestLegacyPluginInstall,
|
||||
requestPluginInstall,
|
||||
} from '../../store';
|
||||
import { InstallType } from '../../plugin';
|
||||
import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store';
|
||||
|
||||
interface PluginCardProps {
|
||||
plugin: StorePlugin | LegacyStorePlugin;
|
||||
plugin: StorePlugin;
|
||||
}
|
||||
|
||||
const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
const [selectedOption, setSelectedOption] = useState<number>(0);
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const root = plugin.tags.some((tag) => tag === 'root');
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="deckyStoreCard"
|
||||
style={{
|
||||
padding: '30px',
|
||||
paddingTop: '10px',
|
||||
paddingBottom: '10px',
|
||||
marginLeft: '20px',
|
||||
marginRight: '20px',
|
||||
marginBottom: '20px',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
{/* TODO: abstract this messy focus hackiness into a custom component in lib */}
|
||||
<Focusable
|
||||
className="deckyStoreCard"
|
||||
ref={containerRef}
|
||||
onActivate={(_: CustomEvent) => {
|
||||
buttonRef.current!.focus();
|
||||
}}
|
||||
onCancel={(_: CustomEvent) => {
|
||||
if (containerRef.current!.querySelectorAll('* :focus').length === 0) {
|
||||
Router.NavigateBackOrOpenMenu();
|
||||
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
|
||||
} else {
|
||||
containerRef.current!.focus();
|
||||
}
|
||||
}}
|
||||
<div
|
||||
className="deckyStoreCardImageContainer"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#ACB2C924',
|
||||
height: 'unset',
|
||||
marginBottom: 'unset',
|
||||
// boxShadow: var(--gpShadow-Medium);
|
||||
scrollSnapAlign: 'start',
|
||||
boxSizing: 'border-box',
|
||||
width: '320px',
|
||||
height: '200px',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div className="deckyStoreCardHeader" style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<a
|
||||
style={{ fontSize: '18pt', padding: '10px' }}
|
||||
className={joinClassNames(staticClasses.Text)}
|
||||
// onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)}
|
||||
<SuspensefulImage
|
||||
className="deckyStoreCardImage"
|
||||
suspenseHeight="200px"
|
||||
suspenseWidth="320px"
|
||||
style={{
|
||||
width: '320px',
|
||||
height: '200px',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
src={plugin.image_url}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="deckyStoreCardInfo"
|
||||
style={{
|
||||
width: 'calc(100% - 320px)', // The calc is here so that the info section doesn't expand into the image
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
marginLeft: '1em',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<span
|
||||
className="deckyStoreCardTitle"
|
||||
style={{
|
||||
fontSize: '1.25em',
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
width: '90%',
|
||||
}}
|
||||
>
|
||||
{isLegacyPlugin(plugin) ? (
|
||||
<div className="deckyStoreCardNameContainer">
|
||||
<span className="deckyStoreCardLegacyRepoOwner" style={{ color: 'grey' }}>
|
||||
{plugin.artifact.split('/')[0]}/
|
||||
</span>
|
||||
{plugin.artifact.split('/')[1]}
|
||||
</div>
|
||||
{plugin.name}
|
||||
</span>
|
||||
<span
|
||||
className="deckyStoreCardAuthor"
|
||||
style={{
|
||||
marginRight: 'auto',
|
||||
fontSize: '1em',
|
||||
}}
|
||||
>
|
||||
{plugin.author}
|
||||
</span>
|
||||
<span
|
||||
className="deckyStoreCardDescription"
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
color: '#969696',
|
||||
WebkitLineClamp: root ? '2' : '3',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
}}
|
||||
>
|
||||
{plugin.description ? (
|
||||
plugin.description
|
||||
) : (
|
||||
plugin.name
|
||||
<span>
|
||||
<i style={{ color: '#666' }}>{t('PluginCard.plugin_no_desc')}</i>
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
className="deckyStoreCardBody"
|
||||
>
|
||||
<SuspensefulImage
|
||||
className="deckyStoreCardImage"
|
||||
suspenseWidth="256px"
|
||||
style={{
|
||||
width: 'auto',
|
||||
height: '160px',
|
||||
}}
|
||||
src={
|
||||
isLegacyPlugin(plugin)
|
||||
? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.artifact.replace(
|
||||
'/',
|
||||
'_',
|
||||
)}.png`
|
||||
: `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.name.replace(
|
||||
'/',
|
||||
'_',
|
||||
)}.png`
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
className="deckyStoreCardInfo"
|
||||
>
|
||||
<p
|
||||
className={joinClassNames(staticClasses.PanelSectionRow)}
|
||||
style={{ marginTop: '0px', marginLeft: '16px' }}
|
||||
>
|
||||
<span style={{ paddingLeft: '0px' }}>Author: {plugin.author}</span>
|
||||
</p>
|
||||
<p
|
||||
className={joinClassNames(staticClasses.PanelSectionRow)}
|
||||
style={{
|
||||
marginLeft: '16px',
|
||||
marginTop: '0px',
|
||||
marginBottom: '0px',
|
||||
marginRight: '16px',
|
||||
}}
|
||||
>
|
||||
<span style={{ paddingLeft: '0px' }}>{plugin.description}</span>
|
||||
</p>
|
||||
<p
|
||||
className={joinClassNames('deckyStoreCardTagsContainer', staticClasses.PanelSectionRow)}
|
||||
style={{
|
||||
padding: '0 16px',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '5px 10px',
|
||||
}}
|
||||
>
|
||||
<span style={{ padding: '5px 0' }}>Tags:</span>
|
||||
{plugin.tags.map((tag: string) => (
|
||||
<span
|
||||
className="deckyStoreCardTag"
|
||||
style={{
|
||||
padding: '5px',
|
||||
borderRadius: '5px',
|
||||
background: tag == 'root' ? '#842029' : '#ACB2C947',
|
||||
}}
|
||||
>
|
||||
{tag == 'root' ? 'Requires root' : tag}
|
||||
</span>
|
||||
))}
|
||||
{isLegacyPlugin(plugin) && (
|
||||
<span
|
||||
className="deckyStoreCardTag deckyStoreCardLegacyTag"
|
||||
style={{
|
||||
color: '#232120',
|
||||
padding: '5px',
|
||||
borderRadius: '5px',
|
||||
background: '#EDE841',
|
||||
}}
|
||||
>
|
||||
legacy
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="deckyStoreCardActionsContainer"
|
||||
style={{
|
||||
width: '100%',
|
||||
alignSelf: 'flex-end',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
>
|
||||
<Focusable
|
||||
className="deckyStoreCardActions"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
</span>
|
||||
{root && (
|
||||
<div
|
||||
className="deckyStoreCardInstallButtonContainer"
|
||||
className="deckyStoreCardDescription deckyStoreCardDescriptionRoot"
|
||||
style={{
|
||||
flex: '1',
|
||||
fontSize: '13px',
|
||||
color: '#fee75c',
|
||||
marginTop: 'auto',
|
||||
}}
|
||||
>
|
||||
<DialogButton
|
||||
className="deckyStoreCardInstallButton"
|
||||
ref={buttonRef}
|
||||
onClick={() =>
|
||||
isLegacyPlugin(plugin)
|
||||
? requestLegacyPluginInstall(plugin, Object.keys(plugin.versions)[selectedOption])
|
||||
: requestPluginInstall(plugin.name, plugin.versions[selectedOption])
|
||||
<i>{t('PluginCard.plugin_full_access')}</i>{' '}
|
||||
<a
|
||||
className="deckyStoreCardDescriptionRootLink"
|
||||
href="https://deckbrew.xyz/root"
|
||||
target="_blank"
|
||||
style={{
|
||||
color: '#fee75c',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
deckbrew.xyz/root
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="deckyStoreCardButtonRow">
|
||||
<PanelSectionRow>
|
||||
<Focusable style={{ display: 'flex', gap: '5px', padding: 0 }}>
|
||||
<div
|
||||
className="deckyStoreCardInstallContainer"
|
||||
style={
|
||||
{
|
||||
paddingTop: '0px',
|
||||
paddingBottom: '0px',
|
||||
flexGrow: 1,
|
||||
'--field-negative-horizontal-margin': 0,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
Install
|
||||
</DialogButton>
|
||||
</div>
|
||||
<div
|
||||
className="deckyStoreCardVersionDropdownContainer"
|
||||
style={{
|
||||
flex: '0.2',
|
||||
}}
|
||||
>
|
||||
<Dropdown
|
||||
rgOptions={
|
||||
(isLegacyPlugin(plugin)
|
||||
? Object.keys(plugin.versions).map((v, k) => ({
|
||||
data: k,
|
||||
label: v,
|
||||
}))
|
||||
: plugin.versions.map((version: StorePluginVersion, index) => ({
|
||||
data: index,
|
||||
label: version.name,
|
||||
}))) as SingleDropdownOption[]
|
||||
}
|
||||
strDefaultLabel={'Select a version'}
|
||||
selectedOption={selectedOption}
|
||||
onChange={({ data }) => setSelectedOption(data)}
|
||||
/>
|
||||
</div>
|
||||
</Focusable>
|
||||
<ButtonItem
|
||||
bottomSeparator="none"
|
||||
layout="below"
|
||||
onClick={() =>
|
||||
requestPluginInstall(plugin.name, plugin.versions[selectedOption], InstallType.INSTALL)
|
||||
}
|
||||
>
|
||||
<span className="deckyStoreCardInstallText">{t('PluginCard.plugin_install')}</span>
|
||||
</ButtonItem>
|
||||
</div>
|
||||
<div className="deckyStoreCardVersionContainer" style={{ minWidth: '130px' }}>
|
||||
<Dropdown
|
||||
rgOptions={
|
||||
plugin.versions.map((version: StorePluginVersion, index) => ({
|
||||
data: index,
|
||||
label: version.name,
|
||||
})) as SingleDropdownOption[]
|
||||
}
|
||||
menuLabel={t('PluginCard.plugin_version_label') as string}
|
||||
selectedOption={selectedOption}
|
||||
onChange={({ data }) => setSelectedOption(data)}
|
||||
/>
|
||||
</div>
|
||||
</Focusable>
|
||||
</PanelSectionRow>
|
||||
</div>
|
||||
</Focusable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
import { SteamSpinner } from 'decky-frontend-lib';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownOption,
|
||||
Focusable,
|
||||
PanelSectionRow,
|
||||
SteamSpinner,
|
||||
Tabs,
|
||||
TextField,
|
||||
findModule,
|
||||
} from 'decky-frontend-lib';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import logo from '../../../assets/plugin_store.png';
|
||||
import Logger from '../../logger';
|
||||
import { LegacyStorePlugin, StorePlugin, getLegacyPluginList, getPluginList } from '../../store';
|
||||
import { StorePlugin, getPluginList } from '../../store';
|
||||
import PluginCard from './PluginCard';
|
||||
|
||||
const logger = new Logger('FilePicker');
|
||||
const logger = new Logger('Store');
|
||||
|
||||
const StorePage: FC<{}> = () => {
|
||||
const [currentTabRoute, setCurrentTabRoute] = useState<string>('browse');
|
||||
const [data, setData] = useState<StorePlugin[] | null>(null);
|
||||
const [legacyData, setLegacyData] = useState<LegacyStorePlugin[] | null>(null);
|
||||
const { TabCount } = findModule((m) => {
|
||||
if (m?.TabCount && m?.TabTitle) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -17,27 +34,15 @@ const StorePage: FC<{}> = () => {
|
||||
logger.log('got data!', res);
|
||||
setData(res);
|
||||
})();
|
||||
(async () => {
|
||||
const res = await getLegacyPluginList();
|
||||
logger.log('got legacy data!', res);
|
||||
setLegacyData(res);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '40px',
|
||||
height: 'calc( 100% - 40px )',
|
||||
overflowY: 'scroll',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
marginTop: '40px',
|
||||
height: 'calc( 100% - 40px )',
|
||||
background: '#0005',
|
||||
}}
|
||||
>
|
||||
{!data ? (
|
||||
@@ -45,18 +50,198 @@ const StorePage: FC<{}> = () => {
|
||||
<SteamSpinner />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{data.map((plugin: StorePlugin) => (
|
||||
<PluginCard plugin={plugin} />
|
||||
))}
|
||||
{!legacyData ? (
|
||||
<SteamSpinner />
|
||||
) : (
|
||||
legacyData.map((plugin: LegacyStorePlugin) => <PluginCard plugin={plugin} />)
|
||||
)}
|
||||
</div>
|
||||
<Tabs
|
||||
activeTab={currentTabRoute}
|
||||
onShowTab={(tabId: string) => {
|
||||
setCurrentTabRoute(tabId);
|
||||
}}
|
||||
tabs={[
|
||||
{
|
||||
title: t('Store.store_tabs.title'),
|
||||
content: <BrowseTab children={{ data: data }} />,
|
||||
id: 'browse',
|
||||
renderTabAddon: () => <span className={TabCount}>{data.length}</span>,
|
||||
},
|
||||
{
|
||||
title: t('Store.store_tabs.about'),
|
||||
content: <AboutTab />,
|
||||
id: 'about',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sortOptions = useMemo(
|
||||
(): DropdownOption[] => [
|
||||
{ data: 1, label: t('Store.store_tabs.alph_desc') },
|
||||
{ data: 2, label: t('Store.store_tabs.alph_asce') },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
// const filterOptions = useMemo((): DropdownOption[] => [{ data: 1, label: 'All' }], []);
|
||||
|
||||
const [selectedSort, setSort] = useState<number>(sortOptions[0].data);
|
||||
// const [selectedFilter, setFilter] = useState<number>(filterOptions[0].data);
|
||||
const [searchFieldValue, setSearchValue] = useState<string>('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
.deckyStoreCardInstallContainer > .Panel {
|
||||
padding: 0;
|
||||
}
|
||||
`}</style>
|
||||
{/* This should be used once filtering is added
|
||||
|
||||
<PanelSectionRow>
|
||||
<Focusable style={{ display: 'flex', maxWidth: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '47.5%',
|
||||
}}
|
||||
>
|
||||
<span className="DialogLabel">{t("Store.store_sort.label")}</span>
|
||||
<Dropdown
|
||||
menuLabel={t("Store.store_sort.label") as string}
|
||||
rgOptions={sortOptions}
|
||||
strDefaultLabel={t("Store.store_sort.label_def") as string}
|
||||
selectedOption={selectedSort}
|
||||
onChange={(e) => setSort(e.data)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '47.5%',
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
<span className="DialogLabel">{t("Store.store_filter.label")}</span>
|
||||
<Dropdown
|
||||
menuLabel={t("Store.store_filter.label")}
|
||||
rgOptions={filterOptions}
|
||||
strDefaultLabel={t("Store.store_filter.label_def")}
|
||||
selectedOption={selectedFilter}
|
||||
onChange={(e) => setFilter(e.data)}
|
||||
/>
|
||||
</div>
|
||||
</Focusable>
|
||||
</PanelSectionRow>
|
||||
<div style={{ justifyContent: 'center', display: 'flex' }}>
|
||||
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<TextField label={t("Store.store_search.label")} value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
|
||||
</div>
|
||||
</Focusable>
|
||||
</div>
|
||||
*/}
|
||||
<PanelSectionRow>
|
||||
<Focusable style={{ display: 'flex', maxWidth: '100%' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minWidth: '100%',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
<span className="DialogLabel">{t('Store.store_sort.label')}</span>
|
||||
<Dropdown
|
||||
menuLabel={t('Store.store_sort.label') as string}
|
||||
rgOptions={sortOptions}
|
||||
strDefaultLabel={t('Store.store_sort.label_def') as string}
|
||||
selectedOption={selectedSort}
|
||||
onChange={(e) => setSort(e.data)}
|
||||
/>
|
||||
</div>
|
||||
</Focusable>
|
||||
</PanelSectionRow>
|
||||
<div style={{ justifyContent: 'center', display: 'flex' }}>
|
||||
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<TextField
|
||||
label={t('Store.store_search.label')}
|
||||
value={searchFieldValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Focusable>
|
||||
</div>
|
||||
<div>
|
||||
{data.children.data
|
||||
.filter((plugin: StorePlugin) => {
|
||||
return (
|
||||
plugin.name.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
|
||||
plugin.description.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
|
||||
plugin.author.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
|
||||
plugin.tags.some((tag: string) => tag.toLowerCase().includes(searchFieldValue.toLowerCase()))
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (selectedSort % 2 === 1) return a.name.localeCompare(b.name);
|
||||
else return b.name.localeCompare(a.name);
|
||||
})
|
||||
.map((plugin: StorePlugin) => (
|
||||
<PluginCard plugin={plugin} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AboutTab: FC<{}> = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
.deckyStoreAboutHeader {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-top: 20px;
|
||||
}
|
||||
`}</style>
|
||||
<img
|
||||
src={logo}
|
||||
style={{
|
||||
width: '256px',
|
||||
height: 'auto',
|
||||
alignSelf: 'center',
|
||||
}}
|
||||
/>
|
||||
<span className="deckyStoreAboutHeader">Testing</span>
|
||||
<span>
|
||||
{t('Store.store_testing_cta')}{' '}
|
||||
<a
|
||||
href="https://deckbrew.xyz/testing"
|
||||
target="_blank"
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
deckbrew.xyz/testing
|
||||
</a>
|
||||
</span>
|
||||
<span className="deckyStoreAboutHeader">{t('Store.store_contrib.label')}</span>
|
||||
<span>{t('Store.store_contrib.desc')}</span>
|
||||
<span className="deckyStoreAboutHeader">{t('Store.store_source.label')}</span>
|
||||
<span>{t('Store.store_source.desc')}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { findModuleChild, sleep } from 'decky-frontend-lib';
|
||||
import { FaReact } from 'react-icons/fa';
|
||||
|
||||
import Logger from './logger';
|
||||
import { getSetting } from './utils/settings';
|
||||
import TranslationHelper, { TranslationClass } from './utils/TranslationHelper';
|
||||
|
||||
const logger = new Logger('DeveloperMode');
|
||||
|
||||
let removeSettingsObserver: () => void = () => {};
|
||||
|
||||
export async function setShowValveInternal(show: boolean) {
|
||||
let settingsMod: any;
|
||||
while (!settingsMod) {
|
||||
settingsMod = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (typeof m[prop]?.settings?.bIsValveEmail !== 'undefined') return m[prop];
|
||||
}
|
||||
});
|
||||
if (!settingsMod) {
|
||||
logger.debug('[ValveInternal] waiting for settingsMod');
|
||||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (show) {
|
||||
removeSettingsObserver = settingsMod[
|
||||
Object.getOwnPropertySymbols(settingsMod).find((x) => x.toString() == 'Symbol(mobx administration)') as any
|
||||
].observe((e: any) => {
|
||||
e.newValue.bIsValveEmail = true;
|
||||
});
|
||||
settingsMod.m_Settings.bIsValveEmail = true;
|
||||
logger.log('Enabled Valve Internal menu');
|
||||
} else {
|
||||
removeSettingsObserver();
|
||||
settingsMod.m_Settings.bIsValveEmail = false;
|
||||
logger.log('Disabled Valve Internal menu');
|
||||
}
|
||||
}
|
||||
|
||||
export async function setShouldConnectToReactDevTools(enable: boolean) {
|
||||
window.DeckyPluginLoader.toaster.toast({
|
||||
title: enable ? (
|
||||
<TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'enabling'} />
|
||||
) : (
|
||||
<TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'disabling'} />
|
||||
),
|
||||
body: <TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'5secreload'} />,
|
||||
icon: <FaReact />,
|
||||
});
|
||||
await sleep(5000);
|
||||
return enable
|
||||
? window.DeckyPluginLoader.callServerMethod('enable_rdt')
|
||||
: window.DeckyPluginLoader.callServerMethod('disable_rdt');
|
||||
}
|
||||
|
||||
export async function startup() {
|
||||
const isValveInternalEnabled = await getSetting('developer.valve_internal', false);
|
||||
const isRDTEnabled = await getSetting('developer.rdt.enabled', false);
|
||||
|
||||
if (isValveInternalEnabled) setShowValveInternal(isValveInternalEnabled);
|
||||
|
||||
if ((isRDTEnabled && !window.deckyHasConnectedRDT) || (!isRDTEnabled && window.deckyHasConnectedRDT))
|
||||
setShouldConnectToReactDevTools(isRDTEnabled);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { DeckyState } from './components/DeckyState';
|
||||
import { getSetting, setSetting } from './utils/settings';
|
||||
|
||||
/**
|
||||
* A Service class for managing the state and actions related to the hidden plugins feature
|
||||
*
|
||||
* It's mostly responsible for sending setting updates to the server and keeping the local state in sync.
|
||||
*/
|
||||
export class HiddenPluginsService {
|
||||
constructor(private deckyState: DeckyState) {}
|
||||
|
||||
init() {
|
||||
getSetting<string[]>('hiddenPlugins', []).then((hiddenPlugins) => {
|
||||
this.deckyState.setHiddenPlugins(hiddenPlugins);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the new hidden plugins list to the server and persists it locally in the decky state
|
||||
*
|
||||
* @param hiddenPlugins The new list of hidden plugins
|
||||
*/
|
||||
async update(hiddenPlugins: string[]) {
|
||||
await setSetting('hiddenPlugins', hiddenPlugins);
|
||||
this.deckyState.setHiddenPlugins(hiddenPlugins);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the state of hidden plugins in the local state
|
||||
*/
|
||||
async invalidate() {
|
||||
this.deckyState.setHiddenPlugins(await getSetting('hiddenPlugins', []));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Sets up DFL, then loads start.ts which starts up the loader
|
||||
(async () => {
|
||||
console.debug('Setting up decky-frontend-lib...');
|
||||
window.DFL = await import('decky-frontend-lib');
|
||||
await import('./start');
|
||||
})();
|
||||
@@ -0,0 +1,51 @@
|
||||
import { DeckyState } from './components/DeckyState';
|
||||
import { getSetting, setSetting } from './utils/settings';
|
||||
|
||||
export interface NotificationSettings {
|
||||
deckyUpdates: boolean;
|
||||
pluginUpdates: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_NOTIFICATION_SETTINGS: NotificationSettings = {
|
||||
deckyUpdates: true,
|
||||
pluginUpdates: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* A Service class for managing the notification settings
|
||||
*
|
||||
* It's mostly responsible for sending setting updates to the server and keeping the local state in sync.
|
||||
*/
|
||||
export class NotificationService {
|
||||
constructor(private deckyState: DeckyState) {}
|
||||
|
||||
async init() {
|
||||
const notificationSettings = await getSetting<Partial<NotificationSettings>>('notificationSettings', {});
|
||||
|
||||
// Adding a fallback to the default settings to be backwards compatible if we ever add new notification settings
|
||||
this.deckyState.setNotificationSettings({
|
||||
...DEFAULT_NOTIFICATION_SETTINGS,
|
||||
...notificationSettings,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the new notification settings to the server and persists it locally in the decky state
|
||||
*
|
||||
* @param notificationSettings The new notification settings
|
||||
*/
|
||||
async update(notificationSettings: NotificationSettings) {
|
||||
await setSetting('notificationSettings', notificationSettings);
|
||||
this.deckyState.setNotificationSettings(notificationSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* For a specific event, returns true if a notification should be shown
|
||||
*
|
||||
* @param event The notification event that should be checked
|
||||
* @returns true if the notification should be shown
|
||||
*/
|
||||
shouldNotify(event: keyof NotificationSettings) {
|
||||
return this.deckyState.publicState().notificationSettings[event];
|
||||
}
|
||||
}
|
||||
+175
-43
@@ -1,48 +1,57 @@
|
||||
import {
|
||||
ConfirmModal,
|
||||
ModalRoot,
|
||||
PanelSection,
|
||||
PanelSectionRow,
|
||||
Patch,
|
||||
QuickAccessTab,
|
||||
Router,
|
||||
quickAccessMenuClasses,
|
||||
showModal,
|
||||
sleep,
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { lazy } from 'react';
|
||||
import { FaPlug } from 'react-icons/fa';
|
||||
import { FC, lazy } from 'react';
|
||||
import { FaExclamationCircle, FaPlug } from 'react-icons/fa';
|
||||
|
||||
import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
|
||||
import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from './components/DeckyState';
|
||||
import LegacyPlugin from './components/LegacyPlugin';
|
||||
import { File, FileSelectionType } from './components/modals/filepicker';
|
||||
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
|
||||
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
|
||||
import PluginInstallModal from './components/modals/PluginInstallModal';
|
||||
import PluginUninstallModal from './components/modals/PluginUninstallModal';
|
||||
import NotificationBadge from './components/NotificationBadge';
|
||||
import PluginView from './components/PluginView';
|
||||
import WithSuspense from './components/WithSuspense';
|
||||
import { HiddenPluginsService } from './hidden-plugins-service';
|
||||
import Logger from './logger';
|
||||
import { Plugin } from './plugin';
|
||||
import { NotificationService } from './notification-service';
|
||||
import { InstallType, Plugin } from './plugin';
|
||||
import RouterHook from './router-hook';
|
||||
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
|
||||
import { checkForUpdates } from './store';
|
||||
import TabsHook from './tabs-hook';
|
||||
import OldTabsHook from './tabs-hook.old';
|
||||
import Toaster from './toaster';
|
||||
import { VerInfo, callUpdaterMethod } from './updater';
|
||||
import { getSetting, setSetting } from './utils/settings';
|
||||
import TranslationHelper, { TranslationClass } from './utils/TranslationHelper';
|
||||
|
||||
const StorePage = lazy(() => import('./components/store/Store'));
|
||||
const SettingsPage = lazy(() => import('./components/settings'));
|
||||
|
||||
const FilePicker = lazy(() => import('./components/modals/filepicker'));
|
||||
|
||||
declare global {
|
||||
interface Window {}
|
||||
}
|
||||
|
||||
class PluginLoader extends Logger {
|
||||
private plugins: Plugin[] = [];
|
||||
private tabsHook: TabsHook = new TabsHook();
|
||||
private tabsHook: TabsHook | OldTabsHook = document.title == 'SP' ? new OldTabsHook() : new TabsHook();
|
||||
// private windowHook: WindowHook = new WindowHook();
|
||||
private routerHook: RouterHook = new RouterHook();
|
||||
private toaster: Toaster = new Toaster();
|
||||
public toaster: Toaster = new Toaster();
|
||||
private deckyState: DeckyState = new DeckyState();
|
||||
|
||||
public hiddenPluginsService = new HiddenPluginsService(this.deckyState);
|
||||
public notificationService = new NotificationService(this.deckyState);
|
||||
|
||||
private reloadLock: boolean = false;
|
||||
// stores a list of plugin names which requested to be reloaded
|
||||
private pluginReloadQueue: { name: string; version?: string }[] = [];
|
||||
@@ -51,6 +60,7 @@ class PluginLoader extends Logger {
|
||||
|
||||
constructor() {
|
||||
super(PluginLoader.name);
|
||||
this.tabsHook.init();
|
||||
this.log('Initialized');
|
||||
|
||||
const TabBadge = () => {
|
||||
@@ -89,11 +99,21 @@ class PluginLoader extends Logger {
|
||||
);
|
||||
});
|
||||
|
||||
initSteamFixes();
|
||||
|
||||
initFilepickerPatches();
|
||||
|
||||
this.getUserInfo();
|
||||
|
||||
this.updateVersion();
|
||||
}
|
||||
|
||||
public async getUserInfo() {
|
||||
const userInfo = (await this.callServerMethod('get_user_info')).result as UserInfo;
|
||||
setSetting('user_info.user_name', userInfo.username);
|
||||
setSetting('user_info.user_home', userInfo.path);
|
||||
}
|
||||
|
||||
public async updateVersion() {
|
||||
const versionInfo = (await callUpdaterMethod('get_version')).result as VerInfo;
|
||||
this.deckyState.setVersionInfo(versionInfo);
|
||||
@@ -104,12 +124,20 @@ class PluginLoader extends Logger {
|
||||
public async notifyUpdates() {
|
||||
const versionInfo = await this.updateVersion();
|
||||
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
|
||||
this.toaster.toast({
|
||||
title: 'Decky',
|
||||
body: `Update to ${versionInfo?.remote?.tag_name} available!`,
|
||||
onClick: () => Router.Navigate('/decky/settings'),
|
||||
});
|
||||
this.deckyState.setHasLoaderUpdate(true);
|
||||
if (this.notificationService.shouldNotify('deckyUpdates')) {
|
||||
this.toaster.toast({
|
||||
title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
|
||||
body: (
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_LOADER}
|
||||
trans_text="decky_update_available"
|
||||
i18n_args={{ tag_name: versionInfo?.remote?.tag_name }}
|
||||
/>
|
||||
),
|
||||
onClick: () => Router.Navigate('/decky/settings'),
|
||||
});
|
||||
}
|
||||
}
|
||||
await sleep(7000);
|
||||
await this.notifyPluginUpdates();
|
||||
@@ -123,44 +151,57 @@ class PluginLoader extends Logger {
|
||||
|
||||
public async notifyPluginUpdates() {
|
||||
const updates = await this.checkPluginUpdates();
|
||||
if (updates?.size > 0) {
|
||||
if (updates?.size > 0 && this.notificationService.shouldNotify('pluginUpdates')) {
|
||||
this.toaster.toast({
|
||||
title: 'Decky',
|
||||
body: `Updates available for ${updates.size} plugin${updates.size > 1 ? 's' : ''}!`,
|
||||
title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
|
||||
body: (
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_LOADER}
|
||||
trans_text="plugin_update"
|
||||
i18n_args={{ count: updates.size }}
|
||||
/>
|
||||
),
|
||||
onClick: () => Router.Navigate('/decky/settings/plugins'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
|
||||
public addPluginInstallPrompt(
|
||||
artifact: string,
|
||||
version: string,
|
||||
request_id: string,
|
||||
hash: string,
|
||||
install_type: number,
|
||||
) {
|
||||
showModal(
|
||||
<PluginInstallModal
|
||||
artifact={artifact}
|
||||
version={version}
|
||||
hash={hash}
|
||||
installType={install_type}
|
||||
onOK={() => this.callServerMethod('confirm_plugin_install', { request_id })}
|
||||
onCancel={() => this.callServerMethod('cancel_plugin_install', { request_id })}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
public uninstallPlugin(name: string) {
|
||||
public addMultiplePluginsInstallPrompt(
|
||||
request_id: string,
|
||||
requests: { name: string; version: string; hash: string; install_type: InstallType }[],
|
||||
) {
|
||||
showModal(
|
||||
<ConfirmModal
|
||||
onOK={async () => {
|
||||
await this.callServerMethod('uninstall_plugin', { name });
|
||||
}}
|
||||
onCancel={() => {
|
||||
// do nothing
|
||||
}}
|
||||
>
|
||||
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
|
||||
Uninstall {name}?
|
||||
</div>
|
||||
</ConfirmModal>,
|
||||
<MultiplePluginsInstallModal
|
||||
requests={requests}
|
||||
onOK={() => this.callServerMethod('confirm_plugin_install', { request_id })}
|
||||
onCancel={() => this.callServerMethod('cancel_plugin_install', { request_id })}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
public uninstallPlugin(name: string, title: string, buttonText: string, description: string) {
|
||||
showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
|
||||
}
|
||||
|
||||
public hasPlugin(name: string) {
|
||||
return Boolean(this.plugins.find((plugin) => plugin.name == name));
|
||||
}
|
||||
@@ -172,14 +213,31 @@ class PluginLoader extends Logger {
|
||||
}
|
||||
}
|
||||
|
||||
public init() {
|
||||
getSetting('developer.enabled', false).then((val) => {
|
||||
if (val) import('./developer').then((developer) => developer.startup());
|
||||
});
|
||||
|
||||
//* Grab and set plugin order
|
||||
getSetting<string[]>('pluginOrder', []).then((pluginOrder) => {
|
||||
console.log(pluginOrder);
|
||||
this.deckyState.setPluginOrder(pluginOrder);
|
||||
});
|
||||
|
||||
this.hiddenPluginsService.init();
|
||||
this.notificationService.init();
|
||||
}
|
||||
|
||||
public deinit() {
|
||||
this.routerHook.removeRoute('/decky/store');
|
||||
this.routerHook.removeRoute('/decky/settings');
|
||||
deinitSteamFixes();
|
||||
deinitFilepickerPatches();
|
||||
this.focusWorkaroundPatch?.unpatch();
|
||||
}
|
||||
|
||||
public unloadPlugin(name: string) {
|
||||
console.log('Plugin List: ', this.plugins);
|
||||
const plugin = this.plugins.find((plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''));
|
||||
plugin?.onDismount?.();
|
||||
this.plugins = this.plugins.filter((p) => p !== plugin);
|
||||
@@ -225,14 +283,62 @@ class PluginLoader extends Logger {
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
let plugin_export = await eval(await res.text());
|
||||
let plugin = plugin_export(this.createPluginAPI(name));
|
||||
this.plugins.push({
|
||||
...plugin,
|
||||
name: name,
|
||||
version: version,
|
||||
});
|
||||
try {
|
||||
let plugin_export = await eval(await res.text());
|
||||
let plugin = plugin_export(this.createPluginAPI(name));
|
||||
this.plugins.push({
|
||||
...plugin,
|
||||
name: name,
|
||||
version: version,
|
||||
});
|
||||
} catch (e) {
|
||||
this.error('Error loading plugin ' + name, e);
|
||||
const TheError: FC<{}> = () => (
|
||||
<PanelSection>
|
||||
<PanelSectionRow>
|
||||
<div
|
||||
className={quickAccessMenuClasses.FriendsTitle}
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
>
|
||||
<TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="error" />
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
<PanelSectionRow>
|
||||
<pre style={{ overflowX: 'scroll' }}>
|
||||
<code>{e instanceof Error ? e.stack : JSON.stringify(e)}</code>
|
||||
</pre>
|
||||
</PanelSectionRow>
|
||||
<PanelSectionRow>
|
||||
<div className={quickAccessMenuClasses.Text}>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_LOADER}
|
||||
trans_text="plugin_error_uninstall"
|
||||
i18n_args={{ name: name }}
|
||||
/>
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
</PanelSection>
|
||||
);
|
||||
this.plugins.push({
|
||||
name: name,
|
||||
version: version,
|
||||
content: <TheError />,
|
||||
icon: <FaExclamationCircle />,
|
||||
});
|
||||
this.toaster.toast({
|
||||
title: (
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_LOADER}
|
||||
trans_text="plugin_load_error.toast"
|
||||
i18n_args={{ name: name }}
|
||||
/>
|
||||
),
|
||||
body: '' + e,
|
||||
icon: <FaExclamationCircle />,
|
||||
});
|
||||
}
|
||||
} else throw new Error(`${name} frontend_bundle not OK`);
|
||||
}
|
||||
|
||||
@@ -261,8 +367,26 @@ class PluginLoader extends Logger {
|
||||
|
||||
openFilePicker(
|
||||
startPath: string,
|
||||
includeFiles?: boolean,
|
||||
selectFiles?: boolean,
|
||||
regex?: RegExp,
|
||||
): Promise<{ path: string; realpath: string }> {
|
||||
if (selectFiles) {
|
||||
return this.openFilePickerV2(FileSelectionType.FILE, startPath, true, true, regex);
|
||||
} else {
|
||||
return this.openFilePickerV2(FileSelectionType.FOLDER, startPath, false, true, regex);
|
||||
}
|
||||
}
|
||||
|
||||
openFilePickerV2(
|
||||
select: FileSelectionType,
|
||||
startPath: string,
|
||||
includeFiles?: boolean,
|
||||
includeFolders?: boolean,
|
||||
filter?: RegExp | ((file: File) => boolean),
|
||||
extensions?: string[],
|
||||
showHiddenFiles?: boolean,
|
||||
allowAllFiles?: boolean,
|
||||
max?: number,
|
||||
): Promise<{ path: string; realpath: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const Content = ({ closeModal }: { closeModal?: () => void }) => (
|
||||
@@ -277,9 +401,15 @@ class PluginLoader extends Logger {
|
||||
<FilePicker
|
||||
startPath={startPath}
|
||||
includeFiles={includeFiles}
|
||||
regex={regex}
|
||||
includeFolders={includeFolders}
|
||||
filter={filter}
|
||||
validFileExtensions={extensions}
|
||||
allowAllFiles={allowAllFiles}
|
||||
defaultHidden={showHiddenFiles}
|
||||
onSubmit={resolve}
|
||||
closeModal={closeModal}
|
||||
fileSelType={select}
|
||||
max={max}
|
||||
/>
|
||||
</WithSuspense>
|
||||
</ModalRoot>
|
||||
@@ -294,6 +424,7 @@ class PluginLoader extends Logger {
|
||||
toaster: this.toaster,
|
||||
callServerMethod: this.callServerMethod,
|
||||
openFilePicker: this.openFilePicker,
|
||||
openFilePickerV2: this.openFilePickerV2,
|
||||
async callPluginMethod(methodName: string, args = {}) {
|
||||
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
|
||||
method: 'POST',
|
||||
@@ -312,6 +443,7 @@ class PluginLoader extends Logger {
|
||||
fetchNoCors(url: string, request: any = {}) {
|
||||
let args = { method: 'POST', headers: {} };
|
||||
const req = { ...args, ...request, url, data: request.body };
|
||||
req?.body && delete req.body;
|
||||
return this.callServerMethod('http_request', req);
|
||||
},
|
||||
executeInTab(tab: string, runAsync: boolean, code: string) {
|
||||
|
||||
@@ -6,3 +6,9 @@ export interface Plugin {
|
||||
onDismount?(): void;
|
||||
alwaysRender?: boolean;
|
||||
}
|
||||
|
||||
export enum InstallType {
|
||||
INSTALL,
|
||||
REINSTALL,
|
||||
UPDATE,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Patch, afterPatch, findModuleChild } from 'decky-frontend-lib';
|
||||
import { ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
|
||||
import { FC, ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
|
||||
import type { Route } from 'react-router';
|
||||
|
||||
import {
|
||||
DeckyGlobalComponentsState,
|
||||
DeckyGlobalComponentsStateContextProvider,
|
||||
useDeckyGlobalComponentsState,
|
||||
} from './components/DeckyGlobalComponentsState';
|
||||
import {
|
||||
DeckyRouterState,
|
||||
DeckyRouterStateContextProvider,
|
||||
@@ -22,8 +27,10 @@ class RouterHook extends Logger {
|
||||
private memoizedRouter: any;
|
||||
private gamepadWrapper: any;
|
||||
private routerState: DeckyRouterState = new DeckyRouterState();
|
||||
private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
|
||||
private wrapperPatch: Patch;
|
||||
private routerPatch?: Patch;
|
||||
public routes?: any[];
|
||||
|
||||
constructor() {
|
||||
super('RouterHook');
|
||||
@@ -42,24 +49,28 @@ class RouterHook extends Logger {
|
||||
|
||||
let Route: new () => Route;
|
||||
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
||||
let toReplace = new Map<string, ReactNode>();
|
||||
const DeckyWrapper = ({ children }: { children: ReactElement }) => {
|
||||
const { routes, routePatches } = useDeckyRouterState();
|
||||
|
||||
const routeList = children.props.children[0].props.children;
|
||||
|
||||
const processList = (
|
||||
routeList: any[],
|
||||
routes: Map<string, RouterEntry> | null,
|
||||
routePatches: Map<string, Set<RoutePatch>>,
|
||||
save: boolean,
|
||||
) => {
|
||||
this.debug('Route list: ', routeList);
|
||||
if (save) this.routes = routeList;
|
||||
let routerIndex = routeList.length;
|
||||
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
|
||||
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
|
||||
const newRouterArray: ReactElement[] = [];
|
||||
routes.forEach(({ component, props }, path) => {
|
||||
newRouterArray.push(
|
||||
<Route path={path} {...props}>
|
||||
{createElement(component)}
|
||||
</Route>,
|
||||
);
|
||||
});
|
||||
routeList[routerIndex] = newRouterArray;
|
||||
if (routes) {
|
||||
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
|
||||
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
|
||||
const newRouterArray: ReactElement[] = [];
|
||||
routes.forEach(({ component, props }, path) => {
|
||||
newRouterArray.push(
|
||||
<Route path={path} {...props}>
|
||||
{createElement(component)}
|
||||
</Route>,
|
||||
);
|
||||
});
|
||||
routeList[routerIndex] = newRouterArray;
|
||||
}
|
||||
}
|
||||
routeList.forEach((route: Route, index: number) => {
|
||||
const replaced = toReplace.get(route?.props?.path as string);
|
||||
@@ -85,19 +96,38 @@ class RouterHook extends Logger {
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
let toReplace = new Map<string, ReactNode>();
|
||||
const DeckyWrapper = ({ children }: { children: ReactElement }) => {
|
||||
const { routes, routePatches } = useDeckyRouterState();
|
||||
const mainRouteList = children.props.children[0].props.children;
|
||||
const ingameRouteList = children.props.children[1].props.children; // /appoverlay and /apprunning
|
||||
processList(mainRouteList, routes, routePatches, true);
|
||||
processList(ingameRouteList, null, routePatches, false);
|
||||
|
||||
this.debug('Rerendered routes list');
|
||||
return children;
|
||||
};
|
||||
|
||||
let renderedComponents: ReactElement[] = [];
|
||||
|
||||
const DeckyGlobalComponentsWrapper = () => {
|
||||
const { components } = useDeckyGlobalComponentsState();
|
||||
if (renderedComponents.length != components.size) {
|
||||
this.debug('Rerendering global components');
|
||||
renderedComponents = Array.from(components.values()).map((GComponent) => <GComponent />);
|
||||
}
|
||||
return <>{renderedComponents}</>;
|
||||
};
|
||||
|
||||
this.wrapperPatch = afterPatch(this.gamepadWrapper, 'render', (_: any, ret: any) => {
|
||||
if (ret?.props?.children?.props?.children?.length == 5) {
|
||||
if (
|
||||
ret.props.children.props.children[2]?.props?.children?.[0]?.type?.type
|
||||
?.toString()
|
||||
?.includes('GamepadUI.Settings.Root()')
|
||||
) {
|
||||
if (ret?.props?.children?.props?.children?.length == 5 || ret?.props?.children?.props?.children?.length == 4) {
|
||||
const idx = ret?.props?.children?.props?.children?.length == 4 ? 1 : 2;
|
||||
const potentialSettingsRootString =
|
||||
ret.props.children.props.children[idx]?.props?.children?.[0]?.type?.type?.toString() || '';
|
||||
if (potentialSettingsRootString?.includes('Settings.Root()')) {
|
||||
if (!this.router) {
|
||||
this.router = ret.props.children.props.children[2]?.props?.children?.[0]?.type;
|
||||
this.router = ret.props.children.props.children[idx]?.props?.children?.[0]?.type;
|
||||
this.routerPatch = afterPatch(this.router, 'type', (_: any, ret: any) => {
|
||||
if (!Route)
|
||||
Route = ret.props.children[0].props.children.find((x: any) => x.props.path == '/createaccount').type;
|
||||
@@ -111,7 +141,12 @@ class RouterHook extends Logger {
|
||||
this.memoizedRouter = memo(this.router.type);
|
||||
this.memoizedRouter.isDeckyRouter = true;
|
||||
}
|
||||
ret.props.children.props.children[2].props.children[0].type = this.memoizedRouter;
|
||||
ret.props.children.props.children.push(
|
||||
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
|
||||
<DeckyGlobalComponentsWrapper />
|
||||
</DeckyGlobalComponentsStateContextProvider>,
|
||||
);
|
||||
ret.props.children.props.children[idx].props.children[0].type = this.memoizedRouter;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
@@ -126,6 +161,14 @@ class RouterHook extends Logger {
|
||||
return this.routerState.addPatch(path, patch);
|
||||
}
|
||||
|
||||
addGlobalComponent(name: string, component: FC) {
|
||||
this.globalComponentsState.addComponent(name, component);
|
||||
}
|
||||
|
||||
removeGlobalComponent(name: string) {
|
||||
this.globalComponentsState.removeComponent(name);
|
||||
}
|
||||
|
||||
removePatch(path: string, patch: RoutePatch) {
|
||||
this.routerState.removePatch(path, patch);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ButtonItem, CommonUIModule, webpackCache } from 'decky-frontend-lib';
|
||||
import { forwardRef } from 'react';
|
||||
import i18n from 'i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import PluginLoader from './plugin-loader';
|
||||
import { DeckyUpdater } from './updater';
|
||||
@@ -11,39 +12,46 @@ declare global {
|
||||
importDeckyPlugin: Function;
|
||||
syncDeckyPlugins: Function;
|
||||
deckyHasLoaded: boolean;
|
||||
deckyHasConnectedRDT?: boolean;
|
||||
deckyAuthToken: string;
|
||||
webpackJsonp: any;
|
||||
DFL?: any;
|
||||
}
|
||||
}
|
||||
|
||||
// HACK to fix plugins using webpack v4 push
|
||||
|
||||
const v4Cache = {};
|
||||
for (let m of Object.keys(webpackCache)) {
|
||||
v4Cache[m] = { exports: webpackCache[m] };
|
||||
}
|
||||
|
||||
if (!window.webpackJsonp || window.webpackJsonp.deckyShimmed) {
|
||||
window.webpackJsonp = {
|
||||
deckyShimmed: true,
|
||||
push: (mod: any): any => {
|
||||
if (mod[1].get_require) return { c: v4Cache };
|
||||
},
|
||||
};
|
||||
CommonUIModule.__deckyButtonItemShim = forwardRef((props: any, ref: any) => {
|
||||
// tricks the old filter into working
|
||||
const dummy = `childrenContainerWidth:"min"`;
|
||||
return <ButtonItem ref={ref} _shim={dummy} {...props} />;
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
load: 'currentOnly',
|
||||
detection: {
|
||||
order: ['querystring', 'navigator'],
|
||||
lookupQuerystring: 'lng',
|
||||
},
|
||||
//debug: true,
|
||||
lng: navigator.language,
|
||||
fallbackLng: 'en-US',
|
||||
interpolation: {
|
||||
escapeValue: true,
|
||||
},
|
||||
returnEmptyString: false,
|
||||
backend: {
|
||||
loadPath: 'http://127.0.0.1:1337/locales/{{lng}}.json',
|
||||
customHeaders: {
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
requestOptions: {
|
||||
credentials: 'include',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
window.DeckyPluginLoader?.dismountAll();
|
||||
window.DeckyPluginLoader?.deinit();
|
||||
|
||||
window.DeckyPluginLoader = new PluginLoader();
|
||||
window.DeckyPluginLoader.init();
|
||||
window.importDeckyPlugin = function (name: string, version: string) {
|
||||
window.DeckyPluginLoader?.importPlugin(name, version);
|
||||
};
|
||||
@@ -59,8 +67,11 @@ if (!window.webpackJsonp || window.webpackJsonp.deckyShimmed) {
|
||||
if (!window.DeckyPluginLoader.hasPlugin(plugin.name))
|
||||
window.DeckyPluginLoader?.importPlugin(plugin.name, plugin.version);
|
||||
}
|
||||
|
||||
window.DeckyPluginLoader.checkPluginUpdates();
|
||||
};
|
||||
|
||||
setTimeout(() => window.syncDeckyPlugins(), 5000);
|
||||
})();
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,13 @@
|
||||
## What's this?
|
||||
|
||||
`steamfixes` contains various fixes and workaround for things Valve has broken that cause Decky issues.
|
||||
|
||||
## Current fixes:
|
||||
|
||||
- StartRestart() -> StartShutdown(false) override:
|
||||
|
||||
StartRestart() breaks CEF debugging, StartShutdown(false) doesn't. We can safely replace StartRestart() with StartShutdown(false) as gamescope-session will automatically restart the steam client anyway if it shuts down, bypassing the broken restart codepath. Added 12/29/2022
|
||||
|
||||
- ExecuteSteamURL UI reload fix:
|
||||
|
||||
Starting sometime in November 2022, Valve broke reloading the Steam UI pages via location.reload, as it won't properly start the UI. We can manually trigger UI startup if we detect no active input contexts by calling `SteamClient.URL.ExecuteSteamURL("steam://open/settings/")` Added 12/29/2022
|
||||
@@ -0,0 +1,12 @@
|
||||
import reloadFix from './reload';
|
||||
import restartFix from './restart';
|
||||
let fixes: Function[] = [];
|
||||
|
||||
export function deinitSteamFixes() {
|
||||
fixes.forEach((deinit) => deinit());
|
||||
}
|
||||
|
||||
export async function initSteamFixes() {
|
||||
fixes.push(await reloadFix());
|
||||
fixes.push(await restartFix());
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { getFocusNavController, sleep } from 'decky-frontend-lib';
|
||||
|
||||
import Logger from '../logger';
|
||||
|
||||
const logger = new Logger('ReloadSteamFix');
|
||||
|
||||
declare global {
|
||||
var GamepadNavTree: any;
|
||||
}
|
||||
|
||||
export default async function reloadFix() {
|
||||
// Hack to unbreak the ui when reloading it
|
||||
await sleep(4000);
|
||||
if (getFocusNavController()?.m_rgAllContexts?.length == 0) {
|
||||
SteamClient.URL.ExecuteSteamURL('steam://open/settings');
|
||||
logger.log('Applied UI reload fix.');
|
||||
}
|
||||
|
||||
// This steamfix does not need to deinit.
|
||||
return () => {};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Patch, findModuleChild, replacePatch, sleep } from 'decky-frontend-lib';
|
||||
|
||||
import Logger from '../logger';
|
||||
|
||||
const logger = new Logger('RestartSteamFix');
|
||||
|
||||
let patch: Patch;
|
||||
|
||||
function rePatch() {
|
||||
// If you patch anything on SteamClient within the first few seconds of the client having loaded it will get redefined for some reason, so repatch any of these changes that occur with History.listen or an interval
|
||||
patch = replacePatch(window.SteamClient.User, 'StartRestart', () => SteamClient.User.StartShutdown(false));
|
||||
}
|
||||
|
||||
export default async function restartFix() {
|
||||
try {
|
||||
rePatch();
|
||||
// TODO type and add to frontend-lib
|
||||
let History: any;
|
||||
|
||||
while (!History) {
|
||||
History = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.m_history) return m[prop].m_history;
|
||||
}
|
||||
});
|
||||
if (!History) {
|
||||
logger.debug('Waiting 5s for history to become available.');
|
||||
await sleep(5000);
|
||||
}
|
||||
}
|
||||
|
||||
function repatchIfNeeded() {
|
||||
if (window.SteamClient.User.StartRestart !== patch.patchedFunction) {
|
||||
rePatch();
|
||||
}
|
||||
}
|
||||
|
||||
const unlisten = History.listen(repatchIfNeeded);
|
||||
|
||||
// Just in case
|
||||
setTimeout(repatchIfNeeded, 5000);
|
||||
setTimeout(repatchIfNeeded, 10000);
|
||||
|
||||
return () => {
|
||||
unlisten();
|
||||
patch.unpatch();
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error('Error patching StartRestart', e);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
+68
-49
@@ -1,10 +1,16 @@
|
||||
import { ConfirmModal, showModal, staticClasses } from 'decky-frontend-lib';
|
||||
import { InstallType, Plugin } from './plugin';
|
||||
import { getSetting, setSetting } from './utils/settings';
|
||||
|
||||
import { Plugin } from './plugin';
|
||||
export enum Store {
|
||||
Default,
|
||||
Testing,
|
||||
Custom,
|
||||
}
|
||||
|
||||
export interface StorePluginVersion {
|
||||
name: string;
|
||||
hash: string;
|
||||
artifact: string | undefined | null;
|
||||
}
|
||||
|
||||
export interface StorePlugin {
|
||||
@@ -14,31 +20,55 @@ export interface StorePlugin {
|
||||
author: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
image_url: string;
|
||||
}
|
||||
|
||||
export interface LegacyStorePlugin {
|
||||
artifact: string;
|
||||
versions: {
|
||||
[version: string]: string;
|
||||
};
|
||||
author: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
export interface PluginInstallRequest {
|
||||
plugin: string;
|
||||
selectedVer: StorePluginVersion;
|
||||
installType: InstallType;
|
||||
}
|
||||
|
||||
// name: version
|
||||
export type PluginUpdateMapping = Map<string, StorePluginVersion>;
|
||||
|
||||
export function getPluginList(): Promise<StorePlugin[]> {
|
||||
return fetch('https://beta.deckbrew.xyz/plugins', {
|
||||
method: 'GET',
|
||||
}).then((r) => r.json());
|
||||
}
|
||||
|
||||
export function getLegacyPluginList(): Promise<LegacyStorePlugin[]> {
|
||||
return fetch('https://plugins.deckbrew.xyz/get_plugins', {
|
||||
method: 'GET',
|
||||
}).then((r) => r.json());
|
||||
export async function getPluginList(): Promise<StorePlugin[]> {
|
||||
let version = await window.DeckyPluginLoader.updateVersion();
|
||||
let store = await getSetting<Store>('store', Store.Default);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
export async function installFromURL(url: string) {
|
||||
@@ -49,37 +79,26 @@ export async function installFromURL(url: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
|
||||
showModal(
|
||||
<ConfirmModal
|
||||
onOK={() => {
|
||||
window.DeckyPluginLoader.callServerMethod('install_plugin', {
|
||||
name: plugin.artifact,
|
||||
artifact: `https://github.com/${plugin.artifact}/archive/refs/tags/${selectedVer}.zip`,
|
||||
version: selectedVer,
|
||||
hash: plugin.versions[selectedVer],
|
||||
});
|
||||
}}
|
||||
onCancel={() => {
|
||||
// do nothing
|
||||
}}
|
||||
>
|
||||
<div className={staticClasses.Title} style={{ flexDirection: 'column', boxShadow: 'unset' }}>
|
||||
Using legacy plugins
|
||||
</div>
|
||||
You are currently installing a <b>legacy</b> plugin. Legacy plugins are no longer supported and may have issues.
|
||||
Legacy plugins do not support gamepad input. To interact with a legacy plugin, you will need to use the
|
||||
touchscreen.
|
||||
</ConfirmModal>,
|
||||
);
|
||||
}
|
||||
|
||||
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) {
|
||||
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion, installType: InstallType) {
|
||||
const artifactUrl = selectedVer.artifact ?? pluginUrl(selectedVer.hash);
|
||||
await window.DeckyPluginLoader.callServerMethod('install_plugin', {
|
||||
name: plugin,
|
||||
artifact: `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`,
|
||||
artifact: artifactUrl,
|
||||
version: selectedVer.name,
|
||||
hash: selectedVer.hash,
|
||||
install_type: installType,
|
||||
});
|
||||
}
|
||||
|
||||
export async function requestMultiplePluginInstalls(requests: PluginInstallRequest[]) {
|
||||
await window.DeckyPluginLoader.callServerMethod('install_plugins', {
|
||||
requests: requests.map(({ plugin, installType, selectedVer }) => ({
|
||||
name: plugin,
|
||||
artifact: selectedVer.artifact ?? pluginUrl(selectedVer.hash),
|
||||
version: selectedVer.name,
|
||||
hash: selectedVer.hash,
|
||||
install_type: installType,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,6 +114,6 @@ export async function checkForUpdates(plugins: Plugin[]): Promise<PluginUpdateMa
|
||||
return updateMap;
|
||||
}
|
||||
|
||||
export function isLegacyPlugin(plugin: LegacyStorePlugin | StorePlugin): plugin is LegacyStorePlugin {
|
||||
return 'artifact' in plugin;
|
||||
function pluginUrl(hash: string) {
|
||||
return `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${hash}.zip`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// TabsHook for versions before the Desktop merge
|
||||
import { Patch, afterPatch, sleep } from 'decky-frontend-lib';
|
||||
import { memo } from 'react';
|
||||
|
||||
import NewTabsHook from './tabs-hook';
|
||||
|
||||
declare global {
|
||||
interface Array<T> {
|
||||
__filter: any;
|
||||
}
|
||||
}
|
||||
|
||||
const isTabsArray = (tabs: any) => {
|
||||
const length = tabs.length;
|
||||
return length >= 7 && tabs[length - 1]?.tab;
|
||||
};
|
||||
|
||||
class TabsHook extends NewTabsHook {
|
||||
// private keys = 7;
|
||||
private quickAccess: any;
|
||||
private tabRenderer: any;
|
||||
private memoizedQuickAccess: any;
|
||||
private cNode: any;
|
||||
|
||||
private qAPTree: any;
|
||||
private rendererTree: any;
|
||||
|
||||
private cNodePatch?: Patch;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.log('Initialized stable TabsHook');
|
||||
}
|
||||
|
||||
init() {
|
||||
const self = this;
|
||||
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
|
||||
let scrollRoot: any;
|
||||
async function findScrollRoot(currentNode: any, iters: number): Promise<any> {
|
||||
if (iters >= 30) {
|
||||
self.error(
|
||||
'Scroll root was not found before hitting the recursion limit, a developer will need to increase the limit.',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
currentNode = currentNode?.child;
|
||||
if (currentNode?.type?.prototype?.RemoveSmartScrollContainer) {
|
||||
self.log(`Scroll root was found in ${iters} recursion cycles`);
|
||||
return currentNode;
|
||||
}
|
||||
if (!currentNode) return null;
|
||||
if (currentNode.sibling) {
|
||||
let node = await findScrollRoot(currentNode.sibling, iters + 1);
|
||||
if (node !== null) return node;
|
||||
}
|
||||
return await findScrollRoot(currentNode, iters + 1);
|
||||
}
|
||||
(async () => {
|
||||
scrollRoot = await findScrollRoot(tree, 0);
|
||||
while (!scrollRoot) {
|
||||
this.log('Failed to find scroll root node, reattempting in 5 seconds');
|
||||
await sleep(5000);
|
||||
scrollRoot = await findScrollRoot(tree, 0);
|
||||
}
|
||||
let newQA: any;
|
||||
let newQATabRenderer: any;
|
||||
this.cNodePatch = afterPatch(scrollRoot.stateNode, 'render', (_: any, ret: any) => {
|
||||
if (!this.quickAccess && ret.props.children.props.children[4]) {
|
||||
this.quickAccess = ret?.props?.children?.props?.children[4].type;
|
||||
newQA = (...args: any) => {
|
||||
const ret = this.quickAccess.type(...args);
|
||||
if (ret) {
|
||||
if (!newQATabRenderer) {
|
||||
this.tabRenderer = ret.props.children[1].children.type;
|
||||
newQATabRenderer = (...qamArgs: any[]) => {
|
||||
const oFilter = Array.prototype.filter;
|
||||
Array.prototype.filter = function (...args: any[]) {
|
||||
if (isTabsArray(this)) {
|
||||
self.render(this, qamArgs[0].visible);
|
||||
}
|
||||
// @ts-ignore
|
||||
return oFilter.call(this, ...args);
|
||||
};
|
||||
// TODO remove array hack entirely and use this instead const tabs = ret.props.children.props.children[0].props.children[1].props.children[0].props.children[0].props.tabs
|
||||
const ret = this.tabRenderer(...qamArgs);
|
||||
Array.prototype.filter = oFilter;
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
this.rendererTree = ret.props.children[1].children;
|
||||
ret.props.children[1].children.type = newQATabRenderer;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
this.memoizedQuickAccess = memo(newQA);
|
||||
this.memoizedQuickAccess.isDeckyQuickAccess = true;
|
||||
}
|
||||
if (ret.props.children.props.children[4]) {
|
||||
this.qAPTree = ret.props.children.props.children[4];
|
||||
ret.props.children.props.children[4].type = this.memoizedQuickAccess;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
this.cNode = scrollRoot;
|
||||
this.cNode.stateNode.forceUpdate();
|
||||
this.log('Finished initial injection');
|
||||
})();
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.cNodePatch?.unpatch();
|
||||
if (this.qAPTree) this.qAPTree.type = this.quickAccess;
|
||||
if (this.rendererTree) this.rendererTree.type = this.tabRenderer;
|
||||
if (this.cNode) this.cNode.stateNode.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
export default TabsHook;
|
||||
+88
-84
@@ -1,5 +1,5 @@
|
||||
import { Patch, QuickAccessTab, afterPatch, sleep } from 'decky-frontend-lib';
|
||||
import { memo } from 'react';
|
||||
// TabsHook for versions after the Desktop merge
|
||||
import { Patch, QuickAccessTab, afterPatch, findInReactTree, sleep } from 'decky-frontend-lib';
|
||||
|
||||
import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState';
|
||||
import Logger from './logger';
|
||||
@@ -8,16 +8,8 @@ declare global {
|
||||
interface Window {
|
||||
__TABS_HOOK_INSTANCE: any;
|
||||
}
|
||||
interface Array<T> {
|
||||
__filter: any;
|
||||
}
|
||||
}
|
||||
|
||||
const isTabsArray = (tabs: any) => {
|
||||
const length = tabs.length;
|
||||
return length >= 7 && tabs[length - 1]?.tab;
|
||||
};
|
||||
|
||||
interface Tab {
|
||||
id: QuickAccessTab | number;
|
||||
title: any;
|
||||
@@ -28,15 +20,8 @@ interface Tab {
|
||||
class TabsHook extends Logger {
|
||||
// private keys = 7;
|
||||
tabs: Tab[] = [];
|
||||
private quickAccess: any;
|
||||
private tabRenderer: any;
|
||||
private memoizedQuickAccess: any;
|
||||
private cNode: any;
|
||||
|
||||
private qAPTree: any;
|
||||
private rendererTree: any;
|
||||
|
||||
private cNodePatch?: Patch;
|
||||
private qAMRoot?: any;
|
||||
private qamPatch?: Patch;
|
||||
|
||||
constructor() {
|
||||
super('TabsHook');
|
||||
@@ -44,86 +29,89 @@ class TabsHook extends Logger {
|
||||
this.log('Initialized');
|
||||
window.__TABS_HOOK_INSTANCE?.deinit?.();
|
||||
window.__TABS_HOOK_INSTANCE = this;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
init() {
|
||||
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
|
||||
let scrollRoot: any;
|
||||
async function findScrollRoot(currentNode: any, iters: number): Promise<any> {
|
||||
if (iters >= 30) {
|
||||
self.error(
|
||||
'Scroll root was not found before hitting the recursion limit, a developer will need to increase the limit.',
|
||||
);
|
||||
let qAMRoot: any;
|
||||
const findQAMRoot = (currentNode: any, iters: number): any => {
|
||||
if (iters >= 65) {
|
||||
// currently 45
|
||||
return null;
|
||||
}
|
||||
currentNode = currentNode?.child;
|
||||
if (currentNode?.type?.prototype?.RemoveSmartScrollContainer) {
|
||||
self.log(`Scroll root was found in ${iters} recursion cycles`);
|
||||
if (
|
||||
typeof currentNode?.memoizedProps?.visible == 'boolean' &&
|
||||
currentNode?.type?.toString()?.includes('QuickAccessMenuBrowserView')
|
||||
) {
|
||||
this.log(`QAM root was found in ${iters} recursion cycles`);
|
||||
return currentNode;
|
||||
}
|
||||
if (!currentNode) return null;
|
||||
if (currentNode.sibling) {
|
||||
let node = await findScrollRoot(currentNode.sibling, iters + 1);
|
||||
if (currentNode.child) {
|
||||
let node = findQAMRoot(currentNode.child, iters + 1);
|
||||
if (node !== null) return node;
|
||||
}
|
||||
return await findScrollRoot(currentNode, iters + 1);
|
||||
}
|
||||
(async () => {
|
||||
scrollRoot = await findScrollRoot(tree, 0);
|
||||
while (!scrollRoot) {
|
||||
this.log('Failed to find scroll root node, reattempting in 5 seconds');
|
||||
await sleep(5000);
|
||||
scrollRoot = await findScrollRoot(tree, 0);
|
||||
if (currentNode.sibling) {
|
||||
let node = findQAMRoot(currentNode.sibling, iters + 1);
|
||||
if (node !== null) return node;
|
||||
}
|
||||
let newQA: any;
|
||||
let newQATabRenderer: any;
|
||||
this.cNodePatch = afterPatch(scrollRoot.stateNode, 'render', (_: any, ret: any) => {
|
||||
if (!this.quickAccess && ret.props.children.props.children[4]) {
|
||||
this.quickAccess = ret?.props?.children?.props?.children[4].type;
|
||||
newQA = (...args: any) => {
|
||||
const ret = this.quickAccess.type(...args);
|
||||
if (ret) {
|
||||
if (!newQATabRenderer) {
|
||||
this.tabRenderer = ret.props.children[1].children.type;
|
||||
newQATabRenderer = (...qamArgs: any[]) => {
|
||||
const oFilter = Array.prototype.filter;
|
||||
Array.prototype.filter = function (...args: any[]) {
|
||||
if (isTabsArray(this)) {
|
||||
self.render(this, qamArgs[0].visible);
|
||||
}
|
||||
// @ts-ignore
|
||||
return oFilter.call(this, ...args);
|
||||
};
|
||||
// TODO remove array hack entirely and use this instead const tabs = ret.props.children.props.children[0].props.children[1].props.children[0].props.children[0].props.tabs
|
||||
const ret = this.tabRenderer(...qamArgs);
|
||||
Array.prototype.filter = oFilter;
|
||||
return ret;
|
||||
};
|
||||
return null;
|
||||
};
|
||||
(async () => {
|
||||
qAMRoot = findQAMRoot(tree, 0);
|
||||
while (!qAMRoot) {
|
||||
this.error(
|
||||
'Failed to find QAM root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.',
|
||||
);
|
||||
await sleep(5000);
|
||||
qAMRoot = findQAMRoot(tree, 0);
|
||||
}
|
||||
this.qAMRoot = qAMRoot;
|
||||
let patchedInnerQAM: any;
|
||||
this.qamPatch = afterPatch(qAMRoot.return, 'type', (_: any, ret: any) => {
|
||||
try {
|
||||
if (!qAMRoot?.child) {
|
||||
qAMRoot = findQAMRoot(tree, 0);
|
||||
this.qAMRoot = qAMRoot;
|
||||
}
|
||||
if (qAMRoot?.child && !qAMRoot?.child?.type?.decky) {
|
||||
afterPatch(qAMRoot.child, 'type', (_: any, ret: any) => {
|
||||
try {
|
||||
const qamTabsRenderer = findInReactTree(ret, (x) => x?.props?.onFocusNavDeactivated);
|
||||
if (patchedInnerQAM) {
|
||||
qamTabsRenderer.type = patchedInnerQAM;
|
||||
} else {
|
||||
afterPatch(qamTabsRenderer, 'type', (innerArgs: any, ret: any) => {
|
||||
const tabs = findInReactTree(ret, (x) => x?.props?.tabs);
|
||||
this.render(tabs.props.tabs, innerArgs[0].visible);
|
||||
return ret;
|
||||
});
|
||||
patchedInnerQAM = qamTabsRenderer.type;
|
||||
}
|
||||
} catch (e) {
|
||||
this.error('Error patching QAM inner', e);
|
||||
}
|
||||
this.rendererTree = ret.props.children[1].children;
|
||||
ret.props.children[1].children.type = newQATabRenderer;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
this.memoizedQuickAccess = memo(newQA);
|
||||
this.memoizedQuickAccess.isDeckyQuickAccess = true;
|
||||
}
|
||||
if (ret.props.children.props.children[4]) {
|
||||
this.qAPTree = ret.props.children.props.children[4];
|
||||
ret.props.children.props.children[4].type = this.memoizedQuickAccess;
|
||||
return ret;
|
||||
});
|
||||
qAMRoot.child.type.decky = true;
|
||||
qAMRoot.child.alternate.type = qAMRoot.child.type;
|
||||
}
|
||||
} catch (e) {
|
||||
this.error('Error patching QAM', e);
|
||||
}
|
||||
|
||||
return ret;
|
||||
});
|
||||
this.cNode = scrollRoot;
|
||||
this.cNode.stateNode.forceUpdate();
|
||||
|
||||
if (qAMRoot.return.alternate) {
|
||||
qAMRoot.return.alternate.type = qAMRoot.return.type;
|
||||
}
|
||||
this.log('Finished initial injection');
|
||||
})();
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.cNodePatch?.unpatch();
|
||||
if (this.qAPTree) this.qAPTree.type = this.quickAccess;
|
||||
if (this.rendererTree) this.rendererTree.type = this.tabRenderer;
|
||||
if (this.cNode) this.cNode.stateNode.forceUpdate();
|
||||
this.qamPatch?.unpatch();
|
||||
this.qAMRoot.return.alternate.type = this.qAMRoot.return.type;
|
||||
}
|
||||
|
||||
add(tab: Tab) {
|
||||
@@ -137,13 +125,29 @@ class TabsHook extends Logger {
|
||||
}
|
||||
|
||||
render(existingTabs: any[], visible: boolean) {
|
||||
let deckyTabAmount = existingTabs.reduce((prev: any, cur: any) => (cur.decky ? prev + 1 : prev), 0);
|
||||
if (deckyTabAmount == this.tabs.length) {
|
||||
for (let tab of existingTabs) {
|
||||
if (tab?.decky) {
|
||||
if (tab?.qAMVisibilitySetter) {
|
||||
tab?.qAMVisibilitySetter(visible);
|
||||
} else {
|
||||
tab.initialVisibility = visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const { title, icon, content, id } of this.tabs) {
|
||||
existingTabs.push({
|
||||
const tab: any = {
|
||||
key: id,
|
||||
title,
|
||||
tab: icon,
|
||||
panel: <QuickAccessVisibleStateProvider visible={visible}>{content}</QuickAccessVisibleStateProvider>,
|
||||
});
|
||||
decky: true,
|
||||
initialVisibility: visible,
|
||||
};
|
||||
tab.panel = <QuickAccessVisibleStateProvider tab={tab}>{content}</QuickAccessVisibleStateProvider>;
|
||||
existingTabs.push(tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user