Merge remote-tracking branch 'origin/main'

This commit is contained in:
Weblate
2023-06-08 14:40:08 +00:00
16 changed files with 304 additions and 59 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ For more information about Decky Loader as well as documentation and development
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu. 1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
1. Select "Switch to Desktop". 1. Select "Switch to Desktop".
1. Navigate to this Github page on a browser of your choice. 1. Navigate to this Github page on a browser of your choice.
1. Download the [installer file](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop). 1. Download the [installer file](https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop). (If using firefox, it will be named `decky_installer.desktop.download`. Rename it to `decky_installer.desktop` before running it)
1. Drag the file onto your desktop and double click it to run it. 1. Drag the file onto your desktop and double click it to run it.
1. Either type your admin password or allow Decky to temporarily set your admin password to `Decky!` (this password will be removed after the installer finishes) 1. Either type your admin password or allow Decky to temporarily set your admin password to `Decky!` (this password will be removed after the installer finishes)
1. Choose the version of Decky Loader you want to install. 1. Choose the version of Decky Loader you want to install.
+16 -4
View File
@@ -122,10 +122,7 @@ class PluginBrowser:
logger.debug("Plugin %s was stopped", name) logger.debug("Plugin %s was stopped", name)
del self.plugins[name] del self.plugins[name]
logger.debug("Plugin %s was removed from the dictionary", name) logger.debug("Plugin %s was removed from the dictionary", name)
current_plugin_order = self.settings.getSetting("pluginOrder") self.cleanup_plugin_settings(name)
current_plugin_order.remove(name)
self.settings.setSetting("pluginOrder", current_plugin_order)
logger.debug("Plugin %s was removed from the pluginOrder setting", name)
logger.debug("removing files %s" % str(name)) logger.debug("removing files %s" % str(name))
rmtree(plugin_dir) rmtree(plugin_dir)
except FileNotFoundError: except FileNotFoundError:
@@ -234,3 +231,18 @@ class PluginBrowser:
def cancel_plugin_install(self, request_id): def cancel_plugin_install(self, request_id):
self.install_requests.pop(request_id) self.install_requests.pop(request_id)
def cleanup_plugin_settings(self, name):
"""Removes any settings related to a plugin. Propably called when a plugin is uninstalled.
Args:
name (string): The name of the plugin
"""
hidden_plugins = self.settings.getSetting("hiddenPlugins", [])
hidden_plugins.remove(name)
self.settings.setSetting("hiddenPlugins", hidden_plugins)
plugin_order = self.settings.getSetting("pluginOrder")
plugin_order.remove(name)
self.settings.setSetting("pluginOrder", plugin_order)
logger.debug("Removed any settings for plugin %s", name)
+2 -1
View File
@@ -395,6 +395,7 @@ async def get_tab_lambda(test) -> Tab:
return tab return tab
SHARED_CTX_NAMES = ["SharedJSContext", "Steam Shared Context presented by Valve™", "Steam", "SP"] SHARED_CTX_NAMES = ["SharedJSContext", "Steam Shared Context presented by Valve™", "Steam", "SP"]
CLOSEABLE_URLS = ["about:blank", "data:text/html,%3Cbody%3E%3C%2Fbody%3E"] # Closing anything other than these *really* likes to crash Steam
DO_NOT_CLOSE_URL = "Valve Steam Gamepad/default" # Steam Big Picture Mode tab DO_NOT_CLOSE_URL = "Valve Steam Gamepad/default" # Steam Big Picture Mode tab
def tab_is_gamepadui(t: Tab) -> bool: def tab_is_gamepadui(t: Tab) -> bool:
@@ -415,7 +416,7 @@ async def inject_to_tab(tab_name, js, run_async=False):
async def close_old_tabs(): async def close_old_tabs():
tabs = await get_tabs() tabs = await get_tabs()
for t in tabs: for t in tabs:
if not t.title or (t.title not in SHARED_CTX_NAMES and DO_NOT_CLOSE_URL not in t.url): if not t.title or (t.title not in SHARED_CTX_NAMES and any(url in t.url for url in CLOSEABLE_URLS) and DO_NOT_CLOSE_URL not in t.url):
logger.debug("Closing tab: " + getattr(t, "title", "Untitled")) logger.debug("Closing tab: " + getattr(t, "title", "Untitled"))
await t.close() await t.close()
await sleep(0.5) await sleep(0.5)
+9
View File
@@ -17,6 +17,13 @@
"select": "Use this folder" "select": "Use this folder"
} }
}, },
"PluginView": {
"hidden_one": "1 plugin is hidden from this list",
"hidden_other": "{{count}} plugins are hidden from this list"
},
"PluginListLabel": {
"hidden": "Hidden from the quick access menu"
},
"PluginCard": { "PluginCard": {
"plugin_full_access": "This plugin has full access to your Steam Deck.", "plugin_full_access": "This plugin has full access to your Steam Deck.",
"plugin_install": "Install", "plugin_install": "Install",
@@ -73,6 +80,8 @@
"reload": "Reload", "reload": "Reload",
"uninstall": "Uninstall", "uninstall": "Uninstall",
"update_to": "Update to {{name}}", "update_to": "Update to {{name}}",
"show": "Quick access: Show",
"hide": "Quick access: Hide",
"update_all_one": "Update 1 plugin", "update_all_one": "Update 1 plugin",
"update_all_other": "Update {{count}} plugins" "update_all_other": "Update {{count}} plugins"
}, },
+2
View File
@@ -2,3 +2,5 @@ node_modules/
.yalc .yalc
yalc.lock yalc.lock
stats.html
+1
View File
@@ -33,6 +33,7 @@
"rollup-plugin-delete": "^2.0.0", "rollup-plugin-delete": "^2.0.0",
"rollup-plugin-external-globals": "^0.6.1", "rollup-plugin-external-globals": "^0.6.1",
"rollup-plugin-polyfill-node": "^0.10.2", "rollup-plugin-polyfill-node": "^0.10.2",
"rollup-plugin-visualizer": "^5.9.0",
"tslib": "^2.5.2", "tslib": "^2.5.2",
"typescript": "^4.9.5" "typescript": "^4.9.5"
}, },
+94
View File
@@ -93,6 +93,9 @@ devDependencies:
rollup-plugin-polyfill-node: rollup-plugin-polyfill-node:
specifier: ^0.10.2 specifier: ^0.10.2
version: 0.10.2(rollup@2.79.1) version: 0.10.2(rollup@2.79.1)
rollup-plugin-visualizer:
specifier: ^5.9.0
version: 5.9.0(rollup@2.79.1)
tslib: tslib:
specifier: ^2.5.2 specifier: ^2.5.2
version: 2.5.2 version: 2.5.2
@@ -1235,6 +1238,15 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
dev: true dev: true
/cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
dev: true
/clone-buffer@1.0.0: /clone-buffer@1.0.0:
resolution: {integrity: sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==} resolution: {integrity: sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
@@ -1414,6 +1426,11 @@ packages:
clone: 1.0.4 clone: 1.0.4
dev: true dev: true
/define-lazy-prop@2.0.0:
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
engines: {node: '>=8'}
dev: true
/define-properties@1.2.0: /define-properties@1.2.0:
resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1770,6 +1787,11 @@ packages:
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dev: true dev: true
/get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
dev: true
/get-intrinsic@1.2.1: /get-intrinsic@1.2.1:
resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
dependencies: dependencies:
@@ -2126,6 +2148,12 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/is-docker@2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
hasBin: true
dev: true
/is-extglob@2.1.1: /is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -2222,6 +2250,13 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/is-wsl@2.2.0:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
dependencies:
is-docker: 2.2.1
dev: true
/isarray@1.0.0: /isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
dev: true dev: true
@@ -2891,6 +2926,15 @@ packages:
mimic-fn: 2.1.0 mimic-fn: 2.1.0
dev: true dev: true
/open@8.4.2:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'}
dependencies:
define-lazy-prop: 2.0.0
is-docker: 2.2.1
is-wsl: 2.2.0
dev: true
/ora@5.4.1: /ora@5.4.1:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -3237,6 +3281,11 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
dev: true dev: true
/require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
dev: true
/resolve-from@3.0.0: /resolve-from@3.0.0:
resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -3318,6 +3367,23 @@ packages:
rollup: 2.79.1 rollup: 2.79.1
dev: true dev: true
/rollup-plugin-visualizer@5.9.0(rollup@2.79.1):
resolution: {integrity: sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg==}
engines: {node: '>=14'}
hasBin: true
peerDependencies:
rollup: 2.x || 3.x
peerDependenciesMeta:
rollup:
optional: true
dependencies:
open: 8.4.2
picomatch: 2.3.1
rollup: 2.79.1
source-map: 0.7.4
yargs: 17.7.2
dev: true
/rollup@2.79.1: /rollup@2.79.1:
resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@@ -3425,6 +3491,11 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/source-map@0.7.4:
resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
engines: {node: '>= 8'}
dev: true
/sourcemap-codec@1.4.8: /sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
deprecated: Please use @jridgewell/sourcemap-codec instead deprecated: Please use @jridgewell/sourcemap-codec instead
@@ -3958,10 +4029,33 @@ packages:
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
dev: true dev: true
/y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
dev: true
/yallist@3.1.1: /yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
dev: true dev: true
/yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
dev: true
/yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
dependencies:
cliui: 8.0.1
escalade: 3.1.1
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
dev: true
/zwitch@2.0.4: /zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: false dev: false
+3 -1
View File
@@ -7,6 +7,7 @@ import typescript from '@rollup/plugin-typescript';
import { defineConfig } from 'rollup'; import { defineConfig } from 'rollup';
import del from 'rollup-plugin-delete'; import del from 'rollup-plugin-delete';
import externalGlobals from 'rollup-plugin-external-globals'; import externalGlobals from 'rollup-plugin-external-globals';
import { visualizer } from 'rollup-plugin-visualizer';
const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL']; const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL'];
@@ -16,7 +17,7 @@ export default defineConfig({
del({ targets: '../backend/static/*', force: true }), del({ targets: '../backend/static/*', force: true }),
commonjs(), commonjs(),
nodeResolve({ nodeResolve({
browser: true browser: true,
}), }),
externalGlobals({ externalGlobals({
react: 'SP_REACT', react: 'SP_REACT',
@@ -33,6 +34,7 @@ export default defineConfig({
'process.env.NODE_ENV': JSON.stringify('production'), 'process.env.NODE_ENV': JSON.stringify('production'),
}), }),
image(), image(),
visualizer(),
], ],
preserveEntrySignatures: false, preserveEntrySignatures: false,
output: { output: {
+13 -5
View File
@@ -7,6 +7,7 @@ import { VerInfo } from '../updater';
interface PublicDeckyState { interface PublicDeckyState {
plugins: Plugin[]; plugins: Plugin[];
pluginOrder: string[]; pluginOrder: string[];
hiddenPlugins: string[];
activePlugin: Plugin | null; activePlugin: Plugin | null;
updates: PluginUpdateMapping | null; updates: PluginUpdateMapping | null;
hasLoaderUpdate?: boolean; hasLoaderUpdate?: boolean;
@@ -17,6 +18,7 @@ interface PublicDeckyState {
export class DeckyState { export class DeckyState {
private _plugins: Plugin[] = []; private _plugins: Plugin[] = [];
private _pluginOrder: string[] = []; private _pluginOrder: string[] = [];
private _hiddenPlugins: string[] = [];
private _activePlugin: Plugin | null = null; private _activePlugin: Plugin | null = null;
private _updates: PluginUpdateMapping | null = null; private _updates: PluginUpdateMapping | null = null;
private _hasLoaderUpdate: boolean = false; private _hasLoaderUpdate: boolean = false;
@@ -29,6 +31,7 @@ export class DeckyState {
return { return {
plugins: this._plugins, plugins: this._plugins,
pluginOrder: this._pluginOrder, pluginOrder: this._pluginOrder,
hiddenPlugins: this._hiddenPlugins,
activePlugin: this._activePlugin, activePlugin: this._activePlugin,
updates: this._updates, updates: this._updates,
hasLoaderUpdate: this._hasLoaderUpdate, hasLoaderUpdate: this._hasLoaderUpdate,
@@ -52,6 +55,11 @@ export class DeckyState {
this.notifyUpdate(); this.notifyUpdate();
} }
setHiddenPlugins(hiddenPlugins: string[]) {
this._hiddenPlugins = hiddenPlugins;
this.notifyUpdate();
}
setActivePlugin(name: string) { setActivePlugin(name: string) {
this._activePlugin = this._plugins.find((plugin) => plugin.name === name) ?? null; this._activePlugin = this._plugins.find((plugin) => plugin.name === name) ?? null;
this.notifyUpdate(); this.notifyUpdate();
@@ -111,11 +119,11 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
return () => deckyState.eventBus.removeEventListener('update', onUpdate); return () => deckyState.eventBus.removeEventListener('update', onUpdate);
}, []); }, []);
const setIsLoaderUpdating = (hasUpdate: boolean) => deckyState.setIsLoaderUpdating(hasUpdate); const setIsLoaderUpdating = deckyState.setIsLoaderUpdating.bind(deckyState);
const setVersionInfo = (versionInfo: VerInfo) => deckyState.setVersionInfo(versionInfo); const setVersionInfo = deckyState.setVersionInfo.bind(deckyState);
const setActivePlugin = (name: string) => deckyState.setActivePlugin(name); const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
const closeActivePlugin = () => deckyState.closeActivePlugin(); const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
const setPluginOrder = (pluginOrder: string[]) => deckyState.setPluginOrder(pluginOrder); const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
return ( return (
<DeckyStateContext.Provider <DeckyStateContext.Provider
+11
View File
@@ -8,6 +8,8 @@ import {
staticClasses, staticClasses,
} from 'decky-frontend-lib'; } from 'decky-frontend-lib';
import { VFC, useEffect, useState } from 'react'; import { VFC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa';
import { Plugin } from '../plugin'; import { Plugin } from '../plugin';
import { useDeckyState } from './DeckyState'; import { useDeckyState } from './DeckyState';
@@ -16,8 +18,10 @@ import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView'; import TitleView from './TitleView';
const PluginView: VFC = () => { const PluginView: VFC = () => {
const { hiddenPlugins } = useDeckyState();
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState(); const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
const visible = useQuickAccessVisible(); const visible = useQuickAccessVisible();
const { t } = useTranslation();
const [pluginList, setPluginList] = useState<Plugin[]>( const [pluginList, setPluginList] = useState<Plugin[]>(
plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)), plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)),
@@ -48,6 +52,7 @@ const PluginView: VFC = () => {
<PanelSection> <PanelSection>
{pluginList {pluginList
.filter((p) => p.content) .filter((p) => p.content)
.filter(({ name }) => !hiddenPlugins.includes(name))
.map(({ name, icon }) => ( .map(({ name, icon }) => (
<PanelSectionRow key={name}> <PanelSectionRow key={name}>
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}> <ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
@@ -59,6 +64,12 @@ const PluginView: VFC = () => {
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
))} ))}
{hiddenPlugins.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}>
<FaEyeSlash />
<div>{t('PluginView.hidden', { count: hiddenPlugins.length })}</div>
</div>
)}
</PanelSection> </PanelSection>
</div> </div>
</> </>
@@ -0,0 +1,30 @@
import { ConfirmModal } from 'decky-frontend-lib';
import { FC } from 'react';
interface PluginUninstallModalProps {
name: string;
title: string;
buttonText: string;
description: string;
closeModal?(): void;
}
const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => {
return (
<ConfirmModal
closeModal={closeModal}
onOK={async () => {
await window.DeckyPluginLoader.callServerMethod('uninstall_plugin', { name });
// uninstalling a plugin resets the hidden setting for it server-side
// we invalidate here so if you re-install it, you won't have an out-of-date hidden filter
await window.DeckyPluginLoader.hiddenPluginsService.invalidate();
}}
strTitle={title}
strOKButtonText={buttonText}
>
{description}
</ConfirmModal>
);
};
export default PluginUninstallModal;
@@ -0,0 +1,34 @@
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa';
interface PluginListLabelProps {
hidden: boolean;
name: string;
version?: string;
}
const PluginListLabel: FC<PluginListLabelProps> = ({ name, hidden, version }) => {
const { t } = useTranslation();
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<div>{version ? `${name} - ${version}` : name}</div>
{hidden && (
<div
style={{
fontSize: '0.8rem',
color: '#dcdedf',
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<FaEyeSlash />
{t('PluginListLabel.hidden')}
</div>
)}
</div>
);
};
export default PluginListLabel;
@@ -22,10 +22,7 @@ import {
} from '../../../../store'; } from '../../../../store';
import { useSetting } from '../../../../utils/hooks/useSetting'; import { useSetting } from '../../../../utils/hooks/useSetting';
import { useDeckyState } from '../../../DeckyState'; import { useDeckyState } from '../../../DeckyState';
import PluginListLabel from './PluginListLabel';
function labelToName(pluginLabel: string, pluginVersion?: string): string {
return pluginVersion ? pluginLabel.substring(0, pluginLabel.indexOf(` - ${pluginVersion}`)) : pluginLabel;
}
async function reinstallPlugin(pluginName: string, currentVersion?: string) { async function reinstallPlugin(pluginName: string, currentVersion?: string) {
const serverData = await getPluginList(); const serverData = await getPluginList();
@@ -36,10 +33,17 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {
} }
} }
function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) { type PluginTableData = PluginData & { name: string; hidden: boolean; onHide(): void; onShow(): void };
const data = props.entry.data;
function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }) {
const { t } = useTranslation(); const { t } = useTranslation();
let pluginName = labelToName(props.entry.label, data?.version);
// nothing to display without this data...
if (!props.entry.data) {
return null;
}
const { name, update, version, onHide, onShow, hidden } = props.entry.data;
const showCtxMenu = (e: MouseEvent | GamepadEvent) => { const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
showContextMenu( showContextMenu(
@@ -47,7 +51,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
<MenuItem <MenuItem
onSelected={() => { onSelected={() => {
try { try {
fetch(`http://127.0.0.1:1337/plugins/${pluginName}/reload`, { fetch(`http://127.0.0.1:1337/plugins/${name}/reload`, {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { headers: {
@@ -58,7 +62,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
console.error('Error Reloading Plugin Backend', err); console.error('Error Reloading Plugin Backend', err);
} }
window.DeckyPluginLoader.importPlugin(pluginName, data?.version); window.DeckyPluginLoader.importPlugin(name, version);
}} }}
> >
{t('PluginListIndex.reload')} {t('PluginListIndex.reload')}
@@ -66,15 +70,20 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
<MenuItem <MenuItem
onSelected={() => onSelected={() =>
window.DeckyPluginLoader.uninstallPlugin( window.DeckyPluginLoader.uninstallPlugin(
pluginName, name,
t('PluginLoader.plugin_uninstall.title', { name: pluginName }), t('PluginLoader.plugin_uninstall.title', { name }),
t('PluginLoader.plugin_uninstall.button'), t('PluginLoader.plugin_uninstall.button'),
t('PluginLoader.plugin_uninstall.desc', { name: pluginName }), t('PluginLoader.plugin_uninstall.desc', { name }),
) )
} }
> >
{t('PluginListIndex.uninstall')} {t('PluginListIndex.uninstall')}
</MenuItem> </MenuItem>
{hidden ? (
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
) : (
<MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem>
)}
</Menu>, </Menu>,
e.currentTarget ?? window, e.currentTarget ?? window,
); );
@@ -82,22 +91,22 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
return ( return (
<> <>
{data?.update ? ( {update ? (
<DialogButton <DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)} onClick={() => requestPluginInstall(name, update, InstallType.UPDATE)}
onOKButton={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)} onOKButton={() => requestPluginInstall(name, update, InstallType.UPDATE)}
> >
<div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}>
{t('PluginListIndex.update_to', { name: data?.update?.name })} {t('PluginListIndex.update_to', { name: update.name })}
<FaDownload style={{ paddingLeft: '1rem' }} /> <FaDownload style={{ paddingLeft: '1rem' }} />
</div> </div>
</DialogButton> </DialogButton>
) : ( ) : (
<DialogButton <DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={() => reinstallPlugin(pluginName, data?.version)} onClick={() => reinstallPlugin(name, version)}
onOKButton={() => reinstallPlugin(pluginName, data?.version)} onOKButton={() => reinstallPlugin(name, version)}
> >
<div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}>
{t('PluginListIndex.reinstall')} {t('PluginListIndex.reinstall')}
@@ -130,7 +139,7 @@ type PluginData = {
}; };
export default function PluginList() { export default function PluginList() {
const { plugins, updates, pluginOrder, setPluginOrder } = useDeckyState(); const { plugins, updates, pluginOrder, setPluginOrder, hiddenPlugins } = useDeckyState();
const [_, setPluginOrderSetting] = useSetting<string[]>( const [_, setPluginOrderSetting] = useSetting<string[]>(
'pluginOrder', 'pluginOrder',
plugins.map((plugin) => plugin.name), plugins.map((plugin) => plugin.name),
@@ -141,22 +150,29 @@ export default function PluginList() {
window.DeckyPluginLoader.checkPluginUpdates(); window.DeckyPluginLoader.checkPluginUpdates();
}, []); }, []);
const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginData>[]>([]); const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]);
const hiddenPluginsService = window.DeckyPluginLoader.hiddenPluginsService;
useEffect(() => { useEffect(() => {
setPluginEntries( setPluginEntries(
plugins.map((plugin) => { plugins.map(({ name, version }) => {
const hidden = hiddenPlugins.includes(name);
return { return {
label: plugin.version ? `${plugin.name} - ${plugin.version}` : plugin.name, label: <PluginListLabel name={name} hidden={hidden} version={version} />,
position: pluginOrder.indexOf(name),
data: { data: {
update: updates?.get(plugin.name), name,
version: plugin.version, hidden,
version,
update: updates?.get(name),
onHide: () => hiddenPluginsService.update([...hiddenPlugins, name]),
onShow: () => hiddenPluginsService.update(hiddenPlugins.filter((pluginName) => name !== pluginName)),
}, },
position: pluginOrder.indexOf(plugin.name),
}; };
}), }),
); );
}, [plugins, updates]); }, [plugins, updates, hiddenPlugins]);
if (plugins.length === 0) { if (plugins.length === 0) {
return ( return (
@@ -166,8 +182,8 @@ export default function PluginList() {
); );
} }
function onSave(entries: ReorderableEntry<PluginData>[]) { function onSave(entries: ReorderableEntry<PluginTableData>[]) {
const newOrder = entries.map((entry) => labelToName(entry.label, entry?.data?.version)); const newOrder = entries.map((entry) => entry.data!.name);
console.log(newOrder); console.log(newOrder);
setPluginOrder(newOrder); setPluginOrder(newOrder);
setPluginOrderSetting(newOrder); setPluginOrderSetting(newOrder);
@@ -200,7 +216,7 @@ export default function PluginList() {
</DialogButton> </DialogButton>
)} )}
<DialogControlsSection style={{ marginTop: 0 }}> <DialogControlsSection style={{ marginTop: 0 }}>
<ReorderableList<PluginData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} /> <ReorderableList<PluginTableData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} />
</DialogControlsSection> </DialogControlsSection>
</DialogBody> </DialogBody>
); );
+34
View File
@@ -0,0 +1,34 @@
import { DeckyState } from './components/DeckyState';
import { getSetting, setSetting } from './utils/settings';
/**
* A Service class for managing the state and actions related to the hidden plugins feature
*
* It's mostly responsible for sending setting updates to the server and keeping the local state in sync.
*/
export class HiddenPluginsService {
constructor(private deckyState: DeckyState) {}
init() {
getSetting<string[]>('hiddenPlugins', []).then((hiddenPlugins) => {
this.deckyState.setHiddenPlugins(hiddenPlugins);
});
}
/**
* Sends the new hidden plugins list to the server and persists it locally in the decky state
*
* @param hiddenPlugins The new list of hidden plugins
*/
async update(hiddenPlugins: string[]) {
await setSetting('hiddenPlugins', hiddenPlugins);
this.deckyState.setHiddenPlugins(hiddenPlugins);
}
/**
* Refreshes the state of hidden plugins in the local state
*/
async invalidate() {
this.deckyState.setHiddenPlugins(await getSetting('hiddenPlugins', []));
}
}
+7 -16
View File
@@ -1,5 +1,4 @@
import { import {
ConfirmModal,
ModalRoot, ModalRoot,
PanelSection, PanelSection,
PanelSectionRow, PanelSectionRow,
@@ -18,9 +17,11 @@ import LegacyPlugin from './components/LegacyPlugin';
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches'; import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal'; import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
import PluginInstallModal from './components/modals/PluginInstallModal'; import PluginInstallModal from './components/modals/PluginInstallModal';
import PluginUninstallModal from './components/modals/PluginUninstallModal';
import NotificationBadge from './components/NotificationBadge'; import NotificationBadge from './components/NotificationBadge';
import PluginView from './components/PluginView'; import PluginView from './components/PluginView';
import WithSuspense from './components/WithSuspense'; import WithSuspense from './components/WithSuspense';
import { HiddenPluginsService } from './hidden-plugins-service';
import Logger from './logger'; import Logger from './logger';
import { InstallType, Plugin } from './plugin'; import { InstallType, Plugin } from './plugin';
import RouterHook from './router-hook'; import RouterHook from './router-hook';
@@ -45,6 +46,7 @@ class PluginLoader extends Logger {
private routerHook: RouterHook = new RouterHook(); private routerHook: RouterHook = new RouterHook();
public toaster: Toaster = new Toaster(); public toaster: Toaster = new Toaster();
private deckyState: DeckyState = new DeckyState(); private deckyState: DeckyState = new DeckyState();
public hiddenPluginsService = new HiddenPluginsService(this.deckyState);
private reloadLock: boolean = false; private reloadLock: boolean = false;
// stores a list of plugin names which requested to be reloaded // stores a list of plugin names which requested to be reloaded
@@ -182,21 +184,8 @@ class PluginLoader extends Logger {
); );
} }
public uninstallPlugin(name: string, title: string, button_text: string, description: string) { public uninstallPlugin(name: string, title: string, buttonText: string, description: string) {
showModal( showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
<ConfirmModal
onOK={async () => {
await this.callServerMethod('uninstall_plugin', { name });
}}
onCancel={() => {
// do nothing
}}
strTitle={title}
strOKButtonText={button_text}
>
{description}
</ConfirmModal>,
);
} }
public hasPlugin(name: string) { public hasPlugin(name: string) {
@@ -220,6 +209,8 @@ class PluginLoader extends Logger {
console.log(pluginOrder); console.log(pluginOrder);
this.deckyState.setPluginOrder(pluginOrder); this.deckyState.setPluginOrder(pluginOrder);
}); });
this.hiddenPluginsService.init();
} }
public deinit() { public deinit() {