diff --git a/apps/desktop/src/api/commands/package.rs b/apps/desktop/src/api/commands/package.rs index a646f0a5..53bfa25c 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, PackageBody, 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, None, None) // TODO + .get_all_versions(&project_id, game_versions, loaders, page, page_size) .await?) } @@ -61,7 +66,7 @@ pub async fn get_provider_package_versions( provider: Providers, versions: Vec, ) -> Result, String> { - Ok(provider.get_versions("", versions).await?) // TODO + Ok(provider.get_versions(versions).await?) } #[specta::specta] @@ -70,7 +75,7 @@ pub async fn get_provider_package_version( provider: Providers, version: String, ) -> Result { - Ok(provider.get_version("", &version).await?) // TODO + Ok(provider.get_version("", &version).await?) // TODO } #[derive(specta::Type, serde::Deserialize, serde::Serialize)] @@ -116,10 +121,7 @@ pub async fn get_provider_authors( #[specta::specta] #[tauri::command] -pub async fn get_package_body( - provider: Providers, - body: PackageBody -) -> Result { +pub async fn get_package_body(provider: Providers, body: PackageBody) -> Result { Ok(provider.get_package_body(&body).await?) } diff --git a/apps/frontend/src/ui/components/content/FormattedLog.tsx b/apps/frontend/src/ui/components/content/FormattedLog.tsx index 12b8ae72..b789dad3 100644 --- a/apps/frontend/src/ui/components/content/FormattedLog.tsx +++ b/apps/frontend/src/ui/components/content/FormattedLog.tsx @@ -1,8 +1,8 @@ +import type { Ref } from '@solid-primitives/refs'; +import type { OnUpdatedEventListenerArgs, OverlayScrollbars } from 'overlayscrollbars'; import { ChevronDownDoubleIcon } from '@untitled-theme/icons-solid'; import { OverlayScrollbarsComponent, type OverlayScrollbarsComponentRef } from 'overlayscrollbars-solid'; import { createEffect, createMemo, createSignal, Index, Show, untrack } from 'solid-js'; -import type { Ref } from '@solid-primitives/refs'; -import type { OnUpdatedEventListenerArgs, OverlayScrollbars } from 'overlayscrollbars'; import Button from '../base/Button'; import styles from './FormattedLog.module.scss'; diff --git a/apps/frontend/src/ui/components/content/ProviderIcon.tsx b/apps/frontend/src/ui/components/content/ProviderIcon.tsx index 4c16a8be..b112319a 100644 --- a/apps/frontend/src/ui/components/content/ProviderIcon.tsx +++ b/apps/frontend/src/ui/components/content/ProviderIcon.tsx @@ -1,6 +1,6 @@ +import type { ManagedPackage, Providers } from '@onelauncher/client/bindings'; import ModrinthImage from '~assets/logos/modrinth.svg?component-solid'; import { type Component, type JSX, Match, Show, splitProps, Switch } from 'solid-js'; -import type { ManagedPackage, Providers } from '@onelauncher/client/bindings'; export function getProviderLogoElement(provider: ManagedPackage | Providers): string | Component { const providerName = (typeof provider === 'string' ? provider : provider.provider)?.toLowerCase() as Lowercase; diff --git a/apps/frontend/src/ui/hooks/usePagination.tsx b/apps/frontend/src/ui/hooks/usePagination.tsx index 1ab2f55a..62274636 100644 --- a/apps/frontend/src/ui/hooks/usePagination.tsx +++ b/apps/frontend/src/ui/hooks/usePagination.tsx @@ -108,6 +108,7 @@ function usePagination(initialOptions: PaginationOptions) { totalPages, offset, reset, + options, Navigation, }; } diff --git a/apps/frontend/src/ui/pages/browser/BrowserPackage.tsx b/apps/frontend/src/ui/pages/browser/BrowserPackage.tsx index 17f95fde..1b91cea0 100644 --- a/apps/frontend/src/ui/pages/browser/BrowserPackage.tsx +++ b/apps/frontend/src/ui/pages/browser/BrowserPackage.tsx @@ -11,11 +11,11 @@ import Tooltip from '~ui/components/base/Tooltip'; import Markdown from '~ui/components/content/Markdown'; import Spinner from '~ui/components/Spinner'; import useBrowser from '~ui/hooks/useBrowser'; -import useCommand from '~ui/hooks/useCommand'; +import useCommand, { tryResult } from '~ui/hooks/useCommand'; import usePagination from '~ui/hooks/usePagination'; import usePromptOpener from '~ui/hooks/usePromptOpener'; import { abbreviateNumber, formatAsRelative } from '~utils'; -import { type Accessor, createContext, createEffect, createSignal, For, Match, on, type ParentProps, type Resource, Show, Switch, useContext } from 'solid-js'; +import { type Accessor, createContext, createEffect, createResource, createSignal, For, Match, on, type ParentProps, type Resource, Show, Switch, useContext } from 'solid-js'; import { getLicenseUrl, getPackageUrl, upperFirst } from '../../../utils'; interface BrowserModParams extends Params { @@ -148,7 +148,7 @@ function BrowserSidebar(props: { package: ManagedPackage }) {
-

