refactor: categories pt. 1 (pain)

This commit is contained in:
Szedann 2025-08-06 23:44:18 +02:00
parent 6841051b7a
commit b4746b8ec6
6 changed files with 167 additions and 112 deletions

View file

@ -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<SearchResult>; 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 (
<Link

View file

@ -1,4 +1,4 @@
import type { ClusterModel, GameLoader, ManagedPackage, ManagedVersion, Paginated, Provider, SearchQuery, SearchResult } from '@/bindings.gen';
import type { ClusterModel, GameLoader, ManagedPackage, ManagedVersion, PackageCategories, Paginated, Provider, SearchQuery, SearchResult } from '@/bindings.gen';
import type { BindingCommands } from '@/types/global';
import type { UndefinedInitialDataOptions } from '@tanstack/react-query';
import type { Dispatch, SetStateAction } from 'react';
@ -37,7 +37,7 @@ export function BrowserProvider(props: any) {
categories: null,
game_versions: null,
loaders: null,
package_types: ['mod'],
package_type: 'mod',
},
query: null,
limit: 18 as unknown as bigint,
@ -67,6 +67,20 @@ export function BrowserProvider(props: any) {
);
}
export function getCategories(categories: PackageCategories) {
if ('Mod' in categories)
return categories.Mod;
if ('ResourcePack' in categories)
return categories.ResourcePack;
if ('Shader' in categories)
return categories.Shader;
if ('DataPack' in categories)
return categories.DataPack;
if ('ModPack' in categories)
return categories.ModPack;
return [];
}
export function useBrowserSearch(provider: Provider, query: SearchQuery, options?: Omit<UndefinedInitialDataOptions<Paginated<SearchResult>>, '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<string> | null; loaders?: Array<GameLoader> | null; offset?: number; limit: number } & Omit<UndefinedInitialDataOptions<Paginated<ManagedVersion>>, '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<UndefinedInitialDataOptions<Paginated<ManagedVersion>>, '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) {

View file

@ -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}
</Markdown>
</div>
</TabPanel>
@ -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(() => {

View file

@ -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<NonReadonly<typeof browserCategories>>;
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 (
<div className="top-0 h-fit min-w-50">
@ -82,12 +91,12 @@ function BrowserCategories() {
<h5 className="my-1 text-sm uppercase opacity-60 font-light">Categories</h5>
{categories.map(category => (
<p
aria-selected={context.query.filters?.categories?.includes(category.id)}
aria-selected={context.query.filters?.categories?.[categoryName].includes(category)}
className="text-sm capitalize opacity-60 hover:opacity-90 text-fg-primary hover:text-fg-primary-hover aria-selected:opacity-100"
key={category.id}
onClick={() => switchCategory(category.id)}
key={category}
onClick={() => switchCategory(category)}
>
{category.display}
{category}
</p>
))}
</div>

View file

@ -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<string> = ['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<CategoryItem> {
switch (packageType) {
case 'mod':
return this.mod(provider);
}
Mod: ModCategories,
ResourcePack: ResourcePackCategories,
ModPack: ModpackCategories,
Shader: ShaderCategories,
DataPack: ModCategories,
} as const satisfies Record<Key<PackageCategories>, ReadonlyArray<string>>;
return [];
},
mod(provider: Provider = 'Modrinth'): Array<CategoryItem> {
return Object.values(modMapping[provider]);
},
type CategoryIdMap = {
[K in keyof typeof browserCategories as Lowercase<K>]: K
};
const modMapping: Record<Provider, Array<CategoryItem>> = {
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',
};

View file

@ -133,3 +133,9 @@ export function formatAsDuration(seconds: number | bigint | Date): string {
export function includes<T, TArray extends T>(list: { includes: (arg0: TArray) => boolean }, element: T): element is TArray {
return list.includes(element as unknown as TArray);
}
export type Key<T> = T extends Record<infer K, any> ? K : never;
export type NonReadonly<T> = {
[P in keyof T]: T[P] extends readonly infer O ? O : P
};