Compare commits

...

5 Commits

Author SHA1 Message Date
AAGaming caf37d681f Fix tab jank on latest steam beta 2023-04-28 22:29:46 -04:00
EMERALD 93151e4e5e Add file picker plugin install, plugin installs to developer page (#405) 2023-04-25 19:20:39 -07:00
suchmememanyskill d6f336d84b Feat/configurable paths (#404) 2023-04-24 20:12:42 -07:00
Beebles 4777963b65 Make patch notes modal only show current branch (#429) 2023-04-23 17:18:54 -07:00
Party Wumpus fc193f98db Fix browser.py (#431) 2023-04-21 20:14:34 -07:00
17 changed files with 319 additions and 162 deletions
+59 -40
View File
@@ -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
View File
@@ -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):
+31 -2
View File
@@ -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")
]
+67 -11
View File
@@ -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
+16 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 = {}
+9
View File
@@ -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
View File
@@ -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
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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",
+4 -4
View File
@@ -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>