From 292e7df7f843f690003087d1df8cd1ae849d6ede Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Wed, 16 Jul 2025 23:18:15 +0200 Subject: [PATCH 01/10] feat: search results and queries --- .../frontend/src/hooks/useBrowser.tsx | 10 +- .../frontend/src/routes/app/browser/index.tsx | 98 +++++++++++++++++-- .../frontend/src/routes/app/browser/route.tsx | 37 ++++--- .../src/api/packages/provider/modrinth.rs | 4 +- 4 files changed, 122 insertions(+), 27 deletions(-) diff --git a/apps/onelauncher/frontend/src/hooks/useBrowser.tsx b/apps/onelauncher/frontend/src/hooks/useBrowser.tsx index 51be9ff1..20ce789b 100644 --- a/apps/onelauncher/frontend/src/hooks/useBrowser.tsx +++ b/apps/onelauncher/frontend/src/hooks/useBrowser.tsx @@ -1,8 +1,9 @@ -import type { ClusterModel, Provider, SearchQuery } from '@/bindings.gen'; +import type { ClusterModel, Paginated, Provider, SearchQuery, SearchResult } from '@/bindings.gen'; +import type { UndefinedInitialDataOptions } from '@tanstack/react-query'; import { bindings } from '@/main'; import { PROVIDERS } from '@/utils'; import { useCommand } from '@onelauncher/common'; -import { createContext, useContext, useMemo, useState } from 'react'; +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; export interface BrowserControllerType { cluster: ClusterModel | undefined; @@ -52,6 +53,7 @@ export function BrowserProvider(props: any) { ); } -export function useBrowserSearch(provider: Provider, query: SearchQuery) { - return useCommand('searchPackages', () => bindings.core.searchPackages(provider, query)); +export function useBrowserSearch(provider: Provider, query: SearchQuery, options?: Omit>, 'queryKey' | 'queryFn'> | undefined) { + const validFilters = useMemo(() => Object.values(query.filters ?? {}).filter(a => a).length > 0, [query.filters]); + return useCommand('searchPackages', () => bindings.core.searchPackages(provider, validFilters ? query : { ...query, filters: null }), options); } diff --git a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx index 43cc0b81..dd8bedc8 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx @@ -1,8 +1,11 @@ +import type { Provider, SearchResult } from '@/bindings.gen'; import OneConfigLogo from '@/assets/logos/oneconfig.svg'; import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser'; +import { abbreviateNumber } from '@/utils'; import { Button, Show } from '@onelauncher/common/components'; import { createFileRoute, useSearch } from '@tanstack/react-router'; -import { ChevronRightIcon } from '@untitled-theme/icons-react'; +import { ChevronRightIcon, Download01Icon, HeartIcon } from '@untitled-theme/icons-react'; +import { useEffect } from 'react'; import { BrowserLayout } from './route'; export const Route = createFileRoute('/app/browser/')({ @@ -14,6 +17,7 @@ function RouteComponent() {
+
); @@ -21,9 +25,8 @@ function RouteComponent() { function Featured() { const context = useBrowserContext(); - const search = useBrowserSearch(context.provider, context.query); return ( - + 0)}>
Featured
@@ -50,13 +53,90 @@ function Featured() {
- {search.status} - {search.isSuccess && ( - <> - {search.data.items.map(item =>

{item.name}

)} - - )}
); } + +function Search() { + const context = useBrowserContext(); + const search = useBrowserSearch(context.provider, context.query); + useEffect(() => { + search.refetch(); + }, [context.provider, context.query]); + + return ( +
+ + {search.data?.items.map(item => ( + + ))} + +
+ ); +} + +function PackageItem(props: SearchResult & { provider: Provider }) { + function redirect() { + } + + return ( +
+
+ + )} + when={props.icon_url} + > + {`Icon + {`Icon + +
+
+
+

{props.title}

+

+ By + {' '} + {props.author} + {' '} + on + {' '} + {props.provider} +

+
+ +

{props.description}

+ +
+ +
+ + {abbreviateNumber(props.downloads)} +
+ + {/* 0}> +
+ + {abbreviateNumber(props.follows)} +
+
*/} +
+
+
+
+ ); +} diff --git a/apps/onelauncher/frontend/src/routes/app/browser/route.tsx b/apps/onelauncher/frontend/src/routes/app/browser/route.tsx index b15a71d6..b17d938b 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/route.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/route.tsx @@ -7,6 +7,7 @@ import { browserCategories } from '@/utils/browser'; import { Dropdown, Show, TextField } from '@onelauncher/common/components'; import { createFileRoute, Outlet } from '@tanstack/react-router'; import { SearchMdIcon } from '@untitled-theme/icons-react'; +import { useEffect } from 'react'; export const Route = createFileRoute('/app/browser')({ component: RouteComponent, @@ -51,11 +52,16 @@ export function BrowserLayout(props: any) { function BrowserSidebar() { const context = useBrowserContext(); const clusters = useClusters(); + useEffect(() => { + const game_versions = context.cluster?.mc_version ? [context.cluster.mc_version] : null; + const loaders = context.cluster?.mc_loader ? [context.cluster.mc_loader] : null; + context.setQuery({ ...context.query, filters: { ...defaultFilters, ...context.query.filters, game_versions, loaders } }); + }, [context.cluster]); return (
-
Active Cluster
+
Active Cluster
{ const cluster = clusters?.find(cluster => cluster.id.toString() === id); @@ -71,7 +77,7 @@ function BrowserSidebar() {
-
Provider
+
Provider
context.setProvider(id as typeof context.provider)} selectedKey={context.provider}> {PROVIDERS.map(provider => ( @@ -88,22 +94,22 @@ function BrowserSidebar() { ); } -const defaultFilters:Filters = { +const defaultFilters: Filters = { categories: null, game_versions: null, loaders: null, - package_types: null -} + package_types: null, +}; function BrowserCategories() { const context = useBrowserContext(); const categories = browserCategories.byPackageType('mod', context.provider); - function switchCategory(category:string){ - if(context.query.filters?.categories?.includes(category)) - context.setQuery({...context.query, filters: {...defaultFilters, ...context.query.filters, categories: (context.query.filters?.categories ?? []).filter(cat=>cat!==category)}}) - else - context.setQuery({...context.query, filters: {...defaultFilters, ...context.query.filters, categories: [...(context.query.filters?.categories ?? []), category]}}) + function switchCategory(category: string) { + const newCategories = context.query.filters?.categories?.includes(category) + ? context.query.filters.categories.filter(cat => cat !== category) + : [...(context.query.filters?.categories ?? []), category]; + context.setQuery({ ...context.query, filters: { ...defaultFilters, ...context.query.filters, categories: newCategories.length > 0 ? newCategories : null } }); } return ( @@ -112,13 +118,13 @@ function BrowserCategories() {
-
Categories
+
Categories
{categories.map(category => (

switchCategory(category.id)} + onClick={() => switchCategory(category.id)} > {category.display}

@@ -131,6 +137,7 @@ function BrowserCategories() { } function BrowserToolbar() { + const context = useBrowserContext(); return (
@@ -138,6 +145,12 @@ function BrowserToolbar() {
} + onKeyDown={(e) => { + if (e.key !== 'Enter') + return; + e.preventDefault(); + context.setQuery({ ...context.query, query: e.currentTarget.value }); + }} placeholder="Search for content" />
diff --git a/packages/core/src/api/packages/provider/modrinth.rs b/packages/core/src/api/packages/provider/modrinth.rs index 2712b28c..e1bcb77f 100644 --- a/packages/core/src/api/packages/provider/modrinth.rs +++ b/packages/core/src/api/packages/provider/modrinth.rs @@ -70,11 +70,11 @@ impl ProviderExt for ModrinthProviderImpl { if let Some(loaders) = &filters.loaders { for loader in loaders { - builder.and(Facet("loaders".to_string(), FacetOperation::Eq, loader.to_string())); + builder.and(Facet("categories".to_string(), FacetOperation::Eq, loader.to_string())); } } - params.append_pair("facets", &builder.build()); + params.append_pair("facets", &format!("[{}]", &builder.build())); } if let Some(sort) = &query.sort { From 85443ee5a9982c0da0fb0154f46e5e93ec01134f Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:28:29 +0200 Subject: [PATCH 02/10] fix: browser main page --- .../src/components/content/PackageItem.tsx | 72 ++++++++++++ .../frontend/src/routes/app/browser/index.tsx | 111 ++++++------------ .../src/routes/app/browser/search.tsx | 35 ++++++ .../onelauncher/frontend/src/utils/browser.ts | 2 +- 4 files changed, 141 insertions(+), 79 deletions(-) create mode 100644 apps/onelauncher/frontend/src/components/content/PackageItem.tsx create mode 100644 apps/onelauncher/frontend/src/routes/app/browser/search.tsx diff --git a/apps/onelauncher/frontend/src/components/content/PackageItem.tsx b/apps/onelauncher/frontend/src/components/content/PackageItem.tsx new file mode 100644 index 00000000..f974362b --- /dev/null +++ b/apps/onelauncher/frontend/src/components/content/PackageItem.tsx @@ -0,0 +1,72 @@ +import type { Provider, SearchResult } from '@/bindings.gen'; +import { abbreviateNumber } from '@/utils'; +import { Show } from '@onelauncher/common/components'; +import { Download01Icon } from '@untitled-theme/icons-react'; + +export function PackageGrid({ items, provider }: { items: Array; provider: Provider }) { + return ( +
+ {items.map(item => ( + + ))} +
+ ); +} + +export function PackageItem({ provider, ...item }: SearchResult & { provider: Provider }) { + function redirect() { + } + + return ( +
+
+ + )} + when={item.icon_url} + > + {`Icon + {`Icon + +
+
+
+

{item.title}

+

+ By + {' '} + {item.author} + {' '} + on + {' '} + {provider} +

+
+ +

{item.description}

+ +
+ +
+ + {abbreviateNumber(item.downloads)} +
+
+
+
+
+ ); +} diff --git a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx index dd8bedc8..9ca0ce77 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx @@ -1,15 +1,17 @@ -import type { Provider, SearchResult } from '@/bindings.gen'; +import type { Provider } from '@/bindings.gen'; import OneConfigLogo from '@/assets/logos/oneconfig.svg'; +import { PackageGrid } from '@/components/content/PackageItem'; import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser'; -import { abbreviateNumber } from '@/utils'; +import { PROVIDERS } from '@/utils'; import { Button, Show } from '@onelauncher/common/components'; -import { createFileRoute, useSearch } from '@tanstack/react-router'; -import { ChevronRightIcon, Download01Icon, HeartIcon } from '@untitled-theme/icons-react'; -import { useEffect } from 'react'; +import { createFileRoute } from '@tanstack/react-router'; +import { ChevronRightIcon } from '@untitled-theme/icons-react'; +import { useState } from 'react'; import { BrowserLayout } from './route'; export const Route = createFileRoute('/app/browser/')({ - component: RouteComponent, + component: RouteComponent + }); function RouteComponent() { @@ -17,7 +19,7 @@ function RouteComponent() {
- +
); @@ -58,85 +60,38 @@ function Featured() { ); } -function Search() { - const context = useBrowserContext(); - const search = useBrowserSearch(context.provider, context.query); - useEffect(() => { - search.refetch(); - }, [context.provider, context.query]); - +function Lists() { return ( -
- - {search.data?.items.map(item => ( - - ))} - +
+ {PROVIDERS.map(provider => ( + + ))}
); } -function PackageItem(props: SearchResult & { provider: Provider }) { - function redirect() { - } - +function List({ provider }: { provider: Provider }) { + const search = useBrowserSearch(provider, { + filters: null, + limit: 18 as unknown as bigint, + offset: null, + query: null, + sort: null, + }); + const [expanded, setExpanded] = useState(false); return ( -
-
- - )} - when={props.icon_url} - > - {`Icon - {`Icon +
+

{provider}

+
+ {search.isSuccess + ? + :

Loading...

} + +
+ +
-
-
-

{props.title}

-

- By - {' '} - {props.author} - {' '} - on - {' '} - {props.provider} -

-
- -

{props.description}

- -
- -
- - {abbreviateNumber(props.downloads)} -
- - {/* 0}> -
- - {abbreviateNumber(props.follows)} -
-
*/} -
-
-
); } diff --git a/apps/onelauncher/frontend/src/routes/app/browser/search.tsx b/apps/onelauncher/frontend/src/routes/app/browser/search.tsx new file mode 100644 index 00000000..aec3657f --- /dev/null +++ b/apps/onelauncher/frontend/src/routes/app/browser/search.tsx @@ -0,0 +1,35 @@ +import { PackageGrid } from '@/components/content/PackageItem'; +import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser'; +import { Show } from '@onelauncher/common/components'; +import { createFileRoute } from '@tanstack/react-router'; +import { useEffect } from 'react'; +import { BrowserLayout } from './route'; + +export const Route = createFileRoute('/app/browser/search')({ + component: RouteComponent, +}); + +function RouteComponent() { + return ( + +
+ +
+
+ ); +} + +function Search() { + const context = useBrowserContext(); + const search = useBrowserSearch(context.provider, context.query, { + }); + useEffect(() => { + search.refetch(); + }, [context.provider, context.query]); + + return ( + + + + ); +} diff --git a/apps/onelauncher/frontend/src/utils/browser.ts b/apps/onelauncher/frontend/src/utils/browser.ts index 375db1d3..b32f8c6f 100644 --- a/apps/onelauncher/frontend/src/utils/browser.ts +++ b/apps/onelauncher/frontend/src/utils/browser.ts @@ -6,7 +6,7 @@ export interface CategoryItem { } export const browserCategories = { - byPackageType(packageType: 'mod' | string, provider: string): Array { + byPackageType(packageType: 'mod' | string & {}, provider: string): Array { switch (packageType) { case 'mod': return this.mod(provider); From 608c3b62cc9d6a33e0e31a4229ca6f2afb16b847 Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Sat, 19 Jul 2025 16:17:49 +0200 Subject: [PATCH 03/10] fix: context breaking --- .../frontend/src/routes/app/browser/route.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/onelauncher/frontend/src/routes/app/browser/route.tsx b/apps/onelauncher/frontend/src/routes/app/browser/route.tsx index b17d938b..ab0ae053 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/route.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/route.tsx @@ -4,10 +4,10 @@ import { BrowserProvider, useBrowserContext } from '@/hooks/useBrowser'; import { useClusters } from '@/hooks/useCluster'; import { PROVIDERS } from '@/utils'; import { browserCategories } from '@/utils/browser'; -import { Dropdown, Show, TextField } from '@onelauncher/common/components'; +import { AnimatedOutlet, Dropdown, Show, TextField } from '@onelauncher/common/components'; import { createFileRoute, Outlet } from '@tanstack/react-router'; import { SearchMdIcon } from '@untitled-theme/icons-react'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; export const Route = createFileRoute('/app/browser')({ component: RouteComponent, @@ -17,7 +17,11 @@ function RouteComponent() { return (
- +
); @@ -138,13 +142,16 @@ function BrowserCategories() { function BrowserToolbar() { const context = useBrowserContext(); + const [query, setQuery] = useState(context.query.query ?? "") return (
} + onChange={e=>setQuery(e.currentTarget.value)} onKeyDown={(e) => { if (e.key !== 'Enter') return; From 996df8caa8217ef50c52ab3024704e2e21392686 Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Sat, 19 Jul 2025 18:26:28 +0200 Subject: [PATCH 04/10] chore: change to ES2023 --- apps/onelauncher/frontend/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/onelauncher/frontend/tsconfig.json b/apps/onelauncher/frontend/tsconfig.json index 6325fbce..d92bf525 100644 --- a/apps/onelauncher/frontend/tsconfig.json +++ b/apps/onelauncher/frontend/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "target": "ES2022", "jsx": "react-jsx", - "lib": ["ES2022", "DOM", "DOM.Iterable"], + "lib": ["ES2023", "DOM", "DOM.Iterable"], "baseUrl": ".", "module": "ESNext", From 4ff7066742d2dc6b7b3963e1224124c1dd7c518a Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:01:42 +0200 Subject: [PATCH 05/10] fix: package donation data --- packages/core/src/api/packages/data.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/api/packages/data.rs b/packages/core/src/api/packages/data.rs index d046ea63..a26c5639 100644 --- a/packages/core/src/api/packages/data.rs +++ b/packages/core/src/api/packages/data.rs @@ -201,14 +201,14 @@ pub struct PackageLinks { #[onelauncher_macro::specta] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PackageDonationUrl { - #[serde(alias = "id")] - pub platform: PackageDonationPlatform, + pub id: PackageDonationPlatform, pub url: String, } /// https://api.modrinth.com/v2/tag/donation_platform #[onelauncher_macro::specta] #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum PackageDonationPlatform { Patreon, #[serde(alias = "bmac")] From a3ab0d88c98510a5a234dde8db434db4e9ddb567 Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:02:46 +0200 Subject: [PATCH 06/10] feat: get users from author --- .../core/src/api/packages/provider/modrinth.rs | 14 +++++--------- packages/core/src/api/tauri/commands.rs | 9 ++++++++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/core/src/api/packages/provider/modrinth.rs b/packages/core/src/api/packages/provider/modrinth.rs index e1bcb77f..a08dd67f 100644 --- a/packages/core/src/api/packages/provider/modrinth.rs +++ b/packages/core/src/api/packages/provider/modrinth.rs @@ -510,8 +510,8 @@ impl From for ManagedUser { struct ModrinthUser { pub id: String, pub username: String, - pub avatar_url: String, - pub bio: String, + pub avatar_url: Option, + pub bio: Option, } impl From for ManagedUser { @@ -520,16 +520,12 @@ impl From for ManagedUser { id: value.id.clone(), username: value.username, url: Some(format!( - "{}/user/{}", + "{}user/{}", Provider::Modrinth.website(), value.id )), - avatar_url: Some(value.avatar_url), - bio: if value.bio.is_empty() || value.bio.eq_ignore_ascii_case("a modrinth creator.") { - None - } else { - Some(value.bio) - }, + avatar_url: value.avatar_url, + bio: value.bio, is_organization_user: false, role: None, } diff --git a/packages/core/src/api/tauri/commands.rs b/packages/core/src/api/tauri/commands.rs index 2ac6b8a4..4fa6576e 100644 --- a/packages/core/src/api/tauri/commands.rs +++ b/packages/core/src/api/tauri/commands.rs @@ -1,7 +1,7 @@ use interpulse::api::minecraft::Version; use onelauncher_entity::{icon::Icon, loader::GameLoader, package::Provider, packages, resolution::Resolution}; use sea_orm::ActiveValue::Set; -use crate::{api::packages::{data::{SearchResult, ManagedPackage, ManagedUser, ManagedVersion, SearchQuery}, provider::ProviderExt}, store::{Settings, State}, utils::pagination::Paginated}; +use crate::{api::packages::{data::{ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery, SearchResult}, provider::ProviderExt}, store::{Settings, State}, utils::pagination::Paginated}; use tauri::{AppHandle, Runtime}; use crate::{api::{self, cluster::dao::ClusterId}, error::{LauncherError, LauncherResult}, store::{credentials::MinecraftCredentials, Core}}; @@ -99,6 +99,9 @@ pub trait TauriLauncherApi { #[taurpc(alias = "downloadPackage")] async fn download_package(provider: Provider, package_id: String, version_id: String, cluster_id: ClusterId, skip_compatibility: Option) -> LauncherResult; + + #[taurpc(alias = "getUsersFromAuthor")] + async fn get_users_from_author(provider: Provider, author: PackageAuthor) -> LauncherResult>; } @@ -425,4 +428,8 @@ impl TauriLauncherApi for TauriLauncherApiImpl { Ok(model) } + + async fn get_users_from_author(self, provider: Provider, author: PackageAuthor) -> LauncherResult> { + Ok(provider.get_users_from_author(author).await?) + } } From ac7bef4fd033c9373d2271e6275832e4589e25b3 Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:07:27 +0200 Subject: [PATCH 07/10] feat: package page --- .../src/components/content/PackageItem.tsx | 3 + .../app/browser/package.$provider.$slug.tsx | 282 ++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx diff --git a/apps/onelauncher/frontend/src/components/content/PackageItem.tsx b/apps/onelauncher/frontend/src/components/content/PackageItem.tsx index f974362b..63e9830f 100644 --- a/apps/onelauncher/frontend/src/components/content/PackageItem.tsx +++ b/apps/onelauncher/frontend/src/components/content/PackageItem.tsx @@ -1,6 +1,7 @@ import type { Provider, SearchResult } from '@/bindings.gen'; import { abbreviateNumber } from '@/utils'; import { Show } from '@onelauncher/common/components'; +import { useNavigate } from '@tanstack/react-router'; import { Download01Icon } from '@untitled-theme/icons-react'; export function PackageGrid({ items, provider }: { items: Array; provider: Provider }) { @@ -14,7 +15,9 @@ export function PackageGrid({ items, provider }: { items: Array; p } export function PackageItem({ provider, ...item }: SearchResult & { provider: Provider }) { + const navigate = useNavigate() function redirect() { + navigate({to: "/app/browser/package/$provider/$slug", params: {provider, slug: item.slug}}) } 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 new file mode 100644 index 00000000..ffb75066 --- /dev/null +++ b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx @@ -0,0 +1,282 @@ +import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, Paginated, Provider } from '@/bindings.gen' +import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser' +import { useClusters } from '@/hooks/useCluster' +import { bindings } from '@/main' +import { abbreviateNumber, formatAsRelative, PROVIDERS } from '@/utils' +import { useCommand } from '@onelauncher/common' +import { Show, Tooltip, Button, Dropdown } from '@onelauncher/common/components' +import { createFileRoute, Link } from '@tanstack/react-router' +import { openUrl } from '@tauri-apps/plugin-opener' +import { Download01Icon, LinkExternal01Icon, File02Icon, CalendarIcon, ClockRewindIcon, ChevronDownIcon, ChevronUpIcon } from '@untitled-theme/icons-react' +import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { Collection, ListBox, ListBoxItem, Popover, Select } from 'react-aria-components' + + +export const Route = createFileRoute('/app/browser/package/$provider/$slug')({ + component: RouteComponent, +}) + +function includes(list:{includes: (arg0:C)=>boolean}, element:T): element is C{ + return list.includes(element as unknown as C) +} + +type PackageContextType = { + pkg: ManagedPackage|undefined + versions: Paginated|undefined +} + +const PackageContext = createContext({ + pkg: undefined, + versions: undefined +}) + +function RouteComponent() { + const {provider, slug} = Route.useParams() + if(!includes(PROVIDERS, provider)) throw new Error("Invalid provider"); + const packageData = usePackageData(provider, slug) + const browserContext = useBrowserContext() + const {data: versions} = usePackageVersions(provider, slug, { + mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : null, + loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : null, + limit: 50}) + + return ( + + +
+ + +
+
+ About + Versions + + +
+ +
+ +
+
+
+
+
+ ) +} + + +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 authors = useCommand("getUsersFromAuthor", ()=>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

+ + {contentPackage.provider} + {' '} + Page + +
*/} + +
+

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 colorForType(type: string) { + switch (type.toLowerCase()) { + case 'release': + return 'bg-code-trace'; + case 'snapshot': + return 'bg-code-debug'; + case 'beta': + return 'bg-code-warn'; + case 'alpha': + return 'bg-code-error'; + default: + return 'bg-border/05'; + } +} + +function InstallButton({...pkg}:ManagedPackage){ + const triggerRef = useRef(null) + const elementRef = useRef(null) + const {provider, slug} = Route.useParams() + const [open, setOpen] = useState(false) + const clusters = useClusters() + const browserContext = useBrowserContext() + const {versions} = useContext(PackageContext) + const version = useMemo(()=>{ + if(!versions || !browserContext.cluster) return undefined; + return versions.items.findLast(version=> + version.mc_versions.includes(browserContext.cluster!.mc_version) + && version.loaders.includes(browserContext.cluster!.mc_loader) + ) + },[browserContext.cluster, versions]) + + function download(){ + if(!version || !browserContext.cluster || !includes(PROVIDERS, provider)) return false; + downloadPackage(browserContext.cluster, provider, version) + } + + + 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 downloadPackage(cluster: ClusterModel, provider:Provider, version: ManagedVersion, skipCompatibility = false){ + return bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility) +} From 8edce35f76b026b3e4f222ffef0a2a69ee543e6f Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:10:39 +0200 Subject: [PATCH 08/10] fix: browser bugs --- .../frontend/src/hooks/useBrowser.tsx | 17 ++++++++++++++++- .../frontend/src/routes/app/browser/search.tsx | 12 ++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/onelauncher/frontend/src/hooks/useBrowser.tsx b/apps/onelauncher/frontend/src/hooks/useBrowser.tsx index 20ce789b..522af87b 100644 --- a/apps/onelauncher/frontend/src/hooks/useBrowser.tsx +++ b/apps/onelauncher/frontend/src/hooks/useBrowser.tsx @@ -1,9 +1,10 @@ -import type { ClusterModel, Paginated, Provider, SearchQuery, SearchResult } from '@/bindings.gen'; +import type { ClusterModel, GameLoader, ManagedPackage, ManagedVersion, Paginated, Provider, SearchQuery, SearchResult } from '@/bindings.gen'; import type { UndefinedInitialDataOptions } from '@tanstack/react-query'; import { bindings } from '@/main'; import { PROVIDERS } from '@/utils'; import { useCommand } from '@onelauncher/common'; import { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import { useNavigate } from '@tanstack/react-router'; export interface BrowserControllerType { cluster: ClusterModel | undefined; @@ -28,6 +29,7 @@ export function BrowserProvider(props: any) { const { children } = props; const [cluster, setCluster] = useState(undefined); const [provider, setProvider] = useState(PROVIDERS[0]); + const navigate = useNavigate() const [query, setQuery] = useState({ filters: null, query: null, @@ -36,6 +38,11 @@ export function BrowserProvider(props: any) { sort: null, }); + useEffect(()=>{ + if(query.filters?.categories || query.query) + navigate({to: "/app/browser/search"}) + },[query]) + const context = useMemo(() => ({ cluster, setCluster, @@ -57,3 +64,11 @@ export function useBrowserSearch(provider: Provider, query: SearchQuery, options const validFilters = useMemo(() => Object.values(query.filters ?? {}).filter(a => a).length > 0, [query.filters]); return useCommand('searchPackages', () => bindings.core.searchPackages(provider, validFilters ? query : { ...query, filters: null }), options); } + +export function usePackageData(provider: Provider, slug: string, options?: Omit, "queryKey" | "queryFn"> | undefined){ + return useCommand("getPackage", () => bindings.core.getPackage(provider, slug), options) +} + +export function usePackageVersions(provider: Provider, slug: string, {mc_versions, loaders, offset, limit, ...options}:{mc_versions?:Array|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) +} diff --git a/apps/onelauncher/frontend/src/routes/app/browser/search.tsx b/apps/onelauncher/frontend/src/routes/app/browser/search.tsx index aec3657f..d02f5caa 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/search.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/search.tsx @@ -25,11 +25,15 @@ function Search() { }); useEffect(() => { search.refetch(); - }, [context.provider, context.query]); + }, [context.provider, context.query, context.cluster]); return ( - - - + <> + { + search.data + ? + :

Loading

+ } + ); } From 4286f054ec06696afb1b9f2930da33b699a8538c Mon Sep 17 00:00:00 2001 From: not a cow <104355555+not-a-cowfr@users.noreply.github.com> Date: Sat, 19 Jul 2025 20:10:14 -0700 Subject: [PATCH 09/10] Use cargo binstall in setup scripts (#340) * use cargo binstall if it exists * powershell --- packages/scripts/setup.ps1 | 3 +-- packages/scripts/setup.sh | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/scripts/setup.ps1 b/packages/scripts/setup.ps1 index fcf91cc6..75503a94 100644 --- a/packages/scripts/setup.ps1 +++ b/packages/scripts/setup.ps1 @@ -198,8 +198,7 @@ https://learn.microsoft.com/windows/package-manager/winget/ Write-Host Write-Host 'Installing Rust tools...' -ForegroundColor Yellow - cargo install cargo-watch - cargo install sqlx-cli + if (Get-Command cargo-binstall -ea 0) { cargo binstall cargo-watch sqlx-cli } else { cargo install cargo-watch sqlx-cli } if ($LASTEXITCODE -ne 0) { Exit-WithError 'Failed to install Rust tools' } diff --git a/packages/scripts/setup.sh b/packages/scripts/setup.sh index 8c35af75..5ef07cda 100755 --- a/packages/scripts/setup.sh +++ b/packages/scripts/setup.sh @@ -184,7 +184,7 @@ esac if [ "${CI:-}" != "true" ]; then echo "installing rust tools..." _tools="cargo-watch sqlx-cli" - echo "$_tools" | xargs cargo install + echo "$_tools" | xargs cargo $(has cargo-binstall && echo binstall || echo install) fi echo 'your machine has been setup for onelauncher development!' From 5e9b93bea2aaf54a3e1d92d44099723142dff39e Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Sun, 20 Jul 2025 13:26:31 +0200 Subject: [PATCH 10/10] feat: search pagination --- .../frontend/src/hooks/usePagination.tsx | 118 +++++++++++++++ .../app/browser/package.$provider.$slug.tsx | 141 +++++++++--------- .../src/routes/app/browser/search.tsx | 37 ++++- .../frontend/src/styles/global.css | 6 +- 4 files changed, 231 insertions(+), 71 deletions(-) create mode 100644 apps/onelauncher/frontend/src/hooks/usePagination.tsx diff --git a/apps/onelauncher/frontend/src/hooks/usePagination.tsx b/apps/onelauncher/frontend/src/hooks/usePagination.tsx new file mode 100644 index 00000000..33d5937e --- /dev/null +++ b/apps/onelauncher/frontend/src/hooks/usePagination.tsx @@ -0,0 +1,118 @@ +import { Button, Show } from '@onelauncher/common/components'; +import { ChevronRightIcon, ChevronLeftIcon } from '@untitled-theme/icons-react'; +import { useState } from 'react'; + +export interface PaginationOptions { + itemsCount: number; + itemsPerPage: number; + initialPage?: number; +}; + +function usePagination(options: PaginationOptions) { + // const [options, setOptions] = useState(initialOptions); + + const [page, setPage] = useState(Math.max(1, options.initialPage || 1)); + + const next = () => setPage(page => page + 1); + const prev = () => setPage(page => page - 1); + + const totalPages = Math.ceil(options.itemsCount / options.itemsPerPage); + + const hasNext = page < totalPages; + const hasPrev = page > 1; + + const offset = (page - 1) * options.itemsPerPage; + + // Exported + function reset(/*options: PaginationOptions*/) { + setPage(1); + } + + function getVisiblePageNumbers(page: number) { + const pages: number[] = []; + + const left = page - 1 > 1 ? page - 1 : null; + const right = page + 1 < totalPages ? page + 1 : null; + + if (left !== null) + pages.push(left); + + if (page !== 1 && page !== totalPages) + pages.push(page); + + if (right !== null) + pages.push(right); + + return pages; + } + + function PaginationBtn(props: { page: number }) { + return ( + + ); + } + + const Navigation = () => ( +
+ +
+ ); + + return { + page, + next, + prev, + hasNext, + hasPrev, + totalPages, + offset, + reset, + options, + Navigation, + }; +} + +export default usePagination; 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 ffb75066..e3d3f84b 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 @@ -13,16 +13,16 @@ import { Collection, ListBox, ListBoxItem, Popover, Select } from 'react-aria-co export const Route = createFileRoute('/app/browser/package/$provider/$slug')({ - component: RouteComponent, + component: RouteComponent, }) -function includes(list:{includes: (arg0:C)=>boolean}, element:T): element is C{ +function includes(list: { includes: (arg0: C) => boolean }, element: T): element is C { return list.includes(element as unknown as C) } type PackageContextType = { - pkg: ManagedPackage|undefined - versions: Paginated|undefined + pkg: ManagedPackage | undefined + versions: Paginated | undefined } const PackageContext = createContext({ @@ -31,25 +31,26 @@ const PackageContext = createContext({ }) function RouteComponent() { - const {provider, slug} = Route.useParams() - if(!includes(PROVIDERS, provider)) throw new Error("Invalid provider"); + const { provider, slug } = Route.useParams() + if (!includes(PROVIDERS, provider)) throw new Error("Invalid provider"); const packageData = usePackageData(provider, slug) const browserContext = useBrowserContext() - const {data: versions} = usePackageVersions(provider, slug, { - mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : null, - loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : null, - limit: 50}) + 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 + }) return ( - +
- About - Versions + About + Versions
@@ -65,13 +66,13 @@ function RouteComponent() { } -function BrowserSidebar({package: pkg}: { package: ManagedPackage }) { - const {provider} = Route.useParams() +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 authors = useCommand("getUsersFromAuthor", ()=>bindings.core.getUsersFromAuthor(provider as Provider, pkg.author)) + const authors = useCommand("getUsersFromAuthor", () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author)) return (
@@ -120,8 +121,8 @@ function BrowserSidebar({package: pkg}: { package: ManagedPackage }) {

Authors

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

Loading...

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

Loading...

}
@@ -183,100 +184,104 @@ function colorForType(type: string) { } } -function InstallButton({...pkg}:ManagedPackage){ - const triggerRef = useRef(null) - const elementRef = useRef(null) - const {provider, slug} = Route.useParams() +function InstallButton({ ...pkg }: ManagedPackage) { + const triggerRef = useRef(null) + const { provider, slug } = Route.useParams() const [open, setOpen] = useState(false) const clusters = useClusters() const browserContext = useBrowserContext() - const {versions} = useContext(PackageContext) - const version = useMemo(()=>{ - if(!versions || !browserContext.cluster) return undefined; - return versions.items.findLast(version=> + const { versions } = useContext(PackageContext) + 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) ) - },[browserContext.cluster, versions]) + }, [browserContext.cluster, versions]) - function download(){ - if(!version || !browserContext.cluster || !includes(PROVIDERS, provider)) return false; + function download() { + if (!version || !browserContext.cluster || !includes(PROVIDERS, provider)) return false; downloadPackage(browserContext.cluster, provider, version) } return ( - { + const cluster = clusters?.find(cluster => cluster.id as unknown as number == e) + if (cluster) browserContext.setCluster(cluster) + }}> +
+ + + + + {item => {item.name}} + + + +
- - - - {item=> {item.name}} - - - - + ) } -function Author({author}:{author:ManagedUser}){ +function Author({ author }: { author: ManagedUser }) { //TODO: onclick and fallback avatar url return ( - {if(author.url) openUrl(author.url)}} - > - {`${author.username}'s - + + ) } -function downloadPackage(cluster: ClusterModel, provider:Provider, version: ManagedVersion, skipCompatibility = false){ +function downloadPackage(cluster: ClusterModel, provider: Provider, version: ManagedVersion, skipCompatibility = false) { return bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility) } diff --git a/apps/onelauncher/frontend/src/routes/app/browser/search.tsx b/apps/onelauncher/frontend/src/routes/app/browser/search.tsx index d02f5caa..e31ffb6a 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/search.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/search.tsx @@ -2,8 +2,11 @@ import { PackageGrid } from '@/components/content/PackageItem'; import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser'; import { Show } from '@onelauncher/common/components'; import { createFileRoute } from '@tanstack/react-router'; -import { useEffect } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { BrowserLayout } from './route'; +import type { Paginated, SearchResult } from '@/bindings.gen'; +import type { PaginationOptions } from '@/hooks/usePagination'; +import usePagination from '@/hooks/usePagination'; export const Route = createFileRoute('/app/browser/search')({ component: RouteComponent, @@ -31,9 +34,39 @@ function Search() { <> { search.data - ? + ? :

Loading

} ); } + +function Results({results}:{results:Paginated}){ + const context = useBrowserContext(); + const [oldTotal, setOldTotal] = useState(results.total) + const pagination = usePagination({ + itemsCount: results.total as unknown as number, + itemsPerPage: context.query.limit as unknown as number + }) + useEffect(()=>{ + context.setQuery({...context.query, offset: pagination.offset as unknown as bigint}) + },[pagination.page]) + useEffect(()=>{ + if(oldTotal === results.total) return; + pagination.reset() + console.log("pagination reset", results.total) + setOldTotal(results.total) + },[results.total]) + return ( +
+
+ +
+ {results.items.length == 0 + ?

No results

+ : + } + +
+ ) +} diff --git a/apps/onelauncher/frontend/src/styles/global.css b/apps/onelauncher/frontend/src/styles/global.css index 06a0ee4a..33ed62d6 100644 --- a/apps/onelauncher/frontend/src/styles/global.css +++ b/apps/onelauncher/frontend/src/styles/global.css @@ -29,4 +29,8 @@ mask: linear-gradient(270deg, transparent, black 50%); background: linear-gradient(270deg, transparent, rgba(0, 0, 0, 0.85) 80%); -} \ No newline at end of file +} + +@utility text-trim{ + text-box: trim-both cap alphabetic +}