{props.package.title}

+

{props.package.title}

{props.package.package_type} {' '} @@ -383,7 +383,7 @@ function BrowserPackageVersions() { const context = useContext(BrowserPackageContext); const MAX_ITEMS_PER_PAGE = 20; - const { page, Navigation } = usePagination({ + const { page, Navigation, reset, options } = usePagination({ itemsCount: () => context?.pkg().versions.length || 0, itemsPerPage: () => MAX_ITEMS_PER_PAGE, }); @@ -399,13 +399,25 @@ function BrowserPackageVersions() { return newestToOldest.slice(start, end); }; - const [versions, { refetch }] = useCommand(context, async () => { + const [versions, { refetch }] = createResource(context, async () => { if (context === null) - return { error: 'No package context', status: 'error' }; + throw new Error('Context is null'); + + if (context.pkg().versions.length === 0) { + const [data, pagination] = (await tryResult(() => bridge.commands.getAllProviderPackageVersions(context.pkg().provider, context.pkg().id, null, null, page() - 1, MAX_ITEMS_PER_PAGE))) || []; + + if (options().itemsCount() !== pagination.totalCount) + reset({ + itemsCount: () => pagination.totalCount || 0, + itemsPerPage: () => MAX_ITEMS_PER_PAGE, + }); + + return data; + } const list = getVersionsForPage(page()); - return await bridge.commands.getProviderPackageVersions(context.pkg().provider, list); + return await tryResult(() => bridge.commands.getProviderPackageVersions(context.pkg().provider, list)); }); let container!: HTMLDivElement; @@ -484,8 +496,8 @@ function VersionRow(props: ManagedVersion) {

-

{props.version_id}

-

{props.name}

+

{props.version_display}

+

{props.name === props.version_display ? props.files[0]?.file_name : props.name}

diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 630c407a..1358e436 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -33,6 +33,14 @@ export default defineConfig(async ({ mode }) => { emptyOutDir: false, }, + css: { + preprocessorOptions: { + scss: { + api: 'modern-compiler', + }, + }, + }, + clearScreen: false, server: { port: 8001, diff --git a/apps/testing/examples/search_packages.rs b/apps/testing/examples/search_packages.rs index 6ce50830..58e5f558 100644 --- a/apps/testing/examples/search_packages.rs +++ b/apps/testing/examples/search_packages.rs @@ -31,4 +31,4 @@ async fn search(provider: Providers) -> Result<()> { println!("{:#?}", results); Ok(()) -} \ No newline at end of file +} diff --git a/packages/core/build.rs b/packages/core/build.rs index e57d8343..e7d1fb00 100644 --- a/packages/core/build.rs +++ b/packages/core/build.rs @@ -1,10 +1,7 @@ use std::path::PathBuf; fn main() { - let paths = vec![ - get_root_workspace().join(".env"), - PathBuf::from(".env"), - ]; + let paths = vec![get_root_workspace().join(".env"), PathBuf::from(".env")]; for path in paths { if path.exists() { @@ -33,9 +30,11 @@ fn get_root_workspace() -> PathBuf { } fn load_from_path(path: PathBuf) { - let vars = dotenvy::EnvLoader::with_path(path).load().expect("failed to find .env file"); + let vars = dotenvy::EnvLoader::with_path(path) + .load() + .expect("failed to find .env file"); - for (key, value) in vars { - println!("cargo:rustc-env={key}={value}"); - } -} \ No newline at end of file + for (key, value) in vars { + println!("cargo:rustc-env={key}={value}"); + } +} diff --git a/packages/core/src/api/cluster/content/package.rs b/packages/core/src/api/cluster/content/package.rs index eb8ac777..49c7158c 100644 --- a/packages/core/src/api/cluster/content/package.rs +++ b/packages/core/src/api/cluster/content/package.rs @@ -33,7 +33,7 @@ pub async fn find_managed_version( ) .await?; - Ok(versions + Ok(versions.0 .iter() .find(|v| { let check_game_version = game_version @@ -46,7 +46,7 @@ pub async fn find_managed_version( let check_package_version = package_version .as_ref() - .map_or(true, |pv| pv == &v.version_id); + .map_or(true, |pv| pv == &v.version_display); check_game_version && check_loader && check_package_version }) diff --git a/packages/core/src/api/package/content/curseforge.rs b/packages/core/src/api/package/content/curseforge.rs index 0d3b9dd3..38008ba8 100644 --- a/packages/core/src/api/package/content/curseforge.rs +++ b/packages/core/src/api/package/content/curseforge.rs @@ -7,7 +7,10 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use url::Url; -use crate::{data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType}, store::{ProviderSearchResults, SearchResult}, utils::http, Result, State}; +use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType}; +use crate::store::{ManagedVersionFile, ManagedVersionReleaseType, ProviderSearchResults, SearchResult}; +use crate::utils::{http, pagination::Pagination}; +use crate::{Result, State}; /// Mapping of Curseforge "classes" to their respective category ids #[derive(Debug, Default, Serialize_repr, Deserialize_repr)] @@ -57,6 +60,18 @@ pub enum CurseforgeLoader { NeoForge = 6, } +impl From for CurseforgeLoader { + fn from(loader: String) -> Self { + match loader.to_lowercase().as_str() { + "forge" => CurseforgeLoader::Forge, + "fabric" => CurseforgeLoader::Fabric, + "quilt" => CurseforgeLoader::Quilt, + "neoforge" => CurseforgeLoader::NeoForge, + _ => CurseforgeLoader::Any, + } + } +} + impl From for CurseforgeLoader { fn from(loader: Loader) -> Self { match loader { @@ -69,48 +84,50 @@ impl From for CurseforgeLoader { } } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -#[allow(dead_code)] -pub struct Pagination { - pub index: u32, - pub page_size: u32, - pub result_count: u32, - pub total_count: u32, +impl Into for CurseforgeLoader { + fn into(self) -> Loader { + match self { + CurseforgeLoader::Forge => Loader::Forge, + CurseforgeLoader::Fabric => Loader::Fabric, + CurseforgeLoader::Quilt => Loader::Quilt, + CurseforgeLoader::NeoForge => Loader::NeoForge, + _ => Loader::Vanilla, + } + } } #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CurseforgePackage { - pub id: u32, - pub game_id: u32, + pub id: u32, + pub game_id: u32, #[serde(alias = "classId")] pub package_type: CFPackageType, - pub name: String, - pub slug: String, - pub links: Links, - pub summary: String, - // pub status: u32, - pub download_count: u64, - pub is_featured: bool, - // pub primary_category_id: u32, - pub categories: Vec, - pub authors: Vec, - pub logo: Logo, - // pub screenshots: Vec, - // pub main_file_id: u32, - // pub latest_files: Vec, - // pub latest_files_indexes: Vec, - // pub latest_early_access_files_indexes: Vec, - pub date_created: DateTime, - pub date_modified: DateTime, - pub date_released: DateTime, + pub name: String, + pub slug: String, + pub links: Links, + pub summary: String, + // pub status: u32, + pub download_count: u64, + pub is_featured: bool, + // pub primary_category_id: u32, + pub categories: Vec, + pub authors: Vec, + pub logo: Logo, + // pub screenshots: Vec, + // pub main_file_id: u32, + // pub latest_files: Vec, + // pub latest_files_indexes: Vec, + // pub latest_early_access_files_indexes: Vec, + pub date_created: DateTime, + pub date_modified: DateTime, + pub date_released: DateTime, #[serde(default)] - pub allow_mod_distribution: bool, - // pub game_popularity_rank: u32, - // pub is_available: bool, - pub thumbs_up_count: u32, - pub rating: Option, + pub allow_mod_distribution: bool, + // pub game_popularity_rank: u32, + // pub is_available: bool, + pub thumbs_up_count: u32, + pub rating: Option, } impl From for ManagedPackage { @@ -137,15 +154,19 @@ impl From for ManagedPackage { optional_categories: None, license: None, author: crate::store::Author::Users( - package.authors.into_iter().map(|a| ManagedUser { - id: a.id.to_string(), - username: a.name, - url: Some(a.url), - is_organization_user: false, - avatar_url: None, - bio: None, - role: None, - }).collect() + package + .authors + .into_iter() + .map(|a| ManagedUser { + id: a.id.to_string(), + username: a.name, + url: Some(a.url), + is_organization_user: false, + avatar_url: None, + bio: None, + role: None, + }) + .collect(), ), is_archived: false, } @@ -158,7 +179,11 @@ impl Into for CurseforgePackage { title: self.name, slug: self.slug, project_id: self.id.to_string(), - author: self.authors.first().and_then(|a| Some(a.name.clone())).unwrap_or(String::from("Unknown")), + author: self + .authors + .first() + .and_then(|a| Some(a.name.clone())) + .unwrap_or(String::from("Unknown")), description: self.summary, client_side: crate::store::PackageSide::Unknown, server_side: crate::store::PackageSide::Unknown, @@ -178,134 +203,223 @@ impl Into for CurseforgePackage { #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Links { - pub website_url: Option, - pub wiki_url: Option, - pub issues_url: Option, - pub source_url: Option, + pub website_url: Option, + pub wiki_url: Option, + pub issues_url: Option, + pub source_url: Option, } #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Category { - pub id: u32, - pub game_id: u32, - pub name: String, - pub slug: String, - pub url: String, - pub icon_url: String, - pub date_modified: String, - // pub parent_category_id: u32, - // pub display_index: u32, + pub id: u32, + pub game_id: u32, + pub name: String, + pub slug: String, + pub url: String, + pub icon_url: String, + pub date_modified: String, + // pub parent_category_id: u32, + // pub display_index: u32, } #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Author { - pub id: u32, - pub name: String, - pub url: String, + pub id: u32, + pub name: String, + pub url: String, } #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Logo { - pub id: u32, - pub mod_id: u32, - pub title: String, - pub description: String, - pub thumbnail_url: String, - pub url: String, + pub id: u32, + pub mod_id: u32, + pub title: String, + pub description: String, + pub thumbnail_url: String, + pub url: String, } #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Screenshot { - pub id: u32, - pub mod_id: u32, - pub title: String, - pub description: String, - pub thumbnail_url: String, - pub url: String, + pub id: u32, + pub mod_id: u32, + pub title: String, + pub description: String, + pub thumbnail_url: String, + pub url: String, } -#[derive(Default, Debug, Serialize, Deserialize)] +#[derive(Default, Serialize_repr, Deserialize_repr, Clone, Debug)] +#[repr(u8)] +pub enum ModFileReleaseType { + #[default] + Release = 1, + Beta = 2, + Alpha = 3, +} + +impl Into for ModFileReleaseType { + fn into(self) -> ManagedVersionReleaseType { + match self { + ModFileReleaseType::Release => ManagedVersionReleaseType::Release, + ModFileReleaseType::Beta => ManagedVersionReleaseType::Beta, + ModFileReleaseType::Alpha => ManagedVersionReleaseType::Alpha, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ModFile { - pub id: u32, - pub game_id: u32, - pub mod_id: u32, - pub is_available: bool, - pub display_name: String, - pub file_name: String, - // pub release_type: u32, - // pub file_status: u32, - // pub hashes: Vec, - // pub file_date: String, - // pub file_length: u32, - pub download_count: u32, - // pub file_size_on_disk: u32, - pub download_url: Option, - pub game_versions: Vec, - pub sortable_game_versions: Vec, - pub dependencies: Vec, - // pub expose_as_alternative: bool, - // pub parent_project_file_id: u32, - // pub alternate_file_id: u32, - // pub is_server_pack: bool, - // pub server_pack_file_id: u32, - // pub is_early_access_content: bool, - // pub early_access_end_date: String, - // pub file_fingerprint: u32, - // pub modules: Vec, + pub id: u32, + pub game_id: u32, + pub mod_id: u32, + pub is_available: bool, + pub display_name: String, + pub file_name: String, + pub release_type: ModFileReleaseType, + // pub file_status: u32, + pub hashes: Vec, + pub file_date: DateTime, + pub file_length: u64, + pub download_count: u32, + pub file_size_on_disk: Option, + pub download_url: Option, + pub game_versions: Vec, + pub sortable_game_versions: Vec, + pub dependencies: Vec, + // pub expose_as_alternative: bool, + // pub parent_project_file_id: u32, + // pub alternate_file_id: u32, + // pub is_server_pack: bool, + // pub server_pack_file_id: u32, + // pub is_early_access_content: bool, + // pub early_access_end_date: String, + // pub file_fingerprint: u32, + // pub modules: Vec, } -#[derive(Default, Debug, Serialize, Deserialize)] +impl Into for ModFile { + fn into(self) -> ManagedVersion { + let mut hashes = HashMap::new(); + + for hash in self.hashes { + hashes.insert(hash.algo.name(), hash.value); + } + + let mut game_versions = Vec::new(); + let mut loader = CurseforgeLoader::Any; + + for version in self.sortable_game_versions { + if version.game_version.is_empty() { + // This is most likely a loader + loader = CurseforgeLoader::from(version.game_version_name); + continue; + } + + game_versions.push(version.game_version); + } + + let mut files = Vec::new(); + if let Some(download_url) = self.download_url { + files.push(ManagedVersionFile { + url: download_url, + file_name: self.file_name, + primary: true, + size: self.file_size_on_disk.unwrap_or(self.file_length), + file_type: Some(crate::store::PackageFile::RequiredPack), + hashes, + }); + } + + ManagedVersion { + package_id: self.mod_id.to_string(), + id: self.id.to_string(), + name: self.display_name.clone(), + author: String::new(), + loaders: vec![Into::::into(loader)], + changelog: String::new(), + changelog_url: None, + deps: vec![], + downloads: self.download_count, + featured: false, + is_available: self.is_available && files.len() > 0, + files, + game_versions: game_versions, + published: self.file_date, + version_display: self.display_name, + version_type: self.release_type.into(), + } + } +} + +#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)] +#[repr(u8)] +pub enum HashAlgorithm { + Sha1 = 1, + MD5 = 2, +} + +impl HashAlgorithm { + pub fn name(&self) -> String { + match self { + Self::Sha1 => "sha1", + Self::MD5 => "md5", + }.to_string() + } +} + +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Hash { - pub value: String, - pub algo: u32, + pub value: String, + pub algo: HashAlgorithm, } #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SortableGameVersion { - pub game_version_name: String, - pub game_version_padded: String, - pub game_version: String, - pub game_version_release_date: String, - pub game_version_type_id: u32, + pub game_version_name: String, + pub game_version_padded: String, + pub game_version: String, + pub game_version_release_date: String, + pub game_version_type_id: u32, } #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Dependency { - pub mod_id: u32, - pub relation_type: u32, + pub mod_id: u32, + pub relation_type: u32, } #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Module { - pub name: String, - pub fingerprint: u32, + pub name: String, + pub fingerprint: u32, } #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ModFilesIndex { - pub game_version: String, - pub file_id: u32, - pub filename: String, - pub release_type: u32, - pub game_version_type_id: u32, + pub game_version: String, + pub file_id: u32, + pub filename: String, + pub release_type: u32, + pub game_version_type_id: u32, #[serde(default)] - pub mod_loader: CurseforgeLoader, + pub mod_loader: CurseforgeLoader, } // https://docs.curseforge.com/rest-api/?shell#tocS_ModsSearchSortField #[allow(dead_code)] enum SearchSort { - Featured = 1, + Featured = 1, Popularity = 2, LastUpdated = 3, Name = 4, @@ -319,28 +433,38 @@ struct CFData { data: T, } -pub async fn fetch_url_builder(url: &str, url_builder: Option) -> Result { - // TODO: Get the API key from settings, fallback to constant, and error if missing - let key = crate::constants::CURSEFORGE_API_KEY.ok_or(anyhow::anyhow!("missing curseforge api key"))?; +#[derive(Default, Debug, Serialize, Deserialize)] +struct CFPaginatedData { + pagination: Pagination, + data: T, +} - let mut headers = HashMap::<&str, &str>::new(); +pub async fn fetch_advanced( + method: Method, + url: &str, + url_builder: Option, + headers: Option>, + json_body: Option, +) -> Result { + // TODO: Get the API key from settings, fallback to constant, and error if missing + let key = crate::constants::CURSEFORGE_API_KEY + .ok_or(anyhow::anyhow!("missing curseforge api key"))?; + + let mut headers = headers.unwrap_or(HashMap::<&str, &str>::new()); headers.insert("x-api-key", key); - let url = &mut Url::parse(format!( - "{}{}", - crate::constants::CURSEFORGE_API_URL, - url, - ).as_str())?; + let url = + &mut Url::parse(format!("{}{}", crate::constants::CURSEFORGE_API_URL, url).as_str())?; if let Some(url_builder) = url_builder { url_builder(url); } http::fetch_advanced( - Method::GET, + method, url.as_str(), None, - None, + json_body, Some(headers), None, &State::get().await?.fetch_semaphore, @@ -348,6 +472,13 @@ pub async fn fetch_url_builder(url: &str, url_builder: Opti .await } +pub async fn fetch_url_builder( + url: &str, + url_builder: Option, +) -> Result { + fetch_advanced(Method::GET, url, url_builder, None, None).await +} + async fn fetch(url: &str) -> Result { fetch_url_builder(url, None::).await } @@ -362,11 +493,7 @@ pub async fn search( loaders: Option>, package_types: Option>, ) -> Result { - #[derive(Deserialize)] - struct CFSearchResults { - pagination: Pagination, - data: Vec - } + type CFSearchResults = CFPaginatedData>; let results: CFSearchResults = serde_json::from_slice( &fetch_url_builder( @@ -374,10 +501,16 @@ pub async fn search( Some(Box::new(move |url: &mut Url| { let params = &mut url.query_pairs_mut(); - params.append_pair("gameId", crate::constants::CURSEFORGE_GAME_ID.to_string().as_str()); + params.append_pair( + "gameId", + crate::constants::CURSEFORGE_GAME_ID.to_string().as_str(), + ); params.append_pair("pageSize", limit.unwrap_or(10).to_string().as_str()); params.append_pair("index", offset.unwrap_or(0).to_string().as_str()); - params.append_pair("sortField", (SearchSort::Popularity as u8).to_string().as_str()); + params.append_pair( + "sortField", + (SearchSort::Popularity as u8).to_string().as_str(), + ); params.append_pair("sortOrder", "desc"); if let Some(query) = query { @@ -386,11 +519,33 @@ pub async fn search( if let Some(game_versions) = game_versions { // Need to create a list e.g. ["1.16.5", "1.17.1"] - params.append_pair("gameVersions", format!("[{}]", game_versions.into_iter().map(|v| format!("\"{}\"", v)).collect::>().join(",")).as_str()); + params.append_pair( + "gameVersions", + format!( + "[{}]", + game_versions + .into_iter() + .map(|v| format!("\"{}\"", v)) + .collect::>() + .join(",") + ) + .as_str(), + ); } if let Some(loaders) = loaders { - params.append_pair("modLoaderTypes", format!("[{}]", loaders.into_iter().map(|l| (CurseforgeLoader::from(l) as u32).to_string()).collect::>().join(",")).as_str()); + params.append_pair( + "modLoaderTypes", + format!( + "[{}]", + loaders + .into_iter() + .map(|l| (CurseforgeLoader::from(l) as u32).to_string()) + .collect::>() + .join(",") + ) + .as_str(), + ); } if let Some(package_types) = package_types { @@ -399,42 +554,105 @@ pub async fn search( } if package_types.len() > 1 { - tracing::warn!("curseforge provider does not support multiple package types"); + tracing::warn!( + "curseforge provider does not support multiple package types" + ); } let package_type = package_types.first().unwrap(); - params.append_pair("classId", (CFPackageType::from(*package_type) as u32).to_string().as_str()); + params.append_pair( + "classId", + (CFPackageType::from(*package_type) as u32) + .to_string() + .as_str(), + ); } - })) - ).await?)?; + })), + ) + .await?, + )?; Ok(ProviderSearchResults { total: results.pagination.total_count, - results: results.data.into_iter().map(Into::into).collect::>(), + results: results + .data + .into_iter() + .map(Into::into) + .collect::>(), provider: crate::package::content::Providers::Curseforge, }) } pub async fn get(id: u32) -> Result { - Ok(serde_json::from_slice::>( - &fetch(format!("/v1/mods/{}", id).as_str()) - .await?, - )?.data) + Ok( + serde_json::from_slice::>(&fetch(format!("/v1/mods/{}", id).as_str()).await?)? + .data, + ) } pub async fn get_package_body(url: String) -> Result { - Ok(serde_json::from_slice::>( - &fetch(&url).await?, - )?.data) + Ok(serde_json::from_slice::>(&fetch(&url).await?)?.data) +} + +pub async fn get_all_versions( + project_id: &str, + game_versions: Option>, + loaders: Option>, + page: Option, + page_size: Option, +) -> Result<(Vec, Pagination)> { + let data: CFPaginatedData> = serde_json::from_slice::>( + &fetch_url_builder( + format!("/v1/mods/{project_id}/files").as_str(), + Some(Box::new(move |url: &mut Url| { + if let Some(game_versions) = game_versions { + if let Some(version) = game_versions.first() { + url.query_pairs_mut().append_pair("gameVersion", version); + } + } + + if let Some(loader) = loaders { + if let Some(loader) = loader.first() { + url.query_pairs_mut().append_pair( + "modLoaderType", + (CurseforgeLoader::from(*loader) as u8).to_string().as_str(), + ); + } + } + + let page = page.unwrap_or(0); + url.query_pairs_mut().append_pair("index", page.to_string().as_str()); + + let page_size = page_size.unwrap_or(10); + url.query_pairs_mut().append_pair("pageSize", page_size.to_string().as_str()); + })), + ) + .await?, + )?; + + Ok((data.data, data.pagination)) +} + +pub async fn get_versions( + versions: Vec, +) -> Result> { + let mut map = serde_json::Map::new(); + map.insert("fileIds".to_string(), versions.iter().map(|v| v.parse::().unwrap()).collect::>().into()); + + let json_body = serde_json::Value::Object(map); + + Ok(serde_json::from_slice::>( + &fetch_advanced( + Method::POST, + format!("/v1/mods/files").as_str(), + None::, + None, + Some(json_body) + ) + .await?, + )? + .data) } -// pub async fn get_all_versions( -// project_id: &str, -// game_versions: Option>, -// loaders: Option>, -// page: Option, -// page_size: Option, -// ) -> Result> { -// } \ No newline at end of file diff --git a/packages/core/src/api/package/content/mod.rs b/packages/core/src/api/package/content/mod.rs index 74df8030..110c1a3d 100644 --- a/packages/core/src/api/package/content/mod.rs +++ b/packages/core/src/api/package/content/mod.rs @@ -8,10 +8,11 @@ use serde::{Deserialize, Serialize}; use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType}; use crate::package::content::modrinth::FacetBuilder; use crate::store::{Author, PackageBody, ProviderSearchResults}; +use crate::utils::pagination::Pagination; use crate::Result; -mod modrinth; mod curseforge; +mod modrinth; /// Providers for content packages #[cfg_attr(feature = "specta", derive(specta::Type))] @@ -59,35 +60,49 @@ impl Providers { open_source: Option, ) -> Result { match self { - Self::Modrinth => modrinth::search( - query, - limit, - offset, - Some(|mut builder| build_facets( - &mut builder, - categories, + Self::Modrinth => { + modrinth::search( + query, + limit, + offset, + Some(|mut builder| { + build_facets( + &mut builder, + categories, + game_versions, + loaders, + package_types, + open_source, + ) + }), + ) + .await + } + Self::Curseforge => { + curseforge::search( + query, + limit, + offset, game_versions, + categories, loaders, package_types, - open_source - )), - ).await, - Self::Curseforge => curseforge::search( - query, - limit, - offset, - game_versions, - categories, - loaders, - package_types - ).await + ) + .await + } } } pub async fn get(&self, slug_or_id: &str) -> Result { Ok(match self { Self::Modrinth => modrinth::get(slug_or_id).await?.into(), - Self::Curseforge => curseforge::get(slug_or_id.parse::().map_err(|err| anyhow::anyhow!(err))?).await?.into(), + Self::Curseforge => curseforge::get( + slug_or_id + .parse::() + .map_err(|err| anyhow::anyhow!(err))?, + ) + .await? + .into(), }) } @@ -109,35 +124,33 @@ impl Providers { loaders: Option>, page: Option, page_size: Option, - ) -> Result> { - Ok( - match self { - Self::Modrinth => modrinth::get_all_versions(project_id, game_versions, loaders), - // Self::Curseforge => curseforge::get_all_versions( - // project_id, - // game_versions.map(|list| list.first()), - // loaders.map(|list| list.first()), - // page, - // page_size - // ), - Self::Curseforge => todo!(), + ) -> Result<(Vec, Pagination)> { + Ok(match self { + Self::Modrinth => { + let data = modrinth::get_all_versions(project_id, game_versions, loaders, page, page_size).await?; + (data.0.into_iter().map(Into::into).collect(), data.1) } - .await? - .into_iter() - .map(Into::into) - .collect() - ) + + Self::Curseforge => { + let data = curseforge::get_all_versions(project_id, game_versions, loaders, page, page_size).await?; + (data.0.into_iter().map(Into::into).collect(), data.1) + } + }) } - pub async fn get_versions(&self, pkg_id: &str, versions: Vec) -> Result> { + pub async fn get_versions(&self, versions: Vec) -> Result> { Ok(match self { - Self::Modrinth => modrinth::get_versions(versions), - Self::Curseforge => todo!(), - } - .await? - .into_iter() - .map(Into::into) - .collect()) + Self::Modrinth => modrinth::get_versions(versions) + .await? + .into_iter() + .map(Into::into) + .collect(), + Self::Curseforge => curseforge::get_versions(versions) + .await? + .into_iter() + .map(Into::into) + .collect(), + }) } pub async fn get_version(&self, pkg_id: &str, version_id: &str) -> Result { @@ -151,25 +164,21 @@ impl Providers { pub async fn get_authors(&self, author: &Author) -> Result> { Ok(match author { - Author::Team { .. } => { - match self { - Self::Modrinth => modrinth::get_authors(author).await?, - _ => return Err(anyhow::anyhow!("{} does not support teams", self).into()), - } + Author::Team { .. } => match self { + Self::Modrinth => modrinth::get_authors(author).await?, + _ => return Err(anyhow::anyhow!("{} does not support teams", self).into()), }, - Author::Users(users) => users.to_owned() + Author::Users(users) => users.to_owned(), }) } pub async fn get_package_body(&self, body: &PackageBody) -> Result { Ok(match body { - PackageBody::Url(url) => { - match self { - Self::Curseforge => curseforge::get_package_body(url.to_owned()).await?, - _ => return Err(anyhow::anyhow!("{} does not support direct URLs", self).into()), - } + PackageBody::Url(url) => match self { + Self::Curseforge => curseforge::get_package_body(url.to_owned()).await?, + _ => return Err(anyhow::anyhow!("{} does not support direct URLs", self).into()), }, - PackageBody::Markdown(markdown) => markdown.to_owned() + PackageBody::Markdown(markdown) => markdown.to_owned(), }) } } diff --git a/packages/core/src/api/package/content/modrinth.rs b/packages/core/src/api/package/content/modrinth.rs index 549d6987..24407aa7 100644 --- a/packages/core/src/api/package/content/modrinth.rs +++ b/packages/core/src/api/package/content/modrinth.rs @@ -3,10 +3,10 @@ use std::fmt::{Display, Formatter}; use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType}; use crate::store::{ - Author, License, ManagedVersionFile, PackageFile, PackageSide, ProviderSearchResults, - SearchResult, + Author, License, ManagedVersionFile, ManagedVersionReleaseType, PackageFile, PackageSide, ProviderSearchResults, SearchResult }; use crate::utils::http::fetch; +use crate::utils::pagination::Pagination; use crate::{Result, State}; use chrono::{DateTime, Utc}; @@ -145,7 +145,7 @@ pub struct File { pub url: String, pub filename: String, pub primary: bool, - pub size: u32, + pub size: u64, pub file_type: Option, } @@ -171,15 +171,16 @@ impl From for ManagedVersion { name: value.name, featured: value.featured, - version_id: value.version_number, + version_display: value.version_number, changelog: value.changelog, changelog_url: value.changelog_url, published: value.date_published, downloads: value.downloads, - version_type: value.version_type, + version_type: ManagedVersionReleaseType::from(value.version_type), files: value.files.into_iter().map(Into::into).collect(), + is_available: true, deps: vec![], // TODO [`ManagedDependency`]? game_versions: value.game_versions, loaders: value @@ -439,7 +440,9 @@ pub async fn get_all_versions( project_id: &str, game_versions: Option>, loaders: Option>, -) -> Result> { + page: Option, + page_size: Option, +) -> Result<(Vec, Pagination)> { let mut url = url::Url::parse(format_url!("/project/{}/version", project_id).as_str())?; if let Some(game_versions) = game_versions { url.query_pairs_mut().append_pair( @@ -467,14 +470,26 @@ pub async fn get_all_versions( ); } - Ok(serde_json::from_slice( + let data = serde_json::from_slice::>( &fetch( format_url!("/project/{}/version", project_id).as_str(), None, &State::get().await?.fetch_semaphore, ) .await?, - )?) + )?; + + let data_len = data.len() as u32; + + Ok(( + data, + Pagination { + index: page.unwrap_or(1), + page_size: page_size.unwrap_or(10), + result_count: data_len, + total_count: data_len, + }, + )) } pub async fn get_versions(versions: Vec) -> Result> { diff --git a/packages/core/src/store/package.rs b/packages/core/src/store/package.rs index 5b151af5..a719f9cf 100644 --- a/packages/core/src/store/package.rs +++ b/packages/core/src/store/package.rs @@ -13,8 +13,6 @@ use tokio::fs::DirEntry; use super::{ClusterPath, Clusters, Directories, PackagePath}; -// TODO: Curseforge, Modrinth, SkyClient integration - /// Represents types of packages handled by the launcher. #[cfg_attr(feature = "specta", derive(specta::Type))] #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -554,7 +552,7 @@ impl PackageMetadata { Self::Managed { package_id: package.id, version_id: version.id, - version_formatted: version.version_id, + version_formatted: version.version_display, title: package.title, provider: package.provider, package_type: package.package_type, @@ -662,15 +660,16 @@ pub struct ManagedVersion { pub name: String, pub featured: bool, - pub version_id: String, + pub version_display: String, pub changelog: String, pub changelog_url: Option, pub published: DateTime, pub downloads: u32, - pub version_type: String, + pub version_type: ManagedVersionReleaseType, pub files: Vec, + pub is_available: bool, pub deps: Vec, pub game_versions: Vec, pub loaders: Vec, @@ -683,6 +682,39 @@ impl ManagedVersion { } } +/// Universal version release type +#[cfg_attr(feature = "specta", derive(specta::Type))] +#[derive(Default, Serialize, Deserialize, Clone, Debug)] +pub enum ManagedVersionReleaseType { + #[default] + Release, + Alpha, + Beta, + Snapshot, +} + +impl From for ManagedVersionReleaseType { + fn from(s: String) -> Self { + match s.to_lowercase().as_str() { + "alpha" => Self::Alpha, + "beta" => Self::Beta, + "snapshot" => Self::Snapshot, + _ => Self::Release, + } + } +} + +impl ToString for ManagedVersionReleaseType { + fn to_string(&self) -> String { + match self { + Self::Release => "Release", + Self::Alpha => "Alpha", + Self::Beta => "Beta", + Self::Snapshot => "Snapshot", + }.to_string() + } +} + /// Universal interface for managed package files. #[cfg_attr(feature = "specta", derive(specta::Type))] #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] @@ -691,7 +723,7 @@ pub struct ManagedVersionFile { pub file_name: String, pub primary: bool, - pub size: u32, + pub size: u64, pub file_type: Option, pub hashes: HashMap, } diff --git a/packages/core/src/store/processor.rs b/packages/core/src/store/processor.rs index fac6d534..9a527249 100644 --- a/packages/core/src/store/processor.rs +++ b/packages/core/src/store/processor.rs @@ -297,13 +297,7 @@ impl Processor { if let Some(process) = self.get(key) { let process = process.clone(); let process = process.write().await; - if process - .current_child - .write() - .await - .try_wait()? - .is_none() - { + if process.current_child.write().await.try_wait()?.is_none() { keys.push(key); } } @@ -334,13 +328,7 @@ impl Processor { if let Some(process) = self.get(key) { let process = process.clone(); let process = process.write().await; - if process - .current_child - .write() - .await - .try_wait()? - .is_none() - { + if process.current_child.write().await.try_wait()?.is_none() { clusters.push(process.cluster_path.clone()); } } @@ -355,13 +343,7 @@ impl Processor { if let Some(process) = self.get(key) { let process = process.clone(); let process = process.write().await; - if process - .current_child - .write() - .await - .try_wait()? - .is_none() - { + if process.current_child.write().await.try_wait()?.is_none() { if let Some(cluster) = cluster::get(&process.cluster_path.clone()).await? { clusters.push(cluster); } diff --git a/packages/core/src/utils/mod.rs b/packages/core/src/utils/mod.rs index 8b95dcf1..67eafca0 100644 --- a/packages/core/src/utils/mod.rs +++ b/packages/core/src/utils/mod.rs @@ -7,8 +7,8 @@ pub mod http; pub mod java; -pub mod watcher; pub mod pagination; +pub mod watcher; #[cfg(feature = "tauri")] pub mod window; diff --git a/packages/core/src/utils/pagination.rs b/packages/core/src/utils/pagination.rs index d5cd4f85..da740228 100644 --- a/packages/core/src/utils/pagination.rs +++ b/packages/core/src/utils/pagination.rs @@ -1,5 +1,18 @@ +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "specta", derive(specta::Type))] +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[allow(dead_code)] +pub struct Pagination { + pub index: u32, + pub page_size: u16, + pub result_count: u32, + pub total_count: u32, +} + pub fn get_page_offset(page: Option, page_size: Option) -> (u32, u16) { - let page = page.unwrap_or(1); + let page = page.unwrap_or(0); let page_size = page_size.unwrap_or(10); let offset = (page - 1) * page_size as u32; (offset, page_size) @@ -7,4 +20,4 @@ pub fn get_page_offset(page: Option, page_size: Option) -> (u32, u16) pub fn get_page_count(total: u32, page_size: u16) -> u32 { (total as f64 / page_size as f64).ceil() as u32 -} \ No newline at end of file +}