Skip to content

Commit f6144f9

Browse files
feat: sync with local plugin status in store (#733)
* fix: useDeckyState proper type and safety * refactor: plugin list Avoids unneeded re-renders. See https://react.dev/learn/you-might-not-need-an-effect#caching-expensive-calculations * feat: sync with local plugin status in store Adds some QoL changes to the plugin store browser: - Add ✓ icon to currently installed plugin version in version selector - Change install button label depending on the install type that the button would trigger - Adds icon to install button for clarity The goal is to make it clear to the user what the current state of the installed plugin is, and what would be the impact of installing the selected version. Resolves #360 * lint: prettier * fix: add missing translations * refactor: safer translation strings on install Prefer using `t(...)` instead of `TranslationHelper` since it ensures that the translation keys are not missing in the locale files when running the `extractext` task. By adding comments with `t(...)` calls, `i18next-parser` will generate the strings as if they were present as literals in the code (see https://github.com/i18next/i18next-parser#caveats). This does _not_ suppress the warnings (since `i18next-parser` does not have access to TS types, so it cannot infer template literals) but it at least makes it less likely that a translation will be missed by mistake, have typos, etc.
1 parent 79bb62a commit f6144f9

File tree

11 files changed

+211
-99
lines changed

11 files changed

+211
-99
lines changed

backend/decky_loader/browser.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class PluginInstallType(IntEnum):
2929
INSTALL = 0
3030
REINSTALL = 1
3131
UPDATE = 2
32+
DOWNGRADE = 3
33+
OVERWRITE = 4
3234

3335
class PluginInstallRequest(TypedDict):
3436
name: str
@@ -323,5 +325,5 @@ def cleanup_plugin_settings(self, name: str):
323325
if name in plugin_order:
324326
plugin_order.remove(name)
325327
self.settings.setSetting("pluginOrder", plugin_order)
326-
328+
327329
logger.debug("Removed any settings for plugin %s", name)

backend/decky_loader/locales/en-US.json

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@
5252
"MultiplePluginsInstallModal": {
5353
"confirm": "Are you sure you want to make the following modifications?",
5454
"description": {
55+
"downgrade": "Downgrade {{name}} to {{version}}",
5556
"install": "Install {{name}} {{version}}",
57+
"overwrite": "Overwrite {{name}} with {{version}}",
5658
"reinstall": "Reinstall {{name}} {{version}}",
5759
"update": "Update {{name}} to {{version}}"
5860
},
@@ -61,30 +63,51 @@
6163
"loading": "Working"
6264
},
6365
"title": {
66+
"downgrade_one": "Downgrade 1 plugin",
67+
"downgrade_other": "Downgrade {{count}} plugins",
6468
"install_one": "Install 1 plugin",
6569
"install_other": "Install {{count}} plugins",
6670
"mixed_one": "Modify {{count}} plugin",
6771
"mixed_other": "Modify {{count}} plugins",
72+
"overwrite_one": "Overwrite 1 plugin",
73+
"overwrite_other": "Overwrite {{count}} plugins",
6874
"reinstall_one": "Reinstall 1 plugin",
6975
"reinstall_other": "Reinstall {{count}} plugins",
7076
"update_one": "Update 1 plugin",
7177
"update_other": "Update {{count}} plugins"
7278
}
7379
},
7480
"PluginCard": {
81+
"plugin_downgrade": "Downgrade",
7582
"plugin_full_access": "This plugin has full access to your Steam Deck.",
7683
"plugin_install": "Install",
7784
"plugin_no_desc": "No description provided.",
85+
"plugin_overwrite": "Overwrite",
86+
"plugin_reinstall": "Reinstall",
87+
"plugin_update": "Update",
7888
"plugin_version_label": "Plugin Version"
7989
},
8090
"PluginInstallModal": {
91+
"downgrade": {
92+
"button_idle": "Downgrade",
93+
"button_processing": "Downgrading",
94+
"desc": "Are you sure you want to downgrade {{artifact}} to version {{version}}?",
95+
"title": "Downgrade {{artifact}}"
96+
},
8197
"install": {
8298
"button_idle": "Install",
8399
"button_processing": "Installing",
84100
"desc": "Are you sure you want to install {{artifact}} {{version}}?",
85101
"title": "Install {{artifact}}"
86102
},
87103
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
104+
"not_installed": "(not installed)",
105+
"overwrite": {
106+
"button_idle": "Overwrite",
107+
"button_processing": "Overwriting",
108+
"desc": "Are you sure you want to overwrite {{artifact}} with version {{version}}?",
109+
"title": "Overwrite {{artifact}}"
110+
},
88111
"reinstall": {
89112
"button_idle": "Reinstall",
90113
"button_processing": "Reinstalling",
@@ -94,7 +117,7 @@
94117
"update": {
95118
"button_idle": "Update",
96119
"button_processing": "Updating",
97-
"desc": "Are you sure you want to update {{artifact}} {{version}}?",
120+
"desc": "Are you sure you want to update {{artifact}} to version {{version}}?",
98121
"title": "Update {{artifact}}"
99122
}
100123
},

frontend/src/components/DeckyState.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,17 @@ interface DeckyStateContext extends PublicDeckyState {
128128
closeActivePlugin(): void;
129129
}
130130

131-
const DeckyStateContext = createContext<DeckyStateContext>(null as any);
131+
const DeckyStateContext = createContext<DeckyStateContext | null>(null);
132132

133-
export const useDeckyState = () => useContext(DeckyStateContext);
133+
export const useDeckyState = () => {
134+
const deckyState = useContext(DeckyStateContext);
135+
136+
if (deckyState === null) {
137+
throw new Error('useDeckyState needs a parent DeckyStateContext');
138+
}
139+
140+
return deckyState;
141+
};
134142

135143
interface Props {
136144
deckyState: DeckyState;

frontend/src/components/PluginView.tsx

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
2-
import { FC, useEffect, useState } from 'react';
2+
import { FC, useMemo } from 'react';
33
import { useTranslation } from 'react-i18next';
44
import { FaEyeSlash } from 'react-icons/fa';
55

6-
import { Plugin } from '../plugin';
76
import { useDeckyState } from './DeckyState';
87
import NotificationBadge from './NotificationBadge';
98
import { useQuickAccessVisible } from './QuickAccessVisibleState';
109
import TitleView from './TitleView';
1110

1211
const PluginView: FC = () => {
13-
const { hiddenPlugins } = useDeckyState();
14-
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
12+
const { plugins, hiddenPlugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } =
13+
useDeckyState();
1514
const visible = useQuickAccessVisible();
1615
const { t } = useTranslation();
1716

18-
const [pluginList, setPluginList] = useState<Plugin[]>(
19-
plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)),
20-
);
21-
22-
useEffect(() => {
23-
setPluginList(plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)));
17+
const pluginList = useMemo(() => {
2418
console.log('updating PluginView after changes');
19+
20+
return [...plugins]
21+
.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name))
22+
.filter((p) => p.content)
23+
.filter(({ name }) => !hiddenPlugins.includes(name));
2524
}, [plugins, pluginOrder]);
2625

