mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 20:25:04 +03:00
Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e8e0fc7c1 | |||
| 8049417e03 | |||
| f4c0a8b5aa | |||
| d3584a9931 | |||
| 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 |
@@ -12,6 +12,7 @@ body:
|
||||
- label: I have searched existing issues
|
||||
- label: This issue is not a duplicate of an existing one
|
||||
- label: I have checked the [common issues section in the readme file](https://github.com/SteamDeckHomebrew/decky-loader#-common-issues)
|
||||
- label: I have attached logs to this bug report (failure to include logs will mean your issue will not be responded too).
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -70,4 +71,4 @@ body:
|
||||
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: false
|
||||
required: true
|
||||
|
||||
@@ -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.
|
||||
@@ -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' }}
|
||||
|
||||
@@ -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'
|
||||
@@ -15,13 +15,3 @@ jobs:
|
||||
pushd frontend
|
||||
npm install
|
||||
npm run lint
|
||||
|
||||
- name: Run black (Python formatting)
|
||||
uses: lgeiger/black-action@v1.0.1
|
||||
with:
|
||||
args: "./backend --experimental-string-processing --config ./backend/pyproject.toml"
|
||||
|
||||
- name: Run ruff (Python linting)
|
||||
uses: jpetrucciani/ruff-check@main
|
||||
with:
|
||||
path: "./backend"
|
||||
|
||||
Vendored
+1
-1
@@ -4,4 +4,4 @@
|
||||
"deckpass" : "ssap",
|
||||
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
|
||||
"deckdir" : "/home/deck"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+8
@@ -41,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",
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
<br>
|
||||
Decky Loader
|
||||
<br>
|
||||
<a name="logo" href="https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop"><img src="./docs/images/download_button.png" alt="Download decky" width="350"></a>
|
||||
<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%">
|
||||
@@ -33,40 +34,39 @@ 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 or 8080, please change its port to something else or uninstall it.
|
||||
- If you run the installer and it just opens a file in a text editor: click the (...) button in the top right of dolphin (the file manager) then 'configure' and 'configure dolphin'. Click on the 'confirmations' tab and set 'when opening an executable file' to 'run script'.
|
||||
- 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
|
||||
- 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. Navigate to this Github page on a browser of your choice.
|
||||
1. Press the 'Download' button at the top of the page.
|
||||
1. Run the downloaded file by clicking on it in Dolphin (the file manager).
|
||||
1. Either type your admin password or allow Decky to temporarily set your password to `Decky!`
|
||||
1. Download the [installer file](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop).
|
||||
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.
|
||||
- **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).
|
||||
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. Run the installer file again, and select `uninstall decky loader`
|
||||
1. Run the installer file again, and select `uninstall decky loader`.
|
||||
- There is also a fast uninstall for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/uninstall.sh | sh` and type your password when prompted.
|
||||
|
||||
## 🚀 Getting Started
|
||||
@@ -84,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
|
||||
```
|
||||
|
||||
+118
-123
@@ -1,33 +1,27 @@
|
||||
# Full imports
|
||||
import json
|
||||
|
||||
# import pprint
|
||||
# from pprint import pformat
|
||||
|
||||
# Partial imports
|
||||
from aiohttp import ClientSession
|
||||
from asyncio import sleep
|
||||
from aiohttp import ClientSession, web
|
||||
from asyncio import get_event_loop, sleep
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
from logging import getLogger
|
||||
from os import R_OK, W_OK, path, listdir, access, mkdir
|
||||
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 helpers import get_ssl_context, download_remote_binary_to_path
|
||||
from injector import get_gamepadui_tab
|
||||
|
||||
logger = getLogger("Browser")
|
||||
|
||||
|
||||
class PluginInstallContext:
|
||||
def __init__(self, artifact, name, version, hash) -> None:
|
||||
self.artifact = artifact
|
||||
@@ -35,12 +29,12 @@ class PluginInstallContext:
|
||||
self.version = version
|
||||
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):
|
||||
@@ -49,41 +43,30 @@ 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},"
|
||||
f" 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:
|
||||
packageJsonPath = path.join(pluginBasePath, "package.json")
|
||||
pluginBinPath = path.join(pluginBasePath, "bin")
|
||||
packageJsonPath = path.join(pluginBasePath, 'package.json')
|
||||
pluginBinPath = path.join(pluginBasePath, 'bin')
|
||||
|
||||
if access(packageJsonPath, R_OK):
|
||||
with open(packageJsonPath, "r", encoding="utf-8") as f:
|
||||
packageJson = json.load(f)
|
||||
if (
|
||||
"remote_binary" in packageJson
|
||||
and len(packageJson["remote_binary"]) > 0
|
||||
):
|
||||
if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0:
|
||||
# create bin directory if needed.
|
||||
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):
|
||||
call(["chmod", "-R", "777", pluginBinPath])
|
||||
chmod(pluginBinPath, 777)
|
||||
|
||||
rv = True
|
||||
for remoteBinary in packageJson["remote_binary"]:
|
||||
@@ -91,28 +74,15 @@ class PluginBrowser:
|
||||
binName = remoteBinary["name"]
|
||||
binURL = remoteBinary["url"]
|
||||
binHash = remoteBinary["sha256hash"]
|
||||
if not await download_remote_binary_to_path(
|
||||
binURL, binHash, path.join(pluginBinPath, binName)
|
||||
):
|
||||
if not await download_remote_binary_to_path(binURL, binHash, path.join(pluginBinPath, binName)):
|
||||
rv = False
|
||||
raise Exception(
|
||||
"Error Downloading Remote Binary"
|
||||
f" {binName}@{binURL} with hash {binHash} to"
|
||||
f" {path.join(pluginBinPath, binName)}"
|
||||
)
|
||||
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
|
||||
|
||||
call(
|
||||
[
|
||||
"chown",
|
||||
"-R",
|
||||
get_user() + ":" + get_user_group(),
|
||||
self.plugin_path,
|
||||
]
|
||||
)
|
||||
call(["chmod", "-R", "555", pluginBasePath])
|
||||
chown(self.plugin_path)
|
||||
chmod(pluginBasePath, 555)
|
||||
else:
|
||||
rv = True
|
||||
logger.debug("No Remote Binaries to Download")
|
||||
logger.debug(f"No Remote Binaries to Download")
|
||||
|
||||
except Exception as e:
|
||||
rv = False
|
||||
@@ -120,53 +90,57 @@ class PluginBrowser:
|
||||
|
||||
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",
|
||||
encoding="utf-8",
|
||||
) 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 str(path.join(self.plugin_path, folder))
|
||||
except Exception:
|
||||
if plugin['name'] == name:
|
||||
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_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.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 self.plugins[name]:
|
||||
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)
|
||||
current_plugin_order = self.settings.getSetting("pluginOrder")
|
||||
current_plugin_order.remove(name)
|
||||
self.settings.setSetting("pluginOrder", current_plugin_order)
|
||||
logger.debug("Plugin %s was removed from the pluginOrder setting", 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("Error at %s", exc_info=e)
|
||||
logger.error(f"Plugin {name} in {plugin_dir} was not uninstalled")
|
||||
logger.error(f"Error at %s", 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
|
||||
@@ -174,68 +148,89 @@ class PluginBrowser:
|
||||
pluginFolderPath = self.find_plugin_folder(name)
|
||||
if pluginFolderPath:
|
||||
isInstalled = True
|
||||
except Exception:
|
||||
logger.error(
|
||||
f"Failed to determine if {name} is already installed, continuing"
|
||||
" anyway."
|
||||
)
|
||||
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)
|
||||
if isInstalled:
|
||||
try:
|
||||
logger.debug("Uninstalling existing plugin...")
|
||||
await self.uninstall_plugin(name)
|
||||
except Exception:
|
||||
logger.error(f"Plugin {name} could not be uninstalled.")
|
||||
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
|
||||
)
|
||||
else:
|
||||
logger.fatal("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()}")
|
||||
except:
|
||||
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
|
||||
)
|
||||
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.open_websocket()
|
||||
await tab.evaluate_js(
|
||||
f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}',"
|
||||
f" '{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)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from enum import Enum
|
||||
|
||||
class UserType(Enum):
|
||||
HOST_USER = 1
|
||||
EFFECTIVE_USER = 2
|
||||
ROOT = 3
|
||||
+96
-133
@@ -1,17 +1,18 @@
|
||||
import grp
|
||||
import pwd
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import uuid
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
|
||||
import certifi
|
||||
from aiohttp.web import Response, middleware
|
||||
from aiohttp import ClientSession
|
||||
import localplatform
|
||||
from customtypes import UserType
|
||||
from logging import getLogger
|
||||
|
||||
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
||||
|
||||
@@ -21,135 +22,55 @@ ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
||||
frontend_regex = re.compile("^/frontend/.*")
|
||||
|
||||
logger = getLogger("Main")
|
||||
|
||||
def get_ssl_context():
|
||||
return ssl_ctx
|
||||
|
||||
|
||||
def get_csrf_token():
|
||||
return csrf_token
|
||||
|
||||
|
||||
@middleware
|
||||
async def csrf_middleware(request, handler):
|
||||
if (
|
||||
str(request.method) == "OPTIONS"
|
||||
or request.headers.get("Authentication") == csrf_token
|
||||
or str(request.rel_url) == "/auth/token"
|
||||
or str(request.rel_url).startswith("/plugins/load_main/")
|
||||
or str(request.rel_url).startswith("/static/")
|
||||
or str(request.rel_url).startswith("/legacy/")
|
||||
or str(request.rel_url).startswith("/steam_resource/")
|
||||
or str(request.rel_url).startswith("/frontend/")
|
||||
or assets_regex.match(str(request.rel_url))
|
||||
or frontend_regex.match(str(request.rel_url))
|
||||
):
|
||||
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or str(request.rel_url).startswith("/frontend/") or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
|
||||
return await handler(request)
|
||||
return Response(text="Forbidden", status="403")
|
||||
|
||||
|
||||
# Deprecated
|
||||
def set_user():
|
||||
pass
|
||||
|
||||
|
||||
# Get the user id hosting the plugin loader
|
||||
def get_user_id() -> int:
|
||||
proc_path = os.path.realpath(sys.argv[0])
|
||||
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
|
||||
for pw in pws:
|
||||
if proc_path.startswith(os.path.realpath(pw.pw_dir)):
|
||||
return pw.pw_uid
|
||||
raise PermissionError(
|
||||
"The plugin loader does not seem to be hosted by any known user."
|
||||
)
|
||||
|
||||
|
||||
# Get the user hosting the plugin loader
|
||||
def get_user() -> str:
|
||||
return pwd.getpwuid(get_user_id()).pw_name
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# Deprecated
|
||||
def set_user_group() -> str:
|
||||
return get_user_group()
|
||||
|
||||
|
||||
# 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(file_path) -> str:
|
||||
if file_path:
|
||||
return grp.getgrgid(os.stat(file_path).st_gid).gr_name
|
||||
else:
|
||||
return grp.getgrgid(get_user_group_id()).gr_name
|
||||
|
||||
|
||||
# Get the default home path unless a user is specified
|
||||
def get_home_path(username=None) -> str:
|
||||
if username is None:
|
||||
username = get_user()
|
||||
return pwd.getpwnam(username).pw_dir
|
||||
|
||||
|
||||
# Get the default homebrew path unless a home_path is specified
|
||||
def get_homebrew_path(home_path=None) -> str:
|
||||
if home_path is None:
|
||||
home_path = get_home_path()
|
||||
return os.path.join(home_path, "homebrew")
|
||||
return Response(text='Forbidden', status='403')
|
||||
|
||||
# Get the default homebrew path unless a home_path is specified. home_path argument is deprecated
|
||||
def get_homebrew_path(home_path = None) -> str:
|
||||
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)
|
||||
chown_path = get_home_path()
|
||||
parts = os.path.relpath(path, chown_path).split(os.sep)
|
||||
uid = get_user_id()
|
||||
gid = get_user_group_id()
|
||||
for p in parts:
|
||||
chown_path = os.path.join(chown_path, p)
|
||||
os.chown(chown_path, uid, gid)
|
||||
|
||||
localplatform.chown(path)
|
||||
|
||||
# Fetches the version of loader
|
||||
def get_loader_version() -> str:
|
||||
with open(
|
||||
os.path.join(os.path.dirname(sys.argv[0]), ".loader.version"),
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as version_file:
|
||||
return version_file.readline().replace("\n", "")
|
||||
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:
|
||||
@@ -163,37 +84,79 @@ async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
||||
remoteHash = sha256(data.getbuffer()).hexdigest()
|
||||
if binHash == remoteHash:
|
||||
data.seek(0)
|
||||
with open(path, "wb") as f:
|
||||
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}"
|
||||
)
|
||||
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
|
||||
else:
|
||||
rv = False
|
||||
except Exception:
|
||||
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) -> bool:
|
||||
return await localplatform.service_stop(unit_name)
|
||||
|
||||
async def stop_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
||||
cmd = ["systemctl", "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)
|
||||
+123
-176
@@ -2,9 +2,10 @@
|
||||
|
||||
from asyncio import sleep
|
||||
from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from typing import List
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp import ClientSession, WSMsgType
|
||||
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
|
||||
from asyncio.exceptions import TimeoutError
|
||||
import uuid
|
||||
@@ -38,12 +39,9 @@ class Tab:
|
||||
async for message in self.websocket:
|
||||
data = message.json()
|
||||
yield data
|
||||
logger.warn(
|
||||
f"The Tab {self.title} socket has been disconnected while listening for"
|
||||
" messages."
|
||||
)
|
||||
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
|
||||
@@ -56,24 +54,19 @@ class Tab:
|
||||
return None
|
||||
raise RuntimeError("Websocket not opened")
|
||||
|
||||
async def evaluate_js(
|
||||
self, js, run_async=False, manage_socket=True, get_result=True
|
||||
):
|
||||
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd(
|
||||
{
|
||||
"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)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
@@ -81,17 +74,9 @@ class Tab:
|
||||
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,
|
||||
)
|
||||
res = await self.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False, manage_socket)
|
||||
|
||||
if (
|
||||
"result" not in res
|
||||
or "result" not in res["result"]
|
||||
or "value" not in res["result"]["result"]
|
||||
):
|
||||
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"]
|
||||
@@ -101,12 +86,9 @@ class Tab:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd(
|
||||
{
|
||||
"method": "Page.close",
|
||||
},
|
||||
False,
|
||||
)
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.close",
|
||||
}, False)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
@@ -117,42 +99,32 @@ class Tab:
|
||||
"""
|
||||
Enables page domain notifications.
|
||||
"""
|
||||
await self._send_devtools_cmd(
|
||||
{
|
||||
"method": "Page.enable",
|
||||
},
|
||||
False,
|
||||
)
|
||||
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,
|
||||
)
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.disable",
|
||||
}, False)
|
||||
|
||||
async def refresh(self, manage_socket=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,
|
||||
)
|
||||
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
|
||||
@@ -161,70 +133,64 @@ class Tab:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
await self._send_devtools_cmd({"method": "Debugger.enable"}, True)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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",
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"breakpointId": breakpoint_res["result"]["breakpointId"]
|
||||
},
|
||||
},
|
||||
False,
|
||||
)
|
||||
"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.resume"
|
||||
}, False)
|
||||
|
||||
await self._send_devtools_cmd({"method": "Debugger.disable"}, True)
|
||||
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
|
||||
):
|
||||
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:
|
||||
|
||||
@@ -259,44 +225,35 @@ class Tab:
|
||||
"""
|
||||
try:
|
||||
|
||||
wrappedjs = (
|
||||
"""
|
||||
function scriptFunc() {{
|
||||
wrappedjs = """
|
||||
function scriptFunc() {
|
||||
{js}
|
||||
}}
|
||||
if (document.readyState === 'loading') {{
|
||||
addEventListener('DOMContentLoaded', () => {{
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
addEventListener('DOMContentLoaded', () => {
|
||||
scriptFunc();
|
||||
}});
|
||||
}} else {{
|
||||
});
|
||||
} else {
|
||||
scriptFunc();
|
||||
}}
|
||||
""".format(
|
||||
js=js
|
||||
)
|
||||
if add_dom_wrapper
|
||||
else js
|
||||
)
|
||||
}
|
||||
""".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,
|
||||
)
|
||||
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
|
||||
):
|
||||
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`
|
||||
|
||||
@@ -310,28 +267,21 @@ class Tab:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
await self._send_devtools_cmd(
|
||||
{
|
||||
"method": "Page.removeScriptToEvaluateOnNewDocument",
|
||||
"params": {"identifier": script_id},
|
||||
},
|
||||
False,
|
||||
)
|
||||
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
|
||||
)
|
||||
res = await self.evaluate_js(f"document.getElementById('{element_name}') != null", False, manage_socket)
|
||||
|
||||
if (
|
||||
"result" not in res
|
||||
or "result" not in res["result"]
|
||||
or "value" not in res["result"]["result"]
|
||||
):
|
||||
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"]
|
||||
@@ -348,17 +298,23 @@ class Tab:
|
||||
document.head.append(style);
|
||||
style.textContent = `{style}`;
|
||||
}})()
|
||||
""",
|
||||
False,
|
||||
manage_socket,
|
||||
)
|
||||
""", False, manage_socket)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {"success": False, "result": result["result"]}
|
||||
return {
|
||||
"success": False,
|
||||
"result": result["result"]
|
||||
}
|
||||
|
||||
return {"success": True, "result": css_id}
|
||||
return {
|
||||
"success": True,
|
||||
"result": css_id
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "result": e}
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def remove_css(self, css_id, manage_socket=True):
|
||||
try:
|
||||
@@ -370,24 +326,25 @@ class Tab:
|
||||
if (style.nodeName.toLowerCase() == 'style')
|
||||
style.parentNode.removeChild(style);
|
||||
}})()
|
||||
""",
|
||||
False,
|
||||
manage_socket,
|
||||
)
|
||||
""", False, manage_socket)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {"success": False, "result": result}
|
||||
return {
|
||||
"success": False,
|
||||
"result": result
|
||||
}
|
||||
|
||||
return {"success": True}
|
||||
return {
|
||||
"success": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "result": 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,
|
||||
)
|
||||
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
def __repr__(self):
|
||||
@@ -430,45 +387,35 @@ async def get_tab(tab_name) -> 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("Tab not found by lambda")
|
||||
raise ValueError(f"Tab not found by lambda")
|
||||
return tab
|
||||
|
||||
SHARED_CTX_NAMES = ["SharedJSContext", "Steam Shared Context presented by Valve™", "Steam", "SP"]
|
||||
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 == "Steam Shared Context presented by Valve™"
|
||||
or t.title == "Steam"
|
||||
or t.title == "SP"
|
||||
)
|
||||
|
||||
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("GamepadUI Tab not found")
|
||||
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 close_old_tabs():
|
||||
tabs = await get_tabs()
|
||||
for t in tabs:
|
||||
if not t.title or (
|
||||
t.title != "Steam Shared Context presented by Valve™"
|
||||
and t.title != "Steam"
|
||||
and t.title != "SP"
|
||||
):
|
||||
if not t.title or (t.title not in SHARED_CTX_NAMES and DO_NOT_CLOSE_URL not in t.url):
|
||||
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
|
||||
await t.close()
|
||||
await sleep(0.5)
|
||||
|
||||
+57
-106
@@ -6,22 +6,16 @@ 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
|
||||
|
||||
try:
|
||||
from watchdog.observers.inotify import InotifyObserver as Observer
|
||||
except UnsupportedLibc:
|
||||
from watchdog.observers.fsevents import FSEventsObserver as Observer
|
||||
from watchdog.observers import Observer
|
||||
|
||||
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$"])
|
||||
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$'])
|
||||
self.logger = getLogger("file-watcher")
|
||||
self.plugin_path = plugin_path
|
||||
self.queue = queue
|
||||
@@ -32,9 +26,7 @@ class FileChangeHandler(RegexMatchingEventHandler):
|
||||
return
|
||||
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
|
||||
if exists(path.join(self.plugin_path, plugin_dir, "plugin.json")):
|
||||
self.queue.put_nowait(
|
||||
(path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True)
|
||||
)
|
||||
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
|
||||
|
||||
def on_created(self, event):
|
||||
src_path = event.src_path
|
||||
@@ -64,50 +56,39 @@ class FileChangeHandler(RegexMatchingEventHandler):
|
||||
self.logger.debug(f"file modified: {src_path}")
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
|
||||
class Loader:
|
||||
def __init__(self, server_instance, plugin_path, loop, live_reload=False) -> None:
|
||||
self.loop = loop
|
||||
self.logger = getLogger("Loader")
|
||||
self.plugin_path = plugin_path
|
||||
self.logger.info(f"plugin_path: {self.plugin_path}")
|
||||
self.plugins = {}
|
||||
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),
|
||||
|
||||
server_instance.add_routes(
|
||||
[
|
||||
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
||||
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,
|
||||
),
|
||||
# The following is legacy plugin code.
|
||||
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
|
||||
web.get(
|
||||
"/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route
|
||||
),
|
||||
web.get("/steam_resource/{path:.+}", self.get_steam_resource),
|
||||
]
|
||||
)
|
||||
# The following is legacy plugin code.
|
||||
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
|
||||
web.get("/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route),
|
||||
web.get("/steam_resource/{path:.+}", self.get_steam_resource)
|
||||
])
|
||||
|
||||
async def enable_reload_wait(self):
|
||||
if self.live_reload:
|
||||
@@ -120,65 +101,47 @@ 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
|
||||
]
|
||||
)
|
||||
return web.json_response([{"name": str(i) if not i.legacy else "$LEGACY_"+str(i), "version": i.version} for i in plugins])
|
||||
|
||||
def handle_plugin_frontend_assets(self, request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
file = path.join(
|
||||
self.plugin_path,
|
||||
plugin.plugin_directory,
|
||||
"dist/assets",
|
||||
request.match_info["path"],
|
||||
)
|
||||
file = path.join(self.plugin_path, plugin.plugin_directory, "dist/assets", request.match_info["path"])
|
||||
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
def handle_frontend_bundle(self, request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
|
||||
with open(
|
||||
path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"),
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as bundle:
|
||||
return web.Response(
|
||||
text=bundle.read(), content_type="application/javascript"
|
||||
)
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), "r", encoding="utf-8") as bundle:
|
||||
return web.Response(text=bundle.read(), content_type="application/javascript")
|
||||
|
||||
def import_plugin(self, file, plugin_directory, refresh=False, batch=False):
|
||||
try:
|
||||
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
|
||||
if plugin.name in self.plugins:
|
||||
if "debug" not in plugin.flags and refresh:
|
||||
self.logger.info(
|
||||
f"Plugin {plugin.name} is already loaded and has requested to"
|
||||
" not be re-loaded"
|
||||
)
|
||||
return
|
||||
else:
|
||||
self.plugins[plugin.name].stop()
|
||||
self.plugins.pop(plugin.name, None)
|
||||
if not "debug" in plugin.flags and refresh:
|
||||
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
|
||||
return
|
||||
else:
|
||||
self.plugins[plugin.name].stop()
|
||||
self.plugins.pop(plugin.name, None)
|
||||
if plugin.passive:
|
||||
self.logger.info(f"Plugin {plugin.name} is passive")
|
||||
self.plugins[plugin.name] = plugin.start()
|
||||
self.logger.info(f"Loaded {plugin.name}")
|
||||
if not batch:
|
||||
self.loop.create_task(
|
||||
self.dispatch_plugin(
|
||||
plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name,
|
||||
plugin.version,
|
||||
)
|
||||
)
|
||||
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()
|
||||
@@ -190,20 +153,10 @@ class Loader:
|
||||
def import_plugins(self):
|
||||
self.logger.info(f"import plugins from {self.plugin_path}")
|
||||
|
||||
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"))
|
||||
]
|
||||
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,
|
||||
False,
|
||||
True,
|
||||
)
|
||||
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory, False, True)
|
||||
|
||||
async def handle_reloads(self):
|
||||
while True:
|
||||
@@ -220,10 +173,10 @@ class Loader:
|
||||
except JSONDecodeError:
|
||||
args = {}
|
||||
try:
|
||||
if method_name.startswith("_"):
|
||||
raise RuntimeError("Tried to call private method")
|
||||
res["result"] = await plugin.execute_method(method_name, args)
|
||||
res["success"] = True
|
||||
if method_name.startswith("_"):
|
||||
raise RuntimeError("Tried to call private method")
|
||||
res["result"] = await plugin.execute_method(method_name, args)
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
@@ -236,14 +189,9 @@ class Loader:
|
||||
can introduce it more smoothly and give people the chance to sample the new features even
|
||||
without plugin support. They will be removed once legacy plugins are no longer relevant.
|
||||
"""
|
||||
|
||||
async def load_plugin_main_view(self, request):
|
||||
plugin = self.plugins[request.match_info["name"]]
|
||||
with open(
|
||||
path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html),
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
) as template:
|
||||
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>
|
||||
@@ -267,11 +215,14 @@ class Loader:
|
||||
async def get_steam_resource(self, request):
|
||||
tab = await get_tab("SP")
|
||||
try:
|
||||
return web.Response(
|
||||
text=await tab.get_steam_resource(
|
||||
f"https://steamloopback.host/{request.match_info['path']}"
|
||||
),
|
||||
content_type="text/html",
|
||||
)
|
||||
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,183 @@
|
||||
{
|
||||
"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}}"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"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}}"
|
||||
},
|
||||
"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}}"
|
||||
}
|
||||
},
|
||||
"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": "Χρησιμοποιήστε αυτό το φάκελο"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Use this folder"
|
||||
}
|
||||
},
|
||||
"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}}"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_one": "Modify 1 plugin",
|
||||
"mixed_other": "Modify {{count}} plugins",
|
||||
"update_one": "Update 1 plugin",
|
||||
"update_other": "Update {{count}} plugins",
|
||||
"reinstall_one": "Reinstall 1 plugin",
|
||||
"reinstall_other": "Reinstall {{count}} plugins",
|
||||
"install_one": "Install 1 plugin",
|
||||
"install_other": "Install {{count}} plugins"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Confirm",
|
||||
"loading": "Working"
|
||||
},
|
||||
"confirm": "Are you sure you want to make the following modifications?",
|
||||
"description": {
|
||||
"install": "Install {{name}} {{version}}",
|
||||
"update": "Update {{name}} to {{version}}",
|
||||
"reinstall": "Reinstall {{name}} {{version}}"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "No plugins installed!",
|
||||
"plugin_actions": "Plugin Actions",
|
||||
"reinstall": "Reinstall",
|
||||
"reload": "Reload",
|
||||
"uninstall": "Uninstall",
|
||||
"update_to": "Update to {{name}}",
|
||||
"update_all_one": "Update 1 plugin",
|
||||
"update_all_other": "Update {{count}} plugins"
|
||||
},
|
||||
"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!"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"toast_zip": {
|
||||
"body": "Installation failed! Only ZIP files are supported.",
|
||||
"title": "Decky"
|
||||
},
|
||||
"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": {
|
||||
"desc": "Enables Decky's developer settings.",
|
||||
"label": "Developer mode"
|
||||
},
|
||||
"other": {
|
||||
"header": "Other"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Updates"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Developer",
|
||||
"general_title": "General",
|
||||
"navbar_settings": "Decky Settings",
|
||||
"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,113 @@
|
||||
{
|
||||
"SettingsDeveloperIndex": {
|
||||
"third_party_plugins": {
|
||||
"button_install": "Instalar",
|
||||
"button_zip": "Navegar"
|
||||
},
|
||||
"valve_internal": {
|
||||
"desc2": "No toque nada en este menú a menos que sepa lo que hace."
|
||||
},
|
||||
"toast_zip": {
|
||||
"title": "Decky"
|
||||
}
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Instalar",
|
||||
"button_processing": "Instalando"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Reinstalar",
|
||||
"button_processing": "Reinstalando"
|
||||
},
|
||||
"update": {
|
||||
"button_processing": "Actualizando",
|
||||
"button_idle": "Actualizar"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"disabling": "Desactivando",
|
||||
"enabling": "Activando",
|
||||
"5secreload": "Recargando en 5 segundos"
|
||||
},
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"prerelease": "Prelanzamiento",
|
||||
"stable": "Estable",
|
||||
"label": "Canal de actualización"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Este plugin tiene acceso completo a su Steam Deck.",
|
||||
"plugin_install": "Instalar",
|
||||
"plugin_version_label": "Versión de Plugin"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Usar esta carpeta"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"uninstall": "Desinstalar",
|
||||
"reinstall": "Reinstalar",
|
||||
"reload": "Recargar",
|
||||
"plugin_actions": "Acciónes de Plugin",
|
||||
"no_plugin": "¡No hay plugins instalados!"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"error": "Error",
|
||||
"plugin_uninstall": {
|
||||
"button": "Desinstalar"
|
||||
},
|
||||
"decky_title": "Decky"
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "Permitir acceso no autenticado al CEF debugger a cualquier persona en su red"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"updates": {
|
||||
"header": "Actualizaciones"
|
||||
},
|
||||
"about": {
|
||||
"header": "Información"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Modo Desarrollador"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Desarrollador",
|
||||
"general_title": "General",
|
||||
"navbar_settings": "Ajustes de Decky",
|
||||
"plugins_title": "Plugins"
|
||||
},
|
||||
"Store": {
|
||||
"store_search": {
|
||||
"label": "Buscar"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Ordenar"
|
||||
},
|
||||
"store_contrib": {
|
||||
"desc": "Si desea contribuir a la Tienda de Decky Plugin, revise el repositorio SteamDeckHomebrew/decky-plugin-template en GitHub. Información acerca del desarrollo y distribución está disponible en al archivo README.",
|
||||
"label": "Contribuyendo"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Información",
|
||||
"title": "Navegar"
|
||||
},
|
||||
"store_testing_cta": "¡Por favor considera probando plugins nuevos para ayudar al equipo de Decky Loader!"
|
||||
},
|
||||
"Updater": {
|
||||
"updates": {
|
||||
"reloading": "Recargando",
|
||||
"updating": "Actualizando",
|
||||
"checking": "Buscando",
|
||||
"check_button": "Buscar Actualizaciones",
|
||||
"install_button": "Instalar Actualización",
|
||||
"label": "Actualizaciones"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
{
|
||||
"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é !"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"error": "Erreur",
|
||||
"plugin_error_uninstall": "Allez sur <0></0> 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 !"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "Canale di aggiornamento",
|
||||
"prerelease": "Prerilascio",
|
||||
"stable": "Stabile",
|
||||
"testing": "In prova"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Ricarico in 5 secondi",
|
||||
"disabling": "Disabilito i tools di React",
|
||||
"enabling": "Abilito i tools di React"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Usa questa cartella"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"no_plugin": "Nessun plugin installato!",
|
||||
"plugin_actions": "Operazioni sui plugins",
|
||||
"reinstall": "Reinstalla",
|
||||
"reload": "Ricarica",
|
||||
"uninstall": "Rimuovi",
|
||||
"update_to": "Aggiorna a {{name}}",
|
||||
"update_all_one": "Aggiorna un plugin",
|
||||
"update_all_many": "Aggiorna {{count}} plugins",
|
||||
"update_all_other": "Aggiorna {{count}} plugins"
|
||||
},
|
||||
"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!"
|
||||
},
|
||||
"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": {
|
||||
"header": "Altro",
|
||||
"react_devtools": {
|
||||
"desc": "Abilita la connessione ad un computer che esegue i DevTools di React. Cambiando questa impostazione ricaricherà Steam. Imposta l'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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"mixed_one": "Modifica un plugin",
|
||||
"mixed_many": "Modifica {{count}} plugins",
|
||||
"mixed_other": "Modifica {{count}} plugins",
|
||||
"update_one": "Aggiorna un plugin",
|
||||
"update_many": "Aggiorna {{count}} plugins",
|
||||
"update_other": "Aggiorna {{count}} plugins",
|
||||
"reinstall_one": "Reinstalla un plugin",
|
||||
"reinstall_many": "Reinstalla {{count}} plugins",
|
||||
"reinstall_other": "Reinstalla {{count}} plugins",
|
||||
"install_one": "Installa un plugin",
|
||||
"install_many": "Installa {{count}} plugins",
|
||||
"install_other": "Installa {{count}} plugins"
|
||||
},
|
||||
"confirm": "Sei sicuro di voler effettuare le modifiche seguenti?",
|
||||
"ok_button": {
|
||||
"idle": "Conferma",
|
||||
"loading": "Elaboro"
|
||||
},
|
||||
"description": {
|
||||
"install": "Installa {{name}} {{version}}",
|
||||
"update": "Aggiorna {{name}} alla versione {{version}}",
|
||||
"reinstall": "Reinstalla {{name}} {{version}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"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 <0></0> në Decky menu nëse don ta çinstalojsh këtë shtese."
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"no_plugin": "Nuk ka shtesa të instaluar!",
|
||||
"uninstall": "Çinstalo"
|
||||
},
|
||||
"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}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"prerelease": "发布候选",
|
||||
"stable": "稳定",
|
||||
"testing": "测试",
|
||||
"label": "更新通道"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "5 秒钟后重新加载",
|
||||
"disabling": "正在禁用",
|
||||
"enabling": "正在启用"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"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}}"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"error": "错误",
|
||||
"plugin_error_uninstall": "如果你想卸载插件请点击 Decky 菜单中的 <0></0> 图标",
|
||||
"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 内部开发者"
|
||||
}
|
||||
},
|
||||
"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": "更新中"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"testing": "測試版",
|
||||
"label": "更新頻道",
|
||||
"prerelease": "預發佈",
|
||||
"stable": "穩定版"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "5 秒後重新載入",
|
||||
"disabling": "正在停用",
|
||||
"enabling": "正在啟用"
|
||||
},
|
||||
"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": "重新載入"
|
||||
},
|
||||
"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": "其他"
|
||||
},
|
||||
"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": "正在更新"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+60
-89
@@ -1,95 +1,72 @@
|
||||
# Change PyInstaller files permissions
|
||||
import sys
|
||||
from subprocess import call
|
||||
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
call(["chmod", "-R", "755", sys._MEIPASS])
|
||||
from localplatform import (chmod, chown, service_stop, service_start,
|
||||
ON_WINDOWS, get_log_level, get_live_reload,
|
||||
get_server_port, get_server_host, get_chown_plugin_path,
|
||||
get_unprivileged_user, get_unprivileged_path,
|
||||
get_privileged_path)
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
chmod(sys._MEIPASS, 755)
|
||||
# Full imports
|
||||
from asyncio import new_event_loop, set_event_loop, sleep
|
||||
from logging import basicConfig, getLogger
|
||||
from json import dumps, loads
|
||||
from logging import DEBUG, INFO, basicConfig, getLogger
|
||||
from os import getenv, path
|
||||
from traceback import format_exc
|
||||
import multiprocessing
|
||||
|
||||
import aiohttp_cors
|
||||
|
||||
# Partial imports
|
||||
from aiohttp import client_exceptions
|
||||
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_homebrew_path,
|
||||
get_user,
|
||||
get_user_group,
|
||||
stop_systemd_unit,
|
||||
start_systemd_unit,
|
||||
)
|
||||
from injector import get_gamepadui_tab, Tab, close_old_tabs
|
||||
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
|
||||
mkdir_as_user, get_system_pythonpaths)
|
||||
|
||||
from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs
|
||||
from loader import Loader
|
||||
from settings import SettingsManager
|
||||
from updater import Updater
|
||||
from utilities import Utilities
|
||||
from customtypes import UserType
|
||||
|
||||
USER = get_user()
|
||||
GROUP = get_user_group()
|
||||
HOMEBREW_PATH = get_homebrew_path()
|
||||
CONFIG = {
|
||||
"plugin_path": getenv("PLUGIN_PATH", path.join(HOMEBREW_PATH, "plugins")),
|
||||
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
|
||||
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
||||
"server_port": int(getenv("SERVER_PORT", "1337")),
|
||||
"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"], format="[%(module)s][%(levelname)s]: %(message)s"
|
||||
level=get_log_level(),
|
||||
format="[%(module)s][%(levelname)s]: %(message)s"
|
||||
)
|
||||
|
||||
logger = getLogger("Main")
|
||||
|
||||
plugin_path = path.join(get_privileged_path(), "plugins")
|
||||
|
||||
def chown_plugin_dir():
|
||||
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:"
|
||||
f" {code_chmod})"
|
||||
)
|
||||
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 CONFIG["chown_plugin_path"] is True:
|
||||
if get_chown_plugin_path() == True:
|
||||
chown_plugin_dir()
|
||||
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, loop) -> None:
|
||||
self.loop = loop
|
||||
self.web_app = Application()
|
||||
self.web_app.middlewares.append(csrf_middleware)
|
||||
self.cors = aiohttp_cors.setup(
|
||||
self.web_app,
|
||||
defaults={
|
||||
"https://steamloopback.host": aiohttp_cors.ResourceOptions(
|
||||
expose_headers="*", allow_headers="*", allow_credentials=True
|
||||
)
|
||||
},
|
||||
)
|
||||
self.plugin_loader = Loader(
|
||||
self.web_app, 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.cors = aiohttp_cors.setup(self.web_app, defaults={
|
||||
"https://steamloopback.host": aiohttp_cors.ResourceOptions(
|
||||
expose_headers="*",
|
||||
allow_headers="*",
|
||||
allow_credentials=True
|
||||
)
|
||||
})
|
||||
self.plugin_loader = Loader(self.web_app, plugin_path, self.loop, get_live_reload())
|
||||
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
|
||||
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
|
||||
self.utilities = Utilities(self)
|
||||
self.updater = Updater(self)
|
||||
|
||||
@@ -97,9 +74,9 @@ class PluginManager:
|
||||
|
||||
async def startup(_):
|
||||
if self.settings.getSetting("cef_forward", False):
|
||||
self.loop.create_task(start_systemd_unit(REMOTE_DEBUGGER_UNIT))
|
||||
self.loop.create_task(service_start(REMOTE_DEBUGGER_UNIT))
|
||||
else:
|
||||
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT))
|
||||
self.loop.create_task(service_stop(REMOTE_DEBUGGER_UNIT))
|
||||
self.loop.create_task(self.loader_reinjector())
|
||||
self.loop.create_task(self.load_plugins())
|
||||
|
||||
@@ -110,12 +87,8 @@ class PluginManager:
|
||||
|
||||
for route in list(self.web_app.router.routes()):
|
||||
self.cors.add(route)
|
||||
self.web_app.add_routes(
|
||||
[static("/static", path.join(path.dirname(__file__), "static"))]
|
||||
)
|
||||
self.web_app.add_routes(
|
||||
[static("/legacy", path.join(path.dirname(__file__), "legacy"))]
|
||||
)
|
||||
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
|
||||
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
|
||||
|
||||
def exception_handler(self, loop, context):
|
||||
if context["message"] == "Unclosed connection":
|
||||
@@ -130,6 +103,9 @@ class PluginManager:
|
||||
logger.debug("Loading plugins")
|
||||
self.plugin_loader.import_plugins()
|
||||
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
|
||||
if self.settings.getSetting("pluginOrder", None) == None:
|
||||
self.settings.setSetting("pluginOrder", list(self.plugin_loader.plugins.keys()))
|
||||
logger.debug("Did not find pluginOrder setting, set it to default")
|
||||
|
||||
async def loader_reinjector(self):
|
||||
while True:
|
||||
@@ -139,10 +115,7 @@ class PluginManager:
|
||||
while not tab:
|
||||
try:
|
||||
tab = await get_gamepadui_tab()
|
||||
except (
|
||||
client_exceptions.ClientConnectorError,
|
||||
client_exceptions.ServerDisconnectedError,
|
||||
):
|
||||
except (client_exceptions.ClientConnectorError, client_exceptions.ServerDisconnectedError):
|
||||
if not dc:
|
||||
logger.debug("Couldn't connect to debugger, waiting...")
|
||||
dc = True
|
||||
@@ -173,7 +146,7 @@ class PluginManager:
|
||||
# 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:
|
||||
except Exception as e:
|
||||
logger.error("Exception while reading page events " + format_exc())
|
||||
await tab.close_websocket()
|
||||
pass
|
||||
@@ -189,31 +162,29 @@ class PluginManager:
|
||||
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 Exception:
|
||||
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__":
|
||||
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()
|
||||
|
||||
+62
-152
@@ -1,57 +1,33 @@
|
||||
import multiprocessing
|
||||
from asyncio import (
|
||||
Lock,
|
||||
get_event_loop,
|
||||
new_event_loop,
|
||||
open_unix_connection,
|
||||
set_event_loop,
|
||||
sleep,
|
||||
start_unix_server,
|
||||
IncompleteReadError,
|
||||
LimitOverrunError,
|
||||
)
|
||||
from asyncio import (Lock, get_event_loop, new_event_loop,
|
||||
set_event_loop, sleep)
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from json import dumps, load, loads
|
||||
from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from os import path, setgid, setuid, environ
|
||||
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
|
||||
from localsocket import LocalSocket
|
||||
from localplatform import setgid, setuid, get_username, get_home_path
|
||||
from customtypes import UserType
|
||||
import helpers
|
||||
|
||||
multiprocessing.set_start_method("fork")
|
||||
|
||||
BUFFER_LIMIT = 2**20 # 1 MiB
|
||||
|
||||
|
||||
class PluginWrapper:
|
||||
def __init__(self, file, plugin_directory, plugin_path) -> None:
|
||||
self.file = file
|
||||
self.plugin_path = plugin_path
|
||||
self.plugin_directory = plugin_directory
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
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",
|
||||
encoding="utf-8",
|
||||
)
|
||||
)
|
||||
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
|
||||
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
|
||||
package_json = load(
|
||||
open(
|
||||
path.join(plugin_path, plugin_directory, "package.json"),
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
)
|
||||
)
|
||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
|
||||
self.version = package_json["version"]
|
||||
|
||||
self.legacy = False
|
||||
@@ -77,120 +53,76 @@ class PluginWrapper:
|
||||
set_event_loop(new_event_loop())
|
||||
if self.passive:
|
||||
return
|
||||
setgid(0 if "root" in self.flags else helpers.get_user_group_id())
|
||||
setuid(0 if "root" in self.flags else helpers.get_user_id())
|
||||
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"] = helpers.get_home_path(
|
||||
"root" if "root" in self.flags else helpers.get_user()
|
||||
)
|
||||
environ["USER"] = "root" if "root" in self.flags else helpers.get_user()
|
||||
environ["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"] = helpers.get_user()
|
||||
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
|
||||
)
|
||||
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"])
|
||||
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(
|
||||
environ["DECKY_HOME"], "data", self.plugin_directory
|
||||
)
|
||||
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_RUNTIME_DIR"])
|
||||
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(
|
||||
environ["DECKY_HOME"], "logs", self.plugin_directory
|
||||
)
|
||||
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_LOG_DIR"])
|
||||
environ["DECKY_PLUGIN_DIR"] = path.join(
|
||||
self.plugin_path, self.plugin_directory
|
||||
)
|
||||
environ["DECKY_PLUGIN_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 Exception:
|
||||
except:
|
||||
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
|
||||
exit(0)
|
||||
|
||||
async def _unload(self):
|
||||
try:
|
||||
self.log.info(
|
||||
"Attempting to unload with plugin "
|
||||
+ self.name
|
||||
+ '\'s "_unload" function.\n'
|
||||
)
|
||||
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 Exception:
|
||||
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 _setup_socket(self):
|
||||
self.socket = await start_unix_server(
|
||||
self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT
|
||||
)
|
||||
async def _on_new_message(self, message : str) -> str|None:
|
||||
data = loads(message)
|
||||
|
||||
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:
|
||||
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()
|
||||
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, ensure_ascii=False) + "\n").encode("utf-8"))
|
||||
await writer.drain()
|
||||
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")
|
||||
|
||||
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 Exception:
|
||||
await sleep(2)
|
||||
retries += 1
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
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:
|
||||
@@ -203,44 +135,22 @@ class PluginWrapper:
|
||||
return
|
||||
|
||||
async def _(self):
|
||||
if await self._open_socket_if_not_exists():
|
||||
self.writer.write(
|
||||
(dumps({"stop": True}, ensure_ascii=False) + "\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)"
|
||||
)
|
||||
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}, ensure_ascii=False
|
||||
)
|
||||
+ "\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"]
|
||||
@@ -1,15 +0,0 @@
|
||||
[flake8]
|
||||
max-line-length = 88
|
||||
|
||||
[tool.ruff]
|
||||
ignore = [
|
||||
# Ignore line length check and let Black handle it
|
||||
"E501",
|
||||
|
||||
# Ignore SyntaxError due to ruff not supporting pattern matching
|
||||
# https://github.com/charliermarsh/ruff/issues/282
|
||||
"E999",
|
||||
]
|
||||
|
||||
# Assume Python 3.10.
|
||||
target-version = "py310"
|
||||
+16
-20
@@ -1,45 +1,41 @@
|
||||
from json import dump, load
|
||||
from os import mkdir, path, listdir, rename
|
||||
from shutil import chown
|
||||
from localplatform import chown, folder_owner, get_chown_plugin_path
|
||||
from customtypes import UserType
|
||||
|
||||
from helpers import (
|
||||
get_homebrew_path,
|
||||
get_user,
|
||||
get_user_group,
|
||||
get_user_owner,
|
||||
)
|
||||
from helpers import get_homebrew_path
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
def __init__(self, name, settings_directory=None) -> None:
|
||||
USER = get_user()
|
||||
GROUP = get_user_group()
|
||||
def __init__(self, name, settings_directory = None) -> None:
|
||||
wrong_dir = get_homebrew_path()
|
||||
if settings_directory is None:
|
||||
if settings_directory == None:
|
||||
settings_directory = path.join(wrong_dir, "settings")
|
||||
|
||||
self.path = path.join(settings_directory, name + ".json")
|
||||
|
||||
# Create the folder with the correct permission
|
||||
#Create the folder with the correct permission
|
||||
if not path.exists(settings_directory):
|
||||
mkdir(settings_directory)
|
||||
chown(settings_directory, USER, GROUP)
|
||||
|
||||
# Copy all old settings file in the root directory to the correct folder
|
||||
#Copy all old settings file in the root directory to the correct folder
|
||||
for file in listdir(wrong_dir):
|
||||
if file.endswith(".json"):
|
||||
rename(path.join(wrong_dir, file), path.join(settings_directory, file))
|
||||
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:
|
||||
if get_user_owner(settings_directory) != USER:
|
||||
chown(settings_directory, USER, GROUP)
|
||||
|
||||
#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", encoding="utf-8")
|
||||
except FileExistsError:
|
||||
except FileExistsError as e:
|
||||
self.read()
|
||||
pass
|
||||
|
||||
@@ -55,7 +51,7 @@ class SettingsManager:
|
||||
with open(self.path, "w+", encoding="utf-8") as file:
|
||||
dump(self.settings, file, indent=4, ensure_ascii=False)
|
||||
|
||||
def getSetting(self, key, default):
|
||||
def getSetting(self, key, default=None):
|
||||
return self.settings.get(key, default)
|
||||
|
||||
def setSetting(self, key, value):
|
||||
|
||||
+78
-120
@@ -6,7 +6,7 @@ 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
|
||||
|
||||
@@ -16,7 +16,6 @@ from settings import SettingsManager
|
||||
|
||||
logger = getLogger("Updater")
|
||||
|
||||
|
||||
class Updater:
|
||||
def __init__(self, context) -> None:
|
||||
self.context = context
|
||||
@@ -27,27 +26,22 @@ class Updater:
|
||||
"get_version": self.get_version,
|
||||
"do_update": self.do_update,
|
||||
"do_restart": self.do_restart,
|
||||
"check_for_updates": self.check_for_updates,
|
||||
"check_for_updates": self.check_for_updates
|
||||
}
|
||||
self.remoteVer = None
|
||||
self.allRemoteVers = None
|
||||
try:
|
||||
self.localVer = helpers.get_loader_version()
|
||||
except:
|
||||
self.localVer = False
|
||||
self.localVer = helpers.get_loader_version()
|
||||
|
||||
try:
|
||||
self.currentBranch = self.get_branch(self.context.settings)
|
||||
except:
|
||||
self.currentBranch = 0
|
||||
logger.error(
|
||||
'Current branch could not be determined, defaulting to "Stable"'
|
||||
)
|
||||
logger.error("Current branch could not be determined, defaulting to \"Stable\"")
|
||||
|
||||
if context:
|
||||
context.web_app.add_routes(
|
||||
[web.post("/updater/{method_name}", self._handle_server_method_call)]
|
||||
)
|
||||
context.web_app.add_routes([
|
||||
web.post("/updater/{method_name}", self._handle_server_method_call)
|
||||
])
|
||||
context.loop.create_task(self.version_reloader())
|
||||
|
||||
async def _handle_server_method_call(self, request):
|
||||
@@ -71,7 +65,7 @@ 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")
|
||||
return 1
|
||||
else:
|
||||
@@ -92,71 +86,47 @@ class Updater:
|
||||
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!"
|
||||
)
|
||||
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")
|
||||
selectedBranch = self.get_branch(self.context.settings)
|
||||
async with ClientSession() as web:
|
||||
async with web.request(
|
||||
"GET",
|
||||
"https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases",
|
||||
ssl=helpers.get_ssl_context(),
|
||||
) as res:
|
||||
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()
|
||||
if selectedBranch == 0:
|
||||
logger.debug("release type: release")
|
||||
remoteVersions = list(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and not ver["tag_name"].find("-pre") > 0 and ver["tag_name"], remoteVersions))
|
||||
elif selectedBranch == 1:
|
||||
logger.debug("release type: pre-release")
|
||||
remoteVersions = list(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions))
|
||||
else:
|
||||
logger.error("release type: NOT FOUND")
|
||||
raise ValueError("no valid branch found")
|
||||
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,
|
||||
)
|
||||
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["prerelease"]
|
||||
and ver["tag_name"].startswith("v")
|
||||
and ver["tag_name"].find("-pre"),
|
||||
remoteVersions,
|
||||
),
|
||||
None,
|
||||
)
|
||||
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
|
||||
)
|
||||
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
|
||||
return await self.get_version()
|
||||
|
||||
async def version_reloader(self):
|
||||
@@ -166,65 +136,59 @@ class Updater:
|
||||
await self.check_for_updates()
|
||||
except:
|
||||
pass
|
||||
await sleep(60 * 60 * 6) # 6 hours
|
||||
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"
|
||||
|
||||
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:
|
||||
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))
|
||||
# we need to not delete the binary until we have downloaded the new binary!
|
||||
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:
|
||||
remove(path.join(getcwd(), "PluginLoader"))
|
||||
except:
|
||||
pass
|
||||
with open(path.join(getcwd(), "PluginLoader"), "wb") as out:
|
||||
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))
|
||||
with open(path.join(getcwd(), download_temp_filename), "wb") as out:
|
||||
progress = 0
|
||||
raw = 0
|
||||
async for c in res.content.iter_chunked(512):
|
||||
@@ -232,27 +196,21 @@ class Updater:
|
||||
raw += len(c)
|
||||
new_progress = round((raw / total) * 100)
|
||||
if progress != new_progress:
|
||||
self.context.loop.create_task(
|
||||
tab.evaluate_js(
|
||||
f"window.DeckyUpdater.updateProgress({new_progress})",
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
)
|
||||
)
|
||||
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", encoding="utf-8"
|
||||
) as out:
|
||||
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 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")
|
||||
|
||||
+87
-57
@@ -3,13 +3,14 @@ import os
|
||||
from json.decoder import JSONDecodeError
|
||||
from traceback import format_exc
|
||||
|
||||
from asyncio import start_server, gather, open_connection
|
||||
from asyncio import sleep, start_server, gather, open_connection
|
||||
from aiohttp import ClientSession, web
|
||||
|
||||
from logging import getLogger
|
||||
from injector import inject_to_tab, get_gamepadui_tab, close_old_tabs
|
||||
from injector import inject_to_tab, get_gamepadui_tab, close_old_tabs, get_tab
|
||||
import helpers
|
||||
|
||||
import subprocess
|
||||
from localplatform import service_stop, service_start
|
||||
|
||||
class Utilities:
|
||||
def __init__(self, context) -> None:
|
||||
@@ -18,6 +19,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,
|
||||
@@ -31,6 +33,7 @@ class Utilities:
|
||||
"filepicker_ls": self.filepicker_ls,
|
||||
"disable_rdt": self.disable_rdt,
|
||||
"enable_rdt": self.enable_rdt,
|
||||
"get_tab_id": self.get_tab_id
|
||||
}
|
||||
|
||||
self.logger = getLogger("Utilities")
|
||||
@@ -40,9 +43,9 @@ class Utilities:
|
||||
self.rdt_proxy_task = None
|
||||
|
||||
if context:
|
||||
context.web_app.add_routes(
|
||||
[web.post("/methods/{method_name}", self._handle_server_method_call)]
|
||||
)
|
||||
context.web_app.add_routes([
|
||||
web.post("/methods/{method_name}", self._handle_server_method_call)
|
||||
])
|
||||
|
||||
async def _handle_server_method_call(self, request):
|
||||
method_name = request.match_info["method_name"]
|
||||
@@ -60,11 +63,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
|
||||
artifact=artifact,
|
||||
name=name,
|
||||
version=version,
|
||||
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):
|
||||
@@ -78,11 +88,13 @@ class Utilities:
|
||||
|
||||
async def http_request(self, method="", url="", **kwargs):
|
||||
async with ClientSession() as web:
|
||||
res = await web.request(
|
||||
method, url, ssl=helpers.get_ssl_context(), **kwargs
|
||||
)
|
||||
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}
|
||||
return {
|
||||
"status": res.status,
|
||||
"headers": dict(res.headers),
|
||||
"body": text
|
||||
}
|
||||
|
||||
async def ping(self, **kwargs):
|
||||
return "pong"
|
||||
@@ -91,18 +103,26 @@ class Utilities:
|
||||
try:
|
||||
result = await inject_to_tab(tab, code, run_async)
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {"success": False, "result": result["result"]}
|
||||
return {
|
||||
"success": False,
|
||||
"result": result["result"]
|
||||
}
|
||||
|
||||
return {"success": True, "result": result["result"]["result"].get("value")}
|
||||
return {
|
||||
"success": True,
|
||||
"result": result["result"]["result"].get("value")
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "result": e}
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def inject_css_into_tab(self, tab, style):
|
||||
try:
|
||||
css_id = str(uuid.uuid4())
|
||||
|
||||
result = await inject_to_tab(
|
||||
tab,
|
||||
result = await inject_to_tab(tab,
|
||||
f"""
|
||||
(function() {{
|
||||
const style = document.createElement('style');
|
||||
@@ -110,21 +130,27 @@ class Utilities:
|
||||
document.head.append(style);
|
||||
style.textContent = `{style}`;
|
||||
}})()
|
||||
""",
|
||||
False,
|
||||
)
|
||||
""", False)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {"success": False, "result": result["result"]}
|
||||
return {
|
||||
"success": False,
|
||||
"result": result["result"]
|
||||
}
|
||||
|
||||
return {"success": True, "result": css_id}
|
||||
return {
|
||||
"success": True,
|
||||
"result": css_id
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "result": e}
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def remove_css_from_tab(self, tab, css_id):
|
||||
try:
|
||||
result = await inject_to_tab(
|
||||
tab,
|
||||
result = await inject_to_tab(tab,
|
||||
f"""
|
||||
(function() {{
|
||||
let style = document.getElementById("{css_id}");
|
||||
@@ -132,16 +158,22 @@ class Utilities:
|
||||
if (style.nodeName.toLowerCase() == 'style')
|
||||
style.parentNode.removeChild(style);
|
||||
}})()
|
||||
""",
|
||||
False,
|
||||
)
|
||||
""", False)
|
||||
|
||||
if "exceptionDetails" in result["result"]:
|
||||
return {"success": False, "result": result}
|
||||
return {
|
||||
"success": False,
|
||||
"result": result
|
||||
}
|
||||
|
||||
return {"success": True}
|
||||
return {
|
||||
"success": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "result": e}
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def get_setting(self, key, default):
|
||||
return self.context.settings.getSetting(key, default)
|
||||
@@ -150,11 +182,11 @@ 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):
|
||||
@@ -163,7 +195,7 @@ class Utilities:
|
||||
# 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
|
||||
file_names = sorted(os.listdir(path)) # Alphabetical
|
||||
|
||||
files = []
|
||||
|
||||
@@ -172,15 +204,16 @@ class Utilities:
|
||||
is_dir = os.path.isdir(full_path)
|
||||
|
||||
if is_dir or include_files:
|
||||
files.append(
|
||||
{
|
||||
"isdir": is_dir,
|
||||
"name": file,
|
||||
"realpath": os.path.realpath(full_path),
|
||||
}
|
||||
)
|
||||
files.append({
|
||||
"isdir": is_dir,
|
||||
"name": file,
|
||||
"realpath": os.path.realpath(full_path)
|
||||
})
|
||||
|
||||
return {"realpath": os.path.realpath(path), "files": files}
|
||||
return {
|
||||
"realpath": os.path.realpath(path),
|
||||
"files": files
|
||||
}
|
||||
|
||||
# Based on https://stackoverflow.com/a/46422554/13174603
|
||||
def start_rdt_proxy(self, ip, port):
|
||||
@@ -190,10 +223,10 @@ class Utilities:
|
||||
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)
|
||||
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)
|
||||
@@ -214,14 +247,11 @@ class Utilities:
|
||||
self.stop_rdt_proxy()
|
||||
ip = self.context.settings.getSetting("developer.rdt.ip", None)
|
||||
|
||||
if ip is not 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 = (
|
||||
"""
|
||||
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
|
||||
@@ -229,13 +259,10 @@ class Utilities:
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return FocusNavController?.m_ActiveContext?.ActiveWindow || window;
|
||||
return (GamepadNavTree?.m_context?.m_controller || FocusNavController)?.m_ActiveContext?.ActiveWindow || window;
|
||||
}
|
||||
});
|
||||
"""
|
||||
+ await res.text()
|
||||
+ "\n}"
|
||||
)
|
||||
""" + await res.text() + "\n}"
|
||||
if res.status != 200:
|
||||
self.logger.error("Failed to connect to React DevTools at " + ip)
|
||||
return False
|
||||
@@ -261,3 +288,6 @@ class Utilities:
|
||||
await close_old_tabs()
|
||||
await tab.evaluate_js("location.reload();", False, True, False)
|
||||
self.logger.info("React DevTools disabled")
|
||||
|
||||
async def get_tab_id(self, name):
|
||||
return (await get_tab(name)).id
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@ Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Vendored
+2
-1
@@ -9,7 +9,8 @@ Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
@@ -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 |
@@ -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', 'it-IT'],
|
||||
// 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,
|
||||
// }
|
||||
}
|
||||
+17
-13
@@ -12,28 +12,29 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^21.1.0",
|
||||
"@rollup/plugin-image": "^3.0.1",
|
||||
"@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": "^7.9.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"
|
||||
"tslib": "^2.5.2",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"importSort": {
|
||||
".js, .jsx, .ts, .tsx": {
|
||||
@@ -42,10 +43,13 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "^3.18.10",
|
||||
"react-file-icon": "^1.2.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"decky-frontend-lib": "3.21.1",
|
||||
"i18next": "^22.5.0",
|
||||
"i18next-http-backend": "^2.2.1",
|
||||
"react-file-icon": "^1.3.0",
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"remark-gfm": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2072
-835
File diff suppressed because it is too large
Load Diff
@@ -11,11 +11,13 @@ import externalGlobals from 'rollup-plugin-external-globals';
|
||||
const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL'];
|
||||
|
||||
export default defineConfig({
|
||||
input: 'src/index.tsx',
|
||||
input: 'src/index.ts',
|
||||
plugins: [
|
||||
del({ targets: '../backend/static/*', force: true }),
|
||||
commonjs(),
|
||||
nodeResolve(),
|
||||
nodeResolve({
|
||||
browser: true
|
||||
}),
|
||||
externalGlobals({
|
||||
react: 'SP_REACT',
|
||||
'react-dom': 'SP_REACTDOM',
|
||||
@@ -44,4 +46,4 @@ export default defineConfig({
|
||||
if (hiddenWarnings.some((warning) => message.code === warning)) return;
|
||||
handleWarning(message);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import { VerInfo } from '../updater';
|
||||
|
||||
interface PublicDeckyState {
|
||||
plugins: Plugin[];
|
||||
pluginOrder: string[];
|
||||
activePlugin: Plugin | null;
|
||||
updates: PluginUpdateMapping | null;
|
||||
hasLoaderUpdate?: boolean;
|
||||
@@ -15,6 +16,7 @@ interface PublicDeckyState {
|
||||
|
||||
export class DeckyState {
|
||||
private _plugins: Plugin[] = [];
|
||||
private _pluginOrder: string[] = [];
|
||||
private _activePlugin: Plugin | null = null;
|
||||
private _updates: PluginUpdateMapping | null = null;
|
||||
private _hasLoaderUpdate: boolean = false;
|
||||
@@ -26,6 +28,7 @@ export class DeckyState {
|
||||
publicState(): PublicDeckyState {
|
||||
return {
|
||||
plugins: this._plugins,
|
||||
pluginOrder: this._pluginOrder,
|
||||
activePlugin: this._activePlugin,
|
||||
updates: this._updates,
|
||||
hasLoaderUpdate: this._hasLoaderUpdate,
|
||||
@@ -44,6 +47,11 @@ export class DeckyState {
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setPluginOrder(pluginOrder: string[]) {
|
||||
this._pluginOrder = pluginOrder;
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
setActivePlugin(name: string) {
|
||||
this._activePlugin = this._plugins.find((plugin) => plugin.name === name) ?? null;
|
||||
this.notifyUpdate();
|
||||
@@ -78,6 +86,7 @@ interface DeckyStateContext extends PublicDeckyState {
|
||||
setVersionInfo(versionInfo: VerInfo): void;
|
||||
setIsLoaderUpdating(hasUpdate: boolean): void;
|
||||
setActivePlugin(name: string): void;
|
||||
setPluginOrder(pluginOrder: string[]): void;
|
||||
closeActivePlugin(): void;
|
||||
}
|
||||
|
||||
@@ -106,10 +115,18 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
|
||||
const setVersionInfo = (versionInfo: VerInfo) => deckyState.setVersionInfo(versionInfo);
|
||||
const setActivePlugin = (name: string) => deckyState.setActivePlugin(name);
|
||||
const closeActivePlugin = () => deckyState.closeActivePlugin();
|
||||
const setPluginOrder = (pluginOrder: string[]) => deckyState.setPluginOrder(pluginOrder);
|
||||
|
||||
return (
|
||||
<DeckyStateContext.Provider
|
||||
value={{ ...publicDeckyState, setIsLoaderUpdating, setVersionInfo, setActivePlugin, closeActivePlugin }}
|
||||
value={{
|
||||
...publicDeckyState,
|
||||
setIsLoaderUpdating,
|
||||
setVersionInfo,
|
||||
setActivePlugin,
|
||||
closeActivePlugin,
|
||||
setPluginOrder,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DeckyStateContext.Provider>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Focusable, Router } 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';
|
||||
@@ -22,7 +22,7 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
|
||||
onActivate={() => {}}
|
||||
onOKButton={() => {
|
||||
props.onDismiss?.();
|
||||
Router.NavigateToExternalWeb(aRef.current!.href);
|
||||
Navigation.NavigateToExternalWeb(aRef.current!.href);
|
||||
}}
|
||||
style={{ display: 'inline' }}
|
||||
>
|
||||
|
||||
@@ -7,17 +7,27 @@ import {
|
||||
scrollClasses,
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { VFC } from 'react';
|
||||
import { VFC, useEffect, useState } from 'react';
|
||||
|
||||
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 { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
|
||||
const visible = useQuickAccessVisible();
|
||||
|
||||
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 (
|
||||
<Focusable onCancelButton={closeActivePlugin}>
|
||||
@@ -36,7 +46,7 @@ const PluginView: VFC = () => {
|
||||
<TitleView />
|
||||
<div className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}>
|
||||
<PanelSection>
|
||||
{plugins
|
||||
{pluginList
|
||||
.filter((p) => p.content)
|
||||
.map(({ name, icon }) => (
|
||||
<PanelSectionRow key={name}>
|
||||
|
||||
@@ -4,15 +4,11 @@ const QuickAccessVisibleState = createContext<boolean>(true);
|
||||
|
||||
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
|
||||
|
||||
export const QuickAccessVisibleStateProvider: FC<{ initial: boolean; setter: ((val: boolean) => {}[]) | never[] }> = ({
|
||||
children,
|
||||
initial,
|
||||
setter,
|
||||
}) => {
|
||||
export const QuickAccessVisibleStateProvider: FC<{ initial: boolean; tab: any }> = ({ children, initial, tab }) => {
|
||||
const [visible, setVisible] = useState<boolean>(initial);
|
||||
const [prev, setPrev] = useState<boolean>(initial);
|
||||
// hack to use an array as a "pointer" to pass the setter up the tree
|
||||
setter[0] = setVisible;
|
||||
// HACK but i can't think of a better way to do this
|
||||
tab.qAMVisibilitySetter = setVisible;
|
||||
if (initial != prev) {
|
||||
setPrev(initial);
|
||||
setVisible(initial);
|
||||
|
||||
@@ -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, 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}
|
||||
@@ -26,14 +39,48 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
|
||||
onCancel={async () => {
|
||||
await onCancel();
|
||||
}}
|
||||
strTitle={`Install ${artifact}`}
|
||||
strOKButtonText={loading ? 'Installing' : 'Install'}
|
||||
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>
|
||||
)
|
||||
}
|
||||
>
|
||||
{hash == 'False' ? (
|
||||
<h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3>
|
||||
) : (
|
||||
`Are you sure you want to install ${artifact} ${version}?`
|
||||
)}
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DialogButton, Focusable, SteamSpinner, TextField } from 'decky-frontend
|
||||
import { useEffect } from 'react';
|
||||
import { FunctionComponent, 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';
|
||||
@@ -47,6 +48,7 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
|
||||
onSubmit,
|
||||
closeModal,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
if (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 });
|
||||
@@ -134,7 +136,15 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{file.name}
|
||||
<span
|
||||
style={{
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
</DialogButton>
|
||||
);
|
||||
@@ -150,7 +160,7 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
|
||||
closeModal?.();
|
||||
}}
|
||||
>
|
||||
Use this folder
|
||||
{t('FilePickerIndex.folder.select')}
|
||||
</DialogButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,6 @@ import Logger from '../../../../logger';
|
||||
|
||||
const logger = new Logger('LibraryPatch');
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
SteamClient: any;
|
||||
appDetailsStore: any;
|
||||
}
|
||||
}
|
||||
|
||||
let patch: Patch;
|
||||
|
||||
function rePatch() {
|
||||
@@ -20,7 +13,9 @@ function rePatch() {
|
||||
const details = window.appDetailsStore.GetAppDetails(appid);
|
||||
logger.debug('game details', details);
|
||||
// strShortcutStartDir
|
||||
const file = await window.DeckyPluginLoader.openFilePicker(details.strShortcutStartDir.replaceAll('"', ''));
|
||||
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('/');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -12,22 +13,23 @@ const DeveloperSettings = lazy(() => import('./pages/developer'));
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [isDeveloper, setIsDeveloper] = useSetting<boolean>('developer.enabled', false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pages = [
|
||||
{
|
||||
title: 'Decky',
|
||||
title: t('SettingsIndex.general_title'),
|
||||
content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
|
||||
route: '/decky/settings/general',
|
||||
icon: <DeckyIcon />,
|
||||
},
|
||||
{
|
||||
title: 'Plugins',
|
||||
title: t('SettingsIndex.plugins_title'),
|
||||
content: <PluginList />,
|
||||
route: '/decky/settings/plugins',
|
||||
icon: <FaPlug />,
|
||||
},
|
||||
{
|
||||
title: 'Developer',
|
||||
title: t('SettingsIndex.developer_title'),
|
||||
content: (
|
||||
<WithSuspense>
|
||||
<DeveloperSettings />
|
||||
|
||||
@@ -1,64 +1,149 @@
|
||||
import { DialogBody, Field, TextField, Toggle } from 'decky-frontend-lib';
|
||||
import { useRef } from 'react';
|
||||
import { FaReact, FaSteamSymbol } from 'react-icons/fa';
|
||||
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 { installFromURL } from '../../../../store';
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
import RemoteDebuggingSettings from '../general/RemoteDebugging';
|
||||
|
||||
const installFromZip = () => {
|
||||
window.DeckyPluginLoader.openFilePicker('/home/deck', true).then((val) => {
|
||||
const url = `file://${val.path}`;
|
||||
console.log(`Installing plugin locally from ${url}`);
|
||||
|
||||
if (url.endsWith('.zip')) {
|
||||
installFromURL(url);
|
||||
} else {
|
||||
window.DeckyPluginLoader.toaster.toast({
|
||||
//title: t('SettingsDeveloperIndex.toast_zip.title'),
|
||||
title: 'Decky',
|
||||
//body: t('SettingsDeveloperIndex.toast_zip.body'),
|
||||
body: 'Installation failed! Only ZIP files are supported.',
|
||||
onClick: installFromZip,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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>
|
||||
<RemoteDebuggingSettings />
|
||||
<Field
|
||||
label="Enable Valve Internal"
|
||||
description={
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
Enables the Valve internal developer menu.{' '}
|
||||
<span style={{ color: 'red' }}>Do not touch anything in this menu unless you know what it does.</span>
|
||||
</span>
|
||||
}
|
||||
icon={<FaSteamSymbol style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={enableValveInternal}
|
||||
onChange={(toggleValue) => {
|
||||
setEnableValveInternal(toggleValue);
|
||||
setShowValveInternal(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Enable React DevTools"
|
||||
description={
|
||||
<>
|
||||
<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' }}>
|
||||
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the
|
||||
IP address before enabling.
|
||||
{t('SettingsDeveloperIndex.valve_internal.desc1')}{' '}
|
||||
<span style={{ color: 'red' }}>{t('SettingsDeveloperIndex.valve_internal.desc2')}</span>
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<div ref={textRef}>
|
||||
<TextField label={'IP'} 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>
|
||||
}
|
||||
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';
|
||||
@@ -14,17 +15,23 @@ enum UpdateBranch {
|
||||
}
|
||||
|
||||
const BranchSelect: FunctionComponent<{}> = () => {
|
||||
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.Prerelease);
|
||||
|
||||
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="Decky Update Channel" childrenContainerWidth={'fixed'}>
|
||||
<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}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { Field, Toggle } from 'decky-frontend-lib';
|
||||
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' }}>
|
||||
Allows unauthenticated access to the CEF debugger to anyone in your network.
|
||||
</span>
|
||||
}
|
||||
label={t('RemoteDebugging.remote_cef.label')}
|
||||
description={<span style={{ whiteSpace: 'pre-line' }}>{t('RemoteDebugging.remote_cef.desc')}</span>}
|
||||
icon={<FaChrome style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -11,17 +12,23 @@ 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="Plugin Store Channel" childrenContainerWidth={'fixed'}>
|
||||
<Field label={t('StoreSelect.store_channel.label')} childrenContainerWidth={'fixed'}>
|
||||
<Dropdown
|
||||
rgOptions={Object.values(Store)
|
||||
.filter((store) => typeof store == 'string')
|
||||
.map((store) => ({
|
||||
label: store,
|
||||
label: tStores[Store[store]],
|
||||
data: Store[store],
|
||||
}))}
|
||||
selectedOption={selectedStore}
|
||||
@@ -33,11 +40,11 @@ const StoreSelect: FunctionComponent<{}> = () => {
|
||||
</Field>
|
||||
{selectedStore == Store.Custom && (
|
||||
<Field
|
||||
label="Custom Store"
|
||||
label={t('StoreSelect.custom_store.label')}
|
||||
indentLevel={1}
|
||||
description={
|
||||
<TextField
|
||||
label={'URL'}
|
||||
label={t('StoreSelect.custom_store.url_label')}
|
||||
value={selectedStoreURL || undefined}
|
||||
onChange={(e) => setSelectedStoreURL(e?.target.value || null)}
|
||||
/>
|
||||
|
||||
@@ -6,15 +6,16 @@ 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 { useTranslation } from 'react-i18next';
|
||||
import { FaExclamation } from 'react-icons/fa';
|
||||
|
||||
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
|
||||
import { findSP } from '../../../../utils/windows';
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
import InlinePatchNotes from '../../../patchnotes/InlinePatchNotes';
|
||||
import WithSuspense from '../../../WithSuspense';
|
||||
@@ -23,6 +24,7 @@ const MarkdownRenderer = lazy(() => import('../../../Markdown'));
|
||||
|
||||
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
|
||||
const SP = findSP();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Focusable onCancelButton={closeModal}>
|
||||
<FocusRing>
|
||||
@@ -39,13 +41,13 @@ 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>
|
||||
@@ -58,7 +60,7 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
|
||||
initialColumn={0}
|
||||
autoFocus={true}
|
||||
fnGetColumnWidth={() => SP.innerWidth}
|
||||
name="Decky Updates"
|
||||
name={t('Updater.decky_updates') as string}
|
||||
/>
|
||||
</FocusRing>
|
||||
</Focusable>
|
||||
@@ -72,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) => {
|
||||
@@ -93,14 +97,14 @@ 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="Decky Updates"
|
||||
label={t('Updater.updates.label')}
|
||||
description={
|
||||
checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? (
|
||||
''
|
||||
) : (
|
||||
<span>Up to date: running {versionInfo?.current}</span>
|
||||
<span>{t('Updater.updates.lat_version', { ver: versionInfo?.current })} </span>
|
||||
)
|
||||
}
|
||||
icon={
|
||||
@@ -129,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
|
||||
@@ -140,7 +144,7 @@ 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>
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import {
|
||||
DialogBody,
|
||||
DialogButton,
|
||||
DialogControlsSection,
|
||||
DialogControlsSectionHeader,
|
||||
Field,
|
||||
TextField,
|
||||
Toggle,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useState } from 'react';
|
||||
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 StoreSelect from './StoreSelect';
|
||||
@@ -22,23 +13,23 @@ export default function GeneralSettings({
|
||||
isDeveloper: boolean;
|
||||
setIsDeveloper: (val: boolean) => void;
|
||||
}) {
|
||||
const [pluginURL, setPluginURL] = useState('');
|
||||
const { versionInfo } = useDeckyState();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DialogBody>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Updates</DialogControlsSectionHeader>
|
||||
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.updates.header')}</DialogControlsSectionHeader>
|
||||
<UpdaterSettings />
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Beta Participation</DialogControlsSectionHeader>
|
||||
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.beta.header')}</DialogControlsSectionHeader>
|
||||
<BranchSelect />
|
||||
<StoreSelect />
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Other</DialogControlsSectionHeader>
|
||||
<Field label="Enable Developer Mode">
|
||||
<DialogControlsSectionHeader>{t('SettingsGeneralIndex.other.header')}</DialogControlsSectionHeader>
|
||||
<Field label={t('SettingsGeneralIndex.developer_mode.label')}>
|
||||
<Toggle
|
||||
value={isDeveloper}
|
||||
onChange={(toggleValue) => {
|
||||
@@ -46,18 +37,10 @@ export default function GeneralSettings({
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Install plugin from URL"
|
||||
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
||||
>
|
||||
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
||||
Install
|
||||
</DialogButton>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>About</DialogControlsSectionHeader>
|
||||
<Field label="Decky Version" focusable={true}>
|
||||
<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>
|
||||
|
||||
@@ -2,78 +2,205 @@ import {
|
||||
DialogBody,
|
||||
DialogButton,
|
||||
DialogControlsSection,
|
||||
Focusable,
|
||||
GamepadEvent,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ReorderableEntry,
|
||||
ReorderableList,
|
||||
showContextMenu,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useEffect } from 'react';
|
||||
import { FaDownload, FaEllipsisH } from 'react-icons/fa';
|
||||
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';
|
||||
|
||||
function labelToName(pluginLabel: string, pluginVersion?: string): string {
|
||||
return pluginVersion ? pluginLabel.substring(0, pluginLabel.indexOf(` - ${pluginVersion}`)) : pluginLabel;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
|
||||
const data = props.entry.data;
|
||||
const { t } = useTranslation();
|
||||
let pluginName = labelToName(props.entry.label, data?.version);
|
||||
|
||||
const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
|
||||
showContextMenu(
|
||||
<Menu label={t('PluginListIndex.plugin_actions')}>
|
||||
<MenuItem
|
||||
onSelected={() => {
|
||||
try {
|
||||
fetch(`http://127.0.0.1:1337/plugins/${pluginName}/reload`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error Reloading Plugin Backend', err);
|
||||
}
|
||||
|
||||
window.DeckyPluginLoader.importPlugin(pluginName, data?.version);
|
||||
}}
|
||||
>
|
||||
{t('PluginListIndex.reload')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelected={() =>
|
||||
window.DeckyPluginLoader.uninstallPlugin(
|
||||
pluginName,
|
||||
t('PluginLoader.plugin_uninstall.title', { name: pluginName }),
|
||||
t('PluginLoader.plugin_uninstall.button'),
|
||||
t('PluginLoader.plugin_uninstall.desc', { name: pluginName }),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('PluginListIndex.uninstall')}
|
||||
</MenuItem>
|
||||
</Menu>,
|
||||
e.currentTarget ?? window,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.update ? (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)}
|
||||
onOKButton={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)}
|
||||
>
|
||||
<div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{t('PluginListIndex.update_to', { name: data?.update?.name })}
|
||||
<FaDownload style={{ paddingLeft: '1rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
) : (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => reinstallPlugin(pluginName, data?.version)}
|
||||
onOKButton={() => reinstallPlugin(pluginName, data?.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 } = useDeckyState();
|
||||
const [_, setPluginOrderSetting] = useSetting<string[]>(
|
||||
'pluginOrder',
|
||||
plugins.map((plugin) => plugin.name),
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
window.DeckyPluginLoader.checkPluginUpdates();
|
||||
}, []);
|
||||
|
||||
const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginData>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setPluginEntries(
|
||||
plugins.map((plugin) => {
|
||||
return {
|
||||
label: plugin.version ? `${plugin.name} - ${plugin.version}` : plugin.name,
|
||||
data: {
|
||||
update: updates?.get(plugin.name),
|
||||
version: plugin.version,
|
||||
},
|
||||
position: pluginOrder.indexOf(plugin.name),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}, [plugins, updates]);
|
||||
|
||||
if (plugins.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<p>No plugins installed</p>
|
||||
<p>{t('PluginListIndex.no_plugin')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function onSave(entries: ReorderableEntry<PluginData>[]) {
|
||||
const newOrder = entries.map((entry) => labelToName(entry.label, entry?.data?.version));
|
||||
console.log(newOrder);
|
||||
setPluginOrder(newOrder);
|
||||
setPluginOrderSetting(newOrder);
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogBody>
|
||||
<DialogControlsSection>
|
||||
<ul style={{ listStyleType: 'none', padding: '0' }}>
|
||||
{plugins.map(({ name, version }) => {
|
||||
const update = updates?.get(name);
|
||||
return (
|
||||
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
|
||||
<span>
|
||||
{name} <span style={{ opacity: '50%' }}>{'(' + version + ')'}</span>
|
||||
</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>
|
||||
{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<PluginData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} />
|
||||
</DialogControlsSection>
|
||||
</DialogBody>
|
||||
);
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
SingleDropdownOption,
|
||||
SuspensefulImage,
|
||||
} from 'decky-frontend-lib';
|
||||
import { FC, useState } from 'react';
|
||||
import { CSSProperties, FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { InstallType } from '../../plugin';
|
||||
import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store';
|
||||
|
||||
interface PluginCardProps {
|
||||
@@ -16,7 +18,9 @@ interface PluginCardProps {
|
||||
|
||||
const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
const [selectedOption, setSelectedOption] = useState<number>(0);
|
||||
const root: boolean = plugin.tags.some((tag) => tag === 'root');
|
||||
const root = plugin.tags.some((tag) => tag === 'root');
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -26,7 +30,6 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
marginRight: '20px',
|
||||
marginBottom: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -55,107 +58,102 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
width: 'calc(100% - 320px)', // The calc is here so that the info section doesn't expand into the image
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
justifyContent: 'space-between',
|
||||
marginLeft: '1em',
|
||||
justifyContent: 'center',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="deckyStoreCardTitle"
|
||||
style={{
|
||||
fontSize: '1.25em',
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
width: '90%',
|
||||
}}
|
||||
>
|
||||
{plugin.name}
|
||||
</span>
|
||||
<span
|
||||
className="deckyStoreCardAuthor"
|
||||
style={{
|
||||
marginRight: 'auto',
|
||||
fontSize: '1em',
|
||||
}}
|
||||
>
|
||||
{plugin.author}
|
||||
</span>
|
||||
<span
|
||||
className="deckyStoreCardDescription"
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
color: '#969696',
|
||||
WebkitLineClamp: root ? '2' : '3',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
}}
|
||||
>
|
||||
{plugin.description ? (
|
||||
plugin.description
|
||||
) : (
|
||||
<span>
|
||||
<i style={{ color: '#666' }}>No description provided.</i>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{root && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<span
|
||||
className="deckyStoreCardDescription deckyStoreCardDescriptionRoot"
|
||||
className="deckyStoreCardTitle"
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
color: '#fee75c',
|
||||
fontSize: '1.25em',
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
width: '90%',
|
||||
}}
|
||||
>
|
||||
<i>This plugin has full access to your Steam Deck.</i>{' '}
|
||||
<a
|
||||
className="deckyStoreCardDescriptionRootLink"
|
||||
href="https://deckbrew.xyz/root"
|
||||
target="_blank"
|
||||
{plugin.name}
|
||||
</span>
|
||||
<span
|
||||
className="deckyStoreCardAuthor"
|
||||
style={{
|
||||
marginRight: 'auto',
|
||||
fontSize: '1em',
|
||||
}}
|
||||
>
|
||||
{plugin.author}
|
||||
</span>
|
||||
<span
|
||||
className="deckyStoreCardDescription"
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
color: '#969696',
|
||||
WebkitLineClamp: root ? '2' : '3',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
}}
|
||||
>
|
||||
{plugin.description ? (
|
||||
plugin.description
|
||||
) : (
|
||||
<span>
|
||||
<i style={{ color: '#666' }}>{t('PluginCard.plugin_no_desc')}</i>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{root && (
|
||||
<div
|
||||
className="deckyStoreCardDescription deckyStoreCardDescriptionRoot"
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
color: '#fee75c',
|
||||
textDecoration: 'none',
|
||||
marginTop: 'auto',
|
||||
}}
|
||||
>
|
||||
deckbrew.xyz/root
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
<div
|
||||
className="deckyStoreCardButtonRow"
|
||||
style={{
|
||||
marginTop: '1em',
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<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', maxWidth: '100%' }}>
|
||||
<Focusable style={{ display: 'flex', gap: '5px', padding: 0 }}>
|
||||
<div
|
||||
className="deckyStoreCardInstallContainer"
|
||||
style={{
|
||||
paddingTop: '0px',
|
||||
paddingBottom: '0px',
|
||||
width: '40%',
|
||||
}}
|
||||
style={
|
||||
{
|
||||
paddingTop: '0px',
|
||||
paddingBottom: '0px',
|
||||
flexGrow: 1,
|
||||
'--field-negative-horizontal-margin': 0,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<ButtonItem
|
||||
bottomSeparator="none"
|
||||
layout="below"
|
||||
onClick={() => requestPluginInstall(plugin.name, plugin.versions[selectedOption])}
|
||||
onClick={() =>
|
||||
requestPluginInstall(plugin.name, plugin.versions[selectedOption], InstallType.INSTALL)
|
||||
}
|
||||
>
|
||||
<span className="deckyStoreCardInstallText">Install</span>
|
||||
<span className="deckyStoreCardInstallText">{t('PluginCard.plugin_install')}</span>
|
||||
</ButtonItem>
|
||||
</div>
|
||||
<div
|
||||
className="deckyStoreCardVersionContainer"
|
||||
style={{
|
||||
marginLeft: '5%',
|
||||
width: '30%',
|
||||
}}
|
||||
>
|
||||
<div className="deckyStoreCardVersionContainer" style={{ minWidth: '130px' }}>
|
||||
<Dropdown
|
||||
rgOptions={
|
||||
plugin.versions.map((version: StorePluginVersion, index) => ({
|
||||
@@ -163,7 +161,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
label: version.name,
|
||||
})) as SingleDropdownOption[]
|
||||
}
|
||||
menuLabel="Plugin Version"
|
||||
menuLabel={t('PluginCard.plugin_version_label') as string}
|
||||
selectedOption={selectedOption}
|
||||
onChange={({ data }) => setSelectedOption(data)}
|
||||
/>
|
||||
|
||||
@@ -9,13 +9,14 @@ import {
|
||||
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 { 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');
|
||||
@@ -25,6 +26,8 @@ const StorePage: FC<{}> = () => {
|
||||
return false;
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await getPluginList();
|
||||
@@ -54,13 +57,13 @@ const StorePage: FC<{}> = () => {
|
||||
}}
|
||||
tabs={[
|
||||
{
|
||||
title: 'Browse',
|
||||
title: t('Store.store_tabs.title'),
|
||||
content: <BrowseTab children={{ data: data }} />,
|
||||
id: 'browse',
|
||||
renderTabAddon: () => <span className={TabCount}>{data.length}</span>,
|
||||
},
|
||||
{
|
||||
title: 'About',
|
||||
title: t('Store.store_tabs.about'),
|
||||
content: <AboutTab />,
|
||||
id: 'about',
|
||||
},
|
||||
@@ -73,10 +76,12 @@ const StorePage: FC<{}> = () => {
|
||||
};
|
||||
|
||||
const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sortOptions = useMemo(
|
||||
(): DropdownOption[] => [
|
||||
{ data: 1, label: 'Alphabetical (A to Z)' },
|
||||
{ data: 2, label: 'Alphabetical (Z to A)' },
|
||||
{ data: 1, label: t('Store.store_tabs.alph_desc') },
|
||||
{ data: 2, label: t('Store.store_tabs.alph_asce') },
|
||||
],
|
||||
[],
|
||||
);
|
||||
@@ -105,11 +110,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
width: '47.5%',
|
||||
}}
|
||||
>
|
||||
<span className="DialogLabel">Sort</span>
|
||||
<span className="DialogLabel">{t("Store.store_sort.label")}</span>
|
||||
<Dropdown
|
||||
menuLabel="Sort"
|
||||
menuLabel={t("Store.store_sort.label") as string}
|
||||
rgOptions={sortOptions}
|
||||
strDefaultLabel="Last Updated (Newest)"
|
||||
strDefaultLabel={t("Store.store_sort.label_def") as string}
|
||||
selectedOption={selectedSort}
|
||||
onChange={(e) => setSort(e.data)}
|
||||
/>
|
||||
@@ -122,11 +127,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
<span className="DialogLabel">Filter</span>
|
||||
<span className="DialogLabel">{t("Store.store_filter.label")}</span>
|
||||
<Dropdown
|
||||
menuLabel="Filter"
|
||||
menuLabel={t("Store.store_filter.label")}
|
||||
rgOptions={filterOptions}
|
||||
strDefaultLabel="All"
|
||||
strDefaultLabel={t("Store.store_filter.label_def")}
|
||||
selectedOption={selectedFilter}
|
||||
onChange={(e) => setFilter(e.data)}
|
||||
/>
|
||||
@@ -136,7 +141,7 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
<div style={{ justifyContent: 'center', display: 'flex' }}>
|
||||
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<TextField label="Search" value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
|
||||
<TextField label={t("Store.store_search.label")} value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
|
||||
</div>
|
||||
</Focusable>
|
||||
</div>
|
||||
@@ -151,11 +156,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
<span className="DialogLabel">Sort</span>
|
||||
<span className="DialogLabel">{t('Store.store_sort.label')}</span>
|
||||
<Dropdown
|
||||
menuLabel="Sort"
|
||||
menuLabel={t('Store.store_sort.label') as string}
|
||||
rgOptions={sortOptions}
|
||||
strDefaultLabel="Last Updated (Newest)"
|
||||
strDefaultLabel={t('Store.store_sort.label_def') as string}
|
||||
selectedOption={selectedSort}
|
||||
onChange={(e) => setSort(e.data)}
|
||||
/>
|
||||
@@ -165,7 +170,11 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
<div style={{ justifyContent: 'center', display: 'flex' }}>
|
||||
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<TextField label="Search" value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
|
||||
<TextField
|
||||
label={t('Store.store_search.label')}
|
||||
value={searchFieldValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Focusable>
|
||||
</div>
|
||||
@@ -192,6 +201,8 @@ const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
|
||||
};
|
||||
|
||||
const AboutTab: FC<{}> = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -216,7 +227,7 @@ const AboutTab: FC<{}> = () => {
|
||||
/>
|
||||
<span className="deckyStoreAboutHeader">Testing</span>
|
||||
<span>
|
||||
Please consider testing new plugins to help the Decky Loader team!{' '}
|
||||
{t('Store.store_testing_cta')}{' '}
|
||||
<a
|
||||
href="https://deckbrew.xyz/testing"
|
||||
target="_blank"
|
||||
@@ -227,13 +238,10 @@ const AboutTab: FC<{}> = () => {
|
||||
deckbrew.xyz/testing
|
||||
</a>
|
||||
</span>
|
||||
<span className="deckyStoreAboutHeader">Contributing</span>
|
||||
<span>
|
||||
If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template
|
||||
repository on GitHub. Information on development and distribution is available in the README.
|
||||
</span>
|
||||
<span className="deckyStoreAboutHeader">Source Code</span>
|
||||
<span>All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.</span>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,27 +1,10 @@
|
||||
import {
|
||||
Navigation,
|
||||
ReactRouter,
|
||||
Router,
|
||||
fakeRenderComponent,
|
||||
findInReactTree,
|
||||
findInTree,
|
||||
findModule,
|
||||
findModuleChild,
|
||||
gamepadDialogClasses,
|
||||
gamepadSliderClasses,
|
||||
playSectionClasses,
|
||||
quickAccessControlsClasses,
|
||||
quickAccessMenuClasses,
|
||||
scrollClasses,
|
||||
scrollPanelClasses,
|
||||
sleep,
|
||||
staticClasses,
|
||||
updaterFieldClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { findModuleChild, sleep } from 'decky-frontend-lib';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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');
|
||||
|
||||
@@ -59,8 +42,12 @@ export async function setShowValveInternal(show: boolean) {
|
||||
|
||||
export async function setShouldConnectToReactDevTools(enable: boolean) {
|
||||
window.DeckyPluginLoader.toaster.toast({
|
||||
title: (enable ? 'Enabling' : 'Disabling') + ' React DevTools',
|
||||
body: 'Reloading in 5 seconds',
|
||||
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);
|
||||
@@ -77,29 +64,4 @@ export async function startup() {
|
||||
|
||||
if ((isRDTEnabled && !window.deckyHasConnectedRDT) || (!isRDTEnabled && window.deckyHasConnectedRDT))
|
||||
setShouldConnectToReactDevTools(isRDTEnabled);
|
||||
|
||||
logger.log('Exposing decky-frontend-lib APIs as DFL');
|
||||
window.DFL = {
|
||||
findModuleChild,
|
||||
findModule,
|
||||
Navigation,
|
||||
Router,
|
||||
ReactRouter,
|
||||
ReactUtils: {
|
||||
fakeRenderComponent,
|
||||
findInReactTree,
|
||||
findInTree,
|
||||
},
|
||||
classes: {
|
||||
scrollClasses,
|
||||
staticClasses,
|
||||
playSectionClasses,
|
||||
scrollPanelClasses,
|
||||
updaterFieldClasses,
|
||||
gamepadDialogClasses,
|
||||
gamepadSliderClasses,
|
||||
quickAccessMenuClasses,
|
||||
quickAccessControlsClasses,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
})();
|
||||
@@ -1,16 +1,28 @@
|
||||
import { ConfirmModal, ModalRoot, Patch, QuickAccessTab, Router, showModal, sleep } from 'decky-frontend-lib';
|
||||
import {
|
||||
ConfirmModal,
|
||||
ModalRoot,
|
||||
PanelSection,
|
||||
PanelSectionRow,
|
||||
Patch,
|
||||
QuickAccessTab,
|
||||
Router,
|
||||
quickAccessMenuClasses,
|
||||
showModal,
|
||||
sleep,
|
||||
} from 'decky-frontend-lib';
|
||||
import { FC, lazy } from 'react';
|
||||
import { FaCog, FaExclamationCircle, FaPlug } from 'react-icons/fa';
|
||||
import { FaExclamationCircle, FaPlug } from 'react-icons/fa';
|
||||
|
||||
import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
|
||||
import LegacyPlugin from './components/LegacyPlugin';
|
||||
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
|
||||
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
|
||||
import PluginInstallModal from './components/modals/PluginInstallModal';
|
||||
import NotificationBadge from './components/NotificationBadge';
|
||||
import PluginView from './components/PluginView';
|
||||
import WithSuspense from './components/WithSuspense';
|
||||
import Logger from './logger';
|
||||
import { Plugin } from './plugin';
|
||||
import { InstallType, Plugin } from './plugin';
|
||||
import RouterHook from './router-hook';
|
||||
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
|
||||
import { checkForUpdates } from './store';
|
||||
@@ -19,6 +31,7 @@ import OldTabsHook from './tabs-hook.old';
|
||||
import Toaster from './toaster';
|
||||
import { VerInfo, callUpdaterMethod } from './updater';
|
||||
import { getSetting } from './utils/settings';
|
||||
import TranslationHelper, { TranslationClass } from './utils/TranslationHelper';
|
||||
|
||||
const StorePage = lazy(() => import('./components/store/Store'));
|
||||
const SettingsPage = lazy(() => import('./components/settings'));
|
||||
@@ -98,8 +111,14 @@ class PluginLoader extends Logger {
|
||||
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!`,
|
||||
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'),
|
||||
});
|
||||
this.deckyState.setHasLoaderUpdate(true);
|
||||
@@ -118,26 +137,52 @@ class PluginLoader extends Logger {
|
||||
const updates = await this.checkPluginUpdates();
|
||||
if (updates?.size > 0) {
|
||||
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(
|
||||
<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, button_text: string, description: string) {
|
||||
showModal(
|
||||
<ConfirmModal
|
||||
onOK={async () => {
|
||||
@@ -146,10 +191,10 @@ class PluginLoader extends Logger {
|
||||
onCancel={() => {
|
||||
// do nothing
|
||||
}}
|
||||
strTitle={`Uninstall ${name}`}
|
||||
strOKButtonText={'Uninstall'}
|
||||
strTitle={title}
|
||||
strOKButtonText={button_text}
|
||||
>
|
||||
Are you sure you want to uninstall {name}?
|
||||
{description}
|
||||
</ConfirmModal>,
|
||||
);
|
||||
}
|
||||
@@ -169,6 +214,12 @@ class PluginLoader extends Logger {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
public deinit() {
|
||||
@@ -238,16 +289,30 @@ class PluginLoader extends Logger {
|
||||
} catch (e) {
|
||||
this.error('Error loading plugin ' + name, e);
|
||||
const TheError: FC<{}> = () => (
|
||||
<>
|
||||
Error:{' '}
|
||||
<pre>
|
||||
<code>{e instanceof Error ? e.stack : JSON.stringify(e)}</code>
|
||||
</pre>
|
||||
<>
|
||||
Please go to <FaCog style={{ display: 'inline' }} /> in the Decky menu if you need to uninstall this
|
||||
plugin.
|
||||
</>
|
||||
</>
|
||||
<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,
|
||||
@@ -255,7 +320,17 @@ class PluginLoader extends Logger {
|
||||
content: <TheError />,
|
||||
icon: <FaExclamationCircle />,
|
||||
});
|
||||
this.toaster.toast({ title: 'Error loading ' + name, body: '' + e, 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`);
|
||||
}
|
||||
|
||||
@@ -6,3 +6,9 @@ export interface Plugin {
|
||||
onDismount?(): void;
|
||||
alwaysRender?: boolean;
|
||||
}
|
||||
|
||||
export enum InstallType {
|
||||
INSTALL,
|
||||
REINSTALL,
|
||||
UPDATE,
|
||||
}
|
||||
|
||||
@@ -123,11 +123,9 @@ class RouterHook extends Logger {
|
||||
this.wrapperPatch = afterPatch(this.gamepadWrapper, 'render', (_: any, ret: any) => {
|
||||
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;
|
||||
if (
|
||||
ret.props.children.props.children[idx]?.props?.children?.[0]?.type?.type
|
||||
?.toString()
|
||||
?.includes('GamepadUI.Settings.Root()')
|
||||
) {
|
||||
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[idx]?.props?.children?.[0]?.type;
|
||||
this.routerPatch = afterPatch(this.router, 'type', (_: any, ret: any) => {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Navigation, Router, sleep } from 'decky-frontend-lib';
|
||||
import i18n from 'i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import PluginLoader from './plugin-loader';
|
||||
import { DeckyUpdater } from './updater';
|
||||
@@ -16,29 +18,38 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
if (!Router.NavigateToAppProperties || !Router.NavigateToLibraryTab || !Router.NavigateToInvites) {
|
||||
while (!Navigation.NavigateToAppProperties) await sleep(100);
|
||||
const shims = {
|
||||
NavigateToAppProperties: Navigation.NavigateToAppProperties,
|
||||
NavigateToInvites: Navigation.NavigateToInvites,
|
||||
NavigateToLibraryTab: Navigation.NavigateToLibraryTab,
|
||||
};
|
||||
(Router as unknown as any).deckyShim = true;
|
||||
Object.assign(Router, shims);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[DECKY]: Error initializing Navigation interface shims', e);
|
||||
}
|
||||
})();
|
||||
|
||||
(async () => {
|
||||
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
|
||||
|
||||
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) {
|
||||
@@ -56,8 +67,11 @@ declare global {
|
||||
if (!window.DeckyPluginLoader.hasPlugin(plugin.name))
|
||||
window.DeckyPluginLoader?.importPlugin(plugin.name, plugin.version);
|
||||
}
|
||||
|
||||
window.DeckyPluginLoader.checkPluginUpdates();
|
||||
};
|
||||
|
||||
setTimeout(() => window.syncDeckyPlugins(), 5000);
|
||||
})();
|
||||
|
||||
export default i18n;
|
||||
@@ -7,6 +7,6 @@ export function deinitSteamFixes() {
|
||||
}
|
||||
|
||||
export async function initSteamFixes() {
|
||||
fixes.push(reloadFix());
|
||||
fixes.push(await reloadFix());
|
||||
fixes.push(await restartFix());
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { getFocusNavController, sleep } from 'decky-frontend-lib';
|
||||
|
||||
import Logger from '../logger';
|
||||
|
||||
const logger = new Logger('ReloadSteamFix');
|
||||
|
||||
export default function reloadFix() {
|
||||
declare global {
|
||||
var GamepadNavTree: any;
|
||||
}
|
||||
|
||||
export default async function reloadFix() {
|
||||
// Hack to unbreak the ui when reloading it
|
||||
if (window.FocusNavController?.m_rgAllContexts?.length == 0) {
|
||||
await sleep(4000);
|
||||
if (getFocusNavController()?.m_rgAllContexts?.length == 0) {
|
||||
SteamClient.URL.ExecuteSteamURL('steam://open/settings');
|
||||
logger.log('Applied UI reload fix.');
|
||||
}
|
||||
|
||||
@@ -4,13 +4,6 @@ import Logger from '../logger';
|
||||
|
||||
const logger = new Logger('RestartSteamFix');
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
SteamClient: any;
|
||||
appDetailsStore: any;
|
||||
}
|
||||
}
|
||||
|
||||
let patch: Patch;
|
||||
|
||||
function rePatch() {
|
||||
|
||||
+26
-4
@@ -1,4 +1,4 @@
|
||||
import { Plugin } from './plugin';
|
||||
import { InstallType, Plugin } from './plugin';
|
||||
import { getSetting, setSetting } from './utils/settings';
|
||||
|
||||
export enum Store {
|
||||
@@ -23,6 +23,12 @@ export interface StorePlugin {
|
||||
image_url: string;
|
||||
}
|
||||
|
||||
export interface PluginInstallRequest {
|
||||
plugin: string;
|
||||
selectedVer: StorePluginVersion;
|
||||
installType: InstallType;
|
||||
}
|
||||
|
||||
// name: version
|
||||
export type PluginUpdateMapping = Map<string, StorePluginVersion>;
|
||||
|
||||
@@ -73,14 +79,26 @@ export async function installFromURL(url: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) {
|
||||
const artifactUrl =
|
||||
selectedVer.artifact ?? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`;
|
||||
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: 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,3 +113,7 @@ export async function checkForUpdates(plugins: Plugin[]): Promise<PluginUpdateMa
|
||||
}
|
||||
return updateMap;
|
||||
}
|
||||
|
||||
function pluginUrl(hash: string) {
|
||||
return `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${hash}.zip`;
|
||||
}
|
||||
|
||||
+70
-82
@@ -1,5 +1,14 @@
|
||||
// TabsHook for versions after the Desktop merge
|
||||
import { Patch, QuickAccessTab, afterPatch, findInReactTree, sleep } from 'decky-frontend-lib';
|
||||
import {
|
||||
Patch,
|
||||
QuickAccessTab,
|
||||
afterPatch,
|
||||
findInReactTree,
|
||||
findSP,
|
||||
gamepadUIClasses,
|
||||
getReactInstance,
|
||||
sleep,
|
||||
} from 'decky-frontend-lib';
|
||||
|
||||
import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState';
|
||||
import Logger from './logger';
|
||||
@@ -31,82 +40,60 @@ class TabsHook extends Logger {
|
||||
window.__TABS_HOOK_INSTANCE = this;
|
||||
}
|
||||
|
||||
init() {
|
||||
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
|
||||
let qAMRoot: any;
|
||||
const findQAMRoot = (currentNode: any, iters: number): any => {
|
||||
if (iters >= 55) {
|
||||
// currently 45
|
||||
return null;
|
||||
}
|
||||
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.child) {
|
||||
let node = findQAMRoot(currentNode.child, iters + 1);
|
||||
if (node !== null) return node;
|
||||
}
|
||||
if (currentNode.sibling) {
|
||||
let node = findQAMRoot(currentNode.sibling, iters + 1);
|
||||
if (node !== null) return node;
|
||||
}
|
||||
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);
|
||||
async init() {
|
||||
this.qAMRoot = await this.getQAMRoot();
|
||||
|
||||
let patchedInnerQAM: any;
|
||||
this.qamPatch = afterPatch(this.qAMRoot.return, 'type', (_: any, ret: any) => {
|
||||
try {
|
||||
if (this.qAMRoot?.child && !this.qAMRoot?.child?.type?.decky) {
|
||||
afterPatch(this.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;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
qAMRoot.child.type.decky = true;
|
||||
qAMRoot.child.alternate.type = qAMRoot.child.type;
|
||||
}
|
||||
} catch (e) {
|
||||
this.error('Error patching QAM', e);
|
||||
} catch (e) {
|
||||
this.error('Error patching QAM inner', e);
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
this.qAMRoot.child.type.decky = true;
|
||||
this.qAMRoot.child.alternate.type = this.qAMRoot.child.type;
|
||||
}
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
if (qAMRoot.return.alternate) {
|
||||
qAMRoot.return.alternate.type = qAMRoot.return.type;
|
||||
} catch (e) {
|
||||
this.error('Error patching QAM', e);
|
||||
}
|
||||
this.log('Finished initial injection');
|
||||
})();
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
if (this.qAMRoot.return.alternate) {
|
||||
this.qAMRoot.return.alternate.type = this.qAMRoot.return.type;
|
||||
}
|
||||
this.log('Finished initial injection');
|
||||
}
|
||||
|
||||
async getQAMRoot() {
|
||||
while (!findSP()) {
|
||||
await sleep(50);
|
||||
}
|
||||
|
||||
const parentNode = findSP().document.querySelector(`.${gamepadUIClasses.BasicUiRoot}`);
|
||||
if (!parentNode) return null;
|
||||
|
||||
return findInReactTree(
|
||||
getReactInstance(parentNode),
|
||||
(n) =>
|
||||
typeof n.memoizedProps?.visible !== 'undefined' && n.type?.toString()?.includes('QuickAccessMenuBrowserView'),
|
||||
);
|
||||
}
|
||||
|
||||
deinit() {
|
||||
@@ -128,22 +115,23 @@ class TabsHook extends Logger {
|
||||
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) tab.panel.props.setter[0](visible);
|
||||
if (tab?.decky && tab?.qAMVisibilitySetter) tab?.qAMVisibilitySetter(visible);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const { title, icon, content, id } of this.tabs) {
|
||||
existingTabs.push({
|
||||
const tab: any = {
|
||||
key: id,
|
||||
title,
|
||||
tab: icon,
|
||||
decky: true,
|
||||
panel: (
|
||||
<QuickAccessVisibleStateProvider initial={visible} setter={[]}>
|
||||
{content}
|
||||
</QuickAccessVisibleStateProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
tab.panel = (
|
||||
<QuickAccessVisibleStateProvider initial={visible} tab={tab}>
|
||||
{content}
|
||||
</QuickAccessVisibleStateProvider>
|
||||
);
|
||||
existingTabs.push(tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,15 @@ class Toaster extends Logger {
|
||||
let instance: any;
|
||||
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
|
||||
const findToasterRoot = (currentNode: any, iters: number): any => {
|
||||
if (iters >= 50) {
|
||||
// currently 40
|
||||
if (iters >= 65) {
|
||||
// currently 65
|
||||
return null;
|
||||
}
|
||||
if (currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder')) {
|
||||
if (
|
||||
currentNode?.memoizedProps?.className?.startsWith?.('gamepadtoasts_GamepadToastPlaceholder') ||
|
||||
currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder') ||
|
||||
currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPopup')
|
||||
) {
|
||||
this.log(`Toaster root was found in ${iters} recursion cycles`);
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { FC } from 'react';
|
||||
import { Translation } from 'react-i18next';
|
||||
|
||||
import Logger from '../logger';
|
||||
import { InstallType } from '../plugin';
|
||||
|
||||
export enum TranslationClass {
|
||||
PLUGIN_LOADER = 'PluginLoader',
|
||||
PLUGIN_INSTALL_MODAL = 'PluginInstallModal',
|
||||
DEVELOPER = 'Developer',
|
||||
}
|
||||
|
||||
interface TranslationHelperProps {
|
||||
trans_class: TranslationClass;
|
||||
trans_text: string;
|
||||
i18n_args?: {};
|
||||
install_type?: number;
|
||||
}
|
||||
|
||||
const logger = new Logger('TranslationHelper');
|
||||
|
||||
const TranslationHelper: FC<TranslationHelperProps> = ({
|
||||
trans_class,
|
||||
trans_text,
|
||||
i18n_args = null,
|
||||
install_type = 0,
|
||||
}) => {
|
||||
return (
|
||||
<Translation>
|
||||
{(t, {}) => {
|
||||
switch (trans_class) {
|
||||
case TranslationClass.PLUGIN_LOADER:
|
||||
return i18n_args
|
||||
? t(TranslationClass.PLUGIN_LOADER + '.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.PLUGIN_LOADER + '.' + trans_text);
|
||||
case TranslationClass.PLUGIN_INSTALL_MODAL:
|
||||
switch (install_type) {
|
||||
case InstallType.INSTALL:
|
||||
return i18n_args
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + trans_text);
|
||||
case InstallType.REINSTALL:
|
||||
return i18n_args
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + trans_text);
|
||||
case InstallType.UPDATE:
|
||||
return i18n_args
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + trans_text);
|
||||
}
|
||||
case TranslationClass.DEVELOPER:
|
||||
return i18n_args
|
||||
? t(TranslationClass.DEVELOPER + '.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.DEVELOPER + '.' + trans_text);
|
||||
default:
|
||||
logger.error('We should never fall in the default case!');
|
||||
return '';
|
||||
}
|
||||
}}
|
||||
</Translation>
|
||||
);
|
||||
};
|
||||
|
||||
export default TranslationHelper;
|
||||
@@ -1,7 +0,0 @@
|
||||
export function findSP(): Window {
|
||||
// old (SP as host)
|
||||
if (document.title == 'SP') return window;
|
||||
// new (SP as popup)
|
||||
return FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.find((x: any) => x.m_ID == 'root_1_').Root
|
||||
.Element.ownerDocument.defaultView;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"target": "ES2020",
|
||||
"target": "ES2021",
|
||||
"jsx": "react",
|
||||
"jsxFactory": "window.SP_REACT.createElement",
|
||||
"jsxFragmentFactory": "window.SP_REACT.Fragment",
|
||||
@@ -16,7 +16,8 @@
|
||||
"strict": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src", "index.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
This module exposes various constants and helpers useful for decky plugins.
|
||||
|
||||
* Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
* Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
* Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`.
|
||||
|
||||
Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended.
|
||||
|
||||
Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`.
|
||||
|
||||
A logging facility `logger` is available which writes to the recommended location.
|
||||
"""
|
||||
|
||||
__version__ = '0.1.0'
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
"""
|
||||
Constants
|
||||
"""
|
||||
|
||||
HOME: str = os.getenv("HOME", default="")
|
||||
"""
|
||||
The home directory of the effective user running the process.
|
||||
Environment variable: `HOME`.
|
||||
If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in.
|
||||
e.g.: `/home/deck`
|
||||
"""
|
||||
|
||||
USER: str = os.getenv("USER", default="")
|
||||
"""
|
||||
The effective username running the process.
|
||||
Environment variable: `USER`.
|
||||
It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in.
|
||||
e.g.: `deck`
|
||||
"""
|
||||
|
||||
DECKY_VERSION: str = os.getenv("DECKY_VERSION", default="")
|
||||
"""
|
||||
The version of the decky loader.
|
||||
Environment variable: `DECKY_VERSION`.
|
||||
e.g.: `v2.5.0-pre1`
|
||||
"""
|
||||
|
||||
DECKY_USER: str = os.getenv("DECKY_USER", default="")
|
||||
"""
|
||||
The user whose home decky resides in.
|
||||
Environment variable: `DECKY_USER`.
|
||||
e.g.: `deck`
|
||||
"""
|
||||
|
||||
DECKY_USER_HOME: str = os.getenv("DECKY_USER_HOME", default="")
|
||||
"""
|
||||
The home of the user where decky resides in.
|
||||
Environment variable: `DECKY_USER_HOME`.
|
||||
e.g.: `/home/deck`
|
||||
"""
|
||||
|
||||
DECKY_HOME: str = os.getenv("DECKY_HOME", default="")
|
||||
"""
|
||||
The root of the decky folder.
|
||||
Environment variable: `DECKY_HOME`.
|
||||
e.g.: `/home/deck/homebrew`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_SETTINGS_DIR: str = os.getenv(
|
||||
"DECKY_PLUGIN_SETTINGS_DIR", default="")
|
||||
"""
|
||||
The recommended path in which to store configuration files (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
e.g.: `/home/deck/homebrew/settings/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_RUNTIME_DIR: str = os.getenv(
|
||||
"DECKY_PLUGIN_RUNTIME_DIR", default="")
|
||||
"""
|
||||
The recommended path in which to store runtime data (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
e.g.: `/home/deck/homebrew/data/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_LOG_DIR: str = os.getenv("DECKY_PLUGIN_LOG_DIR", default="")
|
||||
"""
|
||||
The recommended path in which to store persistent logs (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_LOG_DIR`.
|
||||
e.g.: `/home/deck/homebrew/logs/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_DIR: str = os.getenv("DECKY_PLUGIN_DIR", default="")
|
||||
"""
|
||||
The root of the plugin's directory.
|
||||
Environment variable: `DECKY_PLUGIN_DIR`.
|
||||
e.g.: `/home/deck/homebrew/plugins/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_NAME: str = os.getenv("DECKY_PLUGIN_NAME", default="")
|
||||
"""
|
||||
The name of the plugin as specified in the 'plugin.json'.
|
||||
Environment variable: `DECKY_PLUGIN_NAME`.
|
||||
e.g.: `Example Plugin`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_VERSION: str = os.getenv("DECKY_PLUGIN_VERSION", default="")
|
||||
"""
|
||||
The version of the plugin as specified in the 'package.json'.
|
||||
Environment variable: `DECKY_PLUGIN_VERSION`.
|
||||
e.g.: `0.0.1`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_AUTHOR: str = os.getenv("DECKY_PLUGIN_AUTHOR", default="")
|
||||
"""
|
||||
The author of the plugin as specified in the 'plugin.json'.
|
||||
Environment variable: `DECKY_PLUGIN_AUTHOR`.
|
||||
e.g.: `John Doe`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_LOG: str = os.path.join(DECKY_PLUGIN_LOG_DIR, "plugin.log")
|
||||
"""
|
||||
The path to the plugin's main logfile.
|
||||
Environment variable: `DECKY_PLUGIN_LOG`.
|
||||
e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log`
|
||||
"""
|
||||
|
||||
"""
|
||||
Migration helpers
|
||||
"""
|
||||
|
||||
|
||||
def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories to a new location and remove old locations.
|
||||
Specified files will be migrated to `target_dir`.
|
||||
Specified directories will have their contents recursively migrated to `target_dir`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
file_map: dict[str, str] = {}
|
||||
for f in files_or_directories:
|
||||
if not os.path.exists(f):
|
||||
file_map[f] = ""
|
||||
continue
|
||||
if os.path.isdir(f):
|
||||
src_dir = f
|
||||
src_file = "."
|
||||
file_map[f] = target_dir
|
||||
else:
|
||||
src_dir = os.path.dirname(f)
|
||||
src_file = os.path.basename(f)
|
||||
file_map[f] = os.path.join(target_dir, src_file)
|
||||
subprocess.run(["sh", "-c", "mkdir -p \"$3\"; tar -cf - -C \"$1\" \"$2\" | tar -xf - -C \"$3\" && rm -rf \"$4\"",
|
||||
"_", src_dir, src_file, target_dir, f])
|
||||
return file_map
|
||||
|
||||
|
||||
def migrate_settings(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin settings to the recommended location and remove old locations.
|
||||
Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
return migrate_any(DECKY_PLUGIN_SETTINGS_DIR, *files_or_directories)
|
||||
|
||||
|
||||
def migrate_runtime(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations
|
||||
Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
return migrate_any(DECKY_PLUGIN_RUNTIME_DIR, *files_or_directories)
|
||||
|
||||
|
||||
def migrate_logs(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin logs to the recommended location and remove old locations.
|
||||
Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
return migrate_any(DECKY_PLUGIN_LOG_DIR, *files_or_directories)
|
||||
|
||||
|
||||
"""
|
||||
Logging
|
||||
"""
|
||||
|
||||
logging.basicConfig(filename=DECKY_PLUGIN_LOG,
|
||||
format='[%(asctime)s][%(levelname)s]: %(message)s',
|
||||
force=True)
|
||||
logger: logging.Logger = logging.getLogger()
|
||||
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
|
||||
|
||||
logger.setLevel(logging.INFO)
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
This module exposes various constants and helpers useful for decky plugins.
|
||||
|
||||
* Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
* Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
* Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`.
|
||||
|
||||
Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended.
|
||||
|
||||
Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`.
|
||||
|
||||
A logging facility `logger` is available which writes to the recommended location.
|
||||
"""
|
||||
|
||||
__version__ = '0.1.0'
|
||||
|
||||
import logging
|
||||
|
||||
"""
|
||||
Constants
|
||||
"""
|
||||
|
||||
HOME: str
|
||||
"""
|
||||
The home directory of the effective user running the process.
|
||||
Environment variable: `HOME`.
|
||||
If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in.
|
||||
e.g.: `/home/deck`
|
||||
"""
|
||||
|
||||
USER: str
|
||||
"""
|
||||
The effective username running the process.
|
||||
Environment variable: `USER`.
|
||||
It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in.
|
||||
e.g.: `deck`
|
||||
"""
|
||||
|
||||
DECKY_VERSION: str
|
||||
"""
|
||||
The version of the decky loader.
|
||||
Environment variable: `DECKY_VERSION`.
|
||||
e.g.: `v2.5.0-pre1`
|
||||
"""
|
||||
|
||||
DECKY_USER: str
|
||||
"""
|
||||
The user whose home decky resides in.
|
||||
Environment variable: `DECKY_USER`.
|
||||
e.g.: `deck`
|
||||
"""
|
||||
|
||||
DECKY_USER_HOME: str
|
||||
"""
|
||||
The home of the user where decky resides in.
|
||||
Environment variable: `DECKY_USER_HOME`.
|
||||
e.g.: `/home/deck`
|
||||
"""
|
||||
|
||||
DECKY_HOME: str
|
||||
"""
|
||||
The root of the decky folder.
|
||||
Environment variable: `DECKY_HOME`.
|
||||
e.g.: `/home/deck/homebrew`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_SETTINGS_DIR: str
|
||||
"""
|
||||
The recommended path in which to store configuration files (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
e.g.: `/home/deck/homebrew/settings/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_RUNTIME_DIR: str
|
||||
"""
|
||||
The recommended path in which to store runtime data (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
e.g.: `/home/deck/homebrew/data/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_LOG_DIR: str
|
||||
"""
|
||||
The recommended path in which to store persistent logs (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_LOG_DIR`.
|
||||
e.g.: `/home/deck/homebrew/logs/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_DIR: str
|
||||
"""
|
||||
The root of the plugin's directory.
|
||||
Environment variable: `DECKY_PLUGIN_DIR`.
|
||||
e.g.: `/home/deck/homebrew/plugins/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_NAME: str
|
||||
"""
|
||||
The name of the plugin as specified in the 'plugin.json'.
|
||||
Environment variable: `DECKY_PLUGIN_NAME`.
|
||||
e.g.: `Example Plugin`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_VERSION: str
|
||||
"""
|
||||
The version of the plugin as specified in the 'package.json'.
|
||||
Environment variable: `DECKY_PLUGIN_VERSION`.
|
||||
e.g.: `0.0.1`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_AUTHOR: str
|
||||
"""
|
||||
The author of the plugin as specified in the 'plugin.json'.
|
||||
Environment variable: `DECKY_PLUGIN_AUTHOR`.
|
||||
e.g.: `John Doe`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_LOG: str
|
||||
"""
|
||||
The path to the plugin's main logfile.
|
||||
Environment variable: `DECKY_PLUGIN_LOG`.
|
||||
e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log`
|
||||
"""
|
||||
|
||||
"""
|
||||
Migration helpers
|
||||
"""
|
||||
|
||||
|
||||
def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories to a new location and remove old locations.
|
||||
Specified files will be migrated to `target_dir`.
|
||||
Specified directories will have their contents recursively migrated to `target_dir`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
|
||||
|
||||
def migrate_settings(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin settings to the recommended location and remove old locations.
|
||||
Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
|
||||
|
||||
def migrate_runtime(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations
|
||||
Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
|
||||
|
||||
def migrate_logs(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin logs to the recommended location and remove old locations.
|
||||
Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
Logging
|
||||
"""
|
||||
|
||||
logger: logging.Logger
|
||||
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
|
||||
Reference in New Issue
Block a user