diff --git a/backend/decky_loader/locales/en-US.json b/backend/decky_loader/locales/en-US.json index 836f48781..43d36e9a8 100644 --- a/backend/decky_loader/locales/en-US.json +++ b/backend/decky_loader/locales/en-US.json @@ -237,7 +237,12 @@ }, "store_filter": { "label": "Filter", - "label_def": "All" + "label_def": "All", + "options": { + "all": "All", + "installed": "Installed", + "not_installed": "Not Installed" + } }, "store_search": { "label": "Search" diff --git a/frontend/src/components/store/Store.tsx b/frontend/src/components/store/Store.tsx index 3209ba088..aefef4822 100644 --- a/frontend/src/components/store/Store.tsx +++ b/frontend/src/components/store/Store.tsx @@ -1,8 +1,8 @@ import { Dropdown, - DropdownOption, Focusable, PanelSectionRow, + SingleDropdownOption, SteamSpinner, Tabs, TextField, @@ -13,11 +13,15 @@ import { useTranslation } from 'react-i18next'; import logo from '../../../assets/plugin_store.png'; import Logger from '../../logger'; -import { SortDirections, SortOptions, Store, StorePlugin, getPluginList, getStore } from '../../store'; +import { SortKeys, Store, StoreFilter, StorePlugin, getPluginList, getStore } from '../../store'; import { useDeckyState } from '../DeckyState'; import ExternalLink from '../ExternalLink'; import PluginCard from './PluginCard'; +interface DropdownOptions extends SingleDropdownOption { + data: TData; +} + const logger = new Logger('Store'); const StorePage: FC<{}> = () => { @@ -63,39 +67,99 @@ const StorePage: FC<{}> = () => { ); }; +// Functions for each of the store sort options +const storeSortFunctions: Record['sort']>[0]> = { + 'name-ascending': (a, b) => a.name.localeCompare(b.name), + 'name-descending': (a, b) => b.name.localeCompare(a.name), + 'date-ascending': (a, b) => new Date(a.updated).valueOf() - new Date(b.updated).valueOf(), + 'date-descending': (a, b) => new Date(b.updated).valueOf() - new Date(a.updated).valueOf(), + 'downloads-ascending': (a, b) => b.downloads - a.downloads, + 'downloads-descending': (a, b) => a.downloads - b.downloads, +}; + const BrowseTab: FC<{ setPluginCount: Dispatch> }> = ({ setPluginCount }) => { const { t } = useTranslation(); const dropdownSortOptions = useMemo( - (): DropdownOption[] => [ + (): DropdownOptions[] => [ // ascending and descending order are the wrong way around for the alphabetical sort // this is because it was initially done incorrectly for i18n and 'fixing' it would // make all the translations incorrect - { data: [SortOptions.name, SortDirections.ascending], label: t('Store.store_tabs.alph_desc') }, - { data: [SortOptions.name, SortDirections.descending], label: t('Store.store_tabs.alph_asce') }, - { data: [SortOptions.date, SortDirections.ascending], label: t('Store.store_tabs.date_asce') }, - { data: [SortOptions.date, SortDirections.descending], label: t('Store.store_tabs.date_desc') }, - { data: [SortOptions.downloads, SortDirections.descending], label: t('Store.store_tabs.downloads_desc') }, - { data: [SortOptions.downloads, SortDirections.ascending], label: t('Store.store_tabs.downloads_asce') }, + { data: 'name-ascending', label: t('Store.store_tabs.alph_desc') }, + { data: 'name-descending', label: t('Store.store_tabs.alph_asce') }, + { data: 'date-ascending', label: t('Store.store_tabs.date_asce') }, + { data: 'date-descending', label: t('Store.store_tabs.date_desc') }, + { data: 'downloads-ascending', label: t('Store.store_tabs.downloads_desc') }, + { data: 'downloads-descending', label: t('Store.store_tabs.downloads_asce') }, ], [], ); - // const filterOptions = useMemo((): DropdownOption[] => [{ data: 1, label: 'All' }], []); - const [selectedSort, setSort] = useState<[SortOptions, SortDirections]>(dropdownSortOptions[0].data); - // const [selectedFilter, setFilter] = useState(filterOptions[0].data); + // Our list of filters populates automatically based on the enum and matches directly to locale strings + const filterOptions = useMemo( + () => + Object.keys(StoreFilter).map>( + (key) => ({ + data: StoreFilter[key as keyof typeof StoreFilter], + label: t(`Store.store_filter.options.${StoreFilter[key as keyof typeof StoreFilter]}`), + }), + {}, + ), + [], + ); + + const [selectedSort, setSort] = useState['data']>(dropdownSortOptions[0].data); + const [filter, setFilter] = useState(filterOptions[0].data); const [searchFieldValue, setSearchValue] = useState(''); const [pluginList, setPluginList] = useState(null); const [isTesting, setIsTesting] = useState(false); + const { plugins: installedPlugins } = useDeckyState(); + + const hasInstalledPlugin = (plugin: StorePlugin) => + installedPlugins?.find((installedPlugin) => installedPlugin.name === plugin.name); + + const filterPlugin = (plugin: StorePlugin): boolean => { + switch (filter) { + case StoreFilter.Installed: + return !!hasInstalledPlugin(plugin); + case StoreFilter.NotInstalled: + return !hasInstalledPlugin(plugin); + default: + return true; + } + }; + + const renderedList = useMemo(() => { + // Use an empty array in case it's null + const plugins = pluginList || []; + return ( + <> + {plugins + .filter(filterPlugin) + .filter( + (plugin) => + plugin.name.toLowerCase().includes(searchFieldValue.toLowerCase()) || + plugin.description.toLowerCase().includes(searchFieldValue.toLowerCase()) || + plugin.author.toLowerCase().includes(searchFieldValue.toLowerCase()) || + plugin.tags.some((tag) => tag.toLowerCase().includes(searchFieldValue.toLowerCase())), + ) + .sort(storeSortFunctions[selectedSort]) + .map((plugin) => ( + + ))} + + ); + }, [pluginList, filter, searchFieldValue, selectedSort, installedPlugins, storeSortFunctions]); + useEffect(() => { (async () => { - const res = await getPluginList(selectedSort[0], selectedSort[1]); + const res = await getPluginList(); logger.debug('got data!', res); setPluginList(res); setPluginCount(res.length); })(); - }, [selectedSort]); + }, []); useEffect(() => { (async () => { @@ -105,31 +169,27 @@ const BrowseTab: FC<{ setPluginCount: Dispatch> }> })(); }, []); - const { plugins: installedPlugins } = useDeckyState(); - return ( <> - {/* This should be used once filtering is added - + .deckyStoreCardInstallContainer > .Panel { + padding: 0; + } + `} - +
- {t("Store.store_sort.label")} + {t('Store.store_sort.label')} setSort(e.data)} /> @@ -138,50 +198,20 @@ const BrowseTab: FC<{ setPluginCount: Dispatch> }> style={{ display: 'flex', flexDirection: 'column', - width: '47.5%', - marginLeft: 'auto', + width: '100%', }} > - {t("Store.store_filter.label")} + {t('Store.store_filter.label')} setFilter(e.data)} />
-
- -
- setSearchValue(e.target.value)} /> -
-
-
- */} - - -
- {t('Store.store_sort.label')} - setSort(e.data)} - /> -
-
-
@@ -229,21 +259,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch> }>
) : ( - pluginList - .filter((plugin: StorePlugin) => { - return ( - plugin.name.toLowerCase().includes(searchFieldValue.toLowerCase()) || - plugin.description.toLowerCase().includes(searchFieldValue.toLowerCase()) || - plugin.author.toLowerCase().includes(searchFieldValue.toLowerCase()) || - plugin.tags.some((tag: string) => tag.toLowerCase().includes(searchFieldValue.toLowerCase())) - ); - }) - .map((plugin: StorePlugin) => ( - installedPlugin.name === plugin.name)} - /> - )) + renderedList )}
diff --git a/frontend/src/store.tsx b/frontend/src/store.tsx index dfd9b04bf..cf57984d1 100644 --- a/frontend/src/store.tsx +++ b/frontend/src/store.tsx @@ -9,6 +9,8 @@ export enum Store { Custom, } +export type SortKeys = `${keyof typeof SortOptions}-${keyof typeof SortDirections}`; + export enum SortOptions { name = 'name', date = 'date', @@ -20,9 +22,18 @@ export enum SortDirections { descending = 'desc', } +export enum StoreFilter { + All = 'all', + Installed = 'installed', + NotInstalled = 'not_installed', +} + export interface StorePluginVersion { name: string; hash: string; + created: Date; + downloads: number; + updates: number; artifact: string | undefined | null; } @@ -34,6 +45,10 @@ export interface StorePlugin { description: string; tags: string[]; image_url: string; + downloads: number; + updates: number; + created: Date; + updated: Date; } export interface PluginInstallRequest {