feat: Implement more functionality for Curseforge

This commit is contained in:
LynithDev 2024-10-03 20:48:01 +02:00
parent e458108923
commit 518c7ec8b9
16 changed files with 579 additions and 288 deletions

View file

@ -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<Vec<String>>,
loaders: Option<Vec<Loader>>,
) -> Result<Vec<ManagedVersion>, String> {
page: Option<u32>,
page_size: Option<u16>,
) -> Result<(Vec<ManagedVersion>, 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<String>,
) -> Result<Vec<ManagedVersion>, 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<ManagedVersion, String> {
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<String, String> {
pub async fn get_package_body(provider: Providers, body: PackageBody) -> Result<String, String> {
Ok(provider.get_package_body(&body).await?)
}

View file

@ -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';

View file

@ -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<Providers>;

View file

@ -108,6 +108,7 @@ function usePagination(initialOptions: PaginationOptions) {
totalPages,
offset,
reset,
options,
Navigation,
};
}

View file

@ -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 }) {
</div>
<div class="flex flex-1 flex-col gap-2 p-3">
<div class="flex flex-col gap-2">
<h4 class="text-fg-primary font-medium">{props.package.title}</h4>
<h4 class="text-fg-primary font-medium line-height-snug">{props.package.title}</h4>
<p class="text-xs text-fg-secondary">
<span class="text-fg-primary capitalize">{props.package.package_type}</span>
{' '}
@ -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) {
<td>
<div class="flex flex-col gap-2">
<h3 class="text-lg">{props.version_id}</h3>
<p class="text-wrap text-sm">{props.name}</p>
<h3 class="text-lg">{props.version_display}</h3>
<p class="text-wrap text-sm">{props.name === props.version_display ? props.files[0]?.file_name : props.name}</p>
</div>
</td>

View file

@ -33,6 +33,14 @@ export default defineConfig(async ({ mode }) => {
emptyOutDir: false,
},
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler',
},
},
},
clearScreen: false,
server: {
port: 8001,

View file

@ -31,4 +31,4 @@ async fn search(provider: Providers) -> Result<()> {
println!("{:#?}", results);
Ok(())
}
}

View file

@ -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}");
}
}
for (key, value) in vars {
println!("cargo:rustc-env={key}={value}");
}
}

View file

@ -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
})

View file

