mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Compare commits
35 Commits
v2.3.1-pre2
...
v2.4.8
| Author | SHA1 | Date | |
|---|---|---|---|
| 880b4c2f8f | |||
| 34af340009 | |||
| 80b6115f6f | |||
| 3bed83697e | |||
| 0ffef6e4bf | |||
| 8810a014f3 | |||
| 385552451b | |||
| c2c9d11c66 | |||
| 0474095a40 | |||
| 346f80beb3 | |||
| 2a6bf75f02 | |||
| f73918c902 | |||
| ea35af2050 | |||
| 6232e3da58 | |||
| 35e46f9ccb | |||
| 2b9a80c151 | |||
| a90ed38c89 | |||
| 3653cf5640 | |||
| 0db45ca71e | |||
| 16681fabb5 | |||
| c210523a22 | |||
| 5d8601347a | |||
| 1e02fcf394 | |||
| f923306a7f | |||
| 478fe32527 | |||
| 50764600c8 | |||
| aec7063139 | |||
| c9ee98e0c0 | |||
| 093b064a4e | |||
| 2955681975 | |||
| de42639726 | |||
| 17742e947a | |||
| 898271b33d | |||
| b44896524f | |||
| db7bb236d8 |
@@ -0,0 +1,73 @@
|
||||
name: Bug report
|
||||
description: File a bug/issue
|
||||
title: "[BUG] <title>"
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: low-effort-checks
|
||||
attributes:
|
||||
label: Please confirm
|
||||
description: Issues without all checks may be ignored/closed.
|
||||
options:
|
||||
- label: I have searched existing issues
|
||||
- label: This issue is not a duplicate of an existing one
|
||||
- label: I have checked the [common issues section in the readme file](https://github.com/SteamDeckHomebrew/decky-loader#-common-issues)
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug Report Description
|
||||
description: A clear and concise description of what the bug is and if possible, the steps you used to get to the bug. If appropriate, include screenshots or videos.
|
||||
placeholder: |
|
||||
When I try to use ...
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behaviour
|
||||
description: A brief description of the expected behavior.
|
||||
placeholder: It should be ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: SteamOS version
|
||||
# description: Can be found with `uname -a`
|
||||
# placeholder: "Linux steamdeck 5.13.0-valve36-1-neptune #1 SMP PREEMPT Mon, 19 Dec 2022 23:39:41 +0000 x86_64 GNU/Linux"
|
||||
placeholder: "SteamOS 3.4.3 Stable"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Selected Update Channel
|
||||
description: Which branch of Decky are you on?
|
||||
multiple: false
|
||||
options:
|
||||
- Stable
|
||||
- Prerelease
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Have you modified the read-only filesystem at any point?
|
||||
description: Describe how here, if you haven't done anything you can leave this blank
|
||||
placeholder: Yes, I've installed neofetch via pacman.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Logs
|
||||
description: Please reboot your deck (if possible) when attempting to recreate the issue, then run ``cd ~ && journalctl -b0 -u plugin_loader.service > deckylog.txt``. This will save the log file to ``~`` aka ``/home/deck``. Please upload the file here
|
||||
placeholder: deckylog.txt
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Steam Deck Homebrew Discord Server
|
||||
url: https://discord.gg/ZU74G2NJzk
|
||||
about: Please ask and answer questions here.
|
||||
@@ -31,7 +31,7 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Build PluginLoader
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Print input
|
||||
|
||||
Vendored
+33
-6
@@ -14,7 +14,9 @@
|
||||
"label": "localrun",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"dependsOn" : ["buildall"],
|
||||
"dependsOn": [
|
||||
"buildall"
|
||||
],
|
||||
"detail": "Check for local runs, create a plugins folder",
|
||||
"command": "mkdir -p plugins",
|
||||
"problemMatcher": []
|
||||
@@ -48,6 +50,16 @@
|
||||
"command": "cd frontend && pnpm i",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"script": "watch",
|
||||
"type": "npm",
|
||||
"path": "frontend",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"label": "watchfrontend",
|
||||
"detail": "rollup -c -w",
|
||||
"isBackground": true
|
||||
},
|
||||
{
|
||||
"label": "buildfrontend",
|
||||
"type": "npm",
|
||||
@@ -55,8 +67,7 @@
|
||||
"detail": "rollup -c",
|
||||
"script": "build",
|
||||
"path": "frontend",
|
||||
"problemMatcher": [],
|
||||
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "buildall",
|
||||
@@ -95,7 +106,9 @@
|
||||
"detail": "Run indev PluginLoader on Deck",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"dependsOn" : ["checkforsettings"],
|
||||
"dependsOn": [
|
||||
"checkforsettings"
|
||||
],
|
||||
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PLUGIN_PATH=${config:deckdir}/homebrew/dev/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/services; echo '${config:deckpass}' | sudo -SE python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py'",
|
||||
"problemMatcher": []
|
||||
},
|
||||
@@ -108,6 +121,20 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
// ALL-IN-ONES
|
||||
{
|
||||
"label": "deployandrun",
|
||||
"detail": "Deploy and run, skipping JS build. Useful when combined with npm:watch",
|
||||
"dependsOrder": "sequence",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"dependsOn": [
|
||||
"deploy",
|
||||
"runpydeck"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "updateremote",
|
||||
"detail": "Build and deploy",
|
||||
@@ -115,7 +142,7 @@
|
||||
"group": "none",
|
||||
"dependsOn": [
|
||||
"buildall",
|
||||
"deploy",
|
||||
"deploy"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
@@ -152,4 +179,4 @@
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -55,7 +55,7 @@ class PluginBrowser:
|
||||
pluginBinPath = path.join(pluginBasePath, 'bin')
|
||||
|
||||
if access(packageJsonPath, R_OK):
|
||||
with open(packageJsonPath, 'r') as f:
|
||||
with open(packageJsonPath, "r", encoding="utf-8") as f:
|
||||
packageJson = json.load(f)
|
||||
if "remote_binary" in packageJson and len(packageJson["remote_binary"]) > 0:
|
||||
# create bin directory if needed.
|
||||
@@ -93,7 +93,7 @@ class PluginBrowser:
|
||||
def find_plugin_folder(self, name):
|
||||
for folder in listdir(self.plugin_path):
|
||||
try:
|
||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), 'r') as f:
|
||||
with open(path.join(self.plugin_path, folder, 'plugin.json'), "r", encoding="utf-8") as f:
|
||||
plugin = json.load(f)
|
||||
|
||||
if plugin['name'] == name:
|
||||
|
||||
+21
-11
@@ -1,3 +1,5 @@
|
||||
import grp
|
||||
import pwd
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
@@ -56,6 +58,14 @@ def get_user() -> str:
|
||||
raise ValueError("helpers.get_user method called before user variable was set. Run helpers.set_user first.")
|
||||
return user
|
||||
|
||||
#Get the user owner of the given file path.
|
||||
def get_user_owner(file_path) -> str:
|
||||
return pwd.getpwuid(os.stat(file_path).st_uid)[0]
|
||||
|
||||
#Get the user group of the given file path.
|
||||
def get_user_group(file_path) -> str:
|
||||
return grp.getgrgid(os.stat(file_path).st_gid)[0]
|
||||
|
||||
# Set the global user group. get_user must be called first
|
||||
def set_user_group() -> str:
|
||||
global group
|
||||
@@ -94,18 +104,18 @@ async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
||||
if os.access(os.path.dirname(path), os.W_OK):
|
||||
async with ClientSession() as client:
|
||||
res = await client.get(url, ssl=get_ssl_context())
|
||||
if res.status == 200:
|
||||
data = BytesIO(await res.read())
|
||||
remoteHash = sha256(data.getbuffer()).hexdigest()
|
||||
if binHash == remoteHash:
|
||||
data.seek(0)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data.getbuffer())
|
||||
rv = True
|
||||
else:
|
||||
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
|
||||
if res.status == 200:
|
||||
data = BytesIO(await res.read())
|
||||
remoteHash = sha256(data.getbuffer()).hexdigest()
|
||||
if binHash == remoteHash:
|
||||
data.seek(0)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data.getbuffer())
|
||||
rv = True
|
||||
else:
|
||||
rv = False
|
||||
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
|
||||
else:
|
||||
rv = False
|
||||
except:
|
||||
rv = False
|
||||
|
||||
|
||||
+142
-123
@@ -5,8 +5,8 @@ from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from typing import List
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from aiohttp import ClientSession, WSMsgType
|
||||
from aiohttp.client_exceptions import ClientConnectorError, ClientOSError
|
||||
from asyncio.exceptions import TimeoutError
|
||||
import uuid
|
||||
|
||||
@@ -32,13 +32,16 @@ class Tab:
|
||||
self.websocket = await self.client.ws_connect(self.ws_url)
|
||||
|
||||
async def close_websocket(self):
|
||||
await self.websocket.close()
|
||||
await self.client.close()
|
||||
|
||||
async def listen_for_message(self):
|
||||
async for message in self.websocket:
|
||||
data = message.json()
|
||||
yield data
|
||||
|
||||
logger.warn(f"The Tab {self.title} socket has been disconnected while listening for messages.")
|
||||
await self.close_websocket()
|
||||
|
||||
async def _send_devtools_cmd(self, dc, receive=True):
|
||||
if self.websocket:
|
||||
self.cmd_id += 1
|
||||
@@ -52,20 +55,22 @@ class Tab:
|
||||
raise RuntimeError("Websocket not opened")
|
||||
|
||||
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": js,
|
||||
"userGesture": True,
|
||||
"awaitPromise": run_async
|
||||
}
|
||||
}, get_result)
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": js,
|
||||
"userGesture": True,
|
||||
"awaitPromise": run_async
|
||||
}
|
||||
}, get_result)
|
||||
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def has_global_var(self, var_name, manage_socket=True):
|
||||
@@ -77,15 +82,17 @@ class Tab:
|
||||
return res["result"]["result"]["value"]
|
||||
|
||||
async def close(self, manage_socket=True):
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.close",
|
||||
}, False)
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.close",
|
||||
}, False)
|
||||
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def enable(self):
|
||||
@@ -105,78 +112,82 @@ class Tab:
|
||||
}, False)
|
||||
|
||||
async def refresh(self):
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.reload",
|
||||
}, False)
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Page.reload",
|
||||
}, False)
|
||||
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
|
||||
return
|
||||
async def reload_and_evaluate(self, js, manage_socket=True):
|
||||
"""
|
||||
Reloads the current tab, with JS to run on load via debugger
|
||||
"""
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.enable"
|
||||
}, True)
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.enable"
|
||||
}, True)
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": "location.reload();",
|
||||
"userGesture": True,
|
||||
"awaitPromise": False
|
||||
}
|
||||
}, False)
|
||||
|
||||
breakpoint_res = await self._send_devtools_cmd({
|
||||
"method": "Debugger.setInstrumentationBreakpoint",
|
||||
"params": {
|
||||
"instrumentation": "beforeScriptExecution"
|
||||
}
|
||||
}, True)
|
||||
|
||||
logger.info(breakpoint_res)
|
||||
|
||||
# Page finishes loading when breakpoint hits
|
||||
|
||||
for x in range(20):
|
||||
# this works around 1/5 of the time, so just send it 8 times.
|
||||
# the js accounts for being injected multiple times allowing only one instance to run at a time anyway
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": js,
|
||||
"expression": "location.reload();",
|
||||
"userGesture": True,
|
||||
"awaitPromise": False
|
||||
}
|
||||
}, False)
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.removeBreakpoint",
|
||||
"params": {
|
||||
"breakpointId": breakpoint_res["result"]["breakpointId"]
|
||||
}
|
||||
}, False)
|
||||
breakpoint_res = await self._send_devtools_cmd({
|
||||
"method": "Debugger.setInstrumentationBreakpoint",
|
||||
"params": {
|
||||
"instrumentation": "beforeScriptExecution"
|
||||
}
|
||||
}, True)
|
||||
|
||||
logger.info(breakpoint_res)
|
||||
|
||||
# Page finishes loading when breakpoint hits
|
||||
|
||||
for x in range(20):
|
||||
# this works around 1/5 of the time, so just send it 8 times.
|
||||
# the js accounts for being injected multiple times allowing only one instance to run at a time anyway
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": js,
|
||||
"userGesture": True,
|
||||
"awaitPromise": False
|
||||
}
|
||||
}, False)
|
||||
|
||||
for x in range(4):
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.resume"
|
||||
"method": "Debugger.removeBreakpoint",
|
||||
"params": {
|
||||
"breakpointId": breakpoint_res["result"]["breakpointId"]
|
||||
}
|
||||
}, False)
|
||||
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.disable"
|
||||
}, True)
|
||||
for x in range(4):
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.resume"
|
||||
}, False)
|
||||
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
await self._send_devtools_cmd({
|
||||
"method": "Debugger.disable"
|
||||
}, True)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return
|
||||
|
||||
async def add_script_to_evaluate_on_new_document(self, js, add_dom_wrapper=True, manage_socket=True, get_result=True):
|
||||
@@ -212,32 +223,34 @@ class Tab:
|
||||
(see remove_script_to_evaluate_on_new_document below)
|
||||
None is returned if `get_result` is False
|
||||
"""
|
||||
try:
|
||||
|
||||
wrappedjs = """
|
||||
function scriptFunc() {
|
||||
{js}
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
addEventListener('DOMContentLoaded', () => {
|
||||
scriptFunc();
|
||||
});
|
||||
} else {
|
||||
scriptFunc();
|
||||
}
|
||||
""".format(js=js) if add_dom_wrapper else js
|
||||
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.addScriptToEvaluateOnNewDocument",
|
||||
"params": {
|
||||
"source": wrappedjs
|
||||
wrappedjs = """
|
||||
function scriptFunc() {
|
||||
{js}
|
||||
}
|
||||
}, get_result)
|
||||
if (document.readyState === 'loading') {
|
||||
addEventListener('DOMContentLoaded', () => {
|
||||
scriptFunc();
|
||||
});
|
||||
} else {
|
||||
scriptFunc();
|
||||
}
|
||||
""".format(js=js) if add_dom_wrapper else js
|
||||
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.addScriptToEvaluateOnNewDocument",
|
||||
"params": {
|
||||
"source": wrappedjs
|
||||
}
|
||||
}, get_result)
|
||||
|
||||
finally:
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
return res
|
||||
|
||||
async def remove_script_to_evaluate_on_new_document(self, script_id, manage_socket=True):
|
||||
@@ -250,18 +263,20 @@ class Tab:
|
||||
The identifier of the script to remove (returned from `add_script_to_evaluate_on_new_document`)
|
||||
"""
|
||||
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
try:
|
||||
if manage_socket:
|
||||
await self.open_websocket()
|
||||
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.removeScriptToEvaluateOnNewDocument",
|
||||
"params": {
|
||||
"identifier": script_id
|
||||
}
|
||||
}, False)
|
||||
res = await self._send_devtools_cmd({
|
||||
"method": "Page.removeScriptToEvaluateOnNewDocument",
|
||||
"params": {
|
||||
"identifier": script_id
|
||||
}
|
||||
}, False)
|
||||
|
||||
if manage_socket:
|
||||
await self.close_websocket()
|
||||
finally:
|
||||
if manage_socket:
|
||||
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)
|
||||
@@ -337,28 +352,32 @@ class Tab:
|
||||
|
||||
|
||||
async def get_tabs() -> List[Tab]:
|
||||
async with ClientSession() as web:
|
||||
res = {}
|
||||
res = {}
|
||||
|
||||
while True:
|
||||
try:
|
||||
na = False
|
||||
while True:
|
||||
try:
|
||||
async with ClientSession() as web:
|
||||
res = await web.get(f"{BASE_ADDRESS}/json", timeout=3)
|
||||
except ClientConnectorError:
|
||||
logger.debug("ClientConnectorError excepted.")
|
||||
except ClientConnectorError:
|
||||
if not na:
|
||||
logger.debug("Steam isn't available yet. Wait for a moment...")
|
||||
logger.error(format_exc())
|
||||
await sleep(5)
|
||||
except TimeoutError:
|
||||
logger.warn(f"The request to {BASE_ADDRESS}/json timed out")
|
||||
await sleep(1)
|
||||
else:
|
||||
break
|
||||
|
||||
if res.status == 200:
|
||||
r = await res.json()
|
||||
return [Tab(i) for i in r]
|
||||
na = True
|
||||
await sleep(5)
|
||||
except ClientOSError:
|
||||
logger.warn(f"The request to {BASE_ADDRESS}/json was reset")
|
||||
await sleep(1)
|
||||
except TimeoutError:
|
||||
logger.warn(f"The request to {BASE_ADDRESS}/json timed out")
|
||||
await sleep(1)
|
||||
else:
|
||||
raise Exception(f"/json did not return 200. {await res.text()}")
|
||||
break
|
||||
|
||||
if res.status == 200:
|
||||
r = await res.json()
|
||||
return [Tab(i) for i in r]
|
||||
else:
|
||||
raise Exception(f"/json did not return 200. {await res.text()}")
|
||||
|
||||
|
||||
async def get_tab(tab_name) -> Tab:
|
||||
@@ -377,7 +396,7 @@ async def get_tab_lambda(test) -> 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)
|
||||
tab = next((i for i in tabs if ("https://steamloopback.host/routes/" in i.url and (i.title == "Steam Shared Context presented by Valve™" or i.title == "Steam" or i.title == "SP"))), None)
|
||||
if not tab:
|
||||
raise ValueError(f"GamepadUI Tab not found")
|
||||
return tab
|
||||
|
||||
+4
-4
@@ -118,7 +118,7 @@ class Loader:
|
||||
def handle_frontend_bundle(self, request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), 'r') as bundle:
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), "r", encoding="utf-8") as bundle:
|
||||
return web.Response(text=bundle.read(), content_type="application/javascript")
|
||||
|
||||
def import_plugin(self, file, plugin_directory, refresh=False, batch=False):
|
||||
@@ -186,7 +186,7 @@ class Loader:
|
||||
"""
|
||||
async def load_plugin_main_view(self, request):
|
||||
plugin = self.plugins[request.match_info["name"]]
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template:
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), "r", encoding="utf-8") as template:
|
||||
template_data = template.read()
|
||||
ret = f"""
|
||||
<script src="/legacy/library.js"></script>
|
||||
@@ -202,13 +202,13 @@ class Loader:
|
||||
self.logger.info(path)
|
||||
ret = ""
|
||||
file_path = path.join(self.plugin_path, plugin.plugin_directory, route_path)
|
||||
with open(file_path, 'r') as resource_data:
|
||||
with open(file_path, "r", encoding="utf-8") as resource_data:
|
||||
ret = resource_data.read()
|
||||
|
||||
return web.Response(text=ret)
|
||||
|
||||
async def get_steam_resource(self, request):
|
||||
tab = await get_tab("QuickAccess")
|
||||
tab = await get_tab("SP")
|
||||
try:
|
||||
return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html")
|
||||
except Exception as e:
|
||||
|
||||
+62
-44
@@ -4,7 +4,7 @@ from subprocess import call
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
call(['chmod', '-R', '755', sys._MEIPASS])
|
||||
# Full imports
|
||||
from asyncio import get_event_loop, sleep
|
||||
from asyncio import new_event_loop, set_event_loop, sleep
|
||||
from json import dumps, loads
|
||||
from logging import DEBUG, INFO, basicConfig, getLogger
|
||||
from os import getenv, chmod, path
|
||||
@@ -12,7 +12,7 @@ from traceback import format_exc
|
||||
|
||||
import aiohttp_cors
|
||||
# Partial imports
|
||||
from aiohttp import ClientSession, client_exceptions
|
||||
from aiohttp import client_exceptions, WSMsgType
|
||||
from aiohttp.web import Application, Response, get, run_app, static
|
||||
from aiohttp_jinja2 import setup as jinja_setup
|
||||
|
||||
@@ -21,7 +21,7 @@ from browser import PluginBrowser
|
||||
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
|
||||
get_home_path, get_homebrew_path, get_user,
|
||||
get_user_group, set_user, set_user_group,
|
||||
stop_systemd_unit)
|
||||
stop_systemd_unit, start_systemd_unit)
|
||||
from injector import get_gamepadui_tab, Tab, get_tabs
|
||||
from loader import Loader
|
||||
from settings import SettingsManager
|
||||
@@ -56,15 +56,15 @@ basicConfig(
|
||||
|
||||
logger = getLogger("Main")
|
||||
|
||||
async def chown_plugin_dir(_):
|
||||
async def chown_plugin_dir():
|
||||
code_chown = call(["chown", "-R", USER+":"+GROUP, CONFIG["plugin_path"]])
|
||||
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
|
||||
if code_chown != 0 or code_chmod != 0:
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self) -> None:
|
||||
self.loop = get_event_loop()
|
||||
def __init__(self, loop) -> None:
|
||||
self.loop = loop
|
||||
self.web_app = Application()
|
||||
self.web_app.middlewares.append(csrf_middleware)
|
||||
self.cors = aiohttp_cors.setup(self.web_app, defaults={
|
||||
@@ -81,12 +81,19 @@ class PluginManager:
|
||||
self.updater = Updater(self)
|
||||
|
||||
jinja_setup(self.web_app)
|
||||
if CONFIG["chown_plugin_path"] == True:
|
||||
self.web_app.on_startup.append(chown_plugin_dir)
|
||||
self.loop.create_task(self.loader_reinjector())
|
||||
self.loop.create_task(self.load_plugins())
|
||||
if not self.settings.getSetting("cef_forward", False):
|
||||
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT))
|
||||
|
||||
async def startup(_):
|
||||
if self.settings.getSetting("cef_forward", False):
|
||||
self.loop.create_task(start_systemd_unit(REMOTE_DEBUGGER_UNIT))
|
||||
else:
|
||||
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT))
|
||||
if CONFIG["chown_plugin_path"] == True:
|
||||
chown_plugin_dir()
|
||||
self.loop.create_task(self.loader_reinjector())
|
||||
self.loop.create_task(self.load_plugins())
|
||||
|
||||
self.web_app.on_startup.append(startup)
|
||||
|
||||
self.loop.set_exception_handler(self.exception_handler)
|
||||
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
|
||||
|
||||
@@ -103,46 +110,55 @@ class PluginManager:
|
||||
async def get_auth_token(self, request):
|
||||
return Response(text=get_csrf_token())
|
||||
|
||||
async def wait_for_server(self):
|
||||
async with ClientSession() as web:
|
||||
while True:
|
||||
try:
|
||||
await web.get(f"http://{CONFIG['server_host']}:{CONFIG['server_port']}")
|
||||
return
|
||||
except Exception as e:
|
||||
await sleep(0.1)
|
||||
|
||||
async def load_plugins(self):
|
||||
await self.wait_for_server()
|
||||
# await self.wait_for_server()
|
||||
logger.debug("Loading plugins")
|
||||
self.plugin_loader.import_plugins()
|
||||
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
|
||||
|
||||
async def loader_reinjector(self):
|
||||
while True:
|
||||
tab = None
|
||||
nf = False
|
||||
dc = False
|
||||
while not tab:
|
||||
try:
|
||||
tab = await get_gamepadui_tab()
|
||||
except client_exceptions.ClientConnectorError or client_exceptions.ServerDisconnectedError:
|
||||
logger.debug("Couldn't connect to debugger, waiting 5 seconds.")
|
||||
except (client_exceptions.ClientConnectorError, client_exceptions.ServerDisconnectedError):
|
||||
if not dc:
|
||||
logger.debug("Couldn't connect to debugger, waiting...")
|
||||
dc = True
|
||||
pass
|
||||
except ValueError:
|
||||
logger.debug("Couldn't find GamepadUI tab, waiting 5 seconds")
|
||||
if not nf:
|
||||
logger.debug("Couldn't find GamepadUI tab, waiting...")
|
||||
nf = True
|
||||
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
|
||||
try:
|
||||
async for msg in tab.listen_for_message():
|
||||
# this gets spammed a lot
|
||||
if msg.get("method", None) != "Page.navigatedWithinDocument":
|
||||
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("CEF has requested that we detach.")
|
||||
await tab.close_websocket()
|
||||
break
|
||||
# If this is a forceful disconnect the loop will just stop without any failure message. In this case, injector.py will handle this for us so we don't need to close the socket.
|
||||
# This is because of https://github.com/aio-libs/aiohttp/blob/3ee7091b40a1bc58a8d7846e7878a77640e96996/aiohttp/client_ws.py#L321
|
||||
logger.info("CEF has disconnected...")
|
||||
# At this point the loop starts again and we connect to the freshly started Steam client once it is ready.
|
||||
except Exception as e:
|
||||
logger.error("Exception while reading page events " + format_exc())
|
||||
await tab.close_websocket()
|
||||
pass
|
||||
# while True:
|
||||
# await sleep(5)
|
||||
# if not await tab.has_global_var("deckyHasLoaded", False):
|
||||
@@ -152,15 +168,15 @@ class PluginManager:
|
||||
async def inject_javascript(self, tab: Tab, first=False, request=None):
|
||||
logger.info("Loading Decky frontend!")
|
||||
try:
|
||||
if first:
|
||||
if await tab.has_global_var("deckyHasLoaded", False):
|
||||
tabs = await get_tabs()
|
||||
for t in tabs:
|
||||
if t.title != "Steam" and t.title != "SP":
|
||||
logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
|
||||
await t.close()
|
||||
await sleep(0.5)
|
||||
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => 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)
|
||||
# if first:
|
||||
# if await tab.has_global_var("deckyHasLoaded", False):
|
||||
# tabs = await get_tabs()
|
||||
# for t in tabs:
|
||||
# if t.title != "Steam" and t.title != "SP":
|
||||
# logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
|
||||
# await t.close()
|
||||
# await sleep(0.5)
|
||||
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => SteamClient.User.StartShutdown(false), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}", False, False, False)
|
||||
except:
|
||||
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
|
||||
pass
|
||||
@@ -169,4 +185,6 @@ class PluginManager:
|
||||
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
|
||||
|
||||
if __name__ == "__main__":
|
||||
PluginManager().run()
|
||||
loop = new_event_loop()
|
||||
set_event_loop(loop)
|
||||
PluginManager(loop).run()
|
||||
|
||||
+5
-5
@@ -27,9 +27,9 @@ class PluginWrapper:
|
||||
|
||||
self.version = None
|
||||
|
||||
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r"))
|
||||
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
|
||||
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
|
||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r"))
|
||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
|
||||
self.version = package_json["version"]
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ class PluginWrapper:
|
||||
d["res"] = str(e)
|
||||
d["success"] = False
|
||||
finally:
|
||||
writer.write((dumps(d)+"\n").encode("utf-8"))
|
||||
writer.write((dumps(d, ensure_ascii=False)+"\n").encode("utf-8"))
|
||||
await writer.drain()
|
||||
|
||||
async def _open_socket_if_not_exists(self):
|
||||
@@ -140,7 +140,7 @@ class PluginWrapper:
|
||||
return
|
||||
async def _(self):
|
||||
if await self._open_socket_if_not_exists():
|
||||
self.writer.write((dumps({"stop": True})+"\n").encode("utf-8"))
|
||||
self.writer.write((dumps({ "stop": True }, ensure_ascii=False)+"\n").encode("utf-8"))
|
||||
await self.writer.drain()
|
||||
self.writer.close()
|
||||
get_event_loop().create_task(_(self))
|
||||
@@ -151,7 +151,7 @@ class PluginWrapper:
|
||||
async with self.method_call_lock:
|
||||
if await self._open_socket_if_not_exists():
|
||||
self.writer.write(
|
||||
(dumps({"method": method_name, "args": kwargs})+"\n").encode("utf-8"))
|
||||
(dumps({ "method": method_name, "args": kwargs }, ensure_ascii=False) + "\n").encode("utf-8"))
|
||||
await self.writer.drain()
|
||||
line = bytearray()
|
||||
while True:
|
||||
|
||||
+24
-8
@@ -1,40 +1,56 @@
|
||||
import imp
|
||||
from json import dump, load
|
||||
from os import mkdir, path
|
||||
from os import mkdir, path, listdir, rename
|
||||
from shutil import chown
|
||||
|
||||
from helpers import get_home_path, get_homebrew_path, get_user, set_user
|
||||
from helpers import get_home_path, get_homebrew_path, get_user, set_user, get_user_owner
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
def __init__(self, name, settings_directory = None) -> None:
|
||||
set_user()
|
||||
USER = get_user()
|
||||
wrong_dir = get_homebrew_path(get_home_path(USER))
|
||||
if settings_directory == None:
|
||||
settings_directory = get_homebrew_path(get_home_path(USER))
|
||||
settings_directory = path.join(wrong_dir, "settings")
|
||||
|
||||
self.path = path.join(settings_directory, name + ".json")
|
||||
|
||||
#Create the folder with the correct permission
|
||||
if not path.exists(settings_directory):
|
||||
mkdir(settings_directory)
|
||||
chown(settings_directory, USER, USER)
|
||||
|
||||
#Copy all old settings file in the root directory to the correct folder
|
||||
for file in listdir(wrong_dir):
|
||||
if file.endswith(".json"):
|
||||
rename(path.join(wrong_dir,file),
|
||||
path.join(settings_directory, file))
|
||||
self.path = path.join(settings_directory, name + ".json")
|
||||
|
||||
|
||||
#If the owner of the settings directory is not the user, then set it as the user:
|
||||
if get_user_owner(settings_directory) != USER:
|
||||
chown(settings_directory, USER, USER)
|
||||
|
||||
self.settings = {}
|
||||
|
||||
try:
|
||||
open(self.path, "x")
|
||||
open(self.path, "x", encoding="utf-8")
|
||||
except FileExistsError as e:
|
||||
self.read()
|
||||
pass
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
with open(self.path, "r") as file:
|
||||
with open(self.path, "r", encoding="utf-8") as file:
|
||||
self.settings = load(file)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
def commit(self):
|
||||
with open(self.path, "w+") as file:
|
||||
dump(self.settings, file, indent=4)
|
||||
with open(self.path, "w+", encoding="utf-8") as file:
|
||||
dump(self.settings, file, indent=4, ensure_ascii=False)
|
||||
|
||||
def getSetting(self, key, default):
|
||||
return self.settings.get(key, default)
|
||||
|
||||
+37
-37
@@ -32,7 +32,7 @@ class Updater:
|
||||
self.allRemoteVers = None
|
||||
try:
|
||||
logger.info(getcwd())
|
||||
with open(path.join(getcwd(), ".loader.version"), 'r') as version_file:
|
||||
with open(path.join(getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
|
||||
self.localVer = version_file.readline().replace("\n", "")
|
||||
except:
|
||||
self.localVer = False
|
||||
@@ -87,12 +87,12 @@ class Updater:
|
||||
branch = self.get_branch(self.context.settings)
|
||||
match branch:
|
||||
case 0:
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-release.service"
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service"
|
||||
case 1 | 2:
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-prerelease.service"
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
|
||||
case _:
|
||||
logger.error("You have an invalid branch set... Defaulting to prerelease service, please send the logs to the devs!")
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-prerelease.service"
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
|
||||
return str(url)
|
||||
|
||||
async def get_version(self):
|
||||
@@ -112,20 +112,20 @@ class Updater:
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
|
||||
remoteVersions = await res.json()
|
||||
self.allRemoteVers = remoteVersions
|
||||
logger.debug("determining release type to find, branch is %i" % selectedBranch)
|
||||
if selectedBranch == 0:
|
||||
logger.debug("release type: release")
|
||||
self.remoteVer = next(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and ver["tag_name"], remoteVersions), None)
|
||||
elif selectedBranch == 1:
|
||||
logger.debug("release type: pre-release")
|
||||
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].find("-pre"), remoteVersions), None)
|
||||
else:
|
||||
logger.error("release type: NOT FOUND")
|
||||
raise ValueError("no valid branch found")
|
||||
logger.info("Updated remote version information")
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
|
||||
self.allRemoteVers = remoteVersions
|
||||
logger.debug("determining release type to find, branch is %i" % selectedBranch)
|
||||
if selectedBranch == 0:
|
||||
logger.debug("release type: release")
|
||||
self.remoteVer = next(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and ver["tag_name"], remoteVersions), None)
|
||||
elif selectedBranch == 1:
|
||||
logger.debug("release type: pre-release")
|
||||
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].find("-pre"), remoteVersions), None)
|
||||
else:
|
||||
logger.error("release type: NOT FOUND")
|
||||
raise ValueError("no valid branch found")
|
||||
logger.info("Updated remote version information")
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
|
||||
return await self.get_version()
|
||||
|
||||
async def version_reloader(self):
|
||||
@@ -152,18 +152,18 @@ class Updater:
|
||||
async with web.request("GET", service_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
||||
logger.debug("Downloading service file")
|
||||
data = await res.content.read()
|
||||
logger.debug(str(data))
|
||||
service_file_path = path.join(getcwd(), "plugin_loader.service")
|
||||
try:
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out:
|
||||
out.write(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error at %s", exc_info=e)
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), 'r') as service_file:
|
||||
service_data = service_file.read()
|
||||
service_data = service_data.replace("${HOMEBREW_FOLDER}", "/home/"+helpers.get_user()+"/homebrew")
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), 'w') as service_file:
|
||||
service_file.write(service_data)
|
||||
logger.debug(str(data))
|
||||
service_file_path = path.join(getcwd(), "plugin_loader.service")
|
||||
try:
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), "wb") as out:
|
||||
out.write(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error at %s", exc_info=e)
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), "r", encoding="utf-8") as service_file:
|
||||
service_data = service_file.read()
|
||||
service_data = service_data.replace("${HOMEBREW_FOLDER}", "/home/"+helpers.get_user()+"/homebrew")
|
||||
with open(path.join(getcwd(), "plugin_loader.service"), "w", encoding="utf-8") as service_file:
|
||||
service_file.write(service_data)
|
||||
|
||||
logger.debug("Saved service file")
|
||||
logger.debug("Copying service file over current file.")
|
||||
@@ -191,14 +191,14 @@ class Updater:
|
||||
self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False))
|
||||
progress = new_progress
|
||||
|
||||
with open(path.join(getcwd(), ".loader.version"), "w") as out:
|
||||
out.write(version)
|
||||
with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
|
||||
out.write(version)
|
||||
|
||||
call(['chmod', '+x', path.join(getcwd(), "PluginLoader")])
|
||||
logger.info("Updated loader installation.")
|
||||
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
|
||||
await self.do_restart()
|
||||
await tab.client.close()
|
||||
call(['chmod', '+x', path.join(getcwd(), "PluginLoader")])
|
||||
logger.info("Updated loader installation.")
|
||||
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
|
||||
await self.do_restart()
|
||||
await tab.close_websocket()
|
||||
|
||||
async def do_restart(self):
|
||||
call(["systemctl", "daemon-reload"])
|
||||
|
||||
+19
-18
@@ -80,12 +80,13 @@ class Utilities:
|
||||
|
||||
async def http_request(self, method="", url="", **kwargs):
|
||||
async with ClientSession() as web:
|
||||
async with web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs) as res:
|
||||
return {
|
||||
"status": res.status,
|
||||
"headers": dict(res.headers),
|
||||
"body": await res.text()
|
||||
}
|
||||
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"
|
||||
@@ -241,17 +242,17 @@ class Utilities:
|
||||
if ip != None:
|
||||
self.logger.info("Connecting to React DevTools at " + ip)
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context()) as res:
|
||||
if res.status != 200:
|
||||
self.logger.error("Failed to connect to React DevTools at " + ip)
|
||||
return False
|
||||
self.start_rdt_proxy(ip, 8097)
|
||||
script = "if(!window.deckyHasConnectedRDT){window.deckyHasConnectedRDT=true;\n" + await res.text() + "\n}"
|
||||
self.logger.info("Connected to React DevTools, loading script")
|
||||
tab = await get_gamepadui_tab()
|
||||
# RDT needs to load before React itself to work.
|
||||
result = await tab.reload_and_evaluate(script)
|
||||
self.logger.info(result)
|
||||
res = await web.request("GET", "http://" + ip + ":8097", ssl=helpers.get_ssl_context())
|
||||
if res.status != 200:
|
||||
self.logger.error("Failed to connect to React DevTools at " + ip)
|
||||
return False
|
||||
self.start_rdt_proxy(ip, 8097)
|
||||
script = "if(!window.deckyHasConnectedRDT){window.deckyHasConnectedRDT=true;\n" + await res.text() + "\n}"
|
||||
self.logger.info("Connected to React DevTools, loading script")
|
||||
tab = await get_gamepadui_tab()
|
||||
# RDT needs to load before React itself to work.
|
||||
result = await tab.reload_and_evaluate(script)
|
||||
self.logger.info(result)
|
||||
|
||||
except Exception:
|
||||
self.logger.error("Failed to connect to React DevTools")
|
||||
@@ -261,5 +262,5 @@ class Utilities:
|
||||
self.logger.info("Disabling React DevTools")
|
||||
tab = await get_gamepadui_tab()
|
||||
self.rdt_script_id = None
|
||||
await tab.evaluate_js("location.reload();", False, True, False)
|
||||
await tab.evaluate_js("SteamClient.User.StartRestart();", False, True, False)
|
||||
self.logger.info("React DevTools disabled")
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env xdg-open
|
||||
[Desktop Entry]
|
||||
Name=Install Decky
|
||||
Exec=sh -c 'curl -O --output-dir /tmp/ https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/user_install_script.sh 2> /dev/null && bash /tmp/user_install_script.sh'
|
||||
Icon=steamdeck-gaming-return
|
||||
Terminal=true
|
||||
Type=Application
|
||||
StartupNotify=false
|
||||
Vendored
+3
-1
@@ -11,6 +11,7 @@ HOMEBREW_FOLDER="${USER_DIR}/homebrew"
|
||||
rm -rf "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||
touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging"
|
||||
|
||||
# Download latest release and install it
|
||||
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))")
|
||||
@@ -27,7 +28,7 @@ systemctl --user disable plugin_loader 2> /dev/null
|
||||
systemctl stop plugin_loader 2> /dev/null
|
||||
systemctl disable plugin_loader 2> /dev/null
|
||||
|
||||
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-prerelease.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service
|
||||
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-prerelease.service
|
||||
|
||||
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
|
||||
[Unit]
|
||||
@@ -40,6 +41,7 @@ User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
[Install]
|
||||
|
||||
Vendored
+3
-1
@@ -11,6 +11,7 @@ HOMEBREW_FOLDER="${USER_DIR}/homebrew"
|
||||
rm -rf "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||
touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging"
|
||||
|
||||
# Download latest release and install it
|
||||
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "false"))")
|
||||
@@ -27,7 +28,7 @@ systemctl --user disable plugin_loader 2> /dev/null
|
||||
systemctl stop plugin_loader 2> /dev/null
|
||||
systemctl disable plugin_loader 2> /dev/null
|
||||
|
||||
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/service-updater/dist/plugin_loader-release.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-release.service
|
||||
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-release.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-release.service
|
||||
|
||||
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
|
||||
[Unit]
|
||||
@@ -40,6 +41,7 @@ User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
|
||||
+2
-1
@@ -8,7 +8,8 @@ User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Vendored
+2
-1
@@ -8,7 +8,8 @@ User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Vendored
+164
@@ -0,0 +1,164 @@
|
||||
#!/bin/sh
|
||||
|
||||
# if a password was set by decky, this will run when the program closes
|
||||
temp_pass_cleanup() {
|
||||
echo $PASS | sudo -S -k passwd -d deck
|
||||
}
|
||||
|
||||
# if the script is not root yet, get the password and rerun as root
|
||||
if (( $EUID != 0 )); then
|
||||
PASS_STATUS=$(passwd -S deck 2> /dev/null)
|
||||
if [ "$PASS_STATUS" = "" ]; then
|
||||
echo "Deck user not found. Continuing anyway, as it probably just means user is on a non-steamos system."
|
||||
fi
|
||||
|
||||
if [ "${PASS_STATUS:5:2}" = "NP" ]; then # if no password is set
|
||||
if ( zenity --title="Decky Installer" --width=300 --height=200 --question --text="You appear to have not set an admin password.\nDecky can still install by temporarily setting your password to 'Decky!' and continuing, then removing it when the installer finishes\nAre you okay with that?" ); then
|
||||
yes "Decky!" | passwd deck
|
||||
trap temp_pass_cleanup EXIT # make sure password is removed when application closes
|
||||
PASS="Decky!"
|
||||
else exit 1; fi
|
||||
else
|
||||
# get password
|
||||
FINISHED="false"
|
||||
while [ "$FINISHED" != "true" ]; do
|
||||
PASS=$(zenity --title="Decky Installer" --width=300 --height=100 --entry --hide-text --text="Enter your sudo/admin password")
|
||||
if [[ $? -eq 1 ]] || [[ $? -eq 5 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
if ( echo "$PASS" | sudo -S -k true ); then
|
||||
FINISHED="true"
|
||||
else
|
||||
zenity --title="Decky Installer" --width=150 --height=40 --info --text "Incorrect Password"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if ! [ $USER = "deck" ]; then
|
||||
zenity --title="Decky Installer" --width=300 --height=100 --warning --text "You appear to not be on a deck.\nDecky should still mostly work, but you may not get full functionality."
|
||||
fi
|
||||
|
||||
# get user dir before rerunning as root, otherwise it'll just be 'home/root'
|
||||
USER_DIR="$(getent passwd $USER | cut -d: -f6)"
|
||||
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
|
||||
echo "$PASS" | sudo -S -k sh "$0" "$USER_DIR" "$HOMEBREW_FOLDER" # rerun script as root
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# all code below should be run as root
|
||||
USER_DIR=$1
|
||||
HOMEBREW_FOLDER=$2
|
||||
|
||||
# if decky is already installed, then also add an 'uninstall' prompt
|
||||
if [[ -f "${USER_DIR}/homebrew/services/PluginLoader" ]] ; then
|
||||
BRANCH=$(zenity --title="Decky Installer" --width=360 --height=170 --list --radiolist --text "Select Option:" --hide-header --column "Buttons" --column "Choice" --column "Info" TRUE "release" "(Recommended option)" FALSE "prerelease" "(May be unstable)" FALSE "uninstall decky loader" "")
|
||||
else
|
||||
BRANCH=$(zenity --title="Decky Installer" --width=300 --height=100 --list --radiolist --text "Select Branch:" --hide-header --column "Buttons" --column "Choice" --column "Info" TRUE "release" "(Recommended option)" FALSE "prerelease" "(May be unstable)" )
|
||||
fi
|
||||
if [[ $? -eq 1 ]] || [[ $? -eq 5 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# uninstall if uninstall option was selected
|
||||
if [ "$BRANCH" == "uninstall decky loader" ] ; then
|
||||
(
|
||||
echo "30" ; echo "# Disabling and removing services" ;
|
||||
sudo systemctl disable --now plugin_loader.service > /dev/null
|
||||
sudo rm -f "${USER_DIR}/.config/systemd/user/plugin_loader.service"
|
||||
sudo rm -f "/etc/systemd/system/plugin_loader.service"
|
||||
|
||||
echo "60" ; echo "# Removing Temporary Files" ;
|
||||
rm -rf "/tmp/plugin_loader"
|
||||
rm -rf "/tmp/user_install_script.sh"
|
||||
|
||||
echo "90" ; echo "# Cleaning services folder" ;
|
||||
sudo rm "${HOMEBREW_FOLDER}/services/PluginLoader"
|
||||
echo "100" ; echo "# Uninstall finished, installer can now be closed";
|
||||
) |
|
||||
zenity --progress \
|
||||
--title="Decky Installer" \
|
||||
--width=300 --height=100 \
|
||||
--text="Uninstalling..." \
|
||||
--percentage=0 \
|
||||
--no-cancel
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# otherwise install decky loader
|
||||
(
|
||||
echo "15" ; echo "# Creating file structure" ;
|
||||
rm -rf "${HOMEBREW_FOLDER}/services"
|
||||
sudo mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||
sudo mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||
touch "${USER_DIR}/.steam/steam/.cef-enable-remote-debugging"
|
||||
|
||||
echo "30" ; echo "# Finding latest $BRANCH";
|
||||
if [ $BRANCH = 'prerelease' ] ; then
|
||||
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))")
|
||||
else
|
||||
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "false"))")
|
||||
fi
|
||||
read VERSION DOWNLOADURL < <(echo $(jq -r '.tag_name, .assets[].browser_download_url' <<< ${RELEASE}))
|
||||
|
||||
echo "45" ; echo "# Installing version $VERSION" ;
|
||||
curl -L $DOWNLOADURL -o ${HOMEBREW_FOLDER}/services/PluginLoader 2>&1 | stdbuf -oL tr '\r' '\n' | sed -u 's/^ *\([0-9][0-9]*\).*\( [0-9].*$\)/\1\n#Download Speed\:\2/' | zenity --progress --title "Downloading Decky" --text="Download Speed: 0" --width=300 --height=100 --auto-close --no-cancel 2>/dev/null
|
||||
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
echo $VERSION > ${HOMEBREW_FOLDER}/services/.loader.version
|
||||
|
||||
echo "70" ; echo "# Kiling plugin_loader if it exists" ;
|
||||
systemctl --user stop plugin_loader 2> /dev/null
|
||||
systemctl --user disable plugin_loader 2> /dev/null
|
||||
systemctl stop plugin_loader 2> /dev/null
|
||||
systemctl disable plugin_loader 2> /dev/null
|
||||
|
||||
echo "85" ; echo "# Setting up systemd" ;
|
||||
curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-${BRANCH}.service --output ${HOMEBREW_FOLDER}/services/plugin_loader-${BRANCH}.service
|
||||
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOM
|
||||
|
||||
# if .service file doesn't exist for whatever reason, use backup file instead
|
||||
if [[ -f "${HOMEBREW_FOLDER}/services/plugin_loader-${BRANCH}.service" ]]; then
|
||||
printf "Grabbed latest ${BRANCH} service.\n"
|
||||
sed -i -e "s|\${HOMEBREW_FOLDER}|${HOMEBREW_FOLDER}|" "${HOMEBREW_FOLDER}/services/plugin_loader-${BRANCH}.service"
|
||||
cp -f "${HOMEBREW_FOLDER}/services/plugin_loader-${BRANCH}.service" "/etc/systemd/system/plugin_loader.service"
|
||||
else
|
||||
printf "Could not curl latest ${BRANCH} systemd service, using built-in service as a backup!\n"
|
||||
rm -f "/etc/systemd/system/plugin_loader.service"
|
||||
cp "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" "/etc/systemd/system/plugin_loader.service"
|
||||
fi
|
||||
|
||||
mkdir -p ${HOMEBREW_FOLDER}/services/.systemd
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-${BRANCH}.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-${BRANCH}.service
|
||||
cp ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/.systemd/plugin_loader-backup.service
|
||||
rm ${HOMEBREW_FOLDER}/services/plugin_loader-backup.service ${HOMEBREW_FOLDER}/services/plugin_loader-${BRANCH}.service
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl start plugin_loader
|
||||
systemctl enable plugin_loader
|
||||
echo "100" ; echo "# Install finished, installer can now be closed";
|
||||
) |
|
||||
zenity --progress \
|
||||
--title="Decky Installer" \
|
||||
--width=300 --height=100 \
|
||||
--text="Installing..." \
|
||||
--percentage=0 \
|
||||
--no-cancel # not actually sure how to make the cancel work properly, so it's just not there unless someone else can figure it out
|
||||
|
||||
if [ "$?" = -1 ] ; then
|
||||
zenity --title="Decky Installer" --width=150 --height=70 --error --text="Download interrupted."
|
||||
fi
|
||||
@@ -41,7 +41,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "^3.7.12",
|
||||
"decky-frontend-lib": "^3.18.4",
|
||||
"react-file-icon": "^1.2.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
|
||||
Generated
+4
-4
@@ -10,7 +10,7 @@ specifiers:
|
||||
'@types/react-file-icon': ^1.0.1
|
||||
'@types/react-router': 5.1.18
|
||||
'@types/webpack': ^5.28.0
|
||||
decky-frontend-lib: ^3.7.12
|
||||
decky-frontend-lib: ^3.18.4
|
||||
husky: ^8.0.1
|
||||
import-sort-style-module: ^6.0.0
|
||||
inquirer: ^8.2.4
|
||||
@@ -30,7 +30,7 @@ specifiers:
|
||||
typescript: ^4.7.4
|
||||
|
||||
dependencies:
|
||||
decky-frontend-lib: 3.7.12
|
||||
decky-frontend-lib: 3.18.4
|
||||
react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
|
||||
react-icons: 4.4.0_react@16.14.0
|
||||
react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u
|
||||
@@ -944,8 +944,8 @@ packages:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
/decky-frontend-lib/3.7.12:
|
||||
resolution: {integrity: sha512-whDV9zHuEBFj17zKoT51aRcUxLvSzBNu2lc242/EO9aFFP064FVCrJu+r7CxWe0hlQ7sA4FKX1qgCwsZ6H+PZg==}
|
||||
/decky-frontend-lib/3.18.4:
|
||||
resolution: {integrity: sha512-i3TAe3RJtT1TK0rJgW9Ek5jxMWZRCYLDvqHDylGVieUvuyI7c8X+cogz30pP4cqeGOaA1d/MxBEbhlpD3JhVvg==}
|
||||
dev: false
|
||||
|
||||
/decode-named-character-reference/1.0.2:
|
||||
|
||||
@@ -7,6 +7,11 @@ import replace from '@rollup/plugin-replace';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import { defineConfig } from 'rollup';
|
||||
|
||||
const hiddenWarnings = [
|
||||
"THIS_IS_UNDEFINED",
|
||||
"EVAL"
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
input: 'src/index.tsx',
|
||||
plugins: [
|
||||
@@ -35,5 +40,9 @@ export default defineConfig({
|
||||
chunkFileNames: (chunkInfo) => {
|
||||
return 'chunk-[hash].js'
|
||||
}
|
||||
},
|
||||
onwarn: function ( message, handleWarning ) {
|
||||
if (hiddenWarnings.some(warning => message.code === warning)) return;
|
||||
handleWarning(message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConfirmModal, QuickAccessTab, Router, Spinner, staticClasses } from 'decky-frontend-lib';
|
||||
import { ConfirmModal, Navigation, QuickAccessTab, Spinner, staticClasses } from 'decky-frontend-lib';
|
||||
import { FC, useState } from 'react';
|
||||
|
||||
interface PluginInstallModalProps {
|
||||
@@ -20,7 +20,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
|
||||
onOK={async () => {
|
||||
setLoading(true);
|
||||
await onOK();
|
||||
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
|
||||
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
|
||||
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
|
||||
}}
|
||||
onCancel={async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FaShapes, FaTools } from 'react-icons/fa';
|
||||
import { installFromURL } from '../../../../store';
|
||||
import BranchSelect from './BranchSelect';
|
||||
import RemoteDebuggingSettings from './RemoteDebugging';
|
||||
// import StoreSelect from './StoreSelect';
|
||||
import StoreSelect from './StoreSelect';
|
||||
import UpdaterSettings from './Updater';
|
||||
|
||||
export default function GeneralSettings({
|
||||
@@ -21,7 +21,7 @@ export default function GeneralSettings({
|
||||
<div>
|
||||
<UpdaterSettings />
|
||||
<BranchSelect />
|
||||
{/* <StoreSelect /> */}
|
||||
<StoreSelect />
|
||||
<RemoteDebuggingSettings />
|
||||
<Field
|
||||
label="Developer mode"
|
||||
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
DialogButton,
|
||||
Dropdown,
|
||||
Focusable,
|
||||
Navigation,
|
||||
QuickAccessTab,
|
||||
Router,
|
||||
SingleDropdownOption,
|
||||
SuspensefulImage,
|
||||
joinClassNames,
|
||||
@@ -38,8 +38,8 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||
}}
|
||||
onCancel={(_: CustomEvent) => {
|
||||
if (containerRef.current!.querySelectorAll('* :focus').length === 0) {
|
||||
Router.NavigateBackOrOpenMenu();
|
||||
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
|
||||
Navigation.NavigateBack();
|
||||
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
|
||||
} else {
|
||||
containerRef.current!.focus();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Navigation,
|
||||
ReactRouter,
|
||||
Router,
|
||||
fakeRenderComponent,
|
||||
@@ -26,13 +27,20 @@ const logger = new Logger('DeveloperMode');
|
||||
|
||||
let removeSettingsObserver: () => void = () => {};
|
||||
|
||||
export function setShowValveInternal(show: boolean) {
|
||||
const settingsMod = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (typeof m[prop]?.settings?.bIsValveEmail !== 'undefined') return m[prop];
|
||||
export async function setShowValveInternal(show: boolean) {
|
||||
let settingsMod: any;
|
||||
while (!settingsMod) {
|
||||
settingsMod = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (typeof m[prop]?.settings?.bIsValveEmail !== 'undefined') return m[prop];
|
||||
}
|
||||
});
|
||||
if (!settingsMod) {
|
||||
logger.debug('[ValveInternal] waiting for settingsMod');
|
||||
await sleep(1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (show) {
|
||||
removeSettingsObserver = settingsMod[
|
||||
@@ -74,13 +82,14 @@ export async function startup() {
|
||||
window.DFL = {
|
||||
findModuleChild,
|
||||
findModule,
|
||||
Navigation,
|
||||
Router,
|
||||
ReactRouter,
|
||||
ReactUtils: {
|
||||
fakeRenderComponent,
|
||||
findInReactTree,
|
||||
findInTree,
|
||||
},
|
||||
Router,
|
||||
ReactRouter,
|
||||
classes: {
|
||||
scrollClasses,
|
||||
staticClasses,
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
sleep,
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { lazy } from 'react';
|
||||
import { FaExclamationCircle, FaPlug } from 'react-icons/fa';
|
||||
import { FC, lazy } from 'react';
|
||||
import { FaCog, FaExclamationCircle, FaPlug } from 'react-icons/fa';
|
||||
|
||||
import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
|
||||
import LegacyPlugin from './components/LegacyPlugin';
|
||||
@@ -245,6 +245,24 @@ class PluginLoader extends Logger {
|
||||
});
|
||||
} catch (e) {
|
||||
this.error('Error loading plugin ' + name, e);
|
||||
const TheError: FC<{}> = () => (
|
||||
<>
|
||||
Error:{' '}
|
||||
<pre>
|
||||
<code>{e instanceof Error ? e.stack : JSON.stringify(e)}</code>
|
||||
</pre>
|
||||
<>
|
||||
Please go to <FaCog style={{ display: 'inline' }} /> in the Decky menu if you need to uninstall this
|
||||
plugin.
|
||||
</>
|
||||
</>
|
||||
);
|
||||
this.plugins.push({
|
||||
name: name,
|
||||
version: version,
|
||||
content: <TheError />,
|
||||
icon: <FaExclamationCircle />,
|
||||
});
|
||||
this.toaster.toast({ title: 'Error loading ' + name, body: '' + e, icon: <FaExclamationCircle /> });
|
||||
}
|
||||
} else throw new Error(`${name} frontend_bundle not OK`);
|
||||
|
||||
+40
-42
@@ -1,4 +1,5 @@
|
||||
import { Plugin } from './plugin';
|
||||
import { getSetting, setSetting } from './utils/settings';
|
||||
|
||||
export enum Store {
|
||||
Default,
|
||||
@@ -9,6 +10,7 @@ export enum Store {
|
||||
export interface StorePluginVersion {
|
||||
name: string;
|
||||
hash: string;
|
||||
artifact: string | undefined | null;
|
||||
}
|
||||
|
||||
export interface StorePlugin {
|
||||
@@ -26,47 +28,41 @@ export type PluginUpdateMapping = Map<string, StorePluginVersion>;
|
||||
|
||||
export async function getPluginList(): Promise<StorePlugin[]> {
|
||||
let version = await window.DeckyPluginLoader.updateVersion();
|
||||
return fetch('https://plugins.deckbrew.xyz/plugins', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Decky-Version': version.current,
|
||||
},
|
||||
}).then((r) => r.json());
|
||||
// let store = await getSetting<Store>('store', Store.Default);
|
||||
// let customURL = await getSetting<string>('store-url', 'https://plugins.deckbrew.xyz/plugins');
|
||||
// let storeURL;
|
||||
// if (!store) {
|
||||
// console.log('Could not get a default store, using Default.');
|
||||
// await setSetting('store-url', Store.Default);
|
||||
// return fetch('https://plugins.deckbrew.xyz/plugins', {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'X-Decky-Version': version.current,
|
||||
// },
|
||||
// }).then((r) => r.json());
|
||||
// } else {
|
||||
// switch (+store) {
|
||||
// case Store.Default:
|
||||
// storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
||||
// break;
|
||||
// case Store.Testing:
|
||||
// storeURL = 'https://testing.deckbrew.xyz/plugins';
|
||||
// break;
|
||||
// case Store.Custom:
|
||||
// storeURL = customURL;
|
||||
// break;
|
||||
// default:
|
||||
// console.error('Somehow you ended up without a standard URL, using the default URL.');
|
||||
// storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
||||
// break;
|
||||
// }
|
||||
// return fetch(storeURL, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'X-Decky-Version': version.current,
|
||||
// },
|
||||
// }).then((r) => r.json());
|
||||
// }
|
||||
let store = await getSetting<Store>('store', Store.Default);
|
||||
let customURL = await getSetting<string>('store-url', 'https://plugins.deckbrew.xyz/plugins');
|
||||
let storeURL;
|
||||
if (!store) {
|
||||
console.log('Could not get a default store, using Default.');
|
||||
await setSetting('store-url', Store.Default);
|
||||
return fetch('https://plugins.deckbrew.xyz/plugins', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Decky-Version': version.current,
|
||||
},
|
||||
}).then((r) => r.json());
|
||||
} else {
|
||||
switch (+store) {
|
||||
case Store.Default:
|
||||
storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
||||
break;
|
||||
case Store.Testing:
|
||||
storeURL = 'https://testing.deckbrew.xyz/plugins';
|
||||
break;
|
||||
case Store.Custom:
|
||||
storeURL = customURL;
|
||||
break;
|
||||
default:
|
||||
console.error('Somehow you ended up without a standard URL, using the default URL.');
|
||||
storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
||||
break;
|
||||
}
|
||||
return fetch(storeURL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Decky-Version': version.current,
|
||||
},
|
||||
}).then((r) => r.json());
|
||||
}
|
||||
}
|
||||
|
||||
export async function installFromURL(url: string) {
|
||||
@@ -78,9 +74,11 @@ export async function installFromURL(url: string) {
|
||||
}
|
||||
|
||||
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) {
|
||||
const artifactUrl =
|
||||
selectedVer.artifact ?? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`;
|
||||
await window.DeckyPluginLoader.callServerMethod('install_plugin', {
|
||||
name: plugin,
|
||||
artifact: `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`,
|
||||
artifact: artifactUrl,
|
||||
version: selectedVer.name,
|
||||
hash: selectedVer.hash,
|
||||
});
|
||||
|
||||
@@ -23,7 +23,6 @@ class TabsHook extends Logger {
|
||||
tabs: Tab[] = [];
|
||||
private qAMRoot?: any;
|
||||
private qamPatch?: Patch;
|
||||
private unsubscribeSecurity?: () => void;
|
||||
|
||||
constructor() {
|
||||
super('TabsHook');
|
||||
@@ -114,7 +113,6 @@ class TabsHook extends Logger {
|
||||
deinit() {
|
||||
this.qamPatch?.unpatch();
|
||||
this.qAMRoot.return.alternate.type = this.qAMRoot.return.type;
|
||||
this.unsubscribeSecurity?.();
|
||||
}
|
||||
|
||||
add(tab: Tab) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Patch, ToastData, afterPatch, findInReactTree, sleep } from 'decky-frontend-lib';
|
||||
import { Module, Patch, ToastData, afterPatch, findInReactTree, findModuleChild, sleep } from 'decky-frontend-lib';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import Toast from './components/Toast';
|
||||
@@ -7,6 +7,7 @@ import Logger from './logger';
|
||||
declare global {
|
||||
interface Window {
|
||||
__TOASTER_INSTANCE: any;
|
||||
settingsStore: any;
|
||||
NotificationStore: any;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +17,7 @@ class Toaster extends Logger {
|
||||
// private toasterState: DeckyToasterState = new DeckyToasterState();
|
||||
private node: any;
|
||||
private rNode: any;
|
||||
private settingsModule: any;
|
||||
private audioModule: any;
|
||||
private finishStartup?: () => void;
|
||||
private ready: Promise<void> = new Promise((res) => (this.finishStartup = res));
|
||||
private toasterPatch?: Patch;
|
||||
@@ -127,6 +128,17 @@ class Toaster extends Logger {
|
||||
this.rNode.stateNode.forceUpdate();
|
||||
delete this.rNode.stateNode.shouldComponentUpdate;
|
||||
|
||||
this.audioModule = findModuleChild((m: Module) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
try {
|
||||
if (m[prop].PlayNavSound && m[prop].RegisterCallbackOnPlaySound) return m[prop];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.log('Initialized');
|
||||
this.finishStartup?.();
|
||||
}
|
||||
@@ -135,24 +147,31 @@ class Toaster extends Logger {
|
||||
// toast.duration = toast.duration || 5e3;
|
||||
// this.toasterState.addToast(toast);
|
||||
await this.ready;
|
||||
const settings = this.settingsModule?.settings;
|
||||
let toastData = {
|
||||
nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
|
||||
rtCreated: Date.now(),
|
||||
eType: 15,
|
||||
eType: toast.eType || 11,
|
||||
nToastDurationMS: toast.duration || (toast.duration = 5e3),
|
||||
data: toast,
|
||||
decky: true,
|
||||
};
|
||||
// @ts-ignore
|
||||
toastData.data.appid = () => 0;
|
||||
if (toast.sound === undefined) toast.sound = 6;
|
||||
if (toast.playSound === undefined) toast.playSound = true;
|
||||
if (toast.showToast === undefined) toast.showToast = true;
|
||||
if (
|
||||
(settings?.bDisableAllToasts && !toast.critical) ||
|
||||
(settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
|
||||
(window.settingsStore.settings.bDisableAllToasts && !toast.critical) ||
|
||||
(window.settingsStore.settings.bDisableToastsInGame &&
|
||||
!toast.critical &&
|
||||
window.NotificationStore.BIsUserInGame())
|
||||
)
|
||||
return;
|
||||
window.NotificationStore.m_rgNotificationToasts.push(toastData);
|
||||
window.NotificationStore.DispatchNextToast();
|
||||
if (toast.playSound) this.audioModule?.PlayNavSound(toast.sound);
|
||||
if (toast.showToast) {
|
||||
window.NotificationStore.m_rgNotificationToasts.push(toastData);
|
||||
window.NotificationStore.DispatchNextToast();
|
||||
}
|
||||
}
|
||||
|
||||
deinit() {
|
||||
|
||||
Reference in New Issue
Block a user