feat: featured section WIP
This commit is contained in:
parent
f998722d71
commit
1703318703
3 changed files with 116 additions and 116 deletions
|
|
@ -3,8 +3,8 @@ import type { UndefinedInitialDataOptions } from '@tanstack/react-query';
|
|||
import { bindings } from '@/main';
|
||||
import { PROVIDERS } from '@/utils';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export interface BrowserControllerType {
|
||||
cluster: ClusterModel | undefined;
|
||||
|
|
@ -29,7 +29,7 @@ export function BrowserProvider(props: any) {
|
|||
const { children } = props;
|
||||
const [cluster, setCluster] = useState<ClusterModel | undefined>(undefined);
|
||||
const [provider, setProvider] = useState<Provider>(PROVIDERS[0]);
|
||||
const navigate = useNavigate()
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = useState<SearchQuery>({
|
||||
filters: null,
|
||||
query: null,
|
||||
|
|
@ -38,10 +38,10 @@ export function BrowserProvider(props: any) {
|
|||
sort: null,
|
||||
});
|
||||
|
||||
useEffect(()=>{
|
||||
if(query.filters?.categories || query.query)
|
||||
navigate({to: "/app/browser/search"})
|
||||
},[query])
|
||||
useEffect(() => {
|
||||
if (query.filters?.categories || query.query)
|
||||
navigate({ to: '/app/browser/search' });
|
||||
}, [query]);
|
||||
|
||||
const context = useMemo<BrowserControllerType>(() => ({
|
||||
cluster,
|
||||
|
|
@ -65,10 +65,10 @@ export function useBrowserSearch(provider: Provider, query: SearchQuery, options
|
|||
return useCommand('searchPackages', () => bindings.core.searchPackages(provider, validFilters ? query : { ...query, filters: null }), options);
|
||||
}
|
||||
|
||||
export function usePackageData(provider: Provider, slug: string, options?: Omit<UndefinedInitialDataOptions<ManagedPackage>, "queryKey" | "queryFn"> | undefined){
|
||||
return useCommand("getPackage", () => bindings.core.getPackage(provider, slug), options)
|
||||
export function usePackageData(provider: Provider, slug: string, options?: Omit<UndefinedInitialDataOptions<ManagedPackage>, 'queryKey' | 'queryFn'> | undefined) {
|
||||
return useCommand('getPackage', () => 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">){
|
||||
return useCommand("getPackageVersions", ()=>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_versions, loaders, offset, limit, ...options }: { mc_versions?: Array<string> | null; loaders?: Array<GameLoader> | null; offset?: number; limit: number } & Omit<UndefinedInitialDataOptions<Paginated<ManagedVersion>>, 'queryKey' | 'queryFn'>) {
|
||||
return useCommand('getPackageVersions', () => bindings.core.getPackageVersions(provider, slug, mc_versions ?? null, loaders ?? null, (offset ?? 0) as unknown as bigint, limit as unknown as bigint), options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import type { Provider } from '@/bindings.gen';
|
||||
import OneConfigLogo from '@/assets/logos/oneconfig.svg';
|
||||
import { PackageGrid } from '@/components/content/PackageItem';
|
||||
import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser';
|
||||
import { useBrowserSearch, usePackageData } from '@/hooks/useBrowser';
|
||||
import { PROVIDERS } from '@/utils';
|
||||
import { Button, Show } from '@onelauncher/common/components';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { ChevronRightIcon } from '@untitled-theme/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { BrowserLayout } from './route';
|
||||
|
||||
export const Route = createFileRoute('/app/browser/')({
|
||||
component: RouteComponent
|
||||
component: RouteComponent,
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -26,17 +26,21 @@ function RouteComponent() {
|
|||
}
|
||||
|
||||
function Featured() {
|
||||
const context = useBrowserContext();
|
||||
// const context = useBrowserContext();
|
||||
const [provider, slug] = useMemo<[Provider, string]>(() => ['Modrinth', 'sodium'], []);
|
||||
const featuredPackage = usePackageData(provider, slug);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Show when={!context.query.query && !(context.query.filters?.categories && context.query.filters.categories.length > 0)}>
|
||||
<Show when={featuredPackage.isSuccess}>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<h5 className="ml-2">Featured</h5>
|
||||
<div className="w-full flex flex-row overflow-hidden rounded-lg bg-page-elevated">
|
||||
<div className="w-full p-1">
|
||||
<img alt="thumbnail" className="aspect-ratio-video h-full rounded-md object-cover object-center" src="https://cdn.modrinth.com/data/AANobbMI/295862f4724dc3f78df3447ad6072b2dcd3ef0c9_96.webp" />
|
||||
<img alt="thumbnail" className="aspect-ratio-video h-full rounded-md object-cover object-center" src={featuredPackage.data?.icon_url ?? ''} />
|
||||
</div>
|
||||
<div className="max-w-84 min-w-52 flex flex-col gap-y-1 p-4">
|
||||
<h2>Sodium</h2>
|
||||
<h2>{featuredPackage.data?.name}</h2>
|
||||
|
||||
<Show when={false}>
|
||||
<div className="w-fit flex flex-row items-center gap-x-1 rounded-lg bg-border/10 px-1.5 py-1 text-fg-primary transition hover:opacity-80">
|
||||
|
|
@ -45,10 +49,10 @@ function Featured() {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
<p className="mt-1 flex-1 leading-normal">Peak mod</p>
|
||||
<p className="mt-1 flex-1 leading-normal">{featuredPackage.data?.short_desc}</p>
|
||||
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button color="ghost">
|
||||
<Button color="ghost" onClick={() => navigate({ to: '/app/browser/package/$provider/$slug', params: { provider, slug } })}>
|
||||
View Package
|
||||
<ChevronRightIcon />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,45 +1,45 @@
|
|||
import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, Paginated, Provider } from '@/bindings.gen'
|
||||
import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser'
|
||||
import { useClusters } from '@/hooks/useCluster'
|
||||
import { bindings } from '@/main'
|
||||
import { abbreviateNumber, formatAsRelative, PROVIDERS } from '@/utils'
|
||||
import { useCommand } from '@onelauncher/common'
|
||||
import { Show, Tooltip, Button, Dropdown } from '@onelauncher/common/components'
|
||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { Download01Icon, LinkExternal01Icon, File02Icon, CalendarIcon, ClockRewindIcon, ChevronDownIcon, ChevronUpIcon } from '@untitled-theme/icons-react'
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Collection, ListBox, ListBoxItem, Popover, Select } from 'react-aria-components'
|
||||
|
||||
import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, Paginated, Provider } from '@/bindings.gen';
|
||||
import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser';
|
||||
import { useClusters } from '@/hooks/useCluster';
|
||||
import { bindings } from '@/main';
|
||||
import { abbreviateNumber, formatAsRelative, PROVIDERS } from '@/utils';
|
||||
import { useCommand } from '@onelauncher/common';
|
||||
import { Button, Show, Tooltip } from '@onelauncher/common/components';
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { CalendarIcon, ChevronDownIcon, ChevronUpIcon, ClockRewindIcon, Download01Icon, File02Icon, LinkExternal01Icon } from '@untitled-theme/icons-react';
|
||||
import { createContext, useContext, useMemo, useRef, useState } from 'react';
|
||||
import { Collection, ListBox, ListBoxItem, Popover, Select } from 'react-aria-components';
|
||||
|
||||
export const Route = createFileRoute('/app/browser/package/$provider/$slug')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
});
|
||||
|
||||
function includes<T, C extends T>(list: { includes: (arg0: C) => boolean }, element: T): element is C {
|
||||
return list.includes(element as unknown as C)
|
||||
function includes<T, TArray extends T>(list: { includes: (arg0: TArray) => boolean }, element: T): element is TArray {
|
||||
return list.includes(element as unknown as TArray);
|
||||
}
|
||||
|
||||
type PackageContextType = {
|
||||
pkg: ManagedPackage | undefined
|
||||
versions: Paginated<ManagedVersion> | undefined
|
||||
interface PackageContextType {
|
||||
pkg: ManagedPackage | undefined;
|
||||
versions: Paginated<ManagedVersion> | undefined;
|
||||
}
|
||||
|
||||
const PackageContext = createContext<PackageContextType>({
|
||||
pkg: undefined,
|
||||
versions: undefined
|
||||
})
|
||||
versions: undefined,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { provider, slug } = Route.useParams()
|
||||
if (!includes(PROVIDERS, provider)) throw new Error("Invalid provider");
|
||||
const packageData = usePackageData(provider, slug)
|
||||
const browserContext = useBrowserContext()
|
||||
const { provider, slug } = Route.useParams();
|
||||
if (!includes(PROVIDERS, provider))
|
||||
throw new Error('Invalid provider');
|
||||
const packageData = usePackageData(provider, slug);
|
||||
const browserContext = useBrowserContext();
|
||||
const { data: versions } = usePackageVersions(provider, slug, {
|
||||
mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : null,
|
||||
loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : null,
|
||||
limit: 50
|
||||
})
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
return (
|
||||
<PackageContext value={{ pkg: packageData.data, versions }}>
|
||||
|
|
@ -49,9 +49,8 @@ function RouteComponent() {
|
|||
|
||||
<div className="min-h-full flex flex-1 flex-col items-start gap-y-4 pb-8">
|
||||
<div className="flex flex-none flex-row gap-x-1 rounded-lg bg-component-bg p-1">
|
||||
<Link to='/app/browser/package/$provider/$slug' params={{ provider, slug }}>About</Link>
|
||||
<Link to='/app/browser/package/$provider/$slug' params={{ provider, slug }}>Versions</Link>
|
||||
|
||||
<Link params={{ provider, slug }} to="/app/browser/package/$provider/$slug">About</Link>
|
||||
<Link params={{ provider, slug }} to="/app/browser/package/$provider/$slug">Versions</Link>
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -62,17 +61,16 @@ function RouteComponent() {
|
|||
</div>
|
||||
</Show>
|
||||
</PackageContext>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
|
||||
const { provider } = Route.useParams()
|
||||
const { provider } = Route.useParams();
|
||||
|
||||
const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, []);
|
||||
const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, []);
|
||||
|
||||
const authors = useCommand("getUsersFromAuthor", () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author))
|
||||
const authors = useCommand('getUsersFromAuthor', () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author));
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-1 max-w-60 min-w-54 flex flex-col gap-y-4">
|
||||
|
|
@ -106,7 +104,7 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<InstallButton {...pkg} />
|
||||
<InstallButton />
|
||||
|
||||
{/* <div className="flex flex-col gap-2 rounded-lg bg-component-bg p-3">
|
||||
<h4 className="text-fg-primary font-bold">Links</h4>
|
||||
|
|
@ -121,7 +119,7 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
|
|||
<h4 className="text-fg-primary font-bold">Authors</h4>
|
||||
{
|
||||
authors.isSuccess
|
||||
? authors.data.map(author => <Author author={author} />)
|
||||
? authors.data.map(author => <Author author={author} key={author.id} />)
|
||||
: <h3>Loading...</h3>
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +131,7 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
|
|||
<div className="flex flex-row items-start gap-x-1">
|
||||
<File02Icon className="h-3 min-w-3 w-3" />
|
||||
License
|
||||
<Link to={pkg.license?.url ?? "#"}>
|
||||
<Link to={pkg.license?.url ?? '#'}>
|
||||
{pkg.license?.name || pkg.license?.id || 'Unknown'}
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -168,60 +166,50 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
function colorForType(type: string) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'release':
|
||||
return 'bg-code-trace';
|
||||
case 'snapshot':
|
||||
return 'bg-code-debug';
|
||||
case 'beta':
|
||||
return 'bg-code-warn';
|
||||
case 'alpha':
|
||||
return 'bg-code-error';
|
||||
default:
|
||||
return 'bg-border/05';
|
||||
}
|
||||
}
|
||||
|
||||
function InstallButton({ ...pkg }: ManagedPackage) {
|
||||
const triggerRef = useRef<HTMLDivElement>(null)
|
||||
const { provider, slug } = Route.useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
const clusters = useClusters()
|
||||
const browserContext = useBrowserContext()
|
||||
const { versions } = useContext(PackageContext)
|
||||
function InstallButton() {
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const { provider } = Route.useParams();
|
||||
const [open, setOpen] = useState(false);
|
||||
const clusters = useClusters();
|
||||
const browserContext = useBrowserContext();
|
||||
const { versions } = useContext(PackageContext);
|
||||
const version = useMemo(() => {
|
||||
if (!versions || !browserContext.cluster) return undefined;
|
||||
if (!versions || !browserContext.cluster)
|
||||
return undefined;
|
||||
return versions.items.findLast(version =>
|
||||
version.mc_versions.includes(browserContext.cluster!.mc_version)
|
||||
&& version.loaders.includes(browserContext.cluster!.mc_loader)
|
||||
)
|
||||
}, [browserContext.cluster, versions])
|
||||
&& version.loaders.includes(browserContext.cluster!.mc_loader));
|
||||
}, [browserContext.cluster, versions]);
|
||||
|
||||
function download() {
|
||||
if (!version || !browserContext.cluster || !includes(PROVIDERS, provider)) return false;
|
||||
downloadPackage(browserContext.cluster, provider, version)
|
||||
if (!version || !browserContext.cluster || !includes(PROVIDERS, provider))
|
||||
return false;
|
||||
downloadPackage(browserContext.cluster, provider, version);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className="h-12 flex flex-row w-full relative">
|
||||
<Button
|
||||
color={version ? "primary" : "secondary"}
|
||||
className="max-w-full flex-1 rounded-r-none!"
|
||||
onClick={download}
|
||||
className="max-w-full flex-1 rounded-r-none disabled:text-white/50 disabled:bg-blue-900"
|
||||
color="primary"
|
||||
isDisabled={!version}
|
||||
onClick={download}
|
||||
>
|
||||
<Download01Icon />
|
||||
<div className='w-full text-sm'>
|
||||
<div className="w-full text-sm">
|
||||
{
|
||||
browserContext.cluster ?
|
||||
version
|
||||
? <span>Download latest to <br /><span className='text-md font-semibold'>{browserContext.cluster.name}</span></span>
|
||||
browserContext.cluster
|
||||
? version
|
||||
? (
|
||||
<span>
|
||||
Download latest to
|
||||
<br />
|
||||
<span className="text-md font-semibold">{browserContext.cluster.name}</span>
|
||||
</span>
|
||||
)
|
||||
: `No matching version found`
|
||||
: "Select a Cluster"
|
||||
: 'Select a Cluster'
|
||||
}
|
||||
</div>
|
||||
</Button>
|
||||
|
|
@ -229,42 +217,50 @@ function InstallButton({ ...pkg }: ManagedPackage) {
|
|||
<Button className="w-8 h-full rounded-l-none border-l border-white/10" onClick={() => setOpen(true)}>
|
||||
{open
|
||||
? <ChevronUpIcon />
|
||||
: <ChevronDownIcon />
|
||||
}
|
||||
: <ChevronDownIcon />}
|
||||
</Button>
|
||||
<Select onOpenChange={setOpen} isOpen={open}
|
||||
aria-label='cluster'
|
||||
onSelectionChange={e => {
|
||||
const cluster = clusters?.find(cluster => cluster.id as unknown as number == e)
|
||||
if (cluster) browserContext.setCluster(cluster)
|
||||
}}>
|
||||
<div className="w-full h-full absolute -z-10" ref={triggerRef}></div>
|
||||
<Select
|
||||
aria-label="cluster" isOpen={open}
|
||||
onOpenChange={setOpen}
|
||||
onSelectionChange={(e) => {
|
||||
const cluster = clusters?.find(cluster => cluster.id as unknown as number === e);
|
||||
if (cluster)
|
||||
browserContext.setCluster(cluster);
|
||||
}}
|
||||
>
|
||||
<div className="w-full h-full absolute -z-10" ref={triggerRef}></div>
|
||||
|
||||
<Popover triggerRef={triggerRef} className="mt-1 rounded-lg shadow-md bg-component-bg border border-component-border -translate-x-full" style={{ width: `${triggerRef.current?.clientWidth}px` }}>
|
||||
<Popover className="mt-1 rounded-lg shadow-md bg-component-bg border border-component-border -translate-x-full" style={{ width: `${triggerRef.current?.clientWidth}px` }} triggerRef={triggerRef}>
|
||||
<ListBox className="outline-none flex flex-col gap-0.5">
|
||||
<Collection items={clusters}>
|
||||
{item => <ListBoxItem id={item.id as unknown as number}
|
||||
className="group/item flex flex-row items-center justify-between gap-2 rounded-lg p-2 w-full text-dropdown-item-text hover:bg-component-bg-hover data-[focused]:bg-component-bg-pressed">{item.name}</ListBoxItem>}
|
||||
{item => (
|
||||
<ListBoxItem
|
||||
className="group/item flex flex-row items-center justify-between gap-2 rounded-lg p-2 w-full text-dropdown-item-text hover:bg-component-bg-hover data-[focused]:bg-component-bg-pressed"
|
||||
id={item.id as unknown as number}
|
||||
>
|
||||
{item.name}
|
||||
</ListBoxItem>
|
||||
)}
|
||||
</Collection>
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function Author({ author }: { author: ManagedUser }) {
|
||||
|
||||
//TODO: onclick and fallback avatar url
|
||||
// TODO: onclick and fallback avatar url
|
||||
return (
|
||||
<a
|
||||
className="flex flex-row items-center gap-x-1 rounded-md p-1 active:bg-component-bg-pressed hover:bg-component-bg-hover"
|
||||
onClick={() => { if (author.url) openUrl(author.url) }}
|
||||
onClick={() => {
|
||||
if (author.url)
|
||||
openUrl(author.url);
|
||||
}}
|
||||
>
|
||||
<img alt={`${author.username}'s avatar`} className="h-8 min-h-8 min-w-8 w-8 rounded-[5px]" src={author.avatar_url || ""} />
|
||||
<img alt={`${author.username}'s avatar`} className="h-8 min-h-8 min-w-8 w-8 rounded-[5px]" src={author.avatar_url || ''} />
|
||||
<div className="flex flex-1 flex-col justify-center gap-y-1">
|
||||
<span>{author.username}</span>
|
||||
|
||||
|
|
@ -279,9 +275,9 @@ function Author({ author }: { author: ManagedUser }) {
|
|||
<LinkExternal01Icon className="h-4 w-4" />
|
||||
</a>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function downloadPackage(cluster: ClusterModel, provider: Provider, version: ManagedVersion, skipCompatibility = false) {
|
||||
return bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility)
|
||||
return bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue