diff --git a/apps/oneclient/frontend/package.json b/apps/oneclient/frontend/package.json index 90354df2..a8df14a4 100644 --- a/apps/oneclient/frontend/package.json +++ b/apps/oneclient/frontend/package.json @@ -14,6 +14,7 @@ "@fontsource/poppins": "catalog:", "@formatjs/intl-durationformat": "catalog:", "@onelauncher/common": "workspace:*", + "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "catalog:", "@tanstack/match-sorter-utils": "catalog:", "@tanstack/react-devtools": "catalog:", @@ -27,6 +28,7 @@ "@tauri-apps/api": "catalog:", "@tauri-apps/plugin-clipboard-manager": "catalog:", "@tauri-apps/plugin-dialog": "catalog:", + "@tauri-apps/plugin-opener": "catalog:", "@untitled-theme/icons-react": "catalog:", "motion": "catalog:", "overlayscrollbars": "catalog:", @@ -35,6 +37,9 @@ "react-aria-components": "catalog:", "react-dom": "catalog:", "react-just-parallax": "catalog:", + "react-markdown": "^10.1.0", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", "skinview3d": "catalog:", "tailwind-merge": "catalog:", "tailwind-variants": "catalog:", diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index d3e6253c..4692aaac 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -167,7 +167,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" @@ -230,11 +230,9 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'folders':'{"fromCluster":["folder_name"],"openCluster":["folder_name"]}', 'events':'{"message":["event"],"ingress":["event"],"process":["event"]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[]}', 'core':'{"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getProfileOrDefault":["name"],"updateClusterById":["id","request"],"createCluster":["options"],"getLogByName":["id","name"],"getGameVersions":[],"getScreenshots":["id"],"writeSettings":["setting"],"getMultiplePackages":["provider","slugs"],"getLogs":["id"],"removeUser":["uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getDefaultUser":["fallback"],"getLoadersForVersion":["mc_version"],"getClusters":[],"setDefaultUser":["uuid"],"getGlobalProfile":[],"getClusterById":["id"],"updateClusterProfile":["name","profile"],"getUsers":[],"getUsersFromAuthor":["provider","author"],"openMsaLogin":[],"launchCluster":["id","uuid"],"searchPackages":["provider","query"],"getWorlds":["id"],"getUser":["uuid"],"getPackageBody":["provider","body"],"readSettings":[],"removeCluster":["id"],"getPackage":["provider","slug"],"fetchMinecraftProfile":["uuid"]}' } +const ARGS_MAP = { 'core':'{"getClusterById":["id"],"getLogByName":["id","name"],"getLogs":["id"],"openMsaLogin":[],"writeSettings":["setting"],"removeUser":["uuid"],"searchPackages":["provider","query"],"getClusters":[],"updateClusterById":["id","request"],"getUser":["uuid"],"getPackage":["provider","slug"],"getLoadersForVersion":["mc_version"],"getUsers":[],"getScreenshots":["id"],"getPackageBody":["provider","body"],"launchCluster":["id","uuid"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"updateClusterProfile":["name","profile"],"getMultiplePackages":["provider","slugs"],"setDefaultUser":["uuid"],"getWorlds":["id"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getUsersFromAuthor":["provider","author"],"fetchMinecraftProfile":["uuid"],"removeCluster":["id"],"readSettings":[],"getGlobalProfile":[],"getDefaultUser":["fallback"],"getGameVersions":[],"createCluster":["options"],"getProfileOrDefault":["name"]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}' } export type Router = { 'oneclient': { openDevTools: () => Promise, getClustersGroupedByMajor: () => Promise> }, -'folders': { fromCluster: (folderName: string) => Promise, -openCluster: (folderName: string) => Promise }, 'core': { getClusters: () => Promise, getClusterById: (id: number) => Promise, removeCluster: (id: number) => Promise, @@ -268,7 +266,9 @@ getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise Promise }, 'events': { ingress: (event: IngressPayload) => Promise, message: (event: MessagePayload) => Promise, -process: (event: ProcessPayload) => Promise } }; +process: (event: ProcessPayload) => Promise }, +'folders': { fromCluster: (folderName: string) => Promise, +openCluster: (folderName: string) => Promise } }; export type { InferCommandOutput } diff --git a/apps/oneclient/frontend/src/components/PackageItem.tsx b/apps/oneclient/frontend/src/components/PackageItem.tsx index 1e534ba3..0ff0fe49 100644 --- a/apps/oneclient/frontend/src/components/PackageItem.tsx +++ b/apps/oneclient/frontend/src/components/PackageItem.tsx @@ -1,38 +1,29 @@ 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 }) { +export function PackageGrid({ items, provider, clusterId }: { items: Array; provider: Provider; clusterId: number }) { 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]); - +export function PackageItem({ provider, clusterId, ...item }: SearchResult & { provider: Provider; clusterId: number }) { return (
- + ); } diff --git a/apps/oneclient/frontend/src/routes/app/cluster/browser/package.tsx b/apps/oneclient/frontend/src/routes/app/cluster/browser/package.tsx new file mode 100644 index 00000000..c5826ee1 --- /dev/null +++ b/apps/oneclient/frontend/src/routes/app/cluster/browser/package.tsx @@ -0,0 +1,377 @@ +import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, PackageDonationUrl, Provider } from '@/bindings.gen'; +import type { HTMLProps } from 'react'; +import { bindings } from '@/main'; +import { abbreviateNumber, formatAsRelative, upperFirst, useCommand, useCommandMut } from '@onelauncher/common'; +import { Button, Show, Tooltip } from '@onelauncher/common/components'; +import { createFileRoute, Link, redirect } from '@tanstack/react-router'; +import { openUrl } from '@tauri-apps/plugin-opener'; +import { CalendarIcon, ClockRewindIcon, Download01Icon, File02Icon, LinkExternal01Icon } from '@untitled-theme/icons-react'; +import { useMemo } from 'react'; +import { Cell, Column, Row, Tab, Table, TableBody, TableHeader, TabList, TabPanel, Tabs } from 'react-aria-components'; +import Markdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; +import { twMerge } from 'tailwind-merge'; + +export interface PackageRouteSearchParams { + packageId: string; +} + +export const Route = createFileRoute('/app/cluster/browser/package')({ + component: RouteComponent, + validateSearch: (search): PackageRouteSearchParams => ({ + packageId: search.packageId as string, + }), + async beforeLoad({ context, search }) { + if (!search.packageId) + throw redirect({ to: '/app/cluster', from: '/app/cluster/browser/package', search }); + const [managedPackage, packageVersions] = await Promise.all([ + bindings.core.getPackage(search.provider, search.packageId), + bindings.core.getPackageVersions(search.provider, search.packageId, context.cluster.mc_version, context.cluster.mc_loader, 0, 64), + ]); + const packageBody = await bindings.core.getPackageBody(search.provider, managedPackage.body); + return { managedPackage, packageBody, packageVersions }; + }, +}); + +function RouteComponent() { + const { managedPackage, packageBody } = Route.useRouteContext(); + return ( + // +
+ + + + + About + Versions + Gallery + + +
+ , + }} + rehypePlugins={[rehypeRaw]} + remarkPlugins={[remarkGfm]} + > + {packageBody} + +
+
+ + + + + + {/* {managedPackage.gallery.map(item => ( + + +
+ + {item.title &&
{item.title}
} +
+
+ + + +
+ ))} */} + +
+
+ +
+ //
+ ); +} + +function CustomA({ href, children, includeIcon, className, ...rest }: { href: string; children: any; includeIcon?: boolean } & HTMLProps) { + return ( + { + e.preventDefault(); + openUrl(href); + }} + {...rest} + > + {children} + {includeIcon && } + + ); +} + +function getPackageUrl(pkg: ManagedPackage): string { + switch (pkg.provider) { + case 'Modrinth': return `https://modrinth.com/project/${pkg.slug}`; + case 'CurseForge': return `https://www.curseforge.com/minecraft/${pkg.package_type}s/${pkg.slug}`; + case 'SkyClient': return ``; + } +} + +function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { + const { provider } = Route.useSearch(); + + const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, [pkg.created]); + const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, [pkg.updated]); + + const authors = useCommand(['getUsersFromAuthor', pkg.author], () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author)); + + return ( +
+
+
+ {`Icon + {`Icon +
+
+
+

{pkg.name}

+

+ {pkg.package_type} + {' '} + on + {' '} + {pkg.provider} +

+
+ +

{pkg.short_desc}

+ +
+ +
+ + {abbreviateNumber(pkg.downloads)} +
+
+
+
+
+ + + +
+

Links

+
+ + {provider} + {' '} + Page + + {(Object.entries(pkg.links) as Array<[keyof typeof pkg.links, typeof pkg.links[keyof typeof pkg.links]]>).filter(a => a[1]).map(link => + typeof link[1] == 'string' + ? ( + + ) + : (link[1] as Array).map(donationLink => ( + + )))} +
+
+ +
+

Authors

+ { + authors.isSuccess + ? authors.data.map(author => ) + :

Loading...

+ } + +
+ +
+

Details

+ +
+ + License + + {pkg.license?.name || pkg.license?.id || 'Unknown'} + +
+
+ + + +
+ + Created + + {formatAsRelative(createdAt!.getTime(), 'en', 'long')} + +
+
+
+ + + +
+ + Last Updated + + {formatAsRelative(updatedAt!.getTime(), 'en', 'long')} + +
+
+
+
+ +
+ ); +} + +function InstallButton() { + const { packageVersions, managedPackage } = Route.useRouteContext(); + const { provider, clusterId } = Route.useSearch(); + const download = useCommandMut(() => bindings.core.downloadPackage(provider, managedPackage.id, packageVersions.items[0].version_id, clusterId, true)); + return ( + + ); +} + +function Author({ author }: { author: ManagedUser }) { + // TODO: onclick and fallback avatar url + return ( + { + if (author.url) + openUrl(author.url); + }} + > + {`${author.username}'s +
+ {author.username} + + + Organization + + + + {author.role} + +
+ +
+ + ); +} + +function colorForType(type: string) { + switch (type.toLowerCase()) { + case 'release': + return 'bg-code-trace/30'; + case 'snapshot': + return 'bg-code-debug/30'; + case 'beta': + return 'bg-code-warn/30'; + case 'alpha': + return 'bg-code-error/30'; + default: + return 'bg-border/30'; + } +} + +function Versions() { + // const { provider } = Route.useSearch(); + // const [offset, setOffset] = useState(0); + + // const { data: versions } = usePackageVersions(provider, id, { + // limit: 20, + // offset, + // }); + + const { packageVersions: versions } = Route.useRouteContext(); + + // const pagination = usePagination({ + // itemsCount: versions?.total as unknown as number, + // itemsPerPage: versions?.limit as unknown as number, + // }); + // const paginationRef = useRef(pagination); + // useEffect(() => { + // setOffset(pagination.offset); + // }, [pagination.offset]); + // useEffect(() => { + // paginationRef.current.reset(); + // }, [versions?.total]); + return ( +
+
+ {/* */} +
+ + + + Name + Game Versions + Loaders + Created + Downloads + + + + {versions?.items.map(item => + )} + +
+
+ ); +} +function VersionRow({ version }: { version: ManagedVersion }) { + const { cluster } = Route.useRouteContext(); + const { provider } = Route.useSearch(); + return ( + + + +
+ {version.release_type.charAt(0).toUpperCase()} +
+
+
+ {version.display_name} + {version.mc_versions.join(', ')} + {version.loaders.map(upperFirst).join(', ')} + {formatAsRelative(Date.parse(version.published))} + {version.downloads} + +