mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e5d991c8d | |||
| 0fe3282828 | |||
| 335d38e12b | |||
| d762860eac | |||
| fdbc508fa8 | |||
| 81fbd0f83f | |||
| 8810a014f3 | |||
| 385552451b | |||
| c2c9d11c66 | |||
| 0474095a40 | |||
| 346f80beb3 | |||
| 2a6bf75f02 | |||
| f73918c902 | |||
| ea35af2050 | |||
| 6232e3da58 | |||
| 35e46f9ccb | |||
| 2b9a80c151 | |||
| a90ed38c89 | |||
| 3653cf5640 | |||
| 0db45ca71e | |||
| 16681fabb5 | |||
| c210523a22 |
@@ -31,7 +31,7 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Build PluginLoader
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Print input
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ class PluginBrowser:
|
||||
pluginBinPath = path.join(pluginBasePath, 'bin')
|
||||
|
||||
if access(packageJsonPath, R_OK):
|
||||
with open(packageJsonPath, 'r') as f:
|
||||
with open(packageJsonPath, "r", encoding="utf-8") as f:
|
||||
packageJson = json.load(f)
|
||||
if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0:
|
||||
# create bin directory if needed.
|
||||
@@ -93,7 +93,7 @@ class PluginBrowser:
|
||||
def find_plugin_folder(self, name):
|
||||
for folder in listdir(self.plugin_path):
|
||||
try:
|
||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), 'r') as f:
|
||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), "r", encoding="utf-8") as f:
|
||||
plugin = json.load(f)
|
||||
|
||||
if plugin['name'] == name:
|
||||
|
||||
+3
-3
@@ -118,7 +118,7 @@ class Loader:
|
||||
def handle_frontend_bundle(self, request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), 'r') 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")
|
||||
|
||||
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):
|
||||
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()
|
||||
ret = f"""
|
||||
<script src="/legacy/library.js"></script>
|
||||
@@ -202,7 +202,7 @@ class Loader:
|
||||
self.logger.info(path)
|
||||
ret = ""
|
||||
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()
|
||||
|
||||
return web.Response(text=ret)
|
||||
|
||||
+9
-9
@@ -168,15 +168,15 @@ class PluginManager:
|
||||
async def inject_javascript(self, tab: Tab, first=False, request=None):
|
||||
logger.info("Loading Decky frontend!")
|
||||
try:
|
||||
# if first:
|
||||
# if await tab.has_global_var("deckyHasLoaded", False):
|
||||
# tabs = await get_tabs()
|
||||
# for t in tabs:
|
||||
# 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)
|
||||
if first:
|
||||
if await tab.has_global_var("deckyHasLoaded", False):
|
||||
tabs = await get_tabs()
|
||||
for t in tabs:
|
||||
if not t.title or (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(() => location.reload(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}", False, False, False)
|
||||
except:
|
||||
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
|
||||
pass
|
||||
|
||||
+5
-5
@@ -27,9 +27,9 @@ class PluginWrapper:
|
||||
|
||||
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")):
|
||||
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"]
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ class PluginWrapper:
|
||||
d["res"] = str(e)
|
||||
d["success"] = False
|
||||
finally:
|
||||
writer.write((dumps(d)+"\n").encode("utf-8"))
|
||||
writer.write((dumps(d, ensure_ascii=False)+"\n").encode("utf-8"))
|
||||
await writer.drain()
|
||||
|
||||
async def _open_socket_if_not_exists(self):
|
||||
@@ -140,7 +140,7 @@ class PluginWrapper:
|
||||
return
|
||||
async def _(self):
|
||||
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()
|
||||
self.writer.close()
|
||||
get_event_loop().create_task(_(self))
|
||||
@@ -151,7 +151,7 @@ class PluginWrapper:
|
||||
async with self.method_call_lock:
|
||||
if await self._open_socket_if_not_exists():
|
||||
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()
|
||||
line = bytearray()
|
||||
while True:
|
||||
|
||||
+4
-4
@@ -35,22 +35,22 @@ class SettingsManager:
|
||||
self.settings = {}
|
||||
|
||||
try:
|
||||
open(self.path, "x")
|
||||
open(self.path, "x", encoding="utf-8")
|
||||
except FileExistsError as e:
|
||||
self.read()
|
||||
pass
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
with open(self.path, "r") as file:
|
||||
with open(self.path, "r", encoding="utf-8") as file:
|
||||
self.settings = load(file)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
def commit(self):
|
||||
with open(self.path, "w+") as file:
|
||||
dump(self.settings, file, indent=4)
|
||||
with open(self.path, "w+", encoding="utf-8") as file:
|
||||
dump(self.settings, file, indent=4, ensure_ascii=False)
|
||||
|
||||
def getSetting(self, key, default):
|
||||
return self.settings.get(key, default)
|
||||
|
||||
+4
-4
@@ -32,7 +32,7 @@ class Updater:
|
||||
self.allRemoteVers = None
|
||||
try:
|
||||
logger.info(getcwd())
|
||||
with open(path.join(getcwd(), ".loader.version"), 'r') as version_file:
|
||||
with open(path.join(getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
|
||||
self.localVer = version_file.readline().replace("\n", "")
|
||||
except:
|
||||
self.localVer = False
|
||||
@@ -159,10 +159,10 @@ class Updater:
|
||||
out.write(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error at %s", exc_info=e)
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), 'r') 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_data.replace("${HOMEBREW_FOLDER}", "/home/"+helpers.get_user()+"/homebrew")
|
||||
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)
|
||||
|
||||
logger.debug("Saved service file")
|
||||
@@ -191,7 +191,7 @@ class Updater:
|
||||
self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False))
|
||||
progress = new_progress
|
||||
|
||||
with open(path.join(getcwd(), ".loader.version"), "w") as out:
|
||||
with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
|
||||
out.write(version)
|
||||
|
||||
call(['chmod', '+x', path.join(getcwd(), "PluginLoader")])
|
||||
|
||||
@@ -81,10 +81,11 @@ class Utilities:
|
||||
async def http_request(self, method="", url="", **kwargs):
|
||||
async with ClientSession() as web:
|
||||
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs)
|
||||
text = await res.text()
|
||||
return {
|
||||
"status": res.status,
|
||||
"headers": dict(res.headers),
|
||||
"body": await res.text()
|
||||
"body": text
|
||||
}
|
||||
|
||||
async def ping(self, **kwargs):
|
||||
|
||||
Vendored
+1
@@ -40,6 +40,7 @@ User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
[Install]
|
||||
|
||||
Vendored
+1
@@ -40,6 +40,7 @@ User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
|
||||
+2
-1
@@ -8,7 +8,8 @@ User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Vendored
+2
-1
@@ -8,7 +8,8 @@ User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "^3.7.14",
|
||||
"decky-frontend-lib": "^3.18.4",
|
||||
"react-file-icon": "^1.2.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
|
||||
Generated
+4
-4
@@ -10,7 +10,7 @@ specifiers:
|
||||
'@types/react-file-icon': ^1.0.1
|
||||
'@types/react-router': 5.1.18
|
||||
'@types/webpack': ^5.28.0
|
||||
decky-frontend-lib: ^3.7.14
|
||||
decky-frontend-lib: ^3.18.4
|
||||
husky: ^8.0.1
|
||||
import-sort-style-module: ^6.0.0
|
||||
inquirer: ^8.2.4
|
||||
@@ -30,7 +30,7 @@ specifiers:
|
||||
typescript: ^4.7.4
|
||||
|
||||
dependencies:
|
||||
decky-frontend-lib: 3.7.14
|
||||
decky-frontend-lib: 3.18.4
|
||||
react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
|
||||
react-icons: 4.4.0_react@16.14.0
|
||||
react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u
|
||||
@@ -944,8 +944,8 @@ packages:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
/decky-frontend-lib/3.7.14:
|
||||
resolution: {integrity: sha512-ShAoP3VqiwkJYukDBHsOF9fk7wYw0VaKpHw6j9WdzLxwZwBcg0J7QBNIFYP3nfA0UgEwSJVEg/22kONiplipmA==}
|
||||
/decky-frontend-lib/3.18.4:
|
||||
resolution: {integrity: sha512-i3TAe3RJtT1TK0rJgW9Ek5jxMWZRCYLDvqHDylGVieUvuyI7c8X+cogz30pP4cqeGOaA1d/MxBEbhlpD3JhVvg==}
|
||||
dev: false
|
||||
|
||||
/decode-named-character-reference/1.0.2:
|
||||
|
||||
+13
-16
@@ -1,30 +1,27 @@
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
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 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 = [
|
||||
"THIS_IS_UNDEFINED",
|
||||
"EVAL"
|
||||
];
|
||||
const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL'];
|
||||
|
||||
export default defineConfig({
|
||||
input: 'src/index.tsx',
|
||||
plugins: [
|
||||
del({ targets: "../backend/static/*", force: true }),
|
||||
del({ targets: '../backend/static/*', force: true }),
|
||||
commonjs(),
|
||||
nodeResolve(),
|
||||
externalGlobals({
|
||||
react: 'SP_REACT',
|
||||
'react-dom': 'SP_REACTDOM',
|
||||
// hack to shut up react-markdown
|
||||
'process': '{cwd: () => {}}',
|
||||
'path': '{dirname: () => {}, join: () => {}, basename: () => {}, extname: () => {}}',
|
||||
'url': '{fileURLToPath: (f) => f}'
|
||||
process: '{cwd: () => {}}',
|
||||
path: '{dirname: () => {}, join: () => {}, basename: () => {}, extname: () => {}}',
|
||||
url: '{fileURLToPath: (f) => f}',
|
||||
}),
|
||||
typescript(),
|
||||
json(),
|
||||
@@ -38,11 +35,11 @@ export default defineConfig({
|
||||
dir: '../backend/static',
|
||||
format: 'esm',
|
||||
chunkFileNames: (chunkInfo) => {
|
||||
return 'chunk-[hash].js'
|
||||
}
|
||||
return 'chunk-[hash].js';
|
||||
},
|
||||
},
|
||||
onwarn: function ( message ) {
|
||||
if (hiddenWarnings.some(warning => message.code === warning)) return;
|
||||
onwarn: function (message, handleWarning) {
|
||||
if (hiddenWarnings.some((warning) => message.code === warning)) return;
|
||||
handleWarning(message);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,13 +14,13 @@ export class DeckyGlobalComponentsState {
|
||||
return { components: this._components };
|
||||
}
|
||||
|
||||
addComponent(path: string, component: FC) {
|
||||
this._components.set(path, component);
|
||||
addComponent(name: string, component: FC) {
|
||||
this._components.set(name, component);
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
removeComponent(path: string) {
|
||||
this._components.delete(path);
|
||||
removeComponent(name: string) {
|
||||
this._components.delete(name);
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ export class DeckyGlobalComponentsState {
|
||||
}
|
||||
|
||||
interface DeckyGlobalComponentsContext extends PublicDeckyGlobalComponentsState {
|
||||
addComponent(path: string, component: FC): void;
|
||||
removeComponent(path: string): void;
|
||||
addComponent(name: string, component: FC): void;
|
||||
removeComponent(name: string): void;
|
||||
}
|
||||
|
||||
const DeckyGlobalComponentsContext = createContext<DeckyGlobalComponentsContext>(null as any);
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { CustomMainMenuItem, ItemPatch, OverlayPatch } from 'decky-frontend-lib';
|
||||
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
interface PublicDeckyMenuState {
|
||||
items: Set<CustomMainMenuItem>;
|
||||
itemPatches: Map<string, Set<ItemPatch>>;
|
||||
overlayPatches: Set<OverlayPatch>;
|
||||
overlayComponents: Set<ReactNode>;
|
||||
}
|
||||
|
||||
export class DeckyMenuState {
|
||||
private _items = new Set<CustomMainMenuItem>();
|
||||
private _itemPatches = new Map<string, Set<ItemPatch>>();
|
||||
private _overlayPatches = new Set<OverlayPatch>();
|
||||
private _overlayComponents = new Set<ReactNode>();
|
||||
|
||||
public eventBus = new EventTarget();
|
||||
|
||||
publicState(): PublicDeckyMenuState {
|
||||
return {
|
||||
items: this._items,
|
||||
itemPatches: this._itemPatches,
|
||||
overlayPatches: this._overlayPatches,
|
||||
overlayComponents: this._overlayComponents,
|
||||
};
|
||||
}
|
||||
|
||||
addItem(item: CustomMainMenuItem) {
|
||||
this._items.add(item);
|
||||
this.notifyUpdate();
|
||||
return item;
|
||||
}
|
||||
|
||||
addPatch(path: string, patch: ItemPatch) {
|
||||
let patchList = this._itemPatches.get(path);
|
||||
if (!patchList) {
|
||||
patchList = new Set();
|
||||
this._itemPatches.set(path, patchList);
|
||||
}
|
||||
patchList.add(patch);
|
||||
this.notifyUpdate();
|
||||
return patch;
|
||||
}
|
||||
|
||||
addOverlayPatch(patch: OverlayPatch) {
|
||||
this._overlayPatches.add(patch);
|
||||
this.notifyUpdate();
|
||||
return patch;
|
||||
}
|
||||
|
||||
addOverlayComponent(component: ReactNode) {
|
||||
this._overlayComponents.add(component);
|
||||
this.notifyUpdate();
|
||||
return component;
|
||||
}
|
||||
|
||||
removePatch(path: string, patch: ItemPatch) {
|
||||
const patchList = this._itemPatches.get(path);
|
||||
patchList?.delete(patch);
|
||||
if (patchList?.size == 0) {
|
||||
this._itemPatches.delete(path);
|
||||
}
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
removeItem(item: CustomMainMenuItem) {
|
||||
this._items.delete(item);
|
||||
this.notifyUpdate();
|
||||
return item;
|
||||
}
|
||||
|
||||
removeOverlayPatch(patch: OverlayPatch) {
|
||||
this._overlayPatches.delete(patch);
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
removeOverlayComponent(component: ReactNode) {
|
||||
this._overlayComponents.delete(component);
|
||||
this.notifyUpdate();
|
||||
}
|
||||
|
||||
private notifyUpdate() {
|
||||
this.eventBus.dispatchEvent(new Event('update'));
|
||||
}
|
||||
}
|
||||
|
||||
interface DeckyMenuStateContext extends PublicDeckyMenuState {
|
||||
addItem: DeckyMenuState['addItem'];
|
||||
addPatch: DeckyMenuState['addPatch'];
|
||||
addOverlayPatch: DeckyMenuState['addOverlayPatch'];
|
||||
addOverlayComponent: DeckyMenuState['addOverlayComponent'];
|
||||
removePatch: DeckyMenuState['removePatch'];
|
||||
removeOverlayPatch: DeckyMenuState['removeOverlayPatch'];
|
||||
removeOverlayComponent: DeckyMenuState['removeOverlayComponent'];
|
||||
removeItem: DeckyMenuState['removeItem'];
|
||||
}
|
||||
|
||||
const DeckyMenuStateContext = createContext<DeckyMenuStateContext>(null as any);
|
||||
|
||||
export const useDeckyMenuState = () => useContext(DeckyMenuStateContext);
|
||||
|
||||
interface Props {
|
||||
deckyMenuState: DeckyMenuState;
|
||||
}
|
||||
|
||||
export const DeckyMenuStateContextProvider: FC<Props> = ({ children, deckyMenuState }) => {
|
||||
const [publicDeckyMenuState, setPublicDeckyMenuState] = useState<PublicDeckyMenuState>({
|
||||
...deckyMenuState.publicState(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function onUpdate() {
|
||||
setPublicDeckyMenuState({ ...deckyMenuState.publicState() });
|
||||
}
|
||||
|
||||
deckyMenuState.eventBus.addEventListener('update', onUpdate);
|
||||
|
||||
return () => deckyMenuState.eventBus.removeEventListener('update', onUpdate);
|
||||
}, []);
|
||||
|
||||
const addItem = deckyMenuState.addItem.bind(deckyMenuState);
|
||||
const addPatch = deckyMenuState.addPatch.bind(deckyMenuState);
|
||||
const addOverlayPatch = deckyMenuState.addOverlayPatch.bind(deckyMenuState);
|
||||
const addOverlayComponent = deckyMenuState.addOverlayComponent.bind(deckyMenuState);
|
||||
const removePatch = deckyMenuState.removePatch.bind(deckyMenuState);
|
||||
const removeOverlayPatch = deckyMenuState.removeOverlayPatch.bind(deckyMenuState);
|
||||
const removeOverlayComponent = deckyMenuState.removeOverlayComponent.bind(deckyMenuState);
|
||||
const removeItem = deckyMenuState.removeItem.bind(deckyMenuState);
|
||||
|
||||
return (
|
||||
<DeckyMenuStateContext.Provider
|
||||
value={{
|
||||
...publicDeckyMenuState,
|
||||
addItem,
|
||||
addPatch,
|
||||
addOverlayPatch,
|
||||
addOverlayComponent,
|
||||
removePatch,
|
||||
removeOverlayPatch,
|
||||
removeOverlayComponent,
|
||||
removeItem,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DeckyMenuStateContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConfirmModal, QuickAccessTab, Router, Spinner, staticClasses } from 'decky-frontend-lib';
|
||||
import { ConfirmModal, Navigation, QuickAccessTab, Spinner, staticClasses } from 'decky-frontend-lib';
|
||||
import { FC, useState } from 'react';
|
||||
|
||||
interface PluginInstallModalProps {
|
||||
@@ -20,7 +20,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
|
||||
onOK={async () => {
|
||||
setLoading(true);
|
||||
await onOK();
|
||||
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
|
||||
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
|
||||
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
|
||||
}}
|
||||
onCancel={async () => {
|
||||
|
||||
@@ -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 {
|
||||
interface Window {
|
||||
@@ -10,36 +14,44 @@ declare global {
|
||||
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 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[]) => {
|
||||
try {
|
||||
const details = window.appDetailsStore.GetAppDetails(appid);
|
||||
console.log(details);
|
||||
logger.debug('game details', details);
|
||||
// strShortcutStartDir
|
||||
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));
|
||||
const pathArr = file.path.split('/');
|
||||
pathArr.pop();
|
||||
const folder = pathArr.join('/');
|
||||
window.SteamClient.Apps.SetShortcutStartDir(appid, JSON.stringify(folder));
|
||||
} 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() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const unlisten = History.listen(() => {
|
||||
if (window.SteamClient.Apps.PromptToChangeShortcut !== patch.patchedFunction) {
|
||||
rePatch();
|
||||
@@ -47,11 +59,11 @@ export default async function libraryPatch() {
|
||||
});
|
||||
|
||||
return () => {
|
||||
patch.unpatch();
|
||||
unlisten();
|
||||
patch.unpatch();
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error patching library file picker', e);
|
||||
logger.error('Error patching library file picker', e);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
DialogButton,
|
||||
Dropdown,
|
||||
Focusable,
|
||||
Navigation,
|
||||
QuickAccessTab,
|
||||
Router,
|
||||
SingleDropdownOption,
|
||||
SuspensefulImage,
|
||||
joinClassNames,
|
||||
@@ -38,8 +38,8 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
}}
|
||||
onCancel={(_: CustomEvent) => {
|
||||
if (containerRef.current!.querySelectorAll('* :focus').length === 0) {
|
||||
Router.NavigateBackOrOpenMenu();
|
||||
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
|
||||
Navigation.NavigateBack();
|
||||
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
|
||||
} else {
|
||||
containerRef.current!.focus();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Navigation,
|
||||
ReactRouter,
|
||||
Router,
|
||||
fakeRenderComponent,
|
||||
@@ -26,13 +27,20 @@ const logger = new Logger('DeveloperMode');
|
||||
|
||||
let removeSettingsObserver: () => void = () => {};
|
||||
|
||||
export function setShowValveInternal(show: boolean) {
|
||||
const settingsMod = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (typeof m[prop]?.settings?.bIsValveEmail !== 'undefined') return m[prop];
|
||||
export async function setShowValveInternal(show: boolean) {
|
||||
let settingsMod: any;
|
||||
while (!settingsMod) {
|
||||
settingsMod = findModuleChild((m) => {
|
||||
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) {
|
||||
removeSettingsObserver = settingsMod[
|
||||
@@ -74,13 +82,14 @@ export async function startup() {
|
||||
window.DFL = {
|
||||
findModuleChild,
|
||||
findModule,
|
||||
Navigation,
|
||||
Router,
|
||||
ReactRouter,
|
||||
ReactUtils: {
|
||||
fakeRenderComponent,
|
||||
findInReactTree,
|
||||
findInTree,
|
||||
},
|
||||
Router,
|
||||
ReactRouter,
|
||||
classes: {
|
||||
scrollClasses,
|
||||
staticClasses,
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import {
|
||||
CustomMainMenuItem,
|
||||
ItemPatch,
|
||||
MainMenuItem,
|
||||
OverlayPatch,
|
||||
afterPatch,
|
||||
findInReactTree,
|
||||
sleep,
|
||||
} from 'decky-frontend-lib';
|
||||
import { FC } from 'react';
|
||||
import { ReactNode, cloneElement, createElement } from 'react';
|
||||
|
||||
import { DeckyMenuState, DeckyMenuStateContextProvider, useDeckyMenuState } from './components/DeckyMenuState';
|
||||
import Logger from './logger';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__MENU_HOOK_INSTANCE: any;
|
||||
}
|
||||
}
|
||||
|
||||
class MenuHook extends Logger {
|
||||
private menuRenderer?: any;
|
||||
private originalRenderer?: any;
|
||||
private menuState: DeckyMenuState = new DeckyMenuState();
|
||||
|
||||
constructor() {
|
||||
super('MenuHook');
|
||||
|
||||
this.log('Initialized');
|
||||
window.__MENU_HOOK_INSTANCE?.deinit?.();
|
||||
window.__MENU_HOOK_INSTANCE = this;
|
||||
}
|
||||
|
||||
init() {
|
||||
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
|
||||
let outerMenuRoot: any;
|
||||
const findMenuRoot = (currentNode: any, iters: number): any => {
|
||||
if (iters >= 60) {
|
||||
// currently 54
|
||||
return null;
|
||||
}
|
||||
if (currentNode?.memoizedProps?.navID == 'MainNavMenuContainer') {
|
||||
this.log(`Menu root was found in ${iters} recursion cycles`);
|
||||
return currentNode;
|
||||
}
|
||||
if (currentNode.child) {
|
||||
let node = findMenuRoot(currentNode.child, iters + 1);
|
||||
if (node !== null) return node;
|
||||
}
|
||||
if (currentNode.sibling) {
|
||||
let node = findMenuRoot(currentNode.sibling, iters + 1);
|
||||
if (node !== null) return node;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
(async () => {
|
||||
outerMenuRoot = findMenuRoot(tree, 0);
|
||||
while (!outerMenuRoot) {
|
||||
this.error(
|
||||
'Failed to find Menu root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.',
|
||||
);
|
||||
await sleep(5000);
|
||||
outerMenuRoot = findMenuRoot(tree, 0);
|
||||
}
|
||||
this.log('found outermenuroot', outerMenuRoot);
|
||||
const menuRenderer = outerMenuRoot.return;
|
||||
this.menuRenderer = menuRenderer;
|
||||
this.originalRenderer = menuRenderer.type;
|
||||
let toReplace = new Map<string, ReactNode>();
|
||||
let alreadyPatched = new Map<string, { total: number; node: ReactNode }>();
|
||||
|
||||
let patchedInnerMenu: any;
|
||||
let overlayComponentManager: any;
|
||||
|
||||
const DeckyOverlayComponentManager = () => {
|
||||
const { overlayComponents } = useDeckyMenuState();
|
||||
|
||||
return <>{overlayComponents.values()}</>;
|
||||
};
|
||||
|
||||
const DeckyInnerMenuWrapper = (props: { innerProps: any }) => {
|
||||
const { overlayPatches } = useDeckyMenuState();
|
||||
|
||||
const rendererRet = this.originalRenderer(props.innerProps);
|
||||
|
||||
// Find the first array of children, this contains [mainmenu, overlay]
|
||||
const childArray = findInReactTree(rendererRet, (x) => x?.[0]?.type);
|
||||
|
||||
// Insert the overlay components manager
|
||||
if (!overlayComponentManager) {
|
||||
overlayComponentManager = <DeckyOverlayComponentManager />;
|
||||
}
|
||||
|
||||
childArray.push(overlayComponentManager);
|
||||
|
||||
// This must be cached in patchedInnerMenu to prevent re-renders
|
||||
if (patchedInnerMenu) {
|
||||
childArray[0].type = patchedInnerMenu;
|
||||
} else {
|
||||
afterPatch(childArray[0], 'type', (_, ret) => {
|
||||
const { itemPatches, items } = useDeckyMenuState();
|
||||
|
||||
const itemList = ret.props.children;
|
||||
|
||||
// Add custom menu items
|
||||
if (items.size > 0) {
|
||||
const button = findInReactTree(ret.props.children, (x) =>
|
||||
x?.type?.toString()?.includes('exactRouteMatch:'),
|
||||
);
|
||||
|
||||
const MenuItemComponent: FC<MainMenuItem> = button.type;
|
||||
|
||||
items.forEach((item) => {
|
||||
let realIndex = 0; // there are some non-item things in the array
|
||||
let count = 0;
|
||||
itemList.forEach((i: any) => {
|
||||
if (count == item.index) return;
|
||||
if (i?.type == MenuItemComponent) count++;
|
||||
realIndex++;
|
||||
});
|
||||
itemList.splice(realIndex, 0, createElement(MenuItemComponent, item));
|
||||
});
|
||||
}
|
||||
|
||||
// Apply and revert patches
|
||||
itemList.forEach((item: { props: MainMenuItem }, index: number) => {
|
||||
if (!item?.props?.route) return;
|
||||
const replaced = toReplace.get(item?.props?.route as string);
|
||||
if (replaced) {
|
||||
itemList[index] = replaced;
|
||||
toReplace.delete(item?.props.route as string);
|
||||
}
|
||||
if (item?.props?.route && (itemPatches.has(item.props.route as string) || itemPatches.has('*'))) {
|
||||
if (
|
||||
item?.props?.route &&
|
||||
alreadyPatched.has(item.props.route) &&
|
||||
alreadyPatched.get(item.props.route)?.total ==
|
||||
(itemPatches.get(item.props.route)?.size || 0) + (itemPatches.get('*')?.size || 0)
|
||||
) {
|
||||
const patched = alreadyPatched.get(item.props.route);
|
||||
this.debug('found already patched', patched);
|
||||
itemList[index] = patched?.node;
|
||||
return;
|
||||
}
|
||||
toReplace.set(item?.props?.route as string, itemList[index]);
|
||||
itemPatches.get(item.props.route as string)?.forEach((patch) => {
|
||||
const oType = itemList[index].type;
|
||||
itemList[index] = patch({
|
||||
...cloneElement(itemList[index]),
|
||||
type: (props: any) => createElement(oType, props),
|
||||
});
|
||||
});
|
||||
itemPatches.get('*')?.forEach((patch) => {
|
||||
const oType = itemList[index].type;
|
||||
itemList[index] = patch({
|
||||
...cloneElement(itemList[index]),
|
||||
type: (props: any) => createElement(oType, props),
|
||||
});
|
||||
});
|
||||
alreadyPatched.set(item.props.route, {
|
||||
total: (itemPatches.get(item.props.route)?.size || 0) + (itemPatches.get('*')?.size || 0),
|
||||
node: itemList[index],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
});
|
||||
patchedInnerMenu = childArray[0].type;
|
||||
}
|
||||
|
||||
// Apply patches to the overlay
|
||||
if (childArray[1]) {
|
||||
overlayPatches.forEach((patch) => (childArray[1] = patch(childArray[1])));
|
||||
}
|
||||
|
||||
return rendererRet;
|
||||
};
|
||||
|
||||
const DeckyOuterMenuWrapper = (props: any) => {
|
||||
return (
|
||||
<DeckyMenuStateContextProvider deckyMenuState={this.menuState}>
|
||||
<DeckyInnerMenuWrapper innerProps={props} />
|
||||
</DeckyMenuStateContextProvider>
|
||||
);
|
||||
};
|
||||
menuRenderer.type = DeckyOuterMenuWrapper;
|
||||
if (menuRenderer.alternate) {
|
||||
menuRenderer.alternate.type = menuRenderer.type;
|
||||
}
|
||||
this.log('Finished initial injection');
|
||||
})();
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.menuRenderer.type = this.originalRenderer;
|
||||
this.menuRenderer.alternate.type = this.menuRenderer.type;
|
||||
}
|
||||
|
||||
addItem(item: CustomMainMenuItem) {
|
||||
return this.menuState.addItem(item);
|
||||
}
|
||||
|
||||
addPatch(path: string, patch: ItemPatch) {
|
||||
return this.menuState.addPatch(path, patch);
|
||||
}
|
||||
|
||||
addOverlayPatch(patch: OverlayPatch) {
|
||||
return this.menuState.addOverlayPatch(patch);
|
||||
}
|
||||
|
||||
addOverlayComponent(component: ReactNode) {
|
||||
return this.menuState.addOverlayComponent(component);
|
||||
}
|
||||
|
||||
removePatch(path: string, patch: ItemPatch) {
|
||||
return this.menuState.removePatch(path, patch);
|
||||
}
|
||||
|
||||
removeItem(item: CustomMainMenuItem) {
|
||||
return this.menuState.removeItem(item);
|
||||
}
|
||||
|
||||
removeOverlayPatch(patch: OverlayPatch) {
|
||||
return this.menuState.removeOverlayPatch(patch);
|
||||
}
|
||||
|
||||
removeOverlayComponent(component: ReactNode) {
|
||||
return this.menuState.removeOverlayComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
export default MenuHook;
|
||||
@@ -1,13 +1,4 @@
|
||||
import {
|
||||
ConfirmModal,
|
||||
ModalRoot,
|
||||
Patch,
|
||||
QuickAccessTab,
|
||||
Router,
|
||||
showModal,
|
||||
sleep,
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { ConfirmModal, ModalRoot, QuickAccessTab, Router, showModal, sleep, staticClasses } from 'decky-frontend-lib';
|
||||
import { FC, lazy } from 'react';
|
||||
import { FaCog, FaExclamationCircle, FaPlug } from 'react-icons/fa';
|
||||
|
||||
@@ -19,8 +10,10 @@ import NotificationBadge from './components/NotificationBadge';
|
||||
import PluginView from './components/PluginView';
|
||||
import WithSuspense from './components/WithSuspense';
|
||||
import Logger from './logger';
|
||||
import MenuHook from './menu-hook';
|
||||
import { Plugin } from './plugin';
|
||||
import RouterHook from './router-hook';
|
||||
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
|
||||
import { checkForUpdates } from './store';
|
||||
import TabsHook from './tabs-hook';
|
||||
import OldTabsHook from './tabs-hook.old';
|
||||
@@ -33,13 +26,10 @@ const SettingsPage = lazy(() => import('./components/settings'));
|
||||
|
||||
const FilePicker = lazy(() => import('./components/modals/filepicker'));
|
||||
|
||||
declare global {
|
||||
interface Window {}
|
||||
}
|
||||
|
||||
class PluginLoader extends Logger {
|
||||
private plugins: Plugin[] = [];
|
||||
private tabsHook: TabsHook | OldTabsHook = document.title == 'SP' ? new OldTabsHook() : new TabsHook();
|
||||
private menuHook: MenuHook = new MenuHook();
|
||||
// private windowHook: WindowHook = new WindowHook();
|
||||
private routerHook: RouterHook = new RouterHook();
|
||||
public toaster: Toaster = new Toaster();
|
||||
@@ -49,11 +39,10 @@ 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.tabsHook.init();
|
||||
this.menuHook.init();
|
||||
this.log('Initialized');
|
||||
|
||||
const TabBadge = () => {
|
||||
@@ -92,6 +81,8 @@ class PluginLoader extends Logger {
|
||||
);
|
||||
});
|
||||
|
||||
initSteamFixes();
|
||||
|
||||
initFilepickerPatches();
|
||||
|
||||
this.updateVersion();
|
||||
@@ -184,8 +175,8 @@ class PluginLoader extends Logger {
|
||||
public deinit() {
|
||||
this.routerHook.removeRoute('/decky/store');
|
||||
this.routerHook.removeRoute('/decky/settings');
|
||||
deinitSteamFixes();
|
||||
deinitFilepickerPatches();
|
||||
this.focusWorkaroundPatch?.unpatch();
|
||||
}
|
||||
|
||||
public unloadPlugin(name: string) {
|
||||
@@ -322,6 +313,7 @@ class PluginLoader extends Logger {
|
||||
|
||||
createPluginAPI(pluginName: string) {
|
||||
return {
|
||||
menuHook: this.menuHook,
|
||||
routerHook: this.routerHook,
|
||||
toaster: this.toaster,
|
||||
callServerMethod: this.callServerMethod,
|
||||
|
||||
@@ -120,6 +120,8 @@ class RouterHook extends Logger {
|
||||
return <>{renderedComponents}</>;
|
||||
};
|
||||
|
||||
let globalComponents: any;
|
||||
|
||||
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;
|
||||
@@ -143,11 +145,17 @@ class RouterHook extends Logger {
|
||||
this.memoizedRouter = memo(this.router.type);
|
||||
this.memoizedRouter.isDeckyRouter = true;
|
||||
}
|
||||
ret.props.children.props.children.push(
|
||||
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
|
||||
<DeckyGlobalComponentsWrapper />
|
||||
</DeckyGlobalComponentsStateContextProvider>,
|
||||
);
|
||||
|
||||
if (!globalComponents) {
|
||||
globalComponents = (
|
||||
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
|
||||
<DeckyGlobalComponentsWrapper />
|
||||
</DeckyGlobalComponentsStateContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
ret.props.children.props.children.push(globalComponents);
|
||||
|
||||
ret.props.children.props.children[idx].props.children[0].type = this.memoizedRouter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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 () => {};
|
||||
}
|
||||
@@ -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 () => {};
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export enum Store {
|
||||
export interface StorePluginVersion {
|
||||
name: string;
|
||||
hash: string;
|
||||
artifact: string | undefined | null;
|
||||
}
|
||||
|
||||
export interface StorePlugin {
|
||||
@@ -73,9 +74,11 @@ export async function installFromURL(url: string) {
|
||||
}
|
||||
|
||||
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) {
|
||||
const artifactUrl =
|
||||
selectedVer.artifact ?? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`;
|
||||
await window.DeckyPluginLoader.callServerMethod('install_plugin', {
|
||||
name: plugin,
|
||||
artifact: `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`,
|
||||
artifact: artifactUrl,
|
||||
version: selectedVer.name,
|
||||
hash: selectedVer.hash,
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import Logger from './logger';
|
||||
declare global {
|
||||
interface Window {
|
||||
__TABS_HOOK_INSTANCE: any;
|
||||
securitystore: any;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +22,6 @@ class TabsHook extends Logger {
|
||||
tabs: Tab[] = [];
|
||||
private qAMRoot?: any;
|
||||
private qamPatch?: Patch;
|
||||
private unsubscribeSecurity?: () => void;
|
||||
|
||||
constructor() {
|
||||
super('TabsHook');
|
||||
@@ -114,7 +112,6 @@ class TabsHook extends Logger {
|
||||
deinit() {
|
||||
this.qamPatch?.unpatch();
|
||||
this.qAMRoot.return.alternate.type = this.qAMRoot.return.type;
|
||||
this.unsubscribeSecurity?.();
|
||||
}
|
||||
|
||||
add(tab: Tab) {
|
||||
|
||||
@@ -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 Toast from './components/Toast';
|
||||
@@ -7,6 +7,7 @@ import Logger from './logger';
|
||||
declare global {
|
||||
interface Window {
|
||||
__TOASTER_INSTANCE: any;
|
||||
settingsStore: any;
|
||||
NotificationStore: any;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +17,7 @@ class Toaster extends Logger {
|
||||
// private toasterState: DeckyToasterState = new DeckyToasterState();
|
||||
private node: any;
|
||||
private rNode: any;
|
||||
private settingsModule: any;
|
||||
private audioModule: any;
|
||||
private finishStartup?: () => void;
|
||||
private ready: Promise<void> = new Promise((res) => (this.finishStartup = res));
|
||||
private toasterPatch?: Patch;
|
||||
@@ -127,6 +128,17 @@ class Toaster extends Logger {
|
||||
this.rNode.stateNode.forceUpdate();
|
||||
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.finishStartup?.();
|
||||
}
|
||||
@@ -135,24 +147,31 @@ class Toaster extends Logger {
|
||||
// toast.duration = toast.duration || 5e3;
|
||||
// this.toasterState.addToast(toast);
|
||||
await this.ready;
|
||||
const settings = this.settingsModule?.settings;
|
||||
let toastData = {
|
||||
nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
|
||||
rtCreated: Date.now(),
|
||||
eType: 15,
|
||||
eType: toast.eType || 11,
|
||||
nToastDurationMS: toast.duration || (toast.duration = 5e3),
|
||||
data: toast,
|
||||
decky: true,
|
||||
};
|
||||
// @ts-ignore
|
||||
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 (
|
||||
(settings?.bDisableAllToasts && !toast.critical) ||
|
||||
(settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
|
||||
(window.settingsStore.settings.bDisableAllToasts && !toast.critical) ||
|
||||
(window.settingsStore.settings.bDisableToastsInGame &&
|
||||
!toast.critical &&
|
||||
window.NotificationStore.BIsUserInGame())
|
||||
)
|
||||
return;
|
||||
window.NotificationStore.m_rgNotificationToasts.push(toastData);
|
||||
window.NotificationStore.DispatchNextToast();
|
||||
if (toast.playSound) this.audioModule?.PlayNavSound(toast.sound);
|
||||
if (toast.showToast) {
|
||||
window.NotificationStore.m_rgNotificationToasts.push(toastData);
|
||||
window.NotificationStore.DispatchNextToast();
|
||||
}
|
||||
}
|
||||
|
||||
deinit() {
|
||||
|
||||
Reference in New Issue
Block a user