mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-14 04:35:07 +03:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| caf37d681f | |||
| 93151e4e5e | |||
| d6f336d84b | |||
| 4777963b65 | |||
| fc193f98db |
+59
-40
@@ -118,7 +118,7 @@ class PluginBrowser:
|
||||
# plugins_snapshot = self.plugins.copy()
|
||||
# snapshot_string = pformat(plugins_snapshot)
|
||||
# logger.debug("current plugins: %s", snapshot_string)
|
||||
if self.plugins[name]:
|
||||
if name in self.plugins:
|
||||
logger.debug("Plugin %s was found", name)
|
||||
self.plugins[name].stop()
|
||||
logger.debug("Plugin %s was stopped", name)
|
||||
@@ -139,6 +139,10 @@ class PluginBrowser:
|
||||
self.loader.watcher.disabled = False
|
||||
|
||||
async def _install(self, artifact, name, version, hash):
|
||||
# Will be set later in code
|
||||
res_zip = None
|
||||
|
||||
# Check if plugin is installed
|
||||
isInstalled = False
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = True
|
||||
@@ -148,47 +152,62 @@ class PluginBrowser:
|
||||
isInstalled = True
|
||||
except:
|
||||
logger.error(f"Failed to determine if {name} is already installed, continuing anyway.")
|
||||
logger.info(f"Installing {name} (Version: {version})")
|
||||
async with ClientSession() as client:
|
||||
logger.debug(f"Fetching {artifact}")
|
||||
res = await client.get(artifact, ssl=get_ssl_context())
|
||||
if res.status == 200:
|
||||
logger.debug("Got 200. Reading...")
|
||||
data = await res.read()
|
||||
logger.debug(f"Read {len(data)} bytes")
|
||||
res_zip = BytesIO(data)
|
||||
if isInstalled:
|
||||
try:
|
||||
logger.debug("Uninstalling existing plugin...")
|
||||
await self.uninstall_plugin(name)
|
||||
except:
|
||||
logger.error(f"Plugin {name} could not be uninstalled.")
|
||||
logger.debug("Unzipping...")
|
||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||
if ret:
|
||||
plugin_folder = self.find_plugin_folder(name)
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
if name in self.loader.plugins:
|
||||
self.loader.plugins[name].stop()
|
||||
self.loader.plugins.pop(name, None)
|
||||
await sleep(1)
|
||||
|
||||
current_plugin_order = self.settings.getSetting("pluginOrder")
|
||||
current_plugin_order.append(name)
|
||||
self.settings.setSetting("pluginOrder", current_plugin_order)
|
||||
logger.debug("Plugin %s was added to the pluginOrder setting", name)
|
||||
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
|
||||
else:
|
||||
logger.fatal(f"Failed Downloading Remote Binaries")
|
||||
|
||||
# Check if the file is a local file or a URL
|
||||
if artifact.startswith("file://"):
|
||||
logger.info(f"Installing {name} from local ZIP file (Version: {version})")
|
||||
res_zip = BytesIO(open(artifact[7:], "rb").read())
|
||||
else:
|
||||
logger.info(f"Installing {name} from URL (Version: {version})")
|
||||
async with ClientSession() as client:
|
||||
logger.debug(f"Fetching {artifact}")
|
||||
res = await client.get(artifact, ssl=get_ssl_context())
|
||||
if res.status == 200:
|
||||
logger.debug("Got 200. Reading...")
|
||||
data = await res.read()
|
||||
logger.debug(f"Read {len(data)} bytes")
|
||||
res_zip = BytesIO(data)
|
||||
else:
|
||||
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
logger.fatal(f"Could not fetch from URL. {await res.text()}")
|
||||
|
||||
# Check to make sure we got the file
|
||||
if res_zip is None:
|
||||
logger.fatal(f"Could not fetch {artifact}")
|
||||
return
|
||||
|
||||
# If plugin is installed, uninstall it
|
||||
if isInstalled:
|
||||
try:
|
||||
logger.debug("Uninstalling existing plugin...")
|
||||
await self.uninstall_plugin(name)
|
||||
except:
|
||||
logger.error(f"Plugin {name} could not be uninstalled.")
|
||||
|
||||
# Install the plugin
|
||||
logger.debug("Unzipping...")
|
||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||
if ret:
|
||||
plugin_folder = self.find_plugin_folder(name)
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
if name in self.loader.plugins:
|
||||
self.loader.plugins[name].stop()
|
||||
self.loader.plugins.pop(name, None)
|
||||
await sleep(1)
|
||||
|
||||
current_plugin_order = self.settings.getSetting("pluginOrder")
|
||||
current_plugin_order.append(name)
|
||||
self.settings.setSetting("pluginOrder", current_plugin_order)
|
||||
logger.debug("Plugin %s was added to the pluginOrder setting", name)
|
||||
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
|
||||
else:
|
||||
logger.fatal(f"Could not fetch from URL. {await res.text()}")
|
||||
logger.fatal(f"Failed Downloading Remote Binaries")
|
||||
else:
|
||||
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
|
||||
async def request_plugin_install(self, artifact, name, version, hash):
|
||||
request_id = str(time())
|
||||
|
||||
+2
-2
@@ -36,9 +36,9 @@ async def csrf_middleware(request, handler):
|
||||
return await handler(request)
|
||||
return Response(text='Forbidden', status='403')
|
||||
|
||||
# Get the default homebrew path unless a home_path is specified
|
||||
# Get the default homebrew path unless a home_path is specified. home_path argument is deprecated
|
||||
def get_homebrew_path(home_path = None) -> str:
|
||||
return os.path.join(home_path if home_path != None else localplatform.get_home_path(), "homebrew")
|
||||
return localplatform.get_unprivileged_path()
|
||||
|
||||
# Recursively create path and chown as user
|
||||
def mkdir_as_user(path):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import platform
|
||||
import platform, os
|
||||
|
||||
ON_WINDOWS = platform.system() == "Windows"
|
||||
ON_LINUX = not ON_WINDOWS
|
||||
@@ -8,4 +8,33 @@ if ON_WINDOWS:
|
||||
import localplatformwin as localplatform
|
||||
else:
|
||||
from localplatformlinux import *
|
||||
import localplatformlinux as localplatform
|
||||
import localplatformlinux as localplatform
|
||||
|
||||
def get_privileged_path() -> str:
|
||||
'''Get path accessible by elevated user. Holds plugins, decky loader and decky loader configs'''
|
||||
return localplatform.get_privileged_path()
|
||||
|
||||
def get_unprivileged_path() -> str:
|
||||
'''Get path accessible by non-elevated user. Holds plugin configuration, plugin data and plugin logs. Externally referred to as the 'Homebrew' directory'''
|
||||
return localplatform.get_unprivileged_path()
|
||||
|
||||
def get_unprivileged_user() -> str:
|
||||
'''Get user that should own files made in unprivileged path'''
|
||||
return localplatform.get_unprivileged_user()
|
||||
|
||||
def get_chown_plugin_path() -> bool:
|
||||
return os.getenv("CHOWN_PLUGIN_PATH", "1") == "1"
|
||||
|
||||
def get_server_host() -> str:
|
||||
return os.getenv("SERVER_HOST", "127.0.0.1")
|
||||
|
||||
def get_server_port() -> int:
|
||||
return int(os.getenv("SERVER_PORT", "1337"))
|
||||
|
||||
def get_live_reload() -> bool:
|
||||
os.getenv("LIVE_RELOAD", "1") == "1"
|
||||
|
||||
def get_log_level() -> int:
|
||||
return {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
|
||||
os.getenv("LOG_LEVEL", "INFO")
|
||||
]
|
||||
@@ -1,19 +1,16 @@
|
||||
import os, pwd, grp, sys
|
||||
import os, pwd, grp, sys, logging
|
||||
from subprocess import call, run, DEVNULL, PIPE, STDOUT
|
||||
from customtypes import UserType
|
||||
|
||||
logger = logging.getLogger("localplatform")
|
||||
|
||||
# Get the user id hosting the plugin loader
|
||||
def _get_user_id() -> int:
|
||||
proc_path = os.path.realpath(sys.argv[0])
|
||||
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
|
||||
for pw in pws:
|
||||
if proc_path.startswith(os.path.realpath(pw.pw_dir)):
|
||||
return pw.pw_uid
|
||||
raise PermissionError("The plugin loader does not seem to be hosted by any known user.")
|
||||
return pwd.getpwnam(_get_user()).pw_uid
|
||||
|
||||
# Get the user hosting the plugin loader
|
||||
def _get_user() -> str:
|
||||
return pwd.getpwuid(_get_user_id()).pw_name
|
||||
return get_unprivileged_user()
|
||||
|
||||
# Get the effective user id of the running process
|
||||
def _get_effective_user_id() -> int:
|
||||
@@ -50,10 +47,12 @@ def _get_user_group() -> str:
|
||||
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
|
||||
user_str = ""
|
||||
|
||||
if (user == UserType.HOST_USER):
|
||||
if user == UserType.HOST_USER:
|
||||
user_str = _get_user()+":"+_get_user_group()
|
||||
elif (user == UserType.EFFECTIVE_USER):
|
||||
elif user == UserType.EFFECTIVE_USER:
|
||||
user_str = _get_effective_user()+":"+_get_effective_user_group()
|
||||
elif user == UserType.ROOT:
|
||||
user_str = "root:root"
|
||||
else:
|
||||
raise Exception("Unknown User Type")
|
||||
|
||||
@@ -135,4 +134,61 @@ async def service_stop(service_name : str) -> bool:
|
||||
async def service_start(service_name : str) -> bool:
|
||||
cmd = ["systemctl", "start", service_name]
|
||||
res = run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
return res.returncode == 0
|
||||
return res.returncode == 0
|
||||
|
||||
def get_privileged_path() -> str:
|
||||
path = os.getenv("PRIVILEGED_PATH")
|
||||
|
||||
if path == None:
|
||||
path = get_unprivileged_path()
|
||||
|
||||
return path
|
||||
|
||||
def _parent_dir(path : str) -> str:
|
||||
if path == None:
|
||||
return None
|
||||
|
||||
if path.endswith('/'):
|
||||
path = path[:-1]
|
||||
|
||||
return os.path.dirname(path)
|
||||
|
||||
def get_unprivileged_path() -> str:
|
||||
path = os.getenv("UNPRIVILEGED_PATH")
|
||||
|
||||
if path == None:
|
||||
path = _parent_dir(os.getenv("PLUGIN_PATH"))
|
||||
|
||||
if path == None:
|
||||
logger.debug("Unprivileged path is not properly configured. Making something up!")
|
||||
# Expected path of loader binary is /home/deck/homebrew/service/PluginLoader
|
||||
path = _parent_dir(_parent_dir(os.path.realpath(sys.argv[0])))
|
||||
|
||||
if not os.path.exists(path):
|
||||
path = None
|
||||
|
||||
if path == None:
|
||||
logger.warn("Unprivileged path is not properly configured. Defaulting to /home/deck/homebrew")
|
||||
path = "/home/deck/homebrew" # We give up
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def get_unprivileged_user() -> str:
|
||||
user = os.getenv("UNPRIVILEGED_USER")
|
||||
|
||||
if user == None:
|
||||
# Lets hope we can extract it from the unprivileged dir
|
||||
dir = os.path.realpath(get_unprivileged_path())
|
||||
|
||||
pws = sorted(pwd.getpwall(), reverse=True, key=lambda pw: len(pw.pw_dir))
|
||||
for pw in pws:
|
||||
if dir.startswith(os.path.realpath(pw.pw_dir)):
|
||||
user = pw.pw_name
|
||||
break
|
||||
|
||||
if user == None:
|
||||
logger.warn("Unprivileged user is not properly configured. Defaulting to 'deck'")
|
||||
user = 'deck'
|
||||
|
||||
return user
|
||||
@@ -35,4 +35,19 @@ async def service_restart(service_name : str) -> bool:
|
||||
return True # Stubbed
|
||||
|
||||
def get_username() -> str:
|
||||
return os.getlogin()
|
||||
return os.getlogin()
|
||||
|
||||
def get_privileged_path() -> str:
|
||||
'''On windows, privileged_path is equal to unprivileged_path'''
|
||||
return get_unprivileged_path()
|
||||
|
||||
def get_unprivileged_path() -> str:
|
||||
path = os.getenv("UNPRIVILEGED_PATH")
|
||||
|
||||
if path == None:
|
||||
path = os.getenv("PRIVILEGED_PATH", os.path.join(os.path.expanduser("~"), "homebrew"))
|
||||
|
||||
return path
|
||||
|
||||
def get_unprivileged_user() -> str:
|
||||
return os.getenv("UNPRIVILEGED_USER", os.getlogin())
|
||||
|
||||
+16
-22
@@ -1,6 +1,10 @@
|
||||
# Change PyInstaller files permissions
|
||||
import sys
|
||||
from localplatform import chmod, chown, service_stop, service_start, ON_WINDOWS
|
||||
from localplatform import (chmod, chown, service_stop, service_start,
|
||||
ON_WINDOWS, get_log_level, get_live_reload,
|
||||
get_server_port, get_server_host, get_chown_plugin_path,
|
||||
get_unprivileged_user, get_unprivileged_path,
|
||||
get_privileged_path)
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
chmod(sys._MEIPASS, 755)
|
||||
# Full imports
|
||||
@@ -20,7 +24,7 @@ from aiohttp_jinja2 import setup as jinja_setup
|
||||
# local modules
|
||||
from browser import PluginBrowser
|
||||
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
|
||||
get_homebrew_path, mkdir_as_user, get_system_pythonpaths)
|
||||
mkdir_as_user, get_system_pythonpaths)
|
||||
|
||||
from injector import get_gamepadui_tab, Tab, get_tabs, close_old_tabs
|
||||
from loader import Loader
|
||||
@@ -29,33 +33,23 @@ from updater import Updater
|
||||
from utilities import Utilities
|
||||
from customtypes import UserType
|
||||
|
||||
HOMEBREW_PATH = get_homebrew_path()
|
||||
CONFIG = {
|
||||
"plugin_path": getenv("PLUGIN_PATH", path.join(HOMEBREW_PATH, "plugins")),
|
||||
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
|
||||
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
||||
"server_port": int(getenv("SERVER_PORT", "1337")),
|
||||
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
|
||||
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
|
||||
getenv("LOG_LEVEL", "INFO")
|
||||
],
|
||||
}
|
||||
|
||||
basicConfig(
|
||||
level=CONFIG["log_level"],
|
||||
level=get_log_level(),
|
||||
format="[%(module)s][%(levelname)s]: %(message)s"
|
||||
)
|
||||
|
||||
logger = getLogger("Main")
|
||||
plugin_path = path.join(get_privileged_path(), "plugins")
|
||||
|
||||
def chown_plugin_dir():
|
||||
if not path.exists(CONFIG["plugin_path"]): # For safety, create the folder before attempting to do anything with it
|
||||
mkdir_as_user(CONFIG["plugin_path"])
|
||||
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
|
||||
mkdir_as_user(plugin_path)
|
||||
|
||||
if not chown(CONFIG["plugin_path"], UserType.HOST_USER) or not chmod(CONFIG["plugin_path"], 555):
|
||||
if not chown(plugin_path, UserType.HOST_USER) or not chmod(plugin_path, 555):
|
||||
logger.error(f"chown/chmod exited with a non-zero exit code")
|
||||
|
||||
if CONFIG["chown_plugin_path"] == True:
|
||||
if get_chown_plugin_path() == True:
|
||||
chown_plugin_dir()
|
||||
|
||||
class PluginManager:
|
||||
@@ -70,9 +64,9 @@ class PluginManager:
|
||||
allow_credentials=True
|
||||
)
|
||||
})
|
||||
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
|
||||
self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "settings"))
|
||||
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.plugin_loader.plugins, self.plugin_loader, self.settings)
|
||||
self.plugin_loader = Loader(self.web_app, plugin_path, self.loop, get_live_reload())
|
||||
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
|
||||
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
|
||||
self.utilities = Utilities(self)
|
||||
self.updater = Updater(self)
|
||||
|
||||
@@ -174,7 +168,7 @@ class PluginManager:
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
return run_app(self.web_app, host=CONFIG["server_host"], port=CONFIG["server_port"], loop=self.loop, access_log=None)
|
||||
return run_app(self.web_app, host=get_server_host(), port=get_server_port(), loop=self.loop, access_log=None)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if ON_WINDOWS:
|
||||
|
||||
+4
-5
@@ -1,6 +1,6 @@
|
||||
from json import dump, load
|
||||
from os import mkdir, path, listdir, rename
|
||||
from localplatform import chown, folder_owner
|
||||
from localplatform import chown, folder_owner, get_chown_plugin_path
|
||||
from customtypes import UserType
|
||||
|
||||
from helpers import get_homebrew_path
|
||||
@@ -17,7 +17,6 @@ class SettingsManager:
|
||||
#Create the folder with the correct permission
|
||||
if not path.exists(settings_directory):
|
||||
mkdir(settings_directory)
|
||||
chown(settings_directory)
|
||||
|
||||
#Copy all old settings file in the root directory to the correct folder
|
||||
for file in listdir(wrong_dir):
|
||||
@@ -28,9 +27,9 @@ class SettingsManager:
|
||||
|
||||
|
||||
#If the owner of the settings directory is not the user, then set it as the user:
|
||||
|
||||
if folder_owner(settings_directory) != UserType.HOST_USER:
|
||||
chown(settings_directory, UserType.HOST_USER, False)
|
||||
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.ROOT
|
||||
if folder_owner(settings_directory) != expected_user:
|
||||
chown(settings_directory, expected_user, False)
|
||||
|
||||
self.settings = {}
|
||||
|
||||
|
||||
@@ -104,6 +104,15 @@ 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()
|
||||
if selectedBranch == 0:
|
||||
logger.debug("release type: release")
|
||||
remoteVersions = list(filter(lambda ver: ver["tag_name"].startswith("v") and not ver["prerelease"] and not ver["tag_name"].find("-pre") > 0 and ver["tag_name"], remoteVersions))
|
||||
elif selectedBranch == 1:
|
||||
logger.debug("release type: pre-release")
|
||||
remoteVersions = list(filter(lambda ver:ver["tag_name"].startswith("v"), remoteVersions))
|
||||
else:
|
||||
logger.error("release type: NOT FOUND")
|
||||
raise ValueError("no valid branch found")
|
||||
self.allRemoteVers = remoteVersions
|
||||
logger.debug("determining release type to find, branch is %i" % selectedBranch)
|
||||
if selectedBranch == 0:
|
||||
|
||||
+2
-1
@@ -9,7 +9,8 @@ Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Vendored
+2
-1
@@ -9,7 +9,8 @@ Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "3.20.5",
|
||||
"decky-frontend-lib": "3.20.6",
|
||||
"react-file-icon": "^1.3.0",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-markdown": "^8.0.6",
|
||||
|
||||
Generated
+4
-4
@@ -2,8 +2,8 @@ lockfileVersion: '6.0'
|
||||
|
||||
dependencies:
|
||||
decky-frontend-lib:
|
||||
specifier: 3.20.5
|
||||
version: 3.20.5
|
||||
specifier: 3.20.6
|
||||
version: 3.20.6
|
||||
react-file-icon:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0(react-dom@16.14.0)(react@16.14.0)
|
||||
@@ -1007,8 +1007,8 @@ packages:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
/decky-frontend-lib@3.20.5:
|
||||
resolution: {integrity: sha512-aXllFYhWovoiyBHNzH8PW9EYgXotY9ysuU9icFNgrOWFotyJV+2KGLnfYEyBlDNiexKvXKVRKPw1gRFX2hP4AQ==}
|
||||
/decky-frontend-lib@3.20.6:
|
||||
resolution: {integrity: sha512-imx3vgVpFm3p9+Pw5cjN964PB6Gt+4HGXkwgDRrySgyTOg8luSUB5DuJPR0Pu5a4wpVNrewKx6Qt+MTqJpN6SQ==}
|
||||
dev: false
|
||||
|
||||
/decode-named-character-reference@1.0.2:
|
||||
|
||||
@@ -29,10 +29,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
|
||||
strTitle={`Install ${artifact}`}
|
||||
strOKButtonText={loading ? 'Installing' : 'Install'}
|
||||
>
|
||||
{hash == 'False' ? (
|
||||
<h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3>
|
||||
) : (
|
||||
`Are you sure you want to install ${artifact} ${version}?`
|
||||
Are you sure you want to install {artifact}
|
||||
{version ? ` ${version}` : ''}?
|
||||
{hash == 'False' && (
|
||||
<span style={{ color: 'red' }}> This plugin does not have a hash, you are installing it at your own risk.</span>
|
||||
)}
|
||||
</ConfirmModal>
|
||||
);
|
||||
|
||||
@@ -134,7 +134,15 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{file.name}
|
||||
<span
|
||||
style={{
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
</DialogButton>
|
||||
);
|
||||
|
||||
@@ -1,64 +1,109 @@
|
||||
import { DialogBody, Field, TextField, Toggle } from 'decky-frontend-lib';
|
||||
import { useRef } from 'react';
|
||||
import { FaReact, FaSteamSymbol } from 'react-icons/fa';
|
||||
import {
|
||||
DialogBody,
|
||||
DialogButton,
|
||||
DialogControlsSection,
|
||||
DialogControlsSectionHeader,
|
||||
Field,
|
||||
TextField,
|
||||
Toggle,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useRef, useState } from 'react';
|
||||
import { FaFileArchive, FaLink, FaReact, FaSteamSymbol } from 'react-icons/fa';
|
||||
|
||||
import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
|
||||
import { installFromURL } from '../../../../store';
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
import RemoteDebuggingSettings from '../general/RemoteDebugging';
|
||||
|
||||
const installFromZip = () => {
|
||||
window.DeckyPluginLoader.openFilePicker('/home/deck', true).then((val) => {
|
||||
const url = `file://${val.path}`;
|
||||
console.log(`Installing plugin locally from ${url}`);
|
||||
|
||||
if (url.endsWith('.zip')) {
|
||||
installFromURL(url);
|
||||
} else {
|
||||
window.DeckyPluginLoader.toaster.toast({
|
||||
title: 'Decky',
|
||||
body: `Installation failed! Only ZIP files are supported.`,
|
||||
onClick: installFromZip,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default function DeveloperSettings() {
|
||||
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
|
||||
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
|
||||
const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', '');
|
||||
const [pluginURL, setPluginURL] = useState('');
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<DialogBody>
|
||||
<RemoteDebuggingSettings />
|
||||
<Field
|
||||
label="Enable Valve Internal"
|
||||
description={
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
Enables the Valve internal developer menu.{' '}
|
||||
<span style={{ color: 'red' }}>Do not touch anything in this menu unless you know what it does.</span>
|
||||
</span>
|
||||
}
|
||||
icon={<FaSteamSymbol style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={enableValveInternal}
|
||||
onChange={(toggleValue) => {
|
||||
setEnableValveInternal(toggleValue);
|
||||
setShowValveInternal(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Enable React DevTools"
|
||||
description={
|
||||
<>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Third-Party Plugins</DialogControlsSectionHeader>
|
||||
<Field label="Install Plugin from ZIP File" icon={<FaFileArchive style={{ display: 'block' }} />}>
|
||||
<DialogButton onClick={installFromZip}>Browse</DialogButton>
|
||||
</Field>
|
||||
<Field
|
||||
label="Install Plugin from URL"
|
||||
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
||||
icon={<FaLink style={{ display: 'block' }} />}
|
||||
>
|
||||
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
||||
Install
|
||||
</DialogButton>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>Other</DialogControlsSectionHeader>
|
||||
<RemoteDebuggingSettings />
|
||||
<Field
|
||||
label="Enable Valve Internal"
|
||||
description={
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the
|
||||
IP address before enabling.
|
||||
Enables the Valve internal developer menu.{' '}
|
||||
<span style={{ color: 'red' }}>Do not touch anything in this menu unless you know what it does.</span>
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<div ref={textRef}>
|
||||
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
icon={<FaReact style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={reactDevtoolsEnabled}
|
||||
// disabled={reactDevtoolsIP == ''}
|
||||
onChange={(toggleValue) => {
|
||||
setReactDevtoolsEnabled(toggleValue);
|
||||
setShouldConnectToReactDevTools(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
}
|
||||
icon={<FaSteamSymbol style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={enableValveInternal}
|
||||
onChange={(toggleValue) => {
|
||||
setEnableValveInternal(toggleValue);
|
||||
setShowValveInternal(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Enable React DevTools"
|
||||
description={
|
||||
<>
|
||||
<span style={{ whiteSpace: 'pre-line' }}>
|
||||
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set
|
||||
the IP address before enabling.
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<div ref={textRef}>
|
||||
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
icon={<FaReact style={{ display: 'block' }} />}
|
||||
>
|
||||
<Toggle
|
||||
value={reactDevtoolsEnabled}
|
||||
// disabled={reactDevtoolsIP == ''}
|
||||
onChange={(toggleValue) => {
|
||||
setReactDevtoolsEnabled(toggleValue);
|
||||
setShouldConnectToReactDevTools(toggleValue);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
</DialogBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1>{versionInfo?.all?.[id]?.name}</h1>
|
||||
<h1>{versionInfo?.all?.[id]?.name || 'Invalid Update Name'}</h1>
|
||||
{versionInfo?.all?.[id]?.body ? (
|
||||
<WithSuspense>
|
||||
<MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer>
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import {
|
||||
DialogBody,
|
||||
DialogButton,
|
||||
DialogControlsSection,
|
||||
DialogControlsSectionHeader,
|
||||
Field,
|
||||
TextField,
|
||||
Toggle,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useState } from 'react';
|
||||
import { DialogBody, DialogControlsSection, DialogControlsSectionHeader, Field, Toggle } from 'decky-frontend-lib';
|
||||
|
||||
import { installFromURL } from '../../../../store';
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
import BranchSelect from './BranchSelect';
|
||||
import StoreSelect from './StoreSelect';
|
||||
@@ -22,7 +12,6 @@ export default function GeneralSettings({
|
||||
isDeveloper: boolean;
|
||||
setIsDeveloper: (val: boolean) => void;
|
||||
}) {
|
||||
const [pluginURL, setPluginURL] = useState('');
|
||||
const { versionInfo } = useDeckyState();
|
||||
|
||||
return (
|
||||
@@ -46,14 +35,6 @@ export default function GeneralSettings({
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Install plugin from URL"
|
||||
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
||||
>
|
||||
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
||||
Install
|
||||
</DialogButton>
|
||||
</Field>
|
||||
</DialogControlsSection>
|
||||
<DialogControlsSection>
|
||||
<DialogControlsSectionHeader>About</DialogControlsSectionHeader>
|
||||
|
||||
Reference in New Issue
Block a user