Compare commits

...

17 Commits

Author SHA1 Message Date
AAGaming 7e1406c0bf add killall to flake
Co-authored-by: Party Wumpus <48649272+PartyWumpus@users.noreply.github.com>
2024-10-11 15:05:16 -04:00
AAGaming 7b32df0948 Add routerhook for desktop UI and a basic sidebar menu for Decky in desktop UI 2024-10-11 15:05:15 -04:00
AAGaming 306b0ff8d6 "fix" react-devtools
works well enough lol
2024-10-11 15:05:15 -04:00
AAGaming dbd7488d8f lint 2024-10-11 15:05:07 -04:00
AAGaming cff3ca504d bump @decky/ui to fix issues on beta 2024-10-11 15:00:54 -04:00
AAGaming 456ccf479a fix dropdownmultiselect on beta 2024-10-11 14:58:37 -04:00
AAGaming 4f05a001fb fix another toString 2024-10-11 14:58:05 -04:00
AAGaming 43aa0497f4 prevent future issues where toString may not be a function (what) 2024-10-11 14:47:20 -04:00
AAGaming 1781c19c11 Fix broken checkboxes on Beta Steam client (#710) 2024-10-04 13:26:00 -04:00
Lukas Senionis 1ef3cb8307 fix(http_request): remove conflicting CORS headers (#708) 2024-10-04 13:15:58 -04:00
WerWolvTranslationBot 5bc4dc684d Translations update from Weblate (#707)
Co-authored-by: ayssia <nynaevealmearah@gmail.com>
2024-10-03 17:28:00 -04:00
AAGaming 4cff530b52 Fix missing components on Oct 2 2024 Steam Beta (#709) 2024-10-03 17:08:35 -04:00
Marco Rodolfi 2f90a4fcf7 Rebase semver parsing on main (#677)
Co-authored-by: Marco Rodolfi <marco.rodolfi.1992@gmail.com>
2024-09-17 06:21:31 -07:00
AAGaming 24fce1e093 bump @decky/ui to include findSP fix 2024-09-16 19:31:04 -04:00
AAGaming 97b12972ee shut ts up 2024-09-16 16:21:00 -04:00
AAGaming f69eb72df9 wait for SteamApp init stage 1 to finish before loading decky's frontend bundle
should fix the startup race condition
2024-09-16 16:17:20 -04:00
WerWolvTranslationBot 24215c0732 Translations update from Weblate (#704)
* Translated using Weblate (German)

Currently translated at 100.0% (158 of 158 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/de/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (158 of 158 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/nl/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (158 of 158 strings)

Translation: Decky/Decky
Translate-URL: https://weblate.werwolv.net/projects/decky/decky/bg/

---------

Co-authored-by: SecularSteve <fairfull.playing@gmail.com>
2024-09-14 23:01:47 -07:00
30 changed files with 1700 additions and 243 deletions
+33 -2
View File
@@ -99,12 +99,14 @@
}
},
"PluginListIndex": {
"freeze": "Замразяване на актуализациите",
"hide": "Бърз достъп: Скриване",
"no_plugin": "Няма инсталирани добавки!",
"plugin_actions": "Действия с добавката",
"reinstall": "Преинсталиране",
"reload": "Презареждане",
"show": "Бърз достъп: Показване",
"unfreeze": "Разрешаване на актуализациите",
"uninstall": "Деинсталиране",
"update_all_one": "Обновяване на 1 добавка",
"update_all_other": "Обновяване на {{count}} добавки",
@@ -192,9 +194,19 @@
"SettingsIndex": {
"developer_title": "Разработчик",
"general_title": "Общи",
"plugins_title": "Добавки"
"plugins_title": "Добавки",
"testing_title": "Тестване"
},
"Store": {
"download_progress_info": {
"download_zip": "Изтегляне на плъгина",
"increment_count": "Увеличаване на броя изтегляния",
"installing_plugin": "Инсталиране на плъгина",
"open_zip": "Отваряне на zip файла",
"parse_zip": "Разглеждане на zip файла",
"start": "Иницииране",
"uninstalling_previous": "Деинсталиране на предишното копие"
},
"store_contrib": {
"desc": "Ако искате да допринесете към магазина за добавки на Decky, разгледайте хранилището SteamDeckHomebrew/decky-plugin-template в GitHub. Може да намерите информация относно разработката и разпространението във файла README.",
"label": "Допринасяне"
@@ -218,9 +230,17 @@
"about": "Относно",
"alph_asce": "По азбучен ред (Я -> А)",
"alph_desc": "По азбучен ред (А -> Я)",
"date_asce": "Най-старият първи",
"date_desc": "Най-новият първи",
"downloads_asce": "Най-малко изтеглени първи",
"downloads_desc": "Най-изтеглени първи",
"title": "Разглеждане"
},
"store_testing_cta": "Помислете дали искате да тествате новите добавки, за да помогнете на екипа на Decky Loader!"
"store_testing_cta": "Помислете дали искате да тествате новите добавки, за да помогнете на екипа на Decky Loader!",
"store_testing_warning": {
"desc": "Можете да използвате този канал на магазина, за да тествате най-новите версии на плъгините. Не забравяйте да оставите обратна връзка в GitHub, за да може плъгинът да бъде актуализиран за всички потребители.",
"label": "Добре дошли в канала на магазина за тестване"
}
},
"StoreSelect": {
"custom_store": {
@@ -234,6 +254,17 @@
"testing": "Тестване"
}
},
"Testing": {
"download": "Изтегляне",
"error": "Грешка при инсталирането на PR",
"header": "Следните версии на Decky Loader са създадени от отворени заявки за изтегляне от трети страни. Екипът на Decky Loader не е проверявал тяхната функционалност или сигурност и е възможно те да са остарели.",
"loading": "Зареждане на отворени заявки за изтегляне...",
"start_download_toast": "Изтегляне на PR #{{id}}"
},
"TitleView": {
"decky_store_desc": "Отворете Decky Store",
"settings_desc": "Отворете настройките на Decky"
},
"Updater": {
"decky_updates": "Обновления на Decky",
"no_patch_notes_desc": "няма бележки за промените в тази версия",
+14 -1
View File
@@ -198,6 +198,15 @@
"testing_title": "Testen"
},
"Store": {
"download_progress_info": {
"download_zip": "Plugin herunterladen",
"increment_count": "Erhöhen der Downloadanzahl",
"installing_plugin": "Plugin installieren",
"open_zip": "Öffnen der Zip-Datei",
"parse_zip": "Parsen der Zip-Datei",
"start": "Initialisieren",
"uninstalling_previous": "Vorherige Kopie deinstallieren"
},
"store_contrib": {
"desc": "Wenn du Erweiterungen im Decky Store veröffentlichen willst, besuche die SteamDeckHomebrew/decky-plugin-template Repository auf GitHub. Informationen rund um Entwicklung und Veröffentlichung findest du in der README.",
"label": "Mitwirken"
@@ -246,7 +255,11 @@
}
},
"Testing": {
"download": "Download"
"download": "Download",
"error": "Fehler beim Installieren von PR",
"header": "Die folgenden Versionen von Decky Loader wurden aus offenen Pull Requests von Dritten erstellt. Das Decky Loader-Team hat ihre Funktionalität oder Sicherheit nicht überprüft, und sie können veraltet sein.",
"loading": "Offene Pull Requests laden...",
"start_download_toast": "Herunterladen von PR #{{id}}"
},
"TitleView": {
"decky_store_desc": "Decky Store Öffnen",
+19 -6
View File
@@ -2,9 +2,9 @@
"BranchSelect": {
"update_channel": {
"label": "Updatekanaal",
"prerelease": "Prerelease",
"prerelease": "Vooruitgave",
"stable": "Stabiel",
"testing": "Testing"
"testing": "Testen"
}
},
"Developer": {
@@ -188,7 +188,7 @@
"header": "Overige"
},
"updates": {
"header": "Updates"
"header": "Bijwerkingen"
}
},
"SettingsIndex": {
@@ -198,6 +198,15 @@
"testing_title": "Testen"
},
"Store": {
"download_progress_info": {
"download_zip": "Plugin downloaden",
"increment_count": "Aantal downloads verhogen",
"installing_plugin": "Plugin installeren",
"open_zip": "Zip-bestand openen",
"parse_zip": "Zip-bestand parseren",
"start": "Initialiseren",
"uninstalling_previous": "Vorige kopie verwijderen"
},
"store_contrib": {
"desc": "Als je wilt bijdragen aan de Decky Plugin Store, kijk dan in de SteamDeckHomebrew/decky-plugin-template repository op GitHub. Informatie over ontwikkeling en distributie is beschikbaar in de README.",
"label": "Bijdragen"
@@ -242,11 +251,15 @@
"custom": "Aangepast",
"default": "Standaard",
"label": "Winkelkanaal",
"testing": "Testing"
"testing": "Testen"
}
},
"Testing": {
"download": "Downloaden"
"download": "Downloaden",
"error": "Fout bij installatie van PR",
"header": "De volgende versies van Decky Loader zijn gebouwd op basis van open Pull Requests van derden. Het Decky Loader-team heeft hun functionaliteit of veiligheid niet gecontroleerd en ze kunnen verouderd zijn.",
"loading": "Openstaande Pull Requests laden...",
"start_download_toast": "PR #{{id}} downloaden"
},
"TitleView": {
"decky_store_desc": "Decky Store openen",
@@ -261,7 +274,7 @@
"checking": "Bezig met controleren op updates",
"cur_version": "Huidige versie: {{ver}}",
"install_button": "Bijwerken",
"label": "Updates",
"label": "Bijwerkingen",
"lat_version": "Bijwerkt: versie {{ver}}",
"reloading": "Bezig met herstarten",
"updating": "Bezig met bijwerken"
+14 -1
View File
@@ -191,6 +191,15 @@
"testing_title": "测试"
},
"Store": {
"download_progress_info": {
"download_zip": "正在下载插件",
"increment_count": "正在计入下载次数",
"installing_plugin": "正在安装插件",
"open_zip": "正在打开 ZIP 文件",
"parse_zip": "正在解析 ZIP 文件",
"start": "正在初始化",
"uninstalling_previous": "正在卸载之前的版本"
},
"store_contrib": {
"desc": "如果你想要提交你的插件到 Decky 插件商店,请访问 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 存储库。有关开发和分发插件的信息,请查看 README 文件。",
"label": "贡献"
@@ -239,7 +248,11 @@
}
},
"Testing": {
"download": "下载"
"download": "下载",
"error": "安装 PR 时出错",
"header": "以下版本的 Decky Loader 是根据开放的第三方 Pull Request 构建的。Decky Loader 团队尚未验证这些版本的功能或安全性,且它们可能已经过期。",
"loading": "正在加载尚未合并的 Pull Request ...",
"start_download_toast": "正在下载 PR #{{id}}"
},
"TitleView": {
"decky_store_desc": "打开 Decky 商店",
+44 -16
View File
@@ -9,7 +9,7 @@ from traceback import format_exc
from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType]
from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection
from aiohttp import ClientSession
from aiohttp import ClientSession, hdrs
from aiohttp.web import Request, StreamResponse, Response, json_response, post
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
@@ -153,14 +153,14 @@ class Utilities:
headers["User-Agent"] = helpers.user_agent
for excluded_header in excluded_default_headers:
self.logger.debug(f"Excluding default header {excluded_header}")
if excluded_header in headers:
self.logger.debug(f"Excluding default header {excluded_header}: {headers[excluded_header]}")
del headers[excluded_header]
if "X-Decky-Fetch-Excluded-Headers" in req.headers:
for excluded_header in req.headers["X-Decky-Fetch-Excluded-Headers"].split(", "):
self.logger.debug(f"Excluding header {excluded_header}")
if excluded_header in headers:
self.logger.debug(f"Excluding header {excluded_header}: {headers[excluded_header]}")
del headers[excluded_header]
for header in req.headers:
@@ -187,7 +187,21 @@ class Utilities:
# defeat the point of this proxy.
async with ClientSession(auto_decompress=False) as web:
async with web.request(req.method, url, headers=headers, data=body, ssl=helpers.get_ssl_context()) as web_res:
res = StreamResponse(headers=web_res.headers, status=web_res.status)
# Whenever the aiohttp_cors is used, it expects a near complete control over whatever headers are needed
# for `aiohttp_cors.ResourceOptions`. As a server, if you delegate CORS handling to aiohttp_cors,
# the headers below must NOT be set. Otherwise they would be overwritten by aiohttp_cors and there would be
# logic bugs, so it was probably a smart choice to assert if the headers are present.
#
# However, this request handler method does not act like our own local server, it always acts like a proxy
# where we do not have control over the response headers. For responses that do not allow CORS, we add the support
# via aiohttp_cors. For responses that allow CORS, we have to remove the conflicting headers to allow
# aiohttp_cors handle it for us as if there was no CORS support.
aiohttp_cors_compatible_headers = web_res.headers.copy()
aiohttp_cors_compatible_headers.popall(hdrs.ACCESS_CONTROL_ALLOW_ORIGIN, default=None)
aiohttp_cors_compatible_headers.popall(hdrs.ACCESS_CONTROL_ALLOW_CREDENTIALS, default=None)
aiohttp_cors_compatible_headers.popall(hdrs.ACCESS_CONTROL_EXPOSE_HEADERS, default=None)
res = StreamResponse(headers=aiohttp_cors_compatible_headers, status=web_res.status)
if web_res.headers.get('Transfer-Encoding', '').lower() == 'chunked':
res.enable_chunked_encoding()
@@ -415,17 +429,28 @@ class Utilities:
async with ClientSession() as web:
res = await web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context())
script = """
if (!window.deckyHasConnectedRDT) {
window.deckyHasConnectedRDT = true;
// This fixes the overlay when hovering over an element in RDT
Object.defineProperty(window, '__REACT_DEVTOOLS_TARGET_WINDOW__', {
enumerable: true,
configurable: true,
get: function() {
return (GamepadNavTree?.m_context?.m_controller || FocusNavController)?.m_ActiveContext?.ActiveWindow || window;
}
});
""" + await res.text() + "\n}"
try {
if (!window.deckyHasConnectedRDT) {
window.deckyHasConnectedRDT = true;
// This fixes the overlay when hovering over an element in RDT
Object.defineProperty(window, '__REACT_DEVTOOLS_TARGET_WINDOW__', {
enumerable: true,
configurable: true,
get: function() {
return window?.DFL?.findSP?.() || window;
}
});
""" + await res.text() + """
// they broke the script so we have to do this ourselves
ReactDevToolsBackend.initialize({
appendComponentStack: true,
breakOnConsoleErrors: false,
showInlineWarningsAndErrors: true,
hideConsoleLogsInStrictMode: false
});
ReactDevToolsBackend.connectToDevTools({port: 8097, host: 'localhost', useHttps: false});
} } catch(e) {console.error('RDT LOAD ERROR', e);}console.log('LOADED RDT');
"""
if res.status != 200:
self.logger.error("Failed to connect to React DevTools at " + ip)
return False
@@ -433,7 +458,10 @@ class Utilities:
self.logger.info("Connected to React DevTools, loading script")
tab = await get_gamepadui_tab()
# RDT needs to load before React itself to work.
await close_old_tabs()
try:
await close_old_tabs()
except Exception:
pass
result = await tab.reload_and_evaluate(script)
self.logger.info(result)
+2
View File
@@ -40,6 +40,8 @@
nodePackages.pnpm
poetry
jq
electron_30-bin
killall
# fixes local pyright not being able to see the pythonpath properly.
(pkgs.writeShellScriptBin "pyright" ''
${pkgs.pyright}/bin/pyright --pythonpath `which python3` "$@" '')
+5 -2
View File
@@ -10,7 +10,8 @@
"lint": "prettier -c src",
"typecheck": "tsc --noEmit",
"format": "prettier -c src -w",
"localize": "i18next"
"localize": "i18next",
"react-devtools": "electron node_modules/react-devtools/app"
},
"devDependencies": {
"@decky/api": "^1.1.1",
@@ -31,6 +32,7 @@
"prettier": "^3.3.2",
"prettier-plugin-import-sort": "^0.0.7",
"react": "18.3.1",
"react-devtools": "^6.0.0",
"react-dom": "18.3.1",
"rollup": "^4.18.0",
"rollup-plugin-delete": "^2.0.0",
@@ -47,7 +49,8 @@
}
},
"dependencies": {
"@decky/ui": "^4.7.1",
"@decky/ui": "^4.8.1",
"compare-versions": "^6.1.1",
"filesize": "^10.1.2",
"i18next": "^23.11.5",
"i18next-http-backend": "^2.5.2",
+1098 -9
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,72 @@
import { FC, useEffect, useRef, useState } from 'react';
import { useDeckyState } from './DeckyState';
import PluginView from './PluginView';
import { QuickAccessVisibleState } from './QuickAccessVisibleState';
const DeckyDesktopSidebar: FC = () => {
const { desktopMenuOpen, setDesktopMenuOpen } = useDeckyState();
const [closed, setClosed] = useState<boolean>(!desktopMenuOpen);
const [openAnimStart, setOpenAnimStart] = useState<boolean>(desktopMenuOpen);
const closedInterval = useRef<number | null>(null);
useEffect(() => {
const anim = requestAnimationFrame(() => setOpenAnimStart(desktopMenuOpen));
return () => cancelAnimationFrame(anim);
}, [desktopMenuOpen]);
useEffect(() => {
closedInterval.current && clearTimeout(closedInterval.current);
if (desktopMenuOpen) {
setClosed(false);
} else {
closedInterval.current = setTimeout(() => setClosed(true), 500);
}
}, [desktopMenuOpen]);
return (
<>
<div
className="deckyDesktopSidebarDim"
style={{
position: 'absolute',
height: 'calc(100% - 78px - 50px)',
width: '100%',
top: '78px',
left: '0px',
zIndex: 998,
background: 'rgba(0, 0, 0, 0.7)',
opacity: openAnimStart ? 1 : 0,
display: desktopMenuOpen || !closed ? 'flex' : 'none',
transition: 'opacity 0.4s cubic-bezier(0.65, 0, 0.35, 1)',
}}
onClick={() => setDesktopMenuOpen(false)}
/>
<div
className="deckyDesktopSidebar"
style={{
position: 'absolute',
height: 'calc(100% - 78px - 50px)',
width: '350px',
paddingLeft: '16px',
top: '78px',
right: '0px',
zIndex: 999,
transition: 'transform 0.4s cubic-bezier(0.65, 0, 0.35, 1)',
transform: openAnimStart ? 'translateX(0px)' : 'translateX(366px)',
overflowY: 'scroll',
// prevents chromium border jank
display: desktopMenuOpen || !closed ? 'flex' : 'none',
flexDirection: 'column',
background: '#171d25',
}}
>
<QuickAccessVisibleState.Provider value={desktopMenuOpen || !closed}>
<PluginView desktop={true} />
</QuickAccessVisibleState.Provider>
</div>
</>
);
};
export default DeckyDesktopSidebar;
@@ -0,0 +1,44 @@
import { CSSProperties, FC } from 'react';
import DeckyDesktopSidebar from './DeckyDesktopSidebar';
import DeckyIcon from './DeckyIcon';
import { useDeckyState } from './DeckyState';
const DeckyDesktopUI: FC = () => {
const { desktopMenuOpen, setDesktopMenuOpen } = useDeckyState();
return (
<>
<style>
{`
.deckyDesktopIcon {
color: #67707b;
}
.deckyDesktopIcon:hover {
color: #fff;
}
`}
</style>
<DeckyIcon
className="deckyDesktopIcon"
width={24}
height={24}
onClick={() => setDesktopMenuOpen(!desktopMenuOpen)}
style={
{
position: 'absolute',
top: '36px', // nav text is 34px but 36px looks nicer to me
right: '10px', // <- is 16px but 10px looks nicer to me
width: '24px',
height: '24px',
cursor: 'pointer',
transition: 'color 0.3s linear',
'-webkit-app-region': 'no-drag',
} as CSSProperties
}
/>
<DeckyDesktopSidebar />
</>
);
};
export default DeckyDesktopUI;
@@ -1,12 +1,17 @@
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import { UIMode } from '../enums';
interface PublicDeckyGlobalComponentsState {
components: Map<string, FC>;
components: Map<UIMode, Map<string, FC>>;
}
export class DeckyGlobalComponentsState {
// TODO a set would be better
private _components = new Map<string, FC>();
private _components = new Map<UIMode, Map<string, FC>>([
[UIMode.BigPicture, new Map()],
[UIMode.Desktop, new Map()],
]);
public eventBus = new EventTarget();
@@ -14,13 +19,19 @@ export class DeckyGlobalComponentsState {
return { components: this._components };
}
addComponent(path: string, component: FC) {
this._components.set(path, component);
addComponent(path: string, component: FC, uiMode: UIMode) {
const components = this._components.get(uiMode);
if (!components) throw new Error(`UI mode ${uiMode} not supported.`);
components.set(path, component);
this.notifyUpdate();
}
removeComponent(path: string) {
this._components.delete(path);
removeComponent(path: string, uiMode: UIMode) {
const components = this._components.get(uiMode);
if (!components) throw new Error(`UI mode ${uiMode} not supported.`);
components.delete(path);
this.notifyUpdate();
}
@@ -30,8 +41,8 @@ export class DeckyGlobalComponentsState {
}
interface DeckyGlobalComponentsContext extends PublicDeckyGlobalComponentsState {
addComponent(path: string, component: FC): void;
removeComponent(path: string): void;
addComponent(path: string, component: FC, uiMode: UIMode): void;
removeComponent(path: string, uiMode: UIMode): void;
}
const DeckyGlobalComponentsContext = createContext<DeckyGlobalComponentsContext>(null as any);
+20 -10
View File
@@ -1,6 +1,8 @@
import { ComponentType, FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import type { RouteProps } from 'react-router';
import { UIMode } from '../enums';
export interface RouterEntry {
props: Omit<RouteProps, 'path' | 'children'>;
component: ComponentType;
@@ -10,12 +12,16 @@ export type RoutePatch = (route: RouteProps) => RouteProps;
interface PublicDeckyRouterState {
routes: Map<string, RouterEntry>;
routePatches: Map<string, Set<RoutePatch>>;
routePatches: Map<UIMode, Map<string, Set<RoutePatch>>>;
}
export class DeckyRouterState {
private _routes = new Map<string, RouterEntry>();
private _routePatches = new Map<string, Set<RoutePatch>>();
// Update when support for new UIModes is added
private _routePatches = new Map<UIMode, Map<string, Set<RoutePatch>>>([
[UIMode.BigPicture, new Map()],
[UIMode.Desktop, new Map()],
]);
public eventBus = new EventTarget();
@@ -28,22 +34,26 @@ export class DeckyRouterState {
this.notifyUpdate();
}
addPatch(path: string, patch: RoutePatch) {
let patchList = this._routePatches.get(path);
addPatch(path: string, patch: RoutePatch, uiMode: UIMode) {
const patchesForMode = this._routePatches.get(uiMode);
if (!patchesForMode) throw new Error(`UI mode ${uiMode} not supported.`);
let patchList = patchesForMode.get(path);
if (!patchList) {
patchList = new Set();
this._routePatches.set(path, patchList);
patchesForMode.set(path, patchList);
}
patchList.add(patch);
this.notifyUpdate();
return patch;
}
removePatch(path: string, patch: RoutePatch) {
const patchList = this._routePatches.get(path);
removePatch(path: string, patch: RoutePatch, uiMode: UIMode) {
const patchesForMode = this._routePatches.get(uiMode);
if (!patchesForMode) throw new Error(`UI mode ${uiMode} not supported.`);
const patchList = patchesForMode.get(path);
patchList?.delete(patch);
if (patchList?.size == 0) {
this._routePatches.delete(path);
patchesForMode.delete(path);
}
this.notifyUpdate();
}
@@ -60,8 +70,8 @@ export class DeckyRouterState {
interface DeckyRouterStateContext extends PublicDeckyRouterState {
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props']): void;
addPatch(path: string, patch: RoutePatch): RoutePatch;
removePatch(path: string, patch: RoutePatch): void;
addPatch(path: string, patch: RoutePatch, uiMode?: UIMode): RoutePatch;
removePatch(path: string, patch: RoutePatch, uiMode?: UIMode): void;
removeRoute(path: string): void;
}
+11
View File
@@ -17,6 +17,7 @@ interface PublicDeckyState {
versionInfo: VerInfo | null;
notificationSettings: NotificationSettings;
userInfo: UserInfo | null;
desktopMenuOpen: boolean;
}
export interface UserInfo {
@@ -36,6 +37,7 @@ export class DeckyState {
private _versionInfo: VerInfo | null = null;
private _notificationSettings = DEFAULT_NOTIFICATION_SETTINGS;
private _userInfo: UserInfo | null = null;
private _desktopMenuOpen: boolean = false;
public eventBus = new EventTarget();
@@ -52,6 +54,7 @@ export class DeckyState {
versionInfo: this._versionInfo,
notificationSettings: this._notificationSettings,
userInfo: this._userInfo,
desktopMenuOpen: this._desktopMenuOpen,
};
}
@@ -115,6 +118,11 @@ export class DeckyState {
this.notifyUpdate();
}
setDesktopMenuOpen(open: boolean) {
this._desktopMenuOpen = open;
this.notifyUpdate();
}
private notifyUpdate() {
this.eventBus.dispatchEvent(new Event('update'));
}
@@ -126,6 +134,7 @@ interface DeckyStateContext extends PublicDeckyState {
setActivePlugin(name: string): void;
setPluginOrder(pluginOrder: string[]): void;
closeActivePlugin(): void;
setDesktopMenuOpen(open: boolean): void;
}
const DeckyStateContext = createContext<DeckyStateContext>(null as any);
@@ -155,6 +164,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
const setDesktopMenuOpen = deckyState.setDesktopMenuOpen.bind(deckyState);
return (
<DeckyStateContext.Provider
@@ -165,6 +175,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
setActivePlugin,
closeActivePlugin,
setPluginOrder,
setDesktopMenuOpen,
}}
>
{children}
+5
View File
@@ -24,6 +24,11 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
props.onDismiss?.();
Navigation.NavigateToExternalWeb(aRef.current!.href);
}}
onClick={(e) => {
e.preventDefault();
props.onDismiss?.();
Navigation.NavigateToExternalWeb(aRef.current!.href);
}}
style={{ display: 'inline' }}
>
<a ref={aRef} {...nodeProps.node.properties}>
+7 -3
View File
@@ -9,7 +9,11 @@ import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView';
const PluginView: FC = () => {
interface PluginViewProps {
desktop?: boolean;
}
const PluginView: FC<PluginViewProps> = ({ desktop = false }) => {
const { hiddenPlugins } = useDeckyState();
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
const visible = useQuickAccessVisible();
@@ -27,7 +31,7 @@ const PluginView: FC = () => {
if (activePlugin) {
return (
<Focusable onCancelButton={closeActivePlugin}>
<TitleView />
<TitleView desktop={desktop} />
<div style={{ height: '100%', paddingTop: '16px' }}>
<ErrorBoundary>{(visible || activePlugin.alwaysRender) && activePlugin.content}</ErrorBoundary>
</div>
@@ -36,7 +40,7 @@ const PluginView: FC = () => {
}
return (
<>
<TitleView />
<TitleView desktop={desktop} />
<div
style={{
paddingTop: '16px',
@@ -1,6 +1,6 @@
import { FC, ReactNode, createContext, useContext, useState } from 'react';
const QuickAccessVisibleState = createContext<boolean>(false);
export const QuickAccessVisibleState = createContext<boolean>(false);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
+21 -8
View File
@@ -14,18 +14,34 @@ const titleStyles: CSSProperties = {
top: '0px',
};
const TitleView: FC = () => {
const { activePlugin, closeActivePlugin } = useDeckyState();
interface TitleViewProps {
desktop?: boolean;
}
const TitleView: FC<TitleViewProps> = ({ desktop }) => {
const { activePlugin, closeActivePlugin, setDesktopMenuOpen } = useDeckyState();
const { t } = useTranslation();
const onSettingsClick = () => {
Navigation.Navigate('/decky/settings');
Navigation.CloseSideMenus();
setDesktopMenuOpen(false);
};
const onStoreClick = () => {
Navigation.Navigate('/decky/store');
Navigation.CloseSideMenus();
setDesktopMenuOpen(false);
};
const buttonStyles = {
height: '28px',
width: '40px',
minWidth: 0,
padding: desktop ? '' : '10px 12px',
display: 'flex',
alignItems: desktop ? 'center' : '',
justifyContent: desktop ? 'center' : '',
};
if (activePlugin === null) {
@@ -33,14 +49,14 @@ const TitleView: FC = () => {
<Focusable style={titleStyles} className={staticClasses.Title}>
<div style={{ marginRight: 'auto', flex: 0.9 }}>Decky</div>
<DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
style={buttonStyles}
onClick={onStoreClick}
onOKActionDescription={t('TitleView.decky_store_desc')}
>
<FaStore style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton>
<DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
style={buttonStyles}
onClick={onSettingsClick}
onOKActionDescription={t('TitleView.settings_desc')}
>
@@ -52,10 +68,7 @@ const TitleView: FC = () => {
return (
<Focusable className={staticClasses.Title} style={titleStyles}>
<DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
onClick={closeActivePlugin}
>
<DialogButton style={buttonStyles} onClick={closeActivePlugin}>
<FaArrowLeft style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton>
{activePlugin?.titleView || <div style={{ flex: 0.9 }}>{activePlugin.name}</div>}
@@ -2,11 +2,11 @@ import {
DialogButton,
DialogCheckbox,
DialogCheckboxProps,
Export,
Marquee,
Menu,
MenuItem,
findModuleExport,
Module,
findModule,
showContextMenu,
} from '@decky/ui';
import { FC, useCallback, useEffect, useState } from 'react';
@@ -14,9 +14,7 @@ import { useTranslation } from 'react-i18next';
import { FaChevronDown } from 'react-icons/fa';
// TODO add to dfl
const dropDownControlButtonClass = findModuleExport((e: Export) =>
e?.toString()?.includes('gamepaddropdown_DropDownControlButton'),
);
const dropDownControlButtonClasses = findModule((m: Module) => m?.DropDownControlButton && m?.['duration-app-launch']);
const DropdownMultiselectItem: FC<
{
@@ -76,7 +74,7 @@ const DropdownMultiselect: FC<{
alignItems: 'center',
maxWidth: '100%',
}}
className={dropDownControlButtonClass}
className={dropDownControlButtonClasses?.DropDownControlButton}
onClick={(evt) => {
evt.preventDefault();
showContextMenu(
@@ -80,7 +80,10 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
onOK={async () => {
setLoading(true);
await onOK();
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
setTimeout(() => {
Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky);
DeckyPluginLoader.setDesktopMenuOpen(true);
}, 250);
setTimeout(() => DeckyPluginLoader.checkPluginUpdates(), 1000);
}}
onCancel={async () => {
@@ -51,7 +51,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
onOK={async () => {
setLoading(true);
await onOK();
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
setTimeout(() => {
Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky);
DeckyPluginLoader.setDesktopMenuOpen(true);
}, 250);
setTimeout(() => DeckyPluginLoader.checkPluginUpdates(), 1000);
}}
onCancel={async () => {
+16 -1
View File
@@ -53,5 +53,20 @@ export default function SettingsPage() {
},
];
return <SidebarNavigation pages={pages} />;
return (
<div className="deckySettingsHeightHack">
<style>
{/* hacky fix to work around height: 720px in desktop ui */}
{`
.deckySettingsHeightHack {
height: 100% !important;
}
.deckySettingsHeightHack > div {
height: 100% !important;
}
`}
</style>
<SidebarNavigation pages={pages} />
</div>
);
}
@@ -6,8 +6,8 @@ import {
Focusable,
ProgressBarWithInfo,
Spinner,
findSP,
showModal,
useWindowRef,
} from '@decky/ui';
import { Suspense, lazy, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -21,45 +21,48 @@ import WithSuspense from '../../../WithSuspense';
const MarkdownRenderer = lazy(() => import('../../../Markdown'));
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
const SP = findSP();
const [outerRef, win] = useWindowRef<HTMLDivElement>();
const { t } = useTranslation();
// TODO proper desktop scrolling
return (
<Focusable onCancelButton={closeModal}>
<Focusable ref={outerRef} onCancelButton={closeModal}>
<FocusRing>
<Carousel
fnItemRenderer={(id: number) => (
<Focusable
style={{
marginTop: '40px',
height: 'calc( 100% - 40px )',
overflowY: 'scroll',
display: 'flex',
justifyContent: 'center',
margin: '40px',
}}
>
<div>
<h1>{versionInfo?.all?.[id]?.name || 'Invalid Update Name'}</h1>
{versionInfo?.all?.[id]?.body ? (
<WithSuspense>
<MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer>
</WithSuspense>
) : (
t('Updater.no_patch_notes_desc')
)}
</div>
</Focusable>
)}
fnGetId={(id) => id}
nNumItems={versionInfo?.all?.length}
nHeight={SP.innerHeight - 40}
nItemHeight={SP.innerHeight - 40}
nItemMarginX={0}
initialColumn={0}
autoFocus={true}
fnGetColumnWidth={() => SP.innerWidth}
name={t('Updater.decky_updates') as string}
/>
{win && (
<Carousel
fnItemRenderer={(id: number) => (
<Focusable
style={{
marginTop: '40px',
height: 'calc( 100% - 40px )',
overflowY: 'scroll',
display: 'flex',
justifyContent: 'center',
margin: '40px',
}}
>
<div>
<h1>{versionInfo?.all?.[id]?.name || 'Invalid Update Name'}</h1>
{versionInfo?.all?.[id]?.body ? (
<WithSuspense>
<MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer>
</WithSuspense>
) : (
t('Updater.no_patch_notes_desc')
)}
</div>
</Focusable>
)}
fnGetId={(id) => id}
nNumItems={versionInfo?.all?.length}
nHeight={(win?.innerHeight || 800) - 40}
nItemHeight={(win?.innerHeight || 800) - 40}
nItemMarginX={0}
initialColumn={0}
autoFocus={true}
fnGetColumnWidth={() => win?.innerHeight || 1280}
name={t('Updater.decky_updates') as string}
/>
)}
</FocusRing>
</Focusable>
);
@@ -72,6 +75,8 @@ export default function UpdaterSettings() {
const [updateProgress, setUpdateProgress] = useState<number>(-1);
const [reloading, setReloading] = useState<boolean>(false);
const [windowRef, win] = useWindowRef<HTMLDivElement>();
const { t } = useTranslation();
useEffect(() => {
@@ -91,11 +96,12 @@ export default function UpdaterSettings() {
}, []);
const showPatchNotes = useCallback(() => {
showModal(<PatchNotesModal versionInfo={versionInfo} />);
}, [versionInfo]);
// TODO set width and height on desktop - needs fixing in DFL?
showModal(<PatchNotesModal versionInfo={versionInfo} />, win!);
}, [versionInfo, win]);
return (
<>
<div ref={windowRef}>
<Field
onOptionsActionDescription={versionInfo?.all ? t('Updater.patch_notes_desc') : undefined}
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
@@ -164,6 +170,6 @@ export default function UpdaterSettings() {
</Suspense>
</InlinePatchNotes>
)}
</>
</div>
);
}
+4
View File
@@ -0,0 +1,4 @@
export enum UIMode {
BigPicture = 4,
Desktop = 7,
}
+8 -18
View File
@@ -5,29 +5,19 @@ interface Window {
}
(async () => {
// Wait for main webpack chunks to definitely be loaded
console.time('[Decky:Boot] Waiting for main Webpack chunks...');
while (!window.webpackChunksteamui || window.webpackChunksteamui.length < 5) {
await new Promise((r) => setTimeout(r, 10)); // Can't use DFL sleep here.
}
console.timeEnd('[Decky:Boot] Waiting for main Webpack chunks...');
console.debug('[Decky:Boot] Frontend init');
// Wait for the React root to be mounted
console.time('[Decky:Boot] Waiting for React root mount...');
let root;
while (
// Does React root node exist?
!(root = document.getElementById('root')) ||
// Does it have a child element?
!(root as any)[Object.keys(root).find((k) => k.startsWith('__reactContainer$')) as string].child
) {
await new Promise((r) => setTimeout(r, 10)); // Can't use DFL sleep here.
console.time('[Decky:Boot] Waiting for SteamApp init stage 1 to finish...');
// @ts-expect-error TODO type BFinishedInitStageOne in @decky/ui
while (!window.App?.BFinishedInitStageOne()) {
await new Promise((r) => setTimeout(r, 0)); // Can't use DFL sleep here.
}
console.timeEnd('[Decky:Boot] Waiting for React root mount...');
console.timeEnd('[Decky:Boot] Waiting for SteamApp init stage 1 to finish...');
if (!window.SP_REACT) {
console.debug('[Decky:Boot] Setting up Webpack & React globals...');
await new Promise((r) => setTimeout(r, 500)); // Can't use DFL sleep here.
// deliberate partial import
const DFLWebpack = await import('@decky/ui/dist/webpack');
window.SP_REACT = DFLWebpack.findModule((m) => m.Component && m.PureComponent && m.useLayoutEffect);
+38 -7
View File
@@ -1,4 +1,4 @@
import { ToastNotification } from '@decky/api';
import type { ToastNotification } from '@decky/api';
import {
ModalRoot,
Navigation,
@@ -13,6 +13,7 @@ import {
import { FC, lazy } from 'react';
import { FaDownload, FaExclamationCircle, FaPlug } from 'react-icons/fa';
import DeckyDesktopUI from './components/DeckyDesktopUI';
import DeckyIcon from './components/DeckyIcon';
import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from './components/DeckyState';
import { File, FileSelectionType } from './components/modals/filepicker';
@@ -24,13 +25,14 @@ import NotificationBadge from './components/NotificationBadge';
import PluginView from './components/PluginView';
import { useQuickAccessVisible } from './components/QuickAccessVisibleState';
import WithSuspense from './components/WithSuspense';
import { UIMode } from './enums';
import ErrorBoundaryHook from './errorboundary-hook';
import { FrozenPluginService } from './frozen-plugins-service';
import { HiddenPluginsService } from './hidden-plugins-service';
import Logger from './logger';
import { NotificationService } from './notification-service';
import { InstallType, Plugin, PluginLoadType } from './plugin';
import RouterHook, { UIMode } from './router-hook';
import RouterHook from './router-hook';
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
import { checkForPluginUpdates } from './store';
import TabsHook from './tabs-hook';
@@ -160,6 +162,21 @@ class PluginLoader extends Logger {
);
});
// needs the 1s wait or the entire app becomes drag target lol
sleep(1000).then(() =>
this.routerHook.addGlobalComponent(
'DeckyDesktopUI',
() => {
return (
<DeckyStateContextProvider deckyState={this.deckyState}>
<DeckyDesktopUI />
</DeckyStateContextProvider>
);
},
UIMode.Desktop,
),
);
initSteamFixes();
initFilepickerPatches();
@@ -168,8 +185,9 @@ class PluginLoader extends Logger {
Promise.all([this.getUserInfo(), this.updateVersion()])
.then(() => this.loadPlugins())
.then(() => this.checkPluginUpdates())
.then(() => this.log('Initialized'));
.then(() => this.log('Initialized'))
.then(() => sleep(30000)) // Internet might not immediately be up
.then(() => this.checkPluginUpdates());
}
private checkForSP(): boolean {
@@ -361,6 +379,7 @@ class PluginLoader extends Logger {
public deinit() {
this.routerHook.removeRoute('/decky/store');
this.routerHook.removeRoute('/decky/settings');
this.routerHook.removeGlobalComponent('DeckyDesktopUI', UIMode.Desktop);
deinitSteamFixes();
deinitFilepickerPatches();
this.routerHook.deinit();
@@ -626,8 +645,8 @@ class PluginLoader extends Logger {
// Things will break *very* badly if plugin code touches this outside of @decky/api, so lets make that clear.
window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit = {
connect: (version: number, pluginName: string) => {
if (version < 1 || version > 2) {
console.warn(`Plugin ${pluginName} requested unsupported api version ${version}.`);
if (version < 1 || version > 3) {
console.warn(`Plugin ${pluginName} requested unsupported API version ${version}.`);
}
const eventListeners: listenerMap = new Map();
@@ -670,12 +689,20 @@ class PluginLoader extends Logger {
_version: 1,
} as any;
// adds useQuickAccessVisible
if (version >= 2) {
backendAPI._version = 2;
backendAPI.useQuickAccessVisible = useQuickAccessVisible;
}
this.debug(`${pluginName} connected to loader API.`);
// adds uiMode param to route patching and global component functions. no functional changes, but we should warn anyway.
if (version >= 3) {
backendAPI._version = 3;
}
this.debug(
`${pluginName} connected to loader API version ${backendAPI._version} (requested version ${version}).`,
);
return backendAPI;
},
};
@@ -732,6 +759,10 @@ class PluginLoader extends Logger {
return pluginAPI;
}
public setDesktopMenuOpen(open: boolean) {
this.deckyState.setDesktopMenuOpen(open);
}
}
export default PluginLoader;
+131 -96
View File
@@ -6,7 +6,9 @@ import {
findInTree,
findModuleByExport,
getReactRoot,
injectFCTrampoline,
sleep,
wrapReactType,
} from '@decky/ui';
import { FC, ReactElement, ReactNode, cloneElement, createElement } from 'react';
import type { Route } from 'react-router';
@@ -23,6 +25,7 @@ import {
RouterEntry,
useDeckyRouterState,
} from './components/DeckyRouterState';
import { UIMode } from './enums';
import Logger from './logger';
declare global {
@@ -31,18 +34,18 @@ declare global {
}
}
export enum UIMode {
BigPicture = 4,
Desktop = 7,
}
const isPatched = Symbol('is patched');
class RouterHook extends Logger {
private routerState: DeckyRouterState = new DeckyRouterState();
private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
private renderedComponents: ReactElement[] = [];
private renderedComponents = new Map<UIMode, ReactElement[]>([
[UIMode.BigPicture, []],
[UIMode.Desktop, []],
]);
private Route: any;
private DesktopRoute: any;
private wrappedDesktopLibraryMemo?: any;
private DeckyGamepadRouterWrapper = this.gamepadRouterWrapper.bind(this);
private DeckyDesktopRouterWrapper = this.desktopRouterWrapper.bind(this);
private DeckyGlobalComponentsWrapper = this.globalComponentsWrapper.bind(this);
@@ -76,6 +79,21 @@ class RouterHook extends Logger {
this.error('Failed to find router stack module');
}
const routerModule = findModuleByExport((e) => e?.displayName == 'Router');
if (routerModule) {
this.DesktopRoute = Object.values(routerModule).find(
(e) =>
typeof e == 'function' &&
e?.prototype?.render?.toString()?.includes('props.computedMatch') &&
e?.prototype?.render?.toString()?.includes('.Children.count('),
);
if (!this.DesktopRoute) {
this.error('Failed to find DesktopRoute component');
}
} else {
this.error('Failed to find router module, desktop routes will not work');
}
this.modeChangeRegistration = SteamClient.UI.RegisterForUIModeChanged((mode: UIMode) => {
this.debug(`UI mode changed to ${mode}`);
if (this.patchedModes.has(mode)) return;
@@ -87,10 +105,10 @@ class RouterHook extends Logger {
this.patchGamepadRouter();
break;
// Not fully implemented yet
// case UIMode.Desktop:
// this.debug("Patching desktop router");
// this.patchDesktopRouter();
// break;
case UIMode.Desktop:
this.debug('Patching desktop router');
this.patchDesktopRouter();
break;
default:
this.warn(`Router patch not implemented for UI mode ${mode}`);
break;
@@ -109,7 +127,7 @@ class RouterHook extends Logger {
await this.waitForUnlock();
let routerNode = findRouterNode();
while (!routerNode) {
this.warn('Failed to find Router node, reattempting in 5 seconds.');
this.warn('Failed to find GamepadUI Router node, reattempting in 5 seconds.');
await sleep(5000);
await this.waitForUnlock();
routerNode = findRouterNode();
@@ -130,50 +148,34 @@ class RouterHook extends Logger {
}
}
// Currently unused
// @ts-expect-error 6133
private async patchDesktopRouter() {
const root = getReactRoot(document.getElementById('root') as any);
const findRouterNode = () =>
findInReactTree(root, (node) => node?.elementType?.type?.toString()?.includes('bShowDesktopUIContent:'));
findInReactTree(root, (node) => {
const typeStr = node?.elementType?.toString?.();
return (
typeStr &&
typeStr?.includes('.IsMainDesktopWindow') &&
typeStr?.includes('.IN_STEAMUI_SHARED_CONTEXT') &&
typeStr?.includes('.ContentFrame') &&
typeStr?.includes('.Console()')
);
});
let routerNode = findRouterNode();
while (!routerNode) {
this.warn('Failed to find Router node, reattempting in 5 seconds.');
this.warn('Failed to find DesktopUI Router node, reattempting in 5 seconds.');
await sleep(5000);
routerNode = findRouterNode();
}
if (routerNode) {
// this.debug("desktop router node", routerNode);
// Patch the component globally
this.desktopRouterPatch = afterPatch(routerNode.elementType, 'type', this.handleDesktopRouterRender.bind(this));
// Swap out the current instance
routerNode.type = routerNode.elementType.type;
if (routerNode?.alternate) {
routerNode.alternate.type = routerNode.type;
}
const patchedRenderer = injectFCTrampoline(routerNode.elementType);
this.desktopRouterPatch = afterPatch(patchedRenderer, 'component', this.handleDesktopRouterRender.bind(this));
// Force a full rerender via our custom error boundary
const errorBoundaryNode = findInTree(routerNode, (e) => e?.stateNode?._deckyForceRerender, {
walkable: ['return'],
});
errorBoundaryNode?.stateNode?._deckyForceRerender?.();
// this.debug("desktop router node", routerNode);
// // Patch the component globally
// this.desktopRouterPatch = afterPatch(routerNode.type.prototype, 'render', this.handleDesktopRouterRender.bind(this));
// const stateNodeClone = { render: routerNode.stateNode.render } as any;
// // Patch the current instance. render is readonly so we have to do this.
// Object.assign(stateNodeClone, routerNode.stateNode);
// Object.setPrototypeOf(stateNodeClone, Object.getPrototypeOf(routerNode.stateNode));
// this.desktopRouterFirstInstancePatch = afterPatch(stateNodeClone, 'render', this.handleDesktopRouterRender.bind(this));
// routerNode.stateNode = stateNodeClone;
// // Swap out the current instance
// if (routerNode?.alternate) {
// routerNode.alternate.type = routerNode.type;
// routerNode.alternate.stateNode = routerNode.stateNode;
// }
// routerNode.stateNode.forceUpdate();
// Force a full rerender via our custom error boundary
// const errorBoundaryNode = findInTree(routerNode, e => e?.stateNode?._deckyForceRerender, { walkable: ["return"] });
// errorBoundaryNode?.stateNode?._deckyForceRerender?.();
}
}
@@ -197,10 +199,18 @@ class RouterHook extends Logger {
const returnVal = (
<>
<DeckyRouterStateContextProvider deckyRouterState={this.routerState}>
<style>
{`
.deckyDesktopDialogPaddingHack + * .DialogContent_InnerWidth {
max-width: unset !important;
}
`}
</style>
<div className="deckyDesktopDialogPaddingHack" />
<DeckyDesktopRouterWrapper>{ret}</DeckyDesktopRouterWrapper>
</DeckyRouterStateContextProvider>
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
<DeckyGlobalComponentsWrapper />
<DeckyGlobalComponentsWrapper uiMode={UIMode.Desktop} />
</DeckyGlobalComponentsStateContextProvider>
</>
);
@@ -220,7 +230,7 @@ class RouterHook extends Logger {
<DeckyGamepadRouterWrapper>{ret}</DeckyGamepadRouterWrapper>
</DeckyRouterStateContextProvider>
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
<DeckyGlobalComponentsWrapper />
<DeckyGlobalComponentsWrapper uiMode={UIMode.BigPicture} />
</DeckyGlobalComponentsStateContextProvider>
</>
);
@@ -228,13 +238,21 @@ class RouterHook extends Logger {
return returnVal;
}
private globalComponentsWrapper() {
private globalComponentsWrapper({ uiMode }: { uiMode: UIMode }) {
const { components } = useDeckyGlobalComponentsState();
if (this.renderedComponents.length != components.size) {
this.debug('Rerendering global components');
this.renderedComponents = Array.from(components.values()).map((GComponent) => <GComponent />);
const componentsForMode = components.get(uiMode);
if (!componentsForMode) {
this.warn(`Couldn't find global components map for uimode ${uiMode}`);
return null;
}
return <>{this.renderedComponents}</>;
if (!this.renderedComponents.has(uiMode) || this.renderedComponents.get(uiMode)?.length != componentsForMode.size) {
this.debug('Rerendering global components for uiMode', uiMode);
this.renderedComponents.set(
uiMode,
Array.from(componentsForMode.values()).map((GComponent) => <GComponent />),
);
}
return <>{this.renderedComponents.get(uiMode)}</>;
}
private gamepadRouterWrapper({ children }: { children: ReactElement }) {
@@ -248,8 +266,8 @@ class RouterHook extends Logger {
}
const mainRouteList = children.props.children[0].props.children;
const ingameRouteList = children.props.children[1].props.children; // /appoverlay and /apprunning
this.processList(mainRouteList, routes, routePatches, true);
this.processList(ingameRouteList, null, routePatches, false);
this.processList(mainRouteList, routes, routePatches.get(UIMode.BigPicture), true, this.Route);
this.processList(ingameRouteList, null, routePatches.get(UIMode.BigPicture), false, this.Route);
this.debug('Rerendered gamepadui routes list');
return children;
@@ -257,22 +275,38 @@ class RouterHook extends Logger {
private desktopRouterWrapper({ children }: { children: ReactElement }) {
// Used to store the new replicated routes we create to allow routes to be unpatched.
this.debug('desktop router wrapper render', children);
const { routes, routePatches } = useDeckyRouterState();
const routeList = findInReactTree(
const mainRouteList = findInReactTree(
children,
(node) => node?.length > 2 && node?.find((elem: any) => elem?.props?.path == '/library/home'),
(node) => node?.length > 2 && node?.find((elem: any) => elem?.props?.path == '/console'),
);
if (!routeList) {
if (!mainRouteList) {
this.debug('routerWrapper wrong component?', children);
return children;
}
const library = children.props.children[1].props.children.props;
if (!Array.isArray(library.children)) {
library.children = [library.children];
this.processList(mainRouteList, routes, routePatches.get(UIMode.Desktop), true, this.DesktopRoute);
const libraryRouteWrapper = mainRouteList.find(
(r: any) => r?.props && 'cm' in r.props && 'bShowDesktopUIContent' in r.props,
);
if (!this.wrappedDesktopLibraryMemo) {
wrapReactType(libraryRouteWrapper);
afterPatch(libraryRouteWrapper.type, 'type', (_, ret) => {
const { routePatches } = useDeckyRouterState();
const libraryRouteList = findInReactTree(
ret,
(node) => node?.length > 1 && node?.find((elem: any) => elem?.props?.path == '/library/downloads'),
);
if (!libraryRouteList) {
this.warn('failed to find library route list', ret);
return ret;
}
this.processList(libraryRouteList, null, routePatches.get(UIMode.Desktop), false, this.DesktopRoute);
return ret;
});
this.wrappedDesktopLibraryMemo = libraryRouteWrapper.type;
} else {
libraryRouteWrapper.type = this.wrappedDesktopLibraryMemo;
}
this.debug('library', library);
this.processList(library.children, routes, routePatches, true);
this.debug('Rerendered desktop routes list');
return children;
@@ -280,11 +314,11 @@ class RouterHook extends Logger {
private processList(
routeList: any[],
routes: Map<string, RouterEntry> | null,
routePatches: Map<string, Set<RoutePatch>>,
routes: Map<string, RouterEntry> | null | undefined,
routePatches: Map<string, Set<RoutePatch>> | null | undefined,
save: boolean,
RouteComponent: any,
) {
const Route = this.Route;
this.debug('Route list: ', routeList);
if (save) this.routes = routeList;
let routerIndex = routeList.length;
@@ -294,59 +328,60 @@ class RouterHook extends Logger {
const newRouterArray: (ReactElement | JSX.Element)[] = [];
routes.forEach(({ component, props }, path) => {
newRouterArray.push(
<Route path={path} {...props}>
<RouteComponent path={path} {...props}>
<ErrorBoundary>{createElement(component)}</ErrorBoundary>
</Route>,
</RouteComponent>,
);
});
routeList[routerIndex] = newRouterArray;
}
}
routeList.forEach((route: Route, index: number) => {
const replaced = this.toReplace.get(route?.props?.path as string);
if (replaced) {
routeList[index].props.children = replaced;
this.toReplace.delete(route?.props?.path as string);
}
if (route?.props?.path && routePatches.has(route.props.path as string)) {
this.toReplace.set(
route?.props?.path as string,
// @ts-ignore
routeList[index].props.children,
);
routePatches.get(route.props.path as string)?.forEach((patch) => {
const oType = routeList[index].props.children.type;
routeList[index].props.children = patch({
...routeList[index].props,
children: {
...cloneElement(routeList[index].props.children),
type: routeList[index].props.children[isPatched] ? oType : (props) => createElement(oType, props),
},
}).children;
routeList[index].props.children[isPatched] = true;
});
}
});
routePatches &&
routeList.forEach((route: Route, index: number) => {
const replaced = this.toReplace.get(route?.props?.path as string);
if (replaced) {
routeList[index].props.children = replaced;
this.toReplace.delete(route?.props?.path as string);
}
if (route?.props?.path && routePatches.has(route.props.path as string)) {
this.toReplace.set(
route?.props?.path as string,
// @ts-ignore
routeList[index].props.children,
);
routePatches.get(route.props.path as string)?.forEach((patch) => {
const oType = routeList[index].props.children.type;
routeList[index].props.children = patch({
...routeList[index].props,
children: {
...cloneElement(routeList[index].props.children),
type: routeList[index].props.children[isPatched] ? oType : (props) => createElement(oType, props),
},
}).children;
routeList[index].props.children[isPatched] = true;
});
}
});
}
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) {
this.routerState.addRoute(path, component, props);
}
addPatch(path: string, patch: RoutePatch) {
return this.routerState.addPatch(path, patch);
addPatch(path: string, patch: RoutePatch, uiMode: UIMode = UIMode.BigPicture) {
return this.routerState.addPatch(path, patch, uiMode);
}
addGlobalComponent(name: string, component: FC) {
this.globalComponentsState.addComponent(name, component);
addGlobalComponent(name: string, component: FC, uiMode: UIMode = UIMode.BigPicture) {
this.globalComponentsState.addComponent(name, component, uiMode);
}
removeGlobalComponent(name: string) {
this.globalComponentsState.removeComponent(name);
removeGlobalComponent(name: string, uiMode: UIMode = UIMode.BigPicture) {
this.globalComponentsState.removeComponent(name, uiMode);
}
removePatch(path: string, patch: RoutePatch) {
this.routerState.removePatch(path, patch);
removePatch(path: string, patch: RoutePatch, uiMode: UIMode = UIMode.BigPicture) {
this.routerState.removePatch(path, patch, uiMode);
}
removeRoute(path: string) {
+11 -1
View File
@@ -1,3 +1,5 @@
import { compare } from 'compare-versions';
import { InstallType, Plugin, installPlugin, installPlugins } from './plugin';
import { getSetting, setSetting } from './utils/settings';
@@ -137,7 +139,15 @@ export async function checkForPluginUpdates(plugins: Plugin[]): Promise<PluginUp
const updateMap = new Map<string, StorePluginVersion>();
for (let plugin of plugins) {
const remotePlugin = serverData?.find((x) => x.name == plugin.name);
if (remotePlugin && remotePlugin.versions?.length > 0 && plugin.version != remotePlugin?.versions?.[0]?.name) {
//FIXME: Ugly hack since plugin.version might be null during evaluation,
//so this will set the older version possible
const curVer = plugin.version ? plugin.version : '0.0';
if (
remotePlugin &&
remotePlugin.versions?.length > 0 &&
plugin.version != remotePlugin?.versions?.[0]?.name &&
compare(remotePlugin?.versions?.[0]?.name, curVer, '>')
) {
updateMap.set(plugin.name, remotePlugin.versions[0]);
}
}
+2 -2
View File
@@ -41,9 +41,9 @@ class TabsHook extends Logger {
init() {
// TODO patch the "embedded" renderer in this module too (seems to be for VR? unsure)
const qamModule = findModuleByExport((e) => e?.type?.toString()?.includes('QuickAccessMenuBrowserView'));
const qamModule = findModuleByExport((e) => e?.type?.toString?.()?.includes('QuickAccessMenuBrowserView'));
const qamRenderer = Object.values(qamModule).find((e: any) =>
e?.type?.toString()?.includes('QuickAccessMenuBrowserView'),
e?.type?.toString?.()?.includes('QuickAccessMenuBrowserView'),
);
const patchHandler = createReactTreePatcher(
+1 -1
View File
@@ -28,7 +28,7 @@ class Toaster extends Logger {
window.__TOASTER_INSTANCE?.deinit?.();
window.__TOASTER_INSTANCE = this;
const ValveToastRenderer = findModuleExport((e) => e?.toString()?.includes(`controller:"notification",method:`));
const ValveToastRenderer = findModuleExport((e) => e?.toString?.()?.includes(`controller:"notification",method:`));
// TODO find a way to undo this if possible?
const patchedRenderer = injectFCTrampoline(ValveToastRenderer);
this.toastPatch = replacePatch(patchedRenderer, 'component', (args: any[]) => {
+1 -1
View File
@@ -30,7 +30,7 @@ while :; do
if [[ $NEWTARGET != "" ]] && [[ $NEWTARGET != $TARGET ]]; then
echo found new tab at $NEWTARGET
TARGET=$NEWTARGET
TARGETURL="devtools://devtools/bundled/inspector.html?remoteFrontend=true&ws=$ADDR/devtools/page/$TARGET"
TARGETURL="http://$ADDR/devtools/inspector.html?ws=$ADDR/devtools/page/$TARGET"
LOCALTARGET=$(echo '{"id": 1, "method": "Target.createTarget", "params": {"background": true, "url": "'$TARGETURL'"}}
{"id": 2, "method": "Target.closeTarget", "params": {"targetId": "'$LOCALTARGET'"}}' \