Skip to content

Commit

Permalink
feat: add ability to individually sort tabs apps
Browse files Browse the repository at this point in the history
  • Loading branch information
jessebofill committed Jul 3, 2024
1 parent b897875 commit 0cb848b
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 65 deletions.
19 changes: 14 additions & 5 deletions src/components/CustomTabContainer.tsx
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -16,6 +17,7 @@ export class CustomTabContainer implements TabContainer {
categoriesToInclude: number;
autoHide: boolean;
dependsOnMicroSDeck: boolean;
sortByOverride: number;

/**
* Creates a new CustomTabContainer.
Expand All @@ -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<FilterType>[], filtersMode: LogicalMode, categoriesToInclude: number, autoHide: boolean) {
constructor(id: string, title: string, position: number, filterSettingsList: TabFilterSettings<FilterType>[], filtersMode: LogicalMode, categoriesToInclude: number, autoHide: boolean, sortByOverride: number) {
this.id = id;
this.title = title;
this.position = position;
Expand All @@ -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 = {
Expand All @@ -57,17 +61,21 @@ export class CustomTabContainer implements TabContainer {
this.checkMicroSDeckDependency();
}

getActualTab(TabContentComponent: TabContentComponent, sortingProps: Omit<TabContentProps, 'collection'>, footer: SteamTab['footer'], collectionAppFilter: any, isMicroSDeckInstalled: boolean): SteamTab | null {
getActualTab(TabContentComponent: TabContentComponent, sortingProps: Omit<TabContentProps, 'collection'>, 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(<SortOverrideMessage eSortBy={this.sortByOverride} />);
if (this.sortByOverride !== -1) footer.onOptionsButton = showSortOverride;

return {
title: this.title,
id: this.id,
footer: footer,
content: <TabContentComponent
collection={this.collection}
{...sortingProps}
setSortBy={sortingProps.setSortBy}
eSortBy={this.sortByOverride === -1 ? sortingProps.eSortBy : this.sortByOverride}
showSortingContextMenu={this.sortByOverride === -1 ? sortingProps.showSortingContextMenu : showSortOverride}
/>,
renderTabAddon: () => {
return <span className={gamepadTabbedPageClasses.TabCount}>
Expand Down Expand Up @@ -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();
}
Expand Down
116 changes: 71 additions & 45 deletions src/components/modals/EditTabModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
ConfirmModal,
DialogCheckbox,
DropdownItem,
Field,
Focusable,
SingleDropdownOption,
TextField,
ToggleField,
afterPatch,
Expand All @@ -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<Required<TabSettings>, 'position' | 'id'>;

Expand All @@ -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<EditTabModalProps> = ({ closeModal, onConfirm, tabId, tabTitle, tabFilters, tabMasterManager, filtersMode, categoriesToInclude, autoHide: _autoHide }) => {
export const EditTabModal: VFC<EditTabModalProps> = ({ closeModal, onConfirm, tabId, tabTitle, tabFilters, tabMasterManager, filtersMode, categoriesToInclude, autoHide: _autoHide, sortBy }) => {
const [name, setName] = useState<string>(tabTitle ?? '');
const [topLevelFilters, setTopLevelFilters] = useState<TabFilterSettings<FilterType>[]>(tabFilters);
const [topLevelLogicMode, setTopLevelLogicMode] = useState<LogicalMode>(filtersMode);
Expand All @@ -46,6 +51,8 @@ export const EditTabModal: VFC<EditTabModalProps> = ({ closeModal, onConfirm, ta
const [canAddFilter, setCanAddFilter] = useState<boolean>(true);
const [patchInput, setPatchInput] = useState<boolean>(true);
const [autoHide, setAutoHide] = useState<boolean>(_autoHide);
const [sortByOverride, setSortByOverride] = useState(sortBy);
const sortOptions: SingleDropdownOption[] = useSortingMenuItems([]);

const nameInputElement = <TextField value={name} placeholder="The title for this tab" onChange={onNameChange} />;

Expand Down Expand Up @@ -84,12 +91,13 @@ export const EditTabModal: VFC<EditTabModalProps> = ({ 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");
}
}
Expand Down Expand Up @@ -118,17 +126,24 @@ export const EditTabModal: VFC<EditTabModalProps> = ({ closeModal, onConfirm, ta
>
<div style={{ padding: "4px 16px 1px" }} className="name-field">
<Field description={
<>
<div style={{ paddingBottom: "6px" }} className={quickAccessControlsClasses.PanelSectionTitle}>
Name
</div>
{nameInputElement}
</>
<>
<div style={{ paddingBottom: "6px" }} className={quickAccessControlsClasses.PanelSectionTitle}>
Name
</div>
{nameInputElement}
</>
} />
</div>
<IncludeCategoriesPanel categoriesToInclude={catsToInclude} setCategoriesToInclude={setCatsToInclude} />
<div className='autohide-toggle-container'>
<ToggleField label='Automatically hide tab if empty' checked={autoHide} onChange={checked => setAutoHide(checked)} bottomSeparator='thick'/>
<div className='field-item-container'>
<ToggleField label='Automatically hide tab if empty' checked={autoHide} onChange={checked => setAutoHide(checked)} bottomSeparator='thick' />
<DropdownItem
label='Sort apps by'
rgOptions={sortOptions}
selectedOption={sortByOverride}
onChange={option => setSortByOverride(option.data)}
bottomSeparator='thick'
/>
</div>
<FiltersPanel
groupFilters={topLevelFilters}
Expand Down Expand Up @@ -159,9 +174,9 @@ const IncludeCategoriesPanel: VFC<IncludeCategoriesPanelProps> = ({ 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];

Expand All @@ -186,8 +201,8 @@ const IncludeCategoriesPanel: VFC<IncludeCategoriesPanelProps> = ({ categoriesTo
<div style={{ padding: "12px 0", float: "left" }} className={quickAccessControlsClasses.PanelSectionTitle}>
Include in tab
</div>
<div style={{padding: "12px 40px", flex: "1"}}>
{!isOpen && <span style={{ fontSize: "12px", lineHeight: "12px", color: "#8b929a"}}>
<div style={{ padding: "12px 40px", flex: "1" }}>
{!isOpen && <span style={{ fontSize: "12px", lineHeight: "12px", color: "#8b929a" }}>
{catStrings.join(', ')}
</span>}
</div>
Expand All @@ -205,12 +220,12 @@ const IncludeCategoriesPanel: VFC<IncludeCategoriesPanelProps> = ({ categoriesTo
{isOpen && (
<div style={{ padding: "10px 18px" }}>
{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 : <DialogCheckbox checked={shouldInclude} onChange={onChange} label={label} />;
})}
</div>)}
Expand All @@ -220,7 +235,8 @@ const IncludeCategoriesPanel: VFC<IncludeCategoriesPanelProps> = ({ categoriesTo
left: "calc(16px - 1.8vw)",
right: "calc(16px - 1.8vw)",
height: "1px",
background: "#ffffff1a" }}
background: "#ffffff1a"
}}
/>
</>
);
Expand All @@ -231,18 +247,27 @@ const IncludeCategoriesPanel: VFC<IncludeCategoriesPanelProps> = ({ categoriesTo
* @param tabMasterManager TabMasterManager instance.
*/
export function showModalNewTab(tabMasterManager: TabMasterManager) {
showModal(
<EditTabModal
onConfirm={(_: any, tabSettings: EditableTabSettings) => {
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(
<EditTabModal
onConfirm={(_: any, tabSettings: EditableTabSettings) => {
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}
/>
);
}

/**
Expand All @@ -251,18 +276,19 @@ export function showModalNewTab(tabMasterManager: TabMasterManager) {
* @param tabMasterManager TabMasterManager instance.
*/
export function showModalEditTab(tabContainer: CustomTabContainer, tabMasterManager: TabMasterManager) {
showModal(
<EditTabModal
onConfirm={(tabId: string | undefined, updatedTabSettings: EditableTabSettings) => {
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(
<EditTabModal
onConfirm={(tabId: string | undefined, updatedTabSettings: EditableTabSettings) => {
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}
/>
);
}
19 changes: 19 additions & 0 deletions src/components/modals/SortOverrideMessage.tsx
Original file line number Diff line number Diff line change
@@ -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<SortOverrideMessageProps> = ({ eSortBy, closeModal }) => {
return (
<ConfirmModal strTitle={`Sort By: ${getESortByLabel(eSortBy)}`} bAlertDialog={true} closeModal={closeModal}>
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.
</ConfirmModal>
);
};
4 changes: 2 additions & 2 deletions src/components/styles/ModalStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
37 changes: 37 additions & 0 deletions src/hooks/useSortingMenuItems.tsx
Original file line number Diff line number Diff line change
@@ -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);
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ declare global {
let securitystore: SecurityStore;
let settingsStore: SettingsStore;
let appAchievementProgressCache: AppAchievementProgressCache;
let LocalizationManager: LocalizationManager;
}


Expand Down
8 changes: 4 additions & 4 deletions src/patches/LibraryPatch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,22 @@ 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[];

if (tabMasterManager.hasSettingsLoaded) {
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;
Expand Down
Loading

0 comments on commit 0cb848b

Please sign in to comment.