diff --git a/.template.env b/.template.env new file mode 100644 index 0000000..434d6c5 --- /dev/null +++ b/.template.env @@ -0,0 +1,3 @@ +#! Use single quotes due to special characters and dotenvy being weird + +CURSEFORGE_API_KEY = '' diff --git a/Cargo.lock b/Cargo.lock index f0b7fb2..1188723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1672,6 +1672,11 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "git+https://github.com/allan2/dotenvy/?rev=db0d6aa8fc8d1b0ddbfe94b5c833372e463eb1c5#db0d6aa8fc8d1b0ddbfe94b5c833372e463eb1c5" + [[package]] name = "dpi" version = "0.1.1" @@ -4375,6 +4380,7 @@ dependencies = [ "chrono", "dirs 5.0.1", "discord-rich-presence", + "dotenvy 0.15.7 (git+https://github.com/allan2/dotenvy/?rev=db0d6aa8fc8d1b0ddbfe94b5c833372e463eb1c5)", "flate2", "futures", "indicatif", @@ -4395,6 +4401,7 @@ dependencies = [ "serde", "serde_ini", "serde_json", + "serde_repr", "sha1_smol", "sha2 0.10.8", "specta 2.0.0-rc.20", @@ -5170,7 +5177,7 @@ dependencies = [ "bigdecimal 0.4.5", "chrono", "diagnostics", - "dotenvy", + "dotenvy 0.15.7 (registry+https://github.com/rust-lang/crates.io-index)", "futures", "include_dir", "indexmap 1.9.3", diff --git a/Cargo.toml b/Cargo.toml index 9fdddfe..701c3cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,7 @@ zeroize = { version = "1.8", features = [ "zeroize_derive" ] } serde = { version = "1.0", features = [ "derive" ] } serde_json = { version = "1.0" } serde_ini = { version = "0.2" } +serde_repr = { version = "0.1" } regex = { version = "1.10" } url = { version = "2.5.2" } strum = { version = "0.26", features = [ "derive" ] } @@ -179,6 +180,7 @@ cocoa = { version = "0.26" } objc = { version = "0.2" } cc = { version = "1.1" } libc = { version = "0.2" } +dotenvy = { git = "https://github.com/allan2/dotenvy/", rev = "db0d6aa8fc8d1b0ddbfe94b5c833372e463eb1c5" } # https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/Cargo.toml#L102 webkit2gtk = { version = "=2.0.1", features = [ "v2_40" ] } diff --git a/apps/desktop/src/api/commands/mod.rs b/apps/desktop/src/api/commands/mod.rs index c96fbe4..8fa041a 100644 --- a/apps/desktop/src/api/commands/mod.rs +++ b/apps/desktop/src/api/commands/mod.rs @@ -67,6 +67,7 @@ macro_rules! collect_commands { get_provider_package_version, search_provider_packages, get_provider_authors, + get_package_body, download_provider_package, // Cluster Packages get_cluster_package, diff --git a/apps/desktop/src/api/commands/package.rs b/apps/desktop/src/api/commands/package.rs index 5469760..b47b4ce 100644 --- a/apps/desktop/src/api/commands/package.rs +++ b/apps/desktop/src/api/commands/package.rs @@ -4,7 +4,10 @@ use onelauncher::cluster::content::package; use onelauncher::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType}; use onelauncher::package::content::Providers; use onelauncher::package::import::{default_launcher_path, ImportType}; -use onelauncher::store::{Author, ClusterPath, Package, PackagePath, ProviderSearchResults}; +use onelauncher::store::{ + Author, ClusterPath, Package, PackageBody, PackagePath, ProviderSearchResults, +}; +use onelauncher::utils::pagination::Pagination; use uuid::Uuid; #[specta::specta] @@ -49,9 +52,11 @@ pub async fn get_all_provider_package_versions( project_id: String, game_versions: Option>, loaders: Option>, -) -> Result, String> { + page: Option, + page_size: Option, +) -> Result<(Vec, Pagination), String> { Ok(provider - .get_all_versions(&project_id, game_versions, loaders) + .get_all_versions(&project_id, game_versions, loaders, page, page_size) .await?) } @@ -70,7 +75,7 @@ pub async fn get_provider_package_version( provider: Providers, version: String, ) -> Result { - Ok(provider.get_version(&version).await?) + Ok(provider.get_version("", &version).await?) // TODO } #[derive(specta::Type, serde::Deserialize, serde::Serialize)] @@ -114,6 +119,12 @@ pub async fn get_provider_authors( Ok(provider.get_authors(&author).await?) } +#[specta::specta] +#[tauri::command] +pub async fn get_package_body(provider: Providers, body: PackageBody) -> Result { + Ok(provider.get_package_body(&body).await?) +} + #[specta::specta] #[tauri::command] pub async fn download_provider_package( @@ -138,6 +149,7 @@ pub async fn download_provider_package( package_version, ) .await?; + package::add_package( &cluster.cluster_path(), pkg_path, diff --git a/apps/frontend/src/ui/components/content/SearchResults.tsx b/apps/frontend/src/ui/components/content/SearchResults.tsx index c3b0422..ef0126e 100644 --- a/apps/frontend/src/ui/components/content/SearchResults.tsx +++ b/apps/frontend/src/ui/components/content/SearchResults.tsx @@ -11,7 +11,6 @@ interface SearchResultsContainerProps { provider: Providers; results: SearchResult[]; header: string | JSX.Element; - category: string; collapsable?: boolean; } @@ -56,7 +55,7 @@ function SearchResultsContainer(props: SearchResultsContainerProps) { -
+
diff --git a/apps/frontend/src/ui/hooks/useBrowser.tsx b/apps/frontend/src/ui/hooks/useBrowser.tsx index 5de57a2..98fa585 100644 --- a/apps/frontend/src/ui/hooks/useBrowser.tsx +++ b/apps/frontend/src/ui/hooks/useBrowser.tsx @@ -1,16 +1,15 @@ -import type { Cluster, ManagedPackage, PackageType, Providers, ProviderSearchQuery, ProviderSearchResults } from '@onelauncher/client/bindings'; -import type { ModalProps } from '~ui/components/overlay/Modal'; +import type { Cluster, ManagedPackage, PackageType, Providers, ProviderSearchQuery, SearchResult } from '@onelauncher/client/bindings'; import { useNavigate } from '@solidjs/router'; import { bridge } from '~imports'; -import Button from '~ui/components/base/Button'; -import Dropdown from '~ui/components/base/Dropdown'; -import Modal, { createModal } from '~ui/components/overlay/Modal'; +import { createModal } from '~ui/components/overlay/Modal'; import BrowserPackage from '~ui/pages/browser/BrowserPackage'; -import { type Accessor, type Context, createContext, createEffect, createSignal, For, on, onMount, type ParentProps, type Setter, Show, untrack, useContext } from 'solid-js'; -import { useRecentCluster } from './useCluster'; -import useCommand, { tryResult } from './useCommand'; +import { PROVIDERS } from '~utils'; +import { type Accessor, type Context, createContext, createEffect, createSignal, on, onMount, type ParentProps, type Setter, useContext } from 'solid-js'; +import { ChooseClusterModal, useRecentCluster } from './useCluster'; +import { tryResult } from './useCommand'; export type ProviderSearchOptions = ProviderSearchQuery & { provider: Providers }; +export type PopularPackages = Record; interface BrowserControllerType { cluster: Accessor; @@ -18,7 +17,6 @@ interface BrowserControllerType { displayBrowser: (cluster?: Cluster | undefined) => void; displayPackage: (id: string, provider: Providers) => void; - displayCategory: (category: string) => void; displayClusterSelector: () => void; search: () => void; @@ -28,16 +26,15 @@ interface BrowserControllerType { packageType: Accessor; setPackageType: Setter; - refreshCache: () => void; - cache: Accessor; - featured: Accessor; + popularPackages: Accessor; + featuredPackage: Accessor; }; const BrowserContext = createContext() as Context; export function BrowserProvider(props: ParentProps) { - const [mainPageCache, setMainPageCache] = createSignal(); - const [featured, setFeatured] = createSignal(); + const [popularPackages, setPopularPackages] = createSignal(); + const [featuredPackage, setFeaturedPackage] = createSignal(); // Used for the current "Browser Mode". It'll only show packages of the selected type const [packageType, setPackageType] = createSignal('mod'); @@ -48,7 +45,7 @@ export function BrowserProvider(props: ParentProps) { const [searchOptions, setSearchOptions] = createSignal({ provider: 'Modrinth', query: '', - limit: 20, + limit: 18, // Even multiples of 3 recommended for grid view (normally 3 columns, 2 columns on very small windows) offset: 0, categories: null, game_versions: null, @@ -60,8 +57,30 @@ export function BrowserProvider(props: ParentProps) { const navigate = useNavigate(); const recentCluster = useRecentCluster(); + const currentlySelectedCluster = (list: Cluster[]) => { + const cluster = controller.cluster(); + + if (cluster === undefined) + return 0; + + if (list === undefined) + return 0; + + const index = list.findIndex(c => c.uuid === cluster.uuid) || 0; + if (index === -1) + return 0; + + return index; + }; + + const chooseCluster = (cluster: Cluster) => { + controller.setCluster(cluster); + }; + const modal = createModal(props => ( currentlySelectedCluster(list) || 0} {...props} /> )); @@ -78,45 +97,55 @@ export function BrowserProvider(props: ParentProps) { navigate(BrowserPackage.buildUrl({ id, provider })); }, - displayCategory(_category: string) { - - }, - displayClusterSelector() { modal.show(); }, - search: () => { - navigate('/browser/search'); - }, + search: () => navigate('/browser/search'), searchQuery: searchOptions, setSearchQuery: setSearchOptions, packageType, setPackageType, - async refreshCache() { - const opts = untrack(searchOptions); - const res = await tryResult(() => bridge.commands.searchProviderPackages('Modrinth', opts)); - - setMainPageCache(res); - - // TODO: Better algorithm for selecting a featured package - const firstPackage = res.results[0]; - if (firstPackage !== undefined) { - const featuredPackage = await tryResult(() => bridge.commands.getProviderPackage('Modrinth', firstPackage.project_id)); - setFeatured(featuredPackage); - } - }, - - cache: mainPageCache, - - featured, + popularPackages, + featuredPackage, }; - onMount(() => { - if (mainPageCache() === undefined) - controller.refreshCache(); + onMount(async () => { + const getOpts = (provider: Providers): ProviderSearchOptions => ({ + provider, + query: '', + limit: 10, + offset: 0, + categories: null, + game_versions: null, + loaders: null, + package_types: ['mod'], + open_source: null, + }); + + const response = await Promise.allSettled( + PROVIDERS.map(provider => tryResult(() => bridge.commands.searchProviderPackages(provider, getOpts(provider)))), + ); + + const popularPackages = response.reduce((acc, res, i) => { + const provider = PROVIDERS[i] as Providers; + + if (res.status === 'fulfilled') + acc[provider] = res.value.results; + + return acc; + }, {} as PopularPackages); + + setPopularPackages(popularPackages); + + // TODO: Better algorithm for selecting a featured package + const firstPackage = popularPackages.Modrinth[0] || popularPackages.Curseforge[0]; + if (firstPackage !== undefined) { + const featuredPackage = await tryResult(() => bridge.commands.getProviderPackage('Modrinth', firstPackage.project_id)); + setFeaturedPackage(featuredPackage); + } }); createEffect(() => { @@ -153,73 +182,3 @@ export default function useBrowser() { return controller; } - -function ChooseClusterModal(props: ModalProps) { - const [selected, setSelected] = createSignal(0); - const [clusters] = useCommand(() => bridge.commands.getClusters()); - const controller = useBrowser(); - - const currentlySelectedCluster = (list: Cluster[]) => { - const cluster = controller.cluster(); - - if (cluster === undefined) - return 0; - - if (list === undefined) - return 0; - - const index = list.findIndex(c => c.uuid === cluster.uuid) || 0; - if (index === -1) - return 0; - - return index; - }; - - createEffect(() => { - setSelected(currentlySelectedCluster(clusters() || [])); - }); - - function chooseCluster() { - const index = untrack(selected); - const clusterz = untrack(clusters); - if (clusterz !== undefined && index !== undefined) - controller.setCluster(clusterz[index]); - - props.hide(); - } - - return ( - , -