mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 22:22:00 +00:00
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
This commit is contained in:
@@ -218,6 +218,10 @@
|
|||||||
"about": "About",
|
"about": "About",
|
||||||
"alph_asce": "Alphabetical (Z to A)",
|
"alph_asce": "Alphabetical (Z to A)",
|
||||||
"alph_desc": "Alphabetical (A to Z)",
|
"alph_desc": "Alphabetical (A to Z)",
|
||||||
|
"date_asce": "Oldest First",
|
||||||
|
"date_desc": "Newest First",
|
||||||
|
"downloads_asce": "Least Downloaded First",
|
||||||
|
"downloads_desc": "Most Downloaded First",
|
||||||
"title": "Browse"
|
"title": "Browse"
|
||||||
},
|
},
|
||||||
"store_testing_cta": "Please consider testing new plugins to help the Decky Loader team!",
|
"store_testing_cta": "Please consider testing new plugins to help the Decky Loader team!",
|
||||||
|
|||||||
@@ -8,20 +8,19 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
findModule,
|
findModule,
|
||||||
} from 'decky-frontend-lib';
|
} 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 { 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 { Store, StorePlugin, getPluginList, getStore } from '../../store';
|
import { SortDirections, SortOptions, Store, StorePlugin, getPluginList, getStore } from '../../store';
|
||||||
import PluginCard from './PluginCard';
|
import PluginCard from './PluginCard';
|
||||||
|
|
||||||
const logger = new Logger('Store');
|
const logger = new Logger('Store');
|
||||||
|
|
||||||
const StorePage: FC<{}> = () => {
|
const StorePage: FC<{}> = () => {
|
||||||
const [currentTabRoute, setCurrentTabRoute] = useState<string>('browse');
|
const [currentTabRoute, setCurrentTabRoute] = useState<string>('browse');
|
||||||
const [data, setData] = useState<StorePlugin[] | null>(null);
|
const [pluginCount, setPluginCount] = useState<number | null>(null);
|
||||||
const [isTesting, setIsTesting] = useState<boolean>(false);
|
|
||||||
const { TabCount } = findModule((m) => {
|
const { TabCount } = findModule((m) => {
|
||||||
if (m?.TabCount && m?.TabTitle) return true;
|
if (m?.TabCount && m?.TabTitle) return true;
|
||||||
return false;
|
return false;
|
||||||
@@ -29,17 +28,6 @@ const StorePage: FC<{}> = () => {
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -49,11 +37,6 @@ const StorePage: FC<{}> = () => {
|
|||||||
background: '#0005',
|
background: '#0005',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!data ? (
|
|
||||||
<div style={{ height: '100%' }}>
|
|
||||||
<SteamSpinner />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Tabs
|
<Tabs
|
||||||
activeTab={currentTabRoute}
|
activeTab={currentTabRoute}
|
||||||
onShowTab={(tabId: string) => {
|
onShowTab={(tabId: string) => {
|
||||||
@@ -62,9 +45,9 @@ const StorePage: FC<{}> = () => {
|
|||||||
tabs={[
|
tabs={[
|
||||||
{
|
{
|
||||||
title: t('Store.store_tabs.title'),
|
title: t('Store.store_tabs.title'),
|
||||||
content: <BrowseTab children={{ data: data, isTesting: isTesting }} />,
|
content: <BrowseTab setPluginCount={setPluginCount} />,
|
||||||
id: 'browse',
|
id: 'browse',
|
||||||
renderTabAddon: () => <span className={TabCount}>{data.length}</span>,
|
renderTabAddon: () => <span className={TabCount}>{pluginCount}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('Store.store_tabs.about'),
|
title: t('Store.store_tabs.about'),
|
||||||
@@ -73,28 +56,52 @@ const StorePage: FC<{}> = () => {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const BrowseTab: FC<{ children: { data: StorePlugin[]; isTesting: boolean } }> = (data) => {
|
const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }> = ({ setPluginCount }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const sortOptions = useMemo(
|
const dropdownSortOptions = useMemo(
|
||||||
(): DropdownOption[] => [
|
(): DropdownOption[] => [
|
||||||
{ data: 1, label: t('Store.store_tabs.alph_desc') },
|
// ascending and descending order are the wrong way around for the alphabetical sort
|
||||||
{ data: 2, label: t('Store.store_tabs.alph_asce') },
|
// 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 filterOptions = useMemo((): DropdownOption[] => [{ data: 1, label: 'All' }], []);
|
||||||
|
const [selectedSort, setSort] = useState<[SortOptions, SortDirections]>(dropdownSortOptions[0].data);
|
||||||
const [selectedSort, setSort] = useState<number>(sortOptions[0].data);
|
|
||||||
// const [selectedFilter, setFilter] = useState<number>(filterOptions[0].data);
|
// const [selectedFilter, setFilter] = useState<number>(filterOptions[0].data);
|
||||||
const [searchFieldValue, setSearchValue] = useState<string>('');
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -117,7 +124,7 @@ const BrowseTab: FC<{ children: { data: StorePlugin[]; isTesting: boolean } }> =
|
|||||||
<span className="DialogLabel">{t("Store.store_sort.label")}</span>
|
<span className="DialogLabel">{t("Store.store_sort.label")}</span>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menuLabel={t("Store.store_sort.label") as string}
|
menuLabel={t("Store.store_sort.label") as string}
|
||||||
rgOptions={sortOptions}
|
rgOptions={dropdownSortOptions}
|
||||||
strDefaultLabel={t("Store.store_sort.label_def") as string}
|
strDefaultLabel={t("Store.store_sort.label_def") as string}
|
||||||
selectedOption={selectedSort}
|
selectedOption={selectedSort}
|
||||||
onChange={(e) => setSort(e.data)}
|
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>
|
<span className="DialogLabel">{t('Store.store_sort.label')}</span>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menuLabel={t('Store.store_sort.label') as string}
|
menuLabel={t('Store.store_sort.label') as string}
|
||||||
rgOptions={sortOptions}
|
rgOptions={dropdownSortOptions}
|
||||||
strDefaultLabel={t('Store.store_sort.label_def') as string}
|
strDefaultLabel={t('Store.store_sort.label_def') as string}
|
||||||
selectedOption={selectedSort}
|
selectedOption={selectedSort}
|
||||||
onChange={(e) => setSort(e.data)}
|
onChange={(e) => setSort(e.data)}
|
||||||
@@ -182,7 +189,7 @@ const BrowseTab: FC<{ children: { data: StorePlugin[]; isTesting: boolean } }> =
|
|||||||
</div>
|
</div>
|
||||||
</Focusable>
|
</Focusable>
|
||||||
</div>
|
</div>
|
||||||
{data.children.isTesting && (
|
{isTesting && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -213,7 +220,12 @@ const BrowseTab: FC<{ children: { data: StorePlugin[]; isTesting: boolean } }> =
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
{data.children.data
|
{!pluginList ? (
|
||||||
|
<div style={{ height: '100%' }}>
|
||||||
|
<SteamSpinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
pluginList
|
||||||
.filter((plugin: StorePlugin) => {
|
.filter((plugin: StorePlugin) => {
|
||||||
return (
|
return (
|
||||||
plugin.name.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
|
plugin.name.toLowerCase().includes(searchFieldValue.toLowerCase()) ||
|
||||||
@@ -222,13 +234,8 @@ const BrowseTab: FC<{ children: { data: StorePlugin[]; isTesting: boolean } }> =
|
|||||||
plugin.tags.some((tag: string) => tag.toLowerCase().includes(searchFieldValue.toLowerCase()))
|
plugin.tags.some((tag: string) => tag.toLowerCase().includes(searchFieldValue.toLowerCase()))
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.map((plugin: StorePlugin) => <PluginCard plugin={plugin} />)
|
||||||
if (selectedSort % 2 === 1) return a.name.localeCompare(b.name);
|
)}
|
||||||
else return b.name.localeCompare(a.name);
|
|
||||||
})
|
|
||||||
.map((plugin: StorePlugin) => (
|
|
||||||
<PluginCard plugin={plugin} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+22
-2
@@ -7,6 +7,17 @@ export enum Store {
|
|||||||
Custom,
|
Custom,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SortOptions {
|
||||||
|
name = 'name',
|
||||||
|
date = 'date',
|
||||||
|
downloads = 'downloads',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SortDirections {
|
||||||
|
ascending = 'asc',
|
||||||
|
descending = 'desc',
|
||||||
|
}
|
||||||
|
|
||||||
export interface StorePluginVersion {
|
export interface StorePluginVersion {
|
||||||
name: string;
|
name: string;
|
||||||
hash: string;
|
hash: string;
|
||||||
@@ -36,11 +47,20 @@ export async function getStore(): Promise<Store> {
|
|||||||
return await getSetting<Store>('store', Store.Default);
|
return await getSetting<Store>('store', Store.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPluginList(): Promise<StorePlugin[]> {
|
export async function getPluginList(
|
||||||
|
sort_by: SortOptions | null = null,
|
||||||
|
sort_direction: SortDirections | null = null,
|
||||||
|
): Promise<StorePlugin[]> {
|
||||||
let version = await window.DeckyPluginLoader.updateVersion();
|
let version = await window.DeckyPluginLoader.updateVersion();
|
||||||
let store = await getSetting<Store | null>('store', null);
|
let store = await getSetting<Store | null>('store', null);
|
||||||
|
|
||||||
let customURL = await getSetting<string>('store-url', 'https://plugins.deckbrew.xyz/plugins');
|
let customURL = await getSetting<string>('store-url', 'https://plugins.deckbrew.xyz/plugins');
|
||||||
|
|
||||||
|
let query: URLSearchParams | string = new URLSearchParams();
|
||||||
|
sort_by && query.set('sort_by', sort_by);
|
||||||
|
sort_direction && query.set('sort_direction', sort_direction);
|
||||||
|
query = '?' + String(query);
|
||||||
|
|
||||||
let storeURL;
|
let storeURL;
|
||||||
if (store === null) {
|
if (store === null) {
|
||||||
console.log('Could not get store, using Default.');
|
console.log('Could not get store, using Default.');
|
||||||
@@ -62,7 +82,7 @@ export async function getPluginList(): Promise<StorePlugin[]> {
|
|||||||
storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return fetch(storeURL, {
|
return fetch(storeURL + query, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Decky-Version': version.current,
|
'X-Decky-Version': version.current,
|
||||||
|
|||||||
Reference in New Issue
Block a user