diff --git a/.github/actions/publish-artifacts/src/index.ts b/.github/actions/publish-artifacts/src/index.ts index d9a30ec8..48c5ba53 100644 --- a/.github/actions/publish-artifacts/src/index.ts +++ b/.github/actions/publish-artifacts/src/index.ts @@ -20,7 +20,7 @@ interface TargetConfig { } interface BuildTarget { updater: false | Updater; - standalone: TargetConfig[]; + standalone: Array; } const osTargets = { @@ -67,7 +67,7 @@ const frontendBundle = 'apps/frontend/dist.tar.xz'; const updaterName = `OneLauncher-Updater-${os}-${arch}`; const frontendName = `OneLauncher-Frontend-${os}-${arch}`; -const globFiles = async (pattern: string): Promise => await (await glob.create(pattern)).glob(); +const globFiles = async (pattern: string): Promise> => await (await glob.create(pattern)).glob(); async function uploadFrontend() { if (!(await exists(frontendBundle))) { diff --git a/apps/oneclient/frontend/package.json b/apps/oneclient/frontend/package.json index c97fb0fb..7a5ea661 100644 --- a/apps/oneclient/frontend/package.json +++ b/apps/oneclient/frontend/package.json @@ -8,8 +8,7 @@ "build": "vite build && tsc", "serve": "vite preview", "lint": "eslint", - "format": "prettier", - "check": "prettier --write . && eslint --fix" + "check": "eslint --fix" }, "dependencies": { "@fontsource/poppins": "catalog:", diff --git a/apps/oneclient/frontend/prettier.config.js b/apps/oneclient/frontend/prettier.config.js deleted file mode 100644 index adcd2f4d..00000000 --- a/apps/oneclient/frontend/prettier.config.js +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-check - -/** @type {import('prettier').Config} */ -const config = { - semi: true, - singleQuote: true, - trailingComma: 'all', -}; - -export default config; diff --git a/apps/oneclient/frontend/src/routes/app/route.tsx b/apps/oneclient/frontend/src/routes/app/route.tsx index 6a4fd5bf..5de18320 100644 --- a/apps/oneclient/frontend/src/routes/app/route.tsx +++ b/apps/oneclient/frontend/src/routes/app/route.tsx @@ -1,10 +1,11 @@ -import { useEffect, type PropsWithChildren } from 'react'; +import type { PropsWithChildren } from 'react'; import { LoaderSuspense, Navbar } from '@/components'; import { GameBackground } from '@/components/GameBackground'; import useAppShellStore from '@/stores/appShellStore'; import { AnimatedOutlet } from '@onelauncher/common/components'; import { createFileRoute, useRouter } from '@tanstack/react-router'; import { AnimatePresence } from 'motion/react'; +import { useEffect } from 'react'; export const Route = createFileRoute('/app')({ component: RouteComponent, @@ -40,12 +41,12 @@ function AppShell({ const setPrevLocation = useAppShellStore(state => state.setPrevLocation); useEffect(() => { - const unsub = router.subscribe("onBeforeNavigate", (e) => { + const unsub = router.subscribe('onBeforeNavigate', (e) => { setPrevLocation(e.fromLocation ?? null); }); return () => unsub(); - }, [router]); + }, [router, setPrevLocation]); return (
diff --git a/apps/oneclient/frontend/tsconfig.json b/apps/oneclient/frontend/tsconfig.json index 3dd88644..e442b494 100644 --- a/apps/oneclient/frontend/tsconfig.json +++ b/apps/oneclient/frontend/tsconfig.json @@ -25,5 +25,5 @@ "skipLibCheck": true, "noUncheckedSideEffectImports": true }, - "include": ["**/*.ts", "**/*.tsx", "prettier.config.js", "vite.config.js"] + "include": ["**/*.ts", "**/*.tsx", "vite.config.ts"] } diff --git a/apps/oneclient/frontend/vite.config.js b/apps/oneclient/frontend/vite.config.ts similarity index 100% rename from apps/oneclient/frontend/vite.config.js rename to apps/oneclient/frontend/vite.config.ts diff --git a/apps/onelauncher/desktop/capabilities/default.json b/apps/onelauncher/desktop/capabilities/default.json index b4bfbae1..2c30d3fa 100644 --- a/apps/onelauncher/desktop/capabilities/default.json +++ b/apps/onelauncher/desktop/capabilities/default.json @@ -42,6 +42,8 @@ "path": "$DATA/OneLauncher" } ] - } + }, + "opener:allow-open-url", + "opener:default" ] } diff --git a/apps/onelauncher/frontend/package.json b/apps/onelauncher/frontend/package.json index 4d0a5931..09f0ddd0 100644 --- a/apps/onelauncher/frontend/package.json +++ b/apps/onelauncher/frontend/package.json @@ -35,6 +35,7 @@ "react-dom": "catalog:", "react-markdown": "^10.1.0", "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", "tailwind-merge": "catalog:", "tailwind-variants": "catalog:", "tailwindcss": "catalog:", diff --git a/apps/onelauncher/frontend/src/components/content/PackageItem.tsx b/apps/onelauncher/frontend/src/components/content/PackageItem.tsx index ef7c9acf..1a1ef302 100644 --- a/apps/onelauncher/frontend/src/components/content/PackageItem.tsx +++ b/apps/onelauncher/frontend/src/components/content/PackageItem.tsx @@ -1,7 +1,7 @@ import type { Provider, SearchResult } from '@/bindings.gen'; import { abbreviateNumber, LOADERS, upperFirst } from '@/utils'; import { Show, Tooltip } from '@onelauncher/common/components'; -import { Link, useNavigate } from '@tanstack/react-router'; +import { Link } from '@tanstack/react-router'; import { Download01Icon } from '@untitled-theme/icons-react'; import { useMemo } from 'react'; import { Focusable } from 'react-aria-components'; @@ -13,7 +13,7 @@ function includes(list: { includes: (arg0: TArray) => boole export function PackageGrid({ items, provider }: { items: Array; provider: Provider }) { return ( -
+
{items.map(item => ( ))} @@ -22,7 +22,7 @@ export function PackageGrid({ items, provider }: { items: Array; p } export function PackageItem({ provider, ...item }: SearchResult & { provider: Provider }) { - const loaders = useMemo(() => item.categories.filter(cat => includes(LOADERS, cat)), []); + const loaders = useMemo(() => item.categories.filter(cat => includes(LOADERS, cat)), [item.categories]); return ( ({ loader: 'vanilla', clusterName: '', @@ -63,6 +64,15 @@ export function NewClusterCreate() { }), { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['getClusters'] }); + // Modal'ı kapat ve formu sıfırla + setIsModalOpen(false); + setCurrentStepIndex(0); + setFormData({ + loader: 'vanilla', + clusterName: '', + clusterVersion: '', + clusterProvider: '', + }); }, }); @@ -154,32 +164,30 @@ export function NewClusterCreate() { />
- - + - -
- + +
+ -
-
- {stepContent} -
- - +
+
+ {stepContent}
+ +
- - +
+
); } diff --git a/apps/onelauncher/frontend/src/components/overlay/Modal.tsx b/apps/onelauncher/frontend/src/components/overlay/Modal.tsx index a25e524f..3f6a0e81 100644 --- a/apps/onelauncher/frontend/src/components/overlay/Modal.tsx +++ b/apps/onelauncher/frontend/src/components/overlay/Modal.tsx @@ -1,18 +1,68 @@ import type { DialogProps } from 'react-aria-components'; +import type { VariantProps } from 'tailwind-variants'; import { Dialog as AriaDialog, Modal as AriaModal, DialogTrigger, ModalOverlay, } from 'react-aria-components'; +import { twMerge } from 'tailwind-merge'; +import { tv } from 'tailwind-variants'; -interface ModalProps extends DialogProps { } +const modalVariants = tv({ + slots: { + overlay: [ + 'fixed inset-0 z-50', + 'flex items-center justify-center', + 'bg-black/50 p-4', + + 'data-[entering]:animate-in data-[entering]:fade-in data-[entering]:duration-200', + 'data-[exiting]:animate-out data-[exiting]:fade-out data-[exiting]:duration-150', + ], + modal: [ + 'data-[entering]:animate-in data-[entering]:zoom-in-95 data-[entering]:duration-200', + 'data-[exiting]:animate-out data-[exiting]:zoom-out-95 data-[exiting]:duration-150', + ], + dialog: [ + 'outline-none', + ], + }, +}); + +export interface ModalProps extends DialogProps, VariantProps { + isOpen?: boolean; + onOpenChange?: (isOpen: boolean) => void; + isDismissable?: boolean; + overlayClassName?: string; + modalClassName?: string; +} + +function Modal({ + className, + isOpen, + onOpenChange, + isDismissable = true, + overlayClassName, + modalClassName, + ...props +}: ModalProps) { + const { overlay, modal, dialog } = modalVariants(); + + const isControlled = isOpen !== undefined || onOpenChange !== undefined; + + const overlayProps = { + className: twMerge(overlay(), overlayClassName), + isDismissable, + ...(isControlled && { isOpen, onOpenChange }), + }; -function Modal({ ...props }: ModalProps) { return ( - - - + + + ); diff --git a/apps/onelauncher/frontend/src/hooks/useBrowser.tsx b/apps/onelauncher/frontend/src/hooks/useBrowser.tsx index f98628e1..0d907c73 100644 --- a/apps/onelauncher/frontend/src/hooks/useBrowser.tsx +++ b/apps/onelauncher/frontend/src/hooks/useBrowser.tsx @@ -32,7 +32,12 @@ export function BrowserProvider(props: any) { const [provider, setProvider] = useState(PROVIDERS[0]); const navigate = useNavigate(); const [query, setQuery] = useState({ - filters: null, + filters: { + categories: null, + game_versions: null, + loaders: null, + package_types: ['mod'], + }, query: null, limit: 18 as unknown as bigint, offset: null, @@ -42,7 +47,7 @@ export function BrowserProvider(props: any) { useEffect(() => { if (query.filters?.categories || query.query) navigate({ to: '/app/browser/search' }); - }, [query]); + }, [navigate, query]); const context = useMemo(() => ({ cluster, @@ -70,6 +75,6 @@ export function usePackageData(provider: Provider, slug: string, options?: Omit< return useCommand(key, () => bindings.core.getPackage(provider, slug), options); } -export function usePackageVersions(provider: Provider, slug: string, { mc_versions, loaders, offset, limit, ...options }: { mc_versions?: Array | null; loaders?: Array | null; offset?: number; limit: number } & Omit>, '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 | null; loaders?: Array | null; offset?: number; limit: number } & Omit>, 'queryKey' | 'queryFn'>, key: false | BindingCommands | (string & {}) = `getPackageVersions.${provider}.${slug}.${mc_versions?.join('')}.${loaders?.join('')}`) { + return useCommand(key, () => bindings.core.getPackageVersions(provider, slug, mc_versions ?? null, loaders ?? null, (offset ?? 0) as unknown as bigint, limit as unknown as bigint), options); } diff --git a/apps/onelauncher/frontend/src/hooks/useCluster.ts b/apps/onelauncher/frontend/src/hooks/useCluster.ts deleted file mode 100644 index b9db6006..00000000 --- a/apps/onelauncher/frontend/src/hooks/useCluster.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { ClusterModel } from '@/bindings.gen'; -import { bindings } from '@/main'; -import { useCommand } from '@onelauncher/common'; -import { useEffect, useState } from 'react'; - -export function useRecentCluster() { - const result = useCommand('getClusters', bindings.core.getClusters); - const [cluster, setCluster] = useState(); - - useEffect(() => { - if (!result.data) - return; - - let mostRecentCluster: ClusterModel | undefined; - - for (const c of result.data) { - if (!mostRecentCluster) { - mostRecentCluster = c; - continue; - } - - const currentPlayed = mostRecentCluster.last_played; - const newPlayed = c.last_played; - - if (typeof currentPlayed !== 'string' && typeof newPlayed === 'string') { - mostRecentCluster = c; - } - else if (typeof currentPlayed === 'string' && typeof newPlayed === 'string') { - const playedAt = new Date(currentPlayed); - const clusterPlayedAt = new Date(newPlayed); - - if (clusterPlayedAt > playedAt) - mostRecentCluster = c; - } - } - - setCluster(mostRecentCluster); - }, [result.data]); - - return cluster; -} - -export function useClusters() { - const result = useCommand('getClusters', bindings.core.getClusters); - const [clusters, setClusters] = useState | undefined>(); - - useEffect(() => { - if (!result.data) - return; - setClusters(result.data); - }, [result.data]); - - return clusters; -} diff --git a/apps/onelauncher/frontend/src/hooks/useCluster.tsx b/apps/onelauncher/frontend/src/hooks/useCluster.tsx new file mode 100644 index 00000000..23220181 --- /dev/null +++ b/apps/onelauncher/frontend/src/hooks/useCluster.tsx @@ -0,0 +1,167 @@ +import type { ClusterModel } from '@/bindings.gen'; +import type { ModalProps } from '@/components/overlay/Modal'; +import Modal from '@/components/overlay/Modal'; +import { bindings } from '@/main'; +import { useCommand } from '@onelauncher/common'; +import { Button, Dropdown } from '@onelauncher/common/components'; +import { useEffect, useState } from 'react'; +import { Heading } from 'react-aria-components'; + +export function useRecentCluster() { + const result = useCommand('getClusters', bindings.core.getClusters); + const [cluster, setCluster] = useState(); + + useEffect(() => { + if (!result.data) + return; + + let mostRecentCluster: ClusterModel | undefined; + + for (const c of result.data) { + if (!mostRecentCluster) { + mostRecentCluster = c; + continue; + } + + const currentPlayed = mostRecentCluster.last_played; + const newPlayed = c.last_played; + + if (typeof currentPlayed !== 'string' && typeof newPlayed === 'string') { + mostRecentCluster = c; + } + else if (typeof currentPlayed === 'string' && typeof newPlayed === 'string') { + const playedAt = new Date(currentPlayed); + const clusterPlayedAt = new Date(newPlayed); + + if (clusterPlayedAt > playedAt) + mostRecentCluster = c; + } + } + + setCluster(mostRecentCluster); + }, [result.data]); + + return cluster; +} + +export function useClusters() { + const result = useCommand('getClusters', bindings.core.getClusters); + const [clusters, setClusters] = useState | undefined>(); + + useEffect(() => { + if (!result.data) + return; + setClusters(result.data); + }, [result.data]); + + return clusters; +} + +type ChooseClusterModalProps = ModalProps & { + clusters?: Array; + confirmText?: string; + selected?: (clusters: Array) => number; + onSelected?: (cluster: ClusterModel) => void; +}; + +export function ChooseClusterModal({ clusters: clusterList, selected: defaultSelected, onSelected, confirmText, ...props }: ChooseClusterModalProps) { + const clusters = useClusters(); + const [selected, setSelected] = useState(0); + + useEffect(() => { + if (!clusters) + return; + + const newSelected = defaultSelected?.(clusters); + + if (newSelected) + setSelected(newSelected); + }, [clusters, defaultSelected]); + + function chooseCluster() { + if (clusters !== undefined) { + const cluster = clusters[selected] as ClusterModel | undefined; + if (cluster !== undefined) + onSelected?.(cluster); + } + } + + return ( + +
+ + Select a Cluster + { + setSelected(index as number); + }} + selectedKey={selected} + > + {clusters?.map((cluster, index) => ( + + {cluster.name} + + ))} + + +
+
+
+
+ ); + + // return ( + // , + // ); } diff --git a/apps/onelauncher/frontend/src/hooks/useSettings.tsx b/apps/onelauncher/frontend/src/hooks/useSettings.tsx index cf820eee..927ea61b 100644 --- a/apps/onelauncher/frontend/src/hooks/useSettings.tsx +++ b/apps/onelauncher/frontend/src/hooks/useSettings.tsx @@ -3,7 +3,7 @@ import type { Settings } from '@/bindings.gen'; import { bindings } from '@/main'; import { useCommand } from '@onelauncher/common'; -import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; +import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; interface SettingsControllerType { settings: Settings | undefined; @@ -12,7 +12,7 @@ interface SettingsControllerType { setSettingsToSave: (settings: Partial | null) => void; settingsChanged: boolean; setSettingsChanged: (changed: boolean) => void; - createSetting: (name: K, value: V) => [V, (value: V) => void]; + createSetting: (name: K, value: V) => [V, (v: V) => void]; } const SettingsContext = createContext(null); @@ -51,14 +51,14 @@ export function SettingsProvider({ children }: SettingsProviderProps) { return () => window.removeEventListener('beforeunload', handleBeforeUnload); }, []); - const saveChangedSettings = () => { + const saveChangedSettings = useCallback(() => { if (!settings) return; writeQuery.refetch(); setSettingsChanged(false); - }; + }, [settings, writeQuery]); const handleSetSettingsToSave = useCallback((newSettings: Partial | null) => { setSettingsToSave(newSettings); @@ -86,7 +86,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) { return [value, setValue]; }, [settings]); - const controller: SettingsControllerType = { + const controller: SettingsControllerType = useMemo(() => ({ settings, saveChangedSettings, settingsToSave, @@ -94,7 +94,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) { settingsChanged, setSettingsChanged, createSetting, - }; + }), [createSetting, handleSetSettingsToSave, saveChangedSettings, settings, settingsChanged, settingsToSave]); if (settings === undefined) return null; diff --git a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx index f376a604..d1cb8ab7 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx @@ -32,6 +32,7 @@ function Featured() { const navigate = useNavigate(); const featuredImageIndex = useMemo(() => featuredPackage.data?.gallery.findIndex(image => image.featured), [featuredPackage.data]); const [selectedImage, setSelectedImage] = useState(featuredImageIndex ?? 0); + return (
@@ -40,7 +41,7 @@ function Featured() {
thumbnail
-
+

{featuredPackage.data?.name}

diff --git a/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx index 4bb30c43..5a649bb0 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx @@ -1,18 +1,22 @@ -import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, Paginated, Provider } from '@/bindings.gen'; +import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, PackageDonationUrl, Provider } from '@/bindings.gen'; +import type { HTMLProps } from 'react'; import Modal from '@/components/overlay/Modal'; import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser'; -import { useClusters } from '@/hooks/useCluster'; +import { ChooseClusterModal, useClusters } from '@/hooks/useCluster'; +import usePagination from '@/hooks/usePagination'; import { bindings } from '@/main'; -import { abbreviateNumber, formatAsRelative, PROVIDERS } from '@/utils'; +import { abbreviateNumber, formatAsRelative, PROVIDERS, upperFirst } from '@/utils'; import { useCommand } from '@onelauncher/common'; -import { Button, Popup, Show, Tooltip } from '@onelauncher/common/components'; +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, useEffect, useMemo, useRef, useState } from 'react'; -import { Collection, ListBox, ListBoxItem, Popover, Pressable, Select, Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; +import { createContext, useEffect, useMemo, useRef, useState } from 'react'; +import { Cell, Collection, Column, ListBox, ListBoxItem, Popover, Pressable, Row, Select, 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 const Route = createFileRoute('/app/browser/package/$provider/$slug')({ component: RouteComponent, @@ -22,14 +26,29 @@ function includes(list: { includes: (arg0: TArray) => boole return list.includes(element as unknown as TArray); } +function CustomA({ href, children, includeIcon, className, ...rest }: { href: string; children: any; includeIcon?: boolean } & HTMLProps) { + return ( + { + e.preventDefault(); + openUrl(href); + }} + {...rest} + > + {children} + {includeIcon && } + + ); +} + interface PackageContextType { pkg: ManagedPackage | undefined; - versions: Paginated | undefined; } const PackageContext = createContext({ pkg: undefined, - versions: undefined, }); function RouteComponent() { @@ -37,15 +56,12 @@ function RouteComponent() { 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, - }); + const packageContextValue = useMemo(() => ({ + pkg: packageData.data, + }), [packageData.data]); return ( - +
@@ -56,31 +72,39 @@ function RouteComponent() { Versions Gallery -
- - {packageData.data?.body} - - - versions - - + +
+ , + }} + rehypePlugins={[rehypeRaw]} + remarkPlugins={[remarkGfm]} + > + {packageData.data?.body} + +
+
+ + + + - {packageData.data?.gallery.map(item => ( - - -
- - {item.title &&
{item.title}
} -
-
- - - -
- ))} + {packageData.data?.gallery.map(item => ( + + +
+ + {item.title &&
{item.title}
} +
+
+ + + +
+ ))} -
-
+
@@ -89,16 +113,24 @@ function RouteComponent() { ); } +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.useParams(); - const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, []); - const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, []); + 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', () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author)); return ( -
+
{`Icon @@ -131,14 +163,36 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { - {/*
+

Links

- - {contentPackage.provider} - {' '} - Page - -
*/} +
+ + {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

@@ -193,17 +247,21 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { function InstallButton() { const triggerRef = useRef(null); - const { provider } = Route.useParams(); + const { provider, slug } = Route.useParams(); + if (!includes(PROVIDERS, provider)) + throw new Error('invalid provider'); const [open, setOpen] = useState(false); const clusters = useClusters(); const browserContext = useBrowserContext(); - const { versions } = useContext(PackageContext); + const { data: versions } = usePackageVersions(provider, slug, { + mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : [], + loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : [], + limit: 1, + }); const version = useMemo(() => { 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)); + return versions.items[0]; }, [browserContext.cluster, versions]); function download() { @@ -248,7 +306,7 @@ function InstallButton() { aria-label="cluster" isOpen={open} onOpenChange={setOpen} onSelectionChange={(e) => { - const cluster = clusters?.find(cluster => cluster.id as unknown as number === e); + const cluster = clusters?.find(item => item.id as unknown as number === e); if (cluster) browserContext.setCluster(cluster); }} @@ -303,6 +361,97 @@ function Author({ author }: { author: ManagedUser }) { ); } +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, slug } = Route.useParams(); + if (!includes(PROVIDERS, provider)) + throw new Error('invalid provider'); + const [offset, setOffset] = useState(0); + const { data: versions } = usePackageVersions(provider, slug, { + limit: 20, + offset, + }); + + const pagination = usePagination({ + itemsCount: versions?.total as unknown as number, + itemsPerPage: versions?.limit as unknown as number, + }); + useEffect(() => { + setOffset(pagination.offset); + }, [pagination.offset]); + useEffect(() => { + pagination.reset(); + }, [versions?.total]); + return ( +
+
+ +
+ + + + Name + Game Versions + Loaders + Created + Downloads + + + + {versions?.items.map(item => + )} + +
+
+ ); +} + +function VersionRow({ version }: { version: ManagedVersion }) { + const { provider } = Route.useParams(); + if (!includes(PROVIDERS, provider)) + throw new Error('invalid provider'); + 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} + + +