diff --git a/apps/desktop/src/api/commands/package.rs b/apps/desktop/src/api/commands/package.rs index cb2a3cf8..9f55bd9b 100644 --- a/apps/desktop/src/api/commands/package.rs +++ b/apps/desktop/src/api/commands/package.rs @@ -55,6 +55,7 @@ pub async fn get_provider_package_version( pub struct ProviderSearchQuery { query: Option, limit: Option, + offset: Option, game_versions: Option>, categories: Option>, loaders: Option>, @@ -69,6 +70,7 @@ pub async fn search_provider_packages(provider: Providers, query: ProviderSearch .search( query.query, query.limit, + query.offset, query.game_versions, query.categories, query.loaders, diff --git a/apps/frontend/src/ui/components/content/SearchResults.tsx b/apps/frontend/src/ui/components/content/SearchResults.tsx index d1be142f..243801b2 100644 --- a/apps/frontend/src/ui/components/content/SearchResults.tsx +++ b/apps/frontend/src/ui/components/content/SearchResults.tsx @@ -1,4 +1,4 @@ -import type { ProviderSearchResults, Providers, SearchResult } from '@onelauncher/client/bindings'; +import type { Providers, SearchResult } from '@onelauncher/client/bindings'; import { ChevronDownIcon, Download01Icon, HeartIcon } from '@untitled-theme/icons-solid'; import { For, Match, Show, Switch, createSignal } from 'solid-js'; import Button from '../base/Button'; @@ -6,11 +6,13 @@ import useBrowser from '~ui/hooks/useBrowser'; import useSettings from '~ui/hooks/useSettings'; import { abbreviateNumber } from '~utils'; -type SearchResultsContainerProps = ProviderSearchResults & { +interface SearchResultsContainerProps { + provider: Providers; + results: SearchResult[]; header: string; category: string; collapsable?: boolean; -}; +} function SearchResultsContainer(props: SearchResultsContainerProps) { const { settings } = useSettings(); diff --git a/apps/frontend/src/ui/hooks/useBrowser.tsx b/apps/frontend/src/ui/hooks/useBrowser.tsx index 43fe3c4f..16aad61f 100644 --- a/apps/frontend/src/ui/hooks/useBrowser.tsx +++ b/apps/frontend/src/ui/hooks/useBrowser.tsx @@ -1,5 +1,5 @@ import { useNavigate } from '@solidjs/router'; -import { type Accessor, type Context, For, type ParentProps, type Setter, Show, createContext, createEffect, createSignal, onMount, untrack, useContext } from 'solid-js'; +import { type Accessor, type Context, For, type ParentProps, type Setter, Show, createContext, createEffect, createSignal, on, onMount, untrack, useContext } from 'solid-js'; import type { Cluster, ManagedPackage, PackageType, ProviderSearchQuery, ProviderSearchResults, Providers } from '@onelauncher/client/bindings'; import useCommand, { tryResult } from './useCommand'; import { useRecentCluster } from './useCluster'; @@ -46,6 +46,7 @@ export function BrowserProvider(props: ParentProps) { const [searchOptions, setSearchOptions] = createSignal({ query: '', limit: 20, + offset: 0, categories: null, game_versions: null, loaders: null, @@ -119,6 +120,21 @@ export function BrowserProvider(props: ParentProps) { setCluster(recentCluster()); }); + createEffect(on(cluster, (cluster) => { + if (cluster !== undefined) + setSearchOptions((prev) => { + const opts: ProviderSearchQuery = { + ...prev, + game_versions: [cluster.meta.mc_version], + }; + + if (cluster.meta.loader !== undefined) + opts.loaders = [cluster.meta.loader]; + + return opts; + }); + })); + return ( {props.children} diff --git a/apps/frontend/src/ui/hooks/usePagination.tsx b/apps/frontend/src/ui/hooks/usePagination.tsx index 62687841..13f3fc9c 100644 --- a/apps/frontend/src/ui/hooks/usePagination.tsx +++ b/apps/frontend/src/ui/hooks/usePagination.tsx @@ -2,26 +2,34 @@ import { ChevronLeftIcon, ChevronRightIcon } from '@untitled-theme/icons-solid'; import { For, Show, createSignal } from 'solid-js'; import Button from '~ui/components/base/Button'; -interface PaginationOptions { +export interface PaginationOptions { itemsCount: () => number; itemsPerPage: () => number; initialPage?: () => number; }; -function usePagination(options: PaginationOptions) { - const [page, setPage] = createSignal(Math.max(1, options.initialPage?.() || 1)); +function usePagination(initialOptions: PaginationOptions) { + const [options, setOptions] = createSignal(initialOptions); + + // eslint-disable-next-line solid/reactivity -- - + const [page, setPage] = createSignal(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 totalPages = () => Math.ceil(options().itemsCount() / options().itemsPerPage()); const hasNext = () => page() < totalPages(); const hasPrev = () => page() > 1; - const offset = () => (page() - 1) * options.itemsPerPage(); + const offset = () => (page() - 1) * options().itemsPerPage(); + + // Exported + function reset(options: PaginationOptions) { + setOptions(options); + setPage(1); + } - // Navigation function getVisiblePageNumbers(page: number) { const pages: number[] = []; @@ -97,6 +105,7 @@ function usePagination(options: PaginationOptions) { hasPrev, totalPages, offset, + reset, Navigation, }; } diff --git a/apps/frontend/src/ui/pages/browser/BrowserCategory.tsx b/apps/frontend/src/ui/pages/browser/BrowserCategory.tsx deleted file mode 100644 index d4332bb3..00000000 --- a/apps/frontend/src/ui/pages/browser/BrowserCategory.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useSearchParams } from '@solidjs/router'; -import ScrollableContainer from '~ui/components/ScrollableContainer'; - -function BrowserCategory() { - const [params] = useSearchParams<{ - category: string; - }>(); - - return ( -
-

- Category = - {' '} - {params.category} -

- - test - -
- ); -} - -export default BrowserCategory; diff --git a/apps/frontend/src/ui/pages/browser/BrowserMain.tsx b/apps/frontend/src/ui/pages/browser/BrowserMain.tsx index 74b545d3..6b22b9ee 100644 --- a/apps/frontend/src/ui/pages/browser/BrowserMain.tsx +++ b/apps/frontend/src/ui/pages/browser/BrowserMain.tsx @@ -1,5 +1,5 @@ import { ChevronRightIcon } from '@untitled-theme/icons-solid'; -import { Show } from 'solid-js'; +import { Show, onMount } from 'solid-js'; import type { ManagedPackage } from '@onelauncher/client/bindings'; import { BrowserContent } from './BrowserRoot'; import useBrowser from '~ui/hooks/useBrowser'; @@ -10,6 +10,13 @@ import SearchResultsContainer from '~ui/components/content/SearchResults'; function BrowserMain() { const browser = useBrowser(); + onMount(() => { + browser.setSearchQuery(prev => ({ + ...prev, + categories: [], + })); + }); + return (
diff --git a/apps/frontend/src/ui/pages/browser/BrowserPackage.tsx b/apps/frontend/src/ui/pages/browser/BrowserPackage.tsx index 87cc486a..2533d7a4 100644 --- a/apps/frontend/src/ui/pages/browser/BrowserPackage.tsx +++ b/apps/frontend/src/ui/pages/browser/BrowserPackage.tsx @@ -145,6 +145,9 @@ function BrowserSidebar(props: { package: ManagedPackage; authors: ManagedUser[] {' '} {props.package.provider}

+ +

Archived

+

{props.package.description}

diff --git a/apps/frontend/src/ui/pages/browser/BrowserRoot.tsx b/apps/frontend/src/ui/pages/browser/BrowserRoot.tsx index 896c4bea..55a053bc 100644 --- a/apps/frontend/src/ui/pages/browser/BrowserRoot.tsx +++ b/apps/frontend/src/ui/pages/browser/BrowserRoot.tsx @@ -1,9 +1,7 @@ -import { Route } from '@solidjs/router'; +import { Route, useLocation } from '@solidjs/router'; import { For, type JSX, type ParentProps } from 'solid-js'; import { SearchMdIcon, Settings01Icon } from '@untitled-theme/icons-solid'; -import type { PackageType } from '@onelauncher/client/bindings'; import BrowserMain from './BrowserMain'; -import BrowserCategory from './BrowserCategory'; import BrowserPackage from './BrowserPackage'; import BrowserSearch from './BrowserSearch'; import Button from '~ui/components/base/Button'; @@ -15,7 +13,6 @@ function BrowserRoutes() { return ( <> - } /> @@ -60,27 +57,49 @@ function BrowserToolbar() { ); } -export interface BrowserSidebarCategory { - name: string; - sub: [string, string][]; -}; +function BrowserCategories() { + const browser = useBrowser(); + const location = useLocation(); + + const getEnabledCategories = () => browser.searchQuery().categories || []; + + const isEnabled = (category: string) => { + return getEnabledCategories().includes(category); + }; + + const toggleCategory = (category: string) => { + const enabledCategories = getEnabledCategories(); + const index = enabledCategories.indexOf(category); + if (index > -1) + enabledCategories.splice(index, 1); + else + enabledCategories.push(category); + + browser.setSearchQuery(prev => ({ + ...prev, + categories: enabledCategories, + })); + + if (!location.pathname.includes('/search')) + browser.search(); + }; -function BrowserCategories(props: { categories: BrowserSidebarCategory[] }) { return (
- + {category => ( @@ -97,31 +116,21 @@ function BrowserSidebar() { return (
-
Active Cluster
-
- -
-
Search Filters
+
+
Active Cluster
+
); } -export type BrowserContentProps = ParentProps & { - categories?: BrowserSidebarCategory[]; -}; - -const categories = (packageType: PackageType) => browserCategories.byPackageType(packageType); - -export function BrowserContent(props: BrowserContentProps) { - const browser = useBrowser(); - +export function BrowserContent(props: ParentProps) { return (
@@ -132,7 +141,7 @@ export function BrowserContent(props: BrowserContentProps) {
- +
diff --git a/apps/frontend/src/ui/pages/browser/BrowserSearch.tsx b/apps/frontend/src/ui/pages/browser/BrowserSearch.tsx index 9d42df5f..51d59a6e 100644 --- a/apps/frontend/src/ui/pages/browser/BrowserSearch.tsx +++ b/apps/frontend/src/ui/pages/browser/BrowserSearch.tsx @@ -5,28 +5,53 @@ import SearchResultsContainer from '~ui/components/content/SearchResults'; import Spinner from '~ui/components/Spinner'; import useBrowser from '~ui/hooks/useBrowser'; import useCommand from '~ui/hooks/useCommand'; +import usePagination, { type PaginationOptions } from '~ui/hooks/usePagination'; function BrowserSearch() { const browser = useBrowser(); - const [results, { refetch }] = useCommand(() => bridge.commands.searchProviderPackages('Modrinth', browser.searchQuery())); - createEffect(on(() => browser.searchQuery().query, () => { + const paginationOptions = (): PaginationOptions => ({ + itemsCount: () => results()?.total || 0, + itemsPerPage: () => browser.searchQuery().limit || 20, + }); + + const pagination = usePagination(paginationOptions()); + + createEffect(on(browser.searchQuery, () => { refetch(); })); + createEffect(on(() => results()?.total, (curr, prev) => { + if (curr !== prev) + pagination.reset(paginationOptions()); + })); + + createEffect(on(pagination.page, () => { + browser.setSearchQuery(prev => ({ + ...prev, + offset: pagination.offset(), + })); + })); + return ( - - - - - + + +
+ + + + + +
+ +
); } diff --git a/apps/frontend/src/utils/browser.ts b/apps/frontend/src/utils/browser.ts index 804cbf53..3bba4a28 100644 --- a/apps/frontend/src/utils/browser.ts +++ b/apps/frontend/src/utils/browser.ts @@ -1,9 +1,12 @@ import type { BrowserListView, PackageType } from '@onelauncher/client/bindings'; -import type { BrowserSidebarCategory } from '~ui/pages/browser/BrowserRoot'; -import { LOADERS, PROVIDERS } from '~utils'; export const BROWSER_VIEWS: BrowserListView[] = ['grid', 'list', 'preview'] as const; +export interface BrowserSidebarCategory { + name: string; + sub: string[]; +}; + export const browserCategories = { byPackageType(packageType: PackageType): BrowserSidebarCategory[] { switch (packageType) { @@ -16,6 +19,8 @@ export const browserCategories = { case 'shaderpack': return this.shaderpack(); } + + return []; }, mod(): BrowserSidebarCategory[] { @@ -23,15 +28,14 @@ export const browserCategories = { { name: 'Categories', sub: [ - ['Adventure', '/adventure'], - ['Magic', '/magic'], - ['Technology', '/technology'], - ['Utility', '/utility'], - ['Worldgen', '/worldgen'], - ['Miscellaneous', '/miscellaneous'], + 'adventure', + 'magic', + 'technology', + 'utility', + 'worldgen', + 'miscellaneous', ], }, - ...this.loader(false), ]; }, @@ -46,31 +50,4 @@ export const browserCategories = { shaderpack(): BrowserSidebarCategory[] { return []; }, - - provider(): BrowserSidebarCategory[] { - return [ - { - name: 'Providers', - sub: PROVIDERS.map(provider => [provider, `/${provider.toLowerCase()}`]), - }, - ]; - }, - - loader(vanilla: boolean = false): BrowserSidebarCategory[] { - const loaders: [string, string][] = []; - - for (const loader of LOADERS) { - if (vanilla === false && loader === 'vanilla') - continue; - - loaders.push([loader, `/${loader.toLowerCase()}`]); - } - - return [ - { - name: 'Loaders', - sub: loaders, - }, - ]; - }, }; diff --git a/apps/testing/examples/search_packages.rs b/apps/testing/examples/search_packages.rs index 8d4eec99..fc737e15 100644 --- a/apps/testing/examples/search_packages.rs +++ b/apps/testing/examples/search_packages.rs @@ -8,6 +8,7 @@ async fn main() -> Result<()> { let results = provider.search( Some("chatting".to_string()), Some(5), + Some(0), None, None, None, diff --git a/packages/core/src/api/package/content/mod.rs b/packages/core/src/api/package/content/mod.rs index 11877bb0..b43ceab8 100644 --- a/packages/core/src/api/package/content/mod.rs +++ b/packages/core/src/api/package/content/mod.rs @@ -44,6 +44,7 @@ impl Providers { &self, query: Option, limit: Option, + offset: Option, game_versions: Option>, categories: Option>, loaders: Option>, @@ -54,6 +55,7 @@ impl Providers { Providers::Modrinth => modrinth::search( query, limit, + offset, Some(|mut builder: FacetBuilder| { if let Some(game_versions) = game_versions { for version in game_versions { @@ -63,7 +65,7 @@ impl Providers { if let Some(categories) = categories { for category in categories { - builder.or(Facet( + builder.and(Facet( "categories".to_string(), FacetOperation::Eq, category, @@ -73,7 +75,7 @@ impl Providers { if let Some(package_types) = package_types { for package_type in package_types { - builder.or(Facet( + builder.and(Facet( "project_types".to_string(), FacetOperation::Eq, package_type.get_name().to_string(), diff --git a/packages/core/src/api/package/content/modrinth.rs b/packages/core/src/api/package/content/modrinth.rs index e8d2fe09..7ccc5101 100644 --- a/packages/core/src/api/package/content/modrinth.rs +++ b/packages/core/src/api/package/content/modrinth.rs @@ -36,8 +36,7 @@ pub struct ModrinthPackage { // pub donation_urls: Vec, pub project_type: PackageType, pub downloads: u32, - #[serde(default)] - pub icon_url: String, + pub icon_url: Option, #[serde(alias = "project_id")] pub id: String, pub team: String, @@ -57,6 +56,8 @@ pub struct ModrinthPackage { pub license: Option, // #[serde(default)] // pub gallery: Vec, + #[serde(default)] + pub status: String, } impl From for ManagedPackage { @@ -71,7 +72,7 @@ impl From for ManagedPackage { versions: value.versions, game_versions: value.game_versions, loaders: value.loaders, - icon_url: Some(value.icon_url), + icon_url: value.icon_url, created: value.published, updated: value.updated, client: value.client_side, @@ -87,6 +88,7 @@ impl From for ManagedPackage { team: value.team, organization: value.organization, }, + is_archived: value.status == "archived", } } } @@ -114,6 +116,7 @@ pub struct Gallery { #[derive(Deserialize)] struct SearchResults { hits: Vec, + total_hits: u32, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -241,20 +244,23 @@ impl FacetBuilder { } pub fn build(&self) -> String { - let mut builder = String::new(); + let mut builder: Vec = vec![]; for facet in &self.facets { - builder.push('['); + let mut stringified = String::new(); + stringified.push('['); for (i, f) in facet.iter().enumerate() { if i != 0 { - builder.push(','); + stringified.push(','); } - builder.push_str(format!("\"{}\"", f.to_string()).as_str()); + stringified.push_str(format!("\"{}\"", f.to_string()).as_str()); } - builder.push(']'); + stringified.push(']'); + + builder.push(stringified); } - builder + builder.join(",") } } @@ -306,6 +312,7 @@ pub async fn list() -> Result> { pub async fn search( query: Option, limit: Option, + offset: Option, facets: Option, ) -> Result where @@ -316,9 +323,10 @@ where let results: SearchResults = serde_json::from_slice( &fetch( format_url!( - "/search?query={}&limit={}{}", + "/search?query={}&limit={}&offset={}{}", query.unwrap_or_default(), limit.unwrap_or(10), + offset.unwrap_or(0), if facets.is_empty() { "".to_string() } else { @@ -335,6 +343,7 @@ where Ok(ProviderSearchResults { provider: Providers::Modrinth, results: results.hits, + total: results.total_hits, }) } diff --git a/packages/core/src/store/package.rs b/packages/core/src/store/package.rs index 7ca9f4bc..e9f088ad 100644 --- a/packages/core/src/store/package.rs +++ b/packages/core/src/store/package.rs @@ -155,6 +155,7 @@ pub struct SearchResult { pub struct ProviderSearchResults { pub provider: Providers, pub results: Vec, + pub total: u32, } /// A struct that represents a Package. @@ -587,8 +588,9 @@ pub struct ManagedPackage { pub categories: Vec, pub optional_categories: Option>, pub license: Option, - pub author: Author, + + pub is_archived: bool, } #[cfg_attr(feature = "specta", derive(specta::Type))]