From b4746b8ec63fb090d490814f2c77a6a6192bf563 Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Wed, 6 Aug 2025 23:44:18 +0200 Subject: [PATCH] refactor: categories pt. 1 (pain) --- .../src/components/content/PackageItem.tsx | 7 +- .../frontend/src/hooks/useBrowser.tsx | 22 +- .../app/browser/package.$provider.$slug.tsx | 8 +- .../frontend/src/routes/app/browser/route.tsx | 41 ++-- .../onelauncher/frontend/src/utils/browser.ts | 195 ++++++++++-------- apps/onelauncher/frontend/src/utils/index.ts | 6 + 6 files changed, 167 insertions(+), 112 deletions(-) diff --git a/apps/onelauncher/frontend/src/components/content/PackageItem.tsx b/apps/onelauncher/frontend/src/components/content/PackageItem.tsx index 44b840c3..1afb8891 100644 --- a/apps/onelauncher/frontend/src/components/content/PackageItem.tsx +++ b/apps/onelauncher/frontend/src/components/content/PackageItem.tsx @@ -1,5 +1,7 @@ import type { Provider, SearchResult } from '@/bindings.gen'; +import { getCategories } from '@/hooks/useBrowser'; import { abbreviateNumber, LOADERS, upperFirst } from '@/utils'; +import { categoryNameFromId } from '@/utils/browser'; import { Show, Tooltip } from '@onelauncher/common/components'; import { Link } from '@tanstack/react-router'; import { Download01Icon } from '@untitled-theme/icons-react'; @@ -22,7 +24,10 @@ export function PackageGrid({ items, provider }: { items: Array; p } export function PackageItem({ provider, ...item }: SearchResult & { provider: Provider }) { - const loaders = useMemo(() => item.categories.filter(cat => includes(LOADERS, cat)), [item.categories]); + const categoryName = categoryNameFromId[item.package_type]; + if (!(categoryName in item.categories)) + throw new Error('invalid categories'); + const loaders = useMemo(() => getCategories(item.categories).filter(cat => includes(LOADERS, cat)), [item.categories]); return ( >, 'queryKey' | 'queryFn'> | undefined) { const validFilters = useMemo(() => Object.values(query.filters ?? {}).filter(a => a).length > 0, [query.filters]); return useCommand('searchPackages', () => bindings.core.searchPackages(provider, validFilters ? query : { ...query, filters: null }), options); @@ -76,8 +90,8 @@ export function usePackageData(provider: Provider, slug: string, options?: Omit< return useCommand(key, () => bindings.core.getPackage(provider, slug), options); } -export function usePackageVersions(provider: Provider, slug: string, { mc_versions, loaders, offset, limit, ...options }: { mc_versions?: Array | null; loaders?: Array | null; offset?: number; limit: number } & Omit>, 'queryKey' | 'queryFn'>, key: false | BindingCommands | (string & {}) = `getPackageVersions.${provider}.${slug}.${mc_versions?.join('')}.${loaders?.join('')}`) { - return useCommand(key, () => bindings.core.getPackageVersions(provider, slug, mc_versions ?? null, loaders ?? null, (offset ?? 0) as unknown as bigint, limit as unknown as bigint), options); +export function usePackageVersions(provider: Provider, slug: string, { mc_version, loader, offset, limit, ...options }: { mc_version?: string | null; loader?: GameLoader | null; offset?: number; limit: number } & Omit>, 'queryKey' | 'queryFn'>, key: false | BindingCommands | (string & {}) = `getPackageVersions.${provider}.${slug}.${mc_version}.${loader}`) { + return useCommand(key, () => bindings.core.getPackageVersions(provider, slug, mc_version ?? null, loader ?? null, (offset ?? 0) as unknown as bigint, limit as unknown as bigint), options); } export function useDownloadPackage(cluster: ClusterModel, provider: Provider, version: ManagedVersion, skipCompatibility = false) { diff --git a/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx index be9446f9..76263a95 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx @@ -81,7 +81,9 @@ function RouteComponent() { rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]} > - {packageData.data?.body} + {'Raw' in (packageData.data?.body ?? { Raw: '' }) + ? (packageData.data?.body ?? { Raw: '' }).Raw + : packageData.data?.body.Url} @@ -254,8 +256,8 @@ function InstallButton() { const clusters = useClusters(); const browserContext = useBrowserContext(); const { data: versions, isLoading: versionsLoading } = usePackageVersions(provider, slug, { - mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : [], - loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : [], + mc_version: browserContext.cluster ? browserContext.cluster.mc_version : null, + loader: browserContext.cluster ? browserContext.cluster.mc_loader : null, limit: 1, }); const version = useMemo(() => { diff --git a/apps/onelauncher/frontend/src/routes/app/browser/route.tsx b/apps/onelauncher/frontend/src/routes/app/browser/route.tsx index cd64ec3c..de748198 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/route.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/route.tsx @@ -1,14 +1,17 @@ -import type { Filters } from '@/bindings.gen'; +import type { Filters, PackageCategories } from '@/bindings.gen'; +import type { Key, NonReadonly } from '@/utils'; import type { PropsWithChildren } from 'react'; import ProviderIcon from '@/components/content/ProviderIcon'; import { BrowserProvider, useBrowserContext } from '@/hooks/useBrowser'; import { useClusters } from '@/hooks/useCluster'; -import { PROVIDERS } from '@/utils'; -import { browserCategories } from '@/utils/browser'; +import { bindings } from '@/main'; +import { PROVIDERS, upperFirst } from '@/utils'; +import { browserCategories, categoryNameFromId } from '@/utils/browser'; +import { useCommand } from '@onelauncher/common'; import { AnimatedOutlet, Dropdown, Show, TextField } from '@onelauncher/common/components'; import { createFileRoute } from '@tanstack/react-router'; import { SearchMdIcon } from '@untitled-theme/icons-react'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { SelectValue } from 'react-aria-components'; export const Route = createFileRoute('/app/browser')({ @@ -59,19 +62,25 @@ const defaultFilters: Filters = { categories: null, game_versions: null, loaders: null, - package_types: null, + package_type: null, }; +type CategoryFilter = Partial>; + function BrowserCategories() { const context = useBrowserContext(); - const categories = browserCategories.byPackageType((context.query.filters?.package_types ?? ['mod'])[0], context.provider); + const categoryName = useMemo(() => categoryNameFromId[context.query.filters?.package_type ?? 'mod'], [context.query.filters]); + const categories = browserCategories[categoryName]; - function switchCategory(category: string) { - const newCategories = context.query.filters?.categories?.includes(category) - ? context.query.filters.categories.filter(cat => cat !== category) - : [...(context.query.filters?.categories ?? []), category]; - context.setQuery({ ...context.query, filters: { ...defaultFilters, ...context.query.filters, categories: newCategories.length > 0 ? newCategories : null } }); - } + const switchCategory = useMemo(() => (category: string) => { + const isCorrectPackageType = context.query.filters && context.query.filters.categories && categoryName in context.query.filters.categories; + const isEnabled = isCorrectPackageType && context.query.filters.categories[categoryName].includes(category); + const newCategories = isCorrectPackageType + ? { [categoryName]: isEnabled ? context.query.filters.categories?.[categoryName].filter(c => c !== category) : [...context.query.filters.categories[categoryName], category] } + : { [categoryName]: [category] }; + + context.setQuery({ ...context.query, filters: { ...defaultFilters, ...context.query.filters, categories: newCategories } }); + }, [context, categoryName]); return (
@@ -82,12 +91,12 @@ function BrowserCategories() {
Categories
{categories.map(category => (

switchCategory(category.id)} + key={category} + onClick={() => switchCategory(category)} > - {category.display} + {category}

))}
diff --git a/apps/onelauncher/frontend/src/utils/browser.ts b/apps/onelauncher/frontend/src/utils/browser.ts index 53619145..10aa2e73 100644 --- a/apps/onelauncher/frontend/src/utils/browser.ts +++ b/apps/onelauncher/frontend/src/utils/browser.ts @@ -1,97 +1,116 @@ -import type { PackageType, Provider } from '@/bindings.gen'; +import type { PackageCategories, PackageModCategory, PackageType, Provider } from '@/bindings.gen'; +import type { Key } from '.'; export const BROWSER_VIEWS: Array = ['grid', 'list'] as const; -export interface CategoryItem { - display: string; - id: string; -} +const ModCategories = [ + 'Adventure', + 'Library', + 'Equipment', + 'Patches', + 'Cosmetic', + 'Food', + 'Magic', + 'Information', + 'Misc', + 'Performance', + 'Redstone', + 'ServerUtil', + 'Storage', + 'Technology', + 'Farming', + 'Automation', + 'Transport', + 'Utility', + 'QoL', + 'WorldGen', + 'Mobs', + 'Economy', + 'Social', +] as const; + +const ModpackCategories = [ + 'Technology', + 'Quests', + 'Optimization', + 'Multiplayer', + 'Magic', + 'LightWeight', + 'Combat', + 'Challenging', + 'Adventure', +] as const; + +const ResourcePackCategories = [ + 'X8', + 'X16', + 'X32', + 'X48', + 'X64', + 'X128', + 'X256', + 'X512', + 'VanillaLike', + 'Utility', + 'Tweaks', + 'Themed', + 'Simplistic', + 'Realistic', + 'Modded', + 'Decoration', + 'Cursed', + 'Combat', + 'Audio', + 'Blocks', + 'CoreShaders', + 'Gui', + 'Fonts', + 'Equipment', + 'Environment', + 'Entities', + 'Items', + 'Locale', + 'Models', +] as const; + +const ShaderCategories = [ + 'VanillaLike', + 'SemiRealistic', + 'Realistic', + 'Fantasy', + 'Cursed', + 'Cartoon', + 'Bloom', + 'Atmosphere', + 'Reflections', + 'Shadows', + 'PBR', + 'PathTracing', + 'Foliage', + 'ColoredLightning', + 'Potato', + 'Low', + 'Medium', + 'High', + 'Ultra', +] as const; export const browserCategories = { - byPackageType(packageType: PackageType, provider: Provider): Array { - switch (packageType) { - case 'mod': - return this.mod(provider); - } + Mod: ModCategories, + ResourcePack: ResourcePackCategories, + ModPack: ModpackCategories, + Shader: ShaderCategories, + DataPack: ModCategories, +} as const satisfies Record, ReadonlyArray>; - return []; - }, - - mod(provider: Provider = 'Modrinth'): Array { - return Object.values(modMapping[provider]); - }, +type CategoryIdMap = { + [K in keyof typeof browserCategories as Lowercase]: K }; -const modMapping: Record> = { - CurseForge: [ - { display: 'Adventure', id: '422' }, - { display: 'API and Library', id: '421' }, - { display: 'Armor and Tools', id: '434' }, - { display: 'Biomes', id: '407' }, - { display: 'Bug Fixes', id: '6821' }, - { display: 'Cosmetic', id: '424' }, - { display: 'Dimensions', id: '410' }, - { display: 'Education', id: '5299' }, - { display: 'Energy', id: '417' }, - { display: 'Farming', id: '416' }, - { display: 'Food', id: '436' }, - { display: 'Forestry', id: '433' }, - { display: 'Magic', id: '419' }, - { display: 'Miscellaneous', id: '425' }, - { display: 'Mobs', id: '411' }, - { display: 'Ores and Resources', id: '408' }, - { display: 'Performance', id: '6814' }, - { display: 'Player Transport', id: '414' }, - { display: 'Redstone', id: '4558' }, - { display: 'Storage', id: '420' }, - { display: 'Structures', id: '409' }, - { display: 'Technology', id: '412' }, - { display: 'Utility & QoL', id: '5191' }, - { display: 'World Generation', id: '406' }, - - // { display: 'Map and Information', id: '423' }, - // { display: 'Server Utility', id: '435' }, - // { display: 'Twitch Integration', id: '4671' }, - // { display: 'Create', id: '6484' }, - // { display: 'Buildcraft', id: '432' }, - // { display: 'Industrial Craft', id: '429' }, - // { display: 'KubeJS', id: '5314' }, - // { display: 'Genetics', id: '418' }, - // { display: 'Blood Magic', id: '4485' }, - // { display: 'Automation', id: '4843' }, - // { display: 'CraftTweaker', id: '4773' }, - // { display: 'Addons', id: '426' }, - // { display: 'Energy, Fluid, and Item Transport', id: '415' }, - // { display: 'Thermal Expansion', id: '427' }, - // { display: 'Thaumcraft', id: '430' }, - // { display: 'Processing', id: '413' }, - // { display: 'Tinker\'s Construct', id: '428' }, - // { display: 'Skyblock', id: '6145' }, - // { display: 'Galacticraft', id: '5232' }, - // { display: 'Applied Energistics 2', id: '4545' }, - // { display: 'Integrated Dynamics', id: '6954' }, - // { display: 'MCreator', id: '4906' }, - ], - Modrinth: [ - { display: 'Adventure', id: 'adventure' }, - { display: 'Cursed', id: 'cursed' }, - { display: 'Decoration', id: 'decoration' }, - { display: 'Economy', id: 'economy' }, - { display: 'Equipment', id: 'equipment' }, - { display: 'Food', id: 'food' }, - { display: 'Game Mechanics', id: 'game-mechanics' }, - { display: 'Library', id: 'library' }, - { display: 'Magic', id: 'magic' }, - { display: 'Management', id: 'management' }, - { display: 'Minigame', id: 'minigame' }, - { display: 'Mobs', id: 'mobs' }, - { display: 'Optimization', id: 'optimization' }, - { display: 'Social', id: 'social' }, - { display: 'Storage', id: 'storage' }, - { display: 'Technology', id: 'technology' }, - { display: 'Transportation', id: 'transportation' }, - { display: 'Utility', id: 'utility' }, - { display: 'World Generation', id: 'worldgen' }, - ], - SkyClient: [], +export const categoryNameFromId: CategoryIdMap = { + mod: 'Mod', + resourcepack: 'ResourcePack', + modpack: 'ModPack', + shader: 'Shader', + datapack: 'DataPack', }; diff --git a/apps/onelauncher/frontend/src/utils/index.ts b/apps/onelauncher/frontend/src/utils/index.ts index 489db655..15db87a6 100644 --- a/apps/onelauncher/frontend/src/utils/index.ts +++ b/apps/onelauncher/frontend/src/utils/index.ts @@ -133,3 +133,9 @@ export function formatAsDuration(seconds: number | bigint | Date): string { export function includes(list: { includes: (arg0: TArray) => boolean }, element: T): element is TArray { return list.includes(element as unknown as TArray); } + +export type Key = T extends Record ? K : never; + +export type NonReadonly = { + [P in keyof T]: T[P] extends readonly infer O ? O : P +};