Skip to content

Commit

Permalink
[Feature] Freeze updates for devs
Browse files Browse the repository at this point in the history
  • Loading branch information
FineWolf committed Jan 28, 2024
1 parent 647f3fe commit f2fe142
Show file tree
Hide file tree
Showing 10 changed files with 107 additions and 12 deletions.
2 changes: 2 additions & 0 deletions backend/locales/en-US.json
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions backend/locales/fr-FR.json
Expand Up @@ -92,12 +92,14 @@
}
},
"PluginListIndex": {
"freeze": "Geler les mises à jour",
"plugin_actions": "Plugin Actions",
"reinstall": "Réinstaller",
"reload": "Recharger",
"uninstall": "Désinstaller",
"update_to": "Mettre à jour vers {{name}}",
"no_plugin": "Aucun plugin installé !",
"unfreeze": "Permettre les mises à jour",
"update_all_one": "",
"update_all_many": "",
"update_all_other": ""
Expand Down
6 changes: 5 additions & 1 deletion backend/src/browser.py
Expand Up @@ -272,12 +272,16 @@ def cleanup_plugin_settings(self, name: str):
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:
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/components/DeckyState.tsx
Expand Up @@ -8,6 +8,7 @@ import { VerInfo } from '../updater';
interface PublicDeckyState {
plugins: Plugin[];
pluginOrder: string[];
frozenPlugins: string[];
hiddenPlugins: string[];
activePlugin: Plugin | null;
updates: PluginUpdateMapping | null;
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/modals/PluginUninstallModal.tsx
Expand Up @@ -15,8 +15,9 @@ const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, butt
closeModal={closeModal}
onOK={async () => {
await window.DeckyPluginLoader.callServerMethod('uninstall_plugin', { name });
// uninstalling a plugin resets the hidden setting for it server-side
// uninstalling a plugin resets the frozen and 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.frozenPluginsService.invalidate();
await window.DeckyPluginLoader.hiddenPluginsService.invalidate();
}}
strTitle={title}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/settings/index.tsx
Expand Up @@ -24,7 +24,7 @@ export default function SettingsPage() {
},
{
title: t('SettingsIndex.plugins_title'),
content: <PluginList />,
content: <PluginList isDeveloper={isDeveloper} />,
route: '/decky/settings/plugins',
icon: <FaPlug />,
},
Expand Down
@@ -1,18 +1,37 @@
import { FC } from 'react';
import { IconsModule, gamepadDialogClasses } from 'decky-frontend-lib';
import { FC, SVGAttributes } from 'react';
import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa';

interface PluginListLabelProps {
frozen: boolean;
hidden: boolean;
name: string;
version?: string;
}

const PluginListLabel: FC<PluginListLabelProps> = ({ name, hidden, version }) => {
const LockIcon = IconsModule['Lock'] as FC<SVGAttributes<SVGElement>>;

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 className={frozen ? gamepadDialogClasses.Disabled : ''}>
{frozen && (
<>
<LockIcon width="1em" />{' '}
</>
)}
{version}
</span>
</>
)}
</div>
{hidden && (
<div
style={{
Expand Down
30 changes: 25 additions & 5 deletions frontend/src/components/settings/pages/plugin_list/index.tsx
Expand Up @@ -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;
};

function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }) {
const { t } = useTranslation();
Expand All @@ -43,7 +52,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(
Expand Down Expand Up @@ -84,6 +93,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,
);
Expand Down Expand Up @@ -138,8 +152,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),
Expand All @@ -151,21 +165,27 @@ export default function PluginList() {
}, []);

const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]);
const frozenPluginsService = window.DeckyPluginLoader.frozenPluginsService;
const hiddenPluginsService = window.DeckyPluginLoader.hiddenPluginsService;

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)),
},
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/frozen-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 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);
}

/**
* Refreshes the state of frozen plugins in the local state
*/
async invalidate() {
this.deckyState.setFrozenPlugins(await getSetting('frozenPlugins', []));
}
}
7 changes: 6 additions & 1 deletion frontend/src/plugin-loader.tsx
Expand Up @@ -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';
Expand Down Expand Up @@ -49,6 +50,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);

Expand Down Expand Up @@ -144,7 +146,9 @@ class PluginLoader extends Logger {
}

public async checkPluginUpdates() {
const updates = await checkForUpdates(this.plugins);
const frozenPlugins = this.deckyState.publicState().frozenPlugins;

const updates = await checkForUpdates(this.plugins.filter((p) => !frozenPlugins.includes(p.name)));
this.deckyState.setUpdates(updates);
return updates;
}
Expand Down Expand Up @@ -224,6 +228,7 @@ class PluginLoader extends Logger {
this.deckyState.setPluginOrder(pluginOrder);
});

this.frozenPluginsService.init();
this.hiddenPluginsService.init();
this.notificationService.init();
}
Expand Down

0 comments on commit f2fe142

Please sign in to comment.