feat: oneclient browser wip
This commit is contained in:
parent
fdf74258a0
commit
6f84f5d1cc
8 changed files with 388 additions and 15 deletions
|
|
@ -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<string, never>
|
||||
|
||||
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<ClusterModel[]>,
|
||||
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<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'oneclient': { openDevTools: () => Promise<void>,
|
||||
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>> },
|
||||
'core': { getClusters: () => Promise<ClusterModel[]>,
|
||||
getClusterById: (id: number) => Promise<ClusterModel | null>,
|
||||
removeCluster: (id: number) => Promise<null>,
|
||||
createCluster: (options: CreateCluster) => Promise<ClusterModel>,
|
||||
|
|
@ -258,14 +265,7 @@ getPackageBody: (provider: Provider, body: ManagedPackageBody) => Promise<string
|
|||
getMultiplePackages: (provider: Provider, slugs: string[]) => Promise<ManagedPackage[]>,
|
||||
getPackageVersions: (provider: Provider, slug: string, mcVersion: string | null, loader: GameLoader | null, offset: number, limit: number) => Promise<Paginated<ManagedVersion>>,
|
||||
downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: number, skipCompatibility: boolean | null) => Promise<PackageModel>,
|
||||
getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<ManagedUser[]> },
|
||||
'folders': { fromCluster: (folderName: string) => Promise<string>,
|
||||
openCluster: (folderName: string) => Promise<null> },
|
||||
'oneclient': { openDevTools: () => Promise<void>,
|
||||
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>> },
|
||||
'events': { ingress: (event: IngressPayload) => Promise<void>,
|
||||
message: (event: MessagePayload) => Promise<void>,
|
||||
process: (event: ProcessPayload) => Promise<void> } };
|
||||
getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<ManagedUser[]> } };
|
||||
|
||||
|
||||
export type { InferCommandOutput }
|
||||
|
|
|
|||
98
apps/oneclient/frontend/src/components/PackageItem.tsx
Normal file
98
apps/oneclient/frontend/src/components/PackageItem.tsx
Normal file
|
|
@ -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<T, TArray extends T>(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<SearchResult>; provider: Provider }) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5! gap-4">
|
||||
{items.map(item => (
|
||||
<PackageItem key={item.project_id} {...item} provider={provider} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Link
|
||||
className="h-full min-w-50 overflow-hidden rounded-lg bg-component-bg hover:bg-component-bg-hover grid grid-rows-[7rem_auto] max-h-74 min-h-74"
|
||||
params={{ provider, id: item.project_id }}
|
||||
tabIndex={0}
|
||||
to="/app/browser/package/$provider/$id"
|
||||
>
|
||||
<div
|
||||
className="relative flex items-center justify-center overflow-hidden w-full h-28"
|
||||
>
|
||||
<Show
|
||||
fallback={(
|
||||
<div
|
||||
className="aspect-ratio-square rounded-md bg-border/05 w-2/5"
|
||||
/>
|
||||
)}
|
||||
when={item.icon_url}
|
||||
>
|
||||
<img alt={`Icon for ${item.title}`} className="absolute z-0 max-w-none w-7/6 opacity-50 blur-xl" src={item.icon_url} />
|
||||
<img
|
||||
alt={`Icon for ${item.title}`}
|
||||
className="relative z-1 aspect-ratio-square rounded-lg image-render-auto h-5/6"
|
||||
src={item.icon_url}
|
||||
/>
|
||||
</Show>
|
||||
{/* <Tooltip className="bg-component-bg-disabled" text={loaders.map(upperFirst).join(', ')}>
|
||||
<Focusable>
|
||||
<div className="flex flex-col rounded-full bg-[#11171C]/75 border-component-border/70 border p-1 absolute top-0 right-0 m-2">
|
||||
{loaders.toSpliced(loaders.length > 3 ? 2 : 3).map(loader => <FlatLoaderIcon className="w-4 m-1" key={loader} loader={loader} />)}
|
||||
{loaders.length > 3 && (
|
||||
<div className="bg-component-bg/50 rounded-full w-6 h-6 flex items-center justify-center">
|
||||
<span className="tracking-tight -ml-0.5 mt-0.5">
|
||||
+
|
||||
{loaders.length - 2}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Focusable>
|
||||
</Tooltip> */}
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2 p-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4 className="text-fg-primary font-medium line-height-normal">{item.title}</h4>
|
||||
<p className="text-xs text-fg-secondary">
|
||||
By
|
||||
{' '}
|
||||
<span className="text-fg-primary">{item.author}</span>
|
||||
{' '}
|
||||
on
|
||||
{' '}
|
||||
{provider}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="max-h-22 flex-1 overflow-hidden text-sm text-fg-secondary line-height-snug">{item.description}</p>
|
||||
|
||||
{/* <div className="flex flex-row gap-4 text-xs">
|
||||
<Show when={provider !== 'SkyClient'}>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Download01Icon className="h-4 w-4" />
|
||||
{abbreviateNumber(item.downloads)}
|
||||
</div>
|
||||
</Show>
|
||||
</div> */}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
136
apps/oneclient/frontend/src/routes/app/cluster/browser/index.tsx
Normal file
136
apps/oneclient/frontend/src/routes/app/cluster/browser/index.tsx
Normal file
|
|
@ -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<string>;
|
||||
}
|
||||
|
||||
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<string>,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const PROVIDERS: Array<Provider> = ['Modrinth', 'CurseForge', 'SkyClient'];
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<div className="relative h-full flex flex-1 flex-col items-center gap-2">
|
||||
<div className="h-full w-full flex flex-1 flex-col items-center gap-y-2">
|
||||
<div className="grid grid-cols-[180px_auto_160px] w-full gap-x-6">
|
||||
<div />
|
||||
<BrowserToolbar />
|
||||
<div />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[180px_auto_160px] w-full gap-x-6 pb-8">
|
||||
<BrowserCategories />
|
||||
|
||||
<div className="h-full flex flex-col gap-y-4">
|
||||
<div className="h-full flex-1">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <BrowserSidebar /> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="top-0 h-fit min-w-50">
|
||||
{/* <div /> */}
|
||||
<div className="flex flex-col gap-y-6 ml-16">
|
||||
<Show when>
|
||||
<div className="flex flex-col gap-y-1.5">
|
||||
<h5 className="my-1 text-sm uppercase opacity-60 font-light">Categories</h5>
|
||||
{browserCategories[categoryNameFromId[packageType]].map(category => (
|
||||
<p
|
||||
aria-selected={categories.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}
|
||||
onClick={() => switchCategory(category)}
|
||||
>
|
||||
{category}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BrowserToolbar() {
|
||||
const search = Route.useSearch();
|
||||
const { provider, query } = search;
|
||||
const navigate = Route.useNavigate();
|
||||
const setQuery = (query: string) => {
|
||||
navigate({ search: { ...search, query } });
|
||||
};
|
||||
return (
|
||||
<div className="w-full flex flex-row justify-between bg-page">
|
||||
<div className="flex flex-row gap-2">
|
||||
<Dropdown
|
||||
onSelectionChange={id => navigate({ search: { ...search, provider: id as Provider } })}
|
||||
placeholder="Select a Provider"
|
||||
selectedKey={provider}
|
||||
>
|
||||
{PROVIDERS.map(provider => (
|
||||
<Dropdown.Item id={provider} key={provider}>
|
||||
<div className="flex flex-row">
|
||||
{/* <ProviderIcon className="size-4 mr-2 self-center" provider={provider} /> */}
|
||||
{provider}
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<TextField
|
||||
className="min-w-64"
|
||||
iconLeft={<SearchMdIcon className="scale-75" />}
|
||||
// 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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<Outlet />
|
||||
);
|
||||
}
|
||||
117
apps/oneclient/frontend/src/utils/browser.ts
Normal file
117
apps/oneclient/frontend/src/utils/browser.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import type { PackageCategories, PackageModCategory, PackageType, Provider } from '@/bindings.gen';
|
||||
|
||||
export const BROWSER_VIEWS: Array<string> = ['grid', 'list'] as const;
|
||||
|
||||
type Key<T> = T extends Record<infer K, any> ? 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<Key<PackageCategories>, ReadonlyArray<string>>;
|
||||
|
||||
type CategoryIdMap = {
|
||||
[K in keyof typeof browserCategories as Lowercase<K>]: K
|
||||
};
|
||||
|
||||
export const categoryNameFromId: CategoryIdMap = {
|
||||
mod: 'Mod',
|
||||
resourcepack: 'ResourcePack',
|
||||
modpack: 'ModPack',
|
||||
shader: 'Shader',
|
||||
datapack: 'DataPack',
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export * from './useCommand';
|
||||
export * from './usePagination';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Add table
Reference in a new issue