summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWeblate <noreply@weblate.org>2023-06-08 14:40:08 +0000
committerWeblate <noreply@weblate.org>2023-06-08 14:40:08 +0000
commit89bbbf6fe4348236f9828a39ddc8f1790cb2f2f4 (patch)
treec175788ffc6917a66db810eb14073c57b750efb3
parentfdc556edeed416843f3e4b9d5c1a9e163dc72d89 (diff)
parent9a05c228a004392df6921a7706f4ae6a62fff2d3 (diff)
downloaddecky-loader-89bbbf6fe4348236f9828a39ddc8f1790cb2f2f4.tar.gz
decky-loader-89bbbf6fe4348236f9828a39ddc8f1790cb2f2f4.zip
Merge remote-tracking branch 'origin/main'
-rw-r--r--README.md2
-rw-r--r--backend/browser.py20
-rw-r--r--backend/injector.py3
-rw-r--r--backend/locales/en-US.json9
-rw-r--r--backend/settings.py2
-rw-r--r--frontend/.gitignore2
-rw-r--r--frontend/package.json1
-rw-r--r--frontend/pnpm-lock.yaml94
-rw-r--r--frontend/rollup.config.js6
-rw-r--r--frontend/src/components/DeckyState.tsx18
-rw-r--r--frontend/src/components/PluginView.tsx11
-rw-r--r--frontend/src/components/modals/PluginUninstallModal.tsx30
-rw-r--r--frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx34
-rw-r--r--frontend/src/components/settings/pages/plugin_list/index.tsx74
-rw-r--r--frontend/src/hidden-plugins-service.tsx34
-rw-r--r--frontend/src/plugin-loader.tsx23
16 files changed, 304 insertions, 59 deletions
diff --git a/README.md b/README.md
index 44363da0..3df8a0e4 100644
--- a/README.md
+++ b/README.md
@@ -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. Select "Switch to Desktop".
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. 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.
diff --git a/backend/browser.py b/backend/browser.py
index 388a01e3..ab71a89d 100644
--- a/backend/browser.py
+++ b/backend/browser.py
@@ -122,10 +122,7 @@ class PluginBrowser:
logger.debug("Plugin %s was stopped", name)
del self.plugins[name]
logger.debug("Plugin %s was removed from the dictionary", name)
- current_plugin_order = self.settings.getSetting("pluginOrder")
- current_plugin_order.remove(name)
- self.settings.setSetting("pluginOrder", current_plugin_order)
- logger.debug("Plugin %s was removed from the pluginOrder setting", name)
+ self.cleanup_plugin_settings(name)
logger.debug("removing files %s" % str(name))
rmtree(plugin_dir)
except FileNotFoundError:
@@ -234,3 +231,18 @@ class PluginBrowser:
def cancel_plugin_install(self, 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)
diff --git a/backend/injector.py b/backend/injector.py
index b28a0349..e3414fee 100644
--- a/backend/injector.py
+++ b/backend/injector.py
@@ -395,6 +395,7 @@ async def get_tab_lambda(test) -> Tab:
return tab
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
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():
tabs = await get_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"))
await t.close()
await sleep(0.5)
diff --git a/backend/locales/en-US.json b/backend/locales/en-US.json
index a347351b..b5c32957 100644
--- a/backend/locales/en-US.json
+++ b/backend/locales/en-US.json
@@ -17,6 +17,13 @@
"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": {
"plugin_full_access": "This plugin has full access to your Steam Deck.",
"plugin_install": "Install",
@@ -73,6 +80,8 @@
"reload": "Reload",
"uninstall": "Uninstall",
"update_to": "Update to {{name}}",
+ "show": "Quick access: Show",
+ "hide": "Quick access: Hide",
"update_all_one": "Update 1 plugin",
"update_all_other": "Update {{count}} plugins"
},
diff --git a/backend/settings.py b/backend/settings.py
index d54ff2b5..c00e6a82 100644
--- a/backend/settings.py
+++ b/backend/settings.py
@@ -22,7 +22,7 @@ class SettingsManager:
for file in listdir(wrong_dir):
if file.endswith(".json"):
rename(path.join(wrong_dir,file),
- path.join(settings_directory, file))
+ path.join(settings_directory, file))
self.path = path.join(settings_directory, name + ".json")
diff --git a/frontend/.gitignore b/frontend/.gitignore
index 5861baa5..58855fb2 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -2,3 +2,5 @@ node_modules/
.yalc
yalc.lock
+
+stats.html
diff --git a/frontend/package.json b/frontend/package.json
index 35d98c65..de0fd5a2 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -33,6 +33,7 @@
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-external-globals": "^0.6.1",
"rollup-plugin-polyfill-node": "^0.10.2",
+ "rollup-plugin-visualizer": "^5.9.0",
"tslib": "^2.5.2",
"typescript": "^4.9.5"
},
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 0f91050b..ad7bb42c 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -93,6 +93,9 @@ devDependencies:
rollup-plugin-polyfill-node:
specifier: ^0.10.2
version: 0.10.2(rollup@2.79.1)
+ rollup-plugin-visualizer:
+ specifier: ^5.9.0
+ version: 5.9.0(rollup@2.79.1)
tslib:
specifier: ^2.5.2
version: 2.5.2
@@ -1235,6 +1238,15 @@ packages:
engines: {node: '>= 10'}
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:
resolution: {integrity: sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==}
engines: {node: '>= 0.10'}
@@ -1414,6 +1426,11 @@ packages:
clone: 1.0.4
dev: true
+ /define-lazy-prop@2.0.0:
+ resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
+ engines: {node: '>=8'}
+ dev: true
+
/define-properties@1.2.0:
resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==}
engines: {node: '>= 0.4'}
@@ -1770,6 +1787,11 @@ packages:
engines: {node: '>=6.9.0'}
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:
resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
dependencies:
@@ -2126,6 +2148,12 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /is-docker@2.2.1:
+ resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
+ engines: {node: '>=8'}
+ hasBin: true
+ dev: true
+
/is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@@ -2222,6 +2250,13 @@ packages:
engines: {node: '>=0.10.0'}
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:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
dev: true
@@ -2891,6 +2926,15 @@ packages:
mimic-fn: 2.1.0
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:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
@@ -3237,6 +3281,11 @@ packages:
engines: {node: '>= 10'}
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:
resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==}
engines: {node: '>=4'}
@@ -3318,6 +3367,23 @@ packages:
rollup: 2.79.1
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:
resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==}
engines: {node: '>=10.0.0'}
@@ -3425,6 +3491,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /source-map@0.7.4:
+ resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
+ engines: {node: '>= 8'}
+ dev: true
+
/sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
deprecated: Please use @jridgewell/sourcemap-codec instead
@@ -3958,10 +4029,33 @@ packages:
engines: {node: '>=0.4'}
dev: true
+ /y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+ dev: true
+
/yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
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:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: false
diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js
index 2573b8a0..ac9ff58c 100644
--- a/frontend/rollup.config.js
+++ b/frontend/rollup.config.js
@@ -7,6 +7,7 @@ import typescript from '@rollup/plugin-typescript';
import { defineConfig } from 'rollup';
import del from 'rollup-plugin-delete';
import externalGlobals from 'rollup-plugin-external-globals';
+import { visualizer } from 'rollup-plugin-visualizer';
const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL'];
@@ -16,7 +17,7 @@ export default defineConfig({
del({ targets: '../backend/static/*', force: true }),
commonjs(),
nodeResolve({
- browser: true
+ browser: true,
}),
externalGlobals({
react: 'SP_REACT',
@@ -33,6 +34,7 @@ export default defineConfig({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
image(),
+ visualizer(),
],
preserveEntrySignatures: false,
output: {
@@ -46,4 +48,4 @@ export default defineConfig({
if (hiddenWarnings.some((warning) => message.code === warning)) return;
handleWarning(message);
},
-}); \ No newline at end of file
+});
diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx
index 67d4b78e..53ef6d2d 100644
--- a/frontend/src/components/DeckyState.tsx
+++ b/frontend/src/components/DeckyState.tsx
@@ -7,6 +7,7 @@ import { VerInfo } from '../updater';
interface PublicDeckyState {
plugins: Plugin[];
pluginOrder: string[];
+ hiddenPlugins: string[];
activePlugin: Plugin | null;
updates: PluginUpdateMapping | null;
hasLoaderUpdate?: boolean;
@@ -17,6 +18,7 @@ interface PublicDeckyState {
export class DeckyState {
private _plugins: Plugin[] = [];
private _pluginOrder: string[] = [];
+ private _hiddenPlugins: string[] = [];
private _activePlugin: Plugin | null = null;
private _updates: PluginUpdateMapping | null = null;
private _hasLoaderUpdate: boolean = false;
@@ -29,6 +31,7 @@ export class DeckyState {
return {
plugins: this._plugins,
pluginOrder: this._pluginOrder,
+ hiddenPlugins: this._hiddenPlugins,
activePlugin: this._activePlugin,
updates: this._updates,
hasLoaderUpdate: this._hasLoaderUpdate,
@@ -52,6 +55,11 @@ export class DeckyState {
this.notifyUpdate();
}
+ setHiddenPlugins(hiddenPlugins: string[]) {
+ this._hiddenPlugins = hiddenPlugins;
+ this.notifyUpdate();
+ }
+
setActivePlugin(name: string) {
this._activePlugin = this._plugins.find((plugin) => plugin.name === name) ?? null;
this.notifyUpdate();
@@ -111,11 +119,11 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
return () => deckyState.eventBus.removeEventListener('update', onUpdate);
}, []);
- const setIsLoaderUpdating = (hasUpdate: boolean) => deckyState.setIsLoaderUpdating(hasUpdate);
- const setVersionInfo = (versionInfo: VerInfo) => deckyState.setVersionInfo(versionInfo);
- const setActivePlugin = (name: string) => deckyState.setActivePlugin(name);
- const closeActivePlugin = () => deckyState.closeActivePlugin();
- const setPluginOrder = (pluginOrder: string[]) => deckyState.setPluginOrder(pluginOrder);
+ const setIsLoaderUpdating = deckyState.setIsLoaderUpdating.bind(deckyState);
+ const setVersionInfo = deckyState.setVersionInfo.bind(deckyState);
+ const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
+ const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
+ const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
return (
<DeckyStateContext.Provider
diff --git a/frontend/src/components/PluginView.tsx b/frontend/src/components/PluginView.tsx
index 3ecc2c86..b53035f7 100644
--- a/frontend/src/components/PluginView.tsx
+++ b/frontend/src/components/PluginView.tsx
@@ -8,6 +8,8 @@ import {
staticClasses,
} from 'decky-frontend-lib';
import { VFC, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaEyeSlash } from 'react-icons/fa';
import { Plugin } from '../plugin';
import { useDeckyState } from './DeckyState';
@@ -16,8 +18,10 @@ import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView';
const PluginView: VFC = () => {
+ const { hiddenPlugins } = useDeckyState();
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
const visible = useQuickAccessVisible();
+ const { t } = useTranslation();
const [pluginList, setPluginList] = useState<Plugin[]>(
plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)),
@@ -48,6 +52,7 @@ const PluginView: VFC = () => {
<PanelSection>
{pluginList
.filter((p) => p.content)
+ .filter(({ name }) => !hiddenPlugins.includes(name))
.map(({ name, icon }) => (
<PanelSectionRow key={name}>
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
@@ -59,6 +64,12 @@ const PluginView: VFC = () => {
</ButtonItem>
</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>
</div>
</>
diff --git a/frontend/src/components/modals/PluginUninstallModal.tsx b/frontend/src/components/modals/PluginUninstallModal.tsx
new file mode 100644
index 00000000..e7ecbc99
--- /dev/null
+++ b/frontend/src/components/modals/PluginUninstallModal.tsx
@@ -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;
diff --git a/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx b/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx
new file mode 100644
index 00000000..a49f808f
--- /dev/null
+++ b/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx
@@ -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;
diff --git a/frontend/src/components/settings/pages/plugin_list/index.tsx b/frontend/src/components/settings/pages/plugin_list/index.tsx
index fab8ec2b..09d06d48 100644
--- a/frontend/src/components/settings/pages/plugin_list/index.tsx
+++ b/frontend/src/components/settings/pages/plugin_list/index.tsx
@@ -22,10 +22,7 @@ import {
} from '../../../../store';
import { useSetting } from '../../../../utils/hooks/useSetting';
import { useDeckyState } from '../../../DeckyState';
-
-function labelToName(pluginLabel: string, pluginVersion?: string): string {
- return pluginVersion ? pluginLabel.substring(0, pluginLabel.indexOf(` - ${pluginVersion}`)) : pluginLabel;
-}
+import PluginListLabel from './PluginListLabel';
async function reinstallPlugin(pluginName: string, currentVersion?: string) {
const serverData = await getPluginList();
@@ -36,10 +33,17 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {
}
}
-function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
- const data = props.entry.data;
+type PluginTableData = PluginData & { name: string; hidden: boolean; onHide(): void; onShow(): void };
+
+function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }) {
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) => {
showContextMenu(
@@ -47,7 +51,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
<MenuItem
onSelected={() => {
try {
- fetch(`http://127.0.0.1:1337/plugins/${pluginName}/reload`, {
+ fetch(`http://127.0.0.1:1337/plugins/${name}/reload`, {
method: 'POST',
credentials: 'include',
headers: {
@@ -58,7 +62,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
console.error('Error Reloading Plugin Backend', err);
}
- window.DeckyPluginLoader.importPlugin(pluginName, data?.version);
+ window.DeckyPluginLoader.importPlugin(name, version);
}}
>
{t('PluginListIndex.reload')}
@@ -66,15 +70,20 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
<MenuItem
onSelected={() =>
window.DeckyPluginLoader.uninstallPlugin(
- pluginName,
- t('PluginLoader.plugin_uninstall.title', { name: pluginName }),
+ name,
+ t('PluginLoader.plugin_uninstall.title', { name }),
t('PluginLoader.plugin_uninstall.button'),
- t('PluginLoader.plugin_uninstall.desc', { name: pluginName }),
+ t('PluginLoader.plugin_uninstall.desc', { name }),
)
}
>
{t('PluginListIndex.uninstall')}
</MenuItem>
+ {hidden ? (
+ <MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
+ ) : (
+ <MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem>
+ )}
</Menu>,
e.currentTarget ?? window,
);
@@ -82,22 +91,22 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
return (
<>
- {data?.update ? (
+ {update ? (
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
- onClick={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)}
- onOKButton={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)}
+ onClick={() => requestPluginInstall(name, update, InstallType.UPDATE)}
+ onOKButton={() => requestPluginInstall(name, update, InstallType.UPDATE)}
>
<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' }} />
</div>
</DialogButton>
) : (
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
- onClick={() => reinstallPlugin(pluginName, data?.version)}
- onOKButton={() => reinstallPlugin(pluginName, data?.version)}
+ onClick={() => reinstallPlugin(name, version)}
+ onOKButton={() => reinstallPlugin(name, version)}
>
<div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}>
{t('PluginListIndex.reinstall')}
@@ -130,7 +139,7 @@ type PluginData = {
};
export default function PluginList() {
- const { plugins, updates, pluginOrder, setPluginOrder } = useDeckyState();
+ const { plugins, updates, pluginOrder, setPluginOrder, hiddenPlugins } = useDeckyState();
const [_, setPluginOrderSetting] = useSetting<string[]>(
'pluginOrder',
plugins.map((plugin) => plugin.name),
@@ -141,22 +150,29 @@ export default function PluginList() {
window.DeckyPluginLoader.checkPluginUpdates();
}, []);
- const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginData>[]>([]);
+ const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]);
+ const hiddenPluginsService = window.DeckyPluginLoader.hiddenPluginsService;
useEffect(() => {
setPluginEntries(
- plugins.map((plugin) => {
+ plugins.map(({ name, version }) => {
+ const hidden = hiddenPlugins.includes(name);
+
return {
- label: plugin.version ? `${plugin.name} - ${plugin.version}` : plugin.name,
+ label: <PluginListLabel name={name} hidden={hidden} version={version} />,
+ position: pluginOrder.indexOf(name),
data: {
- update: updates?.get(plugin.name),
- version: plugin.version,
+ name,
+ 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) {
return (
@@ -166,8 +182,8 @@ export default function PluginList() {
);
}
- function onSave(entries: ReorderableEntry<PluginData>[]) {
- const newOrder = entries.map((entry) => labelToName(entry.label, entry?.data?.version));
+ function onSave(entries: ReorderableEntry<PluginTableData>[]) {
+ const newOrder = entries.map((entry) => entry.data!.name);
console.log(newOrder);
setPluginOrder(newOrder);
setPluginOrderSetting(newOrder);
@@ -200,7 +216,7 @@ export default function PluginList() {
</DialogButton>
)}
<DialogControlsSection style={{ marginTop: 0 }}>
- <ReorderableList<PluginData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} />
+ <ReorderableList<PluginTableData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} />
</DialogControlsSection>
</DialogBody>
);
diff --git a/frontend/src/hidden-plugins-service.tsx b/frontend/src/hidden-plugins-service.tsx
new file mode 100644
index 00000000..0501c453
--- /dev/null
+++ b/frontend/src/hidden-plugins-service.tsx
@@ -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', []));
+ }
+}
diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx
index 57483293..6d20c2f0 100644
--- a/frontend/src/plugin-loader.tsx
+++ b/frontend/src/plugin-loader.tsx
@@ -1,5 +1,4 @@
import {
- ConfirmModal,
ModalRoot,
PanelSection,
PanelSectionRow,
@@ -18,9 +17,11 @@ import LegacyPlugin from './components/LegacyPlugin';
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
import PluginInstallModal from './components/modals/PluginInstallModal';
+import PluginUninstallModal from './components/modals/PluginUninstallModal';
import NotificationBadge from './components/NotificationBadge';
import PluginView from './components/PluginView';
import WithSuspense from './components/WithSuspense';
+import { HiddenPluginsService } from './hidden-plugins-service';
import Logger from './logger';
import { InstallType, Plugin } from './plugin';
import RouterHook from './router-hook';
@@ -45,6 +46,7 @@ class PluginLoader extends Logger {
private routerHook: RouterHook = new RouterHook();
public toaster: Toaster = new Toaster();
private deckyState: DeckyState = new DeckyState();
+ public hiddenPluginsService = new HiddenPluginsService(this.deckyState);
private reloadLock: boolean = false;
// 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) {
- showModal(
- <ConfirmModal
- onOK={async () => {
- await this.callServerMethod('uninstall_plugin', { name });
- }}
- onCancel={() => {
- // do nothing
- }}
- strTitle={title}
- strOKButtonText={button_text}
- >
- {description}
- </ConfirmModal>,
- );
+ public uninstallPlugin(name: string, title: string, buttonText: string, description: string) {
+ showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
}
public hasPlugin(name: string) {
@@ -220,6 +209,8 @@ class PluginLoader extends Logger {
console.log(pluginOrder);
this.deckyState.setPluginOrder(pluginOrder);
});
+
+ this.hiddenPluginsService.init();
}
public deinit() {