diff --git a/src/components/CustomTabContainer.tsx b/src/components/CustomTabContainer.tsx index 979b850..df0e493 100644 --- a/src/components/CustomTabContainer.tsx +++ b/src/components/CustomTabContainer.tsx @@ -14,6 +14,7 @@ export class CustomTabContainer implements TabContainer { collection: Collection; filtersMode: LogicalMode; categoriesToInclude: number; + dependsOnMicroSDeck: boolean; /** * Creates a new CustomTabContainer. @@ -31,6 +32,7 @@ export class CustomTabContainer implements TabContainer { this.filters = filterSettingsList; this.filtersMode = filtersMode; this.categoriesToInclude = categoriesToInclude; + this.dependsOnMicroSDeck = false; //@ts-ignore this.collection = { @@ -47,6 +49,7 @@ export class CustomTabContainer implements TabContainer { }; this.buildCollection(); + this.checkMicroSDeckDependency(); } getActualTab(TabContentComponent: TabContentComponent, sortingProps: Omit, footer: SteamTab['footer'], collectionAppFilter: any): SteamTab { @@ -110,6 +113,14 @@ export class CustomTabContainer implements TabContainer { this.categoriesToInclude = categoriesToInclude; this.filters = filters; this.buildCollection(); + this.checkMicroSDeckDependency; + } + + /** + * Checks and sets whether or not the tab has filters that depend on MicroSDeck plugin. + */ + checkMicroSDeckDependency() { + this.dependsOnMicroSDeck = this.containsFilterType('sd card'); } /** diff --git a/src/components/filters/FilterSelect.tsx b/src/components/filters/FilterSelect.tsx index 63a2359..93abc2f 100644 --- a/src/components/filters/FilterSelect.tsx +++ b/src/components/filters/FilterSelect.tsx @@ -34,8 +34,7 @@ const FilterSelectModal: VFC = ({ selectedOption, onSele "last played": "Selects apps based on when they were last played.", demo: "Selects apps that are/aren't demos.", streamable: "Selects apps that can/can't be streamed from another computer.", - "current card": "Selects apps that are present on the current MicroSD Card", - "on card": "Selects apps that are present on a given MicroSD Card" + "sd card": "Selects apps that are present on the inserted/ specific MicroSD Card", } useEffect(() => {setTimeout(() => setFocusable(true), 10)}, []); diff --git a/src/components/filters/Filters.ts b/src/components/filters/Filters.ts index a978749..ddd04be 100644 --- a/src/components/filters/Filters.ts +++ b/src/components/filters/Filters.ts @@ -1,7 +1,7 @@ import { PluginController } from "../../lib/controllers/PluginController"; import { DateIncludes, DateObj } from '../generic/DatePickers'; -export type FilterType = 'collection' | 'installed' | 'regex' | 'friends' | 'tags' | 'whitelist' | 'blacklist' | 'merge' | 'platform' | 'deck compatibility' | 'review score' | 'time played' | 'size on disk' | 'release date' | 'last played' | 'demo' | 'streamable' | 'current card' | 'on card'; +export type FilterType = 'collection' | 'installed' | 'regex' | 'friends' | 'tags' | 'whitelist' | 'blacklist' | 'merge' | 'platform' | 'deck compatibility' | 'review score' | 'time played' | 'size on disk' | 'release date' | 'last played' | 'demo' | 'streamable' | 'sd card'; export type TimeUnit = 'minutes' | 'hours' | 'days'; export type ThresholdCondition = 'above' | 'below'; @@ -33,9 +33,8 @@ type SizeOnDiskFilterParams = { gbThreshold: number, condition: ThresholdConditi type ReleaseDateFilterParams = { date?: DateObj, daysAgo?: number, condition: ThresholdCondition; }; type LastPlayedFilterParams = { date?: DateObj, daysAgo?: number, condition: ThresholdCondition; }; type DemoFilterParams = { isDemo: boolean; }; -type StreamableFilterParams = { isStreamable: boolean; } -type CurrentCardParams = {} -type OnCardParams = { cardId: string } +type StreamableFilterParams = { isStreamable: boolean; }; +type SdCardParams = { cardId: string } //use 'inserted' for currently inserted card export type FilterParams = T extends 'collection' ? CollectionFilterParams : @@ -55,8 +54,7 @@ export type FilterParams = T extends 'last played' ? LastPlayedFilterParams : T extends 'demo' ? DemoFilterParams : T extends 'streamable' ? StreamableFilterParams : - T extends 'current card' ? CurrentCardParams : - T extends 'on card' ? OnCardParams : + T extends 'sd card' ? SdCardParams : never; export type TabFilterSettings = { @@ -89,8 +87,7 @@ export const FilterDefaultParams: { [key in FilterType]: FilterParams } = { "last played": { date: undefined, condition: 'above' }, "demo": { isDemo: true }, "streamable": { isStreamable: true }, - "current card": {}, - "on card": { cardId: "" }, + "sd card": { cardId: 'inserted' }, }; @@ -107,6 +104,7 @@ export function canBeInverted(filter: TabFilterSettings): boolean { case "tags": case "merge": case "deck compatibility": + case "sd card": return true; case "platform": case "installed": @@ -119,8 +117,6 @@ export function canBeInverted(filter: TabFilterSettings): boolean { case "last played": case "demo": case "streamable": - case "current card": - case "on card": return false; } } @@ -158,8 +154,7 @@ export function isValidParams(filter: TabFilterSettings): boolean { case "size on disk": case "demo": case "streamable": - case "current card": - case "on card": + case "sd card": return true; } } @@ -265,29 +260,21 @@ export function validateFilter(filter: TabFilterSettings): Validatio mergeErrorEntries: mergeErrorEntries }; } - case "on card": { - const cardFilter = filter as TabFilterSettings<'on card'>; + case "sd card": { + const cardFilter = filter as TabFilterSettings<'sd card'>; - const cardsAndGames = MicroSDeck?.CardsAndGames; + let passed = true; + if (PluginController.microSDeckInstalled) { + const cardsAndGames = MicroSDeck?.CardsAndGames; - if(!(cardsAndGames?.length)) { - return { - passed: false, - errors: ["No Cards avaliable"] - } - } - - let passed = false; - for(let [card] of cardsAndGames) { - if(cardFilter.params.cardId == card.uid) { - passed = true; + if (!cardsAndGames?.find(([card]) => cardFilter.params.cardId === card.uid)) { + passed = false; } } - return { passed, - errors: passed ? [] : ["Couldn't find the selected card in the list of current cards."] - } + errors: passed ? [] : ["Couldn't find the selected card in the list of known cards."] + }; } case "regex": case "friends": @@ -304,7 +291,6 @@ export function validateFilter(filter: TabFilterSettings): Validatio case "last played": case "demo": case "streamable": - case "current card": return { passed: true, errors: [] @@ -446,21 +432,10 @@ export class Filter { const isStreamable = appOverview.per_client_data.some((clientData) => clientData.client_name !== "This machine" && clientData.installed); return params.isStreamable ? isStreamable : !isStreamable; }, - //@ts-ignore params is unused - 'current card': (params: FilterParams<'current card'>, appOverview: SteamAppOverview) => { - const currentCardAndGames = MicroSDeck?.CurrentCardAndGames; - - if(!currentCardAndGames) return false; - - const [_, games] = currentCardAndGames; - - return !!games.find((game) => +game.uid == appOverview.appid); - }, - 'on card': (params: FilterParams<'on card'>, appOverview: SteamAppOverview) => { - const cardsAndGames = MicroSDeck?.CardsAndGames; - const card = cardsAndGames?.find(([card]) => card.uid == params.cardId); + 'sd card': (params: FilterParams<'sd card'>, appOverview: SteamAppOverview) => { + const card = params.cardId === 'inserted' ? MicroSDeck?.CurrentCardAndGames : MicroSDeck?.CardsAndGames?.find(([card]) => card.uid == params.cardId); - if(!card)return false; + if (!card) return false; return !!card[1].find((game) => +game.uid == appOverview.appid); }, diff --git a/src/index.tsx b/src/index.tsx index c420960..c5e0070 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -39,8 +39,10 @@ import { DocPage } from "./components/docs/DocsPage"; import { IncludeCategories } from "./lib/Utils"; import { PresetMenu } from './components/menus/PresetMenu'; import { MicroSDeckManager } from "@cebbinghaus/microsdeck"; +import { MicroSDeckInterop } from './lib/controllers/MicroSDeckInterop'; declare global { + let DeckyPluginLoader: { pluginReloadQueue: { name: string; version?: string; }[]; }; var MicroSDeck: MicroSDeckManager | undefined; var SteamClient: SteamClient; let collectionStore: CollectionStore; @@ -65,6 +67,9 @@ interface TabEntryInteractablesProps { const Content: VFC<{}> = ({ }) => { const { visibleTabsList, hiddenTabsList, tabsMap, tabMasterManager } = useTabMasterContext(); + // MicroSDeckInterop.checkInstallStateChanged(); + // const isMicroSDeckInstalled = MicroSDeckInterop.state === 'good'; + function TabEntryInteractables({ entry }: TabEntryInteractablesProps) { const tabContainer = tabsMap.get(entry.data!.id)!; return (); @@ -235,9 +240,7 @@ export default definePlugin((serverAPI: ServerAPI) => { PythonInterop.setServer(serverAPI); - const microSDeckManager = window.MicroSDeck = (window.MicroSDeck || new MicroSDeckManager({url: "http://localhost:12412"})); - - const tabMasterManager = new TabMasterManager(microSDeckManager); + const tabMasterManager = new TabMasterManager(); PluginController.setup(serverAPI, tabMasterManager); const loginUnregisterer = PluginController.initOnLogin(async () => { diff --git a/src/lib/Utils.ts b/src/lib/Utils.ts index 930fcf1..9c95472 100644 --- a/src/lib/Utils.ts +++ b/src/lib/Utils.ts @@ -210,7 +210,7 @@ export function debounce(func:Function, wait:number, immediate?:boolean) { /** * Recursive function that checks whether an array of TabFilterSettings contains any filters of a specified type. * @param filters The array of TabFilterSettings to check in. - * @param filterType The filter types to check are included. + * @param filterTypes The filter types to check are included. * @returns Boolean */ export function filtersHaveType(filters: TabFilterSettings[], ...filterTypes: FilterType[] ) { diff --git a/src/lib/controllers/MicroSDeckInterop.ts b/src/lib/controllers/MicroSDeckInterop.ts new file mode 100644 index 0000000..b45c250 --- /dev/null +++ b/src/lib/controllers/MicroSDeckInterop.ts @@ -0,0 +1,64 @@ +import { sleep } from 'decky-frontend-lib'; +import { LogController } from './LogController'; +import { MicroSDeckManager } from '@cebbinghaus/microsdeck'; + +export class MicroSDeckInterop { + public static state: 'not installed' | 'version too low' | 'version too high' | 'good' = 'not installed'; + public static ref: MicroSDeckManager | undefined; + + /** + * Checks if MicroSDeck plugin is installed when loading + */ + //* this is not complete, i have to change it + static async checkInstallStateOnLoad() { + //* add version match verification here + LogController.log("Checking for installation of MicroSDeck..."); + //MicroSDeck is already loaded + if (MicroSDeck) { + LogController.log("MicroSDeck is installed"); + return true; + } else { + //MicroSDeck is in queue to be loaded, wait til it's removed (starts loading) + while (!!DeckyPluginLoader.pluginReloadQueue.find(plugin => plugin.name === 'MicroSDeck')) { + await sleep(200); + } + + //MicroSDeck has either started loading or is not installed at all, wait a little longer to allow it to load. + let tries = 0; + while (!MicroSDeck) { + tries++; + if (tries > 10) { + LogController.log("Could not find MicroSDeck installation"); + return false; // if MicroSDeck isn't found after number of attempts, give up + } + await sleep(100); + } + + LogController.log("MicroSDeck is installed"); + return true; + } + } + + + static checkInstallStateChanged() { + if (!MicroSDeck) { + this.ref = undefined; + this.state = 'not installed'; + } else { + //* window.MicroSDeck = undefined needs to be added back to plugin's onDismount or fire an event there + + + //MicroSDeck has been reinstalled or reloaded + if (MicroSDeck !== this.ref) { + this.ref = MicroSDeck; + + //* resub to new event bus + } + //* check version + } + } + + static checkVersion() { + + } +} diff --git a/src/lib/controllers/PluginController.tsx b/src/lib/controllers/PluginController.tsx index 1b8efec..55eeaed 100644 --- a/src/lib/controllers/PluginController.tsx +++ b/src/lib/controllers/PluginController.tsx @@ -1,4 +1,4 @@ -import { ConfirmModal, ServerAPI, showModal } from "decky-frontend-lib"; +import { ConfirmModal, ServerAPI, showModal, sleep } from "decky-frontend-lib"; import { PythonInterop } from "./PythonInterop"; import { SteamController } from "./SteamController"; import { LogController } from "./LogController"; @@ -45,6 +45,7 @@ export class PluginController { private static tabMasterManager: TabMasterManager; private static steamController: SteamController; + public static microSDeckInstalled: boolean = false; /** * Sets the plugin's serverAPI. @@ -65,6 +66,7 @@ export class PluginController { LogController.log(`User logged in. [DEBUG] username: ${username}.`); if (await this.steamController.waitForServicesToInitialize()) { await PluginController.init(); + this.microSDeckInstalled = await PluginController.isMicroSDeckInstalledOnLoad(); onMount(); } else { PythonInterop.toast("Error", "Failed to initialize, try restarting."); @@ -124,4 +126,37 @@ export class PluginController { static onWakeFromSleep() { this.tabMasterManager.buildTimeBasedFilterTabs(); } + + /** + * Checks if MicroSDeck plugin is installed when loading + */ + //* moving to MicroSDeckInterop + static async isMicroSDeckInstalledOnLoad() { + //* add version match verification here + LogController.log("Checking for installation of MicroSDeck..."); + //MicroSDeck is already loaded + if (MicroSDeck) { + LogController.log("MicroSDeck is installed"); + return true; + } else { + //MicroSDeck is in queue to be loaded, wait til it's removed (starts loading) + while (!!DeckyPluginLoader.pluginReloadQueue.find(plugin => plugin.name === 'MicroSDeck')) { + await sleep(200); + } + + //MicroSDeck has either started loading or is not installed at all, wait a little longer to allow it to load. + let tries = 0; + while (!MicroSDeck) { + tries++; + if (tries > 10) { + LogController.log("Could not find MicroSDeck installation"); + return false; // if MicroSDeck isn't found after number of attempts, give up + } + await sleep(100); + } + + LogController.log("MicroSDeck is installed"); + return true; + } + } } diff --git a/src/patches/LibraryPatch.tsx b/src/patches/LibraryPatch.tsx index dc10bd8..cea0821 100644 --- a/src/patches/LibraryPatch.tsx +++ b/src/patches/LibraryPatch.tsx @@ -12,6 +12,7 @@ import { TabMasterManager } from "../state/TabMasterManager"; import { CustomTabContainer } from "../components/CustomTabContainer"; import { LogController } from "../lib/controllers/LogController"; import { LibraryMenu } from '../components/menus/LibraryMenu'; +import { MicroSDeckInterop } from '../lib/controllers/MicroSDeckInterop'; /** * Patches the Steam library to allow the plugin to change the tabs. @@ -37,6 +38,8 @@ export const patchLibrary = (serverAPI: ServerAPI, tabMasterManager: TabMasterMa return innerPatch.unpatch(); }); + //MicroSDeckInterop.checkInstallStateChanged(); + //* This patch always runs twice afterPatch(ret1, "type", (_: Record[], ret2: ReactElement) => { if (!ret2?.type) { @@ -84,6 +87,9 @@ export const patchLibrary = (serverAPI: ServerAPI, tabMasterManager: TabMasterMa pacthedTabs = tablist.flatMap((tabContainer) => { if (tabContainer.filters) { const footer = { ...(tabTemplate!.footer ?? {}), onMenuButton: getShowMenu(tabContainer.id, tabMasterManager), onMenuActionDescription: 'Tab Master' }; + + //if MicroSDeck isn't installed don't display any tabs that depend on it; return empty array for flat map + //if (!MicroSDeckInterop.state !== 'good' && (tabContainer as CustomTabContainer).dependsOnMicroSDeck) return []; return (tabContainer as CustomTabContainer).getActualTab(tabContentComponent, sortingProps, footer, collectionsAppFilterGamepad); } else { return tabs.find(actualTab => { diff --git a/src/state/TabMasterManager.tsx b/src/state/TabMasterManager.tsx index 2989e8f..817d646 100644 --- a/src/state/TabMasterManager.tsx +++ b/src/state/TabMasterManager.tsx @@ -9,7 +9,6 @@ import { LogController } from "../lib/controllers/LogController"; import { showModal } from "decky-frontend-lib"; import { FixTabErrorsModalRoot } from "../components/modals/FixTabErrorsModal"; import { PresetName, PresetOptions, getPreset } from '../presets/presets'; -import { MicroSDeckManager } from "@cebbinghaus/microsdeck"; /** * Converts a list of filters into a 1D array. @@ -51,7 +50,7 @@ export class TabMasterManager { public eventBus = new EventTarget(); - public readonly microSDeck: MicroSDeckManager; + public microSDeckInstalled: boolean = false; private allGamesReaction: IReactionDisposer | undefined; private favoriteReaction: IReactionDisposer | undefined; @@ -70,9 +69,8 @@ export class TabMasterManager { /** * Creates a new TabMasterManager. */ - constructor(microSDeck: MicroSDeckManager) { + constructor() { this.hasLoaded = false; - this.microSDeck = microSDeck; this.tabsMap = new Map(); } @@ -163,10 +161,6 @@ export class TabMasterManager { private async rebuildCustomTabsOnCollectionChange() { if (!this.hasLoaded) return; - if(MicroSDeck?.Enabled) { - await MicroSDeck.fetchCurrent(); - } - this.visibleTabsList.forEach((tabContainer) => { if (tabContainer.filters && tabContainer.filters.length !== 0) { (tabContainer as CustomTabContainer).buildCollection();