diff options
| author | Álvaro Cuesta <1827495+alvaro-cuesta@users.noreply.github.com> | 2025-01-02 20:38:40 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-02 11:38:40 -0800 |
| commit | f6144f9634482d6bd0ed88a31495c6c6c88add96 (patch) | |
| tree | 21797e504d62c33c1514870a457e36d3b4f3cbac /frontend/src/components/store/PluginCard.tsx | |
| parent | 79bb62a3c4f69dbd57dc037da1269285cae1b26d (diff) | |
| download | decky-loader-75079fafd9b64a4fd1479fbb7726c3fcded3c5fb.tar.gz decky-loader-75079fafd9b64a4fd1479fbb7726c3fcded3c5fb.zip | |
feat: sync with local plugin status in store (#733)v3.1.0-pre1
* 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.
Diffstat (limited to 'frontend/src/components/store/PluginCard.tsx')
| -rw-r--r-- | frontend/src/components/store/PluginCard.tsx | 73 |
1 files changed, 59 insertions, 14 deletions
diff --git a/frontend/src/components/store/PluginCard.tsx b/frontend/src/components/store/PluginCard.tsx index 6e2a3510..f64abd09 100644 --- a/frontend/src/components/store/PluginCard.tsx +++ b/frontend/src/components/store/PluginCard.tsx @@ -1,18 +1,32 @@ import { ButtonItem, Dropdown, Focusable, PanelSectionRow, SingleDropdownOption, SuspensefulImage } from '@decky/ui'; import { CSSProperties, FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { FaArrowDown, FaArrowUp, FaCheck, FaDownload, FaRecycle } from 'react-icons/fa'; -import { InstallType } from '../../plugin'; -import { StorePlugin, StorePluginVersion, requestPluginInstall } from '../../store'; +import { InstallType, Plugin } from '../../plugin'; +import { StorePlugin, requestPluginInstall } from '../../store'; import ExternalLink from '../ExternalLink'; 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 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(); @@ -43,7 +57,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => { height: '200px', objectFit: 'cover', }} - src={plugin.image_url} + src={storePlugin.image_url} /> </div> <div @@ -69,7 +83,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => { width: '90%', }} > - {plugin.name} + {storePlugin.name} </span> <span className="deckyStoreCardAuthor" @@ -78,7 +92,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => { fontSize: '1em', }} > - {plugin.author} + {storePlugin.author} </span> <span className="deckyStoreCardDescription" @@ -91,8 +105,8 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => { display: '-webkit-box', }} > - {plugin.description ? ( - plugin.description + {storePlugin.description ? ( + storePlugin.description ) : ( <span> <i style={{ color: '#666' }}>{t('PluginCard.plugin_no_desc')}</i> @@ -141,18 +155,49 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => { bottomSeparator="none" layout="below" 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> </div> <div className="deckyStoreCardVersionContainer" style={{ minWidth: '130px' }}> <Dropdown rgOptions={ - plugin.versions.map((version: StorePluginVersion, index) => ({ + storePlugin.versions.map((version, 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[] } menuLabel={t('PluginCard.plugin_version_label') as string} |