2726
if (activePlugin) {
@@ -43,20 +42,17 @@ const PluginView: FC = () => {
4342
}}
4443
>
4544
<PanelSection>
46-
{pluginList
47-
.filter((p) => p.content)
48-
.filter(({ name }) => !hiddenPlugins.includes(name))
49-
.map(({ name, icon }) => (
50-
<PanelSectionRow key={name}>
51-
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
52-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
53-
{icon}
54-
<div>{name}</div>
55-
<NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} />
56-
</div>
57-
</ButtonItem>
58-
</PanelSectionRow>
59-
))}
45+
{pluginList.map(({ name, icon }) => (
46+
<PanelSectionRow key={name}>
47+
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
48+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
49+
{icon}
50+
<div>{name}</div>
51+
<NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} />
52+
</div>
53+
</ButtonItem>
54+
</PanelSectionRow>
55+
))}
6056
{hiddenPlugins.length > 0 && (
6157
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}>
6258
<FaEyeSlash />

frontend/src/components/modals/MultiplePluginsInstallModal.tsx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { FC, useEffect, useMemo, useState } from 'react';
33
import { useTranslation } from 'react-i18next';
44
import { FaCheck, FaDownload } from 'react-icons/fa';
55

6-
import { InstallType } from '../../plugin';
6+
import { InstallType, InstallTypeTranslationMapping } from '../../plugin';
77

88
interface MultiplePluginsInstallModalProps {
99
requests: { name: string; version: string; hash: string; install_type: InstallType }[];
@@ -12,13 +12,7 @@ interface MultiplePluginsInstallModalProps {
1212
closeModal?(): void;
1313
}
1414

15-
// values are the JSON keys used in the translation file
16-
const InstallTypeTranslationMapping = {
17-
[InstallType.INSTALL]: 'install',
18-
[InstallType.REINSTALL]: 'reinstall',
19-
[InstallType.UPDATE]: 'update',
20-
} as const satisfies Record<InstallType, string>;
21-
15+
// IMPORTANT! Keep in sync with `t(...)` comments below
2216
type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[InstallType];
2317

2418
const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
@@ -70,6 +64,8 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
7064
if (requests.every(({ install_type }) => install_type === InstallType.INSTALL)) return 'install';
7165
if (requests.every(({ install_type }) => install_type === InstallType.REINSTALL)) return 'reinstall';
7266
if (requests.every(({ install_type }) => install_type === InstallType.UPDATE)) return 'update';
67+
if (requests.every(({ install_type }) => install_type === InstallType.DOWNGRADE)) return 'downgrade';
68+
if (requests.every(({ install_type }) => install_type === InstallType.OVERWRITE)) return 'overwrite';
7369
return 'mixed';
7470
}, [requests]);
7571

