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.
This commit is contained in:
Álvaro Cuesta
2025-01-02 20:38:40 +01:00
committed by GitHub
parent 79bb62a3c4
commit f6144f9634
11 changed files with 211 additions and 99 deletions
+3 -1
View File
@@ -29,6 +29,8 @@ class PluginInstallType(IntEnum):
INSTALL = 0 INSTALL = 0
REINSTALL = 1 REINSTALL = 1
UPDATE = 2 UPDATE = 2
DOWNGRADE = 3
OVERWRITE = 4
class PluginInstallRequest(TypedDict): class PluginInstallRequest(TypedDict):
name: str name: str
@@ -323,5 +325,5 @@ class PluginBrowser:
if name in plugin_order: if name in plugin_order:
plugin_order.remove(name) plugin_order.remove(name)
self.settings.setSetting("pluginOrder", plugin_order) self.settings.setSetting("pluginOrder", plugin_order)
logger.debug("Removed any settings for plugin %s", name) logger.debug("Removed any settings for plugin %s", name)
+24 -1
View File
@@ -52,7 +52,9 @@
"MultiplePluginsInstallModal": { "MultiplePluginsInstallModal": {
"confirm": "Are you sure you want to make the following modifications?", "confirm": "Are you sure you want to make the following modifications?",
"description": { "description": {
"downgrade": "Downgrade {{name}} to {{version}}",
"install": "Install {{name}} {{version}}", "install": "Install {{name}} {{version}}",
"overwrite": "Overwrite {{name}} with {{version}}",
"reinstall": "Reinstall {{name}} {{version}}", "reinstall": "Reinstall {{name}} {{version}}",
"update": "Update {{name}} to {{version}}" "update": "Update {{name}} to {{version}}"
}, },
@@ -61,10 +63,14 @@
"loading": "Working" "loading": "Working"
}, },
"title": { "title": {
"downgrade_one": "Downgrade 1 plugin",
"downgrade_other": "Downgrade {{count}} plugins",
"install_one": "Install 1 plugin", "install_one": "Install 1 plugin",
"install_other": "Install {{count}} plugins", "install_other": "Install {{count}} plugins",
"mixed_one": "Modify {{count}} plugin", "mixed_one": "Modify {{count}} plugin",
"mixed_other": "Modify {{count}} plugins", "mixed_other": "Modify {{count}} plugins",
"overwrite_one": "Overwrite 1 plugin",
"overwrite_other": "Overwrite {{count}} plugins",
"reinstall_one": "Reinstall 1 plugin", "reinstall_one": "Reinstall 1 plugin",
"reinstall_other": "Reinstall {{count}} plugins", "reinstall_other": "Reinstall {{count}} plugins",
"update_one": "Update 1 plugin", "update_one": "Update 1 plugin",
@@ -72,12 +78,22 @@
} }
}, },
"PluginCard": { "PluginCard": {
"plugin_downgrade": "Downgrade",
"plugin_full_access": "This plugin has full access to your Steam Deck.", "plugin_full_access": "This plugin has full access to your Steam Deck.",
"plugin_install": "Install", "plugin_install": "Install",
"plugin_no_desc": "No description provided.", "plugin_no_desc": "No description provided.",
"plugin_overwrite": "Overwrite",
"plugin_reinstall": "Reinstall",
"plugin_update": "Update",
"plugin_version_label": "Plugin Version" "plugin_version_label": "Plugin Version"
}, },
"PluginInstallModal": { "PluginInstallModal": {
"downgrade": {
"button_idle": "Downgrade",
"button_processing": "Downgrading",
"desc": "Are you sure you want to downgrade {{artifact}} to version {{version}}?",
"title": "Downgrade {{artifact}}"
},
"install": { "install": {
"button_idle": "Install", "button_idle": "Install",
"button_processing": "Installing", "button_processing": "Installing",
@@ -85,6 +101,13 @@
"title": "Install {{artifact}}" "title": "Install {{artifact}}"
}, },
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.", "no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
"not_installed": "(not installed)",
"overwrite": {
"button_idle": "Overwrite",
"button_processing": "Overwriting",
"desc": "Are you sure you want to overwrite {{artifact}} with version {{version}}?",
"title": "Overwrite {{artifact}}"
},
"reinstall": { "reinstall": {
"button_idle": "Reinstall", "button_idle": "Reinstall",
"button_processing": "Reinstalling", "button_processing": "Reinstalling",
@@ -94,7 +117,7 @@
"update": { "update": {
"button_idle": "Update", "button_idle": "Update",
"button_processing": "Updating", "button_processing": "Updating",
"desc": "Are you sure you want to update {{artifact}} {{version}}?", "desc": "Are you sure you want to update {{artifact}} to version {{version}}?",
"title": "Update {{artifact}}" "title": "Update {{artifact}}"
} }
}, },
+10 -2
View File
@@ -128,9 +128,17 @@ interface DeckyStateContext extends PublicDeckyState {
closeActivePlugin(): void; closeActivePlugin(): void;
} }
const DeckyStateContext = createContext<DeckyStateContext>(null as any); const DeckyStateContext = createContext<DeckyStateContext | null>(null);
export const useDeckyState = () => useContext(DeckyStateContext); export const useDeckyState = () => {
const deckyState = useContext(DeckyStateContext);
if (deckyState === null) {
throw new Error('useDeckyState needs a parent DeckyStateContext');
}
return deckyState;
};
interface Props { interface Props {
deckyState: DeckyState; deckyState: DeckyState;
+20 -24
View File
@@ -1,27 +1,26 @@
import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui'; import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
import { FC, useEffect, useState } from 'react'; import { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa'; import { FaEyeSlash } from 'react-icons/fa';
import { Plugin } from '../plugin';
import { useDeckyState } from './DeckyState'; import { useDeckyState } from './DeckyState';
import NotificationBadge from './NotificationBadge'; import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState'; import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView'; import TitleView from './TitleView';
const PluginView: FC = () => { const PluginView: FC = () => {
const { hiddenPlugins } = useDeckyState(); const { plugins, hiddenPlugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } =
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState(); useDeckyState();
const visible = useQuickAccessVisible(); const visible = useQuickAccessVisible();
const { t } = useTranslation(); const { t } = useTranslation();
const [pluginList, setPluginList] = useState<Plugin[]>( const pluginList = useMemo(() => {
plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)),
);
useEffect(() => {
setPluginList(plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)));
console.log('updating PluginView after changes'); console.log('updating PluginView after changes');
return [...plugins]
.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name))
.filter((p) => p.content)
.filter(({ name }) => !hiddenPlugins.includes(name));
}, [plugins, pluginOrder]); }, [plugins, pluginOrder]);
if (activePlugin) { if (activePlugin) {
@@ -43,20 +42,17 @@ const PluginView: FC = () => {
}} }}
> >
<PanelSection> <PanelSection>
{pluginList {pluginList.map(({ name, icon }) => (
.filter((p) => p.content) <PanelSectionRow key={name}>
.filter(({ name }) => !hiddenPlugins.includes(name)) <ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
.map(({ name, icon }) => ( <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<PanelSectionRow key={name}> {icon}
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}> <div>{name}</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} />
{icon} </div>
<div>{name}</div> </ButtonItem>
<NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} /> </PanelSectionRow>
</div> ))}
</ButtonItem>
</PanelSectionRow>
))}
{hiddenPlugins.length > 0 && ( {hiddenPlugins.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}>
<FaEyeSlash /> <FaEyeSlash />
@@ -3,7 +3,7 @@ import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaCheck, FaDownload } from 'react-icons/fa'; import { FaCheck, FaDownload } from 'react-icons/fa';
import { InstallType } from '../../plugin'; import { InstallType, InstallTypeTranslationMapping } from '../../plugin';
interface MultiplePluginsInstallModalProps { interface MultiplePluginsInstallModalProps {
requests: { name: string; version: string; hash: string; install_type: InstallType }[]; requests: { name: string; version: string; hash: string; install_type: InstallType }[];
@@ -12,13 +12,7 @@ interface MultiplePluginsInstallModalProps {
closeModal?(): void; closeModal?(): void;
} }
// values are the JSON keys used in the translation file // IMPORTANT! Keep in sync with `t(...)` comments below
const InstallTypeTranslationMapping = {
[InstallType.INSTALL]: 'install',
[InstallType.REINSTALL]: 'reinstall',
[InstallType.UPDATE]: 'update',
} as const satisfies Record<InstallType, string>;
type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[InstallType]; type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[InstallType];
const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
@@ -70,6 +64,8 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
if (requests.every(({ install_type }) => install_type === InstallType.INSTALL)) return 'install'; if (requests.every(({ install_type }) => install_type === InstallType.INSTALL)) return 'install';
if (requests.every(({ install_type }) => install_type === InstallType.REINSTALL)) return 'reinstall'; if (requests.every(({ install_type }) => install_type === InstallType.REINSTALL)) return 'reinstall';
if (requests.every(({ install_type }) => install_type === InstallType.UPDATE)) return 'update'; if (requests.every(({ install_type }) => install_type === InstallType.UPDATE)) return 'update';
if (requests.every(({ install_type }) => install_type === InstallType.DOWNGRADE)) return 'downgrade';
if (requests.every(({ install_type }) => install_type === InstallType.OVERWRITE)) return 'overwrite';
return 'mixed'; return 'mixed';
}, [requests]); }, [requests]);
@@ -86,14 +82,35 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
onCancel={async () => { onCancel={async () => {
await onCancel(); await onCancel();
}} }}
strTitle={<div>{t(`MultiplePluginsInstallModal.title.${installTypeGrouped}`, { count: requests.length })}</div>} strTitle={
strOKButtonText={t(`MultiplePluginsInstallModal.ok_button.${loading ? 'loading' : 'idle'}`)} <div>
{
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
// t('MultiplePluginsInstallModal.title.install', { count: n })
// t('MultiplePluginsInstallModal.title.reinstall', { count: n })
// t('MultiplePluginsInstallModal.title.update', { count: n })
// t('MultiplePluginsInstallModal.title.downgrade', { count: n })
// t('MultiplePluginsInstallModal.title.overwrite', { count: n })
// t('MultiplePluginsInstallModal.title.mixed', { count: n })
t(`MultiplePluginsInstallModal.title.${installTypeGrouped}`, { count: requests.length })
}
</div>
}
strOKButtonText={
loading ? t('MultiplePluginsInstallModal.ok_button.loading') : t('MultiplePluginsInstallModal.ok_button.idle')
}
> >
<div> <div>
{t('MultiplePluginsInstallModal.confirm')} {t('MultiplePluginsInstallModal.confirm')}
<ul style={{ listStyle: 'none', display: 'flex', flexDirection: 'column', gap: '4px' }}> <ul style={{ listStyle: 'none', display: 'flex', flexDirection: 'column', gap: '4px' }}>
{requests.map(({ name, version, install_type, hash }, i) => { {requests.map(({ name, version, install_type, hash }, i) => {
const installTypeStr = InstallTypeTranslationMapping[install_type]; const installTypeStr = InstallTypeTranslationMapping[install_type];
// IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
// t('MultiplePluginsInstallModal.description.install')
// t('MultiplePluginsInstallModal.description.reinstall')
// t('MultiplePluginsInstallModal.description.update')
// t('MultiplePluginsInstallModal.description.downgrade')
// t('MultiplePluginsInstallModal.description.overwrite')
const description = t(`MultiplePluginsInstallModal.description.${installTypeStr}`, { const description = t(`MultiplePluginsInstallModal.description.${installTypeStr}`, {
name, name,
version, version,
@@ -2,13 +2,13 @@ import { ConfirmModal, Navigation, ProgressBarWithInfo, QuickAccessTab } from '@
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper'; import { InstallType, InstallTypeTranslationMapping } from '../../plugin';
interface PluginInstallModalProps { interface PluginInstallModalProps {
artifact: string; artifact: string;
version: string; version: string;
hash: string; hash: string;
installType: number; installType: InstallType;
onOK(): void; onOK(): void;
onCancel(): void; onCancel(): void;
closeModal?(): void; closeModal?(): void;
@@ -44,6 +44,8 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
}; };
}, []); }, []);
const installTypeTranslationKey = InstallTypeTranslationMapping[installType];
return ( return (
<ConfirmModal <ConfirmModal
bOKDisabled={loading} bOKDisabled={loading}
@@ -59,12 +61,15 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
}} }}
strTitle={ strTitle={
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', width: '100%' }}> <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', width: '100%' }}>
<TranslationHelper {
transClass={TranslationClass.PLUGIN_INSTALL_MODAL} // IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
transText="title" // t('PluginInstallModal.install.title')
i18nArgs={{ artifact: artifact }} // t('PluginInstallModal.reinstall.title')
installType={installType} // t('PluginInstallModal.update.title')
/> // t('PluginInstallModal.downgrade.title')
// t('PluginInstallModal.overwrite.title')
t(`PluginInstallModal.${installTypeTranslationKey}.title`, { artifact: artifact })
}
{loading && ( {loading && (
<div style={{ marginLeft: 'auto' }}> <div style={{ marginLeft: 'auto' }}>
<ProgressBarWithInfo <ProgressBarWithInfo
@@ -80,33 +85,44 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
strOKButtonText={ strOKButtonText={
loading ? ( loading ? (
<div> <div>
<TranslationHelper {
transClass={TranslationClass.PLUGIN_INSTALL_MODAL} // IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
transText="button_processing" // t('PluginInstallModal.install.button_processing')
installType={installType} // t('PluginInstallModal.reinstall.button_processing')
/> // t('PluginInstallModal.update.button_processing')
// t('PluginInstallModal.downgrade.button_processing')
// t('PluginInstallModal.overwrite.button_processing')
t(`PluginInstallModal.${installTypeTranslationKey}.button_processing`)
}
</div> </div>
) : ( ) : (
<div> <div>
<TranslationHelper {
transClass={TranslationClass.PLUGIN_INSTALL_MODAL} // IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
transText="button_idle" // t('PluginInstallModal.install.button_idle')
installType={installType} // t('PluginInstallModal.reinstall.button_idle')
/> // t('PluginInstallModal.update.button_idle')
// t('PluginInstallModal.downgrade.button_idle')
// t('PluginInstallModal.overwrite.button_idle')
t(`PluginInstallModal.${installTypeTranslationKey}.button_idle`)
}
</div> </div>
) )
} }
> >
<div> <div>
<TranslationHelper {
transClass={TranslationClass.PLUGIN_INSTALL_MODAL} // IMPORTANT! These comments are not cosmetic and are needed for `extracttext` task to work
transText="desc" // t('PluginInstallModal.install.desc')
i18nArgs={{ // t('PluginInstallModal.reinstall.desc')
// t('PluginInstallModal.update.desc')
// t('PluginInstallModal.downgrade.desc')
// t('PluginInstallModal.overwrite.desc')
t(`PluginInstallModal.${installTypeTranslationKey}.desc`, {
artifact: artifact, artifact: artifact,
version: version, version: version,
}} })
installType={installType} }
/>
</div> </div>
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>} {hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
</ConfirmModal> </ConfirmModal>
+59 -14
View File
@@ -1,18 +1,32 @@
import { ButtonItem, Dropdown, Focusable, PanelSectionRow, SingleDropdownOption, SuspensefulImage } from '@decky/ui'; import { ButtonItem, Dropdown, Focusable, PanelSectionRow, SingleDropdownOption, SuspensefulImage } from '@decky/ui';
import { CSSProperties, FC, useState } from 'react'; import { CSSProperties, FC, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaArrowDown, FaArrowUp, FaCheck, FaDownload, FaRecycle } from 'react-icons/fa';
import { InstallType } from '../../plugin'; import { InstallType, Plugin } from '../../plugin';
import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store'; import { StorePlugin, requestPluginInstall } from '../../store';
import ExternalLink from '../ExternalLink'; import ExternalLink from '../ExternalLink';
interface PluginCardProps { interface PluginCardProps {
plugin: StorePlugin; storePlugin: StorePlugin;
installedPlugin: Plugin | undefined;
} }
const PluginCard: FC<PluginCardProps> = ({ plugin }) => { const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => {
const [selectedOption, setSelectedOption] = useState<number>(0); const [selectedOption, setSelectedOption] = useState<number>(0);
const root = plugin.tags.some((tag) => tag === 'root'); const installedVersionIndex = storePlugin.versions.findIndex((version) => version.name === installedPlugin?.version);
const installType = // This assumes index in options is inverse to update order (i.e. newer updates are first)
installedPlugin && selectedOption < installedVersionIndex
? InstallType.UPDATE
: installedPlugin && selectedOption === installedVersionIndex
? InstallType.REINSTALL
: installedPlugin && selectedOption > installedVersionIndex
? InstallType.DOWNGRADE
: installedPlugin // can happen if installed version is not in store
? InstallType.OVERWRITE
: InstallType.INSTALL;
const root = storePlugin.tags.some((tag) => tag === 'root');
const { t } = useTranslation(); const { t } = useTranslation();
@@ -43,7 +57,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
height: '200px', height: '200px',
objectFit: 'cover', objectFit: 'cover',
}} }}
src={plugin.image_url} src={storePlugin.image_url}
/> />
</div> </div>
<div <div
@@ -69,7 +83,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
width: '90%', width: '90%',
}} }}
> >
{plugin.name} {storePlugin.name}
</span> </span>
<span <span
className="deckyStoreCardAuthor" className="deckyStoreCardAuthor"
@@ -78,7 +92,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
fontSize: '1em', fontSize: '1em',
}} }}
> >
{plugin.author} {storePlugin.author}
</span> </span>
<span <span
className="deckyStoreCardDescription" className="deckyStoreCardDescription"
@@ -91,8 +105,8 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
display: '-webkit-box', display: '-webkit-box',
}} }}
> >
{plugin.description ? ( {storePlugin.description ? (
plugin.description storePlugin.description
) : ( ) : (
<span> <span>
<i style={{ color: '#666' }}>{t('PluginCard.plugin_no_desc')}</i> <i style={{ color: '#666' }}>{t('PluginCard.plugin_no_desc')}</i>
@@ -141,18 +155,49 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
bottomSeparator="none" bottomSeparator="none"
layout="below" layout="below"
onClick={() => onClick={() =>
requestPluginInstall(plugin.name, plugin.versions[selectedOption], InstallType.INSTALL) requestPluginInstall(storePlugin.name, storePlugin.versions[selectedOption], installType)
} }
> >
<span className="deckyStoreCardInstallText">{t('PluginCard.plugin_install')}</span> <span
className="deckyStoreCardInstallText"
style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '5px' }}
>
{installType === InstallType.UPDATE ? (
<>
<FaArrowUp /> {t('PluginCard.plugin_update')}
</>
) : installType === InstallType.REINSTALL ? (
<>
<FaRecycle /> {t('PluginCard.plugin_reinstall')}
</>
) : installType === InstallType.DOWNGRADE ? (
<>
<FaArrowDown /> {t('PluginCard.plugin_downgrade')}
</>
) : installType === InstallType.OVERWRITE ? (
<>
<FaDownload /> {t('PluginCard.plugin_overwrite')}
</>
) : (
// installType === InstallType.INSTALL (also fallback)
<>
<FaDownload /> {t('PluginCard.plugin_install')}
</>
)}
</span>
</ButtonItem> </ButtonItem>
</div> </div>
<div className="deckyStoreCardVersionContainer" style={{ minWidth: '130px' }}> <div className="deckyStoreCardVersionContainer" style={{ minWidth: '130px' }}>
<Dropdown <Dropdown
rgOptions={ rgOptions={
plugin.versions.map((version: StorePluginVersion, index) => ({ storePlugin.versions.map((version, index) => ({
data: index, data: index,
label: version.name, label: (
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
{version.name}
{installedPlugin && installedVersionIndex === index ? <FaCheck /> : null}
</div>
),
})) as SingleDropdownOption[] })) as SingleDropdownOption[]
} }
menuLabel={t('PluginCard.plugin_version_label') as string} menuLabel={t('PluginCard.plugin_version_label') as string}
+9 -1
View File
@@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next';
import logo from '../../../assets/plugin_store.png'; import logo from '../../../assets/plugin_store.png';
import Logger from '../../logger'; import Logger from '../../logger';
import { SortDirections, SortOptions, Store, StorePlugin, getPluginList, getStore } from '../../store'; import { SortDirections, SortOptions, Store, StorePlugin, getPluginList, getStore } from '../../store';
import { useDeckyState } from '../DeckyState';
import ExternalLink from '../ExternalLink'; import ExternalLink from '../ExternalLink';
import PluginCard from './PluginCard'; import PluginCard from './PluginCard';
@@ -104,6 +105,8 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
})(); })();
}, []); }, []);
const { plugins: installedPlugins } = useDeckyState();
return ( return (
<> <>
<style>{` <style>{`
@@ -235,7 +238,12 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
plugin.tags.some((tag: string) => tag.toLowerCase().includes(searchFieldValue.toLowerCase())) plugin.tags.some((tag: string) => tag.toLowerCase().includes(searchFieldValue.toLowerCase()))
); );
}) })
.map((plugin: StorePlugin) => <PluginCard plugin={plugin} />) .map((plugin: StorePlugin) => (
<PluginCard
storePlugin={plugin}
installedPlugin={installedPlugins.find((installedPlugin) => installedPlugin.name === plugin.name)}
/>
))
)} )}
</div> </div>
</> </>
+5 -3
View File
@@ -146,9 +146,11 @@ class PluginLoader extends Logger {
}); });
this.routerHook.addRoute('/decky/store', () => ( this.routerHook.addRoute('/decky/store', () => (
<WithSuspense route={true}> <DeckyStateContextProvider deckyState={this.deckyState}>
<StorePage /> <WithSuspense route={true}>
</WithSuspense> <StorePage />
</WithSuspense>
</DeckyStateContextProvider>
)); ));
this.routerHook.addRoute('/decky/settings', () => { this.routerHook.addRoute('/decky/settings', () => {
return ( return (
+12
View File
@@ -18,8 +18,20 @@ export enum InstallType {
INSTALL, INSTALL,
REINSTALL, REINSTALL,
UPDATE, UPDATE,
DOWNGRADE,
OVERWRITE,
} }
// values are the JSON keys used in the translation file
// IMPORTANT! keep in sync with `t(...)` comments where this is used
export const InstallTypeTranslationMapping = {
[InstallType.INSTALL]: 'install',
[InstallType.REINSTALL]: 'reinstall',
[InstallType.UPDATE]: 'update',
[InstallType.DOWNGRADE]: 'downgrade',
[InstallType.OVERWRITE]: 'overwrite',
} as const satisfies Record<InstallType, string>;
type installPluginArgs = [ type installPluginArgs = [
artifact: string, artifact: string,
name?: string, name?: string,
+1 -18
View File
@@ -2,11 +2,9 @@ import { FC } from 'react';
import { Translation } from 'react-i18next'; import { Translation } from 'react-i18next';
import Logger from '../logger'; import Logger from '../logger';
import { InstallType } from '../plugin';
export enum TranslationClass { export enum TranslationClass {
PLUGIN_LOADER = 'PluginLoader', PLUGIN_LOADER = 'PluginLoader',
PLUGIN_INSTALL_MODAL = 'PluginInstallModal',
DEVELOPER = 'Developer', DEVELOPER = 'Developer',
} }
@@ -19,7 +17,7 @@ interface TranslationHelperProps {
const logger = new Logger('TranslationHelper'); const logger = new Logger('TranslationHelper');
const TranslationHelper: FC<TranslationHelperProps> = ({ transClass, transText, i18nArgs = null, installType = 0 }) => { const TranslationHelper: FC<TranslationHelperProps> = ({ transClass, transText, i18nArgs = null }) => {
return ( return (
<Translation> <Translation>
{(t, {}) => { {(t, {}) => {
@@ -28,21 +26,6 @@ const TranslationHelper: FC<TranslationHelperProps> = ({ transClass, transText,
return i18nArgs return i18nArgs
? t(TranslationClass.PLUGIN_LOADER + '.' + transText, i18nArgs) ? t(TranslationClass.PLUGIN_LOADER + '.' + transText, i18nArgs)
: t(TranslationClass.PLUGIN_LOADER + '.' + transText); : t(TranslationClass.PLUGIN_LOADER + '.' + transText);
case TranslationClass.PLUGIN_INSTALL_MODAL:
switch (installType) {
case InstallType.INSTALL:
return i18nArgs
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + transText, i18nArgs)
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + transText);
case InstallType.REINSTALL:
return i18nArgs
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + transText, i18nArgs)
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + transText);
case InstallType.UPDATE:
return i18nArgs
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + transText, i18nArgs)
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + transText);
}
case TranslationClass.DEVELOPER: case TranslationClass.DEVELOPER:
return i18nArgs return i18nArgs
? t(TranslationClass.DEVELOPER + '.' + transText, i18nArgs) ? t(TranslationClass.DEVELOPER + '.' + transText, i18nArgs)