mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 08:47:49 +00:00
Add file picker plugin install, plugin installs to developer page (#405)
This commit is contained in:
+57
-38
@@ -139,6 +139,10 @@ class PluginBrowser:
|
|||||||
self.loader.watcher.disabled = False
|
self.loader.watcher.disabled = False
|
||||||
|
|
||||||
async def _install(self, artifact, name, version, hash):
|
async def _install(self, artifact, name, version, hash):
|
||||||
|
# Will be set later in code
|
||||||
|
res_zip = None
|
||||||
|
|
||||||
|
# Check if plugin is installed
|
||||||
isInstalled = False
|
isInstalled = False
|
||||||
if self.loader.watcher:
|
if self.loader.watcher:
|
||||||
self.loader.watcher.disabled = True
|
self.loader.watcher.disabled = True
|
||||||
@@ -148,47 +152,62 @@ class PluginBrowser:
|
|||||||
isInstalled = True
|
isInstalled = True
|
||||||
except:
|
except:
|
||||||
logger.error(f"Failed to determine if {name} is already installed, continuing anyway.")
|
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")
|
# Check if the file is a local file or a URL
|
||||||
current_plugin_order.append(name)
|
if artifact.startswith("file://"):
|
||||||
self.settings.setSetting("pluginOrder", current_plugin_order)
|
logger.info(f"Installing {name} from local ZIP file (Version: {version})")
|
||||||
logger.debug("Plugin %s was added to the pluginOrder setting", name)
|
res_zip = BytesIO(open(artifact[7:], "rb").read())
|
||||||
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
|
else:
|
||||||
else:
|
logger.info(f"Installing {name} from URL (Version: {version})")
|
||||||
logger.fatal(f"Failed Downloading Remote Binaries")
|
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:
|
else:
|
||||||
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
logger.fatal(f"Could not fetch from URL. {await res.text()}")
|
||||||
if self.loader.watcher:
|
|
||||||
self.loader.watcher.disabled = False
|
# 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:
|
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):
|
async def request_plugin_install(self, artifact, name, version, hash):
|
||||||
request_id = str(time())
|
request_id = str(time())
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
|
|||||||
strTitle={`Install ${artifact}`}
|
strTitle={`Install ${artifact}`}
|
||||||
strOKButtonText={loading ? 'Installing' : 'Install'}
|
strOKButtonText={loading ? 'Installing' : 'Install'}
|
||||||
>
|
>
|
||||||
{hash == 'False' ? (
|
Are you sure you want to install {artifact}
|
||||||
<h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3>
|
{version ? ` ${version}` : ''}?
|
||||||
) : (
|
{hash == 'False' && (
|
||||||
`Are you sure you want to install ${artifact} ${version}?`
|
<span style={{ color: 'red' }}> This plugin does not have a hash, you are installing it at your own risk.</span>
|
||||||
)}
|
)}
|
||||||
</ConfirmModal>
|
</ConfirmModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -134,7 +134,15 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{file.name}
|
<span
|
||||||
|
style={{
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textAlign: 'left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</DialogButton>
|
</DialogButton>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,64 +1,109 @@
|
|||||||
import { DialogBody, Field, TextField, Toggle } from 'decky-frontend-lib';
|
import {
|
||||||
import { useRef } from 'react';
|
DialogBody,
|
||||||
import { FaReact, FaSteamSymbol } from 'react-icons/fa';
|
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 { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer';
|
||||||
|
import { installFromURL } from '../../../../store';
|
||||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||||
import RemoteDebuggingSettings from '../general/RemoteDebugging';
|
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() {
|
export default function DeveloperSettings() {
|
||||||
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
|
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
|
||||||
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
|
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
|
||||||
const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', '');
|
const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', '');
|
||||||
|
const [pluginURL, setPluginURL] = useState('');
|
||||||
const textRef = useRef<HTMLDivElement>(null);
|
const textRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<RemoteDebuggingSettings />
|
<DialogControlsSection>
|
||||||
<Field
|
<DialogControlsSectionHeader>Third-Party Plugins</DialogControlsSectionHeader>
|
||||||
label="Enable Valve Internal"
|
<Field label="Install Plugin from ZIP File" icon={<FaFileArchive style={{ display: 'block' }} />}>
|
||||||
description={
|
<DialogButton onClick={installFromZip}>Browse</DialogButton>
|
||||||
<span style={{ whiteSpace: 'pre-line' }}>
|
</Field>
|
||||||
Enables the Valve internal developer menu.{' '}
|
<Field
|
||||||
<span style={{ color: 'red' }}>Do not touch anything in this menu unless you know what it does.</span>
|
label="Install Plugin from URL"
|
||||||
</span>
|
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
||||||
}
|
icon={<FaLink style={{ display: 'block' }} />}
|
||||||
icon={<FaSteamSymbol style={{ display: 'block' }} />}
|
>
|
||||||
>
|
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
|
||||||
<Toggle
|
Install
|
||||||
value={enableValveInternal}
|
</DialogButton>
|
||||||
onChange={(toggleValue) => {
|
</Field>
|
||||||
setEnableValveInternal(toggleValue);
|
</DialogControlsSection>
|
||||||
setShowValveInternal(toggleValue);
|
<DialogControlsSection>
|
||||||
}}
|
<DialogControlsSectionHeader>Other</DialogControlsSectionHeader>
|
||||||
/>
|
<RemoteDebuggingSettings />
|
||||||
</Field>
|
<Field
|
||||||
<Field
|
label="Enable Valve Internal"
|
||||||
label="Enable React DevTools"
|
description={
|
||||||
description={
|
|
||||||
<>
|
|
||||||
<span style={{ whiteSpace: 'pre-line' }}>
|
<span style={{ whiteSpace: 'pre-line' }}>
|
||||||
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set the
|
Enables the Valve internal developer menu.{' '}
|
||||||
IP address before enabling.
|
<span style={{ color: 'red' }}>Do not touch anything in this menu unless you know what it does.</span>
|
||||||
</span>
|
</span>
|
||||||
<br />
|
}
|
||||||
<br />
|
icon={<FaSteamSymbol style={{ display: 'block' }} />}
|
||||||
<div ref={textRef}>
|
>
|
||||||
<TextField label={'IP'} value={reactDevtoolsIP} onChange={(e) => setReactDevtoolsIP(e?.target.value)} />
|
<Toggle
|
||||||
</div>
|
value={enableValveInternal}
|
||||||
</>
|
onChange={(toggleValue) => {
|
||||||
}
|
setEnableValveInternal(toggleValue);
|
||||||
icon={<FaReact style={{ display: 'block' }} />}
|
setShowValveInternal(toggleValue);
|
||||||
>
|
}}
|
||||||
<Toggle
|
/>
|
||||||
value={reactDevtoolsEnabled}
|
</Field>
|
||||||
// disabled={reactDevtoolsIP == ''}
|
<Field
|
||||||
onChange={(toggleValue) => {
|
label="Enable React DevTools"
|
||||||
setReactDevtoolsEnabled(toggleValue);
|
description={
|
||||||
setShouldConnectToReactDevTools(toggleValue);
|
<>
|
||||||
}}
|
<span style={{ whiteSpace: 'pre-line' }}>
|
||||||
/>
|
Enables connection to a computer running React DevTools. Changing this setting will reload Steam. Set
|
||||||
</Field>
|
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>
|
</DialogBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
import {
|
import { DialogBody, DialogControlsSection, DialogControlsSectionHeader, Field, Toggle } from 'decky-frontend-lib';
|
||||||
DialogBody,
|
|
||||||
DialogButton,
|
|
||||||
DialogControlsSection,
|
|
||||||
DialogControlsSectionHeader,
|
|
||||||
Field,
|
|
||||||
TextField,
|
|
||||||
Toggle,
|
|
||||||
} from 'decky-frontend-lib';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { installFromURL } from '../../../../store';
|
|
||||||
import { useDeckyState } from '../../../DeckyState';
|
import { useDeckyState } from '../../../DeckyState';
|
||||||
import BranchSelect from './BranchSelect';
|
import BranchSelect from './BranchSelect';
|
||||||
import StoreSelect from './StoreSelect';
|
import StoreSelect from './StoreSelect';
|
||||||
@@ -22,7 +12,6 @@ export default function GeneralSettings({
|
|||||||
isDeveloper: boolean;
|
isDeveloper: boolean;
|
||||||
setIsDeveloper: (val: boolean) => void;
|
setIsDeveloper: (val: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const [pluginURL, setPluginURL] = useState('');
|
|
||||||
const { versionInfo } = useDeckyState();
|
const { versionInfo } = useDeckyState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,14 +35,6 @@ export default function GeneralSettings({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</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>
|
||||||
<DialogControlsSection>
|
<DialogControlsSection>
|
||||||
<DialogControlsSectionHeader>About</DialogControlsSectionHeader>
|
<DialogControlsSectionHeader>About</DialogControlsSectionHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user