@@ -86,14 +82,35 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
8682
onCancel={async () => {
8783
await onCancel();
8884
}}
89-
strTitle={<div>{t(`MultiplePluginsInstallModal.title.${installTypeGrouped}`, { count: requests.length })}</div>}
90-
strOKButtonText={t(`MultiplePluginsInstallModal.ok_button.${loading ? 'loading' : 'idle'}`)}
85+
strTitle={
86+
<div>
87+
{
88+
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
89+
// t('MultiplePluginsInstallModal.title.install', { count: n })
90+
// t('MultiplePluginsInstallModal.title.reinstall', { count: n })
91+
// t('MultiplePluginsInstallModal.title.update', { count: n })
92+
// t('MultiplePluginsInstallModal.title.downgrade', { count: n })
93+
// t('MultiplePluginsInstallModal.title.overwrite', { count: n })
94+
// t('MultiplePluginsInstallModal.title.mixed', { count: n })
95+
t(`MultiplePluginsInstallModal.title.${installTypeGrouped}`, { count: requests.length })
96+
}
97+
</div>
98+
}
99+
strOKButtonText={
100+
loading ? t('MultiplePluginsInstallModal.ok_button.loading') : t('MultiplePluginsInstallModal.ok_button.idle')
101+
}
91102
>
92103
<div>
93104
{t('MultiplePluginsInstallModal.confirm')}
94105
<ul style={{ listStyle: 'none', display: 'flex', flexDirection: 'column', gap: '4px' }}>
95106
{requests.map(({ name, version, install_type, hash }, i) => {
96107
const installTypeStr = InstallTypeTranslationMapping[install_type];
108+
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
109+
// t('MultiplePluginsInstallModal.description.install')
110+
// t('MultiplePluginsInstallModal.description.reinstall')
111+
// t('MultiplePluginsInstallModal.description.update')
112+
// t('MultiplePluginsInstallModal.description.downgrade')
113+
// t('MultiplePluginsInstallModal.description.overwrite')
97114
const description = t(`MultiplePluginsInstallModal.description.${installTypeStr}`, {
98115
name,
99116
version,

frontend/src/components/modals/PluginInstallModal.tsx

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { ConfirmModal, Navigation, ProgressBarWithInfo, QuickAccessTab } from '@
22
import { FC, useEffect, useState } from 'react';
33
import { useTranslation } from 'react-i18next';
44

5-
import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper';
5+
import { InstallType, InstallTypeTranslationMapping } from '../../plugin';
66

77
interface PluginInstallModalProps {
88
artifact: string;
99
version: string;
1010
hash: string;
11-
installType: number;
11+
installType: InstallType;
1212
onOK(): void;
1313
onCancel(): void;
1414
closeModal?(): void;
@@ -44,6 +44,8 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
4444
};
4545
}, []);
4646

47+
const installTypeTranslationKey = InstallTypeTranslationMapping[installType];
48+
4749
return (
4850
<ConfirmModal
4951
bOKDisabled={loading}
@@ -59,12 +61,15 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
5961
}}
6062
strTitle={
6163
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', width: '100%' }}>
62-
<TranslationHelper
63-
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
64-
transText="title"
65-
i18nArgs={{ artifact: artifact }}
66-
installType={installType}
67-
/>
64+
{
65+
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
66+
// t('PluginInstallModal.install.title')
67+
// t('PluginInstallModal.reinstall.title')
68+
// t('PluginInstallModal.update.title')
69+
// t('PluginInstallModal.downgrade.title')
70+
// t('PluginInstallModal.overwrite.title')
71+
t(`PluginInstallModal.${installTypeTranslationKey}.title`, { artifact: artifact })
72+
}
6873
{loading && (
6974
<div style={{ marginLeft: 'auto' }}>
7075
<ProgressBarWithInfo
@@ -80,33 +85,44 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
8085
strOKButtonText={
8186
loading ? (
8287
<div>
83-
<TranslationHelper
84-
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
85-
transText="button_processing"
86-
installType={installType}
87-
/>
88+
{
89+
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
90+
// t('PluginInstallModal.install.button_processing')
91+
// t('PluginInstallModal.reinstall.button_processing')
92+
// t('PluginInstallModal.update.button_processing')
93+
// t('PluginInstallModal.downgrade.button_processing')
94+
// t('PluginInstallModal.overwrite.button_processing')
95+
t(`PluginInstallModal.${installTypeTranslationKey}.button_processing`)
96+
}
8897
</div>
8998
) : (
9099
<div>
91-
<TranslationHelper
92-
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
93-
transText="button_idle"
94-
installType={installType}
95-
/>
100+
{
101+
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
102+
// t('PluginInstallModal.install.button_idle')
103+
// t('PluginInstallModal.reinstall.button_idle')
104+
// t('PluginInstallModal.update.button_idle')
105+
// t('PluginInstallModal.downgrade.button_idle')
106+
// t('PluginInstallModal.overwrite.button_idle')
107+
t(`PluginInstallModal.${installTypeTranslationKey}.button_idle`)
108+
}
96109
</div>
97110
)
98111
}
99112
>
100113
<div>
101-
<TranslationHelper
102-
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
103-
transText="desc"
104-
i18nArgs={{
114+
{
115+
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
116+
// t('PluginInstallModal.install.desc')
117+
// t('PluginInstallModal.reinstall.desc')
118+
// t('PluginInstallModal.update.desc')
119+
// t('PluginInstallModal.downgrade.desc')
120+
// t('PluginInstallModal.overwrite.desc')
121+
t(`PluginInstallModal.${installTypeTranslationKey}.desc`, {
105122
artifact: artifact,
106123
version: version,
107-
}}
108-
installType={installType}
109-
/>
124+
})
125+
}
110126
</div>
111127
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
112128
</ConfirmModal>

0 commit comments

Comments
 (0)