diff --git a/Contributing.md b/Contributing.md index fad6da4..f6aa660 100644 --- a/Contributing.md +++ b/Contributing.md @@ -43,3 +43,7 @@ Here's what you need to know: - docs - changes to the documentation - refactor - refactoring - when writing a good commit message, keep it short. You can always add details in the commit description + +# Pull Requests + +PRs should follow commitlint conventions as well. If there is one feature or bug fix, the title should be about that. If there are multiple, it should summarize them. diff --git a/assets/filters/docs_achievements-example.png b/assets/filters/docs_achievements-example.png new file mode 100644 index 0000000..79d7944 Binary files /dev/null and b/assets/filters/docs_achievements-example.png differ diff --git a/assets/filters/docs_size-on-disk-example.png b/assets/filters/docs_size-on-disk-example.png index 71296d8..701c6be 100644 Binary files a/assets/filters/docs_size-on-disk-example.png and b/assets/filters/docs_size-on-disk-example.png differ diff --git a/deploy.sh b/deploy.sh index fb39c2b..4d70e93 100644 --- a/deploy.sh +++ b/deploy.sh @@ -60,8 +60,4 @@ done echo "[TASK]: Copying frontend..." scpDirRecursive "./dist" "$deck_home_dir/dist" -#? Copy default files -echo "[TASK]: Copying defaults..." -scpDirRecursive "./defaults" "$deck_home_dir" - -echo "[DONE]" \ No newline at end of file +echo "[DONE]" diff --git a/main.py b/main.py index dbf19a6..6d276f5 100644 --- a/main.py +++ b/main.py @@ -22,9 +22,6 @@ class Plugin: users_dict: dict[str, dict] = None tags: list[dict] = None - docsDirPath = f"{decky_plugin.DECKY_PLUGIN_DIR}/docs" - docs = {} - settings: SettingsManager async def logMessage(self, message, level): @@ -183,14 +180,6 @@ async def set_tab_profiles(self, tab_profiles: dict[str, list[str]]): Plugin.users_dict[Plugin.user_id]["tabProfiles"] = tab_profiles await Plugin.set_setting(self, "usersDict", Plugin.users_dict) - async def get_docs(self): - for docsFileName in os.listdir(self.docsDirPath): - with open(os.path.join(self.docsDirPath, docsFileName), 'r') as docFile: - docName = docsFileName.replace("_", " ").replace(".md", "") - self.docs[docName] = "".join(docFile.readlines()) - - return self.docs - async def read(self) -> None: """ Reads the json from disk diff --git a/package.json b/package.json index bfd5132..2f3f96e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tabmaster", - "version": "2.4.1", + "version": "2.5.0", "description": "Gives you full control over your Steam library! Support for customizing, adding, and hiding Library Tabs.", "scripts": { "build": "shx rm -rf dist && rollup -c", @@ -55,13 +55,14 @@ "husky": "^8.0.3", "markdown-it": "^13.0.1", "rollup": "^2.79.1", + "rollup-plugin-codegen": "^1.0.0", "rollup-plugin-import-assets": "^1.1.1", "shx": "^0.3.4", "tslib": "^2.6.1", "typescript": "^4.9.5" }, "dependencies": { - "@cebbinghaus/microsdeck": "0.9.8-8cc660c", + "@cebbinghaus/microsdeck": "0.10.0-edd7525", "mobx": "^6.12.0", "react-icons": "^4.12.0", "react-virtualized-auto-sizer": "^1.0.20", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b2e89b..0f36d3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,13 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@cebbinghaus/microsdeck': - specifier: 0.9.8-8cc660c - version: 0.9.8-8cc660c + specifier: 0.10.0-edd7525 + version: 0.10.0-edd7525 mobx: specifier: ^6.12.0 version: 6.12.0 @@ -72,6 +76,9 @@ devDependencies: rollup: specifier: ^2.79.1 version: 2.79.1 + rollup-plugin-codegen: + specifier: ^1.0.0 + version: 1.0.0 rollup-plugin-import-assets: specifier: ^1.1.1 version: 1.1.1(rollup@2.79.1) @@ -115,8 +122,8 @@ packages: regenerator-runtime: 0.14.0 dev: false - /@cebbinghaus/microsdeck@0.9.8-8cc660c: - resolution: {integrity: sha512-rfxlRRVF8fTIGxUQf81KT5NUwjfz/ofvyaGsctOGE4LVUF4javAlqIVnPiNgmSAjH/C7mrIE5SUkWNT/ve2bhw==} + /@cebbinghaus/microsdeck@0.10.0-edd7525: + resolution: {integrity: sha512-QPB37kOIz9xU7M00Q0Ee5nkbT5V6Z1eFh55UxR+esWnlzJqViDmC0rJ7d7PpZtnd+s/6CCvGPd1v4zO0oUZ4OA==} dependencies: semver: 7.5.4 dev: false @@ -1816,6 +1823,11 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /rollup-plugin-codegen@1.0.0: + resolution: {integrity: sha512-dDGSP/I/y6BemGaOZoeUZVfjFi6Ky9uMGuFurJVZV08s0Q11qXLPTsMqvy1zmxPJptH2yV9PV7ruBwxQc03TmA==} + engines: {node: '>=14.0'} + dev: true + /rollup-plugin-import-assets@1.1.1(rollup@2.79.1): resolution: {integrity: sha512-u5zJwOjguTf2N+wETq2weNKGvNkuVc1UX/fPgg215p5xPvGOaI6/BTc024E9brvFjSQTfIYqgvwogQdipknu1g==} peerDependencies: @@ -2299,7 +2311,3 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/rollup.config.js b/rollup.config.js index 398979f..42c18ea 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -5,6 +5,7 @@ import replace from '@rollup/plugin-replace'; import typescript from '@rollup/plugin-typescript'; import { defineConfig } from 'rollup'; import importAssets from 'rollup-plugin-import-assets'; +import codegen from 'rollup-plugin-codegen'; import { name } from "./plugin.json"; @@ -14,8 +15,9 @@ const production = process.env.NODE_ENV !== 'development'; export default defineConfig({ input: './src/index.tsx', plugins: [ - commonjs(), nodeResolve({ preferBuiltins: false, browser: true }), + codegen(), + commonjs(), typescript({ sourceMap: !production, inlineSources: !production }), json(), replace({ diff --git a/src/components/MicroSDeckNotice.tsx b/src/components/MicroSDeckNotice.tsx index e61339e..759c0e7 100644 --- a/src/components/MicroSDeckNotice.tsx +++ b/src/components/MicroSDeckNotice.tsx @@ -13,17 +13,17 @@ export const MicroSDeckNotice: VFC = ({ intallState, plug let recommendation = ''; switch (intallState) { - case MicroSDeckInstallState['ver too low']: - case MicroSDeckInstallState['ver too high']: + case MicroSDeckInstallState.VERSION_TOO_LOW: + case MicroSDeckInstallState.VERSION_TOO_HIGH: problem = `a version mismatch was detected. TabMaster expects version ${libVersion}, but version ${pluginVersion} is installed.` - recommendation = intallState === MicroSDeckInstallState['ver too low'] ? 'Please update MicroSDeck to specified version.' : 'Please update TabMaster if available or install specified version of MicroSDeck.' + recommendation = intallState === MicroSDeckInstallState.VERSION_TOO_LOW ? 'Please update MicroSDeck to specified version.' : 'Please update TabMaster if available or install specified version of MicroSDeck.' break - case MicroSDeckInstallState['ver unknown']: + case MicroSDeckInstallState.VERSION_UNKOWN: problem = `TabMaster couldn't correctly determine which version it expects or which version is installed.`; recommendation = 'Please try updating TabMaster or MicroSDeck.'; break - case MicroSDeckInstallState['not installed']: + case MicroSDeckInstallState.NOT_INSTALLED: problem = 'it is not installed.'; recommendation = 'Please install MicroSDeck for these tabs to work.'; } diff --git a/src/components/QuickAccessContent.tsx b/src/components/QuickAccessContent.tsx index 12d0838..aa8bbc5 100644 --- a/src/components/QuickAccessContent.tsx +++ b/src/components/QuickAccessContent.tsx @@ -46,7 +46,7 @@ export const QuickAccessContent: VFC<{}> = ({ }) => { const { visibleTabsList, hiddenTabsList, tabsMap, tabMasterManager } = useTabMasterContext(); const microSDeckInstallState = MicroSDeckInterop.getInstallState(); - const isMicroSDeckInstalled = microSDeckInstallState === MicroSDeckInstallState['good']; + const isMicroSDeckInstalled = microSDeckInstallState === MicroSDeckInstallState.VERSION_COMPATIBLE; const hasSdTabs = !!visibleTabsList.find(tabContainer => (tabContainer as CustomTabContainer).dependsOnMicroSDeck); function TabEntryInteractables({ entry }: TabEntryInteractablesProps) { @@ -144,11 +144,8 @@ export const QuickAccessContent: VFC<{}> = ({ }) => { { hiddenTabsList.map(tabContainer =>
- } - onClick={() => tabMasterManager.showTab(tabContainer.id)} - onOKActionDescription="Unhide tab" - > + {/* @ts-ignore */} + } onClick={() => tabMasterManager.showTab(tabContainer.id)} onOKActionDescription="Unhide tab"> Show
diff --git a/src/components/docs/DocsRouter.tsx b/src/components/docs/DocsRouter.tsx index 7ec358b..17135d6 100644 --- a/src/components/docs/DocsRouter.tsx +++ b/src/components/docs/DocsRouter.tsx @@ -4,6 +4,9 @@ import { VFC, ReactNode } from "react"; import { MdNumbers } from "react-icons/md"; import { DocPage } from "./DocsPage"; +//@ts-ignore +import docs from "./docs.codegen"; + type DocRouteEntry = { title: string, content: ReactNode, @@ -16,16 +19,16 @@ type DocRoutes = { [pageName: string]: DocRouteEntry; }; -type DocsRouterProps = { - docs: DocPages; -}; /** * The documentation pages router for TabMaster. */ -export const DocsRouter: VFC = ({ docs }) => { +export const DocsRouter: VFC = () => { const docPages: DocRoutes = {}; + Object.entries(docs).map(([pageName, doc]) => { + pageName = pageName.replace(/_/g, " "); + docPages[pageName] = { title: pageName, content: , diff --git a/src/components/docs/docs.codegen b/src/components/docs/docs.codegen new file mode 100644 index 0000000..0824570 --- /dev/null +++ b/src/components/docs/docs.codegen @@ -0,0 +1,15 @@ +// @ts-nocheck +const { readdirSync, readFileSync, fil } = require('fs'); +const { parse, join } = require('path'); + +const docsDir = join(__dirname, './plugin-docs'); + +const docFiles = readdirSync(docsDir).filter(file => file.endsWith(".md")); +const docs = {}; +docFiles.forEach(docFile => { + docs[parse(docFile).name] = readFileSync(join(docsDir, docFile), {encoding: "utf-8"}); +}); + +module.exports = function () { + return `export default ${JSON.stringify(docs)}`; +}; diff --git a/defaults/docs/Filters.md b/src/components/docs/plugin-docs/Filters.md similarity index 94% rename from defaults/docs/Filters.md rename to src/components/docs/plugin-docs/Filters.md index 2f992e1..d9fe404 100644 --- a/defaults/docs/Filters.md +++ b/src/components/docs/plugin-docs/Filters.md @@ -24,6 +24,7 @@ - Demo - Streamable - Steam Features + - Achievements - MicroSD Card (Requires MicroSDeck)
@@ -289,6 +290,19 @@ Filters apps based on if they can be streamed or not.
+#### Achievements +**Options:**
+`percentage` - The desired achievement percentage completion of apps to include. +`greater/less` - Whether to include apps that have an achievement completion percentage greater than or equal to the provided percentage, or less than or equal to it. + +**Behavior:**
+Filters apps based on their achievement completion percentage. + +**Example:**
+ + +
+ #### MicroSD Card (Requires MicroSDeck) **Options:**
`MicroSD card` - The MicroSD card to use (if none are showing up, make sure they are showing up in MicroSDeck). diff --git a/defaults/docs/Overview.md b/src/components/docs/plugin-docs/Overview.md similarity index 100% rename from defaults/docs/Overview.md rename to src/components/docs/plugin-docs/Overview.md diff --git a/defaults/docs/Tab_Profiles.md b/src/components/docs/plugin-docs/Tab_Profiles.md similarity index 100% rename from defaults/docs/Tab_Profiles.md rename to src/components/docs/plugin-docs/Tab_Profiles.md diff --git a/defaults/docs/Tabs.md b/src/components/docs/plugin-docs/Tabs.md similarity index 100% rename from defaults/docs/Tabs.md rename to src/components/docs/plugin-docs/Tabs.md diff --git a/defaults/docs/The_Fix_System.md b/src/components/docs/plugin-docs/The_Fix_System.md similarity index 100% rename from defaults/docs/The_Fix_System.md rename to src/components/docs/plugin-docs/The_Fix_System.md diff --git a/src/components/filters/FilterOptions.tsx b/src/components/filters/FilterOptions.tsx index 64ee9c3..ada5e54 100644 --- a/src/components/filters/FilterOptions.tsx +++ b/src/components/filters/FilterOptions.tsx @@ -489,7 +489,8 @@ const TimePlayedFilterOptions: VFC> = ({ index * The options for a size on disk filter. */ const SizeOnDiskFilterOptions: VFC> = ({ index, setContainingGroupFilters, filter, containingGroupFilters }) => { - const [value, setValue] = useState(filter.params.gbThreshold); + const [value, setValue] = useState(filter.params.gbThreshold.toString()); + const [numericValue, setNumericValue] = useState(filter.params.gbThreshold); const [thresholdType, setThresholdType] = useState(filter.params.condition); function updateFilter(threshold: number, threshType: ThresholdCondition) { @@ -501,22 +502,29 @@ const SizeOnDiskFilterOptions: VFC> = ({ inde setContainingGroupFilters(updatedFilters); } - function onSliderChange(value: number) { - updateFilter(value, thresholdType); - setValue(value); + function onSliderChange(e: React.ChangeEvent) { + let parsedValue = 0; + if (e?.target.value !== "" && !isNaN(parseFloat(e?.target.value))) { + parsedValue = parseFloat(e?.target.value); + } + + updateFilter(parsedValue, thresholdType); + setNumericValue(parsedValue); + + setValue(e?.target.value); } function onThreshTypeChange({ data: threshType }: { data: ThresholdCondition; }) { - updateFilter(value, threshType); + updateFilter(numericValue, threshType); setThresholdType(threshType); } return ( - + +
@@ -782,6 +790,46 @@ const SteamFeatureFilterOptions: VFC> = ({ ); }; +/** + * The options for a achievements filter. + */ +const AchievementsFilterOptions: VFC> = ({ index, setContainingGroupFilters, filter, containingGroupFilters }) => { + const [value, setValue] = useState(filter.params.completionPercentage); + const [thresholdType, setThresholdType] = useState(filter.params.condition); + + function updateFilter(threshold: number, threshType: ThresholdCondition) { + const updatedFilter = { ...filter }; + updatedFilter.params.completionPercentage = threshold; + updatedFilter.params.condition = threshType; + const updatedFilters = [...containingGroupFilters]; + updatedFilters[index] = updatedFilter; + setContainingGroupFilters(updatedFilters); + } + + function onSliderChange(value: number) { + updateFilter(value, thresholdType); + setValue(value); + } + + function onThreshTypeChange({ data: threshType }: { data: ThresholdCondition; }) { + updateFilter(value, threshType); + setThresholdType(threshType); + } + + return ( + + +
+ +
+
} + /> + ); +}; + /** * The options for an sd card filter */ @@ -857,6 +905,8 @@ export const FilterOptions: VFC> = ({ index, filt return } containingGroupFilters={containingGroupFilters} setContainingGroupFilters={setContainingGroupFilters} />; case "steam features": return } containingGroupFilters={containingGroupFilters} setContainingGroupFilters={setContainingGroupFilters} />; + case "achievements": + return } containingGroupFilters={containingGroupFilters} setContainingGroupFilters={setContainingGroupFilters} />; case "sd card": return } containingGroupFilters={containingGroupFilters} setContainingGroupFilters={setContainingGroupFilters} />; default: diff --git a/src/components/filters/FilterPreview.tsx b/src/components/filters/FilterPreview.tsx index 9e2cf08..5b59832 100644 --- a/src/components/filters/FilterPreview.tsx +++ b/src/components/filters/FilterPreview.tsx @@ -79,7 +79,7 @@ const TimePlayedFilterPreview: VFC> = ({ filte const SizeOnDiskFilterPreview: VFC> = ({ filter }) => { const { gbThreshold, condition } = filter.params; - return ; + return ; }; const ReleaseDateFilterPreview: VFC> = ({ filter }) => { @@ -122,6 +122,11 @@ const SteamFeaturesFilterPreview: VFC> = ({ return ; }; +const AchievementsFilterPreview: VFC> = ({ filter }) => { + const { completionPercentage, condition } = filter.params; + return ; +}; + const SDCardFilterPreview: VFC> = ({ filter }) => { const isInsertCard = !filter.params.card; const card = (MicroSDeckInterop.isInstallOk() && window.MicroSDeck?.CardsAndGames.find(([card]) => card.uid === filter.params.card)?.[0].name) || filter.params.card; @@ -170,6 +175,8 @@ export const FilterPreview: VFC> = ({ filter }) = return } />; case "steam features": return } />; + case "achievements": + return } /> case "sd card": return } />; default: diff --git a/src/components/filters/Filters.tsx b/src/components/filters/Filters.tsx index 85f22b1..8bff18e 100644 --- a/src/components/filters/Filters.tsx +++ b/src/components/filters/Filters.tsx @@ -3,14 +3,14 @@ import { MicroSDeckInterop } from '../../lib/controllers/MicroSDeckInterop'; import { PluginController } from "../../lib/controllers/PluginController"; import { DateIncludes, DateObj } from '../generic/DatePickers'; import { STEAM_FEATURES_ID_MAP } from "./SteamFeatures"; -import { FaCheckCircle, FaHdd, FaSdCard, FaUserFriends } from "react-icons/fa"; +import { FaCheckCircle, FaHdd, FaSdCard, FaTrophy, FaUserFriends } from "react-icons/fa"; import { IoGrid } from "react-icons/io5"; import { SiSteamdeck } from "react-icons/si"; import { FaAward, FaBan, FaCalendarDays, FaCloudArrowDown, FaCompactDisc, FaListCheck, FaPlay, FaRegClock, FaSteam, FaTags } from "react-icons/fa6"; import { BsClockHistory, BsRegex } from "react-icons/bs"; import { LuCombine } from "react-icons/lu"; -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' | 'steam features' | 'sd 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' | 'steam features' | 'achievements' | 'sd card'; export type TimeUnit = 'minutes' | 'hours' | 'days'; export type ThresholdCondition = 'above' | 'below'; @@ -27,23 +27,24 @@ type CollectionFilterParams = { */ collection?: SteamCollection['id']; }; -type InstalledFilterParams = { installed: boolean; }; -type RegexFilterParams = { regex: string; }; -type FriendsFilterParams = { friends: number[], mode: LogicalMode; }; -type TagsFilterParams = { tags: number[], mode: LogicalMode; }; -type WhitelistFilterParams = { games: number[]; }; -type BlacklistFilterParams = { games: number[]; }; -type MergeFilterParams = { filters: TabFilterSettings[], mode: LogicalMode; }; -type PlatformFilterParams = { platform: SteamPlatform; }; -type DeckCompatFilterParams = { category: number; }; -type ReviewScoreFilterParams = { scoreThreshold: number, condition: ThresholdCondition, type: ReviewScoreType; }; -type TimePlayedFilterParams = { timeThreshold: number, condition: ThresholdCondition, units: TimeUnit; }; -type SizeOnDiskFilterParams = { gbThreshold: number, condition: ThresholdCondition; }; -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 InstalledFilterParams = { installed: boolean }; +type RegexFilterParams = { regex: string }; +type FriendsFilterParams = { friends: number[], mode: LogicalMode }; +type TagsFilterParams = { tags: number[], mode: LogicalMode }; +type WhitelistFilterParams = { games: number[] }; +type BlacklistFilterParams = { games: number[] }; +type MergeFilterParams = { filters: TabFilterSettings[], mode: LogicalMode }; +type PlatformFilterParams = { platform: SteamPlatform }; +type DeckCompatFilterParams = { category: number }; +type ReviewScoreFilterParams = { scoreThreshold: number, condition: ThresholdCondition, type: ReviewScoreType }; +type TimePlayedFilterParams = { timeThreshold: number, condition: ThresholdCondition, units: TimeUnit }; +type SizeOnDiskFilterParams = { gbThreshold: number, condition: ThresholdCondition }; +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 SteamFeaturesFilterParams = { features: number[], mode: LogicalMode }; +type AchievementsFilterParams = { completionPercentage: number, condition: ThresholdCondition } type SdCardParams = { card: undefined | string }; //use undefined for currently inserted card export type FilterParams = @@ -65,6 +66,7 @@ export type FilterParams = T extends 'demo' ? DemoFilterParams : T extends 'streamable' ? StreamableFilterParams : T extends 'steam features' ? SteamFeaturesFilterParams : + T extends 'achievements' ? AchievementsFilterParams : T extends 'sd card' ? SdCardParams : never; @@ -100,6 +102,7 @@ export const FilterDefaultParams: { [key in FilterType]: FilterParams } = { "demo": { isDemo: true }, "streamable": { isStreamable: true }, "steam features": { features: [], mode: 'and' }, + "achievements": { completionPercentage: 10, condition: 'above' }, "sd card": { card: undefined } } @@ -124,6 +127,7 @@ export const FilterDescriptions: { [filterType in FilterType]: string } = { "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.", + achievements: "Selects apps based on their completion percentage.", "steam features": "Selects apps that support specific Steam Features.", "sd card": "Selects apps that are present on the inserted/specific MicroSD Card." } @@ -150,6 +154,7 @@ export const FilterIcons: { [filterType in FilterType]: IconType } = { demo: FaCompactDisc, streamable: FaCloudArrowDown, "steam features": FaListCheck, + achievements: FaTrophy, "sd card": FaSdCard } @@ -180,6 +185,7 @@ export function canBeInverted(filter: TabFilterSettings): boolean { case "release date": case "last played": case "demo": + case "achievements": case "streamable": return false; } @@ -211,14 +217,16 @@ export function isValidParams(filter: TabFilterSettings): boolean { return (filter as TabFilterSettings<'release date'>).params.date !== undefined || (filter as TabFilterSettings<'release date'>).params.daysAgo !== undefined; case "steam features": return (filter as TabFilterSettings<'steam features'>).params.features.length !== 0; + case "size on disk": + return (filter as TabFilterSettings<'size on disk'>).params.gbThreshold !== 0; case "installed": case "platform": case "deck compatibility": case "review score": case "time played": - case "size on disk": case "demo": case "streamable": + case "achievements": case "sd card": return true; } @@ -352,6 +360,7 @@ export function validateFilter(filter: TabFilterSettings): Validatio errors: passed ? [] : ["Couldn't find the selected card in the list of known cards."] }; } + case "size on disk": case "regex": case "friends": case "tags": @@ -362,12 +371,12 @@ export function validateFilter(filter: TabFilterSettings): Validatio case "deck compatibility": case "review score": case "time played": - case "size on disk": case "release date": case "last played": case "demo": case "streamable": case "steam features": + case "achievements": default: return { passed: true, @@ -517,6 +526,10 @@ export class Filter { return params.features.some((feature: number) => appOverview.store_category.includes(feature)); } }, + 'achievements': (params: FilterParams<'achievements'>, appOverview: SteamAppOverview) => { + const percentage = appAchievementProgressCache.GetAchievementProgress(appOverview.appid); + return params.condition === 'above' ? percentage >= params.completionPercentage : percentage <= params.completionPercentage; + }, 'sd card': (params: FilterParams<'sd card'>, appOverview: SteamAppOverview) => { const card = params.card === undefined ? window.MicroSDeck?.CurrentCardAndGames : window.MicroSDeck?.CardsAndGames?.find(([card]) => card.uid == params.card); diff --git a/src/components/filters/SteamFeatures.tsx b/src/components/filters/SteamFeatures.ts similarity index 100% rename from src/components/filters/SteamFeatures.tsx rename to src/components/filters/SteamFeatures.ts diff --git a/src/components/styles/ModalStyles.tsx b/src/components/styles/ModalStyles.tsx index 8f6dbdc..acdbf8d 100644 --- a/src/components/styles/ModalStyles.tsx +++ b/src/components/styles/ModalStyles.tsx @@ -98,6 +98,11 @@ export const ModalStyles: VFC<{}> = ({}) => { color: #a9a9a9; } + /* Filter Option styles */ + .tab-master-modal-scope .size-on-disk-row > div:first-child { + flex-grow: 1; + } + .autohide-toggle-container .${gamepadDialogClasses.Field} { padding: 10px calc(28px + 1.4vw); } diff --git a/src/index.tsx b/src/index.tsx index 7ee7ee5..b62c7dc 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,7 +15,6 @@ import { TabMasterManager } from "./state/TabMasterManager"; import { patchLibrary } from "./patches/LibraryPatch"; import { patchSettings } from "./patches/SettingsPatch"; -import { LogController } from "./lib/controllers/LogController"; import { MicroSDeck } from "@cebbinghaus/microsdeck"; import { MicroSDeckInterop } from './lib/controllers/MicroSDeckInterop'; import { QuickAccessContent, QuickAccessTitleView } from "./components/QuickAccessContent"; @@ -33,6 +32,7 @@ declare global { //* This casing is correct, idk why it doesn't match the others. let securitystore: SecurityStore; let settingsStore: SettingsStore; + let appAchievementProgressCache: AppAchievementProgressCache; } @@ -52,17 +52,11 @@ export default definePlugin((serverAPI: ServerAPI) => { settingsPatch = patchSettings(serverAPI, tabMasterManager); }); - PythonInterop.getDocs().then((pages: DocPages | Error) => { - if (pages instanceof Error) { - LogController.error(pages); - } else { - serverAPI.routerHook.addRoute("/tab-master-docs", () => ( - - - - )); - } - }); + serverAPI.routerHook.addRoute("/tab-master-docs", () => ( + + + + )); return { title: <>, titleView: , diff --git a/src/lib/controllers/MicroSDeckInterop.ts b/src/lib/controllers/MicroSDeckInterop.ts index 8663c41..052a6f7 100644 --- a/src/lib/controllers/MicroSDeckInterop.ts +++ b/src/lib/controllers/MicroSDeckInterop.ts @@ -7,11 +7,11 @@ import { version } from '@cebbinghaus/microsdeck/package.json'; export const microSDeckLibVersion = version; export enum MicroSDeckInstallState { - 'not installed', - 'ver too low', - 'ver too high', - 'ver unknown', - 'good' + NOT_INSTALLED, + VERSION_TOO_LOW, + VERSION_TOO_HIGH, + VERSION_UNKOWN, + VERSION_COMPATIBLE } export class MicroSDeckInterop { @@ -75,7 +75,7 @@ export class MicroSDeckInterop { */ static getInstallState(runChangeHandlerIfNewInstance?: boolean) { if (!window.MicroSDeck) { - return MicroSDeckInstallState['not installed']; + return MicroSDeckInstallState.NOT_INSTALLED; } else { //MicroSDeck has been reinstalled or reloaded @@ -95,7 +95,7 @@ export class MicroSDeckInterop { * @returns boolean */ static isInstallOk(runChangeHandlerIfNewInstance?: boolean) { - return this.getInstallState(runChangeHandlerIfNewInstance) === MicroSDeckInstallState['good']; + return this.getInstallState(runChangeHandlerIfNewInstance) === MicroSDeckInstallState.VERSION_COMPATIBLE; } /** @@ -107,18 +107,18 @@ export class MicroSDeckInterop { const [pluginVerMajor, pluginVerMinor, pluginVerPatch] = window.MicroSDeck!.Version.split(/[.+-]/, 3).map(str => +str); const [libVerMajor, libVerMinor, libVerPatch] = microSDeckLibVersion.split(/[.+-]/, 3).map(str => +str); - if (isNaN(pluginVerMajor) || isNaN(pluginVerMinor) || isNaN(pluginVerPatch) || isNaN(libVerMajor) || isNaN(libVerMinor) || isNaN(libVerPatch)) return MicroSDeckInstallState['ver unknown']; + if (isNaN(pluginVerMajor) || isNaN(pluginVerMinor) || isNaN(pluginVerPatch) || isNaN(libVerMajor) || isNaN(libVerMinor) || isNaN(libVerPatch)) return MicroSDeckInstallState.VERSION_UNKOWN; if (pluginVerMajor === 0 && libVerMajor === 0) { - if (pluginVerMinor > libVerMinor || pluginVerPatch > libVerPatch) return MicroSDeckInstallState['ver too high']; - if (pluginVerMinor < libVerMinor || pluginVerPatch < libVerPatch) return MicroSDeckInstallState['ver too low']; - return MicroSDeckInstallState['good']; + if (pluginVerMinor > libVerMinor) return MicroSDeckInstallState.VERSION_TOO_HIGH; + if (pluginVerMinor < libVerMinor) return MicroSDeckInstallState.VERSION_TOO_LOW; + return MicroSDeckInstallState.VERSION_COMPATIBLE; } - if (pluginVerMajor > libVerMajor) return MicroSDeckInstallState['ver too high']; - if (pluginVerMajor < libVerMajor) return MicroSDeckInstallState['ver too low']; - return MicroSDeckInstallState['good']; + if (pluginVerMajor > libVerMajor) return MicroSDeckInstallState.VERSION_TOO_HIGH; + if (pluginVerMajor < libVerMajor) return MicroSDeckInstallState.VERSION_TOO_LOW; + return MicroSDeckInstallState.VERSION_COMPATIBLE; } else { - return MicroSDeckInstallState['ver too low']; //* version is so old it doesn't have the Version prop. + return MicroSDeckInstallState.VERSION_TOO_LOW; //* version is so old it doesn't have the Version prop. } } } diff --git a/src/lib/controllers/PythonInterop.ts b/src/lib/controllers/PythonInterop.ts index ddcfc56..fa964e3 100644 --- a/src/lib/controllers/PythonInterop.ts +++ b/src/lib/controllers/PythonInterop.ts @@ -98,20 +98,6 @@ export class PythonInterop { return new Error(result.result); } } - - /** - * Gets the plugin's docs. - * @returns A promise resolving to the plugin's docs. - */ - static async getDocs(): Promise { - const result = await this.serverAPI.callPluginMethod<{}, DocPages>("get_docs", {}); - - if (result.success) { - return result.result; - } else { - return new Error(result.result); - } - } /** * Gets the plugin's tabs. diff --git a/src/state/TabMasterManager.tsx b/src/state/TabMasterManager.tsx index 8e34530..d9c2ce4 100644 --- a/src/state/TabMasterManager.tsx +++ b/src/state/TabMasterManager.tsx @@ -64,6 +64,7 @@ export class TabMasterManager { private friendsReaction: IReactionDisposer | undefined; private tagsReaction: IReactionDisposer | undefined; + private achievementsReaction: IReactionDisposer | undefined; private collectionRemoveReaction: IReactionDisposer | undefined; @@ -89,41 +90,45 @@ export class TabMasterManager { } private initReactions(): void { - //* subscribe to changes to all games + // * subscribe to changes to all games this.allGamesReaction = reaction(() => collectionStore.GetCollection("type-games").allApps, this.rebuildCustomTabsOnCollectionChange.bind(this), { delay: 600 }); - //* subscribe to when visible favorites change + // * subscribe to when visible favorites change this.favoriteReaction = reaction(() => collectionStore.GetCollection('favorite').allApps.length, this.handleNumOfVisibleFavoritesChanged.bind(this)); - //*subscribe to when visible soundtracks change + // *subscribe to when visible soundtracks change this.soundtrackReaction = reaction(() => collectionStore.GetCollection('type-music').visibleApps.length, this.handleNumOfVisibleSoundtracksChanged.bind(this)); - //*subscribe to when installed games change + // *subscribe to when installed games change this.installedReaction = reaction(() => collectionStore.GetCollection('local-install').allApps.length, this.rebuildCustomTabsOnCollectionChange.bind(this)); - //* subscribe to game hide or show + // * subscribe to game hide or show this.hiddenReaction = reaction(() => collectionStore.GetCollection("hidden").allApps.length, this.rebuildCustomTabsOnCollectionChange.bind(this), { delay: 50 }); - //* subscribe to non-steam games if they exist + // * subscribe to non-steam games if they exist if (collectionStore.GetCollection('desk-desktop-apps')) { this.nonSteamReaction = reaction(() => collectionStore.GetCollection('desk-desktop-apps').allApps.length, this.rebuildCustomTabsOnCollectionChange.bind(this)); } - //* subscribe for when collections are deleted + // * subscribe for when collections are deleted this.collectionRemoveReaction = reaction(() => collectionStore.userCollections.length, this.handleUserCollectionRemove.bind(this)); this.handleUserCollectionRemove(collectionStore.userCollections.length); //* this loads the collection ids for the first time. - //* subscribe to user's friendlist updates + // * subscribe to user's friendlist updates this.friendsReaction = reaction(() => friendStore.allFriends, this.handleFriendsReaction.bind(this), { delay: 50 }); this.handleFriendsReaction(friendStore.allFriends); - //* subscribe to store tag list changes + // * subscribe to store tag list changes this.tagsReaction = reaction(() => appStore.m_mapStoreTagLocalization, this.storeTagReaction.bind(this), { delay: 50 }); this.storeTagReaction(appStore.m_mapStoreTagLocalization); + + // * subscribe to achievement cache changes + this.achievementsReaction = reaction(() => appAchievementProgressCache.m_achievementProgress.mapCache.size, this.handleAchievementsReaction.bind(this)); + MicroSDeckInterop.initEventHandlers({change: this.handleMicroSDeckChange.bind(this)}); } @@ -316,6 +321,21 @@ export class TabMasterManager { }); } + /** + * Handles updating state when the the achievement cache changes. + * @param _size The size of the achievements map. + */ + private handleAchievementsReaction(_size: number) { + this.visibleTabsList.forEach(tabContainer => { + if (tabContainer.filters) { + const tab = tabContainer as CustomTabContainer; + if (tab.containsFilterType('achievements')) { + tab.buildCollection(); + } + } + }); + } + /** * Checks for tabs with filters that are based on time ago and rebuilds their collections. */ @@ -356,6 +376,8 @@ export class TabMasterManager { if (this.friendsReaction) this.friendsReaction(); if (this.tagsReaction) this.tagsReaction(); + + if (this.achievementsReaction) this.achievementsReaction(); if (this.collectionRemoveReaction) this.collectionRemoveReaction(); } diff --git a/src/types/stores/appAchievementPorgressCache.d.ts b/src/types/stores/appAchievementPorgressCache.d.ts new file mode 100644 index 0000000..6e423bd --- /dev/null +++ b/src/types/stores/appAchievementPorgressCache.d.ts @@ -0,0 +1,8 @@ +// Types for the global appAchievementProgressCache + +type AppAchievementProgressCache = { + GetAchievementProgress: (appId: number) => number; + m_achievementProgress: { + mapCache: Map + } +} diff --git a/src/types/types.d.ts b/src/types/types.d.ts index 30da71b..8da0b4e 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -13,6 +13,11 @@ declare module "*.jpg" { export default content; } +declare module "*/docs.codegen" { + const content: DocPages; + export default content; +} + type Unregisterer = { unregister: () => void; }