mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-12 16:44:02 +03:00
* First iteration for internationalization of the loader * First iteration for internationalization of the loader * Cleanup node mess * Cleanup node mess pt2 * Additional touches * Latest decky changed merged into i18n and updated translation. * Styling fixes * Initial backend hosting implementation * Added correct url path of the loopback server. * Added correct url path of the loopback server. * Some better namespaced text. * Added whitelist for locales path. * Refactor languages and fix hooks logic bugs. * Small typo in language translation structure. * Working backend, automatically swtich languages with steam and language fixes. * Fix to languages * Key fixes * Additional language fixes. * Additional json changes * Final text revision and added a vscode tasks to automatically extract text from code. * Typo in the middleware * Remove unused imports * Cleanup whitespaces. * Import changes * Revert "Import changes" This reverts commit8e8231950f. * Update index.d.ts * Clean up unused imports * Delete pnpm-lock.yaml * Update rollup.config.js * Update PluginInstallModal.tsx * Update index.tsx * Update plugin-loader.tsx * Update plugin-loader.tsx * Revert "Delete pnpm-lock.yaml" This reverts commit3a39f36f21. * Additional strings reworks. * Fixes for issues coming from github merge. * Fixes for master * Styling fixes * Styling pt2 * Missed a few strings in master, * Styling fixes * Additional master merge fixes. * Final cleanup and adaptation to master. * Final empty language cleanup and few string added * Small changes to italian translation * Disabled translation on a few components inside plugin-loader for missing react hooks. * Fixed passing tag to translation. * Disable debug output for reducing console spam. * Return correct content type * Small italian language change * Added support for country code * Fixed missing translation for uninstall popup. * Fix class name shenanigans for toast notification * Update dependencies * Fixed github workflow to include the new locales folder * Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up * Missed a file name change * Updated dev dependencies to latest version * Missed a few dev dependencies * Revert "Update dependencies to latest version (unless it's React) and fixed the new small errors that cropped up" Messed up merge with a different main branch * Messed up deletion of rollup config. * Fix broken pnpm lock file * Missed a localized string during the merge * Fixed a parameter mistake in the uninstall text parameter * Fix pnpm random issues * Small italian language tweaks * Fix wrong parameter passed to the uninstall function call * Another fix on a wrong function parameter * Additional translation text on the store and branch selection channels * Changed the default type passed to map to being able to index the two arrays. * Reverted and reworked the last changes * Distinguish events in UI for installing vs reinstalling plugins * Additional fixes for reinstall prompt * Revert the use of intevalPlural since the parser doesn't seem to support that. * Missed a routing path in the backend * Small bugfixes * Small fixes * Correctly adding the parameter to the request headers. * Refactoring of the UI popup modal * Fix pnpm shenanigans * Final fixes for the install UI localization * Clean up unnedeed backend code * Small rework on text selection. * Cleaned up parser configuration * Removed extracttext dependency to pnpmsetup * Merged translation and cleaned up parser * Fixed JSON structure after manual merge. * Added translation to the file picker * Revert changes to PluginInstallModal * Reworked the text modal for the final time * Missed the proper linted text * Missed the backend change * Final branch cleanup * Fixed small translation bleeding Caused from the manual merge of _old.json files. * fix extra space in browser.py * fix extra newline in plugin-loader.tsx * Cleanup i18next-parser.config.mjs * Update plugin-loader.tsx * Cleanup language files * Better labeling of text * Fixed language typos in BranchSelect * Fixed language typos in StoreSelect * Cleanup plugin-loader.tsx from unused imports * Removed the path bypass since I'm using authentication from the frontend. * Reimplemented this component as a functional component. * Updated dependencies and lockfile * Removed static route from main.py Already handled in loader.py * Small italian coherency fixes * Fix small typography fixes on plugin name uninstall * Fixed italian typo on removal popup * Reenabled manual escaping value in i18next * Set to fallback to the default language if the string in the JSON file is empty. * Fixed pnpm wankery * Added a missed italian text translation string --------- Co-authored-by: AAGaming <aa@mail.catvibers.me>
284 lines
10 KiB
Python
284 lines
10 KiB
Python
import uuid
|
|
import os
|
|
from json.decoder import JSONDecodeError
|
|
from traceback import format_exc
|
|
|
|
from asyncio import sleep, start_server, gather, open_connection
|
|
from aiohttp import ClientSession, web
|
|
|
|
from logging import getLogger
|
|
from injector import inject_to_tab, get_gamepadui_tab, close_old_tabs
|
|
import helpers
|
|
import subprocess
|
|
from localplatform import service_stop, service_start
|
|
|
|
class Utilities:
|
|
def __init__(self, context) -> None:
|
|
self.context = context
|
|
self.util_methods = {
|
|
"ping": self.ping,
|
|
"http_request": self.http_request,
|
|
"install_plugin": self.install_plugin,
|
|
"cancel_plugin_install": self.cancel_plugin_install,
|
|
"confirm_plugin_install": self.confirm_plugin_install,
|
|
"uninstall_plugin": self.uninstall_plugin,
|
|
"execute_in_tab": self.execute_in_tab,
|
|
"inject_css_into_tab": self.inject_css_into_tab,
|
|
"remove_css_from_tab": self.remove_css_from_tab,
|
|
"allow_remote_debugging": self.allow_remote_debugging,
|
|
"disallow_remote_debugging": self.disallow_remote_debugging,
|
|
"set_setting": self.set_setting,
|
|
"get_setting": self.get_setting,
|
|
"filepicker_ls": self.filepicker_ls,
|
|
"disable_rdt": self.disable_rdt,
|
|
"enable_rdt": self.enable_rdt
|
|
}
|
|
|
|
self.logger = getLogger("Utilities")
|
|
|
|
self.rdt_proxy_server = None
|
|
self.rdt_script_id = None
|
|
self.rdt_proxy_task = None
|
|
|
|
if context:
|
|
context.web_app.add_routes([
|
|
web.post("/methods/{method_name}", self._handle_server_method_call)
|
|
])
|
|
|
|
async def _handle_server_method_call(self, request):
|
|
method_name = request.match_info["method_name"]
|
|
try:
|
|
args = await request.json()
|
|
except JSONDecodeError:
|
|
args = {}
|
|
res = {}
|
|
try:
|
|
r = await self.util_methods[method_name](**args)
|
|
res["result"] = r
|
|
res["success"] = True
|
|
except Exception as e:
|
|
res["result"] = str(e)
|
|
res["success"] = False
|
|
return web.json_response(res)
|
|
|
|
async def install_plugin(self, artifact="", name="No name", version="dev", hash=False, install_type=0):
|
|
return await self.context.plugin_browser.request_plugin_install(
|
|
artifact=artifact,
|
|
name=name,
|
|
version=version,
|
|
hash=hash,
|
|
install_type=install_type
|
|
)
|
|
|
|
async def confirm_plugin_install(self, request_id):
|
|
return await self.context.plugin_browser.confirm_plugin_install(request_id)
|
|
|
|
def cancel_plugin_install(self, request_id):
|
|
return self.context.plugin_browser.cancel_plugin_install(request_id)
|
|
|
|
async def uninstall_plugin(self, name):
|
|
return await self.context.plugin_browser.uninstall_plugin(name)
|
|
|
|
async def http_request(self, method="", url="", **kwargs):
|
|
async with ClientSession() as web:
|
|
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs)
|
|
text = await res.text()
|
|
return {
|
|
"status": res.status,
|
|
"headers": dict(res.headers),
|
|
"body": text
|
|
}
|
|
|
|
async def ping(self, **kwargs):
|
|
return "pong"
|
|
|
|
async def execute_in_tab(self, tab, run_async, code):
|
|
try:
|
|
result = await inject_to_tab(tab, code, run_async)
|
|
if "exceptionDetails" in result["result"]:
|
|
return {
|
|
"success": False,
|
|
"result": result["result"]
|
|
}
|
|
|
|
return {
|
|
"success": True,
|
|
"result": result["result"]["result"].get("value")
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"result": e
|
|
}
|
|
|
|
async def inject_css_into_tab(self, tab, style):
|
|
try:
|
|
css_id = str(uuid.uuid4())
|
|
|
|
result = await inject_to_tab(tab,
|
|
f"""
|
|
(function() {{
|
|
const style = document.createElement('style');
|
|
style.id = "{css_id}";
|
|
document.head.append(style);
|
|
style.textContent = `{style}`;
|
|
}})()
|
|
""", False)
|
|
|
|
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_from_tab(self, tab, css_id):
|
|
try:
|
|
result = await inject_to_tab(tab,
|
|
f"""
|
|
(function() {{
|
|
let style = document.getElementById("{css_id}");
|
|
|
|
if (style.nodeName.toLowerCase() == 'style')
|
|
style.parentNode.removeChild(style);
|
|
}})()
|
|
""", False)
|
|
|
|
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_setting(self, key, default):
|
|
return self.context.settings.getSetting(key, default)
|
|
|
|
async def set_setting(self, key, value):
|
|
return self.context.settings.setSetting(key, value)
|
|
|
|
async def allow_remote_debugging(self):
|
|
await service_start(helpers.REMOTE_DEBUGGER_UNIT)
|
|
return True
|
|
|
|
async def disallow_remote_debugging(self):
|
|
await service_stop(helpers.REMOTE_DEBUGGER_UNIT)
|
|
return True
|
|
|
|
async def filepicker_ls(self, path, include_files=True):
|
|
# def sorter(file): # Modification time
|
|
# if os.path.isdir(os.path.join(path, file)) or os.path.isfile(os.path.join(path, file)):
|
|
# return os.path.getmtime(os.path.join(path, file))
|
|
# return 0
|
|
# file_names = sorted(os.listdir(path), key=sorter, reverse=True) # TODO provide more sort options
|
|
file_names = sorted(os.listdir(path)) # Alphabetical
|
|
|
|
files = []
|
|
|
|
for file in file_names:
|
|
full_path = os.path.join(path, file)
|
|
is_dir = os.path.isdir(full_path)
|
|
|
|
if is_dir or include_files:
|
|
files.append({
|
|
"isdir": is_dir,
|
|
"name": file,
|
|
"realpath": os.path.realpath(full_path)
|
|
})
|
|
|
|
return {
|
|
"realpath": os.path.realpath(path),
|
|
"files": files
|
|
}
|
|
|
|
# Based on https://stackoverflow.com/a/46422554/13174603
|
|
def start_rdt_proxy(self, ip, port):
|
|
async def pipe(reader, writer):
|
|
try:
|
|
while not reader.at_eof():
|
|
writer.write(await reader.read(2048))
|
|
finally:
|
|
writer.close()
|
|
async def handle_client(local_reader, local_writer):
|
|
try:
|
|
remote_reader, remote_writer = await open_connection(
|
|
ip, port)
|
|
pipe1 = pipe(local_reader, remote_writer)
|
|
pipe2 = pipe(remote_reader, local_writer)
|
|
await gather(pipe1, pipe2)
|
|
finally:
|
|
local_writer.close()
|
|
|
|
self.rdt_proxy_server = start_server(handle_client, "127.0.0.1", port)
|
|
self.rdt_proxy_task = self.context.loop.create_task(self.rdt_proxy_server)
|
|
|
|
def stop_rdt_proxy(self):
|
|
if self.rdt_proxy_server:
|
|
self.rdt_proxy_server.close()
|
|
self.rdt_proxy_task.cancel()
|
|
|
|
async def _enable_rdt(self):
|
|
# TODO un-hardcode port
|
|
try:
|
|
self.stop_rdt_proxy()
|
|
ip = self.context.settings.getSetting("developer.rdt.ip", None)
|
|
|
|
if ip != None:
|
|
self.logger.info("Connecting to React DevTools at " + ip)
|
|
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}"
|
|
if res.status != 200:
|
|
self.logger.error("Failed to connect to React DevTools at " + ip)
|
|
return False
|
|
self.start_rdt_proxy(ip, 8097)
|
|
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()
|
|
result = await tab.reload_and_evaluate(script)
|
|
self.logger.info(result)
|
|
|
|
except Exception:
|
|
self.logger.error("Failed to connect to React DevTools")
|
|
self.logger.error(format_exc())
|
|
|
|
async def enable_rdt(self):
|
|
self.context.loop.create_task(self._enable_rdt())
|
|
|
|
async def disable_rdt(self):
|
|
self.logger.info("Disabling React DevTools")
|
|
tab = await get_gamepadui_tab()
|
|
self.rdt_script_id = None
|
|
await close_old_tabs()
|
|
await tab.evaluate_js("location.reload();", False, True, False)
|
|
self.logger.info("React DevTools disabled")
|