diff --git a/apps/oneclient/frontend/package.json b/apps/oneclient/frontend/package.json index 93908972..e4e50039 100644 --- a/apps/oneclient/frontend/package.json +++ b/apps/oneclient/frontend/package.json @@ -28,7 +28,6 @@ "@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:", diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 6ef73ffb..425e76b7 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -232,7 +232,7 @@ export type VersionType = */ "old_beta" -const ARGS_MAP = { 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'core':'{"getClusterById":["id"],"getPackage":["provider","slug"],"getMultiplePackages":["provider","slugs"],"getLoadersForVersion":["mc_version"],"getClusters":[],"getLogs":["id"],"getProfileOrDefault":["name"],"openMsaLogin":[],"getUsersFromAuthor":["provider","author"],"launchCluster":["id","uuid"],"getLogByName":["id","name"],"fetchMinecraftProfile":["uuid"],"removeUser":["uuid"],"getUser":["uuid"],"killProcess":["pid"],"updateClusterById":["id","request"],"getGameVersions":[],"isClusterRunning":["cluster_id"],"removeCluster":["id"],"createCluster":["options"],"getRunningProcesses":[],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getPackageBody":["provider","body"],"readSettings":[],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"updateClusterProfile":["name","profile"],"getWorlds":["id"],"getUsers":[],"getRunningProcessesByClusterId":["cluster_id"],"getGlobalProfile":[],"getDefaultUser":["fallback"],"writeSettings":["setting"],"searchPackages":["provider","query"],"setDefaultUser":["uuid"],"getScreenshots":["id"]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}' } +const ARGS_MAP = { 'folders':'{"openCluster":["folder_name"],"fromCluster":["folder_name"]}', 'core':'{"createCluster":["options"],"getGlobalProfile":[],"getLoadersForVersion":["mc_version"],"getScreenshots":["id"],"getUsers":[],"getPackage":["provider","slug"],"getDefaultUser":["fallback"],"getPackageBody":["provider","body"],"writeSettings":["setting"],"open":["input"],"isClusterRunning":["cluster_id"],"getMultiplePackages":["provider","slugs"],"killProcess":["pid"],"updateClusterProfile":["name","profile"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getUsersFromAuthor":["provider","author"],"getLogByName":["id","name"],"removeUser":["uuid"],"fetchMinecraftProfile":["uuid"],"openMsaLogin":[],"getProfileOrDefault":["name"],"getUser":["uuid"],"getClusters":[],"getWorlds":["id"],"launchCluster":["id","uuid"],"readSettings":[],"removeCluster":["id"],"searchPackages":["provider","query"],"getPackageVersions":["provider","slug","mc_version","loader","offset","limit"],"getGameVersions":[],"setDefaultUser":["uuid"],"getClusterById":["id"],"updateClusterById":["id","request"],"getRunningProcessesByClusterId":["cluster_id"],"getLogs":["id"],"getRunningProcesses":[]}', 'oneclient':'{"openDevTools":[],"getClustersGroupedByMajor":[]}', 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}' } export type Router = { 'folders': { fromCluster: (folderName: string) => Promise, openCluster: (folderName: string) => Promise }, 'core': { getClusters: () => Promise, @@ -269,7 +269,8 @@ getMultiplePackages: (provider: Provider, slugs: string[]) => Promise Promise>, downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: number, skipCompatibility: boolean | null) => Promise, getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise, -fetchMinecraftProfile: (uuid: string) => Promise }, +fetchMinecraftProfile: (uuid: string) => Promise, +open: (input: string) => Promise }, 'events': { ingress: (event: IngressPayload) => Promise, message: (event: MessagePayload) => Promise, process: (event: ProcessPayload) => Promise }, diff --git a/apps/oneclient/frontend/src/components/ExternalLink.tsx b/apps/oneclient/frontend/src/components/ExternalLink.tsx new file mode 100644 index 00000000..0dd9dad5 --- /dev/null +++ b/apps/oneclient/frontend/src/components/ExternalLink.tsx @@ -0,0 +1,81 @@ +import type React from 'react'; +import type { HTMLProps } from 'react'; +import { bindings } from '@/main'; +import { LinkExternal01Icon } from '@untitled-theme/icons-react'; +import { useState } from 'react'; +import { twMerge } from 'tailwind-merge'; +import { Overlay } from './overlay'; + +export interface ExternalLinkProps extends HTMLProps { + href?: string | undefined; + children?: React.ReactNode; + includeIcon?: boolean | undefined; + showModal?: boolean | undefined; +} + +export function ExternalLink({ + href, + children, + includeIcon, + showModal = true, + className, + ...rest +}: ExternalLinkProps) { + const [isOpen, setIsOpen] = useState(false); + + // done like this so that "image" links don't have an icon + const withIcon = !!(includeIcon ?? typeof children === 'string'); + + return ( + + { + e.preventDefault(); + if (!href) + return; + + if (showModal) + setIsOpen(true); + else + bindings.core.open(href); + }} + role="button" + {...rest} + > + {children} + {withIcon && } + + + + + External Link +

