diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 26d7cfcf..a19f8789 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -165,7 +165,7 @@ export type SettingProfileModel = { name: string; java_id: number | null; res: R export type Settings = { global_game_settings: SettingProfileModel; allow_parallel_running_clusters: boolean; enable_gamemode: boolean; discord_enabled: boolean; max_concurrent_requests: number; settings_version: number; native_window_frame: boolean } -export type SettingsOsExtra = { enable_gamemode: boolean | null } +export type SettingsOsExtra = Record export type Sort = "Relevance" | "Downloads" | "Newest" | "Updated" @@ -228,8 +228,15 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[]}', 'core':'{"openMsaLogin":[],"readSettings":[],"getProfileOrDefault":["name"],"getLogByName":["id","name"],"getWorlds":["id"],"launchCluster":["id","uuid"],"getGameVersions":[],"getUser":["uuid"],"removeUser":["uuid"],"getUsersFromAuthor":["provider","author"],"getPackage":["provider","slug"],"setDefaultUser":["uuid"],"getMultiplePackages":["provider","slugs"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"updateClusterById":["id","request"],"getGlobalProfile":[],"getLoadersForVersion":["mc_version"],"getPackageBody":["provider","body"],"writeSettings":["setting"],"getScreenshots":["id"],"getLogs":["id"],"getDefaultUser":["fallback"],"getClusterById":["id"],"removeCluster":["id"],"updateClusterProfile":["name","profile"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"createCluster":["options"],"getClusters":[],"searchPackages":["provider","query"],"getUsers":[]}', 'events':'{"ingress":["event"],"process":["event"],"message":["event"]}', 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}' } -export type Router = { 'core': { getClusters: () => Promise, +const ARGS_MAP = { 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'events':'{"ingress":["event"],"process":["event"],"message":["event"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}', 'core':'{"getWorlds":["id"],"launchCluster":["id","uuid"],"getDefaultUser":["fallback"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getClusterById":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getProfileOrDefault":["name"],"updateClusterProfile":["name","profile"],"writeSettings":["setting"],"updateClusterById":["id","request"],"getMultiplePackages":["provider","slugs"],"removeCluster":["id"],"getUser":["uuid"],"getGlobalProfile":[],"openMsaLogin":[],"getPackageBody":["provider","body"],"getLogByName":["id","name"],"getLoadersForVersion":["mc_version"],"searchPackages":["provider","query"],"readSettings":[],"removeUser":["uuid"],"getClusters":[],"getGameVersions":[],"getUsers":[],"getPackage":["provider","slug"],"getUsersFromAuthor":["provider","author"],"getScreenshots":["id"],"getLogs":["id"],"setDefaultUser":["uuid"],"createCluster":["options"]}' } +export type Router = { 'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise }, +'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise }, +'oneclient': { openDevTools: () => Promise, +getClustersGroupedByMajor: () => Promise> }, +'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, removeCluster: (id: number) => Promise, createCluster: (options: CreateCluster) => Promise, @@ -258,14 +265,7 @@ getPackageBody: (provider: Provider, body: ManagedPackageBody) => Promise Promise, getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null, loader: GameLoader | null, offset: number, limit: number) => Promise>, downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: number, skipCompatibility: boolean | null) => Promise, -getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise }, -'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise }, -'oneclient': { openDevTools: () => Promise, -getClustersGroupedByMajor: () => Promise> }, -'events': { ingress: (event: IngressPayload) => Promise, -message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise } }; +getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/components/PackageItem.tsx b/apps/oneclient/frontend/src/components/PackageItem.tsx new file mode 100644 index 00000000..1e534ba3 --- /dev/null +++ b/apps/oneclient/frontend/src/components/PackageItem.tsx @@ -0,0 +1,98 @@ +import type { Provider, SearchResult } from '@/bindings.gen'; +// 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'; +import { useMemo } from 'react'; +import { Focusable } from 'react-aria-components'; + +function includes(list: { includes: (arg0: TArray) => boolean }, element: T): element is TArray { + return list.includes(element as unknown as TArray); +} + +export function PackageGrid({ items, provider }: { items: Array; provider: Provider }) { + return ( +
+ {items.map(item => ( + + ))} +
+ ); +} + +export function PackageItem({ provider, ...item }: SearchResult & { provider: Provider }) { + // 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 ( + +
+ + )} + when={item.icon_url} + > + {`Icon + {`Icon + + {/* + +
+ {loaders.toSpliced(loaders.length > 3 ? 2 : 3).map(loader => )} + {loaders.length > 3 && ( +
+ + + + {loaders.length - 2} + +
+ )} +
+
+
*/} +
+
+
+

{item.title}

+

+ By + {' '} + {item.author} + {' '} + on + {' '} + {provider} +

+
+ +

{item.description}

+ + {/*
+ +
+ + {abbreviateNumber(item.downloads)} +
+
+
*/} +
+ + ); +} diff --git a/apps/oneclient/frontend/src/routes/app/cluster/browser/index.tsx b/apps/oneclient/frontend/src/routes/app/cluster/browser/index.tsx new file mode 100644 index 00000000..fbae86d5 --- /dev/null +++ b/apps/oneclient/frontend/src/routes/app/cluster/browser/index.tsx @@ -0,0 +1,136 @@ +import type { PackageType, Provider } from '@/bindings.gen'; +import { browserCategories, categoryNameFromId } from '@/utils/browser'; +import { Dropdown, Show, TextField } from '@onelauncher/common/components'; +import { createFileRoute } from '@tanstack/react-router'; +import { SearchMdIcon } from '@untitled-theme/icons-react'; +import { useMemo } from 'react'; + +interface BrowserSearchRouteSearchParams { + query?: string; + packageType: PackageType; + categories: Array; +} + +export const Route = createFileRoute('/app/cluster/browser/')({ + component: RouteComponent, + validateSearch: (search): BrowserSearchRouteSearchParams => { + return { + query: search.query as string, + packageType: search.packageType as PackageType, + categories: search.categories as Array, + }; + }, +}); + +const PROVIDERS: Array = ['Modrinth', 'CurseForge', 'SkyClient']; + +function RouteComponent() { + return ( +
+
+
+
+ +
+
+ +
+ + +
+
+ +
+
+ + {/* */} +
+
+
+ ); +} + +function BrowserCategories() { + const search = Route.useSearch(); + const { categories, packageType } = search; + const navigate = Route.useNavigate(); + const switchCategory = useMemo(() => (category: string) => { + const newCategories = categories.includes(category) + ? categories.filter(element => element !== category) + : [...categories, category]; + navigate({ + search: { + ...search, + categories: newCategories, + }, + }); + }, []); + + return ( +
+ {/*
*/} +
+ +
+
Categories
+ {browserCategories[categoryNameFromId[packageType]].map(category => ( +

switchCategory(category)} + > + {category} +

+ ))} +
+
+
+
+ ); +} + +function BrowserToolbar() { + const search = Route.useSearch(); + const { provider, query } = search; + const navigate = Route.useNavigate(); + const setQuery = (query: string) => { + navigate({ search: { ...search, query } }); + }; + return ( +
+
+ navigate({ search: { ...search, provider: id as Provider } })} + placeholder="Select a Provider" + selectedKey={provider} + > + {PROVIDERS.map(provider => ( + +
+ {/* */} + {provider} +
+
+ ))} +
+
+ +
+ } + // onChange={e => setQuery(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key !== 'Enter') + return; + e.preventDefault(); + setQuery(e.currentTarget.value); + }} + placeholder="Search for content" + value={query} + /> +
+
+ ); +} diff --git a/apps/oneclient/frontend/src/routes/app/cluster/browser/route.tsx b/apps/oneclient/frontend/src/routes/app/cluster/browser/route.tsx new file mode 100644 index 00000000..13ce2ae3 --- /dev/null +++ b/apps/oneclient/frontend/src/routes/app/cluster/browser/route.tsx @@ -0,0 +1,23 @@ +import type { Provider } from '@/bindings.gen'; +import { Dropdown, Show } from '@onelauncher/common/components'; +import { createFileRoute, Outlet } from '@tanstack/react-router'; +import { SearchMdIcon } from '@untitled-theme/icons-react'; + +export interface BrowserRouteSearchParams { + provider: Provider; +} + +export const Route = createFileRoute('/app/cluster/browser')({ + component: RouteComponent, + validateSearch: (search): BrowserRouteSearchParams => { + return { + provider: search.provider as Provider, + }; + }, +}); + +function RouteComponent() { + return ( + + ); +} diff --git a/apps/oneclient/frontend/src/utils/browser.ts b/apps/oneclient/frontend/src/utils/browser.ts new file mode 100644 index 00000000..4d3e9b1b --- /dev/null +++ b/apps/oneclient/frontend/src/utils/browser.ts @@ -0,0 +1,117 @@ +import type { PackageCategories, PackageModCategory, PackageType, Provider } from '@/bindings.gen'; + +export const BROWSER_VIEWS: Array = ['grid', 'list'] as const; + +type Key = T extends Record ? K : never; + +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 = { + Mod: ModCategories, + ResourcePack: ResourcePackCategories, + ModPack: ModpackCategories, + Shader: ShaderCategories, + DataPack: ModCategories, +} as const satisfies Record, ReadonlyArray>; + +type CategoryIdMap = { + [K in keyof typeof browserCategories as Lowercase]: K +}; + +export const categoryNameFromId: CategoryIdMap = { + mod: 'Mod', + resourcepack: 'ResourcePack', + modpack: 'ModPack', + shader: 'Shader', + datapack: 'DataPack', +}; diff --git a/apps/onelauncher/frontend/src/routes/app/browser/search.tsx b/apps/onelauncher/frontend/src/routes/app/browser/search.tsx index 371eed2e..f003494b 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/search.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/search.tsx @@ -1,7 +1,7 @@ import type { Paginated, SearchResult } from '@/bindings.gen'; import { PackageGrid } from '@/components/content/PackageItem'; import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser'; -import usePagination from '@/hooks/usePagination'; +import { usePagination } from '@onelauncher/common'; import { createFileRoute } from '@tanstack/react-router'; import { useEffect, useRef, useState } from 'react'; import { BrowserLayout } from './route'; diff --git a/packages/web_common/src/hooks/index.ts b/packages/web_common/src/hooks/index.ts index 62c47efe..1ace166c 100644 --- a/packages/web_common/src/hooks/index.ts +++ b/packages/web_common/src/hooks/index.ts @@ -1 +1,2 @@ export * from './useCommand'; +export * from './usePagination'; diff --git a/apps/onelauncher/frontend/src/hooks/usePagination.tsx b/packages/web_common/src/hooks/usePagination.tsx similarity index 96% rename from apps/onelauncher/frontend/src/hooks/usePagination.tsx rename to packages/web_common/src/hooks/usePagination.tsx index 8590989f..332982b1 100644 --- a/apps/onelauncher/frontend/src/hooks/usePagination.tsx +++ b/packages/web_common/src/hooks/usePagination.tsx @@ -8,7 +8,7 @@ export interface PaginationOptions { initialPage?: number; }; -function usePagination(options: PaginationOptions) { +export function usePagination(options: PaginationOptions) { // const [options, setOptions] = useState(initialOptions); const [page, setPage] = useState(Math.max(1, options.initialPage || 1)); @@ -114,5 +114,3 @@ function usePagination(options: PaginationOptions) { Navigation, }; } - -export default usePagination;