diff options
| -rw-r--r-- | backend/decky_loader/browser.py | 6 | ||||
| -rw-r--r-- | backend/locales/en-US.json | 2 | ||||
| -rw-r--r-- | frontend/src/components/DeckyState.tsx | 8 | ||||
| -rw-r--r-- | frontend/src/components/modals/PluginUninstallModal.tsx | 1 | ||||
| -rw-r--r-- | frontend/src/components/settings/index.tsx | 2 | ||||
| -rw-r--r-- | frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx | 22 | ||||
| -rw-r--r-- | frontend/src/components/settings/pages/plugin_list/index.tsx | 30 | ||||
| -rw-r--r-- | frontend/src/frozen-plugins-service.tsx | 49 | ||||
| -rw-r--r-- | frontend/src/plugin-loader.tsx | 7 |
9 files changed, 116 insertions, 11 deletions
diff --git a/backend/decky_loader/browser.py b/backend/decky_loader/browser.py index 35b36911..def81011 100644 --- a/backend/decky_loader/browser.py +++ b/backend/decky_loader/browser.py @@ -289,12 +289,16 @@ class PluginBrowser: Args: name (string): The name of the plugin """ + frozen_plugins = self.settings.getSetting("frozenPlugins", []) + if name in frozen_plugins: + frozen_plugins.remove(name) + self.settings.setSetting("frozenPlugins", frozen_plugins) + hidden_plugins = self.settings.getSetting("hiddenPlugins", []) if name in hidden_plugins: hidden_plugins.remove(name) self.settings.setSetting("hiddenPlugins", hidden_plugins) - plugin_order = self.settings.getSetting("pluginOrder", []) if name in plugin_order: diff --git a/backend/locales/en-US.json b/backend/locales/en-US.json index ea0542ca..ca18f7da 100644 --- a/backend/locales/en-US.json +++ b/backend/locales/en-US.json @@ -99,12 +99,14 @@ } }, "PluginListIndex": { + "freeze": "Freeze updates", "hide": "Quick access: Hide", "no_plugin": "No plugins installed!", "plugin_actions": "Plugin Actions", "reinstall": "Reinstall", "reload": "Reload", "show": "Quick access: Show", + "unfreeze": "Allow updates", "uninstall": "Uninstall", "update_all_one": "Update 1 plugin", "update_all_other": "Update {{count}} plugins", diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx index d20c8d86..749e27ce 100644 --- a/frontend/src/components/DeckyState.tsx +++ b/frontend/src/components/DeckyState.tsx @@ -8,6 +8,7 @@ import { VerInfo } from '../updater'; interface PublicDeckyState { plugins: Plugin[]; pluginOrder: string[]; + frozenPlugins: string[]; hiddenPlugins: string[]; activePlugin: Plugin | null; updates: PluginUpdateMapping | null; @@ -26,6 +27,7 @@ export interface UserInfo { export class DeckyState { private _plugins: Plugin[] = []; private _pluginOrder: string[] = []; + private _frozenPlugins: string[] = []; private _hiddenPlugins: string[] = []; private _activePlugin: Plugin | null = null; private _updates: PluginUpdateMapping | null = null; @@ -41,6 +43,7 @@ export class DeckyState { return { plugins: this._plugins, pluginOrder: this._pluginOrder, + frozenPlugins: this._frozenPlugins, hiddenPlugins: this._hiddenPlugins, activePlugin: this._activePlugin, updates: this._updates, @@ -67,6 +70,11 @@ export class DeckyState { this.notifyUpdate(); } + setFrozenPlugins(frozenPlugins: string[]) { + this._frozenPlugins = frozenPlugins; + this.notifyUpdate(); + } + setHiddenPlugins(hiddenPlugins: string[]) { this._hiddenPlugins = hiddenPlugins; this.notifyUpdate(); diff --git a/frontend/src/components/modals/PluginUninstallModal.tsx b/frontend/src/components/modals/PluginUninstallModal.tsx index 3fc0a71f..087b634c 100644 --- a/frontend/src/components/modals/PluginUninstallModal.tsx +++ b/frontend/src/components/modals/PluginUninstallModal.tsx @@ -19,6 +19,7 @@ const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, butt await uninstallPlugin(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 DeckyPluginLoader.frozenPluginsService.invalidate(); await DeckyPluginLoader.hiddenPluginsService.invalidate(); }} strTitle={title} diff --git a/frontend/src/components/settings/index.tsx b/frontend/src/components/settings/index.tsx index 568a0a49..80400058 100644 --- a/frontend/src/components/settings/index.tsx +++ b/frontend/src/components/settings/index.tsx @@ -25,7 +25,7 @@ export default function SettingsPage() { }, { title: t('SettingsIndex.plugins_title'), - content: <PluginList />, + content: <PluginList isDeveloper={isDeveloper} />, route: '/decky/settings/plugins', icon: <FaPlug />, }, diff --git a/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx b/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx index a49f808f..fec03e56 100644 --- a/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx +++ b/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx @@ -1,18 +1,34 @@ import { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { FaEyeSlash } from 'react-icons/fa'; +import { FaEyeSlash, FaLock } from 'react-icons/fa'; interface PluginListLabelProps { + frozen: boolean; hidden: boolean; name: string; version?: string; } -const PluginListLabel: FC<PluginListLabelProps> = ({ name, hidden, version }) => { +const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version }) => { const { t } = useTranslation(); return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}> - <div>{version ? `${name} - ${version}` : name}</div> + <div> + {name} + {version && ( + <> + {' - '} + <span style={{ color: frozen ? '#67707b' : 'inherit' }}> + {frozen && ( + <> + <FaLock />{' '} + </> + )} + {version} + </span> + </> + )} + </div> {hidden && ( <div style={{ diff --git a/frontend/src/components/settings/pages/plugin_list/index.tsx b/frontend/src/components/settings/pages/plugin_list/index.tsx index 0728b12d..d8a268ae 100644 --- a/frontend/src/components/settings/pages/plugin_list/index.tsx +++ b/frontend/src/components/settings/pages/plugin_list/index.tsx @@ -33,7 +33,16 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) { } } -type PluginTableData = PluginData & { name: string; hidden: boolean; onHide(): void; onShow(): void }; +type PluginTableData = PluginData & { + name: string; + frozen: boolean; + onFreeze(): void; + onUnfreeze(): void; + hidden: boolean; + onHide(): void; + onShow(): void; + isDeveloper: boolean; +}; const reloadPluginBackend = DeckyBackend.callable<[pluginName: string], void>('loader/reload_plugin'); @@ -45,7 +54,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> } return null; } - const { name, update, version, onHide, onShow, hidden } = props.entry.data; + const { name, update, version, onHide, onShow, hidden, onFreeze, onUnfreeze, frozen, isDeveloper } = props.entry.data; const showCtxMenu = (e: MouseEvent | GamepadEvent) => { showContextMenu( @@ -80,6 +89,11 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> } ) : ( <MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem> )} + {frozen ? ( + <MenuItem onSelected={onUnfreeze}>{t('PluginListIndex.unfreeze')}</MenuItem> + ) : ( + isDeveloper && <MenuItem onSelected={onFreeze}>{t('PluginListIndex.freeze')}</MenuItem> + )} </Menu>, e.currentTarget ?? window, ); @@ -134,8 +148,8 @@ type PluginData = { version?: string; }; -export default function PluginList() { - const { plugins, updates, pluginOrder, setPluginOrder, hiddenPlugins } = useDeckyState(); +export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) { + const { plugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } = useDeckyState(); const [_, setPluginOrderSetting] = useSetting<string[]>( 'pluginOrder', plugins.map((plugin) => plugin.name), @@ -148,20 +162,26 @@ export default function PluginList() { const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]); const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService; + const frozenPluginsService = DeckyPluginLoader.frozenPluginsService; useEffect(() => { setPluginEntries( plugins.map(({ name, version }) => { + const frozen = frozenPlugins.includes(name); const hidden = hiddenPlugins.includes(name); return { - label: <PluginListLabel name={name} hidden={hidden} version={version} />, + label: <PluginListLabel name={name} frozen={frozen} hidden={hidden} version={version} />, position: pluginOrder.indexOf(name), data: { name, + frozen, hidden, + isDeveloper, version, update: updates?.get(name), + onFreeze: () => frozenPluginsService.update([...frozenPlugins, name]), + onUnfreeze: () => frozenPluginsService.update(frozenPlugins.filter((pluginName) => name !== pluginName)), onHide: () => hiddenPluginsService.update([...hiddenPlugins, name]), onShow: () => hiddenPluginsService.update(hiddenPlugins.filter((pluginName) => name !== pluginName)), }, diff --git a/frontend/src/frozen-plugins-service.tsx b/frontend/src/frozen-plugins-service.tsx new file mode 100644 index 00000000..fa43a99e --- /dev/null +++ b/frontend/src/frozen-plugins-service.tsx @@ -0,0 +1,49 @@ +import { DeckyState } from './components/DeckyState'; +import { PluginUpdateMapping } from './store'; +import { getSetting, setSetting } from './utils/settings'; + +/** + * A Service class for managing the state and actions related to the frozen plugins feature. + * + * It's mostly responsible for sending setting updates to the server and keeping the local state in sync. + */ +export class FrozenPluginService { + constructor(private deckyState: DeckyState) {} + + init() { + getSetting<string[]>('frozenPlugins', []).then((frozenPlugins) => { + this.deckyState.setFrozenPlugins(frozenPlugins); + }); + } + + /** + * Sends the new frozen plugins list to the server and persists it locally in the decky state + * + * @param frozenPlugins The new list of frozen plugins + */ + async update(frozenPlugins: string[]) { + await setSetting('frozenPlugins', frozenPlugins); + this.deckyState.setFrozenPlugins(frozenPlugins); + + // Remove pending updates for frozen plugins + const updates = this.deckyState.publicState().updates; + + if (updates) { + const filteredUpdates = new Map() as PluginUpdateMapping; + updates.forEach((v, k) => { + if (!frozenPlugins.includes(k)) { + filteredUpdates.set(k, v); + } + }); + + this.deckyState.setUpdates(filteredUpdates); + } + } + + /** + * Refreshes the state of frozen plugins in the local state + */ + async invalidate() { + this.deckyState.setFrozenPlugins(await getSetting('frozenPlugins', [])); + } +} diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index 7951e944..43073385 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -22,6 +22,7 @@ import PluginUninstallModal from './components/modals/PluginUninstallModal'; import NotificationBadge from './components/NotificationBadge'; import PluginView from './components/PluginView'; import WithSuspense from './components/WithSuspense'; +import { FrozenPluginService } from './frozen-plugins-service'; import { HiddenPluginsService } from './hidden-plugins-service'; import Logger from './logger'; import { NotificationService } from './notification-service'; @@ -47,6 +48,7 @@ class PluginLoader extends Logger { public toaster: Toaster = new Toaster(); private deckyState: DeckyState = new DeckyState(); + public frozenPluginsService = new FrozenPluginService(this.deckyState); public hiddenPluginsService = new HiddenPluginsService(this.deckyState); public notificationService = new NotificationService(this.deckyState); @@ -162,7 +164,9 @@ class PluginLoader extends Logger { } public async checkPluginUpdates() { - const updates = await checkForPluginUpdates(this.plugins); + const frozenPlugins = this.deckyState.publicState().frozenPlugins; + + const updates = await checkForPluginUpdates(this.plugins.filter((p) => !frozenPlugins.includes(p.name))); this.deckyState.setUpdates(updates); return updates; } @@ -242,6 +246,7 @@ class PluginLoader extends Logger { this.deckyState.setPluginOrder(pluginOrder); }); + this.frozenPluginsService.init(); this.hiddenPluginsService.init(); this.notificationService.init(); } |
