mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-24 03:59:13 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2518d1a0b3 | |||
| 010e6a22ab | |||
| 134b896e01 | |||
| 047813b965 | |||
| dbcb549ae2 | |||
| d689614c78 | |||
| ec907627b8 | |||
| a3809222f9 | |||
| 86dc706892 | |||
| 0e409a9f96 | |||
| d58001c323 | |||
| d727ba72f3 | |||
| fa028fa525 | |||
| c947548064 | |||
| 19d5527bdf | |||
| ef51b96f08 | |||
| 617916e8e5 | |||
| 6c4a4d0a44 | |||
| bedcb0fb71 | |||
| 2461f52ca7 | |||
| 3c00eb8cf4 | |||
| 21e1d8504a | |||
| ba93c4add2 | |||
| 61fea41c8a | |||
| e40d3e4db5 | |||
| bbad6bf2be | |||
| 4e04455163 | |||
| 314292b042 | |||
| a264f36966 | |||
| 60c8c5db42 | |||
| 852c52c59a | |||
| 3136ad72ed | |||
| a3659ba425 | |||
| d1887870f5 | |||
| 1892403044 | |||
| f5a1837227 | |||
| 97f95705f8 | |||
| 7c99af9a9a | |||
| b35bd056d5 | |||
| d2da85460d |
+61
-10
@@ -15,6 +15,15 @@ on:
|
||||
- none
|
||||
- prerelease
|
||||
- release
|
||||
bump:
|
||||
type: choice
|
||||
description: Semver to bump
|
||||
default: 'none'
|
||||
options:
|
||||
- none
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -25,6 +34,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Print input
|
||||
run : |
|
||||
echo "release: ${{ github.event.inputs.release }}\n"
|
||||
echo "bump: ${{ github.event.inputs.bump }}\n"
|
||||
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v3
|
||||
|
||||
@@ -106,11 +120,26 @@ jobs:
|
||||
if [[ "$VERSION" =~ "-pre" ]]; then
|
||||
printf "is prerelease, bumping to release\n"
|
||||
OUT=$(semver bump release "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
printf "OUT: ${OUT}\n"\
|
||||
printf "bumping by selected type.\n"
|
||||
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "no type selected, defaulting to patch.\n"
|
||||
OUT=$(semver bump patch "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
elif [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
printf "previous tag is a release, bumping by a patch\n"
|
||||
OUT=$(semver bump minor "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
printf "previous tag is a release, bumping by selected type.\n"
|
||||
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "previous tag is a release, but no bump selected. Defaulting to a patch bump.\n"
|
||||
OUT=$(semver bump patch "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
fi
|
||||
echo "vOUT: v$OUT"
|
||||
echo ::set-output name=tag_name::v$OUT
|
||||
@@ -138,7 +167,7 @@ jobs:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
steps:
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v3
|
||||
|
||||
@@ -170,16 +199,38 @@ jobs:
|
||||
echo "VERS: $VERSION"
|
||||
OUT=""
|
||||
if [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
printf "is release, bumping minor version and prerel\n"
|
||||
OUT=$(semver bump minor "$VERSION")
|
||||
printf "pre-release from release, bumping by selected type and prerel\n"
|
||||
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "type not selected, defaulting to patch\n"
|
||||
OUT=$(semver bump patch "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
OUT="$OUT-pre"
|
||||
OUT=$(semver bump prerel "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
elif [[ "$VERSION" =~ "-pre" ]]; then
|
||||
printf "is a prerelease, bumping prerel\n"
|
||||
OUT=$(semver bump prerel "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
printf "pre-release to pre-release, bumping by selected type and or prerel version\n"
|
||||
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
OUT="$OUT-pre"
|
||||
printf "OUT: ${OUT}\n"
|
||||
printf "bumping prerel\n"
|
||||
OUT=$(semver bump prerel "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "type not selected, defaulting to new pre-release only\n"
|
||||
printf "bumping prerel\n"
|
||||
OUT="$VERSION-pre"
|
||||
printf "OUT: ${OUT}\n"
|
||||
OUT=$(semver bump prerel "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
fi
|
||||
printf "vOUT: v${OUT}\n"
|
||||
echo ::set-output name=tag_name::v$OUT
|
||||
|
||||
- name: Push tag 📤
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
# Plugin Loader [](https://discord.gg/ZU74G2NJzk)
|
||||
# Decky Loader [](https://discord.gg/ZU74G2NJzk)
|
||||
|
||||

|
||||
|
||||
Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugin Loader, documentation + tools for plugin development and more.
|
||||
Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Decky Loader, documentation + tools for plugin development and more.
|
||||
|
||||
## Installation
|
||||
1. Go into the Steam Deck Settings
|
||||
2. Under System -> System Settings toggle `Enable Developer Mode`
|
||||
3. Scroll the sidebar all the way down and click on `Developer`
|
||||
4. Under Miscellaneous, enable `CEF Remote Debugging`
|
||||
5. Click on the `STEAM` button and select `Power` -> `Switch to Desktop`
|
||||
6. Make sure you have a password set with the "passwd" command in terminal to install it ([YouTube Guide](https://www.youtube.com/watch?v=1vOMYGj22rQ)).
|
||||
7. Open a terminal and paste the following command into it:
|
||||
- For the latest pre-release:
|
||||
5. Confirm dialog and wait for system reboot
|
||||
6. Click on the `STEAM` button and select `Power` -> `Switch to Desktop`
|
||||
7. Make sure you have a password set with the "passwd" command in terminal to install it ([YouTube Guide](https://www.youtube.com/watch?v=1vOMYGj22rQ)).
|
||||
- It will look like the password isn't typing properly. That's normal, it's a security feature (Similar to `***` when typing passwords online)
|
||||
8. Open a terminal ("Konsole" is the pre-installed terminal application) and paste the following command into it:
|
||||
- For the latest release (recommended for all users):
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_release.sh | sh`
|
||||
- For the latest pre-release (testing releases, unlikely to be fully stable):
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_prerelease.sh | sh`
|
||||
- For testers/plugin developers:
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_prerelease.sh | sh`
|
||||
- [Wiki Link](https://deckbrew.xyz/en/loader-dev/development)
|
||||
- For the legacy version (unsupported):
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/legacy/dist/install_release.sh | sh`
|
||||
7. Done! Reboot back into Gaming mode and enjoy your plugins!
|
||||
9. Done! Reboot back into Gaming mode and enjoy your plugins!
|
||||
|
||||
### Install/Uninstall Plugins
|
||||
- Using the shopping bag button in the top right corner, you can go to the offical ["Plugin Store"](https://plugins.deckbrew.xyz/)
|
||||
- Simply copy the plugin's folder into `~/homebrew/plugins`
|
||||
- Using the shopping bag button in the top right corner of the plugin menu, you can go to the offical Plugin Store ([Web Preview](https://beta.deckbrew.xyz/)).
|
||||
- Install from URL in the settings menu.
|
||||
- Use the settings menu to uninstall plugins, this will not remove any files made in different directories by plugins.
|
||||
|
||||
### Uninstall
|
||||
@@ -41,8 +43,8 @@ Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugi
|
||||
- There is no complete plugin development documentation yet. However a good starting point is the [Plugin Template](https://github.com/SteamDeckHomebrew/decky-plugin-template) repository.
|
||||
|
||||
## [Contribution](https://deckbrew.xyz/en/loader-dev/development)
|
||||
- Please consult the [Wiki](https://deckbrew.xyz/en/loader-dev/development) for installing development versions of PluginLoader.
|
||||
- This is also useful for Plugin Developers looking to target new but unreleased versions of PluginLoader.
|
||||
- Please consult the [Wiki](https://deckbrew.xyz/en/loader-dev/development) for installing development versions of Decky Loader.
|
||||
- This is also useful for Plugin Developers looking to target new but unreleased versions of Decky Loader.
|
||||
- [Here's how to get the Steam Deck UI on your enviroment of choice.](https://youtu.be/1IAbZte8e7E?t=112)
|
||||
- (The video shows Windows usage but unless you're using Arch WSL/cygwin this script is unsupported on Windows.)
|
||||
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"inputs": {
|
||||
"release": "prerelease"
|
||||
"release": "prerelease",
|
||||
"bump": "none"
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"inputs": {
|
||||
"release": "release"
|
||||
"release": "release",
|
||||
"bump": "none"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
type=$1
|
||||
# bump=$2
|
||||
|
||||
oldartifactsdir="old"
|
||||
|
||||
|
||||
+55
-9
@@ -8,14 +8,14 @@ from concurrent.futures import ProcessPoolExecutor
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
from logging import getLogger
|
||||
from os import path, rename, listdir
|
||||
from os import R_OK, W_OK, path, rename, listdir, access, mkdir
|
||||
from shutil import rmtree
|
||||
from subprocess import call
|
||||
from time import time
|
||||
from zipfile import ZipFile
|
||||
|
||||
# Local modules
|
||||
from helpers import get_ssl_context, get_user, get_user_group
|
||||
from helpers import get_ssl_context, get_user, get_user_group, download_remote_binary_to_path
|
||||
from injector import get_tab, inject_to_tab
|
||||
|
||||
logger = getLogger("Browser")
|
||||
@@ -47,6 +47,48 @@ class PluginBrowser:
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath):
|
||||
rv = False
|
||||
try:
|
||||
packageJsonPath = path.join(pluginBasePath, 'package.json')
|
||||
pluginBinPath = path.join(pluginBasePath, 'bin')
|
||||
|
||||
if access(packageJsonPath, R_OK):
|
||||
with open(packageJsonPath, 'r') as f:
|
||||
packageJson = json.load(f)
|
||||
if len(packageJson["remote_binary"]) > 0:
|
||||
# create bin directory if needed.
|
||||
rc=call(["chmod", "-R", "777", pluginBasePath])
|
||||
if access(pluginBasePath, W_OK):
|
||||
|
||||
if not path.exists(pluginBinPath):
|
||||
mkdir(pluginBinPath)
|
||||
|
||||
if not access(pluginBinPath, W_OK):
|
||||
rc=call(["chmod", "-R", "777", pluginBinPath])
|
||||
|
||||
rv = True
|
||||
for remoteBinary in packageJson["remote_binary"]:
|
||||
# Required Fields. If any Remote Binary is missing these fail the install.
|
||||
binName = remoteBinary["name"]
|
||||
binURL = remoteBinary["url"]
|
||||
binHash = remoteBinary["sha256hash"]
|
||||
if not await download_remote_binary_to_path(binURL, binHash, path.join(pluginBinPath, binName)):
|
||||
rv = False
|
||||
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
|
||||
|
||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
|
||||
rc=call(["chmod", "-R", "555", pluginBasePath])
|
||||
else:
|
||||
rv = True
|
||||
logger.debug(f"No Remote Binaries to Download")
|
||||
|
||||
except Exception as e:
|
||||
rv = False
|
||||
logger.debug(str(e))
|
||||
|
||||
return rv
|
||||
|
||||
def find_plugin_folder(self, name):
|
||||
for folder in listdir(self.plugin_path):
|
||||
@@ -100,14 +142,18 @@ class PluginBrowser:
|
||||
logger.debug("Unzipping...")
|
||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
plugin_dir = self.find_plugin_folder(name)
|
||||
if name in self.loader.plugins:
|
||||
self.loader.plugins[name].stop()
|
||||
self.loader.plugins.pop(name, None)
|
||||
await sleep(1)
|
||||
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_dir)
|
||||
# await inject_to_tab("SP", "window.syncDeckyPlugins()")
|
||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
if name in self.loader.plugins:
|
||||
self.loader.plugins[name].stop()
|
||||
self.loader.plugins.pop(name, None)
|
||||
await sleep(1)
|
||||
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_dir)
|
||||
# await inject_to_tab("SP", "window.syncDeckyPlugins()")
|
||||
else:
|
||||
logger.fatal(f"Failed Downloading Remote Binaries")
|
||||
else:
|
||||
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||
if self.loader.watcher:
|
||||
|
||||
@@ -2,11 +2,15 @@ import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import uuid
|
||||
import os
|
||||
from subprocess import check_output
|
||||
from time import sleep
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
|
||||
import certifi
|
||||
from aiohttp.web import Response, middleware
|
||||
from aiohttp import ClientSession
|
||||
|
||||
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
||||
|
||||
@@ -83,6 +87,30 @@ def get_homebrew_path(home_path = None) -> str:
|
||||
return str(home_path+"/homebrew")
|
||||
# return str(home_path+"/homebrew")
|
||||
|
||||
# Download Remote Binaries to local Plugin
|
||||
async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
||||
rv = False
|
||||
try:
|
||||
if os.access(os.path.dirname(path), os.W_OK):
|
||||
async with ClientSession() as client:
|
||||
res = await client.get(url, ssl=get_ssl_context())
|
||||
if res.status == 200:
|
||||
data = BytesIO(await res.read())
|
||||
remoteHash = sha256(data.getbuffer()).hexdigest()
|
||||
if binHash == remoteHash:
|
||||
data.seek(0)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data.getbuffer())
|
||||
rv = True
|
||||
else:
|
||||
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
|
||||
else:
|
||||
rv = False
|
||||
except:
|
||||
rv = False
|
||||
|
||||
return rv
|
||||
|
||||
async def is_systemd_unit_active(unit_name: str) -> bool:
|
||||
res = subprocess.run(["systemctl", "is-active", unit_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
return res.returncode == 0
|
||||
|
||||
+9
-1
@@ -25,7 +25,7 @@ class FileChangeHandler(RegexMatchingEventHandler):
|
||||
self.logger = getLogger("file-watcher")
|
||||
self.plugin_path = plugin_path
|
||||
self.queue = queue
|
||||
self.disabled = False
|
||||
self.disabled = True
|
||||
|
||||
def maybe_reload(self, src_path):
|
||||
if self.disabled:
|
||||
@@ -70,6 +70,7 @@ class Loader:
|
||||
self.logger.info(f"plugin_path: {self.plugin_path}")
|
||||
self.plugins = {}
|
||||
self.watcher = None
|
||||
self.live_reload = live_reload
|
||||
|
||||
if live_reload:
|
||||
self.reload_queue = Queue()
|
||||
@@ -78,6 +79,7 @@ class Loader:
|
||||
self.observer.schedule(self.watcher, self.plugin_path, recursive=True)
|
||||
self.observer.start()
|
||||
self.loop.create_task(self.handle_reloads())
|
||||
self.loop.create_task(self.enable_reload_wait())
|
||||
|
||||
server_instance.add_routes([
|
||||
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
||||
@@ -92,6 +94,12 @@ class Loader:
|
||||
web.get("/steam_resource/{path:.+}", self.get_steam_resource)
|
||||
])
|
||||
|
||||
async def enable_reload_wait(self):
|
||||
if self.live_reload:
|
||||
await sleep(10)
|
||||
self.logger.info("Hot reload enabled")
|
||||
self.watcher.disabled = False
|
||||
|
||||
async def handle_frontend_assets(self, request):
|
||||
file = path.join(path.dirname(__file__), "static", request.match_info["path"])
|
||||
|
||||
|
||||
+6
-2
@@ -1,9 +1,13 @@
|
||||
# Change PyInstaller files permissions
|
||||
import sys
|
||||
from subprocess import call
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
call(['chmod', '-R', '755', sys._MEIPASS])
|
||||
# Full imports
|
||||
from asyncio import get_event_loop, sleep
|
||||
from json import dumps, loads
|
||||
from logging import DEBUG, INFO, basicConfig, getLogger
|
||||
from os import getenv, path
|
||||
from subprocess import call
|
||||
from os import getenv, chmod
|
||||
from traceback import format_exc
|
||||
|
||||
import aiohttp_cors
|
||||
|
||||
Vendored
+7
-2
@@ -13,8 +13,13 @@ sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||
|
||||
# Download latest release and install it
|
||||
curl -L https://github.com/SteamDeckHomebrew/PluginLoader/releases/latest/download/PluginLoader --output "${HOMEBREW_FOLDER}/services/PluginLoader"
|
||||
chmod +x "${HOMEBREW_FOLDER}/services/PluginLoader"
|
||||
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}))
|
||||
|
||||
printf "Installing version %s...\n" "${VERSION}"
|
||||
curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
echo $VERSION > ${HOMEBREW_FOLDER}/services/.loader.version
|
||||
|
||||
systemctl --user stop plugin_loader 2> /dev/null
|
||||
systemctl --user disable plugin_loader 2> /dev/null
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "decky_frontend",
|
||||
"version": "0.0.1",
|
||||
"version": "2.1.1",
|
||||
"private": true,
|
||||
"license": "GPLV2",
|
||||
"scripts": {
|
||||
|
||||
@@ -24,6 +24,7 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
|
||||
aRef?.current?.click();
|
||||
props.onDismiss?.();
|
||||
}}
|
||||
style={{ display: 'inline' }}
|
||||
>
|
||||
<a ref={aRef} {...nodeProps.node.properties}>
|
||||
{nodeProps.children}
|
||||
|
||||
@@ -32,8 +32,8 @@ const PluginView: VFC = () => {
|
||||
.map(({ name, icon }) => (
|
||||
<PanelSectionRow key={name}>
|
||||
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>{icon}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
{icon}
|
||||
<div>{name}</div>
|
||||
<NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} />
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
|
||||
setLoading(true);
|
||||
await onOK();
|
||||
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
|
||||
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
|
||||
}}
|
||||
onCancel={async () => {
|
||||
await onCancel();
|
||||
|
||||
@@ -113,8 +113,22 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
}}
|
||||
className="deckyStoreCardInfo"
|
||||
>
|
||||
<p className={joinClassNames(staticClasses.PanelSectionRow)}>
|
||||
<span>Author: {plugin.author}</span>
|
||||
<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)}
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import { ConfirmModal, ModalRoot, QuickAccessTab, Router, showModal, sleep, staticClasses } from 'decky-frontend-lib';
|
||||
import {
|
||||
ConfirmModal,
|
||||
ModalRoot,
|
||||
Patch,
|
||||
QuickAccessTab,
|
||||
Router,
|
||||
callOriginal,
|
||||
findModuleChild,
|
||||
replacePatch,
|
||||
showModal,
|
||||
sleep,
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { lazy } from 'react';
|
||||
import { FaPlug } from 'react-icons/fa';
|
||||
|
||||
@@ -39,6 +51,8 @@ class PluginLoader extends Logger {
|
||||
// stores a list of plugin names which requested to be reloaded
|
||||
private pluginReloadQueue: { name: string; version?: string }[] = [];
|
||||
|
||||
private focusWorkaroundPatch?: Patch;
|
||||
|
||||
constructor() {
|
||||
super(PluginLoader.name);
|
||||
this.log('Initialized');
|
||||
@@ -83,6 +97,38 @@ class PluginLoader extends Logger {
|
||||
initFilepickerPatches();
|
||||
|
||||
this.updateVersion();
|
||||
|
||||
const self = this;
|
||||
|
||||
try {
|
||||
// TODO remove all of this once Valve fixes the bug
|
||||
const focusManager = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return false;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.prototype?.TakeFocus) return m[prop];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
this.focusWorkaroundPatch = replacePatch(focusManager.prototype, 'TakeFocus', function () {
|
||||
// @ts-ignore
|
||||
const classList = this.m_node?.m_element.classList;
|
||||
if (
|
||||
// @ts-ignore
|
||||
(this.m_node?.m_element && classList.contains(staticClasses.TabGroupPanel)) ||
|
||||
classList.contains('FriendsListTab') ||
|
||||
classList.contains('FriendsTabList') ||
|
||||
classList.contains('FriendsListAndChatsSteamDeck')
|
||||
) {
|
||||
self.debug('Intercepted friends re-focus');
|
||||
return true;
|
||||
}
|
||||
|
||||
return callOriginal;
|
||||
});
|
||||
} catch (e) {
|
||||
this.error('Friends focus patch failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
public async updateVersion() {
|
||||
@@ -167,6 +213,7 @@ class PluginLoader extends Logger {
|
||||
this.routerHook.removeRoute('/decky/store');
|
||||
this.routerHook.removeRoute('/decky/settings');
|
||||
deinitFilepickerPatches();
|
||||
this.focusWorkaroundPatch?.unpatch();
|
||||
}
|
||||
|
||||
public unloadPlugin(name: string) {
|
||||
|
||||
+24
-10
@@ -47,18 +47,31 @@ class TabsHook extends Logger {
|
||||
const self = this;
|
||||
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
|
||||
let scrollRoot: any;
|
||||
let currentNode = tree;
|
||||
async function findScrollRoot(currentNode: any, iters: number): Promise<any> {
|
||||
if (iters >= 30) {
|
||||
self.error(
|
||||
'Scroll root was not found before hitting the recursion limit, a developer will need to increase the limit.',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
currentNode = currentNode?.child;
|
||||
if (currentNode?.type?.prototype?.RemoveSmartScrollContainer) {
|
||||
self.log(`Scroll root was found in ${iters} recursion cycles`);
|
||||
return currentNode;
|
||||
}
|
||||
if (!currentNode) return null;
|
||||
if (currentNode.sibling) {
|
||||
let node = await findScrollRoot(currentNode.sibling, iters + 1);
|
||||
if (node !== null) return node;
|
||||
}
|
||||
return await findScrollRoot(currentNode, iters + 1);
|
||||
}
|
||||
(async () => {
|
||||
let iters = 0;
|
||||
scrollRoot = await findScrollRoot(tree, 0);
|
||||
while (!scrollRoot) {
|
||||
iters++;
|
||||
currentNode = currentNode?.child;
|
||||
if (iters >= 30 || !currentNode) {
|
||||
iters = 0;
|
||||
currentNode = tree;
|
||||
await sleep(5000);
|
||||
}
|
||||
if (currentNode?.type?.prototype?.RemoveSmartScrollContainer) scrollRoot = currentNode;
|
||||
this.log('Failed to find scroll root node, reattempting in 5 seconds');
|
||||
await sleep(5000);
|
||||
scrollRoot = await findScrollRoot(tree, 0);
|
||||
}
|
||||
let newQA: any;
|
||||
let newQATabRenderer: any;
|
||||
@@ -101,6 +114,7 @@ class TabsHook extends Logger {
|
||||
});
|
||||
this.cNode = scrollRoot;
|
||||
this.cNode.stateNode.forceUpdate();
|
||||
this.log('Finished initial injection');
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ class Toaster extends Logger {
|
||||
|
||||
async init() {
|
||||
let instance: any;
|
||||
|
||||
while (true) {
|
||||
instance = findInReactTree(
|
||||
(document.getElementById('root') as any)._reactRootContainer._internalRoot.current,
|
||||
|
||||
Reference in New Issue
Block a user