mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Compare commits
40 Commits
v2.5.1
...
v2.7.2-pre2
| Author | SHA1 | Date | |
|---|---|---|---|
| d6f336d84b | |||
| 4777963b65 | |||
| fc193f98db | |||
| a07e4d6fe6 | |||
| 4ab7d97ab2 | |||
| 15a6f7fdb8 | |||
| 7d2cff8745 | |||
| ee5ed3faf0 | |||
| 0f36e87cce | |||
| fd325ef1cc | |||
| faf46ba533 | |||
| 94ec434eae | |||
| a223efd6f5 | |||
| 395e45167d | |||
| 0dd0d9f4bd | |||
| 3e5404abdd | |||
| 46abc5a266 | |||
| 88e1e9b869 | |||
| fc0089f7a5 | |||
| d335562328 | |||
| f9624a0859 | |||
| 97bb3fa4c8 | |||
| 611245aec9 | |||
| e1807e8c75 | |||
| b94cfe32d9 | |||
| f1e679c3fb | |||
| e1b138bcbd | |||
| c6be8f6c14 | |||
| ac086cf59e | |||
| 3e120ea312 | |||
| 0b718daa47 | |||
| 0929b9c5cb | |||
| 43b2269ea7 | |||
| 0c4e27cd34 | |||
| 36cf85b08a | |||
| 994da868af | |||
| 2e53fb217a | |||
| c2b76d9099 | |||
| c05e8f9ae0 | |||
| 2dce0646bd |
@@ -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
|
||||
|
||||
@@ -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/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/legacy;/legacy" --add-data "./plugin;/plugin" ./backend/main.py
|
||||
|
||||
- name: Upload package artifact ⬆️
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: PluginLoader Win
|
||||
path: ./dist/PluginLoader.exe
|
||||
|
||||
release:
|
||||
name: Release stable version of the package
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'release' }}
|
||||
@@ -130,9 +173,7 @@ jobs:
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "no type selected, defaulting to patch.\n"
|
||||
OUT=$(semver bump patch "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
printf "no type selected, not bumping for release.\n"
|
||||
fi
|
||||
elif [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
printf "previous tag is a release, bumping by selected type.\n"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Push Updated Plugin Stub to Template
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
copy-stub:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8230315d06ad95c617244d2f265d237a1682d445
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v35.6.3
|
||||
with:
|
||||
separator: ","
|
||||
files: |
|
||||
plugin/*
|
||||
|
||||
- name: Is stub changed
|
||||
id: changed-stub
|
||||
run: |
|
||||
STUB_CHANGED="false"
|
||||
PATHS=(plugin plugin/decky_plugin.pyi)
|
||||
SHA=${{ github.sha }}
|
||||
SHA_PREV=HEAD^
|
||||
FILES=$(git diff $SHA_PREV..$SHA --name-only -- ${PATHS[@]} | jq -Rsc 'split("\n")[:-1] | join (",")')
|
||||
if [[ "$FILES" == *"plugin/decky_plugin.pyi"* ]]; then
|
||||
$STUB_CHANGED="true"
|
||||
echo "Stub has changed, pushing updated stub"
|
||||
else
|
||||
echo "Stub has not changed, exiting."
|
||||
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Push updated stub
|
||||
if: steps.changed-stub.outputs.has_changed == true
|
||||
uses: dmnemec/copy_file_to_another_repo_action@bbebd3da22e4a37d04dca5f782edd5201cb97083
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
source_file: 'plugin/decky_plugin.pyi'
|
||||
destination_repo: 'SteamDeckHomebrew/decky-plugin-template'
|
||||
user_email: '11465594+TrainDoctor@users.noreply.github.com'
|
||||
user_name: 'TrainDoctor'
|
||||
commit_message: 'Updated template with latest plugin stub changes'
|
||||
@@ -0,0 +1,17 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Run linters
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2 # Check out the repository first.
|
||||
- name: Run prettier (JavaScript & TypeScript)
|
||||
run: |
|
||||
pushd frontend
|
||||
npm install
|
||||
npm run lint
|
||||
Vendored
+1
-1
@@ -4,4 +4,4 @@
|
||||
"deckpass" : "ssap",
|
||||
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
|
||||
"deckdir" : "/home/deck"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<h1 align="center">
|
||||
<a name="logo" href="https://deckbrew.xyz/"><img src="https://deckbrew.xyz/logo.png" alt="Deckbrew logo" width="200"></a>
|
||||
<a name="logo" href="https://deckbrew.xyz/"><img src="https://deckbrew.xyz/static/icon-45ca1f5aea376a9ad37e92db906f283e.png" alt="Deckbrew logo" width="200"></a>
|
||||
<br>
|
||||
Decky Loader
|
||||
<br>
|
||||
<a name="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">
|
||||
@@ -11,7 +11,7 @@
|
||||
<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://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,39 +33,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.
|
||||
- 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
|
||||
@@ -83,15 +83,16 @@ Now that you have Decky Loader installed, you can start using plugins. Each plug
|
||||
|
||||
### 🛠️ Plugin Development
|
||||
|
||||
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://discord.gg/ZU74G2NJzk) if you have any questions.
|
||||
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://deckbrew.xyz/discord) if you have any questions.
|
||||
|
||||
### 🤝 Contributing
|
||||
|
||||
Please consult [the wiki page regarding development](https://deckbrew.xyz/en/loader-dev/development) for more information on installing development versions of Decky Loader. You can also install the Steam Deck UI on a Windows or Linux computer for testing by following [this YouTube guide](https://youtu.be/1IAbZte8e7E?t=112).
|
||||
Please consult [the wiki page regarding development](https://wiki.deckbrew.xyz/en/loader-dev/development) for more information on installing development versions of Decky Loader. You can also install the Steam Deck UI on a Windows or Linux computer for testing by following [this YouTube guide](https://youtu.be/1IAbZte8e7E?t=112).
|
||||
|
||||
1. Clone the repository using the latest commit to main before starting your PR.
|
||||
1. In your clone of the repository, run these commands.
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm i
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
+2
-2
@@ -26,10 +26,10 @@ cd ..
|
||||
|
||||
if [[ "$type" == "release" ]]; then
|
||||
printf "release!\n"
|
||||
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts
|
||||
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts --container-architecture linux/amd64
|
||||
elif [[ "$type" == "prerelease" ]]; then
|
||||
printf "prerelease!\n"
|
||||
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts
|
||||
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts --container-architecture linux/amd64
|
||||
else
|
||||
printf "Release type unspecified/badly specified.\n"
|
||||
printf "Options: 'release' or 'prerelease'\n"
|
||||
|
||||
+42
-21
@@ -1,5 +1,7 @@
|
||||
# Full imports
|
||||
import json
|
||||
# import pprint
|
||||
# from pprint import pformat
|
||||
|
||||
# Partial imports
|
||||
from aiohttp import ClientSession, web
|
||||
@@ -10,12 +12,12 @@ from io import BytesIO
|
||||
from logging import getLogger
|
||||
from os import R_OK, W_OK, path, rename, listdir, access, mkdir
|
||||
from shutil import rmtree
|
||||
from subprocess import call
|
||||
from time import time
|
||||
from zipfile import ZipFile
|
||||
from localplatform import chown, chmod
|
||||
|
||||
# Local modules
|
||||
from helpers import get_ssl_context, get_user, get_user_group, download_remote_binary_to_path
|
||||
from helpers import get_ssl_context, download_remote_binary_to_path
|
||||
from injector import get_gamepadui_tab
|
||||
|
||||
logger = getLogger("Browser")
|
||||
@@ -28,10 +30,11 @@ class PluginInstallContext:
|
||||
self.hash = hash
|
||||
|
||||
class PluginBrowser:
|
||||
def __init__(self, plugin_path, plugins, loader) -> None:
|
||||
def __init__(self, plugin_path, plugins, loader, settings) -> None:
|
||||
self.plugin_path = plugin_path
|
||||
self.plugins = plugins
|
||||
self.loader = loader
|
||||
self.settings = settings
|
||||
self.install_requests = {}
|
||||
|
||||
def _unzip_to_plugin_dir(self, zip, name, hash):
|
||||
@@ -40,11 +43,10 @@ class PluginBrowser:
|
||||
return False
|
||||
zip_file = ZipFile(zip)
|
||||
zip_file.extractall(self.plugin_path)
|
||||
plugin_dir = self.find_plugin_folder(name)
|
||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), plugin_dir])
|
||||
code_chmod = call(["chmod", "-R", "555", plugin_dir])
|
||||
if code_chown != 0 or code_chmod != 0:
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
|
||||
plugin_dir = path.join(self.plugin_path, self.find_plugin_folder(name))
|
||||
|
||||
if not chown(plugin_dir) or not chmod(plugin_dir, 555):
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code")
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -59,14 +61,14 @@ class PluginBrowser:
|
||||
packageJson = json.load(f)
|
||||
if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0:
|
||||
# create bin directory if needed.
|
||||
rc=call(["chmod", "-R", "777", pluginBasePath])
|
||||
chmod(pluginBasePath, 777)
|
||||
if access(pluginBasePath, W_OK):
|
||||
|
||||
if not path.exists(pluginBinPath):
|
||||
mkdir(pluginBinPath)
|
||||
|
||||
if not access(pluginBinPath, W_OK):
|
||||
rc=call(["chmod", "-R", "777", pluginBinPath])
|
||||
chmod(pluginBinPath, 777)
|
||||
|
||||
rv = True
|
||||
for remoteBinary in packageJson["remote_binary"]:
|
||||
@@ -78,8 +80,8 @@ class PluginBrowser:
|
||||
rv = False
|
||||
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
|
||||
|
||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
|
||||
rc=call(["chmod", "-R", "555", pluginBasePath])
|
||||
chown(self.plugin_path)
|
||||
chmod(pluginBasePath, 555)
|
||||
else:
|
||||
rv = True
|
||||
logger.debug(f"No Remote Binaries to Download")
|
||||
@@ -90,6 +92,7 @@ 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:
|
||||
@@ -97,7 +100,7 @@ class PluginBrowser:
|
||||
plugin = json.load(f)
|
||||
|
||||
if plugin['name'] == name:
|
||||
return str(path.join(self.plugin_path, folder))
|
||||
return folder
|
||||
except:
|
||||
logger.debug(f"skipping {folder}")
|
||||
|
||||
@@ -105,20 +108,32 @@ class PluginBrowser:
|
||||
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.debug("unloading %s" % str(name))
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
||||
if self.plugins[name]:
|
||||
logger.info(" at dir " + plugin_dir)
|
||||
logger.debug("calling frontend unload for %s" % str(name))
|
||||
res = await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
||||
logger.debug("result of unload from UI: %s", res)
|
||||
# plugins_snapshot = self.plugins.copy()
|
||||
# snapshot_string = pformat(plugins_snapshot)
|
||||
# logger.debug("current plugins: %s", snapshot_string)
|
||||
if name in self.plugins:
|
||||
logger.debug("Plugin %s was found", name)
|
||||
self.plugins[name].stop()
|
||||
logger.debug("Plugin %s was stopped", name)
|
||||
del self.plugins[name]
|
||||
logger.debug("Plugin %s was removed from the dictionary", name)
|
||||
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(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
|
||||
@@ -151,7 +166,8 @@ class PluginBrowser:
|
||||
logger.debug("Unzipping...")
|
||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||
if ret:
|
||||
plugin_dir = self.find_plugin_folder(name)
|
||||
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})")
|
||||
@@ -159,7 +175,12 @@ class PluginBrowser:
|
||||
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)
|
||||
|
||||
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:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from enum import Enum
|
||||
|
||||
class UserType(Enum):
|
||||
HOST_USER = 1
|
||||
EFFECTIVE_USER = 2
|
||||
ROOT = 3
|
||||
+101
-75
@@ -1,29 +1,28 @@
|
||||
import grp
|
||||
import pwd
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import uuid
|
||||
import os
|
||||
from subprocess import check_output
|
||||
from time import sleep
|
||||
import sys
|
||||
import subprocess
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
|
||||
import certifi
|
||||
from aiohttp.web import Response, middleware
|
||||
from aiohttp import ClientSession
|
||||
import localplatform
|
||||
from customtypes import UserType
|
||||
from logging import getLogger
|
||||
|
||||
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
||||
|
||||
# global vars
|
||||
csrf_token = str(uuid.uuid4())
|
||||
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
user = None
|
||||
group = None
|
||||
|
||||
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
||||
frontend_regex = re.compile("^/frontend/.*")
|
||||
logger = getLogger("Main")
|
||||
|
||||
def get_ssl_context():
|
||||
return ssl_ctx
|
||||
@@ -37,65 +36,41 @@ async def csrf_middleware(request, handler):
|
||||
return await handler(request)
|
||||
return Response(text='Forbidden', status='403')
|
||||
|
||||
# Get the user by checking for the first logged in user. As this is run
|
||||
# by systemd at startup the process is likely to start before the user
|
||||
# logs in, so we will wait here until they are available. Note that
|
||||
# other methods such as getenv wont work as there was no $SUDO_USER to
|
||||
# start the systemd service.
|
||||
def set_user():
|
||||
global user
|
||||
cmd = "who | awk '{print $1}' | sort | head -1"
|
||||
while user == None:
|
||||
name = check_output(cmd, shell=True).decode().strip()
|
||||
if name not in [None, '']:
|
||||
user = name
|
||||
sleep(0.1)
|
||||
|
||||
# Get the global user. get_user must be called first.
|
||||
def get_user() -> str:
|
||||
global user
|
||||
if user == None:
|
||||
raise ValueError("helpers.get_user method called before user variable was set. Run helpers.set_user first.")
|
||||
return user
|
||||
|
||||
#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)[0]
|
||||
|
||||
#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)[0]
|
||||
|
||||
# Set the global user group. get_user must be called first
|
||||
def set_user_group() -> str:
|
||||
global group
|
||||
global user
|
||||
if user == None:
|
||||
raise ValueError("helpers.set_user_dir method called before user variable was set. Run helpers.set_user first.")
|
||||
if group == None:
|
||||
group = check_output(["id", "-g", "-n", user]).decode().strip()
|
||||
|
||||
# Get the group of the global user. set_user_group must be called first.
|
||||
def get_user_group() -> str:
|
||||
global group
|
||||
if group == None:
|
||||
raise ValueError("helpers.get_user_group method called before group variable was set. Run helpers.set_user_group first.")
|
||||
return group
|
||||
|
||||
# Get the default home path unless a user is specified
|
||||
def get_home_path(username = None) -> str:
|
||||
if username == None:
|
||||
raise ValueError("Username not defined, no home path can be found.")
|
||||
else:
|
||||
return str("/home/"+username)
|
||||
|
||||
# Get the default homebrew path unless a user is specified
|
||||
# Get the default homebrew path unless a home_path is specified. home_path argument is deprecated
|
||||
def get_homebrew_path(home_path = None) -> str:
|
||||
if home_path == None:
|
||||
raise ValueError("Home path not defined, homebrew dir cannot be determined.")
|
||||
else:
|
||||
return str(home_path+"/homebrew")
|
||||
# return str(home_path+"/homebrew")
|
||||
return localplatform.get_unprivileged_path()
|
||||
|
||||
# Recursively create path and chown as user
|
||||
def mkdir_as_user(path):
|
||||
path = os.path.realpath(path)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
localplatform.chown(path)
|
||||
|
||||
# Fetches the version of loader
|
||||
def get_loader_version() -> str:
|
||||
try:
|
||||
with open(os.path.join(os.getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
|
||||
return version_file.readline().strip()
|
||||
except Exception as e:
|
||||
logger.warn(f"Failed to execute get_loader_version(): {str(e)}")
|
||||
return "unknown"
|
||||
|
||||
# returns the appropriate system python paths
|
||||
def get_system_pythonpaths() -> list[str]:
|
||||
extra_args = {}
|
||||
|
||||
if localplatform.ON_LINUX:
|
||||
# run as normal normal user to also include user python paths
|
||||
extra_args["user"] = localplatform.localplatform._get_user_id()
|
||||
extra_args["env"] = {}
|
||||
|
||||
try:
|
||||
proc = subprocess.run(["python3" if localplatform.ON_LINUX else "python", "-c", "import sys; print('\\n'.join(x for x in sys.path if x))"],
|
||||
capture_output=True, **extra_args)
|
||||
return [x.strip() for x in proc.stdout.decode().strip().split("\n")]
|
||||
except Exception as e:
|
||||
logger.warn(f"Failed to execute get_system_pythonpaths(): {str(e)}")
|
||||
return []
|
||||
|
||||
# Download Remote Binaries to local Plugin
|
||||
async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
||||
@@ -121,16 +96,67 @@ async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
||||
|
||||
return rv
|
||||
|
||||
# Deprecated
|
||||
def set_user():
|
||||
pass
|
||||
|
||||
# Deprecated
|
||||
def set_user_group() -> str:
|
||||
return get_user_group()
|
||||
|
||||
#########
|
||||
# Below is legacy code, provided for backwards compatibility. This will break on windows
|
||||
#########
|
||||
|
||||
# Get the user id hosting the plugin loader
|
||||
def get_user_id() -> int:
|
||||
return localplatform.localplatform._get_user_id()
|
||||
|
||||
# Get the user hosting the plugin loader
|
||||
def get_user() -> str:
|
||||
return localplatform.localplatform._get_user()
|
||||
|
||||
# Get the effective user id of the running process
|
||||
def get_effective_user_id() -> int:
|
||||
return localplatform.localplatform._get_effective_user_id()
|
||||
|
||||
# Get the effective user of the running process
|
||||
def get_effective_user() -> str:
|
||||
return localplatform.localplatform._get_effective_user()
|
||||
|
||||
# Get the effective user group id of the running process
|
||||
def get_effective_user_group_id() -> int:
|
||||
return localplatform.localplatform._get_effective_user_group_id()
|
||||
|
||||
# Get the effective user group of the running process
|
||||
def get_effective_user_group() -> str:
|
||||
return localplatform.localplatform._get_effective_user_group()
|
||||
|
||||
# Get the user owner of the given file path.
|
||||
def get_user_owner(file_path) -> str:
|
||||
return localplatform.localplatform._get_user_owner(file_path)
|
||||
|
||||
# Get the user group of the given file path.
|
||||
def get_user_group(file_path) -> str:
|
||||
return localplatform.localplatform._get_user_group(file_path)
|
||||
|
||||
# Get the group id of the user hosting the plugin loader
|
||||
def get_user_group_id() -> int:
|
||||
return localplatform.localplatform._get_user_group_id()
|
||||
|
||||
# Get the group of the user hosting the plugin loader
|
||||
def get_user_group() -> str:
|
||||
return localplatform.localplatform._get_user_group()
|
||||
|
||||
# Get the default home path unless a user is specified
|
||||
def get_home_path(username = None) -> str:
|
||||
return localplatform.get_home_path(UserType.ROOT if username == "root" else UserType.HOST_USER)
|
||||
|
||||
async def is_systemd_unit_active(unit_name: str) -> bool:
|
||||
res = subprocess.run(["systemctl", "is-active", unit_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
return res.returncode == 0
|
||||
return await localplatform.service_active(unit_name)
|
||||
|
||||
async def stop_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
||||
cmd = ["systemctl", "stop", unit_name]
|
||||
async def stop_systemd_unit(unit_name: str) -> bool:
|
||||
return await localplatform.service_stop(unit_name)
|
||||
|
||||
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
async def start_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
||||
cmd = ["systemctl", "start", unit_name]
|
||||
|
||||
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
async def start_systemd_unit(unit_name: str) -> bool:
|
||||
return await localplatform.service_start(unit_name)
|
||||
+7
-4
@@ -111,7 +111,7 @@ class Tab:
|
||||
"method": "Page.disable",
|
||||
}, False)
|
||||
|
||||
async def refresh(self):
|
||||
async def refresh(self, manage_socket=True):
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
@@ -394,8 +394,11 @@ async def get_tab_lambda(test) -> Tab:
|
||||
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()
|
||||
@@ -412,7 +415,7 @@ async def inject_to_tab(tab_name, js, run_async=False):
|
||||
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)
|
||||
await sleep(0.5)
|
||||
|
||||
+2
-8
@@ -6,19 +6,13 @@ from pathlib import Path
|
||||
from traceback import print_exc
|
||||
|
||||
from aiohttp import web
|
||||
from genericpath import exists
|
||||
from os.path import exists
|
||||
from watchdog.events import RegexMatchingEventHandler
|
||||
from watchdog.utils import UnsupportedLibc
|
||||
|
||||
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$'])
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
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:
|
||||
os.getenv("LIVE_RELOAD", "1") == "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
|
||||
+42
-38
@@ -1,14 +1,19 @@
|
||||
# Change PyInstaller files permissions
|
||||
import sys
|
||||
from subprocess import call
|
||||
from localplatform import (chmod, chown, service_stop, service_start,
|
||||
ON_WINDOWS, get_log_level, get_live_reload,
|
||||
get_server_port, get_server_host, get_chown_plugin_path,
|
||||
get_unprivileged_user, get_unprivileged_path,
|
||||
get_privileged_path)
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
call(['chmod', '-R', '755', sys._MEIPASS])
|
||||
chmod(sys._MEIPASS, 755)
|
||||
# Full imports
|
||||
from asyncio import new_event_loop, set_event_loop, sleep
|
||||
from json import dumps, loads
|
||||
from logging import DEBUG, INFO, basicConfig, getLogger
|
||||
from os import getenv, chmod, path
|
||||
from os import getenv, path
|
||||
from traceback import format_exc
|
||||
import multiprocessing
|
||||
|
||||
import aiohttp_cors
|
||||
# Partial imports
|
||||
@@ -19,50 +24,32 @@ from aiohttp_jinja2 import setup as jinja_setup
|
||||
# local modules
|
||||
from browser import PluginBrowser
|
||||
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
|
||||
get_home_path, get_homebrew_path, get_user,
|
||||
get_user_group, set_user, set_user_group,
|
||||
stop_systemd_unit, start_systemd_unit)
|
||||
mkdir_as_user, get_system_pythonpaths)
|
||||
|
||||
from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs
|
||||
from loader import Loader
|
||||
from settings import SettingsManager
|
||||
from updater import Updater
|
||||
from utilities import Utilities
|
||||
from customtypes import UserType
|
||||
|
||||
# Ensure USER and GROUP vars are set first.
|
||||
# TODO: This isn't the best way to do this but supports the current
|
||||
# implementation. All the config load and environment setting eventually be
|
||||
# moved into init or a config/loader method.
|
||||
set_user()
|
||||
set_user_group()
|
||||
USER = get_user()
|
||||
GROUP = get_user_group()
|
||||
HOME_PATH = "/home/"+USER
|
||||
HOMEBREW_PATH = HOME_PATH+"/homebrew"
|
||||
CONFIG = {
|
||||
"plugin_path": getenv("PLUGIN_PATH", HOMEBREW_PATH+"/plugins"),
|
||||
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
|
||||
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
||||
"server_port": int(getenv("SERVER_PORT", "1337")),
|
||||
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
|
||||
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
|
||||
getenv("LOG_LEVEL", "INFO")
|
||||
],
|
||||
}
|
||||
|
||||
basicConfig(
|
||||
level=CONFIG["log_level"],
|
||||
level=get_log_level(),
|
||||
format="[%(module)s][%(levelname)s]: %(message)s"
|
||||
)
|
||||
|
||||
logger = getLogger("Main")
|
||||
plugin_path = path.join(get_privileged_path(), "plugins")
|
||||
|
||||
def chown_plugin_dir():
|
||||
code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]])
|
||||
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
|
||||
if code_chown != 0 or code_chmod != 0:
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
|
||||
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
|
||||
mkdir_as_user(plugin_path)
|
||||
|
||||
if CONFIG["chown_plugin_path"] == True:
|
||||
if not chown(plugin_path, UserType.HOST_USER) or not chmod(plugin_path, 555):
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code")
|
||||
|
||||
if get_chown_plugin_path() == True:
|
||||
chown_plugin_dir()
|
||||
|
||||
class PluginManager:
|
||||
@@ -77,9 +64,9 @@ class PluginManager:
|
||||
allow_credentials=True
|
||||
)
|
||||
})
|
||||
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
|
||||
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.plugin_loader.plugins, self.plugin_loader)
|
||||
self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "settings"))
|
||||
self.plugin_loader = Loader(self.web_app, plugin_path, self.loop, get_live_reload())
|
||||
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
|
||||
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
|
||||
self.utilities = Utilities(self)
|
||||
self.updater = Updater(self)
|
||||
|
||||
@@ -87,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())
|
||||
|
||||
@@ -116,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:
|
||||
@@ -178,9 +168,23 @@ class PluginManager:
|
||||
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()
|
||||
|
||||
+69
-84
@@ -1,29 +1,27 @@
|
||||
import multiprocessing
|
||||
from asyncio import (Lock, get_event_loop, new_event_loop,
|
||||
open_unix_connection, set_event_loop, sleep,
|
||||
start_unix_server, IncompleteReadError, LimitOverrunError)
|
||||
set_event_loop, sleep)
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from json import dumps, load, loads
|
||||
from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from os import path, setgid, setuid
|
||||
from os import path, environ
|
||||
from signal import SIGINT, signal
|
||||
from sys import exit
|
||||
from sys import exit, path as syspath
|
||||
from time import time
|
||||
|
||||
multiprocessing.set_start_method("fork")
|
||||
|
||||
BUFFER_LIMIT = 2 ** 20 # 1 MiB
|
||||
from localsocket import LocalSocket
|
||||
from localplatform import setgid, setuid, get_username, get_home_path
|
||||
from customtypes import UserType
|
||||
import helpers
|
||||
|
||||
class PluginWrapper:
|
||||
def __init__(self, file, plugin_directory, plugin_path) -> None:
|
||||
self.file = file
|
||||
self.plugin_path = plugin_path
|
||||
self.plugin_directory = plugin_directory
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
self.socket_addr = f"/tmp/plugin_socket_{time()}"
|
||||
self.method_call_lock = Lock()
|
||||
self.socket = LocalSocket(self._on_new_message)
|
||||
|
||||
self.version = None
|
||||
|
||||
@@ -32,7 +30,6 @@ class PluginWrapper:
|
||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
|
||||
self.version = package_json["version"]
|
||||
|
||||
|
||||
self.legacy = False
|
||||
self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
|
||||
self.tile_view_html = json["tile_view_html"] if "tile_view_html" in json else ""
|
||||
@@ -56,16 +53,39 @@ class PluginWrapper:
|
||||
set_event_loop(new_event_loop())
|
||||
if self.passive:
|
||||
return
|
||||
setgid(0 if "root" in self.flags else 1000)
|
||||
setuid(0 if "root" in self.flags else 1000)
|
||||
setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
setuid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
# export a bunch of environment variables to help plugin developers
|
||||
environ["HOME"] = get_home_path(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
|
||||
environ["USER"] = "root" if "root" in self.flags else get_username()
|
||||
environ["DECKY_VERSION"] = helpers.get_loader_version()
|
||||
environ["DECKY_USER"] = get_username()
|
||||
environ["DECKY_USER_HOME"] = helpers.get_home_path()
|
||||
environ["DECKY_HOME"] = helpers.get_homebrew_path()
|
||||
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"])
|
||||
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_RUNTIME_DIR"])
|
||||
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_LOG_DIR"])
|
||||
environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory)
|
||||
environ["DECKY_PLUGIN_NAME"] = self.name
|
||||
environ["DECKY_PLUGIN_VERSION"] = self.version
|
||||
environ["DECKY_PLUGIN_AUTHOR"] = self.author
|
||||
|
||||
# append the plugin's `py_modules` to the recognized python paths
|
||||
syspath.append(path.join(environ["DECKY_PLUGIN_DIR"], "py_modules"))
|
||||
|
||||
spec = spec_from_file_location("_", self.file)
|
||||
module = module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
self.Plugin = module.Plugin
|
||||
|
||||
if hasattr(self.Plugin, "_migration"):
|
||||
get_event_loop().run_until_complete(self.Plugin._migration(self.Plugin))
|
||||
if hasattr(self.Plugin, "_main"):
|
||||
get_event_loop().create_task(self.Plugin._main(self.Plugin))
|
||||
get_event_loop().create_task(self._setup_socket())
|
||||
get_event_loop().create_task(self.socket.setup_server())
|
||||
get_event_loop().run_forever()
|
||||
except:
|
||||
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
|
||||
@@ -73,61 +93,36 @@ class PluginWrapper:
|
||||
|
||||
async def _unload(self):
|
||||
try:
|
||||
self.log.info("Attempting to unload " + self.name + "\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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
@@ -138,34 +133,24 @@ class PluginWrapper:
|
||||
def stop(self):
|
||||
if self.passive:
|
||||
return
|
||||
|
||||
async def _(self):
|
||||
if await self._open_socket_if_not_exists():
|
||||
self.writer.write((dumps({ "stop": True }, 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)")
|
||||
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"]
|
||||
+8
-9
@@ -1,15 +1,14 @@
|
||||
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_home_path, get_homebrew_path, get_user, set_user, get_user_owner
|
||||
from helpers import get_homebrew_path
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
def __init__(self, name, settings_directory = None) -> None:
|
||||
set_user()
|
||||
USER = get_user()
|
||||
wrong_dir = get_homebrew_path(get_home_path(USER))
|
||||
wrong_dir = get_homebrew_path()
|
||||
if settings_directory == None:
|
||||
settings_directory = path.join(wrong_dir, "settings")
|
||||
|
||||
@@ -18,7 +17,6 @@ class SettingsManager:
|
||||
#Create the folder with the correct permission
|
||||
if not path.exists(settings_directory):
|
||||
mkdir(settings_directory)
|
||||
chown(settings_directory, USER, USER)
|
||||
|
||||
#Copy all old settings file in the root directory to the correct folder
|
||||
for file in listdir(wrong_dir):
|
||||
@@ -29,8 +27,9 @@ class SettingsManager:
|
||||
|
||||
|
||||
#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, 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 = {}
|
||||
|
||||
@@ -52,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):
|
||||
|
||||
+63
-52
@@ -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
|
||||
|
||||
from aiohttp import ClientSession, web
|
||||
|
||||
@@ -30,12 +30,7 @@ class Updater:
|
||||
}
|
||||
self.remoteVer = None
|
||||
self.allRemoteVers = None
|
||||
try:
|
||||
logger.info(getcwd())
|
||||
with open(path.join(getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
|
||||
self.localVer = version_file.readline().replace("\n", "")
|
||||
except:
|
||||
self.localVer = False
|
||||
self.localVer = helpers.get_loader_version()
|
||||
|
||||
try:
|
||||
self.currentBranch = self.get_branch(self.context.settings)
|
||||
@@ -70,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:
|
||||
@@ -96,15 +91,12 @@ class Updater:
|
||||
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")
|
||||
@@ -112,14 +104,23 @@ class Updater:
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
|
||||
remoteVersions = await res.json()
|
||||
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")
|
||||
@@ -140,47 +141,54 @@ class Updater:
|
||||
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}", "/home/"+helpers.get_user()+"/homebrew")
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8") as service_file:
|
||||
service_file.write(service_data)
|
||||
if ON_LINUX:
|
||||
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("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!
|
||||
try:
|
||||
remove(path.join(getcwd(), "PluginLoader"))
|
||||
except:
|
||||
pass
|
||||
with open(path.join(getcwd(), "PluginLoader"), "wb") as out:
|
||||
with open(path.join(getcwd(), download_temp_filename), "wb") as out:
|
||||
progress = 0
|
||||
raw = 0
|
||||
async for c in res.content.iter_chunked(512):
|
||||
@@ -194,12 +202,15 @@ class Updater:
|
||||
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")
|
||||
|
||||
@@ -10,7 +10,7 @@ from logging import getLogger
|
||||
from injector import inject_to_tab, get_gamepadui_tab, close_old_tabs
|
||||
import helpers
|
||||
import subprocess
|
||||
|
||||
from localplatform import service_stop, service_start
|
||||
|
||||
class Utilities:
|
||||
def __init__(self, context) -> None:
|
||||
@@ -174,11 +174,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):
|
||||
@@ -251,7 +251,7 @@ 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}"
|
||||
|
||||
+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 |
+13
-13
@@ -12,28 +12,28 @@
|
||||
},
|
||||
"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",
|
||||
"import-sort-style-module": "^6.0.0",
|
||||
"inquirer": "^8.2.4",
|
||||
"prettier": "^2.7.1",
|
||||
"inquirer": "^8.2.5",
|
||||
"prettier": "^2.8.7",
|
||||
"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.0",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"importSort": {
|
||||
".js, .jsx, .ts, .tsx": {
|
||||
@@ -42,10 +42,10 @@
|
||||
}
|
||||
},
|
||||
"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.20.5",
|
||||
"react-file-icon": "^1.3.0",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-markdown": "^8.0.6",
|
||||
"remark-gfm": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+772
-736
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
export default function DeckyIcon() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 456" width="512" height="456">
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: 'none' }}
|
||||
d="M154.33,72.51v49.79c11.78-0.17,23.48,2,34.42,6.39c10.93,4.39,20.89,10.91,29.28,19.18
|
||||
c8.39,8.27,15.06,18.13,19.61,29c4.55,10.87,6.89,22.54,6.89,34.32c0,11.78-2.34,23.45-6.89,34.32
|
||||
c-4.55,10.87-11.21,20.73-19.61,29c-8.39,8.27-18.35,14.79-29.28,19.18c-10.94,4.39-22.63,6.56-34.42,6.39v49.77
|
||||
c36.78,0,72.05-14.61,98.05-40.62c26-26.01,40.61-61.28,40.61-98.05c0-36.78-14.61-72.05-40.61-98.05
|
||||
C226.38,87.12,191.11,72.51,154.33,72.51z"
|
||||
/>
|
||||
|
||||
<ellipse
|
||||
transform="matrix(0.982 -0.1891 0.1891 0.982 -37.1795 32.9988)"
|
||||
style={{ fill: 'none' }}
|
||||
cx="154.33"
|
||||
cy="211.33"
|
||||
rx="69.33"
|
||||
ry="69.33"
|
||||
/>
|
||||
<path style={{ fill: 'none' }} d="M430,97h-52v187h52c7.18,0,13-5.82,13-13V110C443,102.82,437.18,97,430,97z" />
|
||||
<path
|
||||
style={{ fill: 'currentColor' }}
|
||||
d="M432,27h-54V0H0v361c0,52.47,42.53,95,95,95h188c52.47,0,95-42.53,95-95v-7h54c44.18,0,80-35.82,80-80V107
|
||||
C512,62.82,476.18,27,432,27z M85,211.33c0-38.29,31.04-69.33,69.33-69.33c38.29,0,69.33,31.04,69.33,69.33
|
||||
c0,38.29-31.04,69.33-69.33,69.33C116.04,280.67,85,249.62,85,211.33z M252.39,309.23c-26.01,26-61.28,40.62-98.05,40.62v-49.77
|
||||
c11.78,0.17,23.48-2,34.42-6.39c10.93-4.39,20.89-10.91,29.28-19.18c8.39-8.27,15.06-18.13,19.61-29
|
||||
c4.55-10.87,6.89-22.53,6.89-34.32c0-11.78-2.34-23.45-6.89-34.32c-4.55-10.87-11.21-20.73-19.61-29
|
||||
c-8.39-8.27-18.35-14.79-29.28-19.18c-10.94-4.39-22.63-6.56-34.42-6.39V72.51c36.78,0,72.05,14.61,98.05,40.61
|
||||
c26,26.01,40.61,61.28,40.61,98.05C293,247.96,278.39,283.23,252.39,309.23z M443,271c0,7.18-5.82,13-13,13h-52V97h52
|
||||
c7.18,0,13,5.82,13,13V271z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
|
||||
import { CSSProperties, VFC } from 'react';
|
||||
import { FaArrowLeft, FaCog, FaStore } from 'react-icons/fa';
|
||||
import { BsGearFill } from 'react-icons/bs';
|
||||
import { FaArrowLeft, FaStore } from 'react-icons/fa';
|
||||
|
||||
import { useDeckyState } from './DeckyState';
|
||||
|
||||
@@ -26,12 +27,6 @@ const TitleView: VFC = () => {
|
||||
if (activePlugin === null) {
|
||||
return (
|
||||
<Focusable style={titleStyles} className={staticClasses.Title}>
|
||||
<DialogButton
|
||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||
onClick={onSettingsClick}
|
||||
>
|
||||
<FaCog style={{ marginTop: '-4px', display: 'block' }} />
|
||||
</DialogButton>
|
||||
<div style={{ marginRight: 'auto', flex: 0.9 }}>Decky</div>
|
||||
<DialogButton
|
||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||
@@ -39,6 +34,12 @@ const TitleView: VFC = () => {
|
||||
>
|
||||
<FaStore style={{ marginTop: '-4px', display: 'block' }} />
|
||||
</DialogButton>
|
||||
<DialogButton
|
||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||
onClick={onSettingsClick}
|
||||
>
|
||||
<BsGearFill style={{ marginTop: '-4px', display: 'block' }} />
|
||||
</DialogButton>
|
||||
</Focusable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,7 +1,9 @@
|
||||
import { SidebarNavigation } from 'decky-frontend-lib';
|
||||
import { lazy } from 'react';
|
||||
import { FaCode, FaPlug } from 'react-icons/fa';
|
||||
|
||||
import { useSetting } from '../../utils/hooks/useSetting';
|
||||
import DeckyIcon from '../DeckyIcon';
|
||||
import WithSuspense from '../WithSuspense';
|
||||
import GeneralSettings from './pages/general';
|
||||
import PluginList from './pages/plugin_list';
|
||||
@@ -13,19 +15,18 @@ export default function SettingsPage() {
|
||||
|
||||
const pages = [
|
||||
{
|
||||
title: 'General',
|
||||
title: 'Decky',
|
||||
content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
|
||||
route: '/decky/settings/general',
|
||||
icon: <DeckyIcon />,
|
||||
},
|
||||
{
|
||||
title: 'Plugins',
|
||||
content: <PluginList />,
|
||||
route: '/decky/settings/plugins',
|
||||
icon: <FaPlug />,
|
||||
},
|
||||
];
|
||||
|
||||
if (isDeveloper)
|
||||
pages.push({
|
||||
{
|
||||
title: 'Developer',
|
||||
content: (
|
||||
<WithSuspense>
|
||||
@@ -33,7 +34,10 @@ export default function SettingsPage() {
|
||||
</WithSuspense>
|
||||
),
|
||||
route: '/decky/settings/developer',
|
||||
});
|
||||
icon: <FaCode />,
|
||||
visible: isDeveloper,
|
||||
},
|
||||
];
|
||||
|
||||
return <SidebarNavigation title="Decky Settings" showTitle pages={pages} />;
|
||||
return <SidebarNavigation pages={pages} />;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Field, Focusable, TextField, Toggle } from 'decky-frontend-lib';
|
||||
import { DialogBody, Field, TextField, Toggle } from 'decky-frontend-lib';
|
||||
import { useRef } from 'react';
|
||||
import { FaReact, FaSteamSymbol } from 'react-icons/fa';
|
||||
|
||||
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
import RemoteDebuggingSettings from '../general/RemoteDebugging';
|
||||
|
||||
export default function DeveloperSettings() {
|
||||
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
|
||||
@@ -12,7 +13,8 @@ export default function DeveloperSettings() {
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogBody>
|
||||
<RemoteDebuggingSettings />
|
||||
<Field
|
||||
label="Enable Valve Internal"
|
||||
description={
|
||||
@@ -30,55 +32,33 @@ export default function DeveloperSettings() {
|
||||
setShowValveInternal(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>{' '}
|
||||
<Focusable
|
||||
onTouchEnd={
|
||||
reactDevtoolsIP == ''
|
||||
? () => {
|
||||
(textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onClick={
|
||||
reactDevtoolsIP == ''
|
||||
? () => {
|
||||
(textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onOKButton={
|
||||
reactDevtoolsIP == ''
|
||||
? () => {
|
||||
(textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
|
||||
}
|
||||
: undefined
|
||||
</Field>
|
||||
<Field
|
||||
label="Enable React DevTools"
|
||||
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.
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<div ref={textRef}>
|
||||
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
icon={<FaReact style={{ display: 'block' }} />}
|
||||
>
|
||||
<Field
|
||||
label="Enable React DevTools"
|
||||
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.
|
||||
</span>
|
||||
<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>
|
||||
</Focusable>
|
||||
</>
|
||||
<Toggle
|
||||
value={reactDevtoolsEnabled}
|
||||
// disabled={reactDevtoolsIP == ''}
|
||||
onChange={(toggleValue) => {
|
||||
setReactDevtoolsEnabled(toggleValue);
|
||||
setShouldConnectToReactDevTools(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const BranchSelect: FunctionComponent<{}> = () => {
|
||||
return (
|
||||
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
|
||||
// 0 being stable, 1 being pre-release and 2 being nightly
|
||||
<Field label="Update Channel">
|
||||
<Field label="Decky Update Channel" childrenContainerWidth={'fixed'}>
|
||||
<Dropdown
|
||||
rgOptions={Object.values(UpdateBranch)
|
||||
.filter((branch) => typeof branch == 'string')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Field, Toggle } from 'decky-frontend-lib';
|
||||
import { FaBug } from 'react-icons/fa';
|
||||
import { FaChrome } from 'react-icons/fa';
|
||||
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
|
||||
@@ -11,10 +11,10 @@ export default function RemoteDebuggingSettings() {
|
||||
label="Allow Remote CEF Debugging"
|
||||
description={
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
Allow unauthenticated access to the CEF debugger to anyone in your network
|
||||
Allows unauthenticated access to the CEF debugger to anyone in your network.
|
||||
</span>
|
||||
}
|
||||
icon={<FaBug style={{ display: 'block' }} />}
|
||||
icon={<FaChrome style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={allowRemoteDebugging || false}
|
||||
|
||||
@@ -16,7 +16,7 @@ const StoreSelect: FunctionComponent<{}> = () => {
|
||||
// 0 being Default, 1 being Testing and 2 being Custom
|
||||
return (
|
||||
<>
|
||||
<Field label="Store Channel">
|
||||
<Field label="Plugin Store Channel" childrenContainerWidth={'fixed'}>
|
||||
<Dropdown
|
||||
rgOptions={Object.values(Store)
|
||||
.filter((store) => typeof store == 'string')
|
||||
|
||||
@@ -6,15 +6,15 @@ import {
|
||||
Focusable,
|
||||
ProgressBarWithInfo,
|
||||
Spinner,
|
||||
findSP,
|
||||
showModal,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useCallback } from 'react';
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaArrowDown } from 'react-icons/fa';
|
||||
import { 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';
|
||||
@@ -39,7 +39,7 @@ 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>
|
||||
@@ -95,21 +95,21 @@ export default function UpdaterSettings() {
|
||||
<Field
|
||||
onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined}
|
||||
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
|
||||
label="Updates"
|
||||
label="Decky Updates"
|
||||
description={
|
||||
versionInfo && (
|
||||
<span style={{ whiteSpace: 'pre-line' }}>{`Current version: ${versionInfo.current}\n${
|
||||
versionInfo.updatable ? `Latest version: ${versionInfo.remote?.tag_name}` : ''
|
||||
}`}</span>
|
||||
checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? (
|
||||
''
|
||||
) : (
|
||||
<span>Up to date: running {versionInfo?.current}</span>
|
||||
)
|
||||
}
|
||||
icon={
|
||||
!versionInfo ? (
|
||||
<Spinner style={{ width: '1em', height: 20, display: 'block' }} />
|
||||
) : (
|
||||
<FaArrowDown style={{ display: 'block' }} />
|
||||
versionInfo?.remote &&
|
||||
versionInfo?.remote?.tag_name != versionInfo?.current && (
|
||||
<FaExclamation color="var(--gpColor-Yellow)" style={{ display: 'block' }} />
|
||||
)
|
||||
}
|
||||
childrenContainerWidth={'fixed'}
|
||||
>
|
||||
{updateProgress == -1 && !isLoaderUpdating ? (
|
||||
<DialogButton
|
||||
@@ -144,7 +144,7 @@ export default function UpdaterSettings() {
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
{versionInfo?.remote && (
|
||||
{versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current && (
|
||||
<InlinePatchNotes
|
||||
title={versionInfo?.remote.name}
|
||||
date={new Intl.RelativeTimeFormat('en-US', {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { DialogButton, Field, TextField, Toggle } from 'decky-frontend-lib';
|
||||
import {
|
||||
DialogBody,
|
||||
DialogButton,
|
||||
DialogControlsSection,
|
||||
DialogControlsSectionHeader,
|
||||
Field,
|
||||
TextField,
|
||||
Toggle,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useState } from 'react';
|
||||
import { FaShapes, FaTools } from 'react-icons/fa';
|
||||
|
||||
import { installFromURL } from '../../../../store';
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
import BranchSelect from './BranchSelect';
|
||||
import RemoteDebuggingSettings from './RemoteDebugging';
|
||||
import StoreSelect from './StoreSelect';
|
||||
import UpdaterSettings from './Updater';
|
||||
|
||||
@@ -16,34 +23,44 @@ export default function GeneralSettings({
|
||||
setIsDeveloper: (val: boolean) => void;
|
||||
}) {
|
||||
const [pluginURL, setPluginURL] = useState('');
|
||||
const { versionInfo } = useDeckyState();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<UpdaterSettings />
|
||||
<BranchSelect />
|
||||
<StoreSelect />
|
||||
<RemoteDebuggingSettings />
|
||||
<Field
|
||||
label="Developer mode"
|
||||
description={<span style={{ whiteSpace: 'pre-line' }}>Enables Decky's developer settings.</span>}
|
||||
icon={<FaTools style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={isDeveloper}
|
||||
onChange={(toggleValue) => {
|
||||
setIsDeveloper(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Manual plugin install"
|
||||
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
||||
icon={<FaShapes style={{ display: 'block' }} />}
|
||||
>
|
||||
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
||||
Install
|
||||
</DialogButton>
|
||||
</Field>
|
||||
</div>
|
||||
<DialogBody>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Updates</DialogControlsSectionHeader>
|
||||
<UpdaterSettings />
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Beta Participation</DialogControlsSectionHeader>
|
||||
<BranchSelect />
|
||||
<StoreSelect />
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Other</DialogControlsSectionHeader>
|
||||
<Field label="Enable Developer Mode">
|
||||
<Toggle
|
||||
value={isDeveloper}
|
||||
onChange={(toggleValue) => {
|
||||
setIsDeveloper(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</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}>
|
||||
<div style={{ color: 'var(--gpSystemLighterGrey)' }}>{versionInfo?.current}</div>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,117 @@
|
||||
import { DialogButton, Focusable, Menu, MenuItem, showContextMenu } from 'decky-frontend-lib';
|
||||
import { useEffect } from 'react';
|
||||
import { FaDownload, FaEllipsisH } from 'react-icons/fa';
|
||||
import {
|
||||
DialogBody,
|
||||
DialogButton,
|
||||
DialogControlsSection,
|
||||
GamepadEvent,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ReorderableEntry,
|
||||
ReorderableList,
|
||||
showContextMenu,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
|
||||
|
||||
import { requestPluginInstall } from '../../../../store';
|
||||
import { StorePluginVersion, getPluginList, 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);
|
||||
}
|
||||
}
|
||||
|
||||
function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
|
||||
const data = props.entry.data;
|
||||
let pluginName = labelToName(props.entry.label, data?.version);
|
||||
|
||||
const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
|
||||
showContextMenu(
|
||||
<Menu label="Plugin Actions">
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(pluginName, data?.version)}>Reload</MenuItem>
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(pluginName)}>Uninstall</MenuItem>
|
||||
</Menu>,
|
||||
e.currentTarget ?? window,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.update ? (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion)}
|
||||
onOKButton={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion)}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
Update to {data?.update?.name}
|
||||
<FaDownload style={{ paddingLeft: '2rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
) : (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => reinstallPlugin(pluginName, data?.version)}
|
||||
onOKButton={() => reinstallPlugin(pluginName, data?.version)}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
Reinstall
|
||||
<FaRecycle style={{ paddingLeft: '5.3rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
)}
|
||||
<DialogButton
|
||||
style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }}
|
||||
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),
|
||||
);
|
||||
|
||||
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>
|
||||
@@ -20,47 +120,18 @@ export default function PluginList() {
|
||||
);
|
||||
}
|
||||
|
||||
function onSave(entries: ReorderableEntry<PluginData>[]) {
|
||||
const newOrder = entries.map((entry) => labelToName(entry.label, entry?.data?.version));
|
||||
console.log(newOrder);
|
||||
setPluginOrder(newOrder);
|
||||
setPluginOrderSetting(newOrder);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul style={{ listStyleType: 'none' }}>
|
||||
{plugins.map(({ name, version }) => {
|
||||
const update = updates?.get(name);
|
||||
return (
|
||||
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
|
||||
<span>
|
||||
{name} {version}
|
||||
</span>
|
||||
<Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
|
||||
{update && (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => requestPluginInstall(name, update)}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
Update to {update.name}
|
||||
<FaDownload style={{ paddingLeft: '2rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
)}
|
||||
<DialogButton
|
||||
style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }}
|
||||
onClick={(e: MouseEvent) =>
|
||||
showContextMenu(
|
||||
<Menu label="Plugin Actions">
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name, version)}>
|
||||
Reload
|
||||
</MenuItem>
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem>
|
||||
</Menu>,
|
||||
e.currentTarget ?? window,
|
||||
)
|
||||
}
|
||||
>
|
||||
<FaEllipsisH />
|
||||
</DialogButton>
|
||||
</Focusable>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<DialogBody>
|
||||
<DialogControlsSection>
|
||||
<ReorderableList<PluginData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} />
|
||||
</DialogControlsSection>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ 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');
|
||||
|
||||
@@ -56,6 +56,7 @@ declare global {
|
||||
if (!window.DeckyPluginLoader.hasPlugin(plugin.name))
|
||||
window.DeckyPluginLoader?.importPlugin(plugin.name, plugin.version);
|
||||
}
|
||||
|
||||
window.DeckyPluginLoader.checkPluginUpdates();
|
||||
};
|
||||
|
||||
|
||||
@@ -169,6 +169,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() {
|
||||
@@ -180,6 +186,7 @@ class PluginLoader extends Logger {
|
||||
}
|
||||
|
||||
public unloadPlugin(name: string) {
|
||||
console.log('Plugin List: ', this.plugins);
|
||||
const plugin = this.plugins.find((plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''));
|
||||
plugin?.onDismount?.();
|
||||
this.plugins = this.plugins.filter((p) => p !== plugin);
|
||||
@@ -335,7 +342,7 @@ class PluginLoader extends Logger {
|
||||
fetchNoCors(url: string, request: any = {}) {
|
||||
let args = { method: 'POST', headers: {} };
|
||||
const req = { ...args, ...request, url, data: request.body };
|
||||
req?.body && delete req.body
|
||||
req?.body && delete req.body;
|
||||
return this.callServerMethod('http_request', req);
|
||||
},
|
||||
executeInTab(tab: string, runAsync: boolean, code: string) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -35,7 +35,7 @@ class TabsHook extends Logger {
|
||||
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
|
||||
let qAMRoot: any;
|
||||
const findQAMRoot = (currentNode: any, iters: number): any => {
|
||||
if (iters >= 55) {
|
||||
if (iters >= 65) {
|
||||
// currently 45
|
||||
return null;
|
||||
}
|
||||
@@ -128,22 +128,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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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