@ -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<String> 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<Loader> for CurseforgeLoader {
fn from(loader: Loader) -> Self {
match loader {
@ -69,48 +84,50 @@ impl From<Loader> 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<Loader> 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<Category>,
pub authors: Vec<Author>,
pub logo: Logo,
// pub screenshots: Vec<Screenshot>,
// pub main_file_id: u32,
// pub latest_files: Vec<LatestFile>,
// pub latest_files_indexes: Vec<LatestFilesIndex>,
// pub latest_early_access_files_indexes: Vec<LatestEarlyAccessFilesIndex>,
pub date_created: DateTime<Utc>,
pub date_modified: DateTime<Utc>,
pub date_released: DateTime<Utc>,
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<Category>,
pub authors: Vec<Author>,
pub logo: Logo,
// pub screenshots: Vec<Screenshot>,
// pub main_file_id: u32,
// pub latest_files: Vec<LatestFile>,
// pub latest_files_indexes: Vec<LatestFilesIndex>,
// pub latest_early_access_files_indexes: Vec<LatestEarlyAccessFilesIndex>,
pub date_created: DateTime<Utc>,
pub date_modified: DateTime<Utc>,
pub date_released: DateTime<Utc>,
#[serde(default)]
pub allow_mod_distribution: bool,
// pub game_popularity_rank: u32,
// pub is_available: bool,
pub thumbs_up_count: u32,
pub rating: Option<f32>,
pub allow_mod_distribution: bool,
// pub game_popularity_rank: u32,
// pub is_available: bool,
pub thumbs_up_count: u32,
pub rating: Option<f32>,
}
impl From<CurseforgePackage> for ManagedPackage {
@ -137,15 +154,19 @@ impl From<CurseforgePackage> 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<SearchResult> 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<SearchResult> for CurseforgePackage {
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Links {
pub website_url: Option<String>,
pub wiki_url: Option<String>,
pub issues_url: Option<String>,
pub source_url: Option<String>,
pub website_url: Option<String>,
pub wiki_url: Option<String>,
pub issues_url: Option<String>,
pub source_url: Option<String>,
}
#[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<ManagedVersionReleaseType> 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<Hash>,
// pub file_date: String,
// pub file_length: u32,
pub download_count: u32,
// pub file_size_on_disk: u32,
pub download_url: Option<String>,
pub game_versions: Vec<String>,
pub sortable_game_versions: Vec<SortableGameVersion>,
pub dependencies: Vec<Dependency>,
// 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<Module>,
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<Hash>,
pub file_date: DateTime<Utc>,
pub file_length: u64,
pub download_count: u32,
pub file_size_on_disk: Option<u64>,
pub download_url: Option<String>,
pub game_versions: Vec<String>,
pub sortable_game_versions: Vec<SortableGameVersion>,
pub dependencies: Vec<Dependency>,
// 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<Module>,
}
#[derive(Default, Debug, Serialize, Deserialize)]
impl Into<ManagedVersion> 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::<Loader>::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<T> {
data: T,
}
pub async fn fetch_url_builder<F: FnOnce(&mut Url)>(url: &str, url_builder: Option<F>) -> Result<Bytes> {
// 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<T> {
pagination: Pagination,
data: T,
}
let mut headers = HashMap::<&str, &str>::new();
pub async fn fetch_advanced<F: FnOnce(&mut Url)>(
method: Method,
url: &str,
url_builder: Option<F>,
headers: Option<HashMap<&str, &str>>,
json_body: Option<serde_json::Value>,
) -> Result<Bytes> {
// 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<F: FnOnce(&mut Url)>(url: &str, url_builder: Opti
.await
}
pub async fn fetch_url_builder<F: FnOnce(&mut Url)>(
url: &str,
url_builder: Option<F>,
) -> Result<Bytes> {
fetch_advanced(Method::GET, url, url_builder, None, None).await
}
async fn fetch(url: &str) -> Result<Bytes> {
fetch_url_builder(url, None::<fn(&mut Url)>).await
}
@ -362,11 +493,7 @@ pub async fn search(
loaders: Option<Vec<Loader>>,
package_types: Option<Vec<PackageType>>,
) -> Result<ProviderSearchResults> {
#[derive(Deserialize)]
struct CFSearchResults {
pagination: Pagination,
data: Vec<CurseforgePackage>
}
type CFSearchResults = CFPaginatedData<Vec<CurseforgePackage>>;
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::<Vec<String>>().join(",")).as_str());
params.append_pair(
"gameVersions",
format!(
"[{}]",
game_versions
.into_iter()
.map(|v| format!("\"{}\"", v))
.collect::<Vec<String>>()
.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::<Vec<String>>().join(",")).as_str());
params.append_pair(
"modLoaderTypes",
format!(
"[{}]",
loaders
.into_iter()
.map(|l| (CurseforgeLoader::from(l) as u32).to_string())
.collect::<Vec<String>>()
.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::<Vec<SearchResult>>(),
results: results
.data
.into_iter()
.map(Into::into)
.collect::<Vec<SearchResult>>(),
provider: crate::package::content::Providers::Curseforge,
})
}
pub async fn get(id: u32) -> Result<CurseforgePackage> {
Ok(serde_json::from_slice::<CFData<_>>(
&fetch(format!("/v1/mods/{}", id).as_str())
.await?,
)?.data)
Ok(
serde_json::from_slice::<CFData<_>>(&fetch(format!("/v1/mods/{}", id).as_str()).await?)?
.data,
)
}
pub async fn get_package_body(url: String) -> Result<String> {
Ok(serde_json::from_slice::<CFData<_>>(
&fetch(&url).await?,
)?.data)
Ok(serde_json::from_slice::<CFData<_>>(&fetch(&url).await?)?.data)
}
pub async fn get_all_versions(
project_id: &str,
game_versions: Option<Vec<String>>,
loaders: Option<Vec<Loader>>,
page: Option<u32>,
page_size: Option<u16>,
) -> Result<(Vec<ModFile>, Pagination)> {
let data: CFPaginatedData<Vec<ModFile>> = serde_json::from_slice::<CFPaginatedData<_>>(
&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<String>,
) -> Result<Vec<ManagedVersion>> {
let mut map = serde_json::Map::new();
map.insert("fileIds".to_string(), versions.iter().map(|v| v.parse::<u32>().unwrap()).collect::<Vec<u32>>().into());
let json_body = serde_json::Value::Object(map);
Ok(serde_json::from_slice::<CFData<_>>(
&fetch_advanced(
Method::POST,
format!("/v1/mods/files").as_str(),
None::<fn(&mut Url)>,
None,
Some(json_body)
)
.await?,
)?
.data)
}
// pub async fn get_all_versions(
// project_id: &str,
// game_versions: Option<Vec<String>>,
// loaders: Option<Vec<Loader>>,
// page: Option<u32>,
// page_size: Option<u16>,
// ) -> Result<Vec<ManagedVersion>> {
// }

View file

@ -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<bool>,
) -> Result<ProviderSearchResults> {
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<ManagedPackage> {
Ok(match self {
Self::Modrinth => modrinth::get(slug_or_id).await?.into(),
Self::Curseforge => curseforge::get(slug_or_id.parse::<u32>().map_err(|err| anyhow::anyhow!(err))?).await?.into(),
Self::Curseforge => curseforge::get(
slug_or_id
.parse::<u32>()
.map_err(|err| anyhow::anyhow!(err))?,
)
.await?
.into(),
})
}
@ -109,35 +124,33 @@ impl Providers {
loaders: Option<Vec<Loader>>,
page: Option<u32>,
page_size: Option<u16>,
) -> Result<Vec<ManagedVersion>> {
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<ManagedVersion>, 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<String>) -> Result<Vec<ManagedVersion>> {
pub async fn get_versions(&self, versions: Vec<String>) -> Result<Vec<ManagedVersion>> {
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<ManagedVersion> {
@ -151,25 +164,21 @@ impl Providers {
pub async fn get_authors(&self, author: &Author) -> Result<Vec<ManagedUser>> {
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<String> {
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(),
})
}
}

View file

@ -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<PackageFile>,
}
@ -171,15 +171,16 @@ impl From<ModrinthVersion> 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<Vec<String>>,
loaders: Option<Vec<Loader>>,
) -> Result<Vec<ModrinthVersion>> {
page: Option<u32>,
page_size: Option<u16>,
) -> Result<(Vec<ModrinthVersion>, 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::<Vec<ModrinthVersion>>(
&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<String>) -> Result<Vec<ModrinthVersion>> {

View file

@ -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<String>,
pub published: DateTime<Utc>,
pub downloads: u32,
pub version_type: String,
pub version_type: ManagedVersionReleaseType,
pub files: Vec<ManagedVersionFile>,
pub is_available: bool,
pub deps: Vec<ManagedDependency>,
pub game_versions: Vec<String>,
pub loaders: Vec<Loader>,
@ -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<String> 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<PackageFile>,
pub hashes: HashMap<String, String>,
}

View file

@ -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);
}

View file

@ -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;

View file

@ -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<u32>, page_size: Option<u16>) -> (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<u32>, page_size: Option<u16>) -> (u32, u16)
pub fn get_page_count(total: u32, page_size: u16) -> u32 {
(total as f64 / page_size as f64).ceil() as u32
}
}