feat: Add fetching package body

This commit is contained in:
LynithDev 2024-09-22 19:45:36 +02:00
parent c991319463
commit e458108923
12 changed files with 184 additions and 88 deletions

View file

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

View file

@ -4,7 +4,7 @@ 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 uuid::Uuid;
#[specta::specta]
@ -51,7 +51,7 @@ pub async fn get_all_provider_package_versions(
loaders: Option<Vec<Loader>>,
) -> Result<Vec<ManagedVersion>, String> {
Ok(provider
.get_all_versions(&project_id, game_versions, loaders)
.get_all_versions(&project_id, game_versions, loaders, None, None) // TODO
.await?)
}
@ -61,7 +61,7 @@ pub async fn get_provider_package_versions(
provider: Providers,
versions: Vec<String>,
) -> Result<Vec<ManagedVersion>, String> {
Ok(provider.get_versions(versions).await?)
Ok(provider.get_versions("", versions).await?) // TODO
}
#[specta::specta]
@ -70,7 +70,7 @@ pub async fn get_provider_package_version(
provider: Providers,
version: String,
) -> Result<ManagedVersion, String> {
Ok(provider.get_version(&version).await?)
Ok(provider.get_version("", &version).await?) // TODO
}
#[derive(specta::Type, serde::Deserialize, serde::Serialize)]
@ -114,6 +114,15 @@ 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<String, String> {
Ok(provider.get_package_body(&body).await?)
}
#[specta::specta]
#[tauri::command]
pub async fn download_provider_package(

View file

@ -165,10 +165,12 @@ function PackageItem(props: SearchResult & { provider: Providers; row?: boolean
{abbreviateNumber(props.downloads)}
</div>
<div class="flex flex-row items-center gap-2">
<HeartIcon class="h-4 w-4" />
{abbreviateNumber(props.follows)}
</div>
<Show when={props.follows > 0}>
<div class="flex flex-row items-center gap-2">
<HeartIcon class="h-4 w-4" />
{abbreviateNumber(props.follows)}
</div>
</Show>
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
import type { Cluster, ManagedPackage, ManagedUser, ManagedVersion, Providers } from '@onelauncher/client/bindings';
import type { Cluster, ManagedPackage, ManagedVersion, Providers } from '@onelauncher/client/bindings';
import { A, type Params, Route, useSearchParams } from '@solidjs/router';
import { CalendarIcon, ChevronDownIcon, ClockRewindIcon, Download01Icon, File02Icon, HeartIcon, LinkExternal01Icon } from '@untitled-theme/icons-solid';
import SteveHead from '~assets/images/steve.png';
@ -15,7 +15,7 @@ import useCommand from '~ui/hooks/useCommand';
import usePagination from '~ui/hooks/usePagination';
import usePromptOpener from '~ui/hooks/usePromptOpener';
import { abbreviateNumber, formatAsRelative } from '~utils';
import { createContext, createEffect, createSignal, For, Match, on, type ParentProps, Show, Switch, useContext } from 'solid-js';
import { type Accessor, createContext, createEffect, createSignal, For, Match, on, type ParentProps, type Resource, Show, Switch, useContext } from 'solid-js';
import { getLicenseUrl, getPackageUrl, upperFirst } from '../../../utils';
interface BrowserModParams extends Params {
@ -23,14 +23,25 @@ interface BrowserModParams extends Params {
provider: Providers;
}
const BrowserPackageContext = createContext<ManagedPackage | null>(null);
interface BrowserPackageContextType {
pkg: Accessor<ManagedPackage>;
body: Resource<string>;
};
const BrowserPackageContext = createContext<BrowserPackageContextType | null>(null);
const basePath = '/browser/package';
function BrowserPackageProvider(props: ParentProps & { pkg: ManagedPackage }) {
const [body] = useCommand(() => bridge.commands.getPackageBody(props.pkg.provider, props.pkg.body));
const ctx: BrowserPackageContextType = {
pkg: () => props.pkg,
body,
};
return (
// eslint-disable-next-line solid/reactivity -- Should be fine
<BrowserPackageContext.Provider value={props.pkg}>
<BrowserPackageContext.Provider value={ctx}>
{props.children}
</BrowserPackageContext.Provider>
);
@ -67,7 +78,6 @@ function NavLink(props: ParentProps & { href: string }) {
function BrowserPackage(props: ParentProps) {
const [params] = useSearchParams<BrowserModParams>();
const [pkg] = useCommand(() => bridge.commands.getProviderPackage(params.provider!, params.id!));
const [authors] = useCommand(pkg, () => bridge.commands.getProviderAuthors(pkg()!.provider, pkg()!.author));
const links = [
['About', '/'],
@ -81,7 +91,7 @@ function BrowserPackage(props: ParentProps) {
<Show
children={(
<>
<BrowserSidebar authors={authors()!} package={pkg()!} />
<BrowserSidebar package={pkg()!} />
<div class="min-h-full flex flex-1 flex-col items-start gap-y-4 pb-8">
<div class="flex flex-none flex-row gap-x-1 rounded-lg bg-component-bg p-1">
@ -108,7 +118,7 @@ function BrowserPackage(props: ParentProps) {
</div>
</>
)}
when={pkg() !== undefined && authors() !== undefined}
when={pkg() !== undefined}
/>
</div>
</Spinner.Suspense>
@ -123,7 +133,8 @@ BrowserPackage.Routes = BrowserPackageRoutes;
export default BrowserPackage;
function BrowserSidebar(props: { package: ManagedPackage; authors: ManagedUser[] }) {
function BrowserSidebar(props: { package: ManagedPackage }) {
const [authors] = useCommand(() => props.package, () => bridge.commands.getProviderAuthors(props.package.provider, props.package.author));
const createdAt = () => new Date(props.package.created);
const updatedAt = () => new Date(props.package.updated);
const promptOpen = usePromptOpener();
@ -158,10 +169,12 @@ function BrowserSidebar(props: { package: ManagedPackage; authors: ManagedUser[]
{abbreviateNumber(props.package.downloads)}
</div>
<div class="flex flex-row items-center gap-2">
<HeartIcon class="h-4 w-4" />
{abbreviateNumber(props.package.followers)}
</div>
<Show when={props.package.followers > 0}>
<div class="flex flex-row items-center gap-2">
<HeartIcon class="h-4 w-4" />
{abbreviateNumber(props.package.followers)}
</div>
</Show>
</div>
</div>
</div>
@ -170,7 +183,7 @@ function BrowserSidebar(props: { package: ManagedPackage; authors: ManagedUser[]
<div class="flex flex-col gap-2 rounded-lg bg-component-bg p-3">
<h4 class="text-fg-primary font-bold">Links</h4>
<Link href={getPackageUrl(props.package.provider, props.package.id, props.package.package_type)} includeIcon>
<Link href={getPackageUrl(props.package)} includeIcon>
{props.package.provider}
{' '}
Page
@ -179,14 +192,14 @@ function BrowserSidebar(props: { package: ManagedPackage; authors: ManagedUser[]
<div class="flex flex-col gap-2 rounded-lg bg-component-bg p-3">
<h4 class="text-fg-primary font-bold">Authors</h4>
<For each={props.authors}>
<For each={authors()}>
{author => (
<>
<div
class="flex flex-row items-center gap-x-1 rounded-md p-1 active:bg-component-bg-pressed hover:bg-component-bg-hover"
onClick={() => promptOpen(author.url)}
>
<img alt={`${author.username}'s avatar`} class="h-8 min-h-8 min-w-8 w-8 rounded-md" src={author.avatar_url || SteveHead} />
<img alt={`${author.username}'s avatar`} class="h-8 min-h-8 min-w-8 w-8 rounded-[5px]" src={author.avatar_url || SteveHead} />
<div class="flex flex-1 flex-col justify-center gap-y-1">
<span>{author.username}</span>
@ -200,7 +213,7 @@ function BrowserSidebar(props: { package: ManagedPackage; authors: ManagedUser[]
</div>
<LinkExternal01Icon class="h-4 w-4" />
</div>
<Show when={author.is_organization_user === true && props.authors.length > 1}>
<Show when={author.is_organization_user === true && ((authors()?.length || 0) > 1)}>
<div class="h-px w-full bg-gray-05" />
</Show>
</>
@ -346,7 +359,7 @@ function BrowserPackageBody() {
return (
<div class="w-full flex-1 rounded-lg bg-component-bg p-4 px-6">
<Markdown body={context?.body || ''} />
<Markdown body={context?.body?.() || ''} />
</div>
);
}
@ -371,7 +384,7 @@ function BrowserPackageVersions() {
const MAX_ITEMS_PER_PAGE = 20;
const { page, Navigation } = usePagination({
itemsCount: () => context?.versions.length || 0,
itemsCount: () => context?.pkg().versions.length || 0,
itemsPerPage: () => MAX_ITEMS_PER_PAGE,
});
@ -379,7 +392,7 @@ function BrowserPackageVersions() {
if (context === null)
return [];
const newestToOldest = context.versions.toReversed();
const newestToOldest = context.pkg().versions.toReversed();
const start = (page - 1) * MAX_ITEMS_PER_PAGE;
const end = start + MAX_ITEMS_PER_PAGE;
@ -392,7 +405,7 @@ function BrowserPackageVersions() {
const list = getVersionsForPage(page());
return await bridge.commands.getProviderPackageVersions(context.provider, list);
return await bridge.commands.getProviderPackageVersions(context.pkg().provider, list);
});
let container!: HTMLDivElement;

View file

@ -1,6 +1,6 @@
import type { Cluster, ImportType, License, Loader, ManagedPackage, PackageType, Providers, VersionType } from '@onelauncher/client/bindings';
import { DurationFormat } from '@formatjs/intl-durationformat';
import { open } from '@tauri-apps/plugin-shell';
import type { Cluster, ImportType, License, Loader, PackageType, Providers, VersionType } from '@onelauncher/client/bindings';
export function setAsyncTimeout(ms: number): Promise<void>;
export function setAsyncTimeout(callback: () => void, ms: number): Promise<void>;
@ -160,13 +160,23 @@ export function getLicenseUrl(licenseId: string | License | null | undefined): s
return `https://spdx.org/licenses/${id}.html`;
}
export function getPackageUrl(provider: Providers, id: string, package_type: PackageType = 'mod'): string {
const mapping: Record<Providers, string> = {
Modrinth: `https://modrinth.com/${package_type}/${id}`,
Curseforge: `https://www.curseforge.com/minecraft/${package_type}/${id}`,
export function getPackageUrl(pkg: ManagedPackage): string {
const mapping: Record<Providers, () => string> = {
Modrinth: () => `https://modrinth.com/${pkg.package_type}/${pkg.id}`,
Curseforge: () => {
const packageTypeMapping: Record<PackageType, string> = {
mod: 'mc-mods',
shaderpack: 'shaders',
resourcepack: 'texture-packs',
datapack: 'data-packs',
modpack: 'modpacks',
};
return `https://www.curseforge.com/minecraft/${packageTypeMapping[pkg.package_type]}/${pkg.main}`;
},
};
return mapping[provider];
return mapping[pkg.provider]();
}
export const LOADERS: Loader[] = ['vanilla', 'fabric', 'forge', 'neoforge', 'quilt'] as const;

View file

@ -28,6 +28,8 @@ pub async fn find_managed_version(
&package.id,
game_version.as_ref().map(|v| vec![v.to_owned()]).clone(),
loader.map(|l| vec![l]).clone(),
None,
None,
)
.await?;

View file

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use url::Url;
use crate::{data::{Loader, ManagedPackage, PackageType}, store::{ProviderSearchResults, SearchResult}, utils::http, Result, State};
use crate::{data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType}, store::{ProviderSearchResults, SearchResult}, utils::http, Result, State};
/// Mapping of Curseforge "classes" to their respective category ids
#[derive(Debug, Default, Serialize_repr, Deserialize_repr)]
@ -121,23 +121,33 @@ impl From<CurseforgePackage> for ManagedPackage {
provider: super::Providers::Curseforge,
package_type: package.package_type.into(),
description: package.summary,
body: "".to_string(), // TODO: Description is a new HTTP request
main: todo!(),
versions: todo!(),
game_versions: todo!(),
loaders: todo!(),
icon_url: todo!(),
created: todo!(),
updated: todo!(),
client: todo!(),
server: todo!(),
downloads: todo!(),
followers: todo!(),
categories: todo!(),
optional_categories: todo!(),
license: todo!(),
author: todo!(),
is_archived: todo!(),
body: crate::store::PackageBody::Url(format!("/v1/mods/{}/description", package.id)),
main: package.slug,
versions: vec![],
game_versions: vec![],
loaders: vec![],
icon_url: Some(package.logo.url),
created: package.date_created,
updated: package.date_modified,
client: crate::store::PackageSide::Unknown,
server: crate::store::PackageSide::Unknown,
downloads: package.download_count,
followers: package.rating.unwrap_or(0.0) as u32,
categories: vec![], // TODO
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()
),
is_archived: false,
}
}
}
@ -220,7 +230,7 @@ pub struct Screenshot {
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LatestFile {
pub struct ModFile {
pub id: u32,
pub game_id: u32,
pub mod_id: u32,
@ -282,19 +292,7 @@ pub struct Module {
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LatestFilesIndex {
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,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LatestEarlyAccessFilesIndex {
pub struct ModFilesIndex {
pub game_version: String,
pub file_id: u32,
pub filename: String,
@ -423,4 +421,20 @@ pub async fn get(id: u32) -> Result<CurseforgePackage> {
&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)
}
// 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

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType};
use crate::package::content::modrinth::FacetBuilder;
use crate::store::{Author, ProviderSearchResults};
use crate::store::{Author, PackageBody, ProviderSearchResults};
use crate::Result;
mod modrinth;
@ -107,18 +107,29 @@ impl Providers {
project_id: &str,
game_versions: Option<Vec<String>>,
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 => todo!(),
}
.await?
.into_iter()
.map(Into::into)
.collect())
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!(),
}
.await?
.into_iter()
.map(Into::into)
.collect()
)
}
pub async fn get_versions(&self, versions: Vec<String>) -> Result<Vec<ManagedVersion>> {
pub async fn get_versions(&self, pkg_id: &str, versions: Vec<String>) -> Result<Vec<ManagedVersion>> {
Ok(match self {
Self::Modrinth => modrinth::get_versions(versions),
Self::Curseforge => todo!(),
@ -129,7 +140,7 @@ impl Providers {
.collect())
}
pub async fn get_version(&self, version_id: &str) -> Result<ManagedVersion> {
pub async fn get_version(&self, pkg_id: &str, version_id: &str) -> Result<ManagedVersion> {
Ok(match self {
Self::Modrinth => modrinth::get_version(version_id),
Self::Curseforge => todo!(),
@ -139,11 +150,27 @@ impl Providers {
}
pub async fn get_authors(&self, author: &Author) -> Result<Vec<ManagedUser>> {
match self {
Self::Modrinth => modrinth::get_authors(author),
Self::Curseforge => todo!(),
}
.await
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::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::Markdown(markdown) => markdown.to_owned()
})
}
}

View file

@ -68,7 +68,7 @@ impl From<ModrinthPackage> for ManagedPackage {
id: value.id,
title: value.title,
description: value.description,
body: value.body,
body: crate::store::PackageBody::Markdown(value.body),
main: value.slug,
versions: value.versions,
game_versions: value.game_versions,
@ -78,7 +78,7 @@ impl From<ModrinthPackage> for ManagedPackage {
updated: value.updated,
client: value.client_side,
server: value.server_side,
downloads: value.downloads,
downloads: value.downloads as u64,
followers: value.followers,
categories: value.categories,
optional_categories: Some(value.additional_categories),

View file

@ -603,6 +603,13 @@ pub enum Author {
Users(Vec<ManagedUser>),
}
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum PackageBody {
Url(String),
Markdown(String),
}
/// Universal metadata for any managed package from a Mod distribution platform.
#[cfg_attr(feature = "specta", derive(specta::Type))]
#[derive(Serialize, Deserialize, Clone, Debug)]
@ -613,7 +620,7 @@ pub struct ManagedPackage {
pub package_type: PackageType,
pub title: String,
pub description: String,
pub body: String,
pub body: PackageBody,
pub main: String,
pub versions: Vec<String>,
pub game_versions: Vec<String>,
@ -624,7 +631,7 @@ pub struct ManagedPackage {
pub updated: DateTime<Utc>,
pub client: PackageSide,
pub server: PackageSide,
pub downloads: u32,
pub downloads: u64,
pub followers: u32,
pub categories: Vec<String>,
pub optional_categories: Option<Vec<String>>,

View file

@ -8,6 +8,7 @@
pub mod http;
pub mod java;
pub mod watcher;
pub mod pagination;
#[cfg(feature = "tauri")]
pub mod window;

View file

@ -0,0 +1,10 @@
pub fn get_page_offset(page: Option<u32>, page_size: Option<u16>) -> (u32, u16) {
let page = page.unwrap_or(1);
let page_size = page_size.unwrap_or(10);
let offset = (page - 1) * page_size as u32;
(offset, page_size)
}
pub fn get_page_count(total: u32, page_size: u16) -> u32 {
(total as f64 / page_size as f64).ceil() as u32
}