summaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authorJonas Dellinger <jonas@dellinger.dev>2023-06-07 07:35:05 +0200
committerGitHub <noreply@github.com>2023-06-06 22:35:05 -0700
commit47bc910a8482d3d2cc882e28e862ca5ad61063b6 (patch)
treeacc75a2892150df5fd0d151d46d8f75caa062504 /frontend
parent1c6270ccd67f271968a3ab8a30c29f19548765f0 (diff)
downloaddecky-loader-47bc910a8482d3d2cc882e28e862ca5ad61063b6.tar.gz
decky-loader-47bc910a8482d3d2cc882e28e862ca5ad61063b6.zip
Add functionality to hide plugins from quick access menu (#468)
Diffstat (limited to 'frontend')
-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
11 files changed, 275 insertions, 52 deletions
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() {