Are you sure you want to open this link in your browser?

+ + {href} + + { + if (href) + bindings.core.open(href); + + setIsOpen(false); + }, + }, + ]} + /> +
+
+
+ ); +} diff --git a/apps/oneclient/frontend/src/components/Markdown.module.css b/apps/oneclient/frontend/src/components/Markdown.module.css new file mode 100644 index 00000000..714ab4f3 --- /dev/null +++ b/apps/oneclient/frontend/src/components/Markdown.module.css @@ -0,0 +1,148 @@ +.markdown-body h1:first-child { + margin-block-start: 0; + padding-block-start: 0; +} + .markdown-body blockquote, .markdown-body details, .markdown-body dl, .markdown-body ol, .markdown-body p, .markdown-body code, .markdown-body pre, .markdown-body table, .markdown-body ul { + margin-top: 0; + margin-bottom: 16px; +} + .markdown-body li, .markdown-body p { + padding: 0; + line-height: 1.5; +} + .markdown-body h1 { + display: block; +} + .markdown-body h1, .markdown-body h2, .markdown-body h3 { + color: var(--color-contrast); +} + .markdown-body h1, .markdown-body h2 { + padding: 10px 0 5px; + border-bottom: 1px solid var(--color-divider); +} + .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6, .markdown-body li, .markdown-body p { + word-wrap: break-word; + overflow-wrap: anywhere; +} + .markdown-body blockquote { + padding: 0 1em; + color: var(--color-base); + border-left: 0.25em solid var(--color-button-bg); + margin-inline: 0; +} + .markdown-body a { + cursor: pointer; + color: var(--color-blue); +} + .markdown-body a:focus-visible, .markdown-body a:hover { + filter: brightness(1.2); + text-decoration: none; +} + .markdown-body a:active { + filter: brightness(1.1); + text-decoration: none; +} + .markdown-body a:active > img { + scale: 0.98; +} + .markdown-body img { + max-width: 100%; + height: auto; + display: inline-block; +} + .markdown-body html:not(.dark-mode, .oled-mode)selfimg[src$='#gh-dark-mode-only'] { + display: none; +} + .markdown-body html:is(.dark-mode, .oled-mode)selfimg[src$='#gh-light-mode-only'] { + display: none; +} + .markdown-body pre { + margin-top: 1rem; + padding: 14px; + border-radius: var(--radius-sm); + background-color: var(--color-button-bg); + overflow-x: auto; +} + .markdown-body pre code { + font-size: 80%; + padding: 0; + border-radius: 0; +} + .markdown-body code { + padding: 0.2em 0.4em; + font-size: 80%; + border-radius: var(--radius-sm); + background-color: var(--color-button-bg); + color: var(--color-contrast); +} + .markdown-body hr { + margin: 20px 0; + color: var(--color-button-bg); +} + .markdown-body table { + display: block; + width: max-content; + max-width: 100%; + overflow: auto; + border-collapse: separate; + border-spacing: 0; + line-height: 1.5; + border: 0.1rem solid var(--color-button-bg); + border-radius: var(--radius-sm); +} + .markdown-body table th { + font-weight: 600; +} + .markdown-body table td, .markdown-body table th { + padding: 0.4rem 0.85rem; +} + .markdown-body table tr:nth-child(2n) { + background-color: var(--color-accent-contrast); +} + .markdown-body table td:not(:last-of-type), .markdown-body table th:not(:last-of-type) { + border-right: 0.1rem solid var(--color-button-bg); +} + .markdown-body table tr:not(:last-of-type) td, .markdown-body table th { + border-bottom: 0.1rem solid var(--color-button-bg); +} + .markdown-body details { + border: 0.15rem solid var(--color-button-bg); + border-radius: var(--radius-sm); + padding: 0.5rem 0.5rem 0; +} + .markdown-body details summary { + font-weight: bold; + margin: -0.5rem -0.5rem 0; + padding: 0.5rem 0.8rem; + cursor: pointer; + background-color: var(--color-button-bg); + border-radius: var(--radius-xs); +} + .markdown-body details summary:hover { + filter: brightness(0.85); +} + .markdown-body details[open] { + padding: 0.5rem; +} + .markdown-body details[open] summary { + margin-bottom: 0.5rem; + border-radius: var(--radius-xs) var(--radius-xs) 0 0; +} + .markdown-body details[open] > :last-child:not(summary) { + margin-bottom: 0 !important; +} + .markdown-body > :last-child { + margin-bottom: 0 !important; +} + .markdown-body iframe, .markdown-body video { + aspect-ratio: 1.7777777778; + width: 850px; + max-width: 100%; + height: auto; +} + @media screen and (max-width: 1280px) { + .markdown-body iframe, .markdown-body video { + width: 100%; + } +} + \ No newline at end of file diff --git a/apps/oneclient/frontend/src/components/Markdown.tsx b/apps/oneclient/frontend/src/components/Markdown.tsx new file mode 100644 index 00000000..6b598756 --- /dev/null +++ b/apps/oneclient/frontend/src/components/Markdown.tsx @@ -0,0 +1,28 @@ +import ReactMarkdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; +import { twMerge } from 'tailwind-merge'; +import { ExternalLink } from './ExternalLink'; +import styles from './Markdown.module.css'; + +export function Markdown({ + body, + className, +}: { + body: string; + className?: string | undefined; +}) { + return ( +
+ , + }} + rehypePlugins={[rehypeRaw]} + remarkPlugins={[remarkGfm]} + > + {body} + +
+ ); +} diff --git a/apps/oneclient/frontend/src/components/PackageItem.tsx b/apps/oneclient/frontend/src/components/PackageItem.tsx index 0ff0fe49..fd8985b9 100644 --- a/apps/oneclient/frontend/src/components/PackageItem.tsx +++ b/apps/oneclient/frontend/src/components/PackageItem.tsx @@ -1,5 +1,4 @@ import type { Provider, SearchResult } from '@/bindings.gen'; -import { Show, Tooltip } from '@onelauncher/common/components'; import { Link } from '@tanstack/react-router'; export function PackageGrid({ items, provider, clusterId }: { items: Array; provider: Provider; clusterId: number }) { @@ -21,6 +20,7 @@ export function PackageItem({ provider, clusterId, ...item }: SearchResult & { p return ( - - )} - when={item.icon_url} - > - {`Icon - {`Icon - - {/* - -
- {loaders.toSpliced(loaders.length > 3 ? 2 : 3).map(loader => )} - {loaders.length > 3 && ( -
- - + - {loaders.length - 2} - -
- )} -
-
-
*/} + {item.icon_url + ? ( + <> + {`Icon + {`Icon + + ) + : ( +
+ )}
@@ -74,15 +60,6 @@ export function PackageItem({ provider, clusterId, ...item }: SearchResult & { p

{item.description}

- - {/*
- -
- - {abbreviateNumber(item.downloads)} -
-
-
*/}
); diff --git a/apps/oneclient/frontend/src/components/overlay/Overlay.tsx b/apps/oneclient/frontend/src/components/overlay/Overlay.tsx index 28efd91a..b2899277 100644 --- a/apps/oneclient/frontend/src/components/overlay/Overlay.tsx +++ b/apps/oneclient/frontend/src/components/overlay/Overlay.tsx @@ -1,6 +1,8 @@ +import type { ButtonProps } from '@onelauncher/common/components'; +import type { HTMLAttributes } from 'react'; import { Button } from '@onelauncher/common/components'; import { XCloseIcon } from '@untitled-theme/icons-react'; -import { Dialog, Modal, ModalOverlay } from 'react-aria-components'; +import { Dialog, DialogTrigger, Modal, ModalOverlay } from 'react-aria-components'; import { twMerge } from 'tailwind-merge'; import { tv } from 'tailwind-variants'; @@ -51,6 +53,34 @@ export function Overlay({ ); } +Overlay.Trigger = DialogTrigger; + +Overlay.Buttons = function OverlayButtons({ + className, + buttons, +}: Omit, 'children'> & { + buttons?: Array; +}) { + return ( +
= 3 ? 'flex-col' : 'flex-row', + className, + )} + > + {buttons?.map(({ key, size, color, ...rest }) => ( +
+ ); +}; + Overlay.Dialog = function OverlayDialog({ className, children, diff --git a/apps/oneclient/frontend/src/routes/app/cluster/browser/index.tsx b/apps/oneclient/frontend/src/routes/app/cluster/browser/index.tsx index 0d8af0f2..01e8f954 100644 --- a/apps/oneclient/frontend/src/routes/app/cluster/browser/index.tsx +++ b/apps/oneclient/frontend/src/routes/app/cluster/browser/index.tsx @@ -1,13 +1,14 @@ -import type { Filters, PackageCategories, PackageType, Provider } from '@/bindings.gen'; +import type { Filters, PackageCategories, PackageType, Provider, Sort } from '@/bindings.gen'; import { LoaderSuspense } from '@/components'; import { PackageGrid } from '@/components/PackageItem'; import { bindings } from '@/main'; import { browserCategories, categoryNameFromId } from '@/utils/browser'; import { useCommandSuspense, usePagination } from '@onelauncher/common'; -import { Dropdown, Show, TextField } from '@onelauncher/common/components'; +import { Dropdown, TextField } from '@onelauncher/common/components'; import { createFileRoute } from '@tanstack/react-router'; import { SearchMdIcon } from '@untitled-theme/icons-react'; import { useEffect, useMemo, useRef, useState } from 'react'; +import { twMerge } from 'tailwind-merge'; interface BrowserSearchRouteSearchParams { query?: string; @@ -46,13 +47,10 @@ function RouteComponent() {
- + -
- - {/* */} @@ -60,14 +58,16 @@ function RouteComponent() { } function BrowserCategories() { - const search = Route.useSearch(); - const { categories, packageType } = search; + const { categories, packageType } = Route.useSearch(); + const navigate = Route.useNavigate(); const navRef = useRef(navigate); + const switchCategory = useMemo(() => (category: string) => { const newCategories = categories.includes(category) ? categories.filter(element => element !== category) : [...categories, category]; + navRef.current({ search: prev => ({ ...prev, @@ -78,23 +78,22 @@ function BrowserCategories() { return (
- {/*
*/}
- -
-
Categories
- {browserCategories[categoryNameFromId[packageType]].map(category => ( -

switchCategory(category)} - > - {category} -

- ))} -
-
+
+
Categories
+ {browserCategories[categoryNameFromId[packageType]].map(category => ( +

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

+ ))} +
); @@ -104,10 +103,13 @@ function BrowserToolbar() { const search = Route.useSearch(); const { provider, query } = search; const navigate = Route.useNavigate(); + const setQuery = (query: string) => { navigate({ search: { ...search, query } }); }; + const [searchBar, setSearchBar] = useState(query); + return (
@@ -150,17 +152,18 @@ interface SearchQuery { query: string | null; offset: number | null; limit: number; - sort: 'Relevance' | 'Downloads' | 'Newest' | 'Updated' | null; + sort: Sort; filters: Filters | null; } const itemsPerPage = 25; -function SearchResult() { +function SearchResults() { const search = Route.useSearch(); const { cluster } = Route.useRouteContext(); const navigate = Route.useNavigate(); const navRef = useRef(navigate); + const query = useMemo(() => ({ query: search.query ?? null, offset: (search.page - 1) * itemsPerPage, @@ -173,30 +176,38 @@ function SearchResult() { package_type: search.packageType, }, }), [cluster, search]); + const results = useCommandSuspense(['searchPackages', search], () => bindings.core.searchPackages( search.provider, query, )); + const pagination = usePagination({ itemsPerPage: query.limit, itemsCount: results.data.total, }); - const [oldTotal, setOldTotal] = useState(pagination.totalPages); + + const oldTotal = useRef(pagination.totalPages); const paginationRef = useRef(pagination); + useEffect(() => { navRef.current({ search: prev => ({ ...prev, page: pagination.page }) }); }, [pagination.page]); + useEffect(() => { - if (pagination.totalPages === oldTotal) + if (pagination.totalPages === oldTotal.current) return; + paginationRef.current.reset(); - setOldTotal(pagination.totalPages); + oldTotal.current = pagination.totalPages; }, [pagination.totalPages]); + 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 index c5826ee1..f325a871 100644 --- a/apps/oneclient/frontend/src/routes/app/cluster/browser/package.tsx +++ b/apps/oneclient/frontend/src/routes/app/cluster/browser/package.tsx @@ -1,17 +1,14 @@ import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, PackageDonationUrl, Provider } from '@/bindings.gen'; -import type { HTMLProps } from 'react'; +import { LoaderSuspense } from '@/components'; +import { ExternalLink } from '@/components/ExternalLink'; +import { Markdown } from '@/components/Markdown'; 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 { abbreviateNumber, formatAsRelative, upperFirst, useCommand, useCommandMut, useCommandSuspense } from '@onelauncher/common'; +import { Button, Tooltip } from '@onelauncher/common/components'; +import { createFileRoute, redirect } from '@tanstack/react-router'; +import { CalendarIcon, ClockRewindIcon, Download01Icon, File02Icon } 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; @@ -22,22 +19,20 @@ export const Route = createFileRoute('/app/cluster/browser/package')({ validateSearch: (search): PackageRouteSearchParams => ({ packageId: search.packageId as string, }), - async beforeLoad({ context, search }) { + async beforeLoad({ 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 }; + + const managedPackage = await bindings.core.getPackage(search.provider, search.packageId); + + return { managedPackage }; }, }); function RouteComponent() { - const { managedPackage, packageBody } = Route.useRouteContext(); + const { managedPackage } = Route.useRouteContext(); + return ( - //
@@ -45,24 +40,21 @@ function RouteComponent() { About Versions - Gallery + {/* Gallery */} - -
- , - }} - rehypePlugins={[rehypeRaw]} - remarkPlugins={[remarkGfm]} - > - {packageBody} - -
+ + + + + - - + + + + + + {/* {managedPackage.gallery.map(item => ( @@ -81,26 +73,22 @@ function RouteComponent() { -
- //
); } -function CustomA({ href, children, includeIcon, className, ...rest }: { href: string; children: any; includeIcon?: boolean } & HTMLProps) { +function BodyPanel() { + const { provider } = Route.useSearch(); + const { managedPackage } = Route.useRouteContext(); + + const { data: body } = useCommandSuspense(['getPackageBody', provider, managedPackage.body], () => bindings.core.getPackageBody(provider, managedPackage.body)); + return ( - { - e.preventDefault(); - openUrl(href); - }} - {...rest} - > - {children} - {includeIcon && } - +
+
+ +
+
); } @@ -118,7 +106,7 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { 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)); + const authors = useCommand(['getUsersFromAuthor', pkg.author], () => bindings.core.getUsersFromAuthor(provider, pkg.author)); return (
@@ -142,12 +130,12 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {

{pkg.short_desc}

- + {pkg.provider !== 'SkyClient' && (
{abbreviateNumber(pkg.downloads)}
-
+ )}
@@ -157,15 +145,15 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {

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 => ( -

Details

- + {pkg.license !== null && (
License - - {pkg.license?.name || pkg.license?.id || 'Unknown'} - + + {pkg.license.name || pkg.license.id || 'Unknown'} +
-
+ )} - - + {createdAt !== null && ( +
Created - {formatAsRelative(createdAt!.getTime(), 'en', 'long')} + {formatAsRelative(createdAt.getTime(), 'en', 'long')}
-
+ )} - - + {updatedAt !== null && ( +
Last Updated - {formatAsRelative(updatedAt!.getTime(), 'en', 'long')} + {formatAsRelative(updatedAt.getTime(), 'en', 'long')}
-
+ )}
); } +function usePackageVersions() { + const { provider, packageId } = Route.useSearch(); + const { cluster } = Route.useRouteContext(); + const { data: versions } = useCommandSuspense( + ['getPackageVersions', provider, packageId, cluster.mc_version, cluster.mc_loader], + () => bindings.core.getPackageVersions(provider, packageId, cluster.mc_version, cluster.mc_loader, 0, 64), + ); + + return versions; +} + 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)); + const { cluster, managedPackage } = Route.useRouteContext(); + const packageVersions = usePackageVersions(); + + const { provider } = Route.useSearch(); + const download = useCommandMut(() => bindings.core.downloadPackage(provider, managedPackage.id, packageVersions.items[0].version_id, cluster.id, 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} - + {author.is_organization_user && ( Organization - + )} - + {author.role !== null && ( {author.role} - + )}
- -
+ ); } @@ -306,7 +310,7 @@ function Versions() { // offset, // }); - const { packageVersions: versions } = Route.useRouteContext(); + const versions = usePackageVersions(); // const pagination = usePagination({ // itemsCount: versions?.total as unknown as number, @@ -324,7 +328,7 @@ function Versions() {
{/* */}
- +
Name @@ -334,8 +338,8 @@ function Versions() { Downloads - - {versions?.items.map(item => + + {versions.items.map(item => )}
diff --git a/packages/core/src/api/tauri/commands.rs b/packages/core/src/api/tauri/commands.rs index 825a0d18..7cfcf3f4 100644 --- a/packages/core/src/api/tauri/commands.rs +++ b/packages/core/src/api/tauri/commands.rs @@ -176,6 +176,9 @@ pub trait TauriLauncherApi { async fn fetch_player_profile( uuid: String, ) -> LauncherResult; + + // MARK: API: Other + async fn open(input: String) -> LauncherResult<()>; } #[derive(serde::Serialize, serde::Deserialize, specta::Type, Clone)] @@ -617,4 +620,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl { ) -> LauncherResult { crate::utils::minecraft::fetch_player_profile(&uuid).await } + + // MARK: Impl: Other + async fn open(self, input: String) -> LauncherResult<()> { + opener::open(input)?; + Ok(()) + } }