Compare commits

...

52 Commits

Author SHA1 Message Date
Marco Rodolfi 3e120ea312 Fix class name shenanigans for toast notification (#366)
* Fix class name shenanigans for  toast notification

* Corrected number of iterations
2023-02-06 17:30:44 -08:00
Sky Leite 0b718daa47 Add lint job to build workflow (#363)
* Add lint job to build workflow

* Add prettier-plugin-import-sort

* Install prettier plugins before linting

* Use lint script from package.json

* Move linters to separate workflow

* Remove Python and Shell linters

* Remove popd

* Test that prettier properly fails the lint job
2023-02-03 19:40:29 -08:00
EMERALD 0929b9c5cb Specify linux/amd64 Docker architecture (#356) 2023-02-01 17:17:24 -08:00
EMERALD 43b2269ea7 Fix UI inconsistencies, various improvements (#357)
* Make version gray in plugin list

* Settings/store icons together & plugin list fix

* Navigation name/icon improvements

* Decky settings overhaul and other fixes

- Revert the tab icon to a plug
- Rename DeckyFlat function to DeckyIcon
- Add DialogBody to settings pages to improve scrolling
- Add remote debugging settings to the developer settings
- Fix React devtools interactions to work more easily
- Add spacing to React devtools description
- Specify Decky vs. plugin store
- Compact version information by update button
- Add current version to bottom of settings
- Remove unnecessary settings icons
- Change CEF debugger icon to Chrome (bug icon too generic, is Chromium)
- Make buttons/dropdowns in settings have fixed width
- Make download icon act/appear similar to Valve's for Deck

* Final UI adjustments

* Switch plugin settings icon to plug
2023-02-01 17:16:42 -08:00
Party Wumpus 0c4e27cd34 [readme] add installer issue to common issues (#359) 2023-02-01 12:31:44 -08:00
TrainDoctor 36cf85b08a Comment out un-needed pprint usage 2023-01-29 15:27:52 -08:00
TrainDoctor 994da868af Add python logging to browser and plugin 2023-01-29 15:16:16 -08:00
TrainDoctor 2e53fb217a Add better handling for unloading of plugins 2023-01-29 13:59:02 -08:00
Philipp Richter c2b76d9099 Expose useful env vars to plugin processes (#349)
* recommended paths for storing data
* improve helper functions
2023-01-22 16:54:05 -08:00
TrainDoctor c05e8f9ae0 Update build.yml 2023-01-22 16:33:06 -08:00
TrainDoctor 2dce0646bd Update README.md 2023-01-22 16:29:27 -08:00
Beebles 6569f1b268 Fix http_request not allowing bodys (#352) 2023-01-22 14:33:26 -08:00
EMERALD 3ebaac6752 Store and plugin installation visual improvements (#343)
* Redesign store, add comments for filtering

* Improve installation/uninstallation modals

* Fix store comment to be easier to fix

* Add source code info to about page
2023-01-19 18:00:42 -08:00
dependabot[bot] cbbd564860 Bump certifi from 2022.6.15 to 2022.12.7 (#345)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.6.15 to 2022.12.7.
- [Release notes](https://github.com/certifi/python-certifi/releases)
- [Commits](https://github.com/certifi/python-certifi/compare/2022.06.15...2022.12.07)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-19 17:54:50 -08:00
TrainDoctor 635edf7f5b fix releases being called prereleases 2023-01-17 15:37:43 -08:00
Nox 1b6e18bcb3 Updated store CSS (#305)
* PluginCard Store CSS Update

* Fixing CSS

* Updated

* Removed padding
2023-01-16 14:43:16 -08:00
AAGaming 0ad0016c62 move the chown 2023-01-16 14:44:16 -05:00
AAGaming a2716449f9 fix missing await on chown_plugin_dir 2023-01-16 14:17:27 -05:00
AAGaming 649eed89c9 bump dfl 2023-01-16 09:13:30 -05:00
AAGaming 83680fffa2 indicate to DFL that the router has shim applied 2023-01-16 09:12:52 -05:00
AAGaming d695b90baf fix React DevTools again 2023-01-15 21:22:54 -05:00
TrainDoctor 5fdcc56409 Aa/bump dfl navigation fix jan2023 (#341)
* fix React DevTools

* bump DFL to fix Navigation

* Bump DFL and add shims

* fix shims not applying due to timing issue

Co-authored-by: AAGaming <aa@mail.catvibers.me>
2023-01-15 17:40:47 -08:00
Party Wumpus 915997d149 [readme] change terminal commands to point at decky-installer (#342)
* update readme to point at decky-installer for konsole scripts

* test change to make dark/light theme work properly

* Update README.md

* Update README.md

* revert <picture> changes as they didn't actually work
2023-01-14 07:49:23 -08:00
Party Wumpus e8b4c4a307 Fix the download button (#330)
* Remove .desktop file from build.yml

* Make download button link to decky-installer repo

* Point download button to the .desktop file not the .sh file 

woops

* Delete decky_installer.desktop

* Delete user_install_script.sh
2023-01-09 10:16:20 -08:00
Party Wumpus e92b66068a Use the new installer in the readme instructions (#324) 2023-01-08 13:16:44 -08:00
AAGaming b72b327610 Fix reloading UI on updates and restarting steam (#303) 2023-01-07 17:33:28 -08:00
Party Wumpus b8fdff8093 Add feature requests as an issue template (#318)
* Create feature_request.yml

* Update feature_request.yml
2023-01-06 09:23:05 -08:00
AAGaming 880b4c2f8f maybe working fix for jan 05 beta (#316) 2023-01-05 20:00:48 -08:00
TrainDoctor 34af340009 Update config.yml 2023-01-05 18:50:10 -08:00
Party Wumpus 80b6115f6f User Friendlier Installer (#297)
* Add files via upload

* Rename EasierInstallScript.sh to user_install_script.sh

* Add files via upload

* change so it works on deck instead of my desktop

* Update decky_installer.desktop

* make auto password setter work without the password

* Update user_install_script.sh

* make installer exit properly if user does not accept temp password

* Update user_install_script.sh

* add uninstall option

* Update user_install_script.sh

* Update user_install_script.sh

* Update user_install_script.sh

* "optimisation"

* Update user_install_script.sh

* Add sizing to all zenity prompts

* "optimization" part 2

* "Program now runs 50% faster"

:)

* Update user_install_script.sh

* Update user_install_script.sh

* Update user_install_script.sh

* Change text in branch selection in installer

'Select Branch' if choosing between release and prerelease
'Select Option' if choosing between release, prerelease and uninstall

* .desktop file points at where script is going to be

* add comments

* Change "installing" to "uninstalling"

* change it to ask for "sudo/admin" password

* Add secondary loading bar for download progress

Shamelessly stolen (with permission) from emudeck, who stole it from a random blog
No I don't know how that line works, and I don't think I want to.

* Make uninstaller tell user they can exit

* add default text to the download bar just in case

* silence script download

* silence password check
2023-01-02 08:52:11 -08:00
Party Wumpus 3bed83697e Add cef debugging to the installer scripts (#310)
* Update install_prerelease.sh

* Update install_release.sh
2023-01-02 08:46:46 -08:00
Party Wumpus 0ffef6e4bf Better bug report format (#312)
* Add files via upload

* Delete bug_report.md

* Update bug_report.yml

* Update bug_report.yml

* Update bug_report.yml

* Update bug_report.yml

* Update bug_report.yml

* Update bug_report.yml

* Update bug_report.yml
2023-01-02 08:45:42 -08:00
AAGaming 8810a014f3 somehow accidentally left this in 2022-12-29 13:11:11 -05:00
AAGaming 385552451b shut down steam instead of restarting it to avoid broken CEF debugger (gamescope will restart stean for us instead) 2022-12-28 12:24:28 -05:00
AAGaming c2c9d11c66 fix broken valveInternal when on a multi-user deck 2022-12-28 12:23:42 -05:00
Nik 0474095a40 Potentially fix locale issues (#284) 2022-12-16 06:23:04 -08:00
AAGaming 346f80beb3 bump DFL to fix modals, Router -> Navigation in some places 2022-12-15 21:16:22 -05:00
TrainDoctor 2a6bf75f02 Move back to python 3.10.2 in CI 2022-12-10 15:26:51 -08:00
jurassicplayer f73918c902 feat(MoreCustomizableToasts): Allow plugin developers to customize some toast properties (#268)
* Use settingsStore directly

* Change toast etype, add showToast/playSound

* Update DFL and rebase
2022-12-10 15:09:21 -08:00
TrainDoctor ea35af2050 Update build.yml 2022-12-08 15:18:44 -08:00
NGnius (Graham) 6232e3da58 Add custom CDN support for custom stores (#269)
* Add custom CDN support for custom stores

* Update Python for CI
2022-12-07 16:27:32 -08:00
TrainDoctor 35e46f9ccb Update build.yml 2022-12-07 14:31:09 -08:00
TrainDoctor 2b9a80c151 Update install_prerelease.sh 2022-12-04 19:05:29 -08:00
TrainDoctor a90ed38c89 Update install_release.sh 2022-12-04 19:05:16 -08:00
TrainDoctor 3653cf5640 Update plugin_loader-release.service 2022-12-04 19:05:01 -08:00
TrainDoctor 0db45ca71e Update plugin_loader-prerelease.service 2022-12-04 19:04:46 -08:00
AAGaming 16681fabb5 fix http requests 2022-11-19 22:33:51 -05:00
AAGaming c210523a22 fix handleWarning in rollup config 2022-11-19 20:07:08 -05:00
Marco Rodolfi 5d8601347a Fix for wrong path for settings json files (#258)
Co-authored-by: AAGaming <aa@mail.catvibers.me>
2022-11-19 19:34:38 -05:00
AAGaming 1e02fcf394 fix broken trycatch causing occasional injection failures 2022-11-19 19:22:30 -05:00
TrainDoctor f923306a7f Update issue templates 2022-11-19 14:19:33 -08:00
TrainDoctor 478fe32527 Revert "Fix for setting json files ending up in ~/homebrew"
This reverts commit aec7063139.
2022-11-15 15:01:26 -08:00
52 changed files with 1190 additions and 536 deletions
+73
View File
@@ -0,0 +1,73 @@
name: Bug report
description: File a bug/issue
title: "[BUG] <title>"
labels: [bug]
body:
- type: checkboxes
id: low-effort-checks
attributes:
label: Please confirm
description: Issues without all checks may be ignored/closed.
options:
- label: I have searched existing issues
- label: This issue is not a duplicate of an existing one
- label: I have checked the [common issues section in the readme file](https://github.com/SteamDeckHomebrew/decky-loader#-common-issues)
- type: textarea
attributes:
label: Bug Report Description
description: A clear and concise description of what the bug is and if possible, the steps you used to get to the bug. If appropriate, include screenshots or videos.
placeholder: |
When I try to use ...
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Expected Behaviour
description: A brief description of the expected behavior.
placeholder: It should be ...
validations:
required: true
- type: input
attributes:
label: SteamOS version
# description: Can be found with `uname -a`
# placeholder: "Linux steamdeck 5.13.0-valve36-1-neptune #1 SMP PREEMPT Mon, 19 Dec 2022 23:39:41 +0000 x86_64 GNU/Linux"
placeholder: "SteamOS 3.4.3 Stable"
validations:
required: true
- type: dropdown
attributes:
label: Selected Update Channel
description: Which branch of Decky are you on?
multiple: false
options:
- Stable
- Prerelease
validations:
required: true
- type: input
attributes:
label: Have you modified the read-only filesystem at any point?
description: Describe how here, if you haven't done anything you can leave this blank
placeholder: Yes, I've installed neofetch via pacman.
validations:
required: false
- type: textarea
attributes:
label: Logs
description: Please reboot your deck (if possible) when attempting to recreate the issue, then run ``cd ~ && journalctl -b0 -u plugin_loader.service > deckylog.txt``. This will save the log file to ``~`` aka ``/home/deck``. Please upload the file here
placeholder: deckylog.txt
validations:
required: false
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Steam Deck Homebrew Discord Server
url: https://discord.gg/ZU74G2NJzk
about: Please ask and answer questions here.
@@ -0,0 +1,35 @@
name: Feature request
description: Request a new feature (NOT A PLUGIN)
title: "[Request] <title>"
labels: [feature request]
body:
- type: checkboxes
id: low-effort-checks
attributes:
label: Please confirm
description: Issues without all checks may be ignored/closed.
options:
- label: I have searched existing issues
- label: This issue is not a duplicate of an existing one
- label: This is not a request for a plugin
- type: textarea
attributes:
label: Feature Request Description
description: A clear and concise description of what the new feature.
placeholder: |
Decky plugins should be sortable in the quick access menu
validations:
required: true
- type: textarea
attributes:
label: Further Description
description: A further explanation of the feature. If appropriate, include screenshots or videos.
placeholder: |
This would help make the UI clearer and easier to use as there is less clutter in the QAM.
It would also make it faster to access plugins that are used more.
This could be implemented by adding ...
validations:
required: false
+3 -5
View File
@@ -31,7 +31,7 @@ permissions:
jobs: jobs:
build: build:
name: Build PluginLoader name: Build PluginLoader
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- name: Print input - name: Print input
@@ -130,9 +130,7 @@ jobs:
OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT") OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT")
printf "OUT: ${OUT}\n" printf "OUT: ${OUT}\n"
else else
printf "no type selected, defaulting to patch.\n" printf "no type selected, not bumping for release.\n"
OUT=$(semver bump patch "$OUT")
printf "OUT: ${OUT}\n"
fi fi
elif [[ ! "$VERSION" =~ "-pre" ]]; then elif [[ ! "$VERSION" =~ "-pre" ]]; then
printf "previous tag is a release, bumping by selected type.\n" printf "previous tag is a release, bumping by selected type.\n"
@@ -159,7 +157,7 @@ jobs:
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: ${{ github.event_name == 'workflow_dispatch' && !env.ACT }} if: ${{ github.event_name == 'workflow_dispatch' && !env.ACT }}
with: with:
name: Prerelease ${{ steps.ready_tag.outputs.tag_name }} name: Release ${{ steps.ready_tag.outputs.tag_name }}
tag_name: ${{ steps.ready_tag.outputs.tag_name }} tag_name: ${{ steps.ready_tag.outputs.tag_name }}
files: ./dist/PluginLoader files: ./dist/PluginLoader
prerelease: false prerelease: false
+17
View File
@@ -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
+15 -13
View File
@@ -1,7 +1,9 @@
<h1 align="center"> <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> <br>
Decky Loader 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>
</h1> </h1>
<p align="center"> <p align="center">
@@ -34,13 +36,10 @@ For more information about Decky Loader as well as documentation and development
- Crankshaft is incompatible with Decky Loader. If you are using Crankshaft, please uninstall it before installing Decky Loader. - 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. - Syncthing may use port 8080 on Steam Deck, which Decky Loader needs to function. If you are using Syncthing as a service, please change its port to something else.
- If you are using any software that uses port 1337 or 8080, please change its port to something else or uninstall it. - If you are using any software that uses port 1337 or 8080, please change its port to something else or uninstall it.
- If you run the installer and it just opens a file in a text editor: click the (...) button in the top right of dolphin (the file manager) then 'configure' and 'configure dolphin'. Click on the 'confirmations' tab and set 'when opening an executable file' to 'run script'.
## 💾 Installation ## 💾 Installation
- This installation can be done without an admin/sudo password set.
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Settings menu.
1. Navigate to the System menu and scroll to the System Settings. Toggle "Enable Developer Mode" so it is enabled.
1. Navigate to the Developer menu and scroll to Miscellaneous. Toggle "CEF Remote Debugging" so it is enabled.
1. Select "Restart Now" to apply your changes.
1. Prepare a mouse and keyboard if possible. 1. Prepare a mouse and keyboard if possible.
- Keyboards and mice can be connected to the Steam Deck via USB-C or Bluetooth. - 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.
@@ -48,24 +47,27 @@ For more information about Decky Loader as well as documentation and development
- 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. - 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. 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. Select "Switch to Desktop".
1. Open the Konsole app and enter the command `passwd`. You can skip this step if you have already created a sudo password using this command. ([YouTube Guide](https://www.youtube.com/watch?v=1vOMYGj22rQ)) 1. Navigate to this Github page on a browser of your choice.
1. You will be prompted to create a password. Your text will not be visible. After you press enter, you will need to type your password again to confirm. 1. Press the 'Download' button at the top of the page.
1. Choose the version of Decky Loader you want to install and paste the following command into the Konsole app. 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. Choose the version of Decky Loader you want to install.
- **Latest Release** - **Latest Release**
Intended for most users. This is the latest stable version of Decky Loader. Intended for most users. This is the latest stable version of Decky Loader.
`curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_release.sh | sh`
- **Latest Pre-Release** - **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://deckbrew.xyz/en/loader-dev/development).
`curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_prerelease.sh | sh`
1. Open the Return to Gaming Mode shortcut on your desktop. 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 ### 👋 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://discord.gg/ZU74G2NJzk) 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. 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. Select "Switch to Desktop".
1. Open the Konsole app and run `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/uninstall.sh | sh`. 1. Run the installer file again, and select `uninstall decky loader`
- There is also a fast uninstall for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/uninstall.sh | sh` and type your password when prompted.
## 🚀 Getting Started ## 🚀 Getting Started
+2 -2
View File
@@ -26,10 +26,10 @@ cd ..
if [[ "$type" == "release" ]]; then if [[ "$type" == "release" ]]; then
printf "release!\n" 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 elif [[ "$type" == "prerelease" ]]; then
printf "prerelease!\n" 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 else
printf "Release type unspecified/badly specified.\n" printf "Release type unspecified/badly specified.\n"
printf "Options: 'release' or 'prerelease'\n" printf "Options: 'release' or 'prerelease'\n"
+13 -4
View File
@@ -1,5 +1,7 @@
# Full imports # Full imports
import json import json
# import pprint
# from pprint import pformat
# Partial imports # Partial imports
from aiohttp import ClientSession, web from aiohttp import ClientSession, web
@@ -55,7 +57,7 @@ class PluginBrowser:
pluginBinPath = path.join(pluginBasePath, 'bin') pluginBinPath = path.join(pluginBasePath, 'bin')
if access(packageJsonPath, R_OK): if access(packageJsonPath, R_OK):
with open(packageJsonPath, 'r') as f: with open(packageJsonPath, "r", encoding="utf-8") as f:
packageJson = json.load(f) packageJson = json.load(f)
if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0: if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0:
# create bin directory if needed. # create bin directory if needed.
@@ -93,7 +95,7 @@ class PluginBrowser:
def find_plugin_folder(self, name): def find_plugin_folder(self, name):
for folder in listdir(self.plugin_path): for folder in listdir(self.plugin_path):
try: try:
with open(path.join(self.plugin_path, folder, 'plugin.json'), 'r') as f: with open(path.join(self.plugin_path, folder, 'plugin.json'), "r", encoding="utf-8") as f:
plugin = json.load(f) plugin = json.load(f)
if plugin['name'] == name: if plugin['name'] == name:
@@ -108,11 +110,18 @@ class PluginBrowser:
try: try:
logger.info("uninstalling " + name) logger.info("uninstalling " + name)
logger.info(" at dir " + self.find_plugin_folder(name)) logger.info(" at dir " + self.find_plugin_folder(name))
logger.debug("unloading %s" % str(name)) logger.debug("calling frontend unload for %s" % str(name))
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')") res = await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
logger.debug("result of unload from UI: %s", res)
# plugins_snapshot = self.plugins.copy()
# snapshot_string = pformat(plugins_snapshot)
# logger.debug("current plugins: %s", snapshot_string)
if self.plugins[name]: if self.plugins[name]:
logger.debug("Plugin %s was found", name)
self.plugins[name].stop() self.plugins[name].stop()
logger.debug("Plugin %s was stopped", name)
del self.plugins[name] del self.plugins[name]
logger.debug("Plugin %s was removed from the dictionary", name)
logger.debug("removing files %s" % str(name)) logger.debug("removing files %s" % str(name))
rmtree(self.find_plugin_folder(name)) rmtree(self.find_plugin_folder(name))
except FileNotFoundError: except FileNotFoundError:
+70 -39
View File
@@ -1,8 +1,11 @@
import grp
import pwd
import re import re
import ssl import ssl
import subprocess import subprocess
import uuid import uuid
import os import os
import sys
from subprocess import check_output from subprocess import check_output
from time import sleep from time import sleep
from hashlib import sha256 from hashlib import sha256
@@ -17,8 +20,6 @@ REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
# global vars # global vars
csrf_token = str(uuid.uuid4()) csrf_token = str(uuid.uuid4())
ssl_ctx = ssl.create_default_context(cafile=certifi.where()) ssl_ctx = ssl.create_default_context(cafile=certifi.where())
user = None
group = None
assets_regex = re.compile("^/plugins/.*/assets/.*") assets_regex = re.compile("^/plugins/.*/assets/.*")
frontend_regex = re.compile("^/frontend/.*") frontend_regex = re.compile("^/frontend/.*")
@@ -35,57 +36,87 @@ async def csrf_middleware(request, handler):
return await handler(request) return await handler(request)
return Response(text='Forbidden', status='403') return Response(text='Forbidden', status='403')
# Get the user by checking for the first logged in user. As this is run # Deprecated
# 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(): def set_user():
global user pass
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. # Get the user id hosting the plugin loader
def get_user_id() -> int:
proc_path = os.path.realpath(sys.argv[0])
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
for pw in pws:
if proc_path.startswith(os.path.realpath(pw.pw_dir)):
return pw.pw_uid
raise PermissionError("The plugin loader does not seem to be hosted by any known user.")
# Get the user hosting the plugin loader
def get_user() -> str: def get_user() -> str:
global user return pwd.getpwuid(get_user_id()).pw_name
if user == None:
raise ValueError("helpers.get_user method called before user variable was set. Run helpers.set_user first.")
return user
# Set the global user group. get_user must be called first # 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
# Deprecated
def set_user_group() -> str: def set_user_group() -> str:
global group return get_user_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. # 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: def get_user_group() -> str:
global group return grp.getgrgid(get_user_group_id()).gr_name
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 # Get the default home path unless a user is specified
def get_home_path(username = None) -> str: def get_home_path(username = None) -> str:
if username == None: if username == None:
raise ValueError("Username not defined, no home path can be found.") username = get_user()
else: return pwd.getpwnam(username).pw_dir
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
def get_homebrew_path(home_path = None) -> str: def get_homebrew_path(home_path = None) -> str:
if home_path == None: if home_path == None:
raise ValueError("Home path not defined, homebrew dir cannot be determined.") home_path = get_home_path()
else: return os.path.join(home_path, "homebrew")
return str(home_path+"/homebrew")
# return str(home_path+"/homebrew") # Recursively create path and chown as user
def mkdir_as_user(path):
path = os.path.realpath(path)
os.makedirs(path, exist_ok=True)
chown_path = get_home_path()
parts = os.path.relpath(path, chown_path).split(os.sep)
uid = get_user_id()
gid = get_user_group_id()
for p in parts:
chown_path = os.path.join(chown_path, p)
os.chown(chown_path, uid, gid)
# Fetches the version of loader
def get_loader_version() -> str:
with open(os.path.join(os.path.dirname(sys.argv[0]), ".loader.version"), "r", encoding="utf-8") as version_file:
return version_file.readline().replace("\n", "")
# Download Remote Binaries to local Plugin # Download Remote Binaries to local Plugin
async def download_remote_binary_to_path(url, binHash, path) -> bool: async def download_remote_binary_to_path(url, binHash, path) -> bool:
+12 -1
View File
@@ -394,9 +394,12 @@ async def get_tab_lambda(test) -> Tab:
raise ValueError(f"Tab not found by lambda") raise ValueError(f"Tab not found by lambda")
return tab return 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")
async def get_gamepadui_tab() -> Tab: async def get_gamepadui_tab() -> Tab:
tabs = await get_tabs() tabs = await get_tabs()
tab = next((i for i in tabs if ("https://steamloopback.host/routes/" in i.url and (i.title == "Steam" or i.title == "SP"))), None) tab = next((i for i in tabs if tab_is_gamepadui(i)), None)
if not tab: if not tab:
raise ValueError(f"GamepadUI Tab not found") raise ValueError(f"GamepadUI Tab not found")
return tab return tab
@@ -405,3 +408,11 @@ async def inject_to_tab(tab_name, js, run_async=False):
tab = await get_tab(tab_name) tab = await get_tab(tab_name)
return await tab.evaluate_js(js, run_async) return await tab.evaluate_js(js, run_async)
async def close_old_tabs():
tabs = await get_tabs()
for t in tabs:
if not t.title or (t.title != "Steam Shared Context presented by Valve™" and t.title != "Steam" and t.title != "SP"):
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
await t.close()
await sleep(0.5)
+3 -3
View File
@@ -118,7 +118,7 @@ class Loader:
def handle_frontend_bundle(self, request): def handle_frontend_bundle(self, request):
plugin = self.plugins[request.match_info["plugin_name"]] plugin = self.plugins[request.match_info["plugin_name"]]
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), 'r') as bundle: with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), "r", encoding="utf-8") as bundle:
return web.Response(text=bundle.read(), content_type="application/javascript") return web.Response(text=bundle.read(), content_type="application/javascript")
def import_plugin(self, file, plugin_directory, refresh=False, batch=False): def import_plugin(self, file, plugin_directory, refresh=False, batch=False):
@@ -186,7 +186,7 @@ class Loader:
""" """
async def load_plugin_main_view(self, request): async def load_plugin_main_view(self, request):
plugin = self.plugins[request.match_info["name"]] plugin = self.plugins[request.match_info["name"]]
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template: with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), "r", encoding="utf-8") as template:
template_data = template.read() template_data = template.read()
ret = f""" ret = f"""
<script src="/legacy/library.js"></script> <script src="/legacy/library.js"></script>
@@ -202,7 +202,7 @@ class Loader:
self.logger.info(path) self.logger.info(path)
ret = "" ret = ""
file_path = path.join(self.plugin_path, plugin.plugin_directory, route_path) file_path = path.join(self.plugin_path, plugin.plugin_directory, route_path)
with open(file_path, 'r') as resource_data: with open(file_path, "r", encoding="utf-8") as resource_data:
ret = resource_data.read() ret = resource_data.read()
return web.Response(text=ret) return web.Response(text=ret)
+13 -25
View File
@@ -19,27 +19,19 @@ from aiohttp_jinja2 import setup as jinja_setup
# local modules # local modules
from browser import PluginBrowser from browser import PluginBrowser
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token, from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
get_home_path, get_homebrew_path, get_user, get_home_path, get_homebrew_path, get_user, get_user_group,
get_user_group, set_user, set_user_group,
stop_systemd_unit, start_systemd_unit) stop_systemd_unit, start_systemd_unit)
from injector import get_gamepadui_tab, Tab, get_tabs from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs
from loader import Loader from loader import Loader
from settings import SettingsManager from settings import SettingsManager
from updater import Updater from updater import Updater
from utilities import Utilities from utilities import Utilities
# 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() USER = get_user()
GROUP = get_user_group() GROUP = get_user_group()
HOME_PATH = "/home/"+USER HOMEBREW_PATH = get_homebrew_path()
HOMEBREW_PATH = HOME_PATH+"/homebrew"
CONFIG = { CONFIG = {
"plugin_path": getenv("PLUGIN_PATH", HOMEBREW_PATH+"/plugins"), "plugin_path": getenv("PLUGIN_PATH", path.join(HOMEBREW_PATH, "plugins")),
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1", "chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
"server_host": getenv("SERVER_HOST", "127.0.0.1"), "server_host": getenv("SERVER_HOST", "127.0.0.1"),
"server_port": int(getenv("SERVER_PORT", "1337")), "server_port": int(getenv("SERVER_PORT", "1337")),
@@ -56,12 +48,15 @@ basicConfig(
logger = getLogger("Main") logger = getLogger("Main")
async def chown_plugin_dir(): def chown_plugin_dir():
code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]]) code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]])
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]]) code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
if code_chown != 0 or code_chmod != 0: 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})") logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
if CONFIG["chown_plugin_path"] == True:
chown_plugin_dir()
class PluginManager: class PluginManager:
def __init__(self, loop) -> None: def __init__(self, loop) -> None:
self.loop = loop self.loop = loop
@@ -87,8 +82,6 @@ class PluginManager:
self.loop.create_task(start_systemd_unit(REMOTE_DEBUGGER_UNIT)) self.loop.create_task(start_systemd_unit(REMOTE_DEBUGGER_UNIT))
else: else:
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT)) self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT))
if CONFIG["chown_plugin_path"] == True:
chown_plugin_dir()
self.loop.create_task(self.loader_reinjector()) self.loop.create_task(self.loader_reinjector())
self.loop.create_task(self.load_plugins()) self.loop.create_task(self.load_plugins())
@@ -124,7 +117,7 @@ class PluginManager:
while not tab: while not tab:
try: try:
tab = await get_gamepadui_tab() tab = await get_gamepadui_tab()
except client_exceptions.ClientConnectorError or client_exceptions.ServerDisconnectedError: except (client_exceptions.ClientConnectorError, client_exceptions.ServerDisconnectedError):
if not dc: if not dc:
logger.debug("Couldn't connect to debugger, waiting...") logger.debug("Couldn't connect to debugger, waiting...")
dc = True dc = True
@@ -168,15 +161,10 @@ class PluginManager:
async def inject_javascript(self, tab: Tab, first=False, request=None): async def inject_javascript(self, tab: Tab, first=False, request=None):
logger.info("Loading Decky frontend!") logger.info("Loading Decky frontend!")
try: try:
# if first: if first:
# if await tab.has_global_var("deckyHasLoaded", False): if await tab.has_global_var("deckyHasLoaded", False):
# tabs = await get_tabs() await close_old_tabs()
# for t in tabs: await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => location.reload(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}", False, False, False)
# if t.title != "Steam" and t.title != "SP":
# logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
# await t.close()
# await sleep(0.5)
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => SteamClient.User.StartRestart(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}", False, False, False)
except: except:
logger.info("Failed to inject JavaScript into tab\n" + format_exc()) logger.info("Failed to inject JavaScript into tab\n" + format_exc())
pass pass
+32 -9
View File
@@ -7,10 +7,12 @@ from importlib.util import module_from_spec, spec_from_file_location
from json import dumps, load, loads from json import dumps, load, loads
from logging import getLogger from logging import getLogger
from traceback import format_exc from traceback import format_exc
from os import path, setgid, setuid from os import path, setgid, setuid, environ
from signal import SIGINT, signal from signal import SIGINT, signal
from sys import exit from sys import exit
from time import time from time import time
import helpers
from updater import Updater
multiprocessing.set_start_method("fork") multiprocessing.set_start_method("fork")
@@ -19,6 +21,7 @@ BUFFER_LIMIT = 2 ** 20 # 1 MiB
class PluginWrapper: class PluginWrapper:
def __init__(self, file, plugin_directory, plugin_path) -> None: def __init__(self, file, plugin_directory, plugin_path) -> None:
self.file = file self.file = file
self.plugin_path = plugin_path
self.plugin_directory = plugin_directory self.plugin_directory = plugin_directory
self.reader = None self.reader = None
self.writer = None self.writer = None
@@ -27,9 +30,9 @@ class PluginWrapper:
self.version = None self.version = None
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r")) json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")): if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r")) package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
self.version = package_json["version"] self.version = package_json["version"]
@@ -56,8 +59,24 @@ class PluginWrapper:
set_event_loop(new_event_loop()) set_event_loop(new_event_loop())
if self.passive: if self.passive:
return return
setgid(0 if "root" in self.flags else 1000) setgid(0 if "root" in self.flags else helpers.get_user_group_id())
setuid(0 if "root" in self.flags else 1000) setuid(0 if "root" in self.flags else helpers.get_user_id())
# export a bunch of environment variables to help plugin developers
environ["HOME"] = helpers.get_home_path("root" if "root" in self.flags else helpers.get_user())
environ["USER"] = "root" if "root" in self.flags else helpers.get_user()
environ["DECKY_VERSION"] = helpers.get_loader_version()
environ["DECKY_USER"] = helpers.get_user()
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
spec = spec_from_file_location("_", self.file) spec = spec_from_file_location("_", self.file)
module = module_from_spec(spec) module = module_from_spec(spec)
spec.loader.exec_module(module) spec.loader.exec_module(module)
@@ -73,9 +92,12 @@ class PluginWrapper:
async def _unload(self): async def _unload(self):
try: 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"): if hasattr(self.Plugin, "_unload"):
await self.Plugin._unload(self.Plugin) 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: except:
self.log.error("Failed to unload " + self.name + "!\n" + format_exc()) self.log.error("Failed to unload " + self.name + "!\n" + format_exc())
exit(0) exit(0)
@@ -99,6 +121,7 @@ class PluginWrapper:
break break
data = loads(line.decode("utf-8")) data = loads(line.decode("utf-8"))
if "stop" in data: if "stop" in data:
self.log.info("Calling Loader unload function.")
await self._unload() await self._unload()
get_event_loop().stop() get_event_loop().stop()
while get_event_loop().is_running(): while get_event_loop().is_running():
@@ -112,7 +135,7 @@ class PluginWrapper:
d["res"] = str(e) d["res"] = str(e)
d["success"] = False d["success"] = False
finally: finally:
writer.write((dumps(d)+"\n").encode("utf-8")) writer.write((dumps(d, ensure_ascii=False)+"\n").encode("utf-8"))
await writer.drain() await writer.drain()
async def _open_socket_if_not_exists(self): async def _open_socket_if_not_exists(self):
@@ -140,7 +163,7 @@ class PluginWrapper:
return return
async def _(self): async def _(self):
if await self._open_socket_if_not_exists(): if await self._open_socket_if_not_exists():
self.writer.write((dumps({"stop": True})+"\n").encode("utf-8")) self.writer.write((dumps({ "stop": True }, ensure_ascii=False)+"\n").encode("utf-8"))
await self.writer.drain() await self.writer.drain()
self.writer.close() self.writer.close()
get_event_loop().create_task(_(self)) get_event_loop().create_task(_(self))
@@ -151,7 +174,7 @@ class PluginWrapper:
async with self.method_call_lock: async with self.method_call_lock:
if await self._open_socket_if_not_exists(): if await self._open_socket_if_not_exists():
self.writer.write( self.writer.write(
(dumps({"method": method_name, "args": kwargs})+"\n").encode("utf-8")) (dumps({ "method": method_name, "args": kwargs }, ensure_ascii=False) + "\n").encode("utf-8"))
await self.writer.drain() await self.writer.drain()
line = bytearray() line = bytearray()
while True: while True:
+25 -9
View File
@@ -1,40 +1,56 @@
import imp
from json import dump, load from json import dump, load
from os import mkdir, path from os import mkdir, path, listdir, rename
from shutil import chown
from helpers import get_home_path, get_homebrew_path, get_user, set_user from helpers import get_home_path, get_homebrew_path, get_user, get_user_group, get_user_owner
class SettingsManager: class SettingsManager:
def __init__(self, name, settings_directory = None) -> None: def __init__(self, name, settings_directory = None) -> None:
set_user()
USER = get_user() USER = get_user()
GROUP = get_user_group()
wrong_dir = get_homebrew_path()
if settings_directory == None: if settings_directory == None:
settings_directory = path.join(get_homebrew_path(get_home_path(USER)), "settings") settings_directory = path.join(wrong_dir, "settings")
self.path = path.join(settings_directory, name + ".json") self.path = path.join(settings_directory, name + ".json")
#Create the folder with the correct permission
if not path.exists(settings_directory): if not path.exists(settings_directory):
mkdir(settings_directory) mkdir(settings_directory)
chown(settings_directory, USER, GROUP)
#Copy all old settings file in the root directory to the correct folder
for file in listdir(wrong_dir):
if file.endswith(".json"):
rename(path.join(wrong_dir,file),
path.join(settings_directory, file))
self.path = path.join(settings_directory, name + ".json")
#If the owner of the settings directory is not the user, then set it as the user:
if get_user_owner(settings_directory) != USER:
chown(settings_directory, USER, GROUP)
self.settings = {} self.settings = {}
try: try:
open(self.path, "x") open(self.path, "x", encoding="utf-8")
except FileExistsError as e: except FileExistsError as e:
self.read() self.read()
pass pass
def read(self): def read(self):
try: try:
with open(self.path, "r") as file: with open(self.path, "r", encoding="utf-8") as file:
self.settings = load(file) self.settings = load(file)
except Exception as e: except Exception as e:
print(e) print(e)
pass pass
def commit(self): def commit(self):
with open(self.path, "w+") as file: with open(self.path, "w+", encoding="utf-8") as file:
dump(self.settings, file, indent=4) dump(self.settings, file, indent=4, ensure_ascii=False)
def getSetting(self, key, default): def getSetting(self, key, default):
return self.settings.get(key, default) return self.settings.get(key, default)
+5 -7
View File
@@ -31,9 +31,7 @@ class Updater:
self.remoteVer = None self.remoteVer = None
self.allRemoteVers = None self.allRemoteVers = None
try: try:
logger.info(getcwd()) self.localVer = helpers.get_loader_version()
with open(path.join(getcwd(), ".loader.version"), 'r') as version_file:
self.localVer = version_file.readline().replace("\n", "")
except: except:
self.localVer = False self.localVer = False
@@ -159,10 +157,10 @@ class Updater:
out.write(data) out.write(data)
except Exception as e: except Exception as e:
logger.error(f"Error at %s", exc_info=e) logger.error(f"Error at %s", exc_info=e)
with open(path.join(getcwd(), "plugin_loader.service"), 'r') as service_file: with open(path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8") as service_file:
service_data = service_file.read() service_data = service_file.read()
service_data = service_data.replace("${HOMEBREW_FOLDER}", "/home/"+helpers.get_user()+"/homebrew") service_data = service_data.replace("${HOMEBREW_FOLDER}", helpers.get_homebrew_path())
with open(path.join(getcwd(), "plugin_loader.service"), 'w') as service_file: with open(path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8") as service_file:
service_file.write(service_data) service_file.write(service_data)
logger.debug("Saved service file") logger.debug("Saved service file")
@@ -191,7 +189,7 @@ class Updater:
self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False)) self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False))
progress = new_progress progress = new_progress
with open(path.join(getcwd(), ".loader.version"), "w") as out: with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
out.write(version) out.write(version)
call(['chmod', '+x', path.join(getcwd(), "PluginLoader")]) call(['chmod', '+x', path.join(getcwd(), "PluginLoader")])
+22 -5
View File
@@ -7,7 +7,7 @@ from asyncio import sleep, start_server, gather, open_connection
from aiohttp import ClientSession, web from aiohttp import ClientSession, web
from logging import getLogger from logging import getLogger
from injector import inject_to_tab, get_gamepadui_tab from injector import inject_to_tab, get_gamepadui_tab, close_old_tabs
import helpers import helpers
import subprocess import subprocess
@@ -81,10 +81,11 @@ class Utilities:
async def http_request(self, method="", url="", **kwargs): async def http_request(self, method="", url="", **kwargs):
async with ClientSession() as web: async with ClientSession() as web:
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs) res = await web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs)
text = await res.text()
return { return {
"status": res.status, "status": res.status,
"headers": dict(res.headers), "headers": dict(res.headers),
"body": await res.text() "body": text
} }
async def ping(self, **kwargs): async def ping(self, **kwargs):
@@ -232,7 +233,7 @@ class Utilities:
self.rdt_proxy_server.close() self.rdt_proxy_server.close()
self.rdt_proxy_task.cancel() self.rdt_proxy_task.cancel()
async def enable_rdt(self): async def _enable_rdt(self):
# TODO un-hardcode port # TODO un-hardcode port
try: try:
self.stop_rdt_proxy() self.stop_rdt_proxy()
@@ -242,14 +243,26 @@ class Utilities:
self.logger.info("Connecting to React DevTools at " + ip) self.logger.info("Connecting to React DevTools at " + ip)
async with ClientSession() as web: async with ClientSession() as web:
res = await web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context()) res = await web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context())
script = """
if (!window.deckyHasConnectedRDT) {
window.deckyHasConnectedRDT = true;
// This fixes the overlay when hovering over an element in RDT
Object.defineProperty(window, '__REACT_DEVTOOLS_TARGET_WINDOW__', {
enumerable: true,
configurable: true,
get: function() {
return FocusNavController?.m_ActiveContext?.ActiveWindow || window;
}
});
""" + await res.text() + "\n}"
if res.status != 200: if res.status != 200:
self.logger.error("Failed to connect to React DevTools at " + ip) self.logger.error("Failed to connect to React DevTools at " + ip)
return False return False
self.start_rdt_proxy(ip, 8097) self.start_rdt_proxy(ip, 8097)
script = "if(!window.deckyHasConnectedRDT){window.deckyHasConnectedRDT=true;\n" + await res.text() + "\n}"
self.logger.info("Connected to React DevTools, loading script") self.logger.info("Connected to React DevTools, loading script")
tab = await get_gamepadui_tab() tab = await get_gamepadui_tab()
# RDT needs to load before React itself to work. # RDT needs to load before React itself to work.
await close_old_tabs()
result = await tab.reload_and_evaluate(script) result = await tab.reload_and_evaluate(script)
self.logger.info(result) self.logger.info(result)
@@ -257,9 +270,13 @@ class Utilities:
self.logger.error("Failed to connect to React DevTools") self.logger.error("Failed to connect to React DevTools")
self.logger.error(format_exc()) self.logger.error(format_exc())
async def enable_rdt(self):
self.context.loop.create_task(self._enable_rdt())
async def disable_rdt(self): async def disable_rdt(self):
self.logger.info("Disabling React DevTools") self.logger.info("Disabling React DevTools")
tab = await get_gamepadui_tab() tab = await get_gamepadui_tab()
self.rdt_script_id = None self.rdt_script_id = None
await tab.evaluate_js("SteamClient.User.StartRestart();", False, True, False) await close_old_tabs()
await tab.evaluate_js("location.reload();", False, True, False)
self.logger.info("React DevTools disabled") self.logger.info("React DevTools disabled")
+4 -1
View File
@@ -11,10 +11,12 @@ HOMEBREW_FOLDER="${USER_DIR}/homebrew"
rm -rf "${HOMEBREW_FOLDER}/services" rm -rf "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services" sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins" sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging"
# Download latest release and install it # Download latest release and install it
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))") RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))")
read VERSION DOWNLOADURL < <(echo $(jq -r '.tag_name, .assets[].browser_download_url' <<< ${RELEASE})) VERSION=$(jq -r '.tag_name' <<< ${RELEASE} )
DOWNLOADURL=$(jq -r '.assets[].browser_download_url | select(endswith("PluginLoader"))' <<< ${RELEASE})
printf "Installing version %s...\n" "${VERSION}" printf "Installing version %s...\n" "${VERSION}"
curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader
@@ -40,6 +42,7 @@ User=root
Restart=always Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=LOG_LEVEL=DEBUG Environment=LOG_LEVEL=DEBUG
[Install] [Install]
+4 -1
View File
@@ -11,10 +11,12 @@ HOMEBREW_FOLDER="${USER_DIR}/homebrew"
rm -rf "${HOMEBREW_FOLDER}/services" rm -rf "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services" sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins" sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging"
# Download latest release and install it # Download latest release and install it
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "false"))") RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "false"))")
read VERSION DOWNLOADURL < <(echo $(jq -r '.tag_name, .assets[].browser_download_url' <<< ${RELEASE})) VERSION=$(jq -r '.tag_name' <<< ${RELEASE} )
DOWNLOADURL=$(jq -r '.assets[].browser_download_url | select(endswith("PluginLoader"))' <<< ${RELEASE})
printf "Installing version %s...\n" "${VERSION}" printf "Installing version %s...\n" "${VERSION}"
curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader
@@ -40,6 +42,7 @@ User=root
Restart=always Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=LOG_LEVEL=INFO Environment=LOG_LEVEL=INFO
[Install] [Install]
+2 -1
View File
@@ -8,7 +8,8 @@ User=root
Restart=always Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=LOG_LEVEL=DEBUG Environment=LOG_LEVEL=DEBUG
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
+2 -1
View File
@@ -8,7 +8,8 @@ User=root
Restart=always Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services WorkingDirectory=${HOMEBREW_FOLDER}/services
KillSignal=SIGKILL
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=LOG_LEVEL=INFO Environment=LOG_LEVEL=INFO
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+2
View File
@@ -0,0 +1,2 @@
declare module '*.png';
declare module '*.jpg';
+2 -1
View File
@@ -12,6 +12,7 @@
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^21.1.0", "@rollup/plugin-commonjs": "^21.1.0",
"@rollup/plugin-image": "^3.0.1",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0", "@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0", "@rollup/plugin-replace": "^4.0.0",
@@ -41,7 +42,7 @@
} }
}, },
"dependencies": { "dependencies": {
"decky-frontend-lib": "^3.7.14", "decky-frontend-lib": "^3.18.10",
"react-file-icon": "^1.2.0", "react-file-icon": "^1.2.0",
"react-icons": "^4.4.0", "react-icons": "^4.4.0",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
+40 -4
View File
@@ -2,6 +2,7 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@rollup/plugin-commonjs': ^21.1.0 '@rollup/plugin-commonjs': ^21.1.0
'@rollup/plugin-image': ^3.0.1
'@rollup/plugin-json': ^4.1.0 '@rollup/plugin-json': ^4.1.0
'@rollup/plugin-node-resolve': ^13.3.0 '@rollup/plugin-node-resolve': ^13.3.0
'@rollup/plugin-replace': ^4.0.0 '@rollup/plugin-replace': ^4.0.0
@@ -10,7 +11,7 @@ specifiers:
'@types/react-file-icon': ^1.0.1 '@types/react-file-icon': ^1.0.1
'@types/react-router': 5.1.18 '@types/react-router': 5.1.18
'@types/webpack': ^5.28.0 '@types/webpack': ^5.28.0
decky-frontend-lib: ^3.7.14 decky-frontend-lib: ^3.18.10
husky: ^8.0.1 husky: ^8.0.1
import-sort-style-module: ^6.0.0 import-sort-style-module: ^6.0.0
inquirer: ^8.2.4 inquirer: ^8.2.4
@@ -30,7 +31,7 @@ specifiers:
typescript: ^4.7.4 typescript: ^4.7.4
dependencies: dependencies:
decky-frontend-lib: 3.7.14 decky-frontend-lib: 3.18.10
react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
react-icons: 4.4.0_react@16.14.0 react-icons: 4.4.0_react@16.14.0
react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u
@@ -38,6 +39,7 @@ dependencies:
devDependencies: devDependencies:
'@rollup/plugin-commonjs': 21.1.0_rollup@2.76.0 '@rollup/plugin-commonjs': 21.1.0_rollup@2.76.0
'@rollup/plugin-image': 3.0.1_rollup@2.76.0
'@rollup/plugin-json': 4.1.0_rollup@2.76.0 '@rollup/plugin-json': 4.1.0_rollup@2.76.0
'@rollup/plugin-node-resolve': 13.3.0_rollup@2.76.0 '@rollup/plugin-node-resolve': 13.3.0_rollup@2.76.0
'@rollup/plugin-replace': 4.0.0_rollup@2.76.0 '@rollup/plugin-replace': 4.0.0_rollup@2.76.0
@@ -339,6 +341,20 @@ packages:
rollup: 2.76.0 rollup: 2.76.0
dev: true dev: true
/@rollup/plugin-image/3.0.1_rollup@2.76.0:
resolution: {integrity: sha512-F50Sko4Xcc576x7HG9f3MvJKKnBfSmqfVFWJkJgyIEkI8YxZxux28lDbuy0+GsAK6BFl9Gn+TRXOUgHHJbFh3w==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@rollup/pluginutils': 5.0.2_rollup@2.76.0
mini-svg-data-uri: 1.4.4
rollup: 2.76.0
dev: true
/@rollup/plugin-inject/4.0.4_rollup@2.76.0: /@rollup/plugin-inject/4.0.4_rollup@2.76.0:
resolution: {integrity: sha512-4pbcU4J/nS+zuHk+c+OL3WtmEQhqxlZ9uqfjQMQDOHOPld7PsCd8k5LWs8h5wjwJN7MgnAn768F2sDxEP4eNFQ==} resolution: {integrity: sha512-4pbcU4J/nS+zuHk+c+OL3WtmEQhqxlZ9uqfjQMQDOHOPld7PsCd8k5LWs8h5wjwJN7MgnAn768F2sDxEP4eNFQ==}
peerDependencies: peerDependencies:
@@ -422,6 +438,21 @@ packages:
picomatch: 2.3.1 picomatch: 2.3.1
dev: true dev: true
/@rollup/pluginutils/5.0.2_rollup@2.76.0:
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@types/estree': 1.0.0
estree-walker: 2.0.2
picomatch: 2.3.1
rollup: 2.76.0
dev: true
/@types/debug/4.1.7: /@types/debug/4.1.7:
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
dependencies: dependencies:
@@ -944,8 +975,8 @@ packages:
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
/decky-frontend-lib/3.7.14: /decky-frontend-lib/3.18.10:
resolution: {integrity: sha512-ShAoP3VqiwkJYukDBHsOF9fk7wYw0VaKpHw6j9WdzLxwZwBcg0J7QBNIFYP3nfA0UgEwSJVEg/22kONiplipmA==} resolution: {integrity: sha512-2mgbA3sSkuwQR/FnmhXVrcW6LyTS95IuL6muJAmQCruhBvXapDtjk1TcgxqMZxFZwGD1IPnemPYxHZll6IgnZw==}
dev: false dev: false
/decode-named-character-reference/1.0.2: /decode-named-character-reference/1.0.2:
@@ -1936,6 +1967,11 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
/mini-svg-data-uri/1.4.4:
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
hasBin: true
dev: true
/minimatch/3.1.2: /minimatch/3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies: dependencies:
+15 -16
View File
@@ -1,30 +1,28 @@
import commonjs from '@rollup/plugin-commonjs'; import commonjs from '@rollup/plugin-commonjs';
import image from '@rollup/plugin-image';
import json from '@rollup/plugin-json'; import json from '@rollup/plugin-json';
import { nodeResolve } from '@rollup/plugin-node-resolve'; import { nodeResolve } from '@rollup/plugin-node-resolve';
import externalGlobals from "rollup-plugin-external-globals";
import del from 'rollup-plugin-delete'
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
import typescript from '@rollup/plugin-typescript'; import typescript from '@rollup/plugin-typescript';
import { defineConfig, handleWarning } from 'rollup'; import { defineConfig } from 'rollup';
import del from 'rollup-plugin-delete';
import externalGlobals from 'rollup-plugin-external-globals';
const hiddenWarnings = [ const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL'];
"THIS_IS_UNDEFINED",
"EVAL"
];
export default defineConfig({ export default defineConfig({
input: 'src/index.tsx', input: 'src/index.tsx',
plugins: [ plugins: [
del({ targets: "../backend/static/*", force: true }), del({ targets: '../backend/static/*', force: true }),
commonjs(), commonjs(),
nodeResolve(), nodeResolve(),
externalGlobals({ externalGlobals({
react: 'SP_REACT', react: 'SP_REACT',
'react-dom': 'SP_REACTDOM', 'react-dom': 'SP_REACTDOM',
// hack to shut up react-markdown // hack to shut up react-markdown
'process': '{cwd: () => {}}', process: '{cwd: () => {}}',
'path': '{dirname: () => {}, join: () => {}, basename: () => {}, extname: () => {}}', path: '{dirname: () => {}, join: () => {}, basename: () => {}, extname: () => {}}',
'url': '{fileURLToPath: (f) => f}' url: '{fileURLToPath: (f) => f}',
}), }),
typescript(), typescript(),
json(), json(),
@@ -32,17 +30,18 @@ export default defineConfig({
preventAssignment: false, preventAssignment: false,
'process.env.NODE_ENV': JSON.stringify('production'), 'process.env.NODE_ENV': JSON.stringify('production'),
}), }),
image(),
], ],
preserveEntrySignatures: false, preserveEntrySignatures: false,
output: { output: {
dir: '../backend/static', dir: '../backend/static',
format: 'esm', format: 'esm',
chunkFileNames: (chunkInfo) => { chunkFileNames: (chunkInfo) => {
return 'chunk-[hash].js' return 'chunk-[hash].js';
} },
}, },
onwarn: function ( message ) { onwarn: function (message, handleWarning) {
if (hiddenWarnings.some(warning => message.code === warning)) return; if (hiddenWarnings.some((warning) => message.code === warning)) return;
handleWarning(message); handleWarning(message);
} },
}); });
+37
View File
@@ -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>
);
}
+8 -7
View File
@@ -1,6 +1,7 @@
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib'; import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
import { CSSProperties, VFC } from 'react'; 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'; import { useDeckyState } from './DeckyState';
@@ -26,12 +27,6 @@ const TitleView: VFC = () => {
if (activePlugin === null) { if (activePlugin === null) {
return ( return (
<Focusable style={titleStyles} className={staticClasses.Title}> <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> <div style={{ marginRight: 'auto', flex: 0.9 }}>Decky</div>
<DialogButton <DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }} style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
@@ -39,6 +34,12 @@ const TitleView: VFC = () => {
> >
<FaStore style={{ marginTop: '-4px', display: 'block' }} /> <FaStore style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton> </DialogButton>
<DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
onClick={onSettingsClick}
>
<BsGearFill style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton>
</Focusable> </Focusable>
); );
} }
@@ -1,4 +1,4 @@
import { ConfirmModal, QuickAccessTab, Router, Spinner, staticClasses } from 'decky-frontend-lib'; import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
interface PluginInstallModalProps { interface PluginInstallModalProps {
@@ -20,21 +20,20 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
onOK={async () => { onOK={async () => {
setLoading(true); setLoading(true);
await onOK(); await onOK();
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 250); setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000); setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
}} }}
onCancel={async () => { onCancel={async () => {
await onCancel(); await onCancel();
}} }}
strTitle={`Install ${artifact}`}
strOKButtonText={loading ? 'Installing' : 'Install'}
> >
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}> {hash == 'False' ? (
{hash == 'False' ? <h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3> : null} <h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3>
<div style={{ flexDirection: 'row' }}> ) : (
{loading && <Spinner style={{ width: '20px' }} />} {loading ? 'Installing' : 'Install'} {artifact} `Are you sure you want to install ${artifact} ${version}?`
{version ? ' version ' + version : null} )}
{!loading && '?'}
</div>
</div>
</ConfirmModal> </ConfirmModal>
); );
}; };
@@ -1,4 +1,8 @@
import { Patch, findModuleChild, replacePatch } from 'decky-frontend-lib'; import { Patch, findModuleChild, replacePatch, sleep } from 'decky-frontend-lib';
import Logger from '../../../../logger';
const logger = new Logger('LibraryPatch');
declare global { declare global {
interface Window { interface Window {
@@ -10,36 +14,44 @@ declare global {
let patch: Patch; let patch: Patch;
function rePatch() { function rePatch() {
// If you patch anything on SteamClient within the first few seconds of the client having loaded it will get redefined for some reason, so repatch any of these changes that occur within the first 20s of the last patch // If you patch anything on SteamClient within the first few seconds of the client having loaded it will get redefined for some reason, so repatch any of these changes that occur with History.listen or an interval
patch = replacePatch(window.SteamClient.Apps, 'PromptToChangeShortcut', async ([appid]: number[]) => { patch = replacePatch(window.SteamClient.Apps, 'PromptToChangeShortcut', async ([appid]: number[]) => {
try { try {
const details = window.appDetailsStore.GetAppDetails(appid); const details = window.appDetailsStore.GetAppDetails(appid);
console.log(details); logger.debug('game details', details);
// strShortcutStartDir // strShortcutStartDir
const file = await window.DeckyPluginLoader.openFilePicker(details.strShortcutStartDir.replaceAll('"', '')); const file = await window.DeckyPluginLoader.openFilePicker(details.strShortcutStartDir.replaceAll('"', ''));
console.log('user selected', file); logger.debug('user selected', file);
window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path)); window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
const pathArr = file.path.split('/'); const pathArr = file.path.split('/');
pathArr.pop(); pathArr.pop();
const folder = pathArr.join('/'); const folder = pathArr.join('/');
window.SteamClient.Apps.SetShortcutStartDir(appid, JSON.stringify(folder)); window.SteamClient.Apps.SetShortcutStartDir(appid, JSON.stringify(folder));
} catch (e) { } catch (e) {
console.error(e); logger.error(e);
} }
}); });
} }
// TODO type and add to frontend-lib
const History = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.m_history) return m[prop].m_history;
}
});
export default async function libraryPatch() { export default async function libraryPatch() {
try { try {
rePatch(); rePatch();
// TODO type and add to frontend-lib
let History: any;
while (!History) {
History = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.m_history) return m[prop].m_history;
}
});
if (!History) {
logger.debug('Waiting 5s for history to become available.');
await sleep(5000);
}
}
const unlisten = History.listen(() => { const unlisten = History.listen(() => {
if (window.SteamClient.Apps.PromptToChangeShortcut !== patch.patchedFunction) { if (window.SteamClient.Apps.PromptToChangeShortcut !== patch.patchedFunction) {
rePatch(); rePatch();
@@ -47,11 +59,11 @@ export default async function libraryPatch() {
}); });
return () => { return () => {
patch.unpatch();
unlisten(); unlisten();
patch.unpatch();
}; };
} catch (e) { } catch (e) {
console.error('Error patching library file picker', e); logger.error('Error patching library file picker', e);
} }
return () => {}; return () => {};
} }
+11 -7
View File
@@ -1,7 +1,9 @@
import { SidebarNavigation } from 'decky-frontend-lib'; import { SidebarNavigation } from 'decky-frontend-lib';
import { lazy } from 'react'; import { lazy } from 'react';
import { FaCode, FaPlug } from 'react-icons/fa';
import { useSetting } from '../../utils/hooks/useSetting'; import { useSetting } from '../../utils/hooks/useSetting';
import DeckyIcon from '../DeckyIcon';
import WithSuspense from '../WithSuspense'; import WithSuspense from '../WithSuspense';
import GeneralSettings from './pages/general'; import GeneralSettings from './pages/general';
import PluginList from './pages/plugin_list'; import PluginList from './pages/plugin_list';
@@ -13,19 +15,18 @@ export default function SettingsPage() {
const pages = [ const pages = [
{ {
title: 'General', title: 'Decky',
content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />, content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
route: '/decky/settings/general', route: '/decky/settings/general',
icon: <DeckyIcon />,
}, },
{ {
title: 'Plugins', title: 'Plugins',
content: <PluginList />, content: <PluginList />,
route: '/decky/settings/plugins', route: '/decky/settings/plugins',
icon: <FaPlug />,
}, },
]; {
if (isDeveloper)
pages.push({
title: 'Developer', title: 'Developer',
content: ( content: (
<WithSuspense> <WithSuspense>
@@ -33,7 +34,10 @@ export default function SettingsPage() {
</WithSuspense> </WithSuspense>
), ),
route: '/decky/settings/developer', 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 { useRef } from 'react';
import { FaReact, FaSteamSymbol } from 'react-icons/fa'; import { FaReact, FaSteamSymbol } from 'react-icons/fa';
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer'; import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
import { useSetting } from '../../../../utils/hooks/useSetting'; import { useSetting } from '../../../../utils/hooks/useSetting';
import RemoteDebuggingSettings from '../general/RemoteDebugging';
export default function DeveloperSettings() { export default function DeveloperSettings() {
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false); const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
@@ -12,7 +13,8 @@ export default function DeveloperSettings() {
const textRef = useRef<HTMLDivElement>(null); const textRef = useRef<HTMLDivElement>(null);
return ( return (
<> <DialogBody>
<RemoteDebuggingSettings />
<Field <Field
label="Enable Valve Internal" label="Enable Valve Internal"
description={ description={
@@ -30,55 +32,33 @@ export default function DeveloperSettings() {
setShowValveInternal(toggleValue); setShowValveInternal(toggleValue);
}} }}
/> />
</Field>{' '} </Field>
<Focusable <Field
onTouchEnd={ label="Enable React DevTools"
reactDevtoolsIP == '' description={
? () => { <>
(textRef.current?.childNodes[0] as HTMLInputElement)?.focus(); <span style={{ whiteSpace: 'pre-line' }}>
} Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the
: undefined IP address before enabling.
} </span>
onClick={ <br />
reactDevtoolsIP == '' <br />
? () => { <div ref={textRef}>
(textRef.current?.childNodes[0] as HTMLInputElement)?.focus(); <TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
} </div>
: undefined </>
}
onOKButton={
reactDevtoolsIP == ''
? () => {
(textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
}
: undefined
} }
icon={<FaReact style={{ display: 'block' }} />}
> >
<Field <Toggle
label="Enable React DevTools" value={reactDevtoolsEnabled}
description={ disabled={reactDevtoolsIP == ''}
<> onChange={(toggleValue) => {
<span style={{ whiteSpace: 'pre-line' }}> setReactDevtoolsEnabled(toggleValue);
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set setShouldConnectToReactDevTools(toggleValue);
the IP address before enabling. }}
</span> />
<div ref={textRef}> </Field>
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} /> </DialogBody>
</div>
</>
}
icon={<FaReact style={{ display: 'block' }} />}
>
<Toggle
value={reactDevtoolsEnabled}
disabled={reactDevtoolsIP == ''}
onChange={(toggleValue) => {
setReactDevtoolsEnabled(toggleValue);
setShouldConnectToReactDevTools(toggleValue);
}}
/>
</Field>
</Focusable>
</>
); );
} }
@@ -19,7 +19,7 @@ const BranchSelect: FunctionComponent<{}> = () => {
return ( return (
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22) // 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 // 0 being stable, 1 being pre-release and 2 being nightly
<Field label="Update Channel"> <Field label="Decky Update Channel" childrenContainerWidth={'fixed'}>
<Dropdown <Dropdown
rgOptions={Object.values(UpdateBranch) rgOptions={Object.values(UpdateBranch)
.filter((branch) => typeof branch == 'string') .filter((branch) => typeof branch == 'string')
@@ -1,5 +1,5 @@
import { Field, Toggle } from 'decky-frontend-lib'; 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'; import { useSetting } from '../../../../utils/hooks/useSetting';
@@ -11,10 +11,10 @@ export default function RemoteDebuggingSettings() {
label="Allow Remote CEF Debugging" label="Allow Remote CEF Debugging"
description={ description={
<span style={{ whiteSpace: 'pre-line' }}> <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> </span>
} }
icon={<FaBug style={{ display: 'block' }} />} icon={<FaChrome style={{ display: 'block' }} />}
> >
<Toggle <Toggle
value={allowRemoteDebugging || false} value={allowRemoteDebugging || false}
@@ -16,7 +16,7 @@ const StoreSelect: FunctionComponent<{}> = () => {
// 0 being Default, 1 being Testing and 2 being Custom // 0 being Default, 1 being Testing and 2 being Custom
return ( return (
<> <>
<Field label="Store Channel"> <Field label="Plugin Store Channel" childrenContainerWidth={'fixed'}>
<Dropdown <Dropdown
rgOptions={Object.values(Store) rgOptions={Object.values(Store)
.filter((store) => typeof store == 'string') .filter((store) => typeof store == 'string')
@@ -11,7 +11,7 @@ import {
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Suspense, lazy } from 'react'; import { Suspense, lazy } from 'react';
import { useEffect, useState } 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 { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
import { findSP } from '../../../../utils/windows'; import { findSP } from '../../../../utils/windows';
@@ -95,21 +95,21 @@ export default function UpdaterSettings() {
<Field <Field
onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined} onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined}
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined} onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
label="Updates" label="Decky Updates"
description={ description={
versionInfo && ( checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? (
<span style={{ whiteSpace: 'pre-line' }}>{`Current version: ${versionInfo.current}\n${ ''
versionInfo.updatable ? `Latest version: ${versionInfo.remote?.tag_name}` : '' ) : (
}`}</span> <span>Up to date: running {versionInfo?.current}</span>
) )
} }
icon={ icon={
!versionInfo ? ( versionInfo?.remote &&
<Spinner style={{ width: '1em', height: 20, display: 'block' }} /> versionInfo?.remote?.tag_name != versionInfo?.current && (
) : ( <FaExclamation color="var(--gpColor-Yellow)" style={{ display: 'block' }} />
<FaArrowDown style={{ display: 'block' }} />
) )
} }
childrenContainerWidth={'fixed'}
> >
{updateProgress == -1 && !isLoaderUpdating ? ( {updateProgress == -1 && !isLoaderUpdating ? (
<DialogButton <DialogButton
@@ -144,7 +144,7 @@ export default function UpdaterSettings() {
/> />
)} )}
</Field> </Field>
{versionInfo?.remote && ( {versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current && (
<InlinePatchNotes <InlinePatchNotes
title={versionInfo?.remote.name} title={versionInfo?.remote.name}
date={new Intl.RelativeTimeFormat('en-US', { 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 { useState } from 'react';
import { FaShapes, FaTools } from 'react-icons/fa';
import { installFromURL } from '../../../../store'; import { installFromURL } from '../../../../store';
import { useDeckyState } from '../../../DeckyState';
import BranchSelect from './BranchSelect'; import BranchSelect from './BranchSelect';
import RemoteDebuggingSettings from './RemoteDebugging';
import StoreSelect from './StoreSelect'; import StoreSelect from './StoreSelect';
import UpdaterSettings from './Updater'; import UpdaterSettings from './Updater';
@@ -16,34 +23,44 @@ export default function GeneralSettings({
setIsDeveloper: (val: boolean) => void; setIsDeveloper: (val: boolean) => void;
}) { }) {
const [pluginURL, setPluginURL] = useState(''); const [pluginURL, setPluginURL] = useState('');
const { versionInfo } = useDeckyState();
return ( return (
<div> <DialogBody>
<UpdaterSettings /> <DialogControlsSection>
<BranchSelect /> <DialogControlsSectionHeader>Updates</DialogControlsSectionHeader>
<StoreSelect /> <UpdaterSettings />
<RemoteDebuggingSettings /> </DialogControlsSection>
<Field <DialogControlsSection>
label="Developer mode" <DialogControlsSectionHeader>Beta Participation</DialogControlsSectionHeader>
description={<span style={{ whiteSpace: 'pre-line' }}>Enables Decky's developer settings.</span>} <BranchSelect />
icon={<FaTools style={{ display: 'block' }} />} <StoreSelect />
> </DialogControlsSection>
<Toggle <DialogControlsSection>
value={isDeveloper} <DialogControlsSectionHeader>Other</DialogControlsSectionHeader>
onChange={(toggleValue) => { <Field label="Enable Developer Mode">
setIsDeveloper(toggleValue); <Toggle
}} value={isDeveloper}
/> onChange={(toggleValue) => {
</Field> setIsDeveloper(toggleValue);
<Field }}
label="Manual plugin install" />
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />} </Field>
icon={<FaShapes style={{ display: 'block' }} />} <Field
> label="Install plugin from URL"
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}> description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
Install >
</DialogButton> <DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
</Field> Install
</div> </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,4 +1,12 @@
import { DialogButton, Focusable, Menu, MenuItem, showContextMenu } from 'decky-frontend-lib'; import {
DialogBody,
DialogButton,
DialogControlsSection,
Focusable,
Menu,
MenuItem,
showContextMenu,
} from 'decky-frontend-lib';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { FaDownload, FaEllipsisH } from 'react-icons/fa'; import { FaDownload, FaEllipsisH } from 'react-icons/fa';
@@ -21,46 +29,52 @@ export default function PluginList() {
} }
return ( return (
<ul style={{ listStyleType: 'none' }}> <DialogBody>
{plugins.map(({ name, version }) => { <DialogControlsSection>
const update = updates?.get(name); <ul style={{ listStyleType: 'none', padding: '0' }}>
return ( {plugins.map(({ name, version }) => {
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}> const update = updates?.get(name);
<span> return (
{name} {version} <li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
</span> <span>
<Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}> {name} <span style={{ opacity: '50%' }}>{'(' + version + ')'}</span>
{update && ( </span>
<DialogButton <Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} {update && (
onClick={() => requestPluginInstall(name, update)} <DialogButton
> style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
<div style={{ display: 'flex', flexDirection: 'row' }}> onClick={() => requestPluginInstall(name, update)}
Update to {update.name} >
<FaDownload style={{ paddingLeft: '2rem' }} /> <div style={{ display: 'flex', flexDirection: 'row' }}>
</div> Update to {update.name}
</DialogButton> <FaDownload style={{ paddingLeft: '2rem' }} />
)} </div>
<DialogButton </DialogButton>
style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }} )}
onClick={(e: MouseEvent) => <DialogButton
showContextMenu( style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }}
<Menu label="Plugin Actions"> onClick={(e: MouseEvent) =>
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name, version)}> showContextMenu(
Reload <Menu label="Plugin Actions">
</MenuItem> <MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name, version)}>
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem> Reload
</Menu>, </MenuItem>
e.currentTarget ?? window, <MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>
) Uninstall
} </MenuItem>
> </Menu>,
<FaEllipsisH /> e.currentTarget ?? window,
</DialogButton> )
</Focusable> }
</li> >
); <FaEllipsisH />
})} </DialogButton>
</ul> </Focusable>
</li>
);
})}
</ul>
</DialogControlsSection>
</DialogBody>
); );
} }
+143 -152
View File
@@ -1,15 +1,12 @@
import { import {
DialogButton, ButtonItem,
Dropdown, Dropdown,
Focusable, Focusable,
QuickAccessTab, PanelSectionRow,
Router,
SingleDropdownOption, SingleDropdownOption,
SuspensefulImage, SuspensefulImage,
joinClassNames,
staticClasses,
} from 'decky-frontend-lib'; } from 'decky-frontend-lib';
import { FC, useRef, useState } from 'react'; import { FC, useState } from 'react';
import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store'; import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store';
@@ -19,168 +16,162 @@ interface PluginCardProps {
const PluginCard: FC<PluginCardProps> = ({ plugin }) => { const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
const [selectedOption, setSelectedOption] = useState<number>(0); const [selectedOption, setSelectedOption] = useState<number>(0);
const buttonRef = useRef<HTMLDivElement>(null); const root: boolean = plugin.tags.some((tag) => tag === 'root');
const containerRef = useRef<HTMLDivElement>(null);
return ( return (
<div <div
className="deckyStoreCard"
style={{ style={{
padding: '30px', marginLeft: '20px',
paddingTop: '10px', marginRight: '20px',
paddingBottom: '10px', marginBottom: '20px',
display: 'flex',
alignItems: 'center',
}} }}
> >
{/* TODO: abstract this messy focus hackiness into a custom component in lib */} <div
<Focusable className="deckyStoreCardImageContainer"
className="deckyStoreCard"
ref={containerRef}
onActivate={(_: CustomEvent) => {
buttonRef.current!.focus();
}}
onCancel={(_: CustomEvent) => {
if (containerRef.current!.querySelectorAll('* :focus').length === 0) {
Router.NavigateBackOrOpenMenu();
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
} else {
containerRef.current!.focus();
}
}}
style={{ style={{
display: 'flex', width: '320px',
flexDirection: 'column', height: '200px',
background: '#ACB2C924', position: 'relative',
height: 'unset',
marginBottom: 'unset',
// boxShadow: var(--gpShadow-Medium);
scrollSnapAlign: 'start',
boxSizing: 'border-box',
}} }}
> >
<div className="deckyStoreCardHeader" style={{ display: 'flex', alignItems: 'center' }}> <SuspensefulImage
<div className="deckyStoreCardImage"
style={{ fontSize: '18pt', padding: '10px' }} suspenseHeight="200px"
className={joinClassNames(staticClasses.Text)} suspenseWidth="320px"
// onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)}
>
{plugin.name}
</div>
</div>
<div
style={{ style={{
display: 'flex', width: '320px',
flexDirection: 'row', height: '200px',
objectFit: 'cover',
}} }}
className="deckyStoreCardBody" src={plugin.image_url}
> />
<SuspensefulImage </div>
className="deckyStoreCardImage" <div
suspenseWidth="256px" className="deckyStoreCardInfo"
style={{ style={{
width: 'auto', width: 'calc(100% - 320px)', // The calc is here so that the info section doesn't expand into the image
height: '160px', display: 'flex',
}} flexDirection: 'column',
src={plugin.image_url} height: '100%',
/> marginLeft: '1em',
<div justifyContent: 'center',
style={{ }}
display: 'flex', >
flexDirection: 'column', <span
}} className="deckyStoreCardTitle"
className="deckyStoreCardInfo"
>
<p
className={joinClassNames(staticClasses.PanelSectionRow)}
style={{ marginTop: '0px', marginLeft: '16px' }}
>
<span style={{ paddingLeft: '0px' }}>Author: {plugin.author}</span>
</p>
<p
className={joinClassNames(staticClasses.PanelSectionRow)}
style={{
marginLeft: '16px',
marginTop: '0px',
marginBottom: '0px',
marginRight: '16px',
}}
>
<span style={{ paddingLeft: '0px' }}>{plugin.description}</span>
</p>
<p
className={joinClassNames('deckyStoreCardTagsContainer', staticClasses.PanelSectionRow)}
style={{
padding: '0 16px',
display: 'flex',
flexWrap: 'wrap',
gap: '5px 10px',
}}
>
<span style={{ padding: '5px 0' }}>Tags:</span>
{plugin.tags.map((tag: string) => (
<span
className="deckyStoreCardTag"
style={{
padding: '5px',
borderRadius: '5px',
background: tag == 'root' ? '#842029' : '#ACB2C947',
}}
>
{tag == 'root' ? 'Requires root' : tag}
</span>
))}
</p>
</div>
</div>
<div
className="deckyStoreCardActionsContainer"
style={{ style={{
fontSize: '1.25em',
fontWeight: 'bold',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '90%',
}}
>
{plugin.name}
</span>
<span
className="deckyStoreCardAuthor"
style={{
marginRight: 'auto',
fontSize: '1em',
}}
>
{plugin.author}
</span>
<span
className="deckyStoreCardDescription"
style={{
fontSize: '13px',
color: '#969696',
WebkitLineClamp: root ? '2' : '3',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
display: '-webkit-box',
}}
>
{plugin.description ? (
plugin.description
) : (
<span>
<i style={{ color: '#666' }}>No description provided.</i>
</span>
)}
</span>
{root && (
<span
className="deckyStoreCardDescription deckyStoreCardDescriptionRoot"
style={{
fontSize: '13px',
color: '#fee75c',
}}
>
<i>This plugin has full access to your Steam Deck.</i>{' '}
<a
className="deckyStoreCardDescriptionRootLink"
href="https://deckbrew.xyz/root"
target="_blank"
style={{
color: '#fee75c',
textDecoration: 'none',
}}
>
deckbrew.xyz/root
</a>
</span>
)}
<div
className="deckyStoreCardButtonRow"
style={{
marginTop: '1em',
width: '100%', width: '100%',
alignSelf: 'flex-end', overflow: 'hidden',
display: 'flex',
flexDirection: 'row',
}} }}
> >
<Focusable <PanelSectionRow>
className="deckyStoreCardActions" <Focusable style={{ display: 'flex', maxWidth: '100%' }}>
style={{ <div
display: 'flex', className="deckyStoreCardInstallContainer"
flexDirection: 'row', style={{
width: '100%', paddingTop: '0px',
}} paddingBottom: '0px',
> width: '40%',
<div }}
className="deckyStoreCardInstallButtonContainer"
style={{
flex: '1',
}}
>
<DialogButton
className="deckyStoreCardInstallButton"
ref={buttonRef}
onClick={() => requestPluginInstall(plugin.name, plugin.versions[selectedOption])}
> >
Install <ButtonItem
</DialogButton> bottomSeparator="none"
</div> layout="below"
<div onClick={() => requestPluginInstall(plugin.name, plugin.versions[selectedOption])}
className="deckyStoreCardVersionDropdownContainer" >
style={{ <span className="deckyStoreCardInstallText">Install</span>
flex: '0.2', </ButtonItem>
}} </div>
> <div
<Dropdown className="deckyStoreCardVersionContainer"
rgOptions={ style={{
plugin.versions.map((version: StorePluginVersion, index) => ({ marginLeft: '5%',
data: index, width: '30%',
label: version.name, }}
})) as SingleDropdownOption[] >
} <Dropdown
strDefaultLabel={'Select a version'} rgOptions={
selectedOption={selectedOption} plugin.versions.map((version: StorePluginVersion, index) => ({
onChange={({ data }) => setSelectedOption(data)} data: index,
/> label: version.name,
</div> })) as SingleDropdownOption[]
</Focusable> }
menuLabel="Plugin Version"
selectedOption={selectedOption}
onChange={({ data }) => setSelectedOption(data)}
/>
</div>
</Focusable>
</PanelSectionRow>
</div> </div>
</Focusable> </div>
</div> </div>
); );
}; };
+206 -18
View File
@@ -1,6 +1,16 @@
import { SteamSpinner } from 'decky-frontend-lib'; import {
import { FC, useEffect, useState } from 'react'; Dropdown,
DropdownOption,
Focusable,
PanelSectionRow,
SteamSpinner,
Tabs,
TextField,
findModule,
} from 'decky-frontend-lib';
import { FC, useEffect, useMemo, useState } from 'react';
import logo from '../../../assets/plugin_store.png';
import Logger from '../../logger'; import Logger from '../../logger';
import { StorePlugin, getPluginList } from '../../store'; import { StorePlugin, getPluginList } from '../../store';
import PluginCard from './PluginCard'; import PluginCard from './PluginCard';
@@ -8,7 +18,12 @@ import PluginCard from './PluginCard';
const logger = new Logger('FilePicker'); const logger = new Logger('FilePicker');
const StorePage: FC<{}> = () => { const StorePage: FC<{}> = () => {
const [currentTabRoute, setCurrentTabRoute] = useState<string>('browse');
const [data, setData] = useState<StorePlugin[] | null>(null); const [data, setData] = useState<StorePlugin[] | null>(null);
const { TabCount } = findModule((m) => {
if (m?.TabCount && m?.TabTitle) return true;
return false;
});
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -19,19 +34,12 @@ const StorePage: FC<{}> = () => {
}, []); }, []);
return ( return (
<div <>
style={{
marginTop: '40px',
height: 'calc( 100% - 40px )',
overflowY: 'scroll',
}}
>
<div <div
style={{ style={{
display: 'flex', marginTop: '40px',
flexWrap: 'nowrap', height: 'calc( 100% - 40px )',
flexDirection: 'column', background: '#0005',
height: '100%',
}} }}
> >
{!data ? ( {!data ? (
@@ -39,13 +47,193 @@ const StorePage: FC<{}> = () => {
<SteamSpinner /> <SteamSpinner />
</div> </div>
) : ( ) : (
<div> <Tabs
{data.map((plugin: StorePlugin) => ( activeTab={currentTabRoute}
<PluginCard plugin={plugin} /> onShowTab={(tabId: string) => {
))} setCurrentTabRoute(tabId);
</div> }}
tabs={[
{
title: 'Browse',
content: <BrowseTab children={{ data: data }} />,
id: 'browse',
renderTabAddon: () => <span className={TabCount}>{data.length}</span>,
},
{
title: 'About',
content: <AboutTab />,
id: 'about',
},
]}
/>
)} )}
</div> </div>
</>
);
};
const BrowseTab: FC<{ children: { data: StorePlugin[] } }> = (data) => {
const sortOptions = useMemo(
(): DropdownOption[] => [
{ data: 1, label: 'Alphabetical (A to Z)' },
{ data: 2, label: 'Alphabetical (Z to A)' },
],
[],
);
// const filterOptions = useMemo((): DropdownOption[] => [{ data: 1, label: 'All' }], []);
const [selectedSort, setSort] = useState<number>(sortOptions[0].data);
// const [selectedFilter, setFilter] = useState<number>(filterOptions[0].data);
const [searchFieldValue, setSearchValue] = useState<string>('');
return (
<>
<style>{`
.deckyStoreCardInstallContainer > .Panel {
padding: 0;
}
`}</style>
{/* This should be used once filtering is added
<PanelSectionRow>
<Focusable style={{ display: 'flex', maxWidth: '100%' }}>
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '47.5%',
}}
>
<span className="DialogLabel">Sort</span>
<Dropdown
menuLabel="Sort"
rgOptions={sortOptions}
strDefaultLabel="Last Updated (Newest)"
selectedOption={selectedSort}
onChange={(e) => setSort(e.data)}
/>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '47.5%',
marginLeft: 'auto',
}}
>
<span className="DialogLabel">Filter</span>
<Dropdown
menuLabel="Filter"
rgOptions={filterOptions}
strDefaultLabel="All"
selectedOption={selectedFilter}
onChange={(e) => setFilter(e.data)}
/>
</div>
</Focusable>
</PanelSectionRow>
<div style={{ justifyContent: 'center', display: 'flex' }}>
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
<div style={{ width: '100%' }}>
<TextField label="Search" value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
</div>
</Focusable>
</div>
*/}
<PanelSectionRow>
<Focusable style={{ display: 'flex', maxWidth: '100%' }}>
<div
style={{
display: 'flex',
flexDirection: 'column',
minWidth: '100%',
maxWidth: '100%',
}}
>
<span className="DialogLabel">Sort</span>
<Dropdown
menuLabel="Sort"
rgOptions={sortOptions}
strDefaultLabel="Last Updated (Newest)"
selectedOption={selectedSort}
onChange={(e) => setSort(e.data)}
/>
</div>
</Focusable>
</PanelSectionRow>
<div style={{ justifyContent: 'center', display: 'flex' }}>
<Focusable style={{ display: 'flex', alignItems: 'center', width: '96%' }}>
<div style={{ width: '100%' }}>
<TextField label="Search" value={searchFieldValue} onChange={(e) => setSearchValue(e.target.value)} />
</div>
</Focusable>
</div>
<div>
{data.children.data
.filter((plugin: StorePlugin) => {
return (
plugin.name.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
plugin.description.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
plugin.author.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
plugin.tags.some((tag: string) => tag.toLowerCase().includes(searchFieldValue.toLowerCase()))
);
})
.sort((a, b) => {
if (selectedSort % 2 === 1) return a.name.localeCompare(b.name);
else return b.name.localeCompare(a.name);
})
.map((plugin: StorePlugin) => (
<PluginCard plugin={plugin} />
))}
</div>
</>
);
};
const AboutTab: FC<{}> = () => {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
}}
>
<style>{`
.deckyStoreAboutHeader {
font-size: 24px;
font-weight: 600;
margin-top: 20px;
}
`}</style>
<img
src={logo}
style={{
width: '256px',
height: 'auto',
alignSelf: 'center',
}}
/>
<span className="deckyStoreAboutHeader">Testing</span>
<span>
Please consider testing new plugins to help the Decky Loader team!{' '}
<a
href="https://deckbrew.xyz/testing"
target="_blank"
style={{
textDecoration: 'none',
}}
>
deckbrew.xyz/testing
</a>
</span>
<span className="deckyStoreAboutHeader">Contributing</span>
<span>
If you would like to contribute to the Decky Plugin Store, check the SteamDeckHomebrew/decky-plugin-template
repository on GitHub. Information on development and distribution is available in the README.
</span>
<span className="deckyStoreAboutHeader">Source Code</span>
<span>All plugin source code is available on SteamDeckHomebrew/decky-plugin-database repository on GitHub.</span>
</div> </div>
); );
}; };
+17 -8
View File
@@ -1,4 +1,5 @@
import { import {
Navigation,
ReactRouter, ReactRouter,
Router, Router,
fakeRenderComponent, fakeRenderComponent,
@@ -26,13 +27,20 @@ const logger = new Logger('DeveloperMode');
let removeSettingsObserver: () => void = () => {}; let removeSettingsObserver: () => void = () => {};
export function setShowValveInternal(show: boolean) { export async function setShowValveInternal(show: boolean) {
const settingsMod = findModuleChild((m) => { let settingsMod: any;
if (typeof m !== 'object') return undefined; while (!settingsMod) {
for (let prop in m) { settingsMod = findModuleChild((m) => {
if (typeof m[prop]?.settings?.bIsValveEmail !== 'undefined') return m[prop]; if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (typeof m[prop]?.settings?.bIsValveEmail !== 'undefined') return m[prop];
}
});
if (!settingsMod) {
logger.debug('[ValveInternal] waiting for settingsMod');
await sleep(1000);
} }
}); }
if (show) { if (show) {
removeSettingsObserver = settingsMod[ removeSettingsObserver = settingsMod[
@@ -74,13 +82,14 @@ export async function startup() {
window.DFL = { window.DFL = {
findModuleChild, findModuleChild,
findModule, findModule,
Navigation,
Router,
ReactRouter,
ReactUtils: { ReactUtils: {
fakeRenderComponent, fakeRenderComponent,
findInReactTree, findInReactTree,
findInTree, findInTree,
}, },
Router,
ReactRouter,
classes: { classes: {
scrollClasses, scrollClasses,
staticClasses, staticClasses,
+19
View File
@@ -1,3 +1,5 @@
import { Navigation, Router, sleep } from 'decky-frontend-lib';
import PluginLoader from './plugin-loader'; import PluginLoader from './plugin-loader';
import { DeckyUpdater } from './updater'; import { DeckyUpdater } from './updater';
@@ -14,6 +16,23 @@ declare global {
} }
} }
(async () => {
try {
if (!Router.NavigateToAppProperties || !Router.NavigateToLibraryTab || !Router.NavigateToInvites) {
while (!Navigation.NavigateToAppProperties) await sleep(100);
const shims = {
NavigateToAppProperties: Navigation.NavigateToAppProperties,
NavigateToInvites: Navigation.NavigateToInvites,
NavigateToLibraryTab: Navigation.NavigateToLibraryTab,
};
(Router as unknown as any).deckyShim = true;
Object.assign(Router, shims);
}
} catch (e) {
console.error('[DECKY]: Error initializing Navigation interface shims', e);
}
})();
(async () => { (async () => {
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text()); window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
+10 -17
View File
@@ -1,13 +1,4 @@
import { import { ConfirmModal, ModalRoot, Patch, QuickAccessTab, Router, showModal, sleep } from 'decky-frontend-lib';
ConfirmModal,
ModalRoot,
Patch,
QuickAccessTab,
Router,
showModal,
sleep,
staticClasses,
} from 'decky-frontend-lib';
import { FC, lazy } from 'react'; import { FC, lazy } from 'react';
import { FaCog, FaExclamationCircle, FaPlug } from 'react-icons/fa'; import { FaCog, FaExclamationCircle, FaPlug } from 'react-icons/fa';
@@ -21,6 +12,7 @@ import WithSuspense from './components/WithSuspense';
import Logger from './logger'; import Logger from './logger';
import { Plugin } from './plugin'; import { Plugin } from './plugin';
import RouterHook from './router-hook'; import RouterHook from './router-hook';
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
import { checkForUpdates } from './store'; import { checkForUpdates } from './store';
import TabsHook from './tabs-hook'; import TabsHook from './tabs-hook';
import OldTabsHook from './tabs-hook.old'; import OldTabsHook from './tabs-hook.old';
@@ -33,10 +25,6 @@ const SettingsPage = lazy(() => import('./components/settings'));
const FilePicker = lazy(() => import('./components/modals/filepicker')); const FilePicker = lazy(() => import('./components/modals/filepicker'));
declare global {
interface Window {}
}
class PluginLoader extends Logger { class PluginLoader extends Logger {
private plugins: Plugin[] = []; private plugins: Plugin[] = [];
private tabsHook: TabsHook | OldTabsHook = document.title == 'SP' ? new OldTabsHook() : new TabsHook(); private tabsHook: TabsHook | OldTabsHook = document.title == 'SP' ? new OldTabsHook() : new TabsHook();
@@ -92,6 +80,8 @@ class PluginLoader extends Logger {
); );
}); });
initSteamFixes();
initFilepickerPatches(); initFilepickerPatches();
this.updateVersion(); this.updateVersion();
@@ -156,10 +146,10 @@ class PluginLoader extends Logger {
onCancel={() => { onCancel={() => {
// do nothing // do nothing
}} }}
strTitle={`Uninstall ${name}`}
strOKButtonText={'Uninstall'}
> >
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}> Are you sure you want to uninstall {name}?
Uninstall {name}?
</div>
</ConfirmModal>, </ConfirmModal>,
); );
} }
@@ -184,11 +174,13 @@ class PluginLoader extends Logger {
public deinit() { public deinit() {
this.routerHook.removeRoute('/decky/store'); this.routerHook.removeRoute('/decky/store');
this.routerHook.removeRoute('/decky/settings'); this.routerHook.removeRoute('/decky/settings');
deinitSteamFixes();
deinitFilepickerPatches(); deinitFilepickerPatches();
this.focusWorkaroundPatch?.unpatch(); this.focusWorkaroundPatch?.unpatch();
} }
public unloadPlugin(name: string) { public unloadPlugin(name: string) {
console.log('Plugin List: ', this.plugins);
const plugin = this.plugins.find((plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', '')); const plugin = this.plugins.find((plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''));
plugin?.onDismount?.(); plugin?.onDismount?.();
this.plugins = this.plugins.filter((p) => p !== plugin); this.plugins = this.plugins.filter((p) => p !== plugin);
@@ -344,6 +336,7 @@ class PluginLoader extends Logger {
fetchNoCors(url: string, request: any = {}) { fetchNoCors(url: string, request: any = {}) {
let args = { method: 'POST', headers: {} }; let args = { method: 'POST', headers: {} };
const req = { ...args, ...request, url, data: request.body }; const req = { ...args, ...request, url, data: request.body };
req?.body && delete req.body;
return this.callServerMethod('http_request', req); return this.callServerMethod('http_request', req);
}, },
executeInTab(tab: string, runAsync: boolean, code: string) { executeInTab(tab: string, runAsync: boolean, code: string) {
+13
View File
@@ -0,0 +1,13 @@
## What's this?
`steamfixes` contains various fixes and workaround for things Valve has broken that cause Decky issues.
## Current fixes:
- StartRestart() -> StartShutdown(false) override:
StartRestart() breaks CEF debugging, StartShutdown(false) doesn't. We can safely replace StartRestart() with StartShutdown(false) as gamescope-session will automatically restart the steam client anyway if it shuts down, bypassing the broken restart codepath. Added 12/29/2022
- ExecuteSteamURL UI reload fix:
Starting sometime in November 2022, Valve broke reloading the Steam UI pages via location.reload, as it won't properly start the UI. We can manually trigger UI startup if we detect no active input contexts by calling `SteamClient.URL.ExecuteSteamURL("steam://open/settings/")` Added 12/29/2022
+12
View File
@@ -0,0 +1,12 @@
import reloadFix from './reload';
import restartFix from './restart';
let fixes: Function[] = [];
export function deinitSteamFixes() {
fixes.forEach((deinit) => deinit());
}
export async function initSteamFixes() {
fixes.push(reloadFix());
fixes.push(await restartFix());
}
+14
View File
@@ -0,0 +1,14 @@
import Logger from '../logger';
const logger = new Logger('ReloadSteamFix');
export default function reloadFix() {
// Hack to unbreak the ui when reloading it
if (window.FocusNavController?.m_rgAllContexts?.length == 0) {
SteamClient.URL.ExecuteSteamURL('steam://open/settings');
logger.log('Applied UI reload fix.');
}
// This steamfix does not need to deinit.
return () => {};
}
+60
View File
@@ -0,0 +1,60 @@
import { Patch, findModuleChild, replacePatch, sleep } from 'decky-frontend-lib';
import Logger from '../logger';
const logger = new Logger('RestartSteamFix');
declare global {
interface Window {
SteamClient: any;
appDetailsStore: any;
}
}
let patch: Patch;
function rePatch() {
// If you patch anything on SteamClient within the first few seconds of the client having loaded it will get redefined for some reason, so repatch any of these changes that occur with History.listen or an interval
patch = replacePatch(window.SteamClient.User, 'StartRestart', () => SteamClient.User.StartShutdown(false));
}
export default async function restartFix() {
try {
rePatch();
// TODO type and add to frontend-lib
let History: any;
while (!History) {
History = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (m[prop]?.m_history) return m[prop].m_history;
}
});
if (!History) {
logger.debug('Waiting 5s for history to become available.');
await sleep(5000);
}
}
function repatchIfNeeded() {
if (window.SteamClient.User.StartRestart !== patch.patchedFunction) {
rePatch();
}
}
const unlisten = History.listen(repatchIfNeeded);
// Just in case
setTimeout(repatchIfNeeded, 5000);
setTimeout(repatchIfNeeded, 10000);
return () => {
unlisten();
patch.unpatch();
};
} catch (e) {
logger.error('Error patching StartRestart', e);
}
return () => {};
}
+4 -1
View File
@@ -10,6 +10,7 @@ export enum Store {
export interface StorePluginVersion { export interface StorePluginVersion {
name: string; name: string;
hash: string; hash: string;
artifact: string | undefined | null;
} }
export interface StorePlugin { export interface StorePlugin {
@@ -73,9 +74,11 @@ export async function installFromURL(url: string) {
} }
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) { export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) {
const artifactUrl =
selectedVer.artifact ?? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`;
await window.DeckyPluginLoader.callServerMethod('install_plugin', { await window.DeckyPluginLoader.callServerMethod('install_plugin', {
name: plugin, name: plugin,
artifact: `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`, artifact: artifactUrl,
version: selectedVer.name, version: selectedVer.name,
hash: selectedVer.hash, hash: selectedVer.hash,
}); });
-3
View File
@@ -7,7 +7,6 @@ import Logger from './logger';
declare global { declare global {
interface Window { interface Window {
__TABS_HOOK_INSTANCE: any; __TABS_HOOK_INSTANCE: any;
securitystore: any;
} }
} }
@@ -23,7 +22,6 @@ class TabsHook extends Logger {
tabs: Tab[] = []; tabs: Tab[] = [];
private qAMRoot?: any; private qAMRoot?: any;
private qamPatch?: Patch; private qamPatch?: Patch;
private unsubscribeSecurity?: () => void;
constructor() { constructor() {
super('TabsHook'); super('TabsHook');
@@ -114,7 +112,6 @@ class TabsHook extends Logger {
deinit() { deinit() {
this.qamPatch?.unpatch(); this.qamPatch?.unpatch();
this.qAMRoot.return.alternate.type = this.qAMRoot.return.type; this.qAMRoot.return.alternate.type = this.qAMRoot.return.type;
this.unsubscribeSecurity?.();
} }
add(tab: Tab) { add(tab: Tab) {
+33 -11
View File
@@ -1,4 +1,4 @@
import { Patch, ToastData, afterPatch, findInReactTree, sleep } from 'decky-frontend-lib'; import { Module, Patch, ToastData, afterPatch, findInReactTree, findModuleChild, sleep } from 'decky-frontend-lib';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import Toast from './components/Toast'; import Toast from './components/Toast';
@@ -7,6 +7,7 @@ import Logger from './logger';
declare global { declare global {
interface Window { interface Window {
__TOASTER_INSTANCE: any; __TOASTER_INSTANCE: any;
settingsStore: any;
NotificationStore: any; NotificationStore: any;
} }
} }
@@ -16,7 +17,7 @@ class Toaster extends Logger {
// private toasterState: DeckyToasterState = new DeckyToasterState(); // private toasterState: DeckyToasterState = new DeckyToasterState();
private node: any; private node: any;
private rNode: any; private rNode: any;
private settingsModule: any; private audioModule: any;
private finishStartup?: () => void; private finishStartup?: () => void;
private ready: Promise<void> = new Promise((res) => (this.finishStartup = res)); private ready: Promise<void> = new Promise((res) => (this.finishStartup = res));
private toasterPatch?: Patch; private toasterPatch?: Patch;
@@ -39,11 +40,14 @@ class Toaster extends Logger {
let instance: any; let instance: any;
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current; const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
const findToasterRoot = (currentNode: any, iters: number): any => { const findToasterRoot = (currentNode: any, iters: number): any => {
if (iters >= 50) { if (iters >= 65) {
// currently 40 // currently 65
return null; return null;
} }
if (currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder')) { if (
currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder') ||
currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPopup')
) {
this.log(`Toaster root was found in ${iters} recursion cycles`); this.log(`Toaster root was found in ${iters} recursion cycles`);
return currentNode; return currentNode;
} }
@@ -127,6 +131,17 @@ class Toaster extends Logger {
this.rNode.stateNode.forceUpdate(); this.rNode.stateNode.forceUpdate();
delete this.rNode.stateNode.shouldComponentUpdate; delete this.rNode.stateNode.shouldComponentUpdate;
this.audioModule = findModuleChild((m: Module) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
try {
if (m[prop].PlayNavSound && m[prop].RegisterCallbackOnPlaySound) return m[prop];
} catch {
return undefined;
}
}
});
this.log('Initialized'); this.log('Initialized');
this.finishStartup?.(); this.finishStartup?.();
} }
@@ -135,24 +150,31 @@ class Toaster extends Logger {
// toast.duration = toast.duration || 5e3; // toast.duration = toast.duration || 5e3;
// this.toasterState.addToast(toast); // this.toasterState.addToast(toast);
await this.ready; await this.ready;
const settings = this.settingsModule?.settings;
let toastData = { let toastData = {
nNotificationID: window.NotificationStore.m_nNextTestNotificationID++, nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
rtCreated: Date.now(), rtCreated: Date.now(),
eType: 15, eType: toast.eType || 11,
nToastDurationMS: toast.duration || (toast.duration = 5e3), nToastDurationMS: toast.duration || (toast.duration = 5e3),
data: toast, data: toast,
decky: true, decky: true,
}; };
// @ts-ignore // @ts-ignore
toastData.data.appid = () => 0; toastData.data.appid = () => 0;
if (toast.sound === undefined) toast.sound = 6;
if (toast.playSound === undefined) toast.playSound = true;
if (toast.showToast === undefined) toast.showToast = true;
if ( if (
(settings?.bDisableAllToasts && !toast.critical) || (window.settingsStore.settings.bDisableAllToasts && !toast.critical) ||
(settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame()) (window.settingsStore.settings.bDisableToastsInGame &&
!toast.critical &&
window.NotificationStore.BIsUserInGame())
) )
return; return;
window.NotificationStore.m_rgNotificationToasts.push(toastData); if (toast.playSound) this.audioModule?.PlayNavSound(toast.sound);
window.NotificationStore.DispatchNextToast(); if (toast.showToast) {
window.NotificationStore.m_rgNotificationToasts.push(toastData);
window.NotificationStore.DispatchNextToast();
}
} }
deinit() { deinit() {
+1 -1
View File
@@ -18,6 +18,6 @@
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["src"], "include": ["src", "index.d.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }
+1 -1
View File
@@ -2,4 +2,4 @@ aiohttp==3.8.1
aiohttp-jinja2==1.5.0 aiohttp-jinja2==1.5.0
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
watchdog==2.1.7 watchdog==2.1.7
certifi==2022.6.15 certifi==2022.12.7