From fd4ed811be7871f85abbf6b8e86b8eb13b90cf94 Mon Sep 17 00:00:00 2001
From: Party Wumpus <48649272+PartyWumpus@users.noreply.github.com>
Date: Wed, 7 Feb 2024 17:38:08 +0000
Subject: Refactor plugin store and add sorting by downloads and release date
 (#547)

* untested first commit

* fix types & names

* comment out built in sorting for now

* rerun search when sort changes

* fix ts complaints

* use prettier

* stop switch-case fall through

* move spinner

* use locale instead of hardcoded string

* fix typo

* add sorting by downloads & try using the data field in the dropdown for data

* fix typing error

* fix asc/desc in dropdown

* fix asc/desc again. asc = smaller one go first aaaaa

* I don't think i know what ascending means maybe

* use props instead of children, like a normal component
---
 frontend/src/components/store/Store.tsx | 137 +++++++++++++++++---------------
 1 file changed, 72 insertions(+), 65 deletions(-)

(limited to 'frontend/src/components/store/Store.tsx')

diff --git a/frontend/src/components/store/Store.tsx b/frontend/src/components/store/Store.tsx
index 1afbb22b..e3d1b0f1 100644
--- a/frontend/src/components/store/Store.tsx
+++ b/frontend/src/components/store/Store.tsx
@@ -8,20 +8,19 @@ import {
   TextField,
   findModule,
 } from 'decky-frontend-lib';
-import { FC, useEffect, useMemo, useState } from 'react';
+import { Dispatch, FC, SetStateAction, useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import logo from '../../../assets/plugin_store.png';
 import Logger from '../../logger';
-import { Store, StorePlugin, getPluginList, getStore } from '../../store';
+import { SortDirections, SortOptions, Store, StorePlugin, getPluginList, getStore } from '../../store';
 import PluginCard from './PluginCard';
 
 const logger = new Logger('Store');
 
 const StorePage: FC<{}> = () => {
   const [currentTabRoute, setCurrentTabRoute] = useState<string>('browse');
-  const [data, setData] = useState<StorePlugin[] | null>(null);
-  const [isTesting, setIsTesting] = useState<boolean>(false);
+  const [pluginCount, setPluginCount] = useState<number | null>(null);
   const { TabCount } = findModule((m) => {
     if (m?.TabCount && m?.TabTitle) return true;
     return false;
@@ -29,17 +28,6 @@ const StorePage: FC<{}> = () => {
 
   const { t } = useTranslation();
 
-  useEffect(() => {
-    (async () => {
-      const res = await getPluginList();
-      logger.log('got data!', res);
-      setData(res);
-      const storeRes = await getStore();
-      logger.log(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`);
-      setIsTesting(storeRes === Store.Testing);
-    })();
-  }, []);
-
   return (
     <>
       <div
@@ -49,52 +37,71 @@ const StorePage: FC<{}> = () => {
           background: '#0005',
         }}
       >
-        {!data ? (
-          <div style={{ height: '100%' }}>
-            <SteamSpinner />
-          </div>
-        ) : (
-          <Tabs
-            activeTab={currentTabRoute}
-            onShowTab={(tabId: string) => {
-              setCurrentTabRoute(tabId);
-            }}
-            tabs={[
-              {
-                title: t('Store.store_tabs.title'),
-                content: <BrowseTab children={{ data: data, isTesting: isTesting }} />,
-                id: 'browse',
-                renderTabAddon: () => <span className={TabCount}>{data.length}</span>,
-              },
-              {
-                title: t('Store.store_tabs.about'),
-                content: <AboutTab />,
-                id: 'about',
-              },
-            ]}
-          />
-        )}
+        <Tabs
+          activeTab={currentTabRoute}
+          onShowTab={(tabId: string) => {
+            setCurrentTabRoute(tabId);
+          }}
+          tabs={[
+            {
+              title: t('Store.store_tabs.title'),
+              content: <BrowseTab setPluginCount={setPluginCount} />,
+              id: 'browse',
+              renderTabAddon: () => <span className={TabCount}>{pluginCount}</span>,
+            },
+            {
+              title: t('Store.store_tabs.about'),
+              content: <AboutTab />,
+              id: 'about',
+            },
+          ]}
+        />
       </div>
     </>
   );
 };
 
-const BrowseTab: FC<{ children: { data: StorePlugin[]; isTesting: boolean } }> = (data) => {
+const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }> = ({ setPluginCount }) => {
   const { t } = useTranslation();
 
-  const sortOptions = useMemo(
+  const dropdownSortOptions = useMemo(
     (): DropdownOption[] => [
-      { data: 1, label: t('Store.store_tabs.alph_desc') },
-      { data: 2, label: t('Store.store_tabs.alph_asce') },
+      // 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') },
     ],
     [],
   );
 
   // const filterOptions = useMemo((): DropdownOption[] => [{ data: 1, label: 'All' }], []);
-
-  const [selectedSort, setSort] = useState<number>(sortOptions[0].data);
+  const [selectedSort, setSort] = useState<[SortOptions, SortDirections]>(dropdownSortOptions[0].data);
   // const [selectedFilter, setFilter] = useState<number>(filterOptions[0].data);
   const [searchFieldValue, setSearchValue] = useState<string>('');
+  const [pluginList, setPluginList] = useState<StorePlugin[] | null>(null);
+  const [isTesting, setIsTesting] = useState<boolean>(false);
+
+  useEffect(() => {
+    (async () => {
+      const res = await getPluginList(selectedSort[0], selectedSort[1]);
+      logger.log('got data!', res);
+      setPluginList(res);
+      setPluginCount(res.length);
+    })();
+  }, [selectedSort]);
+
+  useEffect(() => {
+    (async () => {
+      const storeRes = await getStore();
+      logger.log(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`);
+      setIsTesting(storeRes === Store.Testing);
+    })();
+  }, []);
 
   return (
     <>
@@ -117,7 +124,7 @@ const BrowseTab: FC<{ children: { data: StorePlugin[]; isTesting: boolean } }> =
             <span className="DialogLabel">{t("Store.store_sort.label")}</span>
             <Dropdown
               menuLabel={t("Store.store_sort.label") as string}
-              rgOptions={sortOptions}
+              rgOptions={dropdownSortOptions}
               strDefaultLabel={t("Store.store_sort.label_def") as string}
               selectedOption={selectedSort}
               onChange={(e) => setSort(e.data)}
@@ -163,7 +170,7 @@ const BrowseTab: FC<{ children: { data: StorePlugin[]; isTesting: boolean } }> =
             <span className="DialogLabel">{t('Store.store_sort.label')}</span>
             <Dropdown
               menuLabel={t('Store.store_sort.label') as string}
-              rgOptions={sortOptions}
+              rgOptions={dropdownSortOptions}
               strDefaultLabel={t('Store.store_sort.label_def') as string}
               selectedOption={selectedSort}
               onChange={(e) => setSort(e.data)}
@@ -182,7 +189,7 @@ const BrowseTab: FC<{ children: { data: StorePlugin[]; isTesting: boolean } }> =
           </div>
         </Focusable>
       </div>
-      {data.children.isTesting && (
+      {isTesting && (
         <div
           style={{
             alignItems: 'center',
@@ -213,22 +220,22 @@ const BrowseTab: FC<{ children: { data: StorePlugin[]; isTesting: boolean } }> =
         </div>
       )}
       <div>
-        {data.children.data
-          .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()))
-            );
-          })
-          .sort((a, b) => {
-            if (selectedSort % 2 === 1) return a.name.localeCompare(b.name);
-            else return b.name.localeCompare(a.name);
-          })
-          .map((plugin: StorePlugin) => (
-            <PluginCard plugin={plugin} />
-          ))}
+        {!pluginList ? (
+          <div style={{ height: '100%' }}>
+            <SteamSpinner />
+          </div>
+        ) : (
+          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) => <PluginCard plugin={plugin} />)
+        )}
       </div>
     </>
   );
-- 
cgit v1.2.3

