mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 08:47:49 +00:00
Compare commits
18 Commits
v2.5.2
...
v2.6.1-pre2
| Author | SHA1 | Date | |
|---|---|---|---|
| d335562328 | |||
| f9624a0859 | |||
| 97bb3fa4c8 | |||
| 611245aec9 | |||
| e1807e8c75 | |||
| b94cfe32d9 | |||
| f1e679c3fb | |||
| e1b138bcbd | |||
| c6be8f6c14 | |||
| ac086cf59e | |||
| 3e120ea312 | |||
| 0b718daa47 | |||
| 0929b9c5cb | |||
| 43b2269ea7 | |||
| 0c4e27cd34 | |||
| 36cf85b08a | |||
| 994da868af | |||
| 2e53fb217a |
@@ -70,4 +70,4 @@ body:
|
||||
description: Please reboot your deck (if possible) when attempting to recreate the issue, then run ``cd ~ && journalctl -b0 -u plugin_loader.service > deckylog.txt``. This will save the log file to ``~`` aka ``/home/deck``. Please upload the file here
|
||||
placeholder: deckylog.txt
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build Python Backend 🛠️
|
||||
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/legacy:/legacy ./backend/*.py
|
||||
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/legacy:/legacy --add-data ./plugin:/plugin ./backend/*.py
|
||||
|
||||
- name: Upload package artifact ⬆️
|
||||
if: ${{ !env.ACT }}
|
||||
|
||||
@@ -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
|
||||
@@ -11,7 +11,7 @@
|
||||
<a href="https://github.com/SteamDeckHomebrew/decky-loader/stargazers"><img src="https://img.shields.io/github/stars/SteamDeckHomebrew/decky-loader" /></a>
|
||||
<a href="https://github.com/SteamDeckHomebrew/decky-loader/commits/main"><img src="https://img.shields.io/github/last-commit/SteamDeckHomebrew/decky-loader.svg" /></a>
|
||||
<a href="https://github.com/SteamDeckHomebrew/decky-loader/blob/main/LICENSE"><img src="https://img.shields.io/github/license/SteamDeckHomebrew/decky-loader" /></a>
|
||||
<a href="https://discord.gg/ZU74G2NJzk"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
|
||||
<a href="https://deckbrew.xyz/discord"><img src="https://img.shields.io/discord/960281551428522045?color=%235865F2&label=discord" /></a>
|
||||
<br>
|
||||
<br>
|
||||
<img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">
|
||||
@@ -36,6 +36,7 @@ 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.
|
||||
- Syncthing may use port 8080 on Steam Deck, which Decky Loader needs to function. If you are using Syncthing as a service, please change its port to something else.
|
||||
- If you are using any software that uses port 1337 or 8080, please change its port to something else or uninstall it.
|
||||
- If you run the installer and it just opens a file in a text editor: click the (...) button in the top right of dolphin (the file manager) then 'configure' and 'configure dolphin'. Click on the 'confirmations' tab and set 'when opening an executable file' to 'run script'.
|
||||
|
||||
## 💾 Installation
|
||||
- This installation can be done without an admin/sudo password set.
|
||||
@@ -61,7 +62,7 @@ For more information about Decky Loader as well as documentation and development
|
||||
|
||||
### 👋 Uninstallation
|
||||
|
||||
We are sorry to see you go! If you are considering uninstalling because you are having issues, please consider [opening an issue](https://github.com/SteamDeckHomebrew/decky-loader/issues) or [joining our Discord](https://discord.gg/ZU74G2NJzk) so we can help you and other users.
|
||||
We are sorry to see you go! If you are considering uninstalling because you are having issues, please consider [opening an issue](https://github.com/SteamDeckHomebrew/decky-loader/issues) or [joining our Discord](https://deckbrew.xyz/discord) so we can help you and other users.
|
||||
|
||||
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
|
||||
1. Select "Switch to Desktop".
|
||||
@@ -83,7 +84,7 @@ Now that you have Decky Loader installed, you can start using plugins. Each plug
|
||||
|
||||
### 🛠️ Plugin Development
|
||||
|
||||
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://discord.gg/ZU74G2NJzk) if you have any questions.
|
||||
There is no complete plugin development documentation yet. However a good starting point is the [plugin template repository](https://github.com/SteamDeckHomebrew/decky-plugin-template). Consider [joining our Discord](https://deckbrew.xyz/discord) if you have any questions.
|
||||
|
||||
### 🤝 Contributing
|
||||
|
||||
@@ -92,6 +93,7 @@ Please consult [the wiki page regarding development](https://deckbrew.xyz/en/loa
|
||||
1. Clone the repository using the latest commit to main before starting your PR.
|
||||
1. In your clone of the repository, run these commands.
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm i
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
+2
-2
@@ -26,10 +26,10 @@ cd ..
|
||||
|
||||
if [[ "$type" == "release" ]]; then
|
||||
printf "release!\n"
|
||||
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts
|
||||
act workflow_dispatch -e act/release.json --artifact-server-path act/artifacts --container-architecture linux/amd64
|
||||
elif [[ "$type" == "prerelease" ]]; then
|
||||
printf "prerelease!\n"
|
||||
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts
|
||||
act workflow_dispatch -e act/prerelease.json --artifact-server-path act/artifacts --container-architecture linux/amd64
|
||||
else
|
||||
printf "Release type unspecified/badly specified.\n"
|
||||
printf "Options: 'release' or 'prerelease'\n"
|
||||
|
||||
+11
-2
@@ -1,5 +1,7 @@
|
||||
# Full imports
|
||||
import json
|
||||
# import pprint
|
||||
# from pprint import pformat
|
||||
|
||||
# Partial imports
|
||||
from aiohttp import ClientSession, web
|
||||
@@ -108,11 +110,18 @@ class PluginBrowser:
|
||||
try:
|
||||
logger.info("uninstalling " + name)
|
||||
logger.info(" at dir " + self.find_plugin_folder(name))
|
||||
logger.debug("unloading %s" % str(name))
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
||||
logger.debug("calling frontend unload for %s" % str(name))
|
||||
res = await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
||||
logger.debug("result of unload from UI: %s", res)
|
||||
# plugins_snapshot = self.plugins.copy()
|
||||
# snapshot_string = pformat(plugins_snapshot)
|
||||
# logger.debug("current plugins: %s", snapshot_string)
|
||||
if self.plugins[name]:
|
||||
logger.debug("Plugin %s was found", name)
|
||||
self.plugins[name].stop()
|
||||
logger.debug("Plugin %s was stopped", name)
|
||||
del self.plugins[name]
|
||||
logger.debug("Plugin %s was removed from the dictionary", name)
|
||||
logger.debug("removing files %s" % str(name))
|
||||
rmtree(self.find_plugin_folder(name))
|
||||
except FileNotFoundError:
|
||||
|
||||
+12
-2
@@ -115,8 +115,18 @@ def mkdir_as_user(path):
|
||||
|
||||
# Fetches the version of loader
|
||||
def get_loader_version() -> str:
|
||||
with open(os.path.join(os.path.dirname(sys.argv[0]), ".loader.version"), "r", encoding="utf-8") as version_file:
|
||||
return version_file.readline().replace("\n", "")
|
||||
try:
|
||||
with open(os.path.join(os.getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
|
||||
return version_file.readline().strip()
|
||||
except:
|
||||
return "unknown"
|
||||
|
||||
# returns the appropriate system python paths
|
||||
def get_system_pythonpaths() -> list[str]:
|
||||
# run as normal normal user to also include user python paths
|
||||
proc = subprocess.run(["python3", "-c", "import sys; print(':'.join(x for x in sys.path if x))"],
|
||||
user=get_user_id(), env={}, capture_output=True)
|
||||
return proc.stdout.decode().strip().split(":")
|
||||
|
||||
# Download Remote Binaries to local Plugin
|
||||
async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
||||
|
||||
+2
-7
@@ -6,14 +6,9 @@ from pathlib import Path
|
||||
from traceback import print_exc
|
||||
|
||||
from aiohttp import web
|
||||
from genericpath import exists
|
||||
from os.path import exists
|
||||
from watchdog.events import RegexMatchingEventHandler
|
||||
from watchdog.utils import UnsupportedLibc
|
||||
|
||||
try:
|
||||
from watchdog.observers.inotify import InotifyObserver as Observer
|
||||
except UnsupportedLibc:
|
||||
from watchdog.observers.fsevents import FSEventsObserver as Observer
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from injector import get_tab, get_gamepadui_tab
|
||||
from plugin import PluginWrapper
|
||||
|
||||
+15
-2
@@ -9,7 +9,7 @@ from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from os import path, setgid, setuid, environ
|
||||
from signal import SIGINT, signal
|
||||
from sys import exit
|
||||
from sys import exit, path as syspath
|
||||
from time import time
|
||||
import helpers
|
||||
from updater import Updater
|
||||
@@ -66,6 +66,7 @@ class PluginWrapper:
|
||||
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_USER_HOME"] = helpers.get_home_path()
|
||||
environ["DECKY_HOME"] = helpers.get_homebrew_path()
|
||||
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"])
|
||||
@@ -77,11 +78,19 @@ class PluginWrapper:
|
||||
environ["DECKY_PLUGIN_NAME"] = self.name
|
||||
environ["DECKY_PLUGIN_VERSION"] = self.version
|
||||
environ["DECKY_PLUGIN_AUTHOR"] = self.author
|
||||
# append the loader's plugin path to the recognized python paths
|
||||
syspath.append(path.realpath(path.join(path.dirname(__file__), "plugin")))
|
||||
# append the plugin's `py_modules` to the recognized python paths
|
||||
syspath.append(path.join(environ["DECKY_PLUGIN_DIR"], "py_modules"))
|
||||
# append the system and user python paths
|
||||
syspath.extend(helpers.get_system_pythonpaths())
|
||||
spec = spec_from_file_location("_", self.file)
|
||||
module = module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
self.Plugin = module.Plugin
|
||||
|
||||
if hasattr(self.Plugin, "_migration"):
|
||||
get_event_loop().run_until_complete(self.Plugin._migration(self.Plugin))
|
||||
if hasattr(self.Plugin, "_main"):
|
||||
get_event_loop().create_task(self.Plugin._main(self.Plugin))
|
||||
get_event_loop().create_task(self._setup_socket())
|
||||
@@ -92,9 +101,12 @@ class PluginWrapper:
|
||||
|
||||
async def _unload(self):
|
||||
try:
|
||||
self.log.info("Attempting to unload " + self.name + "\n")
|
||||
self.log.info("Attempting to unload with plugin " + self.name + "'s \"_unload\" function.\n")
|
||||
if hasattr(self.Plugin, "_unload"):
|
||||
await self.Plugin._unload(self.Plugin)
|
||||
self.log.info("Unloaded " + self.name + "\n")
|
||||
else:
|
||||
self.log.info("Could not find \"_unload\" in " + self.name + "'s main.py" + "\n")
|
||||
except:
|
||||
self.log.error("Failed to unload " + self.name + "!\n" + format_exc())
|
||||
exit(0)
|
||||
@@ -118,6 +130,7 @@ class PluginWrapper:
|
||||
break
|
||||
data = loads(line.decode("utf-8"))
|
||||
if "stop" in data:
|
||||
self.log.info("Calling Loader unload function.")
|
||||
await self._unload()
|
||||
get_event_loop().stop()
|
||||
while get_event_loop().is_running():
|
||||
|
||||
+8
-14
@@ -30,10 +30,7 @@ class Updater:
|
||||
}
|
||||
self.remoteVer = None
|
||||
self.allRemoteVers = None
|
||||
try:
|
||||
self.localVer = helpers.get_loader_version()
|
||||
except:
|
||||
self.localVer = False
|
||||
self.localVer = helpers.get_loader_version()
|
||||
|
||||
try:
|
||||
self.currentBranch = self.get_branch(self.context.settings)
|
||||
@@ -68,7 +65,7 @@ class Updater:
|
||||
logger.debug("current branch: %i" % ver)
|
||||
if ver == -1:
|
||||
logger.info("Current branch is not set, determining branch from version...")
|
||||
if self.localVer.startswith("v") and self.localVer.find("-pre"):
|
||||
if self.localVer.startswith("v") and "-pre" in self.localVer:
|
||||
logger.info("Current version determined to be pre-release")
|
||||
return 1
|
||||
else:
|
||||
@@ -94,15 +91,12 @@ class Updater:
|
||||
return str(url)
|
||||
|
||||
async def get_version(self):
|
||||
if self.localVer:
|
||||
return {
|
||||
"current": self.localVer,
|
||||
"remote": self.remoteVer,
|
||||
"all": self.allRemoteVers,
|
||||
"updatable": self.localVer != None
|
||||
}
|
||||
else:
|
||||
return {"current": "unknown", "remote": self.remoteVer, "all": self.allRemoteVers, "updatable": False}
|
||||
return {
|
||||
"current": self.localVer,
|
||||
"remote": self.remoteVer,
|
||||
"all": self.allRemoteVers,
|
||||
"updatable": self.localVer != "unknown"
|
||||
}
|
||||
|
||||
async def check_for_updates(self):
|
||||
logger.debug("checking for updates")
|
||||
|
||||
@@ -251,7 +251,7 @@ class Utilities:
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return FocusNavController?.m_ActiveContext?.ActiveWindow || window;
|
||||
return (GamepadNavTree?.m_context?.m_controller || FocusNavController)?.m_ActiveContext?.ActiveWindow || window;
|
||||
}
|
||||
});
|
||||
""" + await res.text() + "\n}"
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "^3.18.10",
|
||||
"decky-frontend-lib": "^3.19.1",
|
||||
"react-file-icon": "^1.2.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
|
||||
Generated
+4
-4
@@ -11,7 +11,7 @@ specifiers:
|
||||
'@types/react-file-icon': ^1.0.1
|
||||
'@types/react-router': 5.1.18
|
||||
'@types/webpack': ^5.28.0
|
||||
decky-frontend-lib: ^3.18.10
|
||||
decky-frontend-lib: ^3.19.1
|
||||
husky: ^8.0.1
|
||||
import-sort-style-module: ^6.0.0
|
||||
inquirer: ^8.2.4
|
||||
@@ -31,7 +31,7 @@ specifiers:
|
||||
typescript: ^4.7.4
|
||||
|
||||
dependencies:
|
||||
decky-frontend-lib: 3.18.10
|
||||
decky-frontend-lib: 3.19.1
|
||||
react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
|
||||
react-icons: 4.4.0_react@16.14.0
|
||||
react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u
|
||||
@@ -975,8 +975,8 @@ packages:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
/decky-frontend-lib/3.18.10:
|
||||
resolution: {integrity: sha512-2mgbA3sSkuwQR/FnmhXVrcW6LyTS95IuL6muJAmQCruhBvXapDtjk1TcgxqMZxFZwGD1IPnemPYxHZll6IgnZw==}
|
||||
/decky-frontend-lib/3.19.1:
|
||||
resolution: {integrity: sha512-hU4+EFs74MGzUCv8l1AO2+EBj9RRbnpU19Crm4u+3lbLu6d63U2GsUeQ9ssmNRcOMY1OuVZkRoZBE58soOBJ3A==}
|
||||
dev: false
|
||||
|
||||
/decode-named-character-reference/1.0.2:
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
export default function DeckyIcon() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 456" width="512" height="456">
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: 'none' }}
|
||||
d="M154.33,72.51v49.79c11.78-0.17,23.48,2,34.42,6.39c10.93,4.39,20.89,10.91,29.28,19.18
|
||||
c8.39,8.27,15.06,18.13,19.61,29c4.55,10.87,6.89,22.54,6.89,34.32c0,11.78-2.34,23.45-6.89,34.32
|
||||
c-4.55,10.87-11.21,20.73-19.61,29c-8.39,8.27-18.35,14.79-29.28,19.18c-10.94,4.39-22.63,6.56-34.42,6.39v49.77
|
||||
c36.78,0,72.05-14.61,98.05-40.62c26-26.01,40.61-61.28,40.61-98.05c0-36.78-14.61-72.05-40.61-98.05
|
||||
C226.38,87.12,191.11,72.51,154.33,72.51z"
|
||||
/>
|
||||
|
||||
<ellipse
|
||||
transform="matrix(0.982 -0.1891 0.1891 0.982 -37.1795 32.9988)"
|
||||
style={{ fill: 'none' }}
|
||||
cx="154.33"
|
||||
cy="211.33"
|
||||
rx="69.33"
|
||||
ry="69.33"
|
||||
/>
|
||||
<path style={{ fill: 'none' }} d="M430,97h-52v187h52c7.18,0,13-5.82,13-13V110C443,102.82,437.18,97,430,97z" />
|
||||
<path
|
||||
style={{ fill: 'currentColor' }}
|
||||
d="M432,27h-54V0H0v361c0,52.47,42.53,95,95,95h188c52.47,0,95-42.53,95-95v-7h54c44.18,0,80-35.82,80-80V107
|
||||
C512,62.82,476.18,27,432,27z M85,211.33c0-38.29,31.04-69.33,69.33-69.33c38.29,0,69.33,31.04,69.33,69.33
|
||||
c0,38.29-31.04,69.33-69.33,69.33C116.04,280.67,85,249.62,85,211.33z M252.39,309.23c-26.01,26-61.28,40.62-98.05,40.62v-49.77
|
||||
c11.78,0.17,23.48-2,34.42-6.39c10.93-4.39,20.89-10.91,29.28-19.18c8.39-8.27,15.06-18.13,19.61-29
|
||||
c4.55-10.87,6.89-22.53,6.89-34.32c0-11.78-2.34-23.45-6.89-34.32c-4.55-10.87-11.21-20.73-19.61-29
|
||||
c-8.39-8.27-18.35-14.79-29.28-19.18c-10.94-4.39-22.63-6.56-34.42-6.39V72.51c36.78,0,72.05,14.61,98.05,40.61
|
||||
c26,26.01,40.61,61.28,40.61,98.05C293,247.96,278.39,283.23,252.39,309.23z M443,271c0,7.18-5.82,13-13,13h-52V97h52
|
||||
c7.18,0,13,5.82,13,13V271z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Focusable, Router } from 'decky-frontend-lib';
|
||||
import { Focusable, Navigation } from 'decky-frontend-lib';
|
||||
import { FunctionComponent, useRef } from 'react';
|
||||
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@@ -22,7 +22,7 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
|
||||
onActivate={() => {}}
|
||||
onOKButton={() => {
|
||||
props.onDismiss?.();
|
||||
Router.NavigateToExternalWeb(aRef.current!.href);
|
||||
Navigation.NavigateToExternalWeb(aRef.current!.href);
|
||||
}}
|
||||
style={{ display: 'inline' }}
|
||||
>
|
||||
|
||||
@@ -4,15 +4,11 @@ const QuickAccessVisibleState = createContext<boolean>(true);
|
||||
|
||||
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
|
||||
|
||||
export const QuickAccessVisibleStateProvider: FC<{ initial: boolean; setter: ((val: boolean) => {}[]) | never[] }> = ({
|
||||
children,
|
||||
initial,
|
||||
setter,
|
||||
}) => {
|
||||
export const QuickAccessVisibleStateProvider: FC<{ initial: boolean; tab: any }> = ({ children, initial, tab }) => {
|
||||
const [visible, setVisible] = useState<boolean>(initial);
|
||||
const [prev, setPrev] = useState<boolean>(initial);
|
||||
// hack to use an array as a "pointer" to pass the setter up the tree
|
||||
setter[0] = setVisible;
|
||||
// HACK but i can't think of a better way to do this
|
||||
tab.qAMVisibilitySetter = setVisible;
|
||||
if (initial != prev) {
|
||||
setPrev(initial);
|
||||
setVisible(initial);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
|
||||
import { CSSProperties, VFC } from 'react';
|
||||
import { FaArrowLeft, FaCog, FaStore } from 'react-icons/fa';
|
||||
import { BsGearFill } from 'react-icons/bs';
|
||||
import { FaArrowLeft, FaStore } from 'react-icons/fa';
|
||||
|
||||
import { useDeckyState } from './DeckyState';
|
||||
|
||||
@@ -26,12 +27,6 @@ const TitleView: VFC = () => {
|
||||
if (activePlugin === null) {
|
||||
return (
|
||||
<Focusable style={titleStyles} className={staticClasses.Title}>
|
||||
<DialogButton
|
||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||
onClick={onSettingsClick}
|
||||
>
|
||||
<FaCog style={{ marginTop: '-4px', display: 'block' }} />
|
||||
</DialogButton>
|
||||
<div style={{ marginRight: 'auto', flex: 0.9 }}>Decky</div>
|
||||
<DialogButton
|
||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||
@@ -39,6 +34,12 @@ const TitleView: VFC = () => {
|
||||
>
|
||||
<FaStore style={{ marginTop: '-4px', display: 'block' }} />
|
||||
</DialogButton>
|
||||
<DialogButton
|
||||
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
|
||||
onClick={onSettingsClick}
|
||||
>
|
||||
<BsGearFill style={{ marginTop: '-4px', display: 'block' }} />
|
||||
</DialogButton>
|
||||
</Focusable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,6 @@ import Logger from '../../../../logger';
|
||||
|
||||
const logger = new Logger('LibraryPatch');
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
SteamClient: any;
|
||||
appDetailsStore: any;
|
||||
}
|
||||
}
|
||||
|
||||
let patch: Patch;
|
||||
|
||||
function rePatch() {
|
||||
@@ -20,7 +13,9 @@ function rePatch() {
|
||||
const details = window.appDetailsStore.GetAppDetails(appid);
|
||||
logger.debug('game details', details);
|
||||
// strShortcutStartDir
|
||||
const file = await window.DeckyPluginLoader.openFilePicker(details.strShortcutStartDir.replaceAll('"', ''));
|
||||
const file = await window.DeckyPluginLoader.openFilePicker(
|
||||
details?.strShortcutStartDir.replaceAll('"', '') || '/',
|
||||
);
|
||||
logger.debug('user selected', file);
|
||||
window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
|
||||
const pathArr = file.path.split('/');
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { SidebarNavigation } from 'decky-frontend-lib';
|
||||
import { lazy } from 'react';
|
||||
import { FaCode, FaPlug } from 'react-icons/fa';
|
||||
|
||||
import { useSetting } from '../../utils/hooks/useSetting';
|
||||
import DeckyIcon from '../DeckyIcon';
|
||||
import WithSuspense from '../WithSuspense';
|
||||
import GeneralSettings from './pages/general';
|
||||
import PluginList from './pages/plugin_list';
|
||||
@@ -13,19 +15,18 @@ export default function SettingsPage() {
|
||||
|
||||
const pages = [
|
||||
{
|
||||
title: 'General',
|
||||
title: 'Decky',
|
||||
content: <GeneralSettings isDeveloper={isDeveloper} setIsDeveloper={setIsDeveloper} />,
|
||||
route: '/decky/settings/general',
|
||||
icon: <DeckyIcon />,
|
||||
},
|
||||
{
|
||||
title: 'Plugins',
|
||||
content: <PluginList />,
|
||||
route: '/decky/settings/plugins',
|
||||
icon: <FaPlug />,
|
||||
},
|
||||
];
|
||||
|
||||
if (isDeveloper)
|
||||
pages.push({
|
||||
{
|
||||
title: 'Developer',
|
||||
content: (
|
||||
<WithSuspense>
|
||||
@@ -33,7 +34,10 @@ export default function SettingsPage() {
|
||||
</WithSuspense>
|
||||
),
|
||||
route: '/decky/settings/developer',
|
||||
});
|
||||
icon: <FaCode />,
|
||||
visible: isDeveloper,
|
||||
},
|
||||
];
|
||||
|
||||
return <SidebarNavigation title="Decky Settings" showTitle pages={pages} />;
|
||||
return <SidebarNavigation pages={pages} />;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Field, Focusable, TextField, Toggle } from 'decky-frontend-lib';
|
||||
import { DialogBody, Field, TextField, Toggle } from 'decky-frontend-lib';
|
||||
import { useRef } from 'react';
|
||||
import { FaReact, FaSteamSymbol } from 'react-icons/fa';
|
||||
|
||||
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
import RemoteDebuggingSettings from '../general/RemoteDebugging';
|
||||
|
||||
export default function DeveloperSettings() {
|
||||
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
|
||||
@@ -12,7 +13,8 @@ export default function DeveloperSettings() {
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogBody>
|
||||
<RemoteDebuggingSettings />
|
||||
<Field
|
||||
label="Enable Valve Internal"
|
||||
description={
|
||||
@@ -30,55 +32,33 @@ export default function DeveloperSettings() {
|
||||
setShowValveInternal(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>{' '}
|
||||
<Focusable
|
||||
onTouchEnd={
|
||||
reactDevtoolsIP == ''
|
||||
? () => {
|
||||
(textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onClick={
|
||||
reactDevtoolsIP == ''
|
||||
? () => {
|
||||
(textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onOKButton={
|
||||
reactDevtoolsIP == ''
|
||||
? () => {
|
||||
(textRef.current?.childNodes[0] as HTMLInputElement)?.focus();
|
||||
}
|
||||
: undefined
|
||||
</Field>
|
||||
<Field
|
||||
label="Enable React DevTools"
|
||||
description={
|
||||
<>
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the
|
||||
IP address before enabling.
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<div ref={textRef}>
|
||||
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
icon={<FaReact style={{ display: 'block' }} />}
|
||||
>
|
||||
<Field
|
||||
label="Enable React DevTools"
|
||||
description={
|
||||
<>
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set
|
||||
the IP address before enabling.
|
||||
</span>
|
||||
<div ref={textRef}>
|
||||
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
icon={<FaReact style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={reactDevtoolsEnabled}
|
||||
disabled={reactDevtoolsIP == ''}
|
||||
onChange={(toggleValue) => {
|
||||
setReactDevtoolsEnabled(toggleValue);
|
||||
setShouldConnectToReactDevTools(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</Focusable>
|
||||
</>
|
||||
<Toggle
|
||||
value={reactDevtoolsEnabled}
|
||||
// disabled={reactDevtoolsIP == ''}
|
||||
onChange={(toggleValue) => {
|
||||
setReactDevtoolsEnabled(toggleValue);
|
||||
setShouldConnectToReactDevTools(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const BranchSelect: FunctionComponent<{}> = () => {
|
||||
return (
|
||||
// Returns numerical values from 0 to 2 (with current branch setup as of 8/28/22)
|
||||
// 0 being stable, 1 being pre-release and 2 being nightly
|
||||
<Field label="Update Channel">
|
||||
<Field label="Decky Update Channel" childrenContainerWidth={'fixed'}>
|
||||
<Dropdown
|
||||
rgOptions={Object.values(UpdateBranch)
|
||||
.filter((branch) => typeof branch == 'string')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Field, Toggle } from 'decky-frontend-lib';
|
||||
import { FaBug } from 'react-icons/fa';
|
||||
import { FaChrome } from 'react-icons/fa';
|
||||
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
|
||||
@@ -11,10 +11,10 @@ export default function RemoteDebuggingSettings() {
|
||||
label="Allow Remote CEF Debugging"
|
||||
description={
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
Allow unauthenticated access to the CEF debugger to anyone in your network
|
||||
Allows unauthenticated access to the CEF debugger to anyone in your network.
|
||||
</span>
|
||||
}
|
||||
icon={<FaBug style={{ display: 'block' }} />}
|
||||
icon={<FaChrome style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={allowRemoteDebugging || false}
|
||||
|
||||
@@ -16,7 +16,7 @@ const StoreSelect: FunctionComponent<{}> = () => {
|
||||
// 0 being Default, 1 being Testing and 2 being Custom
|
||||
return (
|
||||
<>
|
||||
<Field label="Store Channel">
|
||||
<Field label="Plugin Store Channel" childrenContainerWidth={'fixed'}>
|
||||
<Dropdown
|
||||
rgOptions={Object.values(Store)
|
||||
.filter((store) => typeof store == 'string')
|
||||
|
||||
@@ -6,15 +6,15 @@ import {
|
||||
Focusable,
|
||||
ProgressBarWithInfo,
|
||||
Spinner,
|
||||
findSP,
|
||||
showModal,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useCallback } from 'react';
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaArrowDown } from 'react-icons/fa';
|
||||
import { FaExclamation } from 'react-icons/fa';
|
||||
|
||||
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
|
||||
import { findSP } from '../../../../utils/windows';
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
import InlinePatchNotes from '../../../patchnotes/InlinePatchNotes';
|
||||
import WithSuspense from '../../../WithSuspense';
|
||||
@@ -95,21 +95,21 @@ export default function UpdaterSettings() {
|
||||
<Field
|
||||
onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined}
|
||||
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
|
||||
label="Updates"
|
||||
label="Decky Updates"
|
||||
description={
|
||||
versionInfo && (
|
||||
<span style={{ whiteSpace: 'pre-line' }}>{`Current version: ${versionInfo.current}\n${
|
||||
versionInfo.updatable ? `Latest version: ${versionInfo.remote?.tag_name}` : ''
|
||||
}`}</span>
|
||||
checkingForUpdates || versionInfo?.remote?.tag_name != versionInfo?.current || !versionInfo?.remote ? (
|
||||
''
|
||||
) : (
|
||||
<span>Up to date: running {versionInfo?.current}</span>
|
||||
)
|
||||
}
|
||||
icon={
|
||||
!versionInfo ? (
|
||||
<Spinner style={{ width: '1em', height: 20, display: 'block' }} />
|
||||
) : (
|
||||
<FaArrowDown style={{ display: 'block' }} />
|
||||
versionInfo?.remote &&
|
||||
versionInfo?.remote?.tag_name != versionInfo?.current && (
|
||||
<FaExclamation color="var(--gpColor-Yellow)" style={{ display: 'block' }} />
|
||||
)
|
||||
}
|
||||
childrenContainerWidth={'fixed'}
|
||||
>
|
||||
{updateProgress == -1 && !isLoaderUpdating ? (
|
||||
<DialogButton
|
||||
@@ -144,7 +144,7 @@ export default function UpdaterSettings() {
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
{versionInfo?.remote && (
|
||||
{versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current && (
|
||||
<InlinePatchNotes
|
||||
title={versionInfo?.remote.name}
|
||||
date={new Intl.RelativeTimeFormat('en-US', {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { DialogButton, Field, TextField, Toggle } from 'decky-frontend-lib';
|
||||
import {
|
||||
DialogBody,
|
||||
DialogButton,
|
||||
DialogControlsSection,
|
||||
DialogControlsSectionHeader,
|
||||
Field,
|
||||
TextField,
|
||||
Toggle,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useState } from 'react';
|
||||
import { FaShapes, FaTools } from 'react-icons/fa';
|
||||
|
||||
import { installFromURL } from '../../../../store';
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
import BranchSelect from './BranchSelect';
|
||||
import RemoteDebuggingSettings from './RemoteDebugging';
|
||||
import StoreSelect from './StoreSelect';
|
||||
import UpdaterSettings from './Updater';
|
||||
|
||||
@@ -16,34 +23,44 @@ export default function GeneralSettings({
|
||||
setIsDeveloper: (val: boolean) => void;
|
||||
}) {
|
||||
const [pluginURL, setPluginURL] = useState('');
|
||||
const { versionInfo } = useDeckyState();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<UpdaterSettings />
|
||||
<BranchSelect />
|
||||
<StoreSelect />
|
||||
<RemoteDebuggingSettings />
|
||||
<Field
|
||||
label="Developer mode"
|
||||
description={<span style={{ whiteSpace: 'pre-line' }}>Enables Decky's developer settings.</span>}
|
||||
icon={<FaTools style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={isDeveloper}
|
||||
onChange={(toggleValue) => {
|
||||
setIsDeveloper(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Manual plugin install"
|
||||
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
||||
icon={<FaShapes style={{ display: 'block' }} />}
|
||||
>
|
||||
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
||||
Install
|
||||
</DialogButton>
|
||||
</Field>
|
||||
</div>
|
||||
<DialogBody>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Updates</DialogControlsSectionHeader>
|
||||
<UpdaterSettings />
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Beta Participation</DialogControlsSectionHeader>
|
||||
<BranchSelect />
|
||||
<StoreSelect />
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Other</DialogControlsSectionHeader>
|
||||
<Field label="Enable Developer Mode">
|
||||
<Toggle
|
||||
value={isDeveloper}
|
||||
onChange={(toggleValue) => {
|
||||
setIsDeveloper(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Install plugin from URL"
|
||||
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
||||
>
|
||||
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
||||
Install
|
||||
</DialogButton>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>About</DialogControlsSectionHeader>
|
||||
<Field label="Decky Version" focusable={true}>
|
||||
<div style={{ color: 'var(--gpSystemLighterGrey)' }}>{versionInfo?.current}</div>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,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 { FaDownload, FaEllipsisH } from 'react-icons/fa';
|
||||
|
||||
@@ -21,46 +29,52 @@ export default function PluginList() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ul style={{ listStyleType: 'none' }}>
|
||||
{plugins.map(({ name, version }) => {
|
||||
const update = updates?.get(name);
|
||||
return (
|
||||
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
|
||||
<span>
|
||||
{name} {version}
|
||||
</span>
|
||||
<Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
|
||||
{update && (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => requestPluginInstall(name, update)}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
Update to {update.name}
|
||||
<FaDownload style={{ paddingLeft: '2rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
)}
|
||||
<DialogButton
|
||||
style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }}
|
||||
onClick={(e: MouseEvent) =>
|
||||
showContextMenu(
|
||||
<Menu label="Plugin Actions">
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name, version)}>
|
||||
Reload
|
||||
</MenuItem>
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem>
|
||||
</Menu>,
|
||||
e.currentTarget ?? window,
|
||||
)
|
||||
}
|
||||
>
|
||||
<FaEllipsisH />
|
||||
</DialogButton>
|
||||
</Focusable>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<DialogBody>
|
||||
<DialogControlsSection>
|
||||
<ul style={{ listStyleType: 'none', padding: '0' }}>
|
||||
{plugins.map(({ name, version }) => {
|
||||
const update = updates?.get(name);
|
||||
return (
|
||||
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
|
||||
<span>
|
||||
{name} <span style={{ opacity: '50%' }}>{'(' + version + ')'}</span>
|
||||
</span>
|
||||
<Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
|
||||
{update && (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => requestPluginInstall(name, update)}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
Update to {update.name}
|
||||
<FaDownload style={{ paddingLeft: '2rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
)}
|
||||
<DialogButton
|
||||
style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }}
|
||||
onClick={(e: MouseEvent) =>
|
||||
showContextMenu(
|
||||
<Menu label="Plugin Actions">
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name, version)}>
|
||||
Reload
|
||||
</MenuItem>
|
||||
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>
|
||||
Uninstall
|
||||
</MenuItem>
|
||||
</Menu>,
|
||||
e.currentTarget ?? window,
|
||||
)
|
||||
}
|
||||
>
|
||||
<FaEllipsisH />
|
||||
</DialogButton>
|
||||
</Focusable>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</DialogControlsSection>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import Logger from '../../logger';
|
||||
import { StorePlugin, getPluginList } from '../../store';
|
||||
import PluginCard from './PluginCard';
|
||||
|
||||
const logger = new Logger('FilePicker');
|
||||
const logger = new Logger('Store');
|
||||
|
||||
const StorePage: FC<{}> = () => {
|
||||
const [currentTabRoute, setCurrentTabRoute] = useState<string>('browse');
|
||||
|
||||
@@ -180,6 +180,7 @@ class PluginLoader extends Logger {
|
||||
}
|
||||
|
||||
public unloadPlugin(name: string) {
|
||||
console.log('Plugin List: ', this.plugins);
|
||||
const plugin = this.plugins.find((plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''));
|
||||
plugin?.onDismount?.();
|
||||
this.plugins = this.plugins.filter((p) => p !== plugin);
|
||||
@@ -335,7 +336,7 @@ class PluginLoader extends Logger {
|
||||
fetchNoCors(url: string, request: any = {}) {
|
||||
let args = { method: 'POST', headers: {} };
|
||||
const req = { ...args, ...request, url, data: request.body };
|
||||
req?.body && delete req.body
|
||||
req?.body && delete req.body;
|
||||
return this.callServerMethod('http_request', req);
|
||||
},
|
||||
executeInTab(tab: string, runAsync: boolean, code: string) {
|
||||
|
||||
@@ -123,11 +123,9 @@ class RouterHook extends Logger {
|
||||
this.wrapperPatch = afterPatch(this.gamepadWrapper, 'render', (_: any, ret: any) => {
|
||||
if (ret?.props?.children?.props?.children?.length == 5 || ret?.props?.children?.props?.children?.length == 4) {
|
||||
const idx = ret?.props?.children?.props?.children?.length == 4 ? 1 : 2;
|
||||
if (
|
||||
ret.props.children.props.children[idx]?.props?.children?.[0]?.type?.type
|
||||
?.toString()
|
||||
?.includes('GamepadUI.Settings.Root()')
|
||||
) {
|
||||
const potentialSettingsRootString =
|
||||
ret.props.children.props.children[idx]?.props?.children?.[0]?.type?.type?.toString() || '';
|
||||
if (potentialSettingsRootString?.includes('Settings.Root()')) {
|
||||
if (!this.router) {
|
||||
this.router = ret.props.children.props.children[idx]?.props?.children?.[0]?.type;
|
||||
this.routerPatch = afterPatch(this.router, 'type', (_: any, ret: any) => {
|
||||
|
||||
@@ -7,6 +7,6 @@ export function deinitSteamFixes() {
|
||||
}
|
||||
|
||||
export async function initSteamFixes() {
|
||||
fixes.push(reloadFix());
|
||||
fixes.push(await reloadFix());
|
||||
fixes.push(await restartFix());
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { getFocusNavController, sleep } from 'decky-frontend-lib';
|
||||
|
||||
import Logger from '../logger';
|
||||
|
||||
const logger = new Logger('ReloadSteamFix');
|
||||
|
||||
export default function reloadFix() {
|
||||
declare global {
|
||||
var GamepadNavTree: any;
|
||||
}
|
||||
|
||||
export default async function reloadFix() {
|
||||
// Hack to unbreak the ui when reloading it
|
||||
if (window.FocusNavController?.m_rgAllContexts?.length == 0) {
|
||||
await sleep(4000);
|
||||
if (getFocusNavController()?.m_rgAllContexts?.length == 0) {
|
||||
SteamClient.URL.ExecuteSteamURL('steam://open/settings');
|
||||
logger.log('Applied UI reload fix.');
|
||||
}
|
||||
|
||||
@@ -4,13 +4,6 @@ import Logger from '../logger';
|
||||
|
||||
const logger = new Logger('RestartSteamFix');
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
SteamClient: any;
|
||||
appDetailsStore: any;
|
||||
}
|
||||
}
|
||||
|
||||
let patch: Patch;
|
||||
|
||||
function rePatch() {
|
||||
|
||||
@@ -128,22 +128,23 @@ class TabsHook extends Logger {
|
||||
let deckyTabAmount = existingTabs.reduce((prev: any, cur: any) => (cur.decky ? prev + 1 : prev), 0);
|
||||
if (deckyTabAmount == this.tabs.length) {
|
||||
for (let tab of existingTabs) {
|
||||
if (tab?.decky) tab.panel.props.setter[0](visible);
|
||||
if (tab?.decky && tab?.qAMVisibilitySetter) tab?.qAMVisibilitySetter(visible);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const { title, icon, content, id } of this.tabs) {
|
||||
existingTabs.push({
|
||||
const tab: any = {
|
||||
key: id,
|
||||
title,
|
||||
tab: icon,
|
||||
decky: true,
|
||||
panel: (
|
||||
<QuickAccessVisibleStateProvider initial={visible} setter={[]}>
|
||||
{content}
|
||||
</QuickAccessVisibleStateProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
tab.panel = (
|
||||
<QuickAccessVisibleStateProvider initial={visible} tab={tab}>
|
||||
{content}
|
||||
</QuickAccessVisibleStateProvider>
|
||||
);
|
||||
existingTabs.push(tab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,14 @@ class Toaster extends Logger {
|
||||
let instance: any;
|
||||
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
|
||||
const findToasterRoot = (currentNode: any, iters: number): any => {
|
||||
if (iters >= 50) {
|
||||
// currently 40
|
||||
if (iters >= 65) {
|
||||
// currently 65
|
||||
return null;
|
||||
}
|
||||
if (currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder')) {
|
||||
if (
|
||||
currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder') ||
|
||||
currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPopup')
|
||||
) {
|
||||
this.log(`Toaster root was found in ${iters} recursion cycles`);
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export function findSP(): Window {
|
||||
// old (SP as host)
|
||||
if (document.title == 'SP') return window;
|
||||
// new (SP as popup)
|
||||
return FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.find((x: any) => x.m_ID == 'root_1_').Root
|
||||
.Element.ownerDocument.defaultView;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"target": "ES2020",
|
||||
"target": "ES2021",
|
||||
"jsx": "react",
|
||||
"jsxFactory": "window.SP_REACT.createElement",
|
||||
"jsxFragmentFactory": "window.SP_REACT.Fragment",
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
This module exposes various constants and helpers useful for decky plugins.
|
||||
|
||||
* Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
* Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
* Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`.
|
||||
|
||||
Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended.
|
||||
|
||||
Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`.
|
||||
|
||||
A logging facility `logger` is available which writes to the recommended location.
|
||||
"""
|
||||
|
||||
__version__ = '0.1.0'
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
"""
|
||||
Constants
|
||||
"""
|
||||
|
||||
HOME: str = os.getenv("HOME", default="")
|
||||
"""
|
||||
The home directory of the effective user running the process.
|
||||
Environment variable: `HOME`.
|
||||
If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in.
|
||||
e.g.: `/home/deck`
|
||||
"""
|
||||
|
||||
USER: str = os.getenv("USER", default="")
|
||||
"""
|
||||
The effective username running the process.
|
||||
Environment variable: `USER`.
|
||||
It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in.
|
||||
e.g.: `deck`
|
||||
"""
|
||||
|
||||
DECKY_VERSION: str = os.getenv("DECKY_VERSION", default="")
|
||||
"""
|
||||
The version of the decky loader.
|
||||
Environment variable: `DECKY_VERSION`.
|
||||
e.g.: `v2.5.0-pre1`
|
||||
"""
|
||||
|
||||
DECKY_USER: str = os.getenv("DECKY_USER", default="")
|
||||
"""
|
||||
The user whose home decky resides in.
|
||||
Environment variable: `DECKY_USER`.
|
||||
e.g.: `deck`
|
||||
"""
|
||||
|
||||
DECKY_USER_HOME: str = os.getenv("DECKY_USER_HOME", default="")
|
||||
"""
|
||||
The home of the user where decky resides in.
|
||||
Environment variable: `DECKY_USER_HOME`.
|
||||
e.g.: `/home/deck`
|
||||
"""
|
||||
|
||||
DECKY_HOME: str = os.getenv("DECKY_HOME", default="")
|
||||
"""
|
||||
The root of the decky folder.
|
||||
Environment variable: `DECKY_HOME`.
|
||||
e.g.: `/home/deck/homebrew`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_SETTINGS_DIR: str = os.getenv(
|
||||
"DECKY_PLUGIN_SETTINGS_DIR", default="")
|
||||
"""
|
||||
The recommended path in which to store configuration files (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
e.g.: `/home/deck/homebrew/settings/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_RUNTIME_DIR: str = os.getenv(
|
||||
"DECKY_PLUGIN_RUNTIME_DIR", default="")
|
||||
"""
|
||||
The recommended path in which to store runtime data (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
e.g.: `/home/deck/homebrew/data/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_LOG_DIR: str = os.getenv("DECKY_PLUGIN_LOG_DIR", default="")
|
||||
"""
|
||||
The recommended path in which to store persistent logs (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_LOG_DIR`.
|
||||
e.g.: `/home/deck/homebrew/logs/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_DIR: str = os.getenv("DECKY_PLUGIN_DIR", default="")
|
||||
"""
|
||||
The root of the plugin's directory.
|
||||
Environment variable: `DECKY_PLUGIN_DIR`.
|
||||
e.g.: `/home/deck/homebrew/plugins/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_NAME: str = os.getenv("DECKY_PLUGIN_NAME", default="")
|
||||
"""
|
||||
The name of the plugin as specified in the 'plugin.json'.
|
||||
Environment variable: `DECKY_PLUGIN_NAME`.
|
||||
e.g.: `Example Plugin`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_VERSION: str = os.getenv("DECKY_PLUGIN_VERSION", default="")
|
||||
"""
|
||||
The version of the plugin as specified in the 'package.json'.
|
||||
Environment variable: `DECKY_PLUGIN_VERSION`.
|
||||
e.g.: `0.0.1`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_AUTHOR: str = os.getenv("DECKY_PLUGIN_AUTHOR", default="")
|
||||
"""
|
||||
The author of the plugin as specified in the 'plugin.json'.
|
||||
Environment variable: `DECKY_PLUGIN_AUTHOR`.
|
||||
e.g.: `John Doe`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_LOG: str = os.path.join(DECKY_PLUGIN_LOG_DIR, "plugin.log")
|
||||
"""
|
||||
The path to the plugin's main logfile.
|
||||
Environment variable: `DECKY_PLUGIN_LOG`.
|
||||
e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log`
|
||||
"""
|
||||
|
||||
"""
|
||||
Migration helpers
|
||||
"""
|
||||
|
||||
|
||||
def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories to a new location and remove old locations.
|
||||
Specified files will be migrated to `target_dir`.
|
||||
Specified directories will have their contents recursively migrated to `target_dir`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
file_map: dict[str, str] = {}
|
||||
for f in files_or_directories:
|
||||
if not os.path.exists(f):
|
||||
file_map[f] = ""
|
||||
continue
|
||||
if os.path.isdir(f):
|
||||
src_dir = f
|
||||
src_file = "."
|
||||
file_map[f] = target_dir
|
||||
else:
|
||||
src_dir = os.path.dirname(f)
|
||||
src_file = os.path.basename(f)
|
||||
file_map[f] = os.path.join(target_dir, src_file)
|
||||
subprocess.run(["sh", "-c", "mkdir -p \"$3\"; tar -cf - -C \"$1\" \"$2\" | tar -xf - -C \"$3\" && rm -rf \"$4\"",
|
||||
"_", src_dir, src_file, target_dir, f])
|
||||
return file_map
|
||||
|
||||
|
||||
def migrate_settings(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin settings to the recommended location and remove old locations.
|
||||
Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
return migrate_any(DECKY_PLUGIN_SETTINGS_DIR, *files_or_directories)
|
||||
|
||||
|
||||
def migrate_runtime(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations
|
||||
Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
return migrate_any(DECKY_PLUGIN_RUNTIME_DIR, *files_or_directories)
|
||||
|
||||
|
||||
def migrate_logs(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin logs to the recommended location and remove old locations.
|
||||
Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
return migrate_any(DECKY_PLUGIN_LOG_DIR, *files_or_directories)
|
||||
|
||||
|
||||
"""
|
||||
Logging
|
||||
"""
|
||||
|
||||
logging.basicConfig(filename=DECKY_PLUGIN_LOG,
|
||||
format='[%(asctime)s][%(levelname)s]: %(message)s',
|
||||
force=True)
|
||||
logger: logging.Logger = logging.getLogger()
|
||||
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
|
||||
|
||||
logger.setLevel(logging.INFO)
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
This module exposes various constants and helpers useful for decky plugins.
|
||||
|
||||
* Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
* Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
* Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`.
|
||||
|
||||
Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended.
|
||||
|
||||
Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`.
|
||||
|
||||
A logging facility `logger` is available which writes to the recommended location.
|
||||
"""
|
||||
|
||||
__version__ = '0.1.0'
|
||||
|
||||
import logging
|
||||
|
||||
"""
|
||||
Constants
|
||||
"""
|
||||
|
||||
HOME: str
|
||||
"""
|
||||
The home directory of the effective user running the process.
|
||||
Environment variable: `HOME`.
|
||||
If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in.
|
||||
e.g.: `/home/deck`
|
||||
"""
|
||||
|
||||
USER: str
|
||||
"""
|
||||
The effective username running the process.
|
||||
Environment variable: `USER`.
|
||||
It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in.
|
||||
e.g.: `deck`
|
||||
"""
|
||||
|
||||
DECKY_VERSION: str
|
||||
"""
|
||||
The version of the decky loader.
|
||||
Environment variable: `DECKY_VERSION`.
|
||||
e.g.: `v2.5.0-pre1`
|
||||
"""
|
||||
|
||||
DECKY_USER: str
|
||||
"""
|
||||
The user whose home decky resides in.
|
||||
Environment variable: `DECKY_USER`.
|
||||
e.g.: `deck`
|
||||
"""
|
||||
|
||||
DECKY_USER_HOME: str
|
||||
"""
|
||||
The home of the user where decky resides in.
|
||||
Environment variable: `DECKY_USER_HOME`.
|
||||
e.g.: `/home/deck`
|
||||
"""
|
||||
|
||||
DECKY_HOME: str
|
||||
"""
|
||||
The root of the decky folder.
|
||||
Environment variable: `DECKY_HOME`.
|
||||
e.g.: `/home/deck/homebrew`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_SETTINGS_DIR: str
|
||||
"""
|
||||
The recommended path in which to store configuration files (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
e.g.: `/home/deck/homebrew/settings/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_RUNTIME_DIR: str
|
||||
"""
|
||||
The recommended path in which to store runtime data (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
e.g.: `/home/deck/homebrew/data/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_LOG_DIR: str
|
||||
"""
|
||||
The recommended path in which to store persistent logs (created automatically).
|
||||
Environment variable: `DECKY_PLUGIN_LOG_DIR`.
|
||||
e.g.: `/home/deck/homebrew/logs/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_DIR: str
|
||||
"""
|
||||
The root of the plugin's directory.
|
||||
Environment variable: `DECKY_PLUGIN_DIR`.
|
||||
e.g.: `/home/deck/homebrew/plugins/decky-plugin-template`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_NAME: str
|
||||
"""
|
||||
The name of the plugin as specified in the 'plugin.json'.
|
||||
Environment variable: `DECKY_PLUGIN_NAME`.
|
||||
e.g.: `Example Plugin`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_VERSION: str
|
||||
"""
|
||||
The version of the plugin as specified in the 'package.json'.
|
||||
Environment variable: `DECKY_PLUGIN_VERSION`.
|
||||
e.g.: `0.0.1`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_AUTHOR: str
|
||||
"""
|
||||
The author of the plugin as specified in the 'plugin.json'.
|
||||
Environment variable: `DECKY_PLUGIN_AUTHOR`.
|
||||
e.g.: `John Doe`
|
||||
"""
|
||||
|
||||
DECKY_PLUGIN_LOG: str
|
||||
"""
|
||||
The path to the plugin's main logfile.
|
||||
Environment variable: `DECKY_PLUGIN_LOG`.
|
||||
e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log`
|
||||
"""
|
||||
|
||||
"""
|
||||
Migration helpers
|
||||
"""
|
||||
|
||||
|
||||
def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories to a new location and remove old locations.
|
||||
Specified files will be migrated to `target_dir`.
|
||||
Specified directories will have their contents recursively migrated to `target_dir`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
|
||||
|
||||
def migrate_settings(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin settings to the recommended location and remove old locations.
|
||||
Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
|
||||
|
||||
def migrate_runtime(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations
|
||||
Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
|
||||
|
||||
def migrate_logs(*files_or_directories: str) -> dict[str, str]:
|
||||
"""
|
||||
Migrate files and directories relating to plugin logs to the recommended location and remove old locations.
|
||||
Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`.
|
||||
Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`.
|
||||
|
||||
Returns the mapping of old -> new location.
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
Logging
|
||||
"""
|
||||
|
||||
logger: logging.Logger
|
||||
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
|
||||
Reference in New Issue
Block a user