diff --git a/src/components/CustomTabContainer.tsx b/src/components/CustomTabContainer.tsx index f7a0ae5..c102e0a 100644 --- a/src/components/CustomTabContainer.tsx +++ b/src/components/CustomTabContainer.tsx @@ -1,7 +1,8 @@ import { EditableTabSettings } from "./modals/EditTabModal"; import { TabFilterSettings, FilterType, Filter } from "./filters/Filters"; import { filtersHaveType, getIncludedCategoriesFromBitField } from "../lib/Utils"; -import { gamepadTabbedPageClasses } from "decky-frontend-lib"; +import { gamepadTabbedPageClasses, showModal } from "decky-frontend-lib"; +import { SortOverrideMessage } from './modals/SortOverrideMessage'; /** * Wrapper for injecting custom tabs. @@ -16,6 +17,7 @@ export class CustomTabContainer implements TabContainer { categoriesToInclude: number; autoHide: boolean; dependsOnMicroSDeck: boolean; + sortByOverride: number; /** * Creates a new CustomTabContainer. @@ -26,8 +28,9 @@ export class CustomTabContainer implements TabContainer { * @param filtersMode boolean operator for top level filters * @param categoriesToInclude A bit field of which categories should be included in the tab. * @param autoHide Whether or not the tab should automatically be hidden if it's collection is empty. + * @param sortByOverride The eSortBy number to force use for sorting. -1 ignores override. */ - constructor(id: string, title: string, position: number, filterSettingsList: TabFilterSettings[], filtersMode: LogicalMode, categoriesToInclude: number, autoHide: boolean) { + constructor(id: string, title: string, position: number, filterSettingsList: TabFilterSettings[], filtersMode: LogicalMode, categoriesToInclude: number, autoHide: boolean, sortByOverride: number) { this.id = id; this.title = title; this.position = position; @@ -36,6 +39,7 @@ export class CustomTabContainer implements TabContainer { this.categoriesToInclude = categoriesToInclude; this.autoHide = autoHide; this.dependsOnMicroSDeck = false; + this.sortByOverride = sortByOverride; //@ts-ignore this.collection = { @@ -57,17 +61,21 @@ export class CustomTabContainer implements TabContainer { this.checkMicroSDeckDependency(); } - getActualTab(TabContentComponent: TabContentComponent, sortingProps: Omit, footer: SteamTab['footer'], collectionAppFilter: any, isMicroSDeckInstalled: boolean): SteamTab | null { + getActualTab(TabContentComponent: TabContentComponent, sortingProps: Omit, footer: SteamTab['footer'] = {}, collectionAppFilter: any, isMicroSDeckInstalled: boolean): SteamTab | null { if (!isMicroSDeckInstalled && this.dependsOnMicroSDeck) return null; if (this.autoHide && this.collection.visibleApps.length === 0) return null; - + const showSortOverride = () => showModal(); + if (this.sortByOverride !== -1) footer.onOptionsButton = showSortOverride; + return { title: this.title, id: this.id, footer: footer, content: , renderTabAddon: () => { return @@ -117,6 +125,7 @@ export class CustomTabContainer implements TabContainer { this.categoriesToInclude = updatedTabInfo.categoriesToInclude; this.filters = updatedTabInfo.filters; this.autoHide = updatedTabInfo.autoHide; + this.sortByOverride = updatedTabInfo.sortByOverride; this.buildCollection(); this.checkMicroSDeckDependency(); } diff --git a/src/components/modals/EditTabModal.tsx b/src/components/modals/EditTabModal.tsx index e4bbe09..24cb034 100644 --- a/src/components/modals/EditTabModal.tsx +++ b/src/components/modals/EditTabModal.tsx @@ -1,8 +1,10 @@ import { ConfirmModal, DialogCheckbox, + DropdownItem, Field, Focusable, + SingleDropdownOption, TextField, ToggleField, afterPatch, @@ -19,6 +21,7 @@ import { FiltersPanel } from "../filters/FiltersPanel"; import { IncludeCategories, capitalizeFirstLetter, getIncludedCategoriesFromBitField, playUISound, updateCategoriesToIncludeBitField } from "../../lib/Utils"; import { BiSolidDownArrow } from "react-icons/bi"; import { CustomTabContainer } from '../CustomTabContainer'; +import { useSortingMenuItems } from '../../hooks/useSortingMenuItems'; export type EditableTabSettings = Omit, 'position' | 'id'>; @@ -31,13 +34,15 @@ type EditTabModalProps = { tabMasterManager: TabMasterManager, filtersMode: LogicalMode, categoriesToInclude: number, //bit field - autoHide: boolean + autoHide: boolean; + sortBy: number; }; + /** * The modal for editing and creating custom tabs. */ -export const EditTabModal: VFC = ({ closeModal, onConfirm, tabId, tabTitle, tabFilters, tabMasterManager, filtersMode, categoriesToInclude, autoHide: _autoHide }) => { +export const EditTabModal: VFC = ({ closeModal, onConfirm, tabId, tabTitle, tabFilters, tabMasterManager, filtersMode, categoriesToInclude, autoHide: _autoHide, sortBy }) => { const [name, setName] = useState(tabTitle ?? ''); const [topLevelFilters, setTopLevelFilters] = useState[]>(tabFilters); const [topLevelLogicMode, setTopLevelLogicMode] = useState(filtersMode); @@ -46,6 +51,8 @@ export const EditTabModal: VFC = ({ closeModal, onConfirm, ta const [canAddFilter, setCanAddFilter] = useState(true); const [patchInput, setPatchInput] = useState(true); const [autoHide, setAutoHide] = useState(_autoHide); + const [sortByOverride, setSortByOverride] = useState(sortBy); + const sortOptions: SingleDropdownOption[] = useSortingMenuItems([]); const nameInputElement = ; @@ -84,12 +91,13 @@ export const EditTabModal: VFC = ({ closeModal, onConfirm, ta filters: topLevelFilters, filtersMode: topLevelLogicMode, categoriesToInclude: catsToInclude, - autoHide: autoHide + autoHide: autoHide, + sortByOverride: sortByOverride }; onConfirm(tabId, updated); closeModal!(); } else { - if(!canAddFilter) PythonInterop.toast("Cannot Save Tab", "Some filters are incomplete"); + if (!canAddFilter) PythonInterop.toast("Cannot Save Tab", "Some filters are incomplete"); else PythonInterop.toast("Cannot Save Tab", "Please add a name and at least 1 filter"); } } @@ -118,17 +126,24 @@ export const EditTabModal: VFC = ({ closeModal, onConfirm, ta >
-
- Name -
- {nameInputElement} - + <> +
+ Name +
+ {nameInputElement} + } />
-
- setAutoHide(checked)} bottomSeparator='thick'/> +
+ setAutoHide(checked)} bottomSeparator='thick' /> + setSortByOverride(option.data)} + bottomSeparator='thick' + />
= ({ categoriesTo const showHiddenCat = Object.entries(catsToIncludeObj).filter(([cat]) => cat !== 'hidden').some(([_cat, checked]) => checked); - const getCatLabel = (category: string) => category === 'music' ? 'Soundtracks' : capitalizeFirstLetter(category) + const getCatLabel = (category: string) => category === 'music' ? 'Soundtracks' : capitalizeFirstLetter(category); - let catStrings = [] + let catStrings = []; for (const cat in catsToIncludeObj) { const include = catsToIncludeObj[cat as keyof typeof catsToIncludeObj]; @@ -186,8 +201,8 @@ const IncludeCategoriesPanel: VFC = ({ categoriesTo
Include in tab
-
- {!isOpen && +
+ {!isOpen && {catStrings.join(', ')} }
@@ -205,12 +220,12 @@ const IncludeCategoriesPanel: VFC = ({ categoriesTo {isOpen && (
{Object.entries(catsToIncludeObj).map(([category, shouldInclude]) => { - const label = getCatLabel(category) + const label = getCatLabel(category); const onChange = (checked: boolean) => { playUISound(checked ? '/sounds/deck_ui_switch_toggle_on.wav' : '/sounds/deck_ui_switch_toggle_off.wav'); setCategoriesToInclude(currentCatsBitField => updateCategoriesToIncludeBitField(currentCatsBitField, { [category]: checked })); - }; + }; return category === 'hidden' && !showHiddenCat ? null : ; })}
)} @@ -220,7 +235,8 @@ const IncludeCategoriesPanel: VFC = ({ categoriesTo left: "calc(16px - 1.8vw)", right: "calc(16px - 1.8vw)", height: "1px", - background: "#ffffff1a" }} + background: "#ffffff1a" + }} /> ); @@ -231,18 +247,27 @@ const IncludeCategoriesPanel: VFC = ({ categoriesTo * @param tabMasterManager TabMasterManager instance. */ export function showModalNewTab(tabMasterManager: TabMasterManager) { - showModal( - { - tabMasterManager.createCustomTab(tabSettings.title, tabMasterManager.getTabs().visibleTabsList.length, tabSettings.filters, tabSettings.filtersMode, tabSettings.categoriesToInclude, tabSettings.autoHide); - }} - tabFilters={[]} - tabMasterManager={tabMasterManager} - filtersMode="and" - categoriesToInclude={IncludeCategories.games} - autoHide={false} - /> - ); + showModal( + { + tabMasterManager.createCustomTab( + tabSettings.title, + tabMasterManager.getTabs().visibleTabsList.length, + tabSettings.filters, + tabSettings.filtersMode, + tabSettings.categoriesToInclude, + tabSettings.autoHide, + tabSettings.sortByOverride + ); + }} + tabFilters={[]} + tabMasterManager={tabMasterManager} + filtersMode="and" + categoriesToInclude={IncludeCategories.games} + autoHide={false} + sortBy={-1} + /> + ); } /** @@ -251,18 +276,19 @@ export function showModalNewTab(tabMasterManager: TabMasterManager) { * @param tabMasterManager TabMasterManager instance. */ export function showModalEditTab(tabContainer: CustomTabContainer, tabMasterManager: TabMasterManager) { - showModal( - { - tabMasterManager.updateCustomTab(tabId!, updatedTabSettings); - }} - tabId={tabContainer.id} - tabTitle={tabContainer.title} - tabFilters={tabContainer.filters} - tabMasterManager={tabMasterManager} - filtersMode={tabContainer.filtersMode} - categoriesToInclude={tabContainer.categoriesToInclude} - autoHide={tabContainer.autoHide} - /> - ); + showModal( + { + tabMasterManager.updateCustomTab(tabId!, updatedTabSettings); + }} + tabId={tabContainer.id} + tabTitle={tabContainer.title} + tabFilters={tabContainer.filters} + tabMasterManager={tabMasterManager} + filtersMode={tabContainer.filtersMode} + categoriesToInclude={tabContainer.categoriesToInclude} + autoHide={tabContainer.autoHide} + sortBy={tabContainer.sortByOverride} + /> + ); } diff --git a/src/components/modals/SortOverrideMessage.tsx b/src/components/modals/SortOverrideMessage.tsx new file mode 100644 index 0000000..c575964 --- /dev/null +++ b/src/components/modals/SortOverrideMessage.tsx @@ -0,0 +1,19 @@ +import { ConfirmModal } from 'decky-frontend-lib'; +import { VFC } from 'react'; +import { getESortByLabel } from '../../hooks/useSortingMenuItems'; + +interface SortOverrideMessageProps { + eSortBy: number; + closeModal?: () => void; +} + +/** + * The message modal to display when sort method is being overriden + */ +export const SortOverrideMessage: VFC = ({ eSortBy, closeModal }) => { + return ( + + The sorting method is overridden by TabMaster for this tab. Set 'Sort apps by' to 'default' in this tabs settings if you would like it to use library sorting. + + ); +}; diff --git a/src/components/styles/ModalStyles.tsx b/src/components/styles/ModalStyles.tsx index acdbf8d..352e272 100644 --- a/src/components/styles/ModalStyles.tsx +++ b/src/components/styles/ModalStyles.tsx @@ -103,11 +103,11 @@ export const ModalStyles: VFC<{}> = ({}) => { flex-grow: 1; } - .autohide-toggle-container .${gamepadDialogClasses.Field} { + .field-item-container .${gamepadDialogClasses.Field} { padding: 10px calc(28px + 1.4vw); } - .autohide-toggle-container .${gamepadDialogClasses.FieldLabel} { + .field-item-container .${gamepadDialogClasses.FieldLabel} { color: #8b929a; font-size: 12px; } diff --git a/src/hooks/useSortingMenuItems.tsx b/src/hooks/useSortingMenuItems.tsx new file mode 100644 index 0000000..e0a5d70 --- /dev/null +++ b/src/hooks/useSortingMenuItems.tsx @@ -0,0 +1,37 @@ +import { useMemo } from 'react'; + +/** + * Gets the localized version of a sorting method + */ +export function getESortByLabel(eSortBy: number) { + const map: { [eSortBy: number]: string; } = { + 1: "#Library_SortByAlphabetical", + 10: "#Library_SortByFriendsPlaying", + 2: "#Library_SortByPctAchievementsComplete", + 3: "#Library_SortByLastUpdated", + 4: "#Library_SortByHoursPlayed", + 5: "#Library_SortByLastPlayed", + 6: "#Library_SortByReleaseDate", + 7: "#Library_SortByAddedToLibrary", + 8: "#Library_SortBySizeOnDisk", + 9: "#Library_SortByMetacriticScore", + 11: "#Library_SortBySteamReview", + }; + return LocalizationManager.LocalizeString(map[eSortBy]); +} + +/** + * Creates the array of sorting SingleDropdownOptions + */ +function getSortingMenuItems() { + return [1, 10, 2, 4, 5, 6, 7, 8, 9, 11].map((e => ({ + data: e, + label: getESortByLabel(e)! + }))); +} + +/** + * Hook to use memoized sort options + * @param deps Dependency array to determine when to recalculate + */ +export const useSortingMenuItems = (deps: any[]) => useMemo(() => [{ label: 'Default', data: -1 }].concat(getSortingMenuItems()), deps); diff --git a/src/index.tsx b/src/index.tsx index b62c7dc..e82b4e4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -33,6 +33,7 @@ declare global { let securitystore: SecurityStore; let settingsStore: SettingsStore; let appAchievementProgressCache: AppAchievementProgressCache; + let LocalizationManager: LocalizationManager; } diff --git a/src/patches/LibraryPatch.tsx b/src/patches/LibraryPatch.tsx index cb76565..4f21d24 100644 --- a/src/patches/LibraryPatch.tsx +++ b/src/patches/LibraryPatch.tsx @@ -78,7 +78,7 @@ export const patchLibrary = (serverAPI: ServerAPI, tabMasterManager: TabMasterMa return tabs; } - const tabContentComponent = tabTemplate!.content.type as TabContentComponent; + const tabContentComponent = tabTemplate.content.type as TabContentComponent; let pacthedTabs: SteamTab[]; @@ -86,14 +86,14 @@ export const patchLibrary = (serverAPI: ServerAPI, tabMasterManager: TabMasterMa let tablist = tabMasterManager.getTabs().visibleTabsList; pacthedTabs = tablist.flatMap((tabContainer) => { if (tabContainer.filters) { - const footer = { ...(tabTemplate!.footer ?? {}), onMenuButton: getShowMenu(tabContainer.id, tabMasterManager), onMenuActionDescription: 'Tab Master' }; + const footer = { ...(tabTemplate.footer ?? {}), onMenuButton: getShowMenu(tabContainer.id, tabMasterManager), onMenuActionDescription: 'Tab Master' }; return (tabContainer as CustomTabContainer).getActualTab(tabContentComponent, sortingProps, footer, collectionsAppFilterGamepad, isMicroSDeckInstalled) || []; } else { return tabs.find(actualTab => { if (actualTab.id === tabContainer.id) { if (!actualTab.footer) actualTab.footer = {}; - actualTab.footer!.onMenuActionDescription = 'Tab Master'; - actualTab.footer!.onMenuButton = getShowMenu(tabContainer.id, tabMasterManager); + actualTab.footer.onMenuActionDescription = 'Tab Master'; + actualTab.footer.onMenuButton = getShowMenu(tabContainer.id, tabMasterManager); return true; } return false; diff --git a/src/state/TabMasterManager.tsx b/src/state/TabMasterManager.tsx index d62caa3..8229e41 100644 --- a/src/state/TabMasterManager.tsx +++ b/src/state/TabMasterManager.tsx @@ -473,17 +473,18 @@ export class TabMasterManager { * @param filtersMode The logic mode for these filters. * @param categoriesToInclude A bit field of which categories should be included in the tab. * @param autoHide Whether or not the tab should automatically be hidden if it's collection is empty. + * @param sortByOverride The eSortBy number to force use for sorting. -1 ignores override. */ - createCustomTab(title: string, position: number, filterSettingsList: TabFilterSettings[], filtersMode: LogicalMode, categoriesToInclude: number, autoHide: boolean) { + createCustomTab(title: string, position: number, filterSettingsList: TabFilterSettings[], filtersMode: LogicalMode, categoriesToInclude: number, autoHide: boolean, sortByOverride: number) { const id = uuidv4(); this.addCollectionReactionsForFilters(flattenFilters(filterSettingsList)); - this.visibleTabsList.push(this.addCustomTabContainer(id, title, position, filterSettingsList, filtersMode, categoriesToInclude, autoHide)); + this.visibleTabsList.push(this.addCustomTabContainer(id, title, position, filterSettingsList, filtersMode, categoriesToInclude, autoHide, sortByOverride)); this.updateAndSave(); } createPresetTab(presetName: Name, tabTitle: string, ...options: PresetOptions) { const { filters, filtersMode, categoriesToInclude } = getPreset(presetName, ...options); - this.createCustomTab(tabTitle, this.visibleTabsList.length, filters, filtersMode, categoriesToInclude, false); + this.createCustomTab(tabTitle, this.visibleTabsList.length, filters, filtersMode, categoriesToInclude, false, -1); } /** @@ -591,9 +592,9 @@ export class TabMasterManager { } for (const keyId in tabsSettings) { - const { id, title, filters: _filters, position, filtersMode, categoriesToInclude, autoHide } = tabsSettings[keyId]; + const { id, title, filters: _filters, position, filtersMode, categoriesToInclude, autoHide, sortByOverride } = tabsSettings[keyId]; const filters = Filter.removeUnknownTypes(_filters) - const tabContainer = filters ? this.addCustomTabContainer(id, title, position, filters, filtersMode!, categoriesToInclude!, autoHide!) : this.addDefaultTabContainer(tabsSettings[keyId]); + const tabContainer = filters ? this.addCustomTabContainer(id, title, position, filters, filtersMode!, categoriesToInclude!, autoHide!, sortByOverride) : this.addDefaultTabContainer(tabsSettings[keyId]); if (favoritesOriginalIndex !== null && favoritesOriginalIndex > -1 && tabContainer.position > favoritesOriginalIndex) { tabContainer.position--; @@ -671,7 +672,8 @@ export class TabMasterManager { filters: tabContainer.filters, filtersMode: (tabContainer as CustomTabContainer).filtersMode, categoriesToInclude: (tabContainer as CustomTabContainer).categoriesToInclude, - autoHide: (tabContainer as CustomTabContainer).autoHide + autoHide: (tabContainer as CustomTabContainer).autoHide, + sortByOverride: (tabContainer as CustomTabContainer).sortByOverride } : tabContainer; @@ -688,10 +690,11 @@ export class TabMasterManager { * @param filterSettingsList The tab's filters. * @param categoriesToInclude A bit field of which categories should be included in the tab. * @param autoHide Whether or not the tab should automatically be hidden if it's collection is empty. + * @param sortByOverride The eSortBy number to force use for sorting. -1 ignores override. * @returns A tab container for this tab. */ - private addCustomTabContainer(tabId: string, title: string, position: number, filterSettingsList: TabFilterSettings[], filtersMode: LogicalMode, categoriesToInclude: number, autoHide: boolean) { - const tabContainer = new CustomTabContainer(tabId, title, position, filterSettingsList, filtersMode, categoriesToInclude, autoHide); + private addCustomTabContainer(tabId: string, title: string, position: number, filterSettingsList: TabFilterSettings[], filtersMode: LogicalMode, categoriesToInclude: number, autoHide: boolean, sortByOverride: number = -1) { + const tabContainer = new CustomTabContainer(tabId, title, position, filterSettingsList, filtersMode, categoriesToInclude, autoHide, sortByOverride); this.tabsMap.set(tabId, tabContainer); return tabContainer; } diff --git a/src/types/SteamTypes.d.ts b/src/types/SteamTypes.d.ts index 7d6eb8a..692a4c4 100644 --- a/src/types/SteamTypes.d.ts +++ b/src/types/SteamTypes.d.ts @@ -45,7 +45,7 @@ type SteamTab = { content: React.ReactElement, footer?: { onOptionsActionDescription?: string, - onOptionsButtion?: () => any, + onOptionsButton?: () => any, onSecondaryActionDescription?: any, //Returns a reactElement onSecondaryButton?: () => any, onMenuActionDescription?: string, @@ -61,3 +61,17 @@ interface TabContentProps { setSortBy: (e: any) => void showSortingContextMenu: (e: any) => void } + +type LocalizationManager = { + AddTokens: unknown; + BLooksLikeToken: (token: string) => boolean; + GetELanguageFallbackOrder: unknown; + GetPreferredLocales: unknown; + GetTokensChangedCallbackList: unknown; + InitDirect: unknown; + InitFromObjects: unknown; + LocalizeIfToken: (token: string, suppressErrors?: boolean) => string | undefined; + LocalizeString: (token: string, suppressErrors?: boolean) => string | undefined; + LocalizeStringFromFallback: (token: string) => string | undefined; + SetPreferredLocales: unknown; +}; diff --git a/src/types/tabs.d.ts b/src/types/tabs.d.ts index f4da115..2b591f2 100644 --- a/src/types/tabs.d.ts +++ b/src/types/tabs.d.ts @@ -8,6 +8,7 @@ type TabContainer = { filtersMode?: LogicalMode //boolean operation combine filters categoriesToInclude?: number //a bit field for categories tab should include autoHide?: boolean + sortByOverride?: number //The eSortBy number to force use for sorting. -1 ignores override. } interface TabSettings extends TabContainer { }