feat: Start working on implementing Curseforge into the frontend + fetch_categories helper script

This commit is contained in:
LynithDev 2024-09-20 22:07:02 +02:00
parent f9788ae49b
commit 0fb2017924
14 changed files with 309 additions and 107 deletions

3
.template.env Normal file
View file

@ -0,0 +1,3 @@
#! Use single quotes due to special characters and dotenvy being weird
CURSEFORGE_API_KEY = ''

9
Cargo.lock generated
View file

@ -1672,6 +1672,11 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "git+https://github.com/allan2/dotenvy/?rev=db0d6aa8fc8d1b0ddbfe94b5c833372e463eb1c5#db0d6aa8fc8d1b0ddbfe94b5c833372e463eb1c5"
[[package]]
name = "dpi"
version = "0.1.1"
@ -4357,7 +4362,7 @@ dependencies = [
"chrono",
"dirs 5.0.1",
"discord-rich-presence",
"dotenvy",
"dotenvy 0.15.7 (git+https://github.com/allan2/dotenvy/?rev=db0d6aa8fc8d1b0ddbfe94b5c833372e463eb1c5)",
"flate2",
"futures",
"indicatif",
@ -5153,7 +5158,7 @@ dependencies = [
"bigdecimal 0.4.5",
"chrono",
"diagnostics",
"dotenvy",
"dotenvy 0.15.7 (registry+https://github.com/rust-lang/crates.io-index)",
"futures",
"include_dir",
"indexmap 1.9.3",

View file

@ -179,7 +179,7 @@ cocoa = { version = "0.26" }
objc = { version = "0.2" }
cc = { version = "1.1" }
libc = { version = "0.2" }
dotenvy = { version = "0.15.7" }
dotenvy = { git = "https://github.com/allan2/dotenvy/", rev = "db0d6aa8fc8d1b0ddbfe94b5c833372e463eb1c5" }
# https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/Cargo.toml#L102
webkit2gtk = { version = "=2.0.1", features = [ "v2_40" ] }

View file

@ -1,5 +1,3 @@
import type { Cluster, ManagedPackage, PackageType, Providers, ProviderSearchQuery, ProviderSearchResults } from '@onelauncher/client/bindings';
import type { ModalProps } from '~ui/components/overlay/Modal';
import { useNavigate } from '@solidjs/router';
import { bridge } from '~imports';
import Button from '~ui/components/base/Button';
@ -7,6 +5,8 @@ import Dropdown from '~ui/components/base/Dropdown';
import Modal, { createModal } from '~ui/components/overlay/Modal';
import BrowserPackage from '~ui/pages/browser/BrowserPackage';
import { type Accessor, type Context, createContext, createEffect, createSignal, For, on, onMount, type ParentProps, type Setter, Show, untrack, useContext } from 'solid-js';
import type { Cluster, ManagedPackage, PackageType, Providers, ProviderSearchQuery, ProviderSearchResults } from '@onelauncher/client/bindings';
import type { ModalProps } from '~ui/components/overlay/Modal';
import { useRecentCluster } from './useCluster';
import useCommand, { tryResult } from './useCommand';

View file

@ -93,7 +93,7 @@ function BrowserCategories() {
<div class="flex flex-col gap-y-6">
<div class="flex flex-col gap-y-2">
<h6 class="my-1">Categories</h6>
<For each={browserCategories.byPackageType(browser.packageType())}>
<For each={browserCategories.byPackageType(browser.packageType(), browser.searchQuery().provider)}>
{category => (
<p
class={`text-md capitalize text-fg-primary hover:text-fg-primary-hover ${isEnabled(category.id) ? 'text-opacity-100! hover:text-opacity-90!' : 'text-opacity-60! hover:text-opacity-70!'}`}

View file

@ -9,7 +9,7 @@ import { BrowserContent } from './BrowserRoot';
function BrowserSearch() {
const browser = useBrowser();
const [results, { refetch }] = useCommand(() => bridge.commands.searchProviderPackages('Modrinth', browser.searchQuery()));
const [results, { refetch }] = useCommand(() => bridge.commands.searchProviderPackages(browser.searchQuery().provider, browser.searchQuery()));
const paginationOptions = (): PaginationOptions => ({
itemsCount: () => results()?.total || 0,

View file

@ -7,74 +7,11 @@ export interface CategoryItem {
id: string;
}
type ModCategories = [
'adventure',
'library',
'equipment',
'decoration',
'food',
'magic',
'performance',
'storage',
'technology',
'utility',
'worldgen',
];
const modMapping: Record<Providers, { [key in ModCategories[number]]: CategoryItem }> = {
Modrinth: {
adventure: {
display: 'Adventure',
id: 'adventure',
},
library: {
display: 'Library',
id: 'library',
},
decoration: {
display: 'Decoration',
id: 'decoration',
},
equipment: {
display: 'Equipment',
id: 'equipment',
},
food: {
display: 'Food',
id: 'food',
},
magic: {
display: 'Magic',
id: 'magic',
},
performance: {
display: 'Performance',
id: 'optimization',
},
storage: {
display: 'Storage',
id: 'storage',
},
technology: {
display: 'Technology',
id: 'technology',
},
utility: {
display: 'Utility',
id: 'utility',
},
worldgen: {
display: 'World Generation',
id: 'worldgen',
},
},
};
export const browserCategories = {
byPackageType(packageType: PackageType): CategoryItem[] {
byPackageType(packageType: PackageType, provider: Providers = 'Modrinth'): CategoryItem[] {
switch (packageType) {
case 'mod':
return this.mod();
return this.mod(provider);
}
return [];
@ -84,3 +21,75 @@ export const browserCategories = {
return Object.values(modMapping[provider]);
},
};
const modMapping: Record<Providers, CategoryItem[]> = {
Curseforge: [
{ display: 'Server Utility', id: '435' },
{ display: 'Armor, Tools, and Weapons', id: '434' },
{ display: 'API and Library', id: '421' },
{ display: 'Adventure and RPG', id: '422' },
{ display: 'World Gen', id: '406' },
{ display: 'Buildcraft', id: '432' },
{ display: 'Twitch Integration', id: '4671' },
{ display: 'Industrial Craft', id: '429' },
{ display: 'Magic', id: '419' },
{ display: 'Cosmetic', id: '424' },
{ display: 'KubeJS', id: '5314' },
{ display: 'Storage', id: '420' },
{ display: 'Mobs', id: '411' },
{ display: 'Ores and Resources', id: '408' },
{ display: 'Create', id: '6484' },
{ display: 'Genetics', id: '418' },
{ display: 'Food', id: '436' },
{ display: 'Automation', id: '4843' },
{ display: 'Performance', id: '6814' },
{ display: 'Farming', id: '416' },
{ display: 'CraftTweaker', id: '4773' },
{ display: 'Structures', id: '409' },
{ display: 'Map and Information', id: '423' },
{ display: 'Bug Fixes', id: '6821' },
{ display: 'Biomes', id: '407' },
{ display: 'Energy', id: '417' },
{ display: 'Addons', id: '426' },
{ display: 'Blood Magic', id: '4485' },
{ display: 'Energy, Fluid, and Item Transport', id: '415' },
{ display: 'Forestry', id: '433' },
{ display: 'Education', id: '5299' },
{ display: 'Technology', id: '412' },
{ display: 'Thermal Expansion', id: '427' },
{ display: 'Dimensions', id: '410' },
{ display: 'Thaumcraft', id: '430' },
{ display: 'Processing', id: '413' },
{ display: 'Tinker\'s Construct', id: '428' },
{ display: 'Miscellaneous', id: '425' },
{ display: 'Skyblock', id: '6145' },
{ display: 'Galacticraft', id: '5232' },
{ display: 'Player Transport', id: '414' },
{ display: 'Applied Energistics 2', id: '4545' },
{ display: 'Integrated Dynamics', id: '6954' },
{ display: 'Redstone', id: '4558' },
{ display: 'Utility & QoL', id: '5191' },
{ display: 'MCreator', id: '4906' },
],
Modrinth: [
{ display: 'Adventure', id: 'adventure' },
{ display: 'Cursed', id: 'cursed' },
{ display: 'Decoration', id: 'decoration' },
{ display: 'Economy', id: 'economy' },
{ display: 'Equipment', id: 'equipment' },
{ display: 'Food', id: 'food' },
{ display: 'Game Mechanics', id: 'game-mechanics' },
{ display: 'Library', id: 'library' },
{ display: 'Magic', id: 'magic' },
{ display: 'Management', id: 'management' },
{ display: 'Minigame', id: 'minigame' },
{ display: 'Mobs', id: 'mobs' },
{ display: 'Optimization', id: 'optimization' },
{ display: 'Social', id: 'social' },
{ display: 'Storage', id: 'storage' },
{ display: 'Technology', id: 'technology' },
{ display: 'Transportation', id: 'transportation' },
{ display: 'Utility', id: 'utility' },
{ display: 'World Generation', id: 'worldgen' },
],
};

View file

@ -1,6 +1,6 @@
import type { Cluster, ImportType, License, Loader, 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>;
@ -163,12 +163,13 @@ export function getLicenseUrl(licenseId: string | License | null | undefined): s
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}`,
};
return mapping[provider];
}
export const LOADERS: Loader[] = ['vanilla', 'fabric', 'forge', 'neoforge', 'quilt'] as const;
export const PROVIDERS: Providers[] = ['Modrinth'] as const;
export const PROVIDERS: Providers[] = ['Modrinth', 'Curseforge'] as const;
export const PACKAGE_TYPES: PackageType[] = ['mod', 'resourcepack', 'datapack', 'shaderpack'] as const;
export const LAUNCHER_IMPORT_TYPES: ImportType[] = ['PrismLauncher', 'Curseforge', 'Modrinth', 'ATLauncher', 'GDLauncher', 'FTBLauncher', 'MultiMC', 'TLauncher', 'Technic'] as const;

View file

@ -1,2 +0,0 @@
# You may need to escape any special characters such as $
CURSEFORGE_API_KEY = ""

View file

@ -1,9 +1,41 @@
fn main() {
let dotenv_path = dotenvy::dotenv().expect("failed to find .env file");
println!("cargo:rerun-if-changed={}", dotenv_path.display());
use std::path::PathBuf;
for env_var in dotenvy::dotenv_iter().unwrap() {
let (key, value) = env_var.unwrap();
fn main() {
let paths = vec![
get_root_workspace().join(".env"),
PathBuf::from(".env"),
];
for path in paths {
if path.exists() {
load_from_path(path);
}
}
}
fn get_root_workspace() -> PathBuf {
let output: Vec<u8> = std::process::Command::new(env!("CARGO"))
.arg("locate-project")
.arg("--workspace")
.arg("--message-format=plain")
.output()
.unwrap()
.stdout;
let root_workspace = String::from_utf8(output).unwrap();
let mut root_workspace = PathBuf::from(root_workspace.trim());
if root_workspace.is_file() {
root_workspace.pop();
}
root_workspace
}
fn load_from_path(path: PathBuf) {
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}");
}
}

View file

@ -71,6 +71,7 @@ impl From<Loader> for CurseforgeLoader {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct Pagination {
pub index: u32,
pub page_size: u32,
@ -89,25 +90,25 @@ pub struct CurseforgePackage {
pub slug: String,
pub links: Links,
pub summary: String,
pub status: u32,
// pub status: u32,
pub download_count: u64,
pub is_featured: bool,
pub primary_category_id: u32,
// 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 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 game_popularity_rank: u32,
// pub is_available: bool,
pub thumbs_up_count: u32,
pub rating: Option<f32>,
}
@ -183,7 +184,7 @@ pub struct Category {
pub url: String,
pub icon_url: String,
pub date_modified: String,
pub parent_category_id: u32,
// pub parent_category_id: u32,
// pub display_index: u32,
}
@ -226,11 +227,11 @@ pub struct LatestFile {
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 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>,
@ -319,7 +320,7 @@ struct CFData<T> {
data: T,
}
pub async fn fetch(url: &str, url_builder: Option<Box<dyn FnOnce(&mut Url)>>) -> Result<Bytes> {
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"))?;
@ -338,7 +339,7 @@ pub async fn fetch(url: &str, url_builder: Option<Box<dyn FnOnce(&mut Url)>>) ->
http::fetch_advanced(
Method::GET,
url.as_str(),
dbg!(url.as_str()),
None,
None,
Some(headers),
@ -348,6 +349,10 @@ pub async fn fetch(url: &str, url_builder: Option<Box<dyn FnOnce(&mut Url)>>) ->
.await
}
async fn fetch(url: &str) -> Result<Bytes> {
fetch_url_builder(url, None::<fn(&mut Url)>).await
}
pub async fn search(
query: Option<String>,
limit: Option<u8>,
@ -364,7 +369,7 @@ pub async fn search(
}
let results: CFSearchResults = serde_json::from_slice(
&fetch(
&fetch_url_builder(
"/v1/mods/search",
Some(Box::new(move |url: &mut Url| {
let params = &mut url.query_pairs_mut();
@ -378,16 +383,16 @@ 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", 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", 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());
}
// TODO: Merge this with categories
if let Some(package_types) = package_types {
params.append_pair("categoryIds", package_types.into_iter().map(|p| (CFPackageType::from(p) as u32).to_string()).collect::<Vec<_>>().join(",").as_str());
params.append_pair("categoryIds", format!("[{}]", package_types.into_iter().map(|p| (CFPackageType::from(p) as u32).to_string()).collect::<Vec<_>>().join(",")).as_str());
}
}))
).await?)?;
@ -401,10 +406,7 @@ pub async fn search(
pub async fn get(id: u32) -> Result<CurseforgePackage> {
Ok(serde_json::from_slice::<CFData<_>>(
&fetch(
format!("/v1/mods/{}", id).as_str(),
None,
)
&fetch(format!("/v1/mods/{}", id).as_str())
.await?,
)?.data)
}

View file

@ -0,0 +1,120 @@
import process from 'node:process';
import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
import dotenv from 'dotenv';
interface MappedCategoryItem {
display: string;
id: string;
}
type MappingList = Record<string, MappedCategoryItem[]>;
const packageTypes = ['mod', 'modpack', 'shader', 'resourcepack'] as const;
type PackageType = typeof packageTypes[number];
async function loadEnv() {
const path = await findWorkspaceDir(process.cwd());
console.log(`Loading .env from ${path}`);
dotenv.config({ path: `${path}/.env` });
}
async function fetchCategoriesCurseforge(packageType: PackageType): Promise<MappingList> {
const key = process.env.CURSEFORGE_API_KEY;
if (key === undefined) {
console.error('CURSEFORGE_API_KEY is not defined');
return {};
}
const response = await fetch('https://api.curseforge.com/v1/categories?gameId=432', {
headers: {
'x-api-key': key,
},
});
interface CFCategory {
id: number;
name: 'Server Utility';
slug: 'server-utility';
url: 'https://www.curseforge.com/minecraft/mc-mods/server-utility';
iconUrl: 'https://media.forgecdn.net/avatars/6/48/635351498950580836.png';
classId?: number;
isClass?: boolean;
parentCategoryId?: number;
};
const data = (await response.json() as { data: CFCategory[] }).data;
// TODO: Add more calssIds
const classIds: Record<PackageType, number> = {
mod: 6,
resourcepack: 12,
modpack: 4471,
shader: 6552,
};
const mapping: MappingList = {};
mapping.Curseforge = data
.filter((category) => {
return category.isClass !== true && category.classId === classIds[packageType];
})
.map((category) => {
return {
display: category.name,
id: category.id.toString(),
};
});
return mapping;
}
async function fetchCategoriesModrinth(packageType: PackageType): Promise<MappingList> {
const response = await fetch('https://api.modrinth.com/v2/tag/category');
interface ModrinthCategory {
icon: string;
name: string;
project_type: PackageType;
header: string;
}
const data = await response.json() as ModrinthCategory[];
const mapping: MappingList = {};
mapping.Modrinth = data
.filter((category) => {
return category.project_type === packageType;
})
.map((category) => {
return {
display: category.name.charAt(0).toUpperCase() + category.name.slice(1),
id: category.name,
};
});
return mapping;
}
(async () => {
const arg = process.argv[2];
if (arg === undefined) {
console.error(`Please provide an argument for package type.\n Valid: '${packageTypes.join('\', \'')}'`);
return;
}
const packageType = arg as PackageType;
await loadEnv();
const cfCategories = await fetchCategoriesCurseforge(packageType);
const modrinthCategories = await fetchCategoriesModrinth(packageType);
const categories = {
...cfCategories,
...modrinthCategories,
};
console.log(categories);
})();

View file

@ -22,10 +22,13 @@
"setup-nix": "./setup.sh",
"autoformat": "./format.sh",
"fix-deb": "./fix-deb.sh",
"dupe": "./dedupe.sh"
"dupe": "./dedupe.sh",
"api:categories": "pnpm tsx ./api/fetch_categories.ts"
},
"dependencies": {
"@pnpm/find-workspace-dir": "^7.0.1",
"consola": "^3.2.3",
"dotenv": "^16.4.5",
"execa": "^9.4.0",
"mustache": "^4.2.0",
"pathe": "^1.1.2",

29
pnpm-lock.yaml generated
View file

@ -264,9 +264,15 @@ importers:
packages/scripts:
dependencies:
'@pnpm/find-workspace-dir':
specifier: ^7.0.1
version: 7.0.1
consola:
specifier: ^3.2.3
version: 3.2.3
dotenv:
specifier: ^16.4.5
version: 16.4.5
execa:
specifier: ^9.4.0
version: 9.4.0
@ -1020,6 +1026,18 @@ packages:
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@pnpm/constants@8.0.0':
resolution: {integrity: sha512-yQosGUvYPpAjb1jOFcdbwekRjZRVxN6C0hHzfRCZrMKbxGjt/E0g0RcFlEDNVZ95tm4oMMcr7nEPa7H7LX3emw==}
engines: {node: '>=18.12'}
'@pnpm/error@6.0.1':
resolution: {integrity: sha512-7yjO0RgmWYb4OKgcWC33yD4Z2CxE7Tm7vXX1SmS7GDifDT/bgZZhHeS2xq/+W6y9yhwIrRSA+7AlQL1NM2wIvw==}
engines: {node: '>=18.12'}
'@pnpm/find-workspace-dir@7.0.1':
resolution: {integrity: sha512-o1LAFM/5MChI6qBolMBOznzatch01UK3wIgoAE/b779qs1FakksB278nMRTwRY58PZSBT+RxZ2RCMjlxPLeVWw==}
engines: {node: '>=18.12'}
'@polka/url@1.0.0-next.25':
resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
@ -4866,6 +4884,17 @@ snapshots:
'@pkgr/core@0.1.1': {}
'@pnpm/constants@8.0.0': {}
'@pnpm/error@6.0.1':
dependencies:
'@pnpm/constants': 8.0.0
'@pnpm/find-workspace-dir@7.0.1':
dependencies:
'@pnpm/error': 6.0.1
find-up: 5.0.0
'@polka/url@1.0.0-next.25': {}
'@prisma/debug@5.19.1': {}