mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 13:32:01 +00:00
preview 10/21/2022 fixes (#234)
* initial fixes: everything working except toasts and patch notes * tabshook changes, disable toaster for now * prettier * oops * implement custom toaster because I am tired of Valve's shit also fix QAM not injecting sometimes * remove extra logging * add findSP, fix patch notes, fix vscode screwup * fix patch notes * show error when plugin frontends fail to load * add get_tab_lambda * add css and has_element helpers to Tab * small modals fixup * Don't forceUpdate QuickAccess on stable * add routes prop used to get tabs component * add more dev utils to DFL global
This commit is contained in:
+3
-3
@@ -16,7 +16,7 @@ from zipfile import ZipFile
|
|||||||
|
|
||||||
# Local modules
|
# Local modules
|
||||||
from helpers import get_ssl_context, get_user, get_user_group, download_remote_binary_to_path
|
from helpers import get_ssl_context, get_user, get_user_group, download_remote_binary_to_path
|
||||||
from injector import get_tab, inject_to_tab
|
from injector import get_gamepadui_tab
|
||||||
|
|
||||||
logger = getLogger("Browser")
|
logger = getLogger("Browser")
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ class PluginBrowser:
|
|||||||
async def uninstall_plugin(self, name):
|
async def uninstall_plugin(self, name):
|
||||||
if self.loader.watcher:
|
if self.loader.watcher:
|
||||||
self.loader.watcher.disabled = True
|
self.loader.watcher.disabled = True
|
||||||
tab = await get_tab("SP")
|
tab = await get_gamepadui_tab()
|
||||||
try:
|
try:
|
||||||
logger.info("uninstalling " + name)
|
logger.info("uninstalling " + name)
|
||||||
logger.info(" at dir " + self.find_plugin_folder(name))
|
logger.info(" at dir " + self.find_plugin_folder(name))
|
||||||
@@ -172,7 +172,7 @@ class PluginBrowser:
|
|||||||
async def request_plugin_install(self, artifact, name, version, hash):
|
async def request_plugin_install(self, artifact, name, version, hash):
|
||||||
request_id = str(time())
|
request_id = str(time())
|
||||||
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
|
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
|
||||||
tab = await get_tab("SP")
|
tab = await get_gamepadui_tab()
|
||||||
await tab.open_websocket()
|
await tab.open_websocket()
|
||||||
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}')")
|
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}')")
|
||||||
|
|
||||||
|
|||||||
+114
-28
@@ -3,6 +3,7 @@
|
|||||||
from asyncio import sleep
|
from asyncio import sleep
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
from aiohttp.client_exceptions import ClientConnectorError
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
@@ -18,6 +19,7 @@ class Tab:
|
|||||||
def __init__(self, res) -> None:
|
def __init__(self, res) -> None:
|
||||||
self.title = res["title"]
|
self.title = res["title"]
|
||||||
self.id = res["id"]
|
self.id = res["id"]
|
||||||
|
self.url = res["url"]
|
||||||
self.ws_url = res["webSocketDebuggerUrl"]
|
self.ws_url = res["webSocketDebuggerUrl"]
|
||||||
|
|
||||||
self.websocket = None
|
self.websocket = None
|
||||||
@@ -64,6 +66,26 @@ class Tab:
|
|||||||
await self.close_websocket()
|
await self.close_websocket()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
async def has_global_var(self, var_name, manage_socket=True):
|
||||||
|
res = await self.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False, manage_socket)
|
||||||
|
|
||||||
|
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return res["result"]["result"]["value"]
|
||||||
|
|
||||||
|
async def close(self, manage_socket=True):
|
||||||
|
if manage_socket:
|
||||||
|
await self.open_websocket()
|
||||||
|
|
||||||
|
res = await self._send_devtools_cmd({
|
||||||
|
"method": "Page.close",
|
||||||
|
}, False)
|
||||||
|
|
||||||
|
if manage_socket:
|
||||||
|
await self.close_websocket()
|
||||||
|
return res
|
||||||
|
|
||||||
async def enable(self):
|
async def enable(self):
|
||||||
"""
|
"""
|
||||||
Enables page domain notifications.
|
Enables page domain notifications.
|
||||||
@@ -80,6 +102,18 @@ class Tab:
|
|||||||
"method": "Page.disable",
|
"method": "Page.disable",
|
||||||
}, False)
|
}, False)
|
||||||
|
|
||||||
|
async def refresh(self):
|
||||||
|
if manage_socket:
|
||||||
|
await self.open_websocket()
|
||||||
|
|
||||||
|
await self._send_devtools_cmd({
|
||||||
|
"method": "Page.reload",
|
||||||
|
}, False)
|
||||||
|
|
||||||
|
if manage_socket:
|
||||||
|
await self.close_websocket()
|
||||||
|
|
||||||
|
return
|
||||||
async def reload_and_evaluate(self, js, manage_socket=True):
|
async def reload_and_evaluate(self, js, manage_socket=True):
|
||||||
"""
|
"""
|
||||||
Reloads the current tab, with JS to run on load via debugger
|
Reloads the current tab, with JS to run on load via debugger
|
||||||
@@ -227,6 +261,71 @@ class Tab:
|
|||||||
if manage_socket:
|
if manage_socket:
|
||||||
await self.close_websocket()
|
await self.close_websocket()
|
||||||
|
|
||||||
|
async def has_element(self, element_name, manage_socket=True):
|
||||||
|
res = await self.evaluate_js(f"document.getElementById('{element_name}') != null", False, manage_socket)
|
||||||
|
|
||||||
|
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return res["result"]["result"]["value"]
|
||||||
|
|
||||||
|
async def inject_css(self, style, manage_socket=True):
|
||||||
|
try:
|
||||||
|
css_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
result = await self.evaluate_js(
|
||||||
|
f"""
|
||||||
|
(function() {{
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = "{css_id}";
|
||||||
|
document.head.append(style);
|
||||||
|
style.textContent = `{style}`;
|
||||||
|
}})()
|
||||||
|
""", False, manage_socket)
|
||||||
|
|
||||||
|
if "exceptionDetails" in result["result"]:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"result": result["result"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"result": css_id
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"result": e
|
||||||
|
}
|
||||||
|
|
||||||
|
async def remove_css(self, css_id, manage_socket=True):
|
||||||
|
try:
|
||||||
|
result = await self.evaluate_js(
|
||||||
|
f"""
|
||||||
|
(function() {{
|
||||||
|
let style = document.getElementById("{css_id}");
|
||||||
|
|
||||||
|
if (style.nodeName.toLowerCase() == 'style')
|
||||||
|
style.parentNode.removeChild(style);
|
||||||
|
}})()
|
||||||
|
""", False, manage_socket)
|
||||||
|
|
||||||
|
if "exceptionDetails" in result["result"]:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"result": result
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"result": e
|
||||||
|
}
|
||||||
|
|
||||||
async def get_steam_resource(self, url):
|
async def get_steam_resource(self, url):
|
||||||
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
|
res = await self.evaluate_js(f'(async function test() {{ return await (await fetch("{url}")).text() }})()', True)
|
||||||
return res["result"]["result"]["value"]
|
return res["result"]["result"]["value"]
|
||||||
@@ -235,7 +334,7 @@ class Tab:
|
|||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
async def get_tabs():
|
async def get_tabs() -> List[Tab]:
|
||||||
async with ClientSession() as web:
|
async with ClientSession() as web:
|
||||||
res = {}
|
res = {}
|
||||||
|
|
||||||
@@ -257,41 +356,28 @@ async def get_tabs():
|
|||||||
raise Exception(f"/json did not return 200. {await res.text()}")
|
raise Exception(f"/json did not return 200. {await res.text()}")
|
||||||
|
|
||||||
|
|
||||||
async def get_tab(tab_name):
|
async def get_tab(tab_name) -> Tab:
|
||||||
tabs = await get_tabs()
|
tabs = await get_tabs()
|
||||||
tab = next((i for i in tabs if i.title == tab_name), None)
|
tab = next((i for i in tabs if i.title == tab_name), None)
|
||||||
if not tab:
|
if not tab:
|
||||||
raise ValueError(f"Tab {tab_name} not found")
|
raise ValueError(f"Tab {tab_name} not found")
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
async def get_tab_lambda(test) -> Tab:
|
||||||
|
tabs = await get_tabs()
|
||||||
|
tab = next((i for i in tabs if test(i)), None)
|
||||||
|
if not tab:
|
||||||
|
raise ValueError(f"Tab not found by lambda")
|
||||||
|
return tab
|
||||||
|
|
||||||
|
async def get_gamepadui_tab() -> Tab:
|
||||||
|
tabs = await get_tabs()
|
||||||
|
tab = next((i for i in tabs if ("https://steamloopback.host/routes/" in i.url and (i.title == "Steam" or i.title == "SP"))), None)
|
||||||
|
if not tab:
|
||||||
|
raise ValueError(f"GamepadUI Tab not found")
|
||||||
|
return tab
|
||||||
|
|
||||||
async def inject_to_tab(tab_name, js, run_async=False):
|
async def inject_to_tab(tab_name, js, run_async=False):
|
||||||
tab = await get_tab(tab_name)
|
tab = await get_tab(tab_name)
|
||||||
|
|
||||||
return await tab.evaluate_js(js, run_async)
|
return await tab.evaluate_js(js, run_async)
|
||||||
|
|
||||||
|
|
||||||
async def tab_has_global_var(tab_name, var_name):
|
|
||||||
try:
|
|
||||||
tab = await get_tab(tab_name)
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
res = await tab.evaluate_js(f"window['{var_name}'] !== null && window['{var_name}'] !== undefined", False)
|
|
||||||
|
|
||||||
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return res["result"]["result"]["value"]
|
|
||||||
|
|
||||||
|
|
||||||
async def tab_has_element(tab_name, element_name):
|
|
||||||
try:
|
|
||||||
tab = await get_tab(tab_name)
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
res = await tab.evaluate_js(f"document.getElementById('{element_name}') != null", False)
|
|
||||||
|
|
||||||
if not "result" in res or not "result" in res["result"] or not "value" in res["result"]["result"]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return res["result"]["result"]["value"]
|
|
||||||
|
|||||||
+3
-2
@@ -15,7 +15,7 @@ try:
|
|||||||
except UnsupportedLibc:
|
except UnsupportedLibc:
|
||||||
from watchdog.observers.fsevents import FSEventsObserver as Observer
|
from watchdog.observers.fsevents import FSEventsObserver as Observer
|
||||||
|
|
||||||
from injector import get_tab, inject_to_tab
|
from injector import get_tab, get_gamepadui_tab
|
||||||
from plugin import PluginWrapper
|
from plugin import PluginWrapper
|
||||||
|
|
||||||
|
|
||||||
@@ -141,7 +141,8 @@ class Loader:
|
|||||||
print_exc()
|
print_exc()
|
||||||
|
|
||||||
async def dispatch_plugin(self, name, version):
|
async def dispatch_plugin(self, name, version):
|
||||||
await inject_to_tab("SP", f"window.importDeckyPlugin('{name}', '{version}')")
|
gpui_tab = await get_gamepadui_tab()
|
||||||
|
await gpui_tab.evaluate_js(f"window.importDeckyPlugin('{name}', '{version}')")
|
||||||
|
|
||||||
def import_plugins(self):
|
def import_plugins(self):
|
||||||
self.logger.info(f"import plugins from {self.plugin_path}")
|
self.logger.info(f"import plugins from {self.plugin_path}")
|
||||||
|
|||||||
+42
-10
@@ -12,7 +12,7 @@ from traceback import format_exc
|
|||||||
|
|
||||||
import aiohttp_cors
|
import aiohttp_cors
|
||||||
# Partial imports
|
# Partial imports
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession, client_exceptions
|
||||||
from aiohttp.web import Application, Response, get, run_app, static
|
from aiohttp.web import Application, Response, get, run_app, static
|
||||||
from aiohttp_jinja2 import setup as jinja_setup
|
from aiohttp_jinja2 import setup as jinja_setup
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
|
|||||||
get_home_path, get_homebrew_path, get_user,
|
get_home_path, get_homebrew_path, get_user,
|
||||||
get_user_group, set_user, set_user_group,
|
get_user_group, set_user, set_user_group,
|
||||||
stop_systemd_unit)
|
stop_systemd_unit)
|
||||||
from injector import inject_to_tab, tab_has_global_var
|
from injector import get_gamepadui_tab, Tab, get_tabs
|
||||||
from loader import Loader
|
from loader import Loader
|
||||||
from settings import SettingsManager
|
from settings import SettingsManager
|
||||||
from updater import Updater
|
from updater import Updater
|
||||||
@@ -118,17 +118,49 @@ class PluginManager:
|
|||||||
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
|
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
|
||||||
|
|
||||||
async def loader_reinjector(self):
|
async def loader_reinjector(self):
|
||||||
await sleep(2)
|
|
||||||
await self.inject_javascript()
|
|
||||||
while True:
|
while True:
|
||||||
await sleep(5)
|
tab = None
|
||||||
if not await tab_has_global_var("SP", "deckyHasLoaded"):
|
while not tab:
|
||||||
logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
try:
|
||||||
await self.inject_javascript()
|
tab = await get_gamepadui_tab()
|
||||||
|
except client_exceptions.ClientConnectorError or client_exceptions.ServerDisconnectedError:
|
||||||
|
logger.debug("Couldn't connect to debugger, waiting 5 seconds.")
|
||||||
|
pass
|
||||||
|
except ValueError:
|
||||||
|
logger.debug("Couldn't find GamepadUI tab, waiting 5 seconds")
|
||||||
|
pass
|
||||||
|
if not tab:
|
||||||
|
await sleep(5)
|
||||||
|
await tab.open_websocket()
|
||||||
|
await tab.enable()
|
||||||
|
await self.inject_javascript(tab, True)
|
||||||
|
async for msg in tab.listen_for_message():
|
||||||
|
logger.debug("Page event: " + str(msg.get("method", None)))
|
||||||
|
if msg.get("method", None) == "Page.domContentEventFired":
|
||||||
|
if not await tab.has_global_var("deckyHasLoaded", False):
|
||||||
|
await self.inject_javascript(tab)
|
||||||
|
if msg.get("method", None) == "Inspector.detached":
|
||||||
|
logger.info("Steam is exiting...")
|
||||||
|
await tab.close_websocket()
|
||||||
|
break
|
||||||
|
# while True:
|
||||||
|
# await sleep(5)
|
||||||
|
# if not await tab.has_global_var("deckyHasLoaded", False):
|
||||||
|
# logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
|
||||||
|
# await self.inject_javascript(tab)
|
||||||
|
|
||||||
async def inject_javascript(self, request=None):
|
async def inject_javascript(self, tab: Tab, first=False, request=None):
|
||||||
|
logger.info("Loading Decky frontend!")
|
||||||
try:
|
try:
|
||||||
await inject_to_tab("SP", "try{if (window.deckyHasLoaded) location.reload();window.deckyHasLoaded = true;(async()=>{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')})();}catch(e){console.error(e)}", True)
|
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(() => location.reload(), 1000)}window.deckyHasLoaded = true;(async()=>{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')})();}catch(e){console.error(e)}", False, False, False)
|
||||||
except:
|
except:
|
||||||
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
|
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
|
||||||
pass
|
pass
|
||||||
|
|||||||
+3
-3
@@ -9,7 +9,7 @@ from subprocess import call
|
|||||||
from aiohttp import ClientSession, web
|
from aiohttp import ClientSession, web
|
||||||
|
|
||||||
import helpers
|
import helpers
|
||||||
from injector import get_tab, inject_to_tab
|
from injector import get_gamepadui_tab, inject_to_tab
|
||||||
from settings import SettingsManager
|
from settings import SettingsManager
|
||||||
|
|
||||||
logger = getLogger("Updater")
|
logger = getLogger("Updater")
|
||||||
@@ -108,7 +108,7 @@ class Updater:
|
|||||||
logger.error("release type: NOT FOUND")
|
logger.error("release type: NOT FOUND")
|
||||||
raise ValueError("no valid branch found")
|
raise ValueError("no valid branch found")
|
||||||
logger.info("Updated remote version information")
|
logger.info("Updated remote version information")
|
||||||
tab = await get_tab("SP")
|
tab = await get_gamepadui_tab()
|
||||||
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
|
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
|
||||||
return await self.get_version()
|
return await self.get_version()
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ class Updater:
|
|||||||
version = self.remoteVer["tag_name"]
|
version = self.remoteVer["tag_name"]
|
||||||
download_url = self.remoteVer["assets"][0]["browser_download_url"]
|
download_url = self.remoteVer["assets"][0]["browser_download_url"]
|
||||||
|
|
||||||
tab = await get_tab("SP")
|
tab = await get_gamepadui_tab()
|
||||||
await tab.open_websocket()
|
await tab.open_websocket()
|
||||||
async with ClientSession() as web:
|
async with ClientSession() as web:
|
||||||
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from asyncio import sleep, start_server, gather, open_connection
|
|||||||
from aiohttp import ClientSession, web
|
from aiohttp import ClientSession, web
|
||||||
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from injector import inject_to_tab, get_tab
|
from injector import inject_to_tab, get_gamepadui_tab
|
||||||
import helpers
|
import helpers
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ class Utilities:
|
|||||||
self.start_rdt_proxy(ip, 8097)
|
self.start_rdt_proxy(ip, 8097)
|
||||||
script = "if(!window.deckyHasConnectedRDT){window.deckyHasConnectedRDT=true;\n" + await res.text() + "\n}"
|
script = "if(!window.deckyHasConnectedRDT){window.deckyHasConnectedRDT=true;\n" + await res.text() + "\n}"
|
||||||
self.logger.info("Connected to React DevTools, loading script")
|
self.logger.info("Connected to React DevTools, loading script")
|
||||||
tab = await get_tab("SP")
|
tab = await get_gamepadui_tab()
|
||||||
# RDT needs to load before React itself to work.
|
# RDT needs to load before React itself to work.
|
||||||
result = await tab.reload_and_evaluate(script)
|
result = await tab.reload_and_evaluate(script)
|
||||||
self.logger.info(result)
|
self.logger.info(result)
|
||||||
@@ -259,7 +259,7 @@ class Utilities:
|
|||||||
|
|
||||||
async def disable_rdt(self):
|
async def disable_rdt(self):
|
||||||
self.logger.info("Disabling React DevTools")
|
self.logger.info("Disabling React DevTools")
|
||||||
tab = await get_tab("SP")
|
tab = await get_gamepadui_tab()
|
||||||
self.rdt_script_id = None
|
self.rdt_script_id = None
|
||||||
await tab.evaluate_js("location.reload();", False, True, False)
|
await tab.evaluate_js("location.reload();", False, True, False)
|
||||||
self.logger.info("React DevTools disabled")
|
self.logger.info("React DevTools disabled")
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"decky-frontend-lib": "^3.6.0",
|
"decky-frontend-lib": "^3.7.0",
|
||||||
"react-file-icon": "^1.2.0",
|
"react-file-icon": "^1.2.0",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^4.4.0",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
|
|||||||
Generated
+4
-4
@@ -10,7 +10,7 @@ specifiers:
|
|||||||
'@types/react-file-icon': ^1.0.1
|
'@types/react-file-icon': ^1.0.1
|
||||||
'@types/react-router': 5.1.18
|
'@types/react-router': 5.1.18
|
||||||
'@types/webpack': ^5.28.0
|
'@types/webpack': ^5.28.0
|
||||||
decky-frontend-lib: ^3.6.0
|
decky-frontend-lib: ^3.7.0
|
||||||
husky: ^8.0.1
|
husky: ^8.0.1
|
||||||
import-sort-style-module: ^6.0.0
|
import-sort-style-module: ^6.0.0
|
||||||
inquirer: ^8.2.4
|
inquirer: ^8.2.4
|
||||||
@@ -30,7 +30,7 @@ specifiers:
|
|||||||
typescript: ^4.7.4
|
typescript: ^4.7.4
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
decky-frontend-lib: 3.6.0
|
decky-frontend-lib: 3.7.0
|
||||||
react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
|
react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
|
||||||
react-icons: 4.4.0_react@16.14.0
|
react-icons: 4.4.0_react@16.14.0
|
||||||
react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u
|
react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u
|
||||||
@@ -944,8 +944,8 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
|
|
||||||
/decky-frontend-lib/3.6.0:
|
/decky-frontend-lib/3.7.0:
|
||||||
resolution: {integrity: sha512-X3VbTbmW7TnBwPW0ui0xjSVoa2UsuKPwI6nFi7LY2ZzmNytCfszk+ZfJSBm2lD2fqV+btqJzr0qFnWFl+bgjEA==}
|
resolution: {integrity: sha512-kq002s74XRrtd0LbN9eVLPtwYQOQhAINKE8X0hsbcz3SVK7wvStVEmepku2m9kONUPN7m+H9eYRqJFGutQybSg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.7
|
minimist: 1.2.7
|
||||||
dev: false
|
dev: false
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface PublicDeckyGlobalComponentsState {
|
||||||
|
components: Map<string, FC>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeckyGlobalComponentsState {
|
||||||
|
// TODO a set would be better
|
||||||
|
private _components = new Map<string, FC>();
|
||||||
|
|
||||||
|
public eventBus = new EventTarget();
|
||||||
|
|
||||||
|
publicState(): PublicDeckyGlobalComponentsState {
|
||||||
|
return { components: this._components };
|
||||||
|
}
|
||||||
|
|
||||||
|
addComponent(path: string, component: FC) {
|
||||||
|
this._components.set(path, component);
|
||||||
|
this.notifyUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeComponent(path: string) {
|
||||||
|
this._components.delete(path);
|
||||||
|
this.notifyUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyUpdate() {
|
||||||
|
this.eventBus.dispatchEvent(new Event('update'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeckyGlobalComponentsContext extends PublicDeckyGlobalComponentsState {
|
||||||
|
addComponent(path: string, component: FC): void;
|
||||||
|
removeComponent(path: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeckyGlobalComponentsContext = createContext<DeckyGlobalComponentsContext>(null as any);
|
||||||
|
|
||||||
|
export const useDeckyGlobalComponentsState = () => useContext(DeckyGlobalComponentsContext);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deckyGlobalComponentsState: DeckyGlobalComponentsState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeckyGlobalComponentsStateContextProvider: FC<Props> = ({
|
||||||
|
children,
|
||||||
|
deckyGlobalComponentsState: deckyGlobalComponentsState,
|
||||||
|
}) => {
|
||||||
|
const [publicDeckyGlobalComponentsState, setPublicDeckyGlobalComponentsState] =
|
||||||
|
useState<PublicDeckyGlobalComponentsState>({
|
||||||
|
...deckyGlobalComponentsState.publicState(),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onUpdate() {
|
||||||
|
setPublicDeckyGlobalComponentsState({ ...deckyGlobalComponentsState.publicState() });
|
||||||
|
}
|
||||||
|
|
||||||
|
deckyGlobalComponentsState.eventBus.addEventListener('update', onUpdate);
|
||||||
|
|
||||||
|
return () => deckyGlobalComponentsState.eventBus.removeEventListener('update', onUpdate);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addComponent = deckyGlobalComponentsState.addComponent.bind(deckyGlobalComponentsState);
|
||||||
|
const removeComponent = deckyGlobalComponentsState.removeComponent.bind(deckyGlobalComponentsState);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DeckyGlobalComponentsContext.Provider
|
||||||
|
value={{ ...publicDeckyGlobalComponentsState, addComponent, removeComponent }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DeckyGlobalComponentsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { ToastData, joinClassNames } from 'decky-frontend-lib';
|
||||||
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
import { ReactElement } from 'react-markdown/lib/react-markdown';
|
||||||
|
|
||||||
|
import { useDeckyToasterState } from './DeckyToasterState';
|
||||||
|
import Toast, { toastClasses } from './Toast';
|
||||||
|
|
||||||
|
interface DeckyToasterProps {}
|
||||||
|
|
||||||
|
interface RenderedToast {
|
||||||
|
component: ReactElement;
|
||||||
|
data: ToastData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeckyToaster: FC<DeckyToasterProps> = () => {
|
||||||
|
const { toasts, removeToast } = useDeckyToasterState();
|
||||||
|
const [renderedToast, setRenderedToast] = useState<RenderedToast | null>(null);
|
||||||
|
console.log(toasts);
|
||||||
|
if (toasts.size > 0) {
|
||||||
|
const [activeToast] = toasts;
|
||||||
|
if (!renderedToast || activeToast != renderedToast.data) {
|
||||||
|
// TODO play toast sound
|
||||||
|
console.log('rendering toast', activeToast);
|
||||||
|
setRenderedToast({ component: <Toast key={Math.random()} toast={activeToast} />, data: activeToast });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (renderedToast) setRenderedToast(null);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
// not actually node but TS is shit
|
||||||
|
let interval: NodeJS.Timer | null;
|
||||||
|
if (renderedToast) {
|
||||||
|
interval = setTimeout(() => {
|
||||||
|
interval = null;
|
||||||
|
console.log('clear toast', renderedToast.data);
|
||||||
|
removeToast(renderedToast.data);
|
||||||
|
}, (renderedToast.data.duration || 5e3) + 1000);
|
||||||
|
console.log('set int', interval);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (interval) {
|
||||||
|
console.log('clearing int', interval);
|
||||||
|
clearTimeout(interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [renderedToast]);
|
||||||
|
return (
|
||||||
|
<div className={joinClassNames('deckyToaster', toastClasses.ToastPlaceholder)}>
|
||||||
|
{renderedToast && renderedToast.component}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeckyToaster;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { ToastData } from 'decky-frontend-lib';
|
||||||
|
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface PublicDeckyToasterState {
|
||||||
|
toasts: Set<ToastData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeckyToasterState {
|
||||||
|
// TODO a set would be better
|
||||||
|
private _toasts: Set<ToastData> = new Set();
|
||||||
|
|
||||||
|
public eventBus = new EventTarget();
|
||||||
|
|
||||||
|
publicState(): PublicDeckyToasterState {
|
||||||
|
return { toasts: this._toasts };
|
||||||
|
}
|
||||||
|
|
||||||
|
addToast(toast: ToastData) {
|
||||||
|
this._toasts.add(toast);
|
||||||
|
this.notifyUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeToast(toast: ToastData) {
|
||||||
|
this._toasts.delete(toast);
|
||||||
|
this.notifyUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyUpdate() {
|
||||||
|
this.eventBus.dispatchEvent(new Event('update'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeckyToasterContext extends PublicDeckyToasterState {
|
||||||
|
addToast(toast: ToastData): void;
|
||||||
|
removeToast(toast: ToastData): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeckyToasterContext = createContext<DeckyToasterContext>(null as any);
|
||||||
|
|
||||||
|
export const useDeckyToasterState = () => useContext(DeckyToasterContext);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deckyToasterState: DeckyToasterState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {
|
||||||
|
const [publicDeckyToasterState, setPublicDeckyToasterState] = useState<PublicDeckyToasterState>({
|
||||||
|
...deckyToasterState.publicState(),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onUpdate() {
|
||||||
|
setPublicDeckyToasterState({ ...deckyToasterState.publicState() });
|
||||||
|
}
|
||||||
|
|
||||||
|
deckyToasterState.eventBus.addEventListener('update', onUpdate);
|
||||||
|
|
||||||
|
return () => deckyToasterState.eventBus.removeEventListener('update', onUpdate);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addToast = deckyToasterState.addToast.bind(deckyToasterState);
|
||||||
|
const removeToast = deckyToasterState.removeToast.bind(deckyToasterState);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DeckyToasterContext.Provider value={{ ...publicDeckyToasterState, addToast, removeToast }}>
|
||||||
|
{children}
|
||||||
|
</DeckyToasterContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Focusable } from 'decky-frontend-lib';
|
import { Focusable, Router } from 'decky-frontend-lib';
|
||||||
import { FunctionComponent, useRef } from 'react';
|
import { FunctionComponent, useRef } from 'react';
|
||||||
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
|
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
@@ -21,8 +21,8 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
|
|||||||
<Focusable
|
<Focusable
|
||||||
onActivate={() => {}}
|
onActivate={() => {}}
|
||||||
onOKButton={() => {
|
onOKButton={() => {
|
||||||
aRef?.current?.click();
|
|
||||||
props.onDismiss?.();
|
props.onDismiss?.();
|
||||||
|
Router.NavigateToExternalWeb(aRef.current!.href);
|
||||||
}}
|
}}
|
||||||
style={{ display: 'inline' }}
|
style={{ display: 'inline' }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
import { FC, createContext, useContext } from 'react';
|
import { FC, createContext, useContext, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
const QuickAccessVisibleState = createContext<boolean>(true);
|
const QuickAccessVisibleState = createContext<boolean>(true);
|
||||||
|
|
||||||
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
|
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
|
||||||
|
|
||||||
interface Props {
|
export const QuickAccessVisibleStateProvider: FC<{}> = ({ children }) => {
|
||||||
visible: boolean;
|
const divRef = useRef<HTMLDivElement>(null);
|
||||||
}
|
const [visible, setVisible] = useState<boolean>(false);
|
||||||
|
useEffect(() => {
|
||||||
export const QuickAccessVisibleStateProvider: FC<Props> = ({ children, visible }) => {
|
const doc: Document | void | null = divRef?.current?.ownerDocument;
|
||||||
return <QuickAccessVisibleState.Provider value={visible}>{children}</QuickAccessVisibleState.Provider>;
|
if (!doc) return;
|
||||||
|
setVisible(doc.visibilityState == 'visible');
|
||||||
|
const onChange = (e: Event) => {
|
||||||
|
setVisible(doc.visibilityState == 'visible');
|
||||||
|
};
|
||||||
|
doc.addEventListener('visibilitychange', onChange);
|
||||||
|
return () => {
|
||||||
|
doc.removeEventListener('visibilitychange', onChange);
|
||||||
|
};
|
||||||
|
}, [divRef]);
|
||||||
|
return (
|
||||||
|
<div ref={divRef}>
|
||||||
|
<QuickAccessVisibleState.Provider value={visible}>{children}</QuickAccessVisibleState.Provider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,10 @@ import { ToastData, findModule, joinClassNames } from 'decky-frontend-lib';
|
|||||||
import { FunctionComponent } from 'react';
|
import { FunctionComponent } from 'react';
|
||||||
|
|
||||||
interface ToastProps {
|
interface ToastProps {
|
||||||
toast: {
|
toast: ToastData;
|
||||||
data: ToastData;
|
|
||||||
nToastDurationMS: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toastClasses = findModule((mod) => {
|
export const toastClasses = findModule((mod) => {
|
||||||
if (typeof mod !== 'object') return false;
|
if (typeof mod !== 'object') return false;
|
||||||
|
|
||||||
if (mod.ToastPlaceholder) {
|
if (mod.ToastPlaceholder) {
|
||||||
@@ -30,21 +27,19 @@ const templateClasses = findModule((mod) => {
|
|||||||
|
|
||||||
const Toast: FunctionComponent<ToastProps> = ({ toast }) => {
|
const Toast: FunctionComponent<ToastProps> = ({ toast }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={toastClasses.ToastPopup}>
|
||||||
style={{ '--toast-duration': `${toast.nToastDurationMS}ms` } as React.CSSProperties}
|
|
||||||
className={toastClasses.toastEnter}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
onClick={toast.data.onClick}
|
style={{ '--toast-duration': `${toast.duration}ms` } as React.CSSProperties}
|
||||||
className={joinClassNames(templateClasses.ShortTemplate, toast.data.className || '')}
|
onClick={toast.onClick}
|
||||||
|
className={joinClassNames(templateClasses.ShortTemplate, toast.className || '')}
|
||||||
>
|
>
|
||||||
{toast.data.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.data.logo}</div>}
|
{toast.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.logo}</div>}
|
||||||
<div className={joinClassNames(templateClasses.Content, toast.data.contentClassName || '')}>
|
<div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}>
|
||||||
<div className={templateClasses.Header}>
|
<div className={templateClasses.Header}>
|
||||||
{toast.data.icon && <div className={templateClasses.Icon}>{toast.data.icon}</div>}
|
{toast.icon && <div className={templateClasses.Icon}>{toast.icon}</div>}
|
||||||
<div className={templateClasses.Title}>{toast.data.title}</div>
|
<div className={templateClasses.Title}>{toast.title}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={templateClasses.Body}>{toast.data.body}</div>
|
<div className={templateClasses.Body}>{toast.body}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { FaArrowDown } from 'react-icons/fa';
|
import { FaArrowDown } from 'react-icons/fa';
|
||||||
|
|
||||||
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
|
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
|
||||||
|
import { findSP } from '../../../../utils/windows';
|
||||||
import { useDeckyState } from '../../../DeckyState';
|
import { useDeckyState } from '../../../DeckyState';
|
||||||
import InlinePatchNotes from '../../../patchnotes/InlinePatchNotes';
|
import InlinePatchNotes from '../../../patchnotes/InlinePatchNotes';
|
||||||
import WithSuspense from '../../../WithSuspense';
|
import WithSuspense from '../../../WithSuspense';
|
||||||
@@ -21,6 +22,7 @@ import WithSuspense from '../../../WithSuspense';
|
|||||||
const MarkdownRenderer = lazy(() => import('../../../Markdown'));
|
const MarkdownRenderer = lazy(() => import('../../../Markdown'));
|
||||||
|
|
||||||
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
|
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
|
||||||
|
const SP = findSP();
|
||||||
return (
|
return (
|
||||||
<Focusable onCancelButton={closeModal}>
|
<Focusable onCancelButton={closeModal}>
|
||||||
<FocusRing>
|
<FocusRing>
|
||||||
@@ -50,12 +52,13 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
|
|||||||
)}
|
)}
|
||||||
fnGetId={(id) => id}
|
fnGetId={(id) => id}
|
||||||
nNumItems={versionInfo?.all?.length}
|
nNumItems={versionInfo?.all?.length}
|
||||||
nHeight={window.innerHeight - 40}
|
nHeight={SP.innerHeight - 40}
|
||||||
nItemHeight={window.innerHeight - 40}
|
nItemHeight={SP.innerHeight - 40}
|
||||||
nItemMarginX={0}
|
nItemMarginX={0}
|
||||||
initialColumn={0}
|
initialColumn={0}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
fnGetColumnWidth={() => window.innerWidth}
|
fnGetColumnWidth={() => SP.innerWidth}
|
||||||
|
name="Decky Updates"
|
||||||
/>
|
/>
|
||||||
</FocusRing>
|
</FocusRing>
|
||||||
</Focusable>
|
</Focusable>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
ReactRouter,
|
ReactRouter,
|
||||||
Router,
|
Router,
|
||||||
|
fakeRenderComponent,
|
||||||
|
findInReactTree,
|
||||||
|
findInTree,
|
||||||
findModule,
|
findModule,
|
||||||
findModuleChild,
|
findModuleChild,
|
||||||
gamepadDialogClasses,
|
gamepadDialogClasses,
|
||||||
@@ -71,6 +74,11 @@ export async function startup() {
|
|||||||
window.DFL = {
|
window.DFL = {
|
||||||
findModuleChild,
|
findModuleChild,
|
||||||
findModule,
|
findModule,
|
||||||
|
ReactUtils: {
|
||||||
|
fakeRenderComponent,
|
||||||
|
findInReactTree,
|
||||||
|
findInTree,
|
||||||
|
},
|
||||||
Router,
|
Router,
|
||||||
ReactRouter,
|
ReactRouter,
|
||||||
classes: {
|
classes: {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
staticClasses,
|
staticClasses,
|
||||||
} from 'decky-frontend-lib';
|
} from 'decky-frontend-lib';
|
||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
import { FaPlug } from 'react-icons/fa';
|
import { FaExclamationCircle, FaPlug } from 'react-icons/fa';
|
||||||
|
|
||||||
import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
|
import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
|
||||||
import LegacyPlugin from './components/LegacyPlugin';
|
import LegacyPlugin from './components/LegacyPlugin';
|
||||||
@@ -41,7 +41,7 @@ class PluginLoader extends Logger {
|
|||||||
private tabsHook: TabsHook = new TabsHook();
|
private tabsHook: TabsHook = new TabsHook();
|
||||||
// private windowHook: WindowHook = new WindowHook();
|
// private windowHook: WindowHook = new WindowHook();
|
||||||
private routerHook: RouterHook = new RouterHook();
|
private routerHook: RouterHook = new RouterHook();
|
||||||
public toaster: Toaster = new Toaster();
|
public toaster: Toaster = new Toaster(this.routerHook);
|
||||||
private deckyState: DeckyState = new DeckyState();
|
private deckyState: DeckyState = new DeckyState();
|
||||||
|
|
||||||
private reloadLock: boolean = false;
|
private reloadLock: boolean = false;
|
||||||
@@ -233,13 +233,18 @@ class PluginLoader extends Logger {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let plugin_export = await eval(await res.text());
|
try {
|
||||||
let plugin = plugin_export(this.createPluginAPI(name));
|
let plugin_export = await eval(await res.text());
|
||||||
this.plugins.push({
|
let plugin = plugin_export(this.createPluginAPI(name));
|
||||||
...plugin,
|
this.plugins.push({
|
||||||
name: name,
|
...plugin,
|
||||||
version: version,
|
name: name,
|
||||||
});
|
version: version,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.error('Error loading plugin ' + name, e);
|
||||||
|
this.toaster.toast({ title: 'Error loading ' + name, body: '' + e, icon: <FaExclamationCircle /> });
|
||||||
|
}
|
||||||
} else throw new Error(`${name} frontend_bundle not OK`);
|
} else throw new Error(`${name} frontend_bundle not OK`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Patch, afterPatch, findModuleChild } from 'decky-frontend-lib';
|
import { Patch, afterPatch, findModuleChild } from 'decky-frontend-lib';
|
||||||
import { ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
|
import { FC, ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
|
||||||
import type { Route } from 'react-router';
|
import type { Route } from 'react-router';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeckyGlobalComponentsState,
|
||||||
|
DeckyGlobalComponentsStateContextProvider,
|
||||||
|
useDeckyGlobalComponentsState,
|
||||||
|
} from './components/DeckyGlobalComponentsState';
|
||||||
import {
|
import {
|
||||||
DeckyRouterState,
|
DeckyRouterState,
|
||||||
DeckyRouterStateContextProvider,
|
DeckyRouterStateContextProvider,
|
||||||
@@ -22,8 +27,10 @@ class RouterHook extends Logger {
|
|||||||
private memoizedRouter: any;
|
private memoizedRouter: any;
|
||||||
private gamepadWrapper: any;
|
private gamepadWrapper: any;
|
||||||
private routerState: DeckyRouterState = new DeckyRouterState();
|
private routerState: DeckyRouterState = new DeckyRouterState();
|
||||||
|
private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
|
||||||
private wrapperPatch: Patch;
|
private wrapperPatch: Patch;
|
||||||
private routerPatch?: Patch;
|
private routerPatch?: Patch;
|
||||||
|
public routes?: any[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('RouterHook');
|
super('RouterHook');
|
||||||
@@ -42,24 +49,28 @@ class RouterHook extends Logger {
|
|||||||
|
|
||||||
let Route: new () => Route;
|
let Route: new () => Route;
|
||||||
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
||||||
let toReplace = new Map<string, ReactNode>();
|
const processList = (
|
||||||
const DeckyWrapper = ({ children }: { children: ReactElement }) => {
|
routeList: any[],
|
||||||
const { routes, routePatches } = useDeckyRouterState();
|
routes: Map<string, RouterEntry> | null,
|
||||||
|
routePatches: Map<string, Set<RoutePatch>>,
|
||||||
const routeList = children.props.children[0].props.children;
|
save: boolean,
|
||||||
|
) => {
|
||||||
|
this.debug('Route list: ', routeList);
|
||||||
|
if (save) this.routes = routeList;
|
||||||
let routerIndex = routeList.length;
|
let routerIndex = routeList.length;
|
||||||
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
|
if (routes) {
|
||||||
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
|
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
|
||||||
const newRouterArray: ReactElement[] = [];
|
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
|
||||||
routes.forEach(({ component, props }, path) => {
|
const newRouterArray: ReactElement[] = [];
|
||||||
newRouterArray.push(
|
routes.forEach(({ component, props }, path) => {
|
||||||
<Route path={path} {...props}>
|
newRouterArray.push(
|
||||||
{createElement(component)}
|
<Route path={path} {...props}>
|
||||||
</Route>,
|
{createElement(component)}
|
||||||
);
|
</Route>,
|
||||||
});
|
);
|
||||||
routeList[routerIndex] = newRouterArray;
|
});
|
||||||
|
routeList[routerIndex] = newRouterArray;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
routeList.forEach((route: Route, index: number) => {
|
routeList.forEach((route: Route, index: number) => {
|
||||||
const replaced = toReplace.get(route?.props?.path as string);
|
const replaced = toReplace.get(route?.props?.path as string);
|
||||||
@@ -85,19 +96,40 @@ class RouterHook extends Logger {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
let toReplace = new Map<string, ReactNode>();
|
||||||
|
const DeckyWrapper = ({ children }: { children: ReactElement }) => {
|
||||||
|
const { routes, routePatches } = useDeckyRouterState();
|
||||||
|
const mainRouteList = children.props.children[0].props.children;
|
||||||
|
const ingameRouteList = children.props.children[1].props.children; // /appoverlay and /apprunning
|
||||||
|
processList(mainRouteList, routes, routePatches, true);
|
||||||
|
processList(ingameRouteList, null, routePatches, false);
|
||||||
|
|
||||||
this.debug('Rerendered routes list');
|
this.debug('Rerendered routes list');
|
||||||
return children;
|
return children;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let renderedComponents: ReactElement[] = [];
|
||||||
|
|
||||||
|
const DeckyGlobalComponentsWrapper = () => {
|
||||||
|
const { components } = useDeckyGlobalComponentsState();
|
||||||
|
if (renderedComponents.length != components.size) {
|
||||||
|
this.debug('Rerendering global components');
|
||||||
|
renderedComponents = Array.from(components.values()).map((GComponent) => <GComponent />);
|
||||||
|
}
|
||||||
|
return <>{renderedComponents}</>;
|
||||||
|
};
|
||||||
|
|
||||||
this.wrapperPatch = afterPatch(this.gamepadWrapper, 'render', (_: any, ret: any) => {
|
this.wrapperPatch = afterPatch(this.gamepadWrapper, 'render', (_: any, ret: any) => {
|
||||||
if (ret?.props?.children?.props?.children?.length == 5) {
|
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 (
|
if (
|
||||||
ret.props.children.props.children[2]?.props?.children?.[0]?.type?.type
|
ret.props.children.props.children[idx]?.props?.children?.[0]?.type?.type
|
||||||
?.toString()
|
?.toString()
|
||||||
?.includes('GamepadUI.Settings.Root()')
|
?.includes('GamepadUI.Settings.Root()')
|
||||||
) {
|
) {
|
||||||
if (!this.router) {
|
if (!this.router) {
|
||||||
this.router = ret.props.children.props.children[2]?.props?.children?.[0]?.type;
|
this.router = ret.props.children.props.children[idx]?.props?.children?.[0]?.type;
|
||||||
this.routerPatch = afterPatch(this.router, 'type', (_: any, ret: any) => {
|
this.routerPatch = afterPatch(this.router, 'type', (_: any, ret: any) => {
|
||||||
if (!Route)
|
if (!Route)
|
||||||
Route = ret.props.children[0].props.children.find((x: any) => x.props.path == '/createaccount').type;
|
Route = ret.props.children[0].props.children.find((x: any) => x.props.path == '/createaccount').type;
|
||||||
@@ -111,7 +143,12 @@ class RouterHook extends Logger {
|
|||||||
this.memoizedRouter = memo(this.router.type);
|
this.memoizedRouter = memo(this.router.type);
|
||||||
this.memoizedRouter.isDeckyRouter = true;
|
this.memoizedRouter.isDeckyRouter = true;
|
||||||
}
|
}
|
||||||
ret.props.children.props.children[2].props.children[0].type = this.memoizedRouter;
|
ret.props.children.props.children.push(
|
||||||
|
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
|
||||||
|
<DeckyGlobalComponentsWrapper />
|
||||||
|
</DeckyGlobalComponentsStateContextProvider>,
|
||||||
|
);
|
||||||
|
ret.props.children.props.children[idx].props.children[0].type = this.memoizedRouter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
@@ -126,6 +163,14 @@ class RouterHook extends Logger {
|
|||||||
return this.routerState.addPatch(path, patch);
|
return this.routerState.addPatch(path, patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addGlobalComponent(name: string, component: FC) {
|
||||||
|
this.globalComponentsState.addComponent(name, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeGlobalComponent(name: string) {
|
||||||
|
this.globalComponentsState.removeComponent(name);
|
||||||
|
}
|
||||||
|
|
||||||
removePatch(path: string, patch: RoutePatch) {
|
removePatch(path: string, patch: RoutePatch) {
|
||||||
this.routerState.removePatch(path, patch);
|
this.routerState.removePatch(path, patch);
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-86
@@ -1,5 +1,4 @@
|
|||||||
import { Patch, QuickAccessTab, afterPatch, sleep } from 'decky-frontend-lib';
|
import { QuickAccessTab, quickAccessMenuClasses, sleep } from 'decky-frontend-lib';
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState';
|
import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
@@ -28,15 +27,7 @@ interface Tab {
|
|||||||
class TabsHook extends Logger {
|
class TabsHook extends Logger {
|
||||||
// private keys = 7;
|
// private keys = 7;
|
||||||
tabs: Tab[] = [];
|
tabs: Tab[] = [];
|
||||||
private quickAccess: any;
|
private oFilter: (...args: any[]) => any;
|
||||||
private tabRenderer: any;
|
|
||||||
private memoizedQuickAccess: any;
|
|
||||||
private cNode: any;
|
|
||||||
|
|
||||||
private qAPTree: any;
|
|
||||||
private rendererTree: any;
|
|
||||||
|
|
||||||
private cNodePatch?: Patch;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('TabsHook');
|
super('TabsHook');
|
||||||
@@ -46,84 +37,63 @@ class TabsHook extends Logger {
|
|||||||
window.__TABS_HOOK_INSTANCE = this;
|
window.__TABS_HOOK_INSTANCE = this;
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
|
const oFilter = (this.oFilter = Array.prototype.filter);
|
||||||
let scrollRoot: any;
|
Array.prototype.filter = function patchedFilter(...args: any[]) {
|
||||||
async function findScrollRoot(currentNode: any, iters: number): Promise<any> {
|
if (isTabsArray(this)) {
|
||||||
if (iters >= 30) {
|
self.render(this);
|
||||||
self.error(
|
|
||||||
'Scroll root was not found before hitting the recursion limit, a developer will need to increase the limit.',
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
currentNode = currentNode?.child;
|
// @ts-ignore
|
||||||
if (currentNode?.type?.prototype?.RemoveSmartScrollContainer) {
|
return oFilter.call(this, ...args);
|
||||||
self.log(`Scroll root was found in ${iters} recursion cycles`);
|
};
|
||||||
return currentNode;
|
|
||||||
}
|
if (document.title != 'SP')
|
||||||
if (!currentNode) return null;
|
try {
|
||||||
if (currentNode.sibling) {
|
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
|
||||||
let node = await findScrollRoot(currentNode.sibling, iters + 1);
|
let qAMRoot: any;
|
||||||
if (node !== null) return node;
|
async function findQAMRoot(currentNode: any, iters: number): Promise<any> {
|
||||||
}
|
if (iters >= 60) {
|
||||||
return await findScrollRoot(currentNode, iters + 1);
|
// currently 44
|
||||||
}
|
return null;
|
||||||
(async () => {
|
}
|
||||||
scrollRoot = await findScrollRoot(tree, 0);
|
currentNode = currentNode?.child;
|
||||||
while (!scrollRoot) {
|
if (
|
||||||
this.log('Failed to find scroll root node, reattempting in 5 seconds');
|
currentNode?.memoizedProps?.className &&
|
||||||
await sleep(5000);
|
currentNode?.memoizedProps?.className.startsWith(quickAccessMenuClasses.ViewPlaceholder)
|
||||||
scrollRoot = await findScrollRoot(tree, 0);
|
) {
|
||||||
}
|
self.log(`QAM root was found in ${iters} recursion cycles`);
|
||||||
let newQA: any;
|
return currentNode;
|
||||||
let newQATabRenderer: any;
|
}
|
||||||
this.cNodePatch = afterPatch(scrollRoot.stateNode, 'render', (_: any, ret: any) => {
|
if (!currentNode) return null;
|
||||||
if (!this.quickAccess && ret.props.children.props.children[4]) {
|
if (currentNode.sibling) {
|
||||||
this.quickAccess = ret?.props?.children?.props?.children[4].type;
|
let node = await findQAMRoot(currentNode.sibling, iters + 1);
|
||||||
newQA = (...args: any) => {
|
if (node !== null) return node;
|
||||||
const ret = this.quickAccess.type(...args);
|
}
|
||||||
if (ret) {
|
return await findQAMRoot(currentNode, iters + 1);
|
||||||
if (!newQATabRenderer) {
|
|
||||||
this.tabRenderer = ret.props.children[1].children.type;
|
|
||||||
newQATabRenderer = (...qamArgs: any[]) => {
|
|
||||||
const oFilter = Array.prototype.filter;
|
|
||||||
Array.prototype.filter = function (...args: any[]) {
|
|
||||||
if (isTabsArray(this)) {
|
|
||||||
self.render(this, qamArgs[0].visible);
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
return oFilter.call(this, ...args);
|
|
||||||
};
|
|
||||||
// TODO remove array hack entirely and use this instead const tabs = ret.props.children.props.children[0].props.children[1].props.children[0].props.children[0].props.tabs
|
|
||||||
const ret = this.tabRenderer(...qamArgs);
|
|
||||||
Array.prototype.filter = oFilter;
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
this.rendererTree = ret.props.children[1].children;
|
|
||||||
ret.props.children[1].children.type = newQATabRenderer;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
this.memoizedQuickAccess = memo(newQA);
|
|
||||||
this.memoizedQuickAccess.isDeckyQuickAccess = true;
|
|
||||||
}
|
}
|
||||||
if (ret.props.children.props.children[4]) {
|
(async () => {
|
||||||
this.qAPTree = ret.props.children.props.children[4];
|
qAMRoot = await findQAMRoot(tree, 0);
|
||||||
ret.props.children.props.children[4].type = this.memoizedQuickAccess;
|
while (!qAMRoot) {
|
||||||
}
|
this.error(
|
||||||
return ret;
|
'Failed to find QAM root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.',
|
||||||
});
|
);
|
||||||
this.cNode = scrollRoot;
|
await sleep(5000);
|
||||||
this.cNode.stateNode.forceUpdate();
|
qAMRoot = await findQAMRoot(tree, 0);
|
||||||
this.log('Finished initial injection');
|
}
|
||||||
})();
|
|
||||||
|
while (!qAMRoot?.stateNode?.forceUpdate) {
|
||||||
|
qAMRoot = qAMRoot.return;
|
||||||
|
}
|
||||||
|
qAMRoot.stateNode.shouldComponentUpdate = () => true;
|
||||||
|
qAMRoot.stateNode.forceUpdate();
|
||||||
|
delete qAMRoot.stateNode.shouldComponentUpdate;
|
||||||
|
})();
|
||||||
|
} catch (e) {
|
||||||
|
this.log('Failed to rerender QAM', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit() {
|
deinit() {
|
||||||
this.cNodePatch?.unpatch();
|
Array.prototype.filter = this.oFilter;
|
||||||
if (this.qAPTree) this.qAPTree.type = this.quickAccess;
|
|
||||||
if (this.rendererTree) this.rendererTree.type = this.tabRenderer;
|
|
||||||
if (this.cNode) this.cNode.stateNode.forceUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add(tab: Tab) {
|
add(tab: Tab) {
|
||||||
@@ -136,13 +106,14 @@ class TabsHook extends Logger {
|
|||||||
this.tabs = this.tabs.filter((tab) => tab.id !== id);
|
this.tabs = this.tabs.filter((tab) => tab.id !== id);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(existingTabs: any[], visible: boolean) {
|
render(existingTabs: any[]) {
|
||||||
for (const { title, icon, content, id } of this.tabs) {
|
for (const { title, icon, content, id } of this.tabs) {
|
||||||
existingTabs.push({
|
existingTabs.push({
|
||||||
key: id,
|
key: id,
|
||||||
title,
|
title,
|
||||||
tab: icon,
|
tab: icon,
|
||||||
panel: <QuickAccessVisibleStateProvider visible={visible}>{content}</QuickAccessVisibleStateProvider>,
|
decky: true,
|
||||||
|
panel: <QuickAccessVisibleStateProvider>{content}</QuickAccessVisibleStateProvider>,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+132
-79
@@ -1,8 +1,10 @@
|
|||||||
import { Patch, ToastData, afterPatch, findInReactTree, findModuleChild, sleep } from 'decky-frontend-lib';
|
import { Patch, ToastData, sleep } from 'decky-frontend-lib';
|
||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
|
import DeckyToaster from './components/DeckyToaster';
|
||||||
|
import { DeckyToasterState, DeckyToasterStateContextProvider } from './components/DeckyToasterState';
|
||||||
import Toast from './components/Toast';
|
import Toast from './components/Toast';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
|
import RouterHook from './router-hook';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -13,12 +15,15 @@ declare global {
|
|||||||
|
|
||||||
class Toaster extends Logger {
|
class Toaster extends Logger {
|
||||||
private instanceRetPatch?: Patch;
|
private instanceRetPatch?: Patch;
|
||||||
|
private routerHook: RouterHook;
|
||||||
|
private toasterState: DeckyToasterState = new DeckyToasterState();
|
||||||
private node: any;
|
private node: any;
|
||||||
private settingsModule: any;
|
private settingsModule: any;
|
||||||
private ready: boolean = false;
|
private ready: boolean = false;
|
||||||
|
|
||||||
constructor() {
|
constructor(routerHook: RouterHook) {
|
||||||
super('Toaster');
|
super('Toaster');
|
||||||
|
this.routerHook = routerHook;
|
||||||
|
|
||||||
window.__TOASTER_INSTANCE?.deinit?.();
|
window.__TOASTER_INSTANCE?.deinit?.();
|
||||||
window.__TOASTER_INSTANCE = this;
|
window.__TOASTER_INSTANCE = this;
|
||||||
@@ -26,87 +31,135 @@ class Toaster extends Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
let instance: any;
|
this.routerHook.addGlobalComponent('DeckyToaster', () => (
|
||||||
|
<DeckyToasterStateContextProvider deckyToasterState={this.toasterState}>
|
||||||
while (true) {
|
<DeckyToaster />
|
||||||
instance = findInReactTree(
|
</DeckyToasterStateContextProvider>
|
||||||
(document.getElementById('root') as any)._reactRootContainer._internalRoot.current,
|
));
|
||||||
(x) => x?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder'),
|
// let instance: any;
|
||||||
);
|
// while (true) {
|
||||||
if (instance) break;
|
// instance = findInReactTree(
|
||||||
this.debug('finding instance');
|
// (document.getElementById('root') as any)._reactRootContainer._internalRoot.current,
|
||||||
await sleep(2000);
|
// (x) => x?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder'),
|
||||||
}
|
// );
|
||||||
|
// if (instance) break;
|
||||||
this.node = instance.return.return;
|
// this.debug('finding instance');
|
||||||
let toast: any;
|
// await sleep(2000);
|
||||||
let renderedToast: ReactNode = null;
|
// }
|
||||||
this.node.stateNode.render = (...args: any[]) => {
|
// // const windowManager = findModuleChild((m) => {
|
||||||
const ret = this.node.stateNode.__proto__.render.call(this.node.stateNode, ...args);
|
// // if (typeof m !== 'object') return false;
|
||||||
if (ret) {
|
// // for (let prop in m) {
|
||||||
this.instanceRetPatch = afterPatch(ret, 'type', (_: any, ret: any) => {
|
// // if (m[prop]?.prototype?.GetRenderElement) return m[prop];
|
||||||
if (ret?.props?.children[1]?.children?.props) {
|
// // }
|
||||||
const currentToast = ret.props.children[1].children.props.notification;
|
// // return false;
|
||||||
if (currentToast?.decky) {
|
// // });
|
||||||
if (currentToast == toast) {
|
// this.node = instance.return.return;
|
||||||
ret.props.children[1].children = renderedToast;
|
// let toast: any;
|
||||||
} else {
|
// let renderedToast: ReactNode = null;
|
||||||
toast = currentToast;
|
// console.log(instance, this.node);
|
||||||
renderedToast = <Toast toast={toast} />;
|
// // replacePatch(window.SteamClient.BrowserView, "Destroy", (args: any[]) => {
|
||||||
ret.props.children[1].children = renderedToast;
|
// // console.debug("destroy", args)
|
||||||
}
|
// // return callOriginal;
|
||||||
} else {
|
// // })
|
||||||
toast = null;
|
// // let node = this.node.child.updateQueue.lastEffect;
|
||||||
renderedToast = null;
|
// // while (node.next && !node.deckyPatched) {
|
||||||
}
|
// // node = node.next;
|
||||||
}
|
// // if (node.deps[1] == "notificationtoasts") {
|
||||||
return ret;
|
// // console.log("Deleting destroy");
|
||||||
});
|
// // node.deckyPatched = true;
|
||||||
this.node.stateNode.shouldComponentUpdate = () => {
|
// // node.create = () => {console.debug("VVVVVVVVVVV")};
|
||||||
return false;
|
// // node.destroy = () => {console.debug("AAAAAAAAAAAAAAAAaaaaaaaaaaaaaaa")};
|
||||||
};
|
// // }
|
||||||
delete this.node.stateNode.render;
|
// // }
|
||||||
}
|
// this.node.stateNode.render = (...args: any[]) => {
|
||||||
return ret;
|
// const ret = this.node.stateNode.__proto__.render.call(this.node.stateNode, ...args);
|
||||||
};
|
// console.log('toast', ret);
|
||||||
this.settingsModule = findModuleChild((m) => {
|
// if (ret) {
|
||||||
if (typeof m !== 'object') return undefined;
|
// console.log(ret)
|
||||||
for (let prop in m) {
|
// // this.instanceRetPatch = replacePatch(ret, 'type', (innerArgs: any) => {
|
||||||
if (typeof m[prop]?.settings && m[prop]?.communityPreferences) return m[prop];
|
// // console.log("inner toast", innerArgs)
|
||||||
}
|
// // // @ts-ignore
|
||||||
});
|
// // const oldEffect = window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect;
|
||||||
this.log('Initialized');
|
// // // @ts-ignore
|
||||||
this.ready = true;
|
// // window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect = (effect, deps) => {
|
||||||
|
// // console.log(effect, deps)
|
||||||
|
// // if (deps?.[1] == "notificationtoasts") {
|
||||||
|
// // console.log("run")
|
||||||
|
// // effect();
|
||||||
|
// // }
|
||||||
|
// // return oldEffect(effect, deps);
|
||||||
|
// // }
|
||||||
|
// // const ret = this.instanceRetPatch?.original(...args);
|
||||||
|
// // console.log("inner ret", ret)
|
||||||
|
// // // @ts-ignore
|
||||||
|
// // window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect = oldEffect;
|
||||||
|
// // return ret
|
||||||
|
// // });
|
||||||
|
// }
|
||||||
|
// // console.log("toast ret", ret)
|
||||||
|
// // if (ret?.props?.children[1]?.children?.props) {
|
||||||
|
// // const currentToast = ret.props.children[1].children.props.notification;
|
||||||
|
// // if (currentToast?.decky) {
|
||||||
|
// // if (currentToast == toast) {
|
||||||
|
// // ret.props.children[1].children = renderedToast;
|
||||||
|
// // } else {
|
||||||
|
// // toast = currentToast;
|
||||||
|
// // renderedToast = <Toast toast={toast} />;
|
||||||
|
// // ret.props.children[1].children = renderedToast;
|
||||||
|
// // }
|
||||||
|
// // } else {
|
||||||
|
// // toast = null;
|
||||||
|
// // renderedToast = null;
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
// // return ret;
|
||||||
|
// // });
|
||||||
|
// // }
|
||||||
|
// return ret;
|
||||||
|
// };
|
||||||
|
// this.settingsModule = findModuleChild((m) => {
|
||||||
|
// if (typeof m !== 'object') return undefined;
|
||||||
|
// for (let prop in m) {
|
||||||
|
// if (typeof m[prop]?.settings && m[prop]?.communityPreferences) return m[prop];
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// // const idx = FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.findIndex((x: any) => x.m_ID == "ToastContainer");
|
||||||
|
// // if (idx > -1) {
|
||||||
|
// // FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.splice(idx, 1)
|
||||||
|
// // }
|
||||||
|
// this.node.stateNode.forceUpdate();
|
||||||
|
// this.node.stateNode.shouldComponentUpdate = () => {
|
||||||
|
// return false;
|
||||||
|
// };
|
||||||
|
// this.log('Initialized');
|
||||||
|
// this.ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async toast(toast: ToastData) {
|
toast(toast: ToastData) {
|
||||||
while (!this.ready) {
|
toast.duration = toast.duration || 5e3;
|
||||||
await sleep(100);
|
this.toasterState.addToast(toast);
|
||||||
}
|
// const settings = this.settingsModule?.settings;
|
||||||
const settings = this.settingsModule?.settings;
|
// let toastData = {
|
||||||
let toastData = {
|
// nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
|
||||||
nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
|
// rtCreated: Date.now(),
|
||||||
rtCreated: Date.now(),
|
// eType: 15,
|
||||||
eType: 15,
|
// nToastDurationMS: toast.duration || 5e3,
|
||||||
nToastDurationMS: toast.duration || 5e3,
|
// data: toast,
|
||||||
data: toast,
|
// decky: true,
|
||||||
decky: true,
|
// };
|
||||||
};
|
// // @ts-ignore
|
||||||
// @ts-ignore
|
// toastData.data.appid = () => 0;
|
||||||
toastData.data.appid = () => 0;
|
// if (
|
||||||
if (
|
// (settings?.bDisableAllToasts && !toast.critical) ||
|
||||||
(settings?.bDisableAllToasts && !toast.critical) ||
|
// (settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
|
||||||
(settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
|
// )
|
||||||
)
|
// return;
|
||||||
return;
|
// window.NotificationStore.m_rgNotificationToasts.push(toastData);
|
||||||
window.NotificationStore.m_rgNotificationToasts.push(toastData);
|
// window.NotificationStore.DispatchNextToast();
|
||||||
window.NotificationStore.DispatchNextToast();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit() {
|
deinit() {
|
||||||
this.instanceRetPatch?.unpatch();
|
this.routerHook.removeGlobalComponent('DeckyToaster');
|
||||||
this.node && delete this.node.stateNode.shouldComponentUpdate;
|
|
||||||
this.node && this.node.stateNode.forceUpdate();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user