From 4e60aefc9de0a23e85d94b01577b7d8c3c51c54e Mon Sep 17 00:00:00 2001 From: emirsassan Date: Mon, 21 Jul 2025 00:09:59 +0300 Subject: [PATCH 01/10] refactor: modal component --- .../launcher/cluster/ClusterCreation.tsx | 52 +++++++++------- .../frontend/src/components/overlay/Modal.tsx | 61 +++++++++++++++++-- 2 files changed, 85 insertions(+), 28 deletions(-) diff --git a/apps/onelauncher/frontend/src/components/launcher/cluster/ClusterCreation.tsx b/apps/onelauncher/frontend/src/components/launcher/cluster/ClusterCreation.tsx index 65b8dff7..29dbc97a 100644 --- a/apps/onelauncher/frontend/src/components/launcher/cluster/ClusterCreation.tsx +++ b/apps/onelauncher/frontend/src/components/launcher/cluster/ClusterCreation.tsx @@ -41,6 +41,7 @@ interface FormValidation { export function NewClusterCreate() { const queryClient = useQueryClient(); const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isModalOpen, setIsModalOpen] = useState(false); const [formData, setFormData] = useState({ 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 373490c4..3f6a0e81 100644 --- a/apps/onelauncher/frontend/src/components/overlay/Modal.tsx +++ b/apps/onelauncher/frontend/src/components/overlay/Modal.tsx @@ -1,19 +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({ className, ...props }: ModalProps) { return ( - - - - + + + ); From 6b42f77ce03ffd732877e04b1493f541e84e65ea Mon Sep 17 00:00:00 2001 From: emirsassan Date: Mon, 21 Jul 2025 11:33:05 +0300 Subject: [PATCH 02/10] feat: open url in the system browser instead of launcher window --- .../desktop/capabilities/default.json | 4 ++- .../app/browser/package.$provider.$slug.tsx | 36 +++++++++++++++---- .../web_common/src/components/Tooltip.tsx | 4 +-- 3 files changed, 34 insertions(+), 10 deletions(-) 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/src/routes/app/browser/package.$provider.$slug.tsx b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx index 4bb30c43..f53350bb 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,4 +1,5 @@ import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, Paginated, Provider } from '@/bindings.gen'; +import type { JSX } from 'react'; import Modal from '@/components/overlay/Modal'; import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser'; import { useClusters } from '@/hooks/useCluster'; @@ -22,6 +23,21 @@ function includes(list: { includes: (arg0: TArray) => boole return list.includes(element as unknown as TArray); } +function CustomA({ href, children }: { href: string; children: any }) { + return ( + { + e.preventDefault(); + openUrl(href); + }} + > + {children} + + ); +} + interface PackageContextType { pkg: ManagedPackage | undefined; versions: Paginated | undefined; @@ -58,23 +74,29 @@ function RouteComponent() {
- {packageData.data?.body} + , + }} rehypePlugins={[rehypeRaw]} + > + {packageData.data?.body} + versions - + {packageData.data?.gallery.map(item => ( -
- - {item.title &&
{item.title}
} +
+ + {item.title &&
{item.title}
}
- - + + ))} diff --git a/packages/web_common/src/components/Tooltip.tsx b/packages/web_common/src/components/Tooltip.tsx index d67ff23e..4682673d 100644 --- a/packages/web_common/src/components/Tooltip.tsx +++ b/packages/web_common/src/components/Tooltip.tsx @@ -11,8 +11,8 @@ import { tv } from 'tailwind-variants'; export const tooltipVariants = tv({ base: [ - 'px-3 py-2 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-lg', - 'border border-gray-700 max-w-xs z-50', + 'px-3 py-2 text-sm font-medium text-white bg-component-bg rounded-lg shadow-lg', + 'border border-component-border max-w-xs z-50', 'data-[entering]:animate-in data-[entering]:fade-in data-[entering]:zoom-in-95', 'data-[exiting]:animate-out data-[exiting]:fade-out data-[exiting]:zoom-out-95', ], From 96907b18add4b3cc9a4a7037917b75174747b26b Mon Sep 17 00:00:00 2001 From: emirsassan Date: Mon, 21 Jul 2025 11:47:26 +0300 Subject: [PATCH 03/10] chore: bigger image display in screenshots --- .../src/routes/app/cluster/screenshots.tsx | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/screenshots.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/screenshots.tsx index 78f3be17..565ac24e 100644 --- a/apps/onelauncher/frontend/src/routes/app/cluster/screenshots.tsx +++ b/apps/onelauncher/frontend/src/routes/app/cluster/screenshots.tsx @@ -1,3 +1,4 @@ +import Modal from '@/components/overlay/Modal'; import ScrollableContainer from '@/components/ScrollableContainer'; import { bindings } from '@/main'; import { useCommand } from '@onelauncher/common'; @@ -6,6 +7,7 @@ import { createFileRoute } from '@tanstack/react-router'; import { convertFileSrc } from '@tauri-apps/api/core'; import { dataDir, join } from '@tauri-apps/api/path'; import { useEffect, useState } from 'react'; +import { Pressable } from 'react-aria-components'; import Sidebar from '../settings/route'; export const Route = createFileRoute('/app/cluster/screenshots')({ @@ -60,15 +62,20 @@ function ScreenshotEntry({ return ( <> -
- {path} -
+ + +
+ {path} +
+
+ + + +
); } From c0b64434329af52cc2dd24e9af7d2f1d5ef76e72 Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:38:12 +0200 Subject: [PATCH 04/10] feat: links and gfm support --- apps/onelauncher/frontend/package.json | 1 + .../app/browser/package.$provider.$slug.tsx | 53 +++++++++++++------ packages/core/src/api/packages/data.rs | 1 + .../src/api/packages/provider/modrinth.rs | 2 + pnpm-lock.yaml | 26 +++++++++ 5 files changed, 68 insertions(+), 15 deletions(-) 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/routes/app/browser/package.$provider.$slug.tsx b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx index f53350bb..64ad7ce0 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,10 +1,10 @@ -import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, Paginated, Provider } from '@/bindings.gen'; -import type { JSX } from 'react'; +import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, PackageDonationUrl, Paginated, Provider } from '@/bindings.gen'; +import type { HTMLProps, JSX } from 'react'; import Modal from '@/components/overlay/Modal'; import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser'; import { useClusters } from '@/hooks/useCluster'; 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 { createFileRoute, Link } from '@tanstack/react-router'; @@ -14,6 +14,8 @@ import { createContext, useContext, useEffect, useMemo, useRef, useState } from import { Collection, ListBox, ListBoxItem, Popover, Pressable, Select, Tab, 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, @@ -23,17 +25,19 @@ function includes(list: { includes: (arg0: TArray) => boole return list.includes(element as unknown as TArray); } -function CustomA({ href, children }: { href: string; children: any }) { +function CustomA({ href, children, includeIcon, className, ...rest }: { href: string; children: any, includeIcon?: boolean } & HTMLProps) { return ( { e.preventDefault(); openUrl(href); }} + {...rest} > {children} + {includeIcon && } ); } @@ -73,11 +77,13 @@ function RouteComponent() { Gallery
- + , - }} rehypePlugins={[rehypeRaw]} + }} + rehypePlugins={[rehypeRaw]} + remarkPlugins={[remarkGfm]} > {packageData.data?.body} @@ -111,6 +117,14 @@ 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(); @@ -120,7 +134,7 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { const authors = useCommand('getUsersFromAuthor', () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author)); return ( -
+
{`Icon @@ -153,14 +167,23 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { - {/*
+

Links

- - {contentPackage.provider} - {' '} - Page - -
*/} +
+ + {provider} + {' '} + Page + + {(Object.entries(pkg.links) as [keyof typeof pkg.links, typeof pkg.links[keyof typeof pkg.links]][]).filter(a=>a[1]).map(link=> + typeof link[1] == "string" + ? + :
+ {(link[1] as PackageDonationUrl[]).map(donationLink=>)} +
+ )} +
+

Authors

diff --git a/packages/core/src/api/packages/data.rs b/packages/core/src/api/packages/data.rs index 8835d57b..5a25a91a 100644 --- a/packages/core/src/api/packages/data.rs +++ b/packages/core/src/api/packages/data.rs @@ -77,6 +77,7 @@ pub struct SearchResult { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ManagedPackage { pub id: String, + pub slug: String, pub provider: Provider, pub package_type: PackageType, pub name: String, diff --git a/packages/core/src/api/packages/provider/modrinth.rs b/packages/core/src/api/packages/provider/modrinth.rs index cc0d4621..00961c4b 100644 --- a/packages/core/src/api/packages/provider/modrinth.rs +++ b/packages/core/src/api/packages/provider/modrinth.rs @@ -346,6 +346,7 @@ struct ModrinthPackage { pub icon_url: Option, #[serde(alias = "project_id")] pub id: String, + pub slug: String, pub team: String, #[serde(default)] pub organization: Option, @@ -373,6 +374,7 @@ impl From for ManagedPackage { Self { provider: Provider::Modrinth, id: value.id, + slug: value.slug, package_type: value.project_type, name: value.title, short_desc: value.description, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d99686d..a3b2d55a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -399,6 +399,9 @@ importers: rehype-raw: specifier: ^7.0.0 version: 7.0.0 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 tailwind-merge: specifier: 'catalog:' version: 3.2.0 @@ -5051,12 +5054,18 @@ packages: rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + remove-accents@0.5.0: resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} @@ -11304,6 +11313,17 @@ snapshots: hast-util-raw: 9.1.0 vfile: 6.0.3 + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -11321,6 +11341,12 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + remove-accents@0.5.0: {} require-from-string@2.0.2: {} From f1ff318a6a62ce0775c5e9654b0564992a5269d7 Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:17:43 +0200 Subject: [PATCH 05/10] default package type --- .../frontend/src/hooks/useBrowser.tsx | 7 ++++- .../app/browser/package.$provider.$slug.tsx | 29 +++++++++---------- .../frontend/src/routes/app/browser/route.tsx | 2 +- .../onelauncher/frontend/src/utils/browser.ts | 4 ++- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/apps/onelauncher/frontend/src/hooks/useBrowser.tsx b/apps/onelauncher/frontend/src/hooks/useBrowser.tsx index f98628e1..a6dd4276 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, 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 64ad7ce0..7920682a 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 @@ -37,7 +37,7 @@ function CustomA({ href, children, includeIcon, className, ...rest }: { href: st {...rest} > {children} - {includeIcon && } + {includeIcon && } ); } @@ -77,7 +77,7 @@ function RouteComponent() { Gallery
- + , @@ -96,7 +96,7 @@ function RouteComponent() { {packageData.data?.gallery.map(item => ( -
+
{item.title &&
{item.title}
}
@@ -118,7 +118,7 @@ function RouteComponent() { } function getPackageUrl(pkg: ManagedPackage): string { - switch (pkg.provider){ + 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 `` @@ -175,12 +175,11 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { {' '} Page - {(Object.entries(pkg.links) as [keyof typeof pkg.links, typeof pkg.links[keyof typeof pkg.links]][]).filter(a=>a[1]).map(link=> + {(Object.entries(pkg.links) as [keyof typeof pkg.links, typeof pkg.links[keyof typeof pkg.links]][]).filter(a => a[1]).map(link => typeof link[1] == "string" - ? - :
- {(link[1] as PackageDonationUrl[]).map(donationLink=>)} -
+ ? + : (link[1] as PackageDonationUrl[]).map(donationLink => ) + )}
@@ -272,12 +271,12 @@ function InstallButton() { browserContext.cluster ? version ? ( - - Download latest to -
- {browserContext.cluster.name} -
- ) + + Download latest to +
+ {browserContext.cluster.name} +
+ ) : `No matching version found` : 'Select a Cluster' } diff --git a/apps/onelauncher/frontend/src/routes/app/browser/route.tsx b/apps/onelauncher/frontend/src/routes/app/browser/route.tsx index 6fac4ec6..ff44bbc0 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/route.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/route.tsx @@ -107,7 +107,7 @@ const defaultFilters: Filters = { function BrowserCategories() { const context = useBrowserContext(); - const categories = browserCategories.byPackageType('mod', context.provider); + const categories = browserCategories.byPackageType((context.query.filters?.package_types ?? ["mod"])[0], context.provider); function switchCategory(category: string) { const newCategories = context.query.filters?.categories?.includes(category) diff --git a/apps/onelauncher/frontend/src/utils/browser.ts b/apps/onelauncher/frontend/src/utils/browser.ts index b32f8c6f..92cc105b 100644 --- a/apps/onelauncher/frontend/src/utils/browser.ts +++ b/apps/onelauncher/frontend/src/utils/browser.ts @@ -1,3 +1,5 @@ +import type { PackageType, Provider } from "@/bindings.gen"; + export const BROWSER_VIEWS: Array = ['grid', 'list'] as const; export interface CategoryItem { @@ -6,7 +8,7 @@ export interface CategoryItem { } export const browserCategories = { - byPackageType(packageType: 'mod' | string & {}, provider: string): Array { + byPackageType(packageType: PackageType, provider: Provider): Array { switch (packageType) { case 'mod': return this.mod(provider); From d803e8606144b01b88ccfda953adb58ffc437fa1 Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Mon, 21 Jul 2025 19:19:10 +0200 Subject: [PATCH 06/10] feat: versions page --- .../frontend/src/hooks/useCluster.ts | 54 ------ .../frontend/src/hooks/useCluster.tsx | 164 +++++++++++++++++ .../app/browser/package.$provider.$slug.tsx | 173 +++++++++++++----- 3 files changed, 296 insertions(+), 95 deletions(-) delete mode 100644 apps/onelauncher/frontend/src/hooks/useCluster.ts create mode 100644 apps/onelauncher/frontend/src/hooks/useCluster.tsx 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..c188f7c1 --- /dev/null +++ b/apps/onelauncher/frontend/src/hooks/useCluster.tsx @@ -0,0 +1,164 @@ +import type { ClusterModel } from '@/bindings.gen'; +import type { ModalProps } from '@/components/overlay/Modal'; +import { bindings } from '@/main'; +import { useCommand } from '@onelauncher/common'; +import { useEffect, useState } from 'react'; +import Modal from '@/components/overlay/Modal'; +import { Button, Dropdown } from '@onelauncher/common/components'; +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?: ClusterModel[]; + confirmText?: string; + selected?: (clusters: ClusterModel[]) => 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]) + + function chooseCluster() { + if (clusters !== undefined && selected !== undefined) { + const cluster = clusters[selected]; + if (cluster !== undefined) + onSelected?.(cluster); + } + } + + return ( + +
+ + Select a Cluster + { + setSelected(index as number) + }} + selectedKey={selected} + > + {clusters?.map((cluster, index) => ( + + {cluster.name} + + ))} + + +
+
+
+
+ ) + + // return ( + // , + //
+
+
- ) + ); // return ( // { - if (props.page === page) + if (pageIndex === page) return; - setPage(props.page); + setPage(pageIndex); }} > - {props.page} + {pageIndex} ); } 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..6d55b3ff 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 (
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 e6a763b2..f39d0fb5 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 @@ -3,6 +3,7 @@ import type { HTMLProps } from 'react'; import Modal from '@/components/overlay/Modal'; import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser'; import { ChooseClusterModal, useClusters } from '@/hooks/useCluster'; +import usePagination from '@/hooks/usePagination'; import { bindings } from '@/main'; import { abbreviateNumber, formatAsRelative, PROVIDERS, upperFirst } from '@/utils'; import { useCommand } from '@onelauncher/common'; @@ -16,7 +17,6 @@ import Markdown from 'react-markdown'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; import { twMerge } from 'tailwind-merge'; -import usePagination from '@/hooks/usePagination'; export const Route = createFileRoute('/app/browser/package/$provider/$slug')({ component: RouteComponent, @@ -26,10 +26,10 @@ 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) { +function CustomA({ href, children, includeIcon, className, ...rest }: { href: string; children: any; includeIcon?: boolean } & HTMLProps) { return ( { e.preventDefault(); @@ -38,7 +38,7 @@ function CustomA({ href, children, includeIcon, className, ...rest }: { href: st {...rest} > {children} - {includeIcon && } + {includeIcon && } ); } @@ -56,10 +56,14 @@ function RouteComponent() { if (!includes(PROVIDERS, provider)) throw new Error('Invalid provider'); const packageData = usePackageData(provider, slug, {}); - const browserContext = useBrowserContext(); + // const browserContext = useBrowserContext(); + + const packageContextValue = useMemo(() => ({ + pkg: packageData.data, + }), [packageData.data]); return ( - +
@@ -84,14 +88,14 @@ function RouteComponent() {
- + {packageData.data?.gallery.map(item => ( -
+
{item.title &&
{item.title}
}
@@ -113,17 +117,17 @@ 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 `` + 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)); @@ -163,18 +167,32 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {

Links

-
- +
+ {provider} {' '} Page - {(Object.entries(pkg.links) as [keyof typeof pkg.links, typeof pkg.links[keyof typeof pkg.links]][]).filter(a => a[1]).map(link => - typeof link[1] == "string" - ? - : (link[1] as PackageDonationUrl[]).map(donationLink => ) - - )} + {(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 => ( + + )))}
@@ -232,19 +250,20 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { function InstallButton() { const triggerRef = useRef(null); const { provider, slug } = Route.useParams(); - if(!includes(PROVIDERS, provider)) throw new Error("invalid provider") + if (!includes(PROVIDERS, provider)) + throw new Error('invalid provider'); const [open, setOpen] = useState(false); const clusters = useClusters(); const browserContext = useBrowserContext(); const { data: versions } = usePackageVersions(provider, slug, { mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : [], loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : [], - limit: 1 + limit: 1, }); const version = useMemo(() => { if (!versions || !browserContext.cluster) return undefined; - return versions.items[0] + return versions.items[0]; }, [browserContext.cluster, versions]); function download() { @@ -268,12 +287,12 @@ function InstallButton() { browserContext.cluster ? version ? ( - - Download latest to -
- {browserContext.cluster.name} -
- ) + + Download latest to +
+ {browserContext.cluster.name} +
+ ) : `No matching version found` : 'Select a Cluster' } @@ -289,7 +308,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); }} @@ -359,50 +378,60 @@ function colorForType(type: string) { } } -function Versions(){ - const {provider, slug} = Route.useParams() - if(!includes(PROVIDERS, provider)) throw new Error("invalid provider") - const [offset, setOffset] = useState(0) +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 + 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
-
- + itemsPerPage: versions?.limit as unknown as number, + }); + + useEffect(() => { + setOffset(pagination.offset); + }, [pagination.offset]); + + useEffect(() => { + pagination.reset(); + }, [pagination, versions?.total]); + + return ( +
+
+ +
+ + + + Name + Game Versions + Loaders + Created + Downloads + + + + {versions?.items.map(item => + )} + +
- - - - 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") +function VersionRow({ version }: { version: ManagedVersion }) { + const { provider } = Route.useParams(); + if (!includes(PROVIDERS, provider)) + throw new Error('invalid provider'); return ( @@ -413,22 +442,22 @@ function VersionRow({version}:{version:ManagedVersion}){ {version.display_name} - {version.mc_versions.join(", ")} - {version.loaders.map(upperFirst).join(", ")} + {version.mc_versions.join(', ')} + {version.loaders.map(upperFirst).join(', ')} {formatAsRelative(Date.parse(version.published))} {version.downloads} -