chore: merge onelauncher/main

This commit is contained in:
LynithDev 2025-07-25 11:54:07 +02:00
commit d7ca81c2ed
78 changed files with 1915 additions and 763 deletions

View file

@ -20,7 +20,7 @@ interface TargetConfig {
}
interface BuildTarget {
updater: false | Updater;
standalone: TargetConfig[];
standalone: Array<TargetConfig>;
}
const osTargets = {
@ -67,7 +67,7 @@ const frontendBundle = 'apps/frontend/dist.tar.xz';
const updaterName = `OneLauncher-Updater-${os}-${arch}`;
const frontendName = `OneLauncher-Frontend-${os}-${arch}`;
const globFiles = async (pattern: string): Promise<string[]> => await (await glob.create(pattern)).glob();
const globFiles = async (pattern: string): Promise<Array<string>> => await (await glob.create(pattern)).glob();
async function uploadFrontend() {
if (!(await exists(frontendBundle))) {

View file

@ -9,10 +9,12 @@ runs:
using: composite
steps:
- name: Install pnpm
uses: pnpm/action-setup@ac5bf11548bf5e19b8aadb8182072616590fa4a6
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@26961cf329f22f6837d5f54c3efd76b480300ace
uses: actions/setup-node@v4
with:
token: ${{ inputs.token }}
check-latest: true

View file

@ -15,23 +15,23 @@ inputs:
runs:
using: composite
steps:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a
with:
toolchain: stable
targets: ${{ inputs.target }}
components: clippy, rustfmt
# - name: Install Rust Stable
# uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a
# with:
# toolchain: stable
# targets: ${{ inputs.target }}
# components: clippy, rustfmt
- name: Install Rust Nightly
uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly
toolchain: nightly-2025-07-23
targets: ${{ inputs.target }}
components: clippy, rustfmt
components: cargo, clippy, rustc, rustfmt
- name: Cache Rust
if: ${{ inputs.restore-cache == 'true' }}
uses: Swatinem/rust-cache@9bdad043e88c75890e36ad3bbc8d27f0090dd609
uses: Swatinem/rust-cache@master
with:
key: ${{ inputs.target }}
save-if: ${{ inputs.save-cache }}

View file

@ -43,5 +43,6 @@ jobs:
releaseName: OneClient __VERSION__
releaseDraft: true
prerelease: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-alpha') || false }}
tauriScript: pnpm oneclient:desktop
tauriScript: pnpm -w oneclient:desktop
args: ${{ matrix.args }}
projectPath: ./apps/oneclient/desktop/

View file

@ -43,5 +43,6 @@ jobs:
releaseName: OneLauncher __VERSION__
releaseDraft: true
prerelease: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-alpha') || false }}
tauriScript: pnpm onelauncher:desktop
tauriScript: pnpm -w onelauncher:desktop
args: ${{ matrix.args }}
projectPath: ./apps/onelauncher/desktop/

1
.husky/pre-commit Normal file
View file

@ -0,0 +1 @@
pnpm lint-staged

2
.nvmrc
View file

@ -1 +1 @@
20
24

View file

@ -23,7 +23,7 @@ We welcome contributions! Please read our [contributing guidelines](CONTRIBUTING
If you encounter any issues, ensure that you are using the following versions (or later) of Rust, Node and Pnpm:
- `rustc` version: **1.89**
- `rustc` version: **nightly-2025-07-23**
- `node` version: **22.15**
- `pnpm` version: **10.13**

View file

@ -1,6 +1,7 @@
use std::collections::HashMap;
use onelauncher_core::{entity::clusters, error::LauncherResult};
use onelauncher_core::entity::clusters;
use onelauncher_core::error::LauncherResult;
use tauri::Runtime;
#[taurpc::procedures(path = "oneclient", export_to = "../frontend/src/bindings.gen.ts")]
@ -22,7 +23,9 @@ impl OneClientApi for OneClientApiImpl {
webview_window.open_devtools();
}
async fn get_clusters_grouped_by_major(self) -> LauncherResult<HashMap<u32, Vec<clusters::Model>>> {
async fn get_clusters_grouped_by_major(
self,
) -> LauncherResult<HashMap<u32, Vec<clusters::Model>>> {
let clusters = onelauncher_core::api::cluster::dao::get_all_clusters().await?;
let mut mapped: HashMap<u32, Vec<clusters::Model>> = HashMap::new();
@ -38,13 +41,10 @@ impl OneClientApi for OneClientApiImpl {
Err(_) => continue, // Skip if parsing fails
};
mapped.entry(major)
.or_insert_with(Vec::new)
.push(cluster);
mapped.entry(major).or_default().push(cluster);
}
}
Ok(mapped)
}
}

View file

@ -35,9 +35,10 @@ async fn initialize_core() -> LauncherResult<()> {
launcher_website: "https://polyfrost.org/".to_string(),
discord_client_id: None, //Some(constants::DISCORD_CLIENT_ID.to_string()), // disabled for now
fetch_attempts: 3,
logger_filter: Some(
format!("{}={level},onelauncher_core={level}", env!("CARGO_PKG_NAME")),
),
logger_filter: Some(format!(
"{}={level},onelauncher_core={level}",
env!("CARGO_PKG_NAME")
)),
..Default::default()
};
@ -68,7 +69,7 @@ async fn initialize_tauri(builder: tauri::Builder<tauri::Wry>) -> LauncherResult
app.emit("single-instance", SingleInstancePayload { args: argv, cwd })
.unwrap();
}))
.plugin(tauri_plugin_updater::Builder::new().build())
// .plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_deep_link::init())
@ -98,9 +99,7 @@ async fn initialize_state(handle: &tauri::AppHandle) -> LauncherResult<()> {
}
pub async fn run() {
initialize_core()
.await
.expect("failed to initialize core");
initialize_core().await.expect("failed to initialize core");
let app = initialize_tauri(tauri::Builder::default())
.await
@ -110,8 +109,7 @@ pub async fn run() {
.await
.expect("failed to initialize state");
initialize_oneclient()
.await;
initialize_oneclient().await;
app.run(|_, _| {});
}

View file

@ -1,4 +1,8 @@
use onelauncher_core::{entity::{clusters::Model as ClusterModel, icon::Icon, loader::GameLoader}, error::LauncherResult, send_error};
use onelauncher_core::entity::clusters::Model as ClusterModel;
use onelauncher_core::entity::icon::Icon;
use onelauncher_core::entity::loader::GameLoader;
use onelauncher_core::error::LauncherResult;
use onelauncher_core::send_error;
pub async fn initialize_oneclient() {
init_clusters().await;
@ -31,12 +35,14 @@ async fn init_clusters() {
for loader in &cluster.mc_loaders {
if let Err(e) = create_cluster_if_not_exist(
&format!("{} {}", mc_version, loader),
&format!("{mc_version} {loader}"),
&mc_version,
*loader,
None,
None,
).await {
)
.await
{
send_error!("failed to create cluster for {}: {}", mc_version, e);
}
}
@ -56,7 +62,13 @@ async fn create_cluster_if_not_exist(
return Ok(None);
}
onelauncher_core::api::cluster::create_cluster(name, mc_version, mc_loader, mc_loader_version, icon_url)
.await
.map(Some)
}
onelauncher_core::api::cluster::create_cluster(
name,
mc_version,
mc_loader,
mc_loader_version,
icon_url,
)
.await
.map(Some)
}

View file

@ -45,6 +45,7 @@
},
"bundle": {
"active": true,
"createUpdaterArtifacts": false,
"targets": [
"app",
"dmg",
@ -66,7 +67,6 @@
"license": "GPL-3.0-only",
"homepage": "https://polyfrost.org/",
"longDescription": "Next-generation open source Minecraft launcher",
"createUpdaterArtifacts": true,
"resources": [],
"externalBin": [],
"linux": {

View file

@ -4,4 +4,5 @@ dist
dist-ssr
*.local
**/*.gen.ts
# **/*.gen.ts
routeTree.gen.ts

View file

@ -8,8 +8,7 @@
"build": "vite build && tsc",
"serve": "vite preview",
"lint": "eslint",
"format": "prettier",
"check": "prettier --write . && eslint --fix"
"check": "eslint --fix"
},
"dependencies": {
"@fontsource/poppins": "catalog:",

View file

@ -1,10 +0,0 @@
// @ts-check
/** @type {import('prettier').Config} */
const config = {
semi: true,
singleQuote: true,
trailingComma: 'all',
};
export default config;

View file

@ -0,0 +1,251 @@
// This file has been generated by Specta. DO NOT EDIT.
import { createTauRPCProxy as createProxy, type InferCommandOutput } from 'taurpc'
type TAURI_CHANNEL<T> = (response: T) => void
export type ClusterError = { type: "InvalidVersion"; data: string } | { type: "MissingJavaVersion"; data: string } | { type: "ClusterDownloading"; data: string } | { type: "ClusterAlreadyRunning"; data: string }
export type ClusterModel = { id: bigint; folder_name: string; stage: ClusterStage; created_at: string; group_id: bigint | null; name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; last_played: string | null; overall_played: bigint | null; icon_url: Icon | null; setting_profile_name: string | null; linked_pack_id: string | null; linked_pack_version: string | null }
export type ClusterStage = "notready" | "downloading" | "repairing" | "ready"
export type ClusterUpdate = { name: string | null; icon_url: Icon | null }
export type CreateCluster = { name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; icon: Icon | null }
export type CryptoError = { type: "InvalidHash"; data: string } | { type: "InvalidAlgorithm"; data: string }
export type DaoError = { type: "NotFound"; data: string } | { type: "AlreadyExists"; data: string } | { type: "InvalidValue"; data: string }
export type DbVec = T[]
export type DirectoryError = { type: "BaseDir"; data: string }
export type DiscordError = { type: "MissingClientId"; data: string } | { type: "ConnectError"; data: string }
export type Filters = { game_versions: string[] | null; loaders: GameLoader[] | null; categories: string[] | null; package_types: PackageType[] | null }
export type GameLoader = "vanilla" | "forge" | "neoforge" | "quilt" | "fabric" | "legacyfabric"
export type IOError = { type: "InvalidAbsolutePath"; data: string } | { type: "IOErrorWrapper"; data: string } | { type: "IOError"; data: string } | { type: "DeserializeError"; data: string } | { type: "AsyncZipError"; data: string }
export type Icon = string
export type IncompatiblePackageType = { type: "McVersion"; data: string } | { type: "Loader"; data: string }
export type IngressError = { type: "NotFound"; data: string }
export type IngressPayload = { id: string; message: string; ingress_type: IngressType; percent: number | null; total: number }
export type IngressType = { Download: { file_name: string } } | "JavaPrepare" | "JavaCheck" | "JavaLocate" | "MinecraftDownload" | { PrepareCluster: { cluster_name: string } }
export type JavaError = { type: "ParseVersion"; data: string } | { type: "Execute"; data: string } | { type: "MissingJava"; data: string }
export type LauncherError = { type: "DirError"; data: DirectoryError } | { type: "IOError"; data: IOError } | { type: "IngressError"; data: IngressError } | { type: "JavaError"; data: JavaError } | { type: "CryptoError"; data: CryptoError } | { type: "DiscordError"; data: DiscordError } | { type: "MetadataError"; data: MetadataError } | { type: "ClusterError"; data: ClusterError } | { type: "MinecraftAuthError"; data: MinecraftAuthError } | { type: "ProcessError"; data: ProcessError } | { type: "PackageError"; data: PackageError } | { type: "DaoError"; data: DaoError } | { type: "SerdeError"; data: string } | { type: "AnyhowError"; data: string } | { type: "DbError"; data: string } | { type: "ReqwestError"; data: string } | { type: "InterpulseError"; data: string } | { type: "RegexError"; data: string } | { type: "SemaphoreError"; data: string } | { type: "UrlError"; data: string } | { type: "TauriError"; data: string }
export type ManagedPackage = { id: string; slug: string; provider: Provider; package_type: PackageType; name: string; short_desc: string; body: string; version_ids: string[]; mc_versions: string[]; loaders: GameLoader[]; icon_url: string | null; created: string; updated: string; client: PackageSide; server: PackageSide; categories: string[]; license: PackageLicense | null; author: PackageAuthor; links: PackageLinks; status: PackageStatus; downloads: bigint; gallery: PackageGallery[] }
export type ManagedUser = { id: string; username: string; url?: string | null; avatar_url?: string | null; bio?: string | null; is_organization_user?: boolean; role?: string | null }
export type ManagedVersion = { version_id: string; project_id: string; display_name: string; display_version: string; changelog: string | null; dependencies: ManagedVersionDependency[]; mc_versions: string[]; release_type: PackageReleaseType; loaders: GameLoader[]; published: string; downloads: bigint; files: ManagedVersionFile[] }
export type ManagedVersionDependency = { version_id: string | null; project_id: string | null; file_name: string | null; dependency_type: PackageDependencyType }
export type ManagedVersionFile = { sha1: string; url: string; file_name: string; primary: boolean; size: bigint }
export type MessageLevel = "Info" | "Warn" | "Error"
export type MessagePayload = { level: MessageLevel; message: string }
export type MetadataError = { type: "FetchError"; data: string } | { type: "NotModdedManifest"; data: string } | { type: "NotVanillaManifest"; data: string } | { type: "ParseError"; data: string } | { type: "NoMatchingLoader"; data: string } | { type: "NoMatchingVersion"; data: string }
export type MinecraftAuthError = { type: "PublicKeyReading"; data: string } | { type: "PKCS8Error"; data: string } | { type: "SerializeError"; data: string } | { type: "DeserializeError"; data: string } | { type: "RequestError"; data: string } | { type: "SigningError"; data: string } | { type: "HashError"; data: string } | { type: "SessionIdError"; data: string }
/**
* A structure of all needed Minecraft credentials for logging in and account management.
*/
export type MinecraftCredentials = {
/**
* The uuid of the credentials.
*/
id: string;
/**
* The username of the Minecraft account.
*/
username: string;
/**
* The access token as a String.
*/
access_token: string;
/**
* The refresh token as a string for [`MinecraftState#refresh`].
*/
refresh_token: string;
/**
* The time that the access token expires as a [`DateTime<Utc>`].
*/
expires: string }
export type PackageAuthor = { Team: { team_id: string; org_id: string | null } } | { Users: ManagedUser[] }
export type PackageDependencyType = "required" | "optional" | "embedded" | "incompatible"
/**
* https://api.modrinth.com/v2/tag/donation_platform
*/
export type PackageDonationPlatform = "patreon" | "buymeacoffee" | "paypal" | "github" | "kofi" | "other"
export type PackageDonationUrl = { id: PackageDonationPlatform; url: string }
export type PackageError = { type: "NoPrimaryFile"; data: string } | { type: "IsModPack"; data: string } | { type: "Incompatible"; data: IncompatiblePackageType }
export type PackageGallery = { url: string; thumbnail_url: string; title: string | null; description: string | null; featured: boolean | null }
/**
* https://spdx.org/licenses/
*/
export type PackageLicense = { id: string; name: string; url: string | null }
/**
* https://docs.curseforge.com/rest-api/#tocS_ModLinks
*/
export type PackageLinks = { website: string | null; issues: string | null; source: string | null; wiki: string | null; donation: PackageDonationUrl[] | null; discord: string | null }
export type PackageModel = { hash: string; file_name: string; version_id: string; published_at: string; display_name: string; display_version: string; package_type: PackageType; provider: Provider; package_id: string; mc_versions: DbVec<string>; mc_loader: DbVec<GameLoader>; icon: Icon | null }
export type PackageReleaseType = "release" | "beta" | "alpha"
export type PackageSide = "unknown" | "required" | "optional" | "unsupported"
export type PackageStatus = "active" | "abandoned"
export type PackageType = "mod" | "resourcepack" | "shader" | "datapack" | "modpack"
export type Paginated<T> = { total: bigint; offset: bigint; limit: bigint; items: T[] }
export type Process = { pid: number; started_at: string; cluster_id: bigint; post_hook: string | null; account_id: string }
export type ProcessError = { type: "HookUnsuccessful"; data: string } | { type: "NoPid"; data: string }
export type ProcessPayload = { type: "Starting"; command: string } | { type: "Started"; process: Process } | { type: "Stopped"; pid: number; exit_code: number } | { type: "Output"; pid: number; output: string }
export type ProfileUpdate = { res: Resolution | null; force_fullscreen: boolean | null; mem_max: number | null; launch_args: string | null; launch_env: string | null; hook_pre: string | null; hook_wrapper: string | null; hook_post: string | null }
export type Provider = "Modrinth" | "Curseforge" | "SkyClient"
export type Resolution = { width: number; height: number }
export type SearchQuery = { query: string | null; offset: bigint | null; limit: bigint | null; sort: Sort | null; filters: Filters | null }
export type SearchResult = { project_id: string; project_type: string; slug: string; author: string; title: string; description: string; categories: string[]; display_categories: string[]; versions: string[]; downloads: bigint; icon_url: string; date_created: string; date_modified: string; latest_version: string; license: string; client_side: PackageSide; server_side: PackageSide;
/**
* List of URLs to images
*/
gallery: string[] }
export type SettingProfileModel = { name: string; java_id: bigint | null; res: Resolution | null; force_fullscreen: boolean | null; mem_max: number | null; launch_args: string | null; launch_env: string | null; hook_pre: string | null; hook_wrapper: string | null; hook_post: string | null; os_extra: SettingsOsExtra | null }
export type Settings = { global_game_settings: SettingProfileModel; allow_parallel_running_clusters: boolean; enable_gamemode: boolean; discord_enabled: boolean; max_concurrent_requests: bigint; settings_version: number; native_window_frame: boolean }
export type SettingsOsExtra = { enable_gamemode: boolean | null }
export type Sort = "Relevance" | "Downloads" | "Newest" | "Updated"
/**
* A game version of Minecraft
*/
export type Version = {
/**
* A unique identifier of the version
*/
id: string;
/**
* The release type of the version
*/
type: VersionType;
/**
* A link to additional information about the version
*/
url: string;
/**
* The latest time a file in this version was updated
*/
time: string;
/**
* The time this version was released
*/
releaseTime: string;
/**
* The SHA1 hash of the additional information about the version
*/
sha1: string;
/**
* Whether the version supports the latest player safety features
*/
complianceLevel: number;
/**
* The SHA1 hash of the original unmodified Minecraft version's JSON
* This is only available when using the Pulseflow mirror
*/
originalSha1?: string | null }
/**
* The version type of Minecraft
*/
export type VersionType =
/**
* A major version, which is stable for all players to use
*/
"release" |
/**
* An experimental version, which is unstable and used for feature previews and beta testing
*/
"snapshot" |
/**
* The oldest versions before the game was released
*/
"old_alpha" |
/**
* Early versions of the game
*/
"old_beta"
const ARGS_MAP = { 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'core':'{"getPackageVersions":["provider","slug","mc_versions","loaders","offset","limit"],"readSettings":[],"getDefaultUser":["fallback"],"getPackageUser":["provider","slug"],"getClusterById":["id"],"updateClusterById":["id","request"],"getClusters":[],"getLoadersForVersion":["mc_version"],"searchPackages":["provider","query"],"removeCluster":["id"],"getPackage":["provider","slug"],"launchCluster":["id","uuid"],"openMsaLogin":[],"getMultiplePackages":["provider","slugs"],"getUsersFromAuthor":["provider","author"],"getGlobalProfile":[],"removeUser":["uuid"],"getWorlds":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getUsers":[],"getGameVersions":[],"getUser":["uuid"],"setDefaultUser":["uuid"],"updateClusterProfile":["name","profile"],"getProfileOrDefault":["name"],"createCluster":["options"],"getScreenshots":["id"],"writeSettings":["setting"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}' }
export type Router = { 'oneclient': { openDevTools: () => Promise<void>,
getClustersGroupedByMajor: () => Promise<Partial<{ [key in number]: ClusterModel[] }>> },
'core': { getClusters: () => Promise<ClusterModel[]>,
getClusterById: (id: bigint) => Promise<ClusterModel | null>,
removeCluster: (id: bigint) => Promise<null>,
createCluster: (options: CreateCluster) => Promise<ClusterModel>,
launchCluster: (id: bigint, uuid: string | null) => Promise<null>,
updateClusterById: (id: bigint, request: ClusterUpdate) => Promise<null>,
getScreenshots: (id: bigint) => Promise<string[]>,
getWorlds: (id: bigint) => Promise<string[]>,
getProfileOrDefault: (name: string | null) => Promise<SettingProfileModel>,
getGlobalProfile: () => Promise<SettingProfileModel>,
updateClusterProfile: (name: string, profile: ProfileUpdate) => Promise<SettingProfileModel>,
getGameVersions: () => Promise<Version[]>,
getLoadersForVersion: (mcVersion: string) => Promise<GameLoader[]>,
getUsers: () => Promise<MinecraftCredentials[]>,
getUser: (uuid: string) => Promise<MinecraftCredentials | null>,
removeUser: (uuid: string) => Promise<null>,
getDefaultUser: (fallback: boolean | null) => Promise<MinecraftCredentials | null>,
setDefaultUser: (uuid: string | null) => Promise<null>,
openMsaLogin: () => Promise<MinecraftCredentials | null>,
readSettings: () => Promise<Settings>,
writeSettings: (setting: Settings) => Promise<null>,
searchPackages: (provider: Provider, query: SearchQuery) => Promise<Paginated<SearchResult>>,
getPackage: (provider: Provider, slug: string) => Promise<ManagedPackage>,
getMultiplePackages: (provider: Provider, slugs: string[]) => Promise<ManagedPackage[]>,
getPackageVersions: (provider: Provider, slug: string, mcVersions: string[] | null, loaders: GameLoader[] | null, offset: bigint, limit: bigint) => Promise<Paginated<ManagedVersion>>,
getPackageUser: (provider: Provider, slug: string) => Promise<ManagedUser>,
downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: bigint, skipCompatibility: boolean | null) => Promise<PackageModel>,
getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<ManagedUser[]> },
'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> } };
export type { InferCommandOutput }
export const createTauRPCProxy = () => createProxy<Router>(ARGS_MAP)

View file

@ -25,5 +25,5 @@
"skipLibCheck": true,
"noUncheckedSideEffectImports": true
},
"include": ["**/*.ts", "**/*.tsx", "prettier.config.js", "vite.config.js"]
"include": ["**/*.ts", "**/*.tsx", "vite.config.ts"]
}

View file

@ -1,5 +1,5 @@
use onelauncher_core::api::proxy::ProxyTauri;
use onelauncher_core::api::tauri::{TauriLauncherEventApi, TauriLauncherApi};
use onelauncher_core::api::tauri::{TauriLauncherApi, TauriLauncherEventApi};
use onelauncher_core::error::LauncherResult;
use onelauncher_core::store::proxy::ProxyState;
use onelauncher_core::store::semaphore::SemaphoreStore;
@ -33,9 +33,10 @@ async fn initialize_core() -> LauncherResult<()> {
launcher_website: "https://polyfrost.org/".to_string(),
discord_client_id: Some(constants::DISCORD_CLIENT_ID.to_string()),
fetch_attempts: 3,
logger_filter: Some(
format!("{}={level},onelauncher_core={level}", env!("CARGO_PKG_NAME")),
),
logger_filter: Some(format!(
"{}={level},onelauncher_core={level}",
env!("CARGO_PKG_NAME")
)),
..Default::default()
};
@ -61,13 +62,14 @@ async fn initialize_tauri(builder: tauri::Builder<tauri::Wry>) -> LauncherResult
.merge(onelauncher_core::api::tauri::TauriLauncherApiImpl.into_handler())
.merge(onelauncher_core::api::tauri::TauriLauncherEventApiImpl.into_handler());
let launcher_path = Dirs::get().await?.base_dir();
let builder = builder
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
println!("{}, {argv:?}, {cwd}", app.package_info().name);
app.emit("single-instance", SingleInstancePayload { args: argv, cwd })
.unwrap();
}))
.plugin(tauri_plugin_updater::Builder::new().build())
// .plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_deep_link::init())
@ -75,10 +77,8 @@ async fn initialize_tauri(builder: tauri::Builder<tauri::Wry>) -> LauncherResult
.menu(tauri::menu::Menu::new)
.invoke_handler(router.into_handler())
.setup(move |app| {
let launcher_path = app.path().data_dir().ok().expect("ok").join("OneLauncher");
app.asset_protocol_scope()
.allow_directory(&launcher_path, true)
.allow_directory(launcher_path, true)
.unwrap();
setup_window(app.handle()).expect("failed to setup main window");

View file

@ -45,6 +45,7 @@
},
"bundle": {
"active": true,
"createUpdaterArtifacts": false,
"targets": [
"app",
"dmg",
@ -66,7 +67,6 @@
"license": "GPL-3.0-only",
"homepage": "https://polyfrost.org/",
"longDescription": "Next-generation open source Minecraft launcher",
"createUpdaterArtifacts": true,
"resources": [],
"externalBin": [],
"linux": {
@ -112,7 +112,7 @@
},
"plugins": {
"updater": {
"pubkey": "",
"active": false,
"endpoints": ["https://polyfrost.org/releases/tauri/{{current_version}}/{{target}}/{{arch}}"]
},
"deep-link": {

View file

@ -4,4 +4,5 @@ dist
dist-ssr
*.local
**/*.gen.ts
# **/*.gen.ts
routeTree.gen.ts

View file

@ -0,0 +1,253 @@
// @ts-nocheck
// This file has been generated by Specta. DO NOT EDIT.
import { createTauRPCProxy as createProxy, type InferCommandOutput } from 'taurpc'
type TAURI_CHANNEL<T> = (response: T) => void
export type ClusterError = { type: "InvalidVersion"; data: string } | { type: "MissingJavaVersion"; data: string } | { type: "ClusterDownloading"; data: string } | { type: "ClusterAlreadyRunning"; data: string }
export type ClusterModel = { id: bigint; folder_name: string; stage: ClusterStage; created_at: string; group_id: bigint | null; name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; last_played: string | null; overall_played: bigint | null; icon_url: Icon | null; setting_profile_name: string | null; linked_pack_id: string | null; linked_pack_version: string | null }
export type ClusterStage = "notready" | "downloading" | "repairing" | "ready"
export type ClusterUpdate = { name: string | null; icon_url: Icon | null }
export type CreateCluster = { name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; icon: Icon | null }
export type CryptoError = { type: "InvalidHash"; data: string } | { type: "InvalidAlgorithm"; data: string }
export type DaoError = { type: "NotFound"; data: string } | { type: "AlreadyExists"; data: string } | { type: "InvalidValue"; data: string }
export type DbVec = T[]
export type DirectoryError = { type: "BaseDir"; data: string }
export type DiscordError = { type: "MissingClientId"; data: string } | { type: "ConnectError"; data: string }
export type Filters = { game_versions: string[] | null; loaders: GameLoader[] | null; categories: string[] | null; package_types: PackageType[] | null }
export type GameLoader = "vanilla" | "forge" | "neoforge" | "quilt" | "fabric" | "legacyfabric"
export type IOError = { type: "InvalidAbsolutePath"; data: string } | { type: "IOErrorWrapper"; data: string } | { type: "IOError"; data: string } | { type: "DeserializeError"; data: string } | { type: "AsyncZipError"; data: string }
export type Icon = string
export type IncompatiblePackageType = { type: "McVersion"; data: string } | { type: "Loader"; data: string }
export type IngressError = { type: "NotFound"; data: string }
export type IngressPayload = { id: string; message: string; ingress_type: IngressType; percent: number | null; total: number }
export type IngressType = { Download: { file_name: string } } | "JavaPrepare" | "JavaCheck" | "JavaLocate" | "MinecraftDownload" | { PrepareCluster: { cluster_name: string } }
export type JavaError = { type: "ParseVersion"; data: string } | { type: "Execute"; data: string } | { type: "MissingJava"; data: string }
export type LauncherError = { type: "DirError"; data: DirectoryError } | { type: "IOError"; data: IOError } | { type: "IngressError"; data: IngressError } | { type: "JavaError"; data: JavaError } | { type: "CryptoError"; data: CryptoError } | { type: "DiscordError"; data: DiscordError } | { type: "MetadataError"; data: MetadataError } | { type: "ClusterError"; data: ClusterError } | { type: "MinecraftAuthError"; data: MinecraftAuthError } | { type: "ProcessError"; data: ProcessError } | { type: "PackageError"; data: PackageError } | { type: "DaoError"; data: DaoError } | { type: "SerdeError"; data: string } | { type: "AnyhowError"; data: string } | { type: "DbError"; data: string } | { type: "ReqwestError"; data: string } | { type: "InterpulseError"; data: string } | { type: "RegexError"; data: string } | { type: "SemaphoreError"; data: string } | { type: "UrlError"; data: string } | { type: "TauriError"; data: string }
export type ManagedPackage = { id: string; slug: string; provider: Provider; package_type: PackageType; name: string; short_desc: string; body: string; version_ids: string[]; mc_versions: string[]; loaders: GameLoader[]; icon_url: string | null; created: string; updated: string; client: PackageSide; server: PackageSide; categories: string[]; license: PackageLicense | null; author: PackageAuthor; links: PackageLinks; status: PackageStatus; downloads: bigint; gallery: PackageGallery[] }
export type ManagedUser = { id: string; username: string; url?: string | null; avatar_url?: string | null; bio?: string | null; is_organization_user?: boolean; role?: string | null }
export type ManagedVersion = { version_id: string; project_id: string; display_name: string; display_version: string; changelog: string | null; dependencies: ManagedVersionDependency[]; mc_versions: string[]; release_type: PackageReleaseType; loaders: GameLoader[]; published: string; downloads: bigint; files: ManagedVersionFile[] }
export type ManagedVersionDependency = { version_id: string | null; project_id: string | null; file_name: string | null; dependency_type: PackageDependencyType }
export type ManagedVersionFile = { sha1: string; url: string; file_name: string; primary: boolean; size: bigint }
export type MessageLevel = "Info" | "Warn" | "Error"
export type MessagePayload = { level: MessageLevel; message: string }
export type MetadataError = { type: "FetchError"; data: string } | { type: "NotModdedManifest"; data: string } | { type: "NotVanillaManifest"; data: string } | { type: "ParseError"; data: string } | { type: "NoMatchingLoader"; data: string } | { type: "NoMatchingVersion"; data: string }
export type MinecraftAuthError = { type: "PublicKeyReading"; data: string } | { type: "PKCS8Error"; data: string } | { type: "SerializeError"; data: string } | { type: "DeserializeError"; data: string } | { type: "RequestError"; data: string } | { type: "SigningError"; data: string } | { type: "HashError"; data: string } | { type: "SessionIdError"; data: string }
/**
* A structure of all needed Minecraft credentials for logging in and account management.
*/
export type MinecraftCredentials = {
/**
* The uuid of the credentials.
*/
id: string;
/**
* The username of the Minecraft account.
*/
username: string;
/**
* The access token as a String.
*/
access_token: string;
/**
* The refresh token as a string for [`MinecraftState#refresh`].
*/
refresh_token: string;
/**
* The time that the access token expires as a [`DateTime<Utc>`].
*/
expires: string }
export type PackageAuthor = { Team: { team_id: string; org_id: string | null } } | { Users: ManagedUser[] }
export type PackageDependencyType = "required" | "optional" | "embedded" | "incompatible"
/**
* https://api.modrinth.com/v2/tag/donation_platform
*/
export type PackageDonationPlatform = "patreon" | "buymeacoffee" | "paypal" | "github" | "kofi" | "other"
export type PackageDonationUrl = { id: PackageDonationPlatform; url: string }
export type PackageError = { type: "NoPrimaryFile"; data: string } | { type: "IsModPack"; data: string } | { type: "Incompatible"; data: IncompatiblePackageType }
export type PackageGallery = { url: string; thumbnail_url: string; title: string | null; description: string | null; featured: boolean | null }
/**
* https://spdx.org/licenses/
*/
export type PackageLicense = { id: string; name: string; url: string | null }
/**
* https://docs.curseforge.com/rest-api/#tocS_ModLinks
*/
export type PackageLinks = { website: string | null; issues: string | null; source: string | null; wiki: string | null; donation: PackageDonationUrl[] | null; discord: string | null }
export type PackageModel = { hash: string; file_name: string; version_id: string; published_at: string; display_name: string; display_version: string; package_type: PackageType; provider: Provider; package_id: string; mc_versions: DbVec<string>; mc_loader: DbVec<GameLoader>; icon: Icon | null }
export type PackageReleaseType = "release" | "beta" | "alpha"
export type PackageSide = "unknown" | "required" | "optional" | "unsupported"
export type PackageStatus = "active" | "abandoned"
export type PackageType = "mod" | "resourcepack" | "shader" | "datapack" | "modpack"
export type Paginated<T> = { total: bigint; offset: bigint; limit: bigint; items: T[] }
export type Process = { pid: number; started_at: string; cluster_id: bigint; post_hook: string | null; account_id: string }
export type ProcessError = { type: "HookUnsuccessful"; data: string } | { type: "NoPid"; data: string }
export type ProcessPayload = { type: "Starting"; command: string } | { type: "Started"; process: Process } | { type: "Stopped"; pid: number; exit_code: number } | { type: "Output"; pid: number; output: string }
export type ProfileUpdate = { res: Resolution | null; force_fullscreen: boolean | null; mem_max: number | null; launch_args: string | null; launch_env: string | null; hook_pre: string | null; hook_wrapper: string | null; hook_post: string | null }
export type Provider = "Modrinth" | "Curseforge" | "SkyClient"
export type Resolution = { width: number; height: number }
export type SearchQuery = { query: string | null; offset: bigint | null; limit: bigint | null; sort: Sort | null; filters: Filters | null }
export type SearchResult = { project_id: string; project_type: string; slug: string; author: string; title: string; description: string; categories: string[]; display_categories: string[]; versions: string[]; downloads: bigint; icon_url: string; date_created: string; date_modified: string; latest_version: string; license: string; client_side: PackageSide; server_side: PackageSide;
/**
* List of URLs to images
*/
gallery: string[] }
export type SettingProfileModel = { name: string; java_id: bigint | null; res: Resolution | null; force_fullscreen: boolean | null; mem_max: number | null; launch_args: string | null; launch_env: string | null; hook_pre: string | null; hook_wrapper: string | null; hook_post: string | null; os_extra: SettingsOsExtra | null }
export type Settings = { global_game_settings: SettingProfileModel; allow_parallel_running_clusters: boolean; enable_gamemode: boolean; discord_enabled: boolean; max_concurrent_requests: bigint; settings_version: number; native_window_frame: boolean }
export type SettingsOsExtra = Record<string, never>
export type Sort = "Relevance" | "Downloads" | "Newest" | "Updated"
/**
* A game version of Minecraft
*/
export type Version = {
/**
* A unique identifier of the version
*/
id: string;
/**
* The release type of the version
*/
type: VersionType;
/**
* A link to additional information about the version
*/
url: string;
/**
* The latest time a file in this version was updated
*/
time: string;
/**
* The time this version was released
*/
releaseTime: string;
/**
* The SHA1 hash of the additional information about the version
*/
sha1: string;
/**
* Whether the version supports the latest player safety features
*/
complianceLevel: number;
/**
* The SHA1 hash of the original unmodified Minecraft version's JSON
* This is only available when using the Pulseflow mirror
*/
originalSha1?: string | null }
/**
* The version type of Minecraft
*/
export type VersionType =
/**
* A major version, which is stable for all players to use
*/
"release" |
/**
* An experimental version, which is unstable and used for feature previews and beta testing
*/
"snapshot" |
/**
* The oldest versions before the game was released
*/
"old_alpha" |
/**
* Early versions of the game
*/
"old_beta"
const ARGS_MAP = { 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'core':'{"removeUser":["uuid"],"writeSettings":["setting"],"updateClusterById":["id","request"],"getScreenshots":["id"],"getClusters":[],"getWorlds":["id"],"getGameVersions":[],"getPackage":["provider","slug"],"getLoadersForVersion":["mc_version"],"getMultiplePackages":["provider","slugs"],"getPackageUser":["provider","slug"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getUsersFromAuthor":["provider","author"],"searchPackages":["provider","query"],"getUser":["uuid"],"getProfileOrDefault":["name"],"updateClusterProfile":["name","profile"],"getPackageVersions":["provider","slug","mc_versions","loaders","offset","limit"],"launchCluster":["id","uuid"],"setDefaultUser":["uuid"],"openMsaLogin":[],"getGlobalProfile":[],"getClusterById":["id"],"getDefaultUser":["fallback"],"readSettings":[],"removeCluster":["id"],"createCluster":["options"],"getUsers":[]}', 'onelauncher':'{"set_window_style":["decorations"],"return_error":[],"open_dev_tools":[]}' }
export type Router = { 'core': { getClusters: () => Promise<ClusterModel[]>,
getClusterById: (id: bigint) => Promise<ClusterModel | null>,
removeCluster: (id: bigint) => Promise<null>,
createCluster: (options: CreateCluster) => Promise<ClusterModel>,
launchCluster: (id: bigint, uuid: string | null) => Promise<null>,
updateClusterById: (id: bigint, request: ClusterUpdate) => Promise<null>,
getScreenshots: (id: bigint) => Promise<string[]>,
getWorlds: (id: bigint) => Promise<string[]>,
getProfileOrDefault: (name: string | null) => Promise<SettingProfileModel>,
getGlobalProfile: () => Promise<SettingProfileModel>,
updateClusterProfile: (name: string, profile: ProfileUpdate) => Promise<SettingProfileModel>,
getGameVersions: () => Promise<Version[]>,
getLoadersForVersion: (mcVersion: string) => Promise<GameLoader[]>,
getUsers: () => Promise<MinecraftCredentials[]>,
getUser: (uuid: string) => Promise<MinecraftCredentials | null>,
removeUser: (uuid: string) => Promise<null>,
getDefaultUser: (fallback: boolean | null) => Promise<MinecraftCredentials | null>,
setDefaultUser: (uuid: string | null) => Promise<null>,
openMsaLogin: () => Promise<MinecraftCredentials | null>,
readSettings: () => Promise<Settings>,
writeSettings: (setting: Settings) => Promise<null>,
searchPackages: (provider: Provider, query: SearchQuery) => Promise<Paginated<SearchResult>>,
getPackage: (provider: Provider, slug: string) => Promise<ManagedPackage>,
getMultiplePackages: (provider: Provider, slugs: string[]) => Promise<ManagedPackage[]>,
getPackageVersions: (provider: Provider, slug: string, mcVersions: string[] | null, loaders: GameLoader[] | null, offset: bigint, limit: bigint) => Promise<Paginated<ManagedVersion>>,
getPackageUser: (provider: Provider, slug: string) => Promise<ManagedUser>,
downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: bigint, skipCompatibility: boolean | null) => Promise<PackageModel>,
getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise<ManagedUser[]> },
'events': { ingress: (event: IngressPayload) => Promise<void>,
message: (event: MessagePayload) => Promise<void>,
process: (event: ProcessPayload) => Promise<void> },
'onelauncher': { return_error: () => Promise<null>,
open_dev_tools: () => Promise<void>,
set_window_style: (decorations: boolean) => Promise<void> } };
export type { InferCommandOutput }
export const createTauRPCProxy = () => createProxy<Router>(ARGS_MAP)

View file

@ -1,7 +1,7 @@
import type { Provider, SearchResult } from '@/bindings.gen';
import { abbreviateNumber, LOADERS, upperFirst } from '@/utils';
import { Show, Tooltip } from '@onelauncher/common/components';
import { Link, useNavigate } from '@tanstack/react-router';
import { Link } from '@tanstack/react-router';
import { Download01Icon } from '@untitled-theme/icons-react';
import { useMemo } from 'react';
import { Focusable } from 'react-aria-components';
@ -13,7 +13,7 @@ function includes<T, TArray extends T>(list: { includes: (arg0: TArray) => boole
export function PackageGrid({ items, provider }: { items: Array<SearchResult>; provider: Provider }) {
return (
<div className="grid grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-2">
<div className="grid grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 min-[1900px]:grid-cols-5! gap-2">
{items.map(item => (
<PackageItem key={item.project_id} {...item} provider={provider} />
))}
@ -22,7 +22,7 @@ export function PackageGrid({ items, provider }: { items: Array<SearchResult>; p
}
export function PackageItem({ provider, ...item }: SearchResult & { provider: Provider }) {
const loaders = useMemo(() => item.categories.filter(cat => includes(LOADERS, cat)), []);
const loaders = useMemo(() => item.categories.filter(cat => includes(LOADERS, cat)), [item.categories]);
return (
<Link

View file

@ -1,19 +1,20 @@
import type { ClusterModel, GameLoader, ManagedPackage, ManagedVersion, Paginated, Provider, SearchQuery, SearchResult } from '@/bindings.gen';
import type { BindingCommands } from '@/types/global';
import type { UndefinedInitialDataOptions } from '@tanstack/react-query';
import type { Dispatch, SetStateAction } from 'react';
import { bindings } from '@/main';
import { PROVIDERS } from '@/utils';
import { useCommand } from '@onelauncher/common';
import { useCommand, useCommandMut } from '@onelauncher/common';
import { useNavigate } from '@tanstack/react-router';
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
export interface BrowserControllerType {
cluster: ClusterModel | undefined;
setCluster: (cluster: ClusterModel | undefined) => void;
setCluster: Dispatch<SetStateAction<ClusterModel | undefined>>;
provider: Provider;
setProvider: (provider: Provider) => void;
setProvider: Dispatch<SetStateAction<Provider>>;
query: SearchQuery;
setQuery: (query: SearchQuery) => void;
setQuery: Dispatch<SetStateAction<SearchQuery>>;
search: () => void;
}
@ -36,7 +37,7 @@ export function BrowserProvider(props: any) {
categories: null,
game_versions: null,
loaders: null,
package_types: ["mod"]
package_types: ['mod'],
},
query: null,
limit: 18 as unknown as bigint,
@ -47,7 +48,7 @@ export function BrowserProvider(props: any) {
useEffect(() => {
if (query.filters?.categories || query.query)
navigate({ to: '/app/browser/search' });
}, [query]);
}, [navigate, query]);
const context = useMemo<BrowserControllerType>(() => ({
cluster,
@ -78,3 +79,7 @@ export function usePackageData(provider: Provider, slug: string, options?: Omit<
export function usePackageVersions(provider: Provider, slug: string, { mc_versions, loaders, offset, limit, ...options }: { mc_versions?: Array<string> | null; loaders?: Array<GameLoader> | null; offset?: number; limit: number } & Omit<UndefinedInitialDataOptions<Paginated<ManagedVersion>>, 'queryKey' | 'queryFn'>, key: false | BindingCommands | (string & {}) = `getPackageVersions.${provider}.${slug}.${mc_versions?.join('')}.${loaders?.join('')}`) {
return useCommand(key, () => bindings.core.getPackageVersions(provider, slug, mc_versions ?? null, loaders ?? null, (offset ?? 0) as unknown as bigint, limit as unknown as bigint), options);
}
export function useDownloadPackage(cluster: ClusterModel, provider: Provider, version: ManagedVersion, skipCompatibility = false) {
return useCommandMut(() => bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility));
}

View file

@ -1,10 +1,10 @@
import type { ClusterModel } from '@/bindings.gen';
import type { ModalProps } from '@/components/overlay/Modal';
import Modal from '@/components/overlay/Modal';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { useEffect, useState } from 'react';
import Modal from '@/components/overlay/Modal';
import { Button, Dropdown } from '@onelauncher/common/components';
import { useEffect, useState } from 'react';
import { Heading } from 'react-aria-components';
export function useRecentCluster() {
@ -58,26 +58,29 @@ export function useClusters() {
}
type ChooseClusterModalProps = ModalProps & {
clusters?: ClusterModel[];
clusters?: Array<ClusterModel>;
confirmText?: string;
selected?: (clusters: ClusterModel[]) => number;
selected?: (clusters: Array<ClusterModel>) => number;
onSelected?: (cluster: ClusterModel) => void;
};
export function ChooseClusterModal({clusters: clusterList, selected: defaultSelected, onSelected, confirmText, ...props}: ChooseClusterModalProps) {
const clusters = useClusters()
export function ChooseClusterModal({ clusters: clusterList, selected: defaultSelected, onSelected, confirmText, ...props }: ChooseClusterModalProps) {
const clusters = useClusters();
const [selected, setSelected] = useState(0);
useEffect(() => {
if (!clusters) return;
const newSelected = defaultSelected?.(clusters)
if (newSelected) setSelected(newSelected)
}, [clusters])
if (!clusters)
return;
const newSelected = defaultSelected?.(clusters);
if (newSelected)
setSelected(newSelected);
}, [clusters, defaultSelected]);
function chooseCluster() {
if (clusters !== undefined && selected !== undefined) {
const cluster = clusters[selected];
if (clusters !== undefined) {
const cluster = clusters[selected] as ClusterModel | undefined;
if (cluster !== undefined)
onSelected?.(cluster);
}
@ -87,35 +90,35 @@ export function ChooseClusterModal({clusters: clusterList, selected: defaultSele
<Modal {...props}>
<div className="min-w-sm flex flex-col rounded-lg bg-page text-center p-4 gap-2">
<Heading slot='title' className='text-xl font-semibold'>Select a Cluster</Heading>
<Dropdown
onSelectionChange={(index) => {
setSelected(index as number)
}}
selectedKey={selected}
>
{clusters?.map((cluster, index) => (
<Dropdown.Item id={index} key={cluster.id}>
{cluster.name}
</Dropdown.Item>
))}
</Dropdown>
<Heading className="text-xl font-semibold" slot="title">Select a Cluster</Heading>
<Dropdown
onSelectionChange={(index) => {
setSelected(index as number);
}}
selectedKey={selected}
>
{clusters?.map((cluster, index) => (
<Dropdown.Item id={index} key={cluster.id}>
{cluster.name}
</Dropdown.Item>
))}
</Dropdown>
<div className='flex gap-2'>
<Button
color="secondary"
children="Close"
slot="close"
/>
<Button
children={confirmText ?? "Save"}
slot="close"
onClick={chooseCluster}
/>
</div>
<div className="flex gap-2">
<Button
children="Close"
color="secondary"
slot="close"
/>
<Button
children={confirmText ?? 'Save'}
onClick={chooseCluster}
slot="close"
/>
</div>
</div>
</Modal>
)
);
// return (
// <Modal

View file

@ -46,20 +46,20 @@ function usePagination(options: PaginationOptions) {
return pages;
}
function PaginationBtn(props: { page: number }) {
function PaginationBtn({ page: pageIndex }: { page: number }) {
return (
<Button
className="p-2 text-lg min-w-8"
color={props.page === page ? 'primary' : 'secondary'}
color={pageIndex === page ? 'primary' : 'secondary'}
onClick={() => {
if (props.page === page)
if (pageIndex === page)
return;
setPage(props.page);
setPage(pageIndex);
}}
>
<span className="text-trim">{props.page}</span>
<span className="text-trim">{pageIndex}</span>
</Button>
);
}

View file

@ -3,7 +3,7 @@
import type { Settings } from '@/bindings.gen';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
interface SettingsControllerType {
settings: Settings | undefined;
@ -12,7 +12,7 @@ interface SettingsControllerType {
setSettingsToSave: (settings: Partial<Settings> | null) => void;
settingsChanged: boolean;
setSettingsChanged: (changed: boolean) => void;
createSetting: <K extends keyof Settings, V = Settings[K]>(name: K, value: V) => [V, (value: V) => void];
createSetting: <K extends keyof Settings, V = Settings[K]>(name: K, value: V) => [V, (v: V) => void];
}
const SettingsContext = createContext<SettingsControllerType | null>(null);
@ -51,14 +51,14 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, []);
const saveChangedSettings = () => {
const saveChangedSettings = useCallback(() => {
if (!settings)
return;
writeQuery.refetch();
setSettingsChanged(false);
};
}, [settings, writeQuery]);
const handleSetSettingsToSave = useCallback((newSettings: Partial<Settings> | null) => {
setSettingsToSave(newSettings);
@ -86,7 +86,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
return [value, setValue];
}, [settings]);
const controller: SettingsControllerType = {
const controller: SettingsControllerType = useMemo(() => ({
settings,
saveChangedSettings,
settingsToSave,
@ -94,7 +94,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
settingsChanged,
setSettingsChanged,
createSetting,
};
}), [createSetting, handleSetSettingsToSave, saveChangedSettings, settings, settingsChanged, settingsToSave]);
if (settings === undefined)
return null;

View file

@ -32,6 +32,7 @@ function Featured() {
const navigate = useNavigate();
const featuredImageIndex = useMemo(() => featuredPackage.data?.gallery.findIndex(image => image.featured), [featuredPackage.data]);
const [selectedImage, setSelectedImage] = useState(featuredImageIndex ?? 0);
return (
<Show when={featuredPackage.isSuccess}>
<div className="flex flex-col gap-y-1">
@ -40,7 +41,7 @@ function Featured() {
<div className="w-full p-1">
<img alt="thumbnail" className="aspect-video h-full w-full rounded-md object-cover object-center" src={featuredPackage.data?.gallery[selectedImage].url} />
</div>
<div className="max-w-64 min-w-52 flex flex-col gap-y-1 p-4">
<div className="max-w-64 min-[1900px]:max-w-96 min-w-52 flex flex-col gap-y-1 p-4">
<h2 className="text-[1.5rem] font-semibold">{featuredPackage.data?.name}</h2>
<Show when={false}>

View file

@ -1,8 +1,9 @@
import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, PackageDonationUrl, Provider } from '@/bindings.gen';
import type { HTMLProps } from 'react';
import Modal from '@/components/overlay/Modal';
import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser';
import { useBrowserContext, useDownloadPackage, usePackageData, usePackageVersions } from '@/hooks/useBrowser';
import { ChooseClusterModal, useClusters } from '@/hooks/useCluster';
import usePagination from '@/hooks/usePagination';
import { bindings } from '@/main';
import { abbreviateNumber, formatAsRelative, PROVIDERS, upperFirst } from '@/utils';
import { useCommand } from '@onelauncher/common';
@ -16,7 +17,6 @@ import Markdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import { twMerge } from 'tailwind-merge';
import usePagination from '@/hooks/usePagination';
export const Route = createFileRoute('/app/browser/package/$provider/$slug')({
component: RouteComponent,
@ -26,10 +26,10 @@ function includes<T, TArray extends T>(list: { includes: (arg0: TArray) => boole
return list.includes(element as unknown as TArray);
}
function CustomA({ href, children, includeIcon, className, ...rest }: { href: string; children: any, includeIcon?: boolean } & HTMLProps<HTMLAnchorElement>) {
function CustomA({ href, children, includeIcon, className, ...rest }: { href: string; children: any; includeIcon?: boolean } & HTMLProps<HTMLAnchorElement>) {
return (
<a
className={twMerge("text-fg-primary underline", className)}
className={twMerge('text-fg-primary underline', className)}
href={href}
onClick={(e) => {
e.preventDefault();
@ -38,7 +38,7 @@ function CustomA({ href, children, includeIcon, className, ...rest }: { href: st
{...rest}
>
{children}
{includeIcon && <LinkExternal01Icon className='inline w-4 ml-1' />}
{includeIcon && <LinkExternal01Icon className="inline w-4 ml-1" />}
</a>
);
}
@ -56,10 +56,12 @@ function RouteComponent() {
if (!includes(PROVIDERS, provider))
throw new Error('Invalid provider');
const packageData = usePackageData(provider, slug, {});
const browserContext = useBrowserContext();
const packageContextValue = useMemo(() => ({
pkg: packageData.data,
}), [packageData.data]);
return (
<PackageContext value={{ pkg: packageData.data }}>
<PackageContext value={packageContextValue}>
<Show when={packageData.isSuccess && !packageData.isFetching}>
<div className="h-full flex flex-1 flex-row items-start gap-x-4">
<BrowserSidebar package={packageData.data!} />
@ -84,14 +86,14 @@ function RouteComponent() {
</div>
</TabPanel>
<TabPanel id="versions">
<Versions/>
<Versions />
</TabPanel>
<TabPanel className="flex flex-wrap gap-2 justify-center" id="gallery">
{packageData.data?.gallery.map(item => (
<Modal.Trigger key={item.url}>
<Pressable>
<div className="rounded-md overflow-hidden bg-component-bg-pressed outline-0 h-64 flex flex-col relative" aria-label='Loaders'>
<div aria-label="Loaders" className="rounded-md overflow-hidden bg-component-bg-pressed outline-0 h-64 flex flex-col relative">
<img className="h-full" src={item.thumbnail_url} />
{item.title && <div className="absolute w-full bottom-0 bg-component-bg-disabled/80 p-2">{item.title}</div>}
</div>
@ -113,17 +115,17 @@ function RouteComponent() {
function getPackageUrl(pkg: ManagedPackage): string {
switch (pkg.provider) {
case 'Modrinth': return `https://modrinth.com/project/${pkg.slug}`
case 'Curseforge': return `https://www.curseforge.com/minecraft/${pkg.package_type}s/${pkg.slug}`
case 'SkyClient': return ``
case 'Modrinth': return `https://modrinth.com/project/${pkg.slug}`;
case 'Curseforge': return `https://www.curseforge.com/minecraft/${pkg.package_type}s/${pkg.slug}`;
case 'SkyClient': return ``;
}
}
function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
const { provider } = Route.useParams();
const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, []);
const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, []);
const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, [pkg.created]);
const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, [pkg.updated]);
const authors = useCommand('getUsersFromAuthor', () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author));
@ -163,18 +165,32 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
<div className="flex flex-col rounded-lg bg-component-bg p-3">
<h4 className="text-fg-primary font-bold">Links</h4>
<div className='flex flex-col'>
<CustomA href={getPackageUrl(pkg)} includeIcon className='text-link hover:text-link-hover'>
<div className="flex flex-col">
<CustomA className="text-link hover:text-link-hover" href={getPackageUrl(pkg)} includeIcon>
{provider}
{' '}
Page
</CustomA>
{(Object.entries(pkg.links) as [keyof typeof pkg.links, typeof pkg.links[keyof typeof pkg.links]][]).filter(a => a[1]).map(link =>
typeof link[1] == "string"
? <CustomA href={link[1]} children={upperFirst(link[0])} className='text-link hover:text-link-hover' includeIcon />
: (link[1] as PackageDonationUrl[]).map(donationLink => <CustomA href={donationLink.url} className='text-link hover:text-link-hover' children={upperFirst(donationLink.id)} includeIcon />)
)}
{(Object.entries(pkg.links) as Array<[keyof typeof pkg.links, typeof pkg.links[keyof typeof pkg.links]]>).filter(a => a[1]).map(link =>
typeof link[1] == 'string'
? (
<CustomA
children={upperFirst(link[0])}
className="text-link hover:text-link-hover"
href={link[1]}
includeIcon
key={link[0]}
/>
)
: (link[1] as Array<PackageDonationUrl>).map(donationLink => (
<CustomA
children={upperFirst(donationLink.id)}
className="text-link hover:text-link-hover"
href={donationLink.url}
includeIcon
key={donationLink.id}
/>
)))}
</div>
</div>
@ -232,26 +248,33 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
function InstallButton() {
const triggerRef = useRef<HTMLDivElement>(null);
const { provider, slug } = Route.useParams();
if(!includes(PROVIDERS, provider)) throw new Error("invalid provider")
if (!includes(PROVIDERS, provider))
throw new Error('invalid provider');
const [open, setOpen] = useState(false);
const clusters = useClusters();
const browserContext = useBrowserContext();
const { data: versions } = usePackageVersions(provider, slug, {
const { data: versions, isLoading: versionsLoading } = usePackageVersions(provider, slug, {
mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : [],
loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : [],
limit: 1
limit: 1,
});
const version = useMemo(() => {
if (!versions || !browserContext.cluster)
return undefined;
return versions.items[0]
return versions.items[0];
}, [browserContext.cluster, versions]);
function download() {
if (!version || !browserContext.cluster || !includes(PROVIDERS, provider))
return false;
downloadPackage(browserContext.cluster, provider, version);
}
const download = useDownloadPackage(browserContext.cluster!, provider, version!, true);
const downloadRef = useRef(download);
useEffect(() => {
downloadRef.current.reset();
}, [browserContext.cluster]);
// function download() {
// if (!version || !browserContext.cluster || !includes(PROVIDERS, provider))
// return false;
// downloadPackage(browserContext.cluster, provider, version);
// }
return (
@ -259,24 +282,39 @@ function InstallButton() {
<Button
className="max-w-full flex-1 rounded-r-none disabled:text-white/50 disabled:bg-blue-900"
color="primary"
isDisabled={!version}
onClick={download}
isDisabled={!version || download.isPending || download.isSuccess}
onClick={() => download.mutate()}
>
<Download01Icon />
<div className="w-full text-sm">
{
browserContext.cluster
<Show when={download.isIdle}>
{browserContext.cluster
? version
? (
<span>
Download latest to
<br />
<span className="text-md font-semibold">{browserContext.cluster.name}</span>
</span>
)
: `No matching version found`
: 'Select a Cluster'
}
<span>
Download latest to
<br />
<span className="text-md font-semibold">{browserContext.cluster.name}</span>
</span>
)
: versionsLoading ? 'Looking for a version...' : 'No matching version found'
: 'Select a Cluster'}
</Show>
<Show when={download.isPending}>
<span>
Downloading latest to
<br />
<span className="text-md font-semibold">{browserContext.cluster?.name ?? 'Unknown'}</span>
</span>
</Show>
<Show when={download.isSuccess}>
<span>
Installed latest into
<br />
<span className="text-md font-semibold">{browserContext.cluster?.name ?? 'Unknown'}</span>
</span>
</Show>
</div>
</Button>
@ -289,7 +327,7 @@ function InstallButton() {
aria-label="cluster" isOpen={open}
onOpenChange={setOpen}
onSelectionChange={(e) => {
const cluster = clusters?.find(cluster => cluster.id as unknown as number === e);
const cluster = clusters?.find(item => item.id as unknown as number === e);
if (cluster)
browserContext.setCluster(cluster);
}}
@ -359,52 +397,57 @@ function colorForType(type: string) {
}
}
function Versions(){
const {provider, slug} = Route.useParams()
if(!includes(PROVIDERS, provider)) throw new Error("invalid provider")
const [offset, setOffset] = useState(0)
function Versions() {
const { provider, slug } = Route.useParams();
if (!includes(PROVIDERS, provider))
throw new Error('invalid provider');
const [offset, setOffset] = useState(0);
const { data: versions } = usePackageVersions(provider, slug, {
limit: 20,
offset
offset,
});
const pagination = usePagination({
itemsCount: versions?.total as unknown as number,
itemsPerPage: versions?.limit as unknown as number
})
useEffect(()=>{
setOffset(pagination.offset)
},[pagination.offset])
useEffect(()=>{
pagination.reset()
},[versions?.total])
return <div className='flex flex-col'>
<div className='flex justify-between'>
<pagination.Navigation/>
itemsPerPage: versions?.limit as unknown as number,
});
const paginationRef = useRef(pagination);
useEffect(() => {
setOffset(pagination.offset);
}, [pagination.offset]);
useEffect(() => {
paginationRef.current.reset();
}, [versions?.total]);
return (
<div className="flex flex-col w-full">
<div className="flex justify-between">
<pagination.Navigation />
</div>
<Table className="border-separate border-spacing-x-none border-spacing-y-1 w-full">
<TableHeader className="text-left">
<Column className="pr-2" />
<Column className="pr-4" isRowHeader>Name</Column>
<Column className="pr-4">Game Versions</Column>
<Column className="pr-4">Loaders</Column>
<Column className="pr-4">Created</Column>
<Column className="pr-4">Downloads</Column>
<Column className="pr-4" />
</TableHeader>
<TableBody className="">
{versions?.items.map(item =>
<VersionRow key={item.version_id} version={item} />)}
</TableBody>
</Table>
</div>
<Table className="border-separate border-spacing-x-none border-spacing-y-1">
<TableHeader className="text-left">
<Column className="pr-2"/>
<Column className="pr-4" isRowHeader>Name</Column>
<Column className="pr-4">Game Versions</Column>
<Column className="pr-4">Loaders</Column>
<Column className="pr-4">Created</Column>
<Column className="pr-4">Downloads</Column>
<Column className="pr-4"/>
</TableHeader>
<TableBody className="">
{versions?.items.map(item=>
<VersionRow version={item}/>
)}
</TableBody>
</Table>
</div>
);
}
function VersionRow({version}:{version:ManagedVersion}){
const {provider} = Route.useParams()
if(!includes(PROVIDERS, provider)) throw new Error("invalid provider")
function VersionRow({ version }: { version: ManagedVersion }) {
const { provider } = Route.useParams();
if (!includes(PROVIDERS, provider))
throw new Error('invalid provider');
return (
<Row className="my-2 bg-page-elevated px-4 [&>td]:py-4">
<Row className="my-2 bg-page-elevated px-4 [&>td]:py-4 w-full">
<Cell className="p-4 my-2 bg-component-bg rounded-l-xl">
<Tooltip text={upperFirst(version.release_type)}>
<div className={`${colorForType(version.release_type)} h-8 w-8 flex items-center justify-center rounded-md`}>
@ -413,22 +456,22 @@ function VersionRow({version}:{version:ManagedVersion}){
</Tooltip>
</Cell>
<Cell className="p-4 px-2 bg-component-bg">{version.display_name}</Cell>
<Cell className="p-4 pl-0 bg-component-bg">{version.mc_versions.join(", ")}</Cell>
<Cell className="p-4 pl-0 bg-component-bg">{version.loaders.map(upperFirst).join(", ")}</Cell>
<Cell className="p-4 pl-0 bg-component-bg">{version.mc_versions.join(', ')}</Cell>
<Cell className="p-4 pl-0 bg-component-bg">{version.loaders.map(upperFirst).join(', ')}</Cell>
<Cell className="p-4 pl-0 bg-component-bg">{formatAsRelative(Date.parse(version.published))}</Cell>
<Cell className="p-4 pl-0 bg-component-bg">{version.downloads}</Cell>
<Cell className="p-4 pl-0 bg-component-bg rounded-r-xl">
<Modal.Trigger>
<Button
size='icon'
color='secondary'
<Button
children={<Download01Icon />}
color="secondary"
size="icon"
/>
<ChooseClusterModal confirmText='Download' onSelected={cluster=>downloadPackage(cluster, provider, version, true)}/>
<ChooseClusterModal confirmText="Download" onSelected={cluster => downloadPackage(cluster, provider, version, true)} />
</Modal.Trigger>
</Cell>
</Row>
)
);
}
function downloadPackage(cluster: ClusterModel, provider: Provider, version: ManagedVersion, skipCompatibility = false) {

View file

@ -1,11 +1,12 @@
import type { Filters } from '@/bindings.gen';
import type { PropsWithChildren } from 'react';
import ProviderIcon from '@/components/content/ProviderIcon';
import { BrowserProvider, useBrowserContext } from '@/hooks/useBrowser';
import { useClusters } from '@/hooks/useCluster';
import { PROVIDERS } from '@/utils';
import { browserCategories } from '@/utils/browser';
import { AnimatedOutlet, Dropdown, Show, TextField } from '@onelauncher/common/components';
import { createFileRoute, Outlet } from '@tanstack/react-router';
import { createFileRoute } from '@tanstack/react-router';
import { SearchMdIcon } from '@untitled-theme/icons-react';
import { useEffect, useState } from 'react';
@ -27,10 +28,10 @@ function RouteComponent() {
);
}
export function BrowserLayout(props: any) {
export function BrowserLayout({ children }: PropsWithChildren) {
return (
<div className="relative h-full flex flex-1 flex-col items-center gap-2">
<div className="h-full w-full max-w-screen-2xl flex flex-1 flex-col items-center gap-y-2">
<div className="h-full w-full flex flex-1 flex-col items-center gap-y-2">
<div className="grid grid-cols-[220px_auto_220px] w-full gap-x-6">
<div />
<BrowserToolbar />
@ -42,7 +43,7 @@ export function BrowserLayout(props: any) {
<div className="h-full flex flex-col gap-y-4">
<div className="h-full flex-1">
{props.children}
{children}
</div>
</div>
@ -56,25 +57,30 @@ export function BrowserLayout(props: any) {
function BrowserSidebar() {
const context = useBrowserContext();
const clusters = useClusters();
useEffect(() => {
const game_versions = context.cluster?.mc_version ? [context.cluster.mc_version] : null;
const loaders = context.cluster?.mc_loader ? [context.cluster.mc_loader] : null;
context.setQuery({ ...context.query, filters: { ...defaultFilters, ...context.query.filters, game_versions, loaders } });
// eslint-disable-next-line react-hooks/exhaustive-deps -- loop
}, [context.cluster]);
return (
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-1">
<h6 className="my-1 uppercase opacity-60">Active Cluster</h6>
<Dropdown
onSelectionChange={(id) => {
const cluster = clusters?.find(cluster => cluster.id.toString() === id);
onSelectionChange={(index) => {
if (!clusters)
return;
const cluster = clusters[index as number];
context.setCluster(cluster);
}}
selectedKey={context.cluster?.id.toString()}
selectedKey={context.cluster ? clusters?.indexOf(context.cluster) : undefined}
>
{clusters?.map(cluster => (
<Dropdown.Item id={cluster.id.toString()} key={cluster.id}>
{clusters?.map((cluster, index) => (
<Dropdown.Item id={index} key={cluster.id}>
{cluster.name}
</Dropdown.Item>
))}
@ -107,7 +113,7 @@ const defaultFilters: Filters = {
function BrowserCategories() {
const context = useBrowserContext();
const categories = browserCategories.byPackageType((context.query.filters?.package_types ?? ["mod"])[0], context.provider);
const categories = browserCategories.byPackageType((context.query.filters?.package_types ?? ['mod'])[0], context.provider);
function switchCategory(category: string) {
const newCategories = context.query.filters?.categories?.includes(category)

View file

@ -1,11 +1,9 @@
import type { Paginated, SearchResult } from '@/bindings.gen';
import type { PaginationOptions } from '@/hooks/usePagination';
import { PackageGrid } from '@/components/content/PackageItem';
import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser';
import usePagination from '@/hooks/usePagination';
import { Show } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { BrowserLayout } from './route';
export const Route = createFileRoute('/app/browser/search')({
@ -25,10 +23,12 @@ function RouteComponent() {
function Search() {
const context = useBrowserContext();
const search = useBrowserSearch(context.provider, context.query, {
});
useEffect(() => {
search.refetch();
}, [context.provider, context.query, context.cluster]);
}, [context.provider, context.query, context.cluster, search]);
return (
<>
@ -44,19 +44,25 @@ function Search() {
function Results({ results }: { results: Paginated<SearchResult> }) {
const context = useBrowserContext();
const [oldTotal, setOldTotal] = useState(results.total);
const pagination = usePagination({
itemsCount: results.total as unknown as number,
itemsPerPage: context.query.limit as unknown as number,
});
const contextRef = useRef(context);
useEffect(() => {
context.setQuery({ ...context.query, offset: pagination.offset as unknown as bigint });
}, [pagination.page]);
contextRef.current.setQuery(query => ({ ...query, offset: pagination.offset as unknown as bigint }));
}, [pagination.offset, pagination.page]);
useEffect(() => {
if (oldTotal === results.total)
return;
pagination.reset();
setOldTotal(results.total);
}, [results.total]);
}, [oldTotal, pagination, results.total]);
return (
<div>
<div className="w-full flex justify-end my-2">

View file

@ -48,9 +48,7 @@ function RouteComponent() {
})();
}, [cluster.data?.folder_name]);
const openClusterDir = async () => {
openPath(launcherDir);
};
const openClusterDir = () => openPath(launcherDir);
async function handleEditMode() {
if (edit)

View file

@ -5,7 +5,7 @@ import { NewClusterCreate } from '@/components/launcher/cluster/ClusterCreation'
import { useRecentCluster } from '@/hooks/useCluster';
import { bindings } from '@/main';
import { formatAsDuration, upperFirst } from '@/utils';
import { useCommand, useCommandMut, useCommandSuspense } from '@onelauncher/common';
import { useCommand, useCommandMut } from '@onelauncher/common';
import { Button, Show } from '@onelauncher/common/components';
import { createFileRoute, Link } from '@tanstack/react-router';
import { convertFileSrc } from '@tauri-apps/api/core';

View file

@ -23,9 +23,7 @@ function RouteComponent() {
})();
}, []);
const openLauncherDir = async () => {
openPath(launcherDir);
};
const openLauncherDir = () => openPath(launcherDir);
const [discordRpc, setDiscordRpc] = createSetting('discord_enabled', settings?.discord_enabled);

View file

@ -86,27 +86,28 @@ function Sidebar(props: SidebarProps) {
return (
<div className="w-56 flex flex-col pr-2">
{Object.keys(links).map((section, i) => (
<div className="flex flex-col gap-y-2" key={i}>
{Object.keys(links).map(section => (
<div className="flex flex-col gap-y-2" key={section}>
<div>
<p className="m-1.5 mt-5 text-xs text-fg-secondary font-medium">{section.toUpperCase()}</p>
<div className="flex flex-col gap-y-1 fill-fg-primary text-fg-primary font-medium">
{links[section].map((link, i) => {
// eslint yaps too much tbh
if (!link)
{links[section].map((entry) => {
if (!entry)
return '';
const [Component, label, link, params] = entry;
return (
<a
className={
`px-3 py-1 rounded-md text-md border hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link[2], link[3]) ? 'bg-component-bg border-component-bg-pressed' : 'border-transparent'}`
`px-3 py-1 rounded-md text-md border hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link, params) ? 'bg-component-bg border-component-bg-pressed' : 'border-transparent'}`
}
key={i}
onClick={() => goto(link[2], link[3])}
key={link}
onClick={() => goto(link, params)}
>
<span className="flex flex-row items-center gap-x-3 *:w-5">
{link[0]}
{link[1]}
{Component}
{label}
</span>
</a>
);

View file

@ -104,7 +104,7 @@ function RouteComponent() {
setIsForwardButtonEnabled(true);
}, [currentStepIndex, tasksStage, tasksCompleted]);
const next = useCallback(async () => {
const next = useCallback(() => {
const isLast = currentStepIndex === steps.length - 1;
if (isLast) {
@ -140,14 +140,17 @@ function RouteComponent() {
setTasksMessage('Starting setup...');
const tasksToRun = [
async () => {
() => new Promise<void>((resolve) => {
setTasksMessage(`Setting language to ${language?.lang}...`);
console.log('Language task completed for:', language?.lang);
},
async () => {
resolve();
}),
() => new Promise<void>((resolve) => {
if (importInstancesMap.size === 0) {
console.log('No instances selected for import.');
resolve();
return;
}
@ -165,7 +168,9 @@ function RouteComponent() {
throw e;
}
}
},
resolve();
}),
];
const results = await Promise.allSettled(tasksToRun.map(task => task()));

View file

@ -1,4 +1,4 @@
import type { PackageType, Provider } from "@/bindings.gen";
import type { PackageType, Provider } from '@/bindings.gen';
export const BROWSER_VIEWS: Array<string> = ['grid', 'list'] as const;

View file

@ -41,12 +41,12 @@ export function int32ToBigInt([high, low]: [number, number]): bigint {
}
export function randomString(len: number = 6, charSet: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
let randomString = '';
let str = '';
for (let i = 0; i < len; i++) {
const randomPos = Math.floor(Math.random() * charSet.length);
randomString += charSet.substring(randomPos, randomPos + 1);
str += charSet.substring(randomPos, randomPos + 1);
}
return randomString;
return str;
}
export function abbreviateNumber(n: number | bigint, locale: string = 'en-US'): string {

View file

@ -27,5 +27,5 @@
"skipLibCheck": true,
"noUncheckedSideEffectImports": true
},
"include": ["**/*.ts", "**/*.tsx", "vite.config.js"]
"include": ["**/*.ts", "**/*.tsx", "vite.config.ts"]
}

View file

@ -23,6 +23,22 @@ export default defineConfig(
return config;
}),
{
ignores: [
'apps/onelauncher/old_frontend',
'apps/*/desktop/capabilities',
'apps/*/desktop/gen',
'**/*.gen.*',
],
// overrides: [
// {
// files: ['vite.config.js'],
// parserOptions: {
// project: null, // disables type-aware linting for this file
// },
// },
// ],
},
{
rules: {
'sort-imports': 'off', // Replaced by perfectionist/sort-named-imports',
@ -31,9 +47,16 @@ export default defineConfig(
'ts/no-use-before-define': 'off',
'style/function-paren-newline': ['error', 'consistent'],
'react/no-context-provider': 'off',
"prefer-const": "off", // disabled due to maximum call stack size,
"style/jsx-one-expression-per-line": ["error", { allow: "non-jsx" }],
"unused-imports/no-unused-imports": "off"
'prefer-const': 'off', // disabled due to maximum call stack size,
'style/jsx-one-expression-per-line': ['error', { allow: 'non-jsx' }],
'unused-imports/no-unused-imports': 'off',
'eslint-comments/no-unlimited-disable': 'off',
'eslint-comments/require-description': ['error', {
ignore: [
'eslint-disable',
],
}],
'no-shadow': 'off',
},
},
);

14
lint-staged.config.mjs Normal file
View file

@ -0,0 +1,14 @@
// @ts-check
/**
* @type {import('lint-staged').Configuration}
*/
const config = {
'*.ts': 'eslint',
'*.{js,mjs,cjs}': 'eslint', // handle JS, MJS, CJS in one fell swoop
'*.rs': [
'rustfmt', // use rustfmt to only check the files provided by lint-staged
// 'cargo clippy -- -D warnings', // sadly clippy can't do this
],
};
export default config;

View file

@ -23,8 +23,7 @@
"scripts": {
"prep": "pnpm scripts prep",
"typecheck": "pnpm -r typecheck",
"bootstrap": "cargo clean && ./packages/scripts/setup.sh && pnpm install && pnpm prep && pnpm codegen",
"bootstrap": "cargo clean && ./packages/scripts/setup.sh && pnpm install && pnpm prep",
"core": "pnpm --filter @onelauncher/core -- ",
"migrate": "sea-orm-cli migrate -d ./packages/migration",
"entity:gen": "sea-orm-cli generate entity -o packages/entity/src/entities --with-serde both --model-extra-attributes 'onelauncher_macro::specta' -t",
@ -34,13 +33,13 @@
"oneclient:desktop": "pnpm --filter @oneclient/desktop -- ",
"web_common": "pnpm --filter @onelauncher/common -- ",
"scripts": "pnpm --filter @onelauncher/scripts -- ",
"format": "pnpm format:es && pnpm format:rs",
"format:es": "pnpm lint:es --fix",
"format:rs": "cargo +nightly fmt && cargo clippy --fix",
"lint": "pnpm lint:es && pnpm lint:rs",
"lint:es": "eslint . --cache --flag unstable_ts_config",
"lint:rs": "cargo +nightly fmt --check && cargo clippy -- -D warnings"
"lint:es": "eslint . --cache",
"lint:rs": "cargo +nightly fmt --check && cargo clippy -- -D warnings",
"prepare": "husky"
},
"workspaces": {
"packages": [
@ -54,6 +53,8 @@
"@tanstack/eslint-config": "catalog:",
"@types/node": "catalog:",
"eslint": "catalog:",
"husky": "^9.1.7",
"lint-staged": "^16.1.2",
"rimraf": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:",

View file

@ -1,9 +1,15 @@
use std::path::Path;
use onelauncher_entity::{cluster_groups, clusters, icon::Icon, loader::GameLoader};
use sea_orm::{ActiveValue::Set, IntoActiveModel, prelude::*};
use onelauncher_entity::icon::Icon;
use onelauncher_entity::loader::GameLoader;
use onelauncher_entity::{cluster_groups, clusters};
use sea_orm::ActiveValue::Set;
use sea_orm::IntoActiveModel;
use sea_orm::prelude::*;
use crate::{error::{DaoError, LauncherResult}, store::State, utils::io};
use crate::error::{DaoError, LauncherResult};
use crate::store::State;
use crate::utils::io;
pub type ClusterId = i64;
@ -35,11 +41,10 @@ pub async fn insert_cluster(
}
/// Updates an existing cluster in the database.
pub async fn update_cluster_by_id<B>(
id: ClusterId,
block: B,
) -> LauncherResult<clusters::Model>
where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveModel> {
pub async fn update_cluster_by_id<B>(id: ClusterId, block: B) -> LauncherResult<clusters::Model>
where
B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveModel>,
{
let state = State::get().await?;
let db = &state.db;
@ -54,7 +59,9 @@ pub async fn update_cluster<B>(
cluster: &mut clusters::Model,
block: B,
) -> LauncherResult<&mut clusters::Model>
where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveModel> {
where
B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult<clusters::ActiveModel>,
{
let state = State::get().await?;
let db = &state.db;
@ -72,9 +79,7 @@ pub async fn delete_cluster_by_id(id: ClusterId) -> LauncherResult<()> {
let state = State::get().await?;
let db = &state.db;
let deleted = clusters::Entity::delete_by_id(id)
.exec(db)
.await?;
let deleted = clusters::Entity::delete_by_id(id).exec(db).await?;
if deleted.rows_affected == 0 {
return Err(DaoError::NotFound.into());
@ -88,15 +93,15 @@ pub async fn get_cluster_by_id(id: ClusterId) -> LauncherResult<Option<clusters:
let state = State::get().await?;
let db = &state.db;
let model = clusters::Entity::find_by_id(id)
.one(db)
.await?;
let model = clusters::Entity::find_by_id(id).one(db).await?;
Ok(model)
}
/// Gets a cluster by its path from the database.
pub async fn get_cluster_by_folder_name(folder_name: &Path) -> LauncherResult<Option<clusters::Model>> {
pub async fn get_cluster_by_folder_name(
folder_name: &Path,
) -> LauncherResult<Option<clusters::Model>> {
let state = State::get().await?;
let db = &state.db;
@ -126,29 +131,26 @@ pub async fn get_all_clusters() -> LauncherResult<Vec<clusters::Model>> {
let state = State::get().await?;
let db = &state.db;
let clusters = clusters::Entity::find()
.all(db)
.await?;
let clusters = clusters::Entity::find().all(db).await?;
Ok(clusters)
}
/// Gets all clusters grouped by their group from the database.
pub async fn get_clusters_grouped() -> LauncherResult<Vec<(cluster_groups::Model, Vec<clusters::Model>)>> {
pub async fn get_clusters_grouped()
-> LauncherResult<Vec<(cluster_groups::Model, Vec<clusters::Model>)>> {
let state = State::get().await?;
let db = &state.db;
let groups = cluster_groups::Entity::find()
.all(db)
.await?;
let groups = cluster_groups::Entity::find().all(db).await?;
let clusters = groups.load_many(clusters::Entity, db)
.await?;
let clusters = groups.load_many(clusters::Entity, db).await?;
let mut grouped = Vec::new();
for (index, group) in groups.iter().enumerate() {
let group_clusters: Vec<clusters::Model> = clusters.get(index).cloned().unwrap_or_else(Vec::new);
let group_clusters: Vec<clusters::Model> =
clusters.get(index).cloned().unwrap_or_else(Vec::new);
grouped.push((group.clone(), group_clusters));
}
@ -157,7 +159,8 @@ pub async fn get_clusters_grouped() -> LauncherResult<Vec<(cluster_groups::Model
}
/// Caches an icon from a file path and updates the cluster to use the cached icon.
/// This function copies the image to the icon cache directory and sets the cluster's icon_url
///
/// This function copies the image to the icon cache directory and sets the cluster's `icon_url`
/// to reference the cached version.
pub async fn set_cluster_icon_by_path(
cluster: &mut clusters::Model,
@ -171,27 +174,38 @@ pub async fn set_cluster_icon_by_path(
let image_bytes = io::read(icon_path).await?;
let hash = crate::utils::crypto::HashAlgorithm::Sha1.hash(&image_bytes).await?;
let hash = crate::utils::crypto::HashAlgorithm::Sha1
.hash(&image_bytes)
.await?;
let cache_dir = crate::store::Dirs::get_caches_dir().await?.join("icons");
io::create_dir_all(&cache_dir).await?;
let cached_file_path = cache_dir.join(format!("{}.png", hash));
let cached_file_path = cache_dir.join(format!("{hash}.png"));
io::write(&cached_file_path, &image_bytes).await?;
let file_icon = Icon::try_from_path(&cached_file_path)
.ok_or_else(|| anyhow::anyhow!("Failed to create file icon from path: {}", cached_file_path.display()))?;
let file_icon = Icon::try_from_path(&cached_file_path).ok_or_else(|| {
anyhow::anyhow!(
"Failed to create file icon from path: {}",
cached_file_path.display()
)
})?;
update_cluster(cluster, |mut active_model: clusters::ActiveModel| async move {
active_model.icon_url = Set(Some(file_icon));
Ok(active_model)
}).await?;
update_cluster(
cluster,
|mut active_model: clusters::ActiveModel| async move {
active_model.icon_url = Set(Some(file_icon));
Ok(active_model)
},
)
.await?;
Ok(())
}
/// Caches an icon from a file path and updates the cluster by ID to use the cached icon.
/// This function copies the image to the icon cache directory and sets the cluster's icon_url
///
/// This function copies the image to the icon cache directory and sets the cluster's `icon_url`
/// to reference the cached version.
pub async fn set_icon_by_id(
cluster_id: ClusterId,
@ -205,21 +219,31 @@ pub async fn set_icon_by_id(
let image_bytes = io::read(icon_path).await?;
let hash = crate::utils::crypto::HashAlgorithm::Sha1.hash(&image_bytes).await?;
let hash = crate::utils::crypto::HashAlgorithm::Sha1
.hash(&image_bytes)
.await?;
let cache_dir = crate::store::Dirs::get_caches_dir().await?.join("icons");
io::create_dir_all(&cache_dir).await?;
let cached_file_path = cache_dir.join(format!("{}.png", hash));
let cached_file_path = cache_dir.join(format!("{hash}.png"));
io::write(&cached_file_path, &image_bytes).await?;
let file_icon = Icon::try_from_path(&cached_file_path)
.ok_or_else(|| anyhow::anyhow!("Failed to create file icon from path: {}", cached_file_path.display()))?;
let file_icon = Icon::try_from_path(&cached_file_path).ok_or_else(|| {
anyhow::anyhow!(
"Failed to create file icon from path: {}",
cached_file_path.display()
)
})?;
let updated_cluster = update_cluster_by_id(cluster_id, |mut active_model: clusters::ActiveModel| async move {
active_model.icon_url = Set(Some(file_icon));
Ok(active_model)
}).await?;
let updated_cluster = update_cluster_by_id(
cluster_id,
|mut active_model: clusters::ActiveModel| async move {
active_model.icon_url = Set(Some(file_icon));
Ok(active_model)
},
)
.await?;
Ok(updated_cluster)
}

View file

@ -1,6 +1,8 @@
//! Authentication flow manager
use crate::{error::LauncherResult, store::{credentials::{MinecraftCredentials, MinecraftLogin}, State}};
use crate::error::LauncherResult;
use crate::store::State;
use crate::store::credentials::{MinecraftCredentials, MinecraftLogin};
/// Begin a Microsoft authentication flow.
#[tracing::instrument]
@ -77,15 +79,13 @@ pub async fn get_user(user: uuid::Uuid) -> LauncherResult<Option<MinecraftCreden
let state = State::get().await?;
let store = state.credentials.read().await;
let user = store
.users
.get(&user)
.cloned();
let user = store.users.get(&user).cloned();
Ok(user)
}
/// Get a fake user for testing or offline mode.
#[must_use]
pub fn get_fake_user() -> MinecraftCredentials {
MinecraftCredentials {
id: uuid::Uuid::new_v4(),
@ -94,4 +94,4 @@ pub fn get_fake_user() -> MinecraftCredentials {
refresh_token: "0".to_string(),
expires: chrono::Utc::now() + chrono::Duration::days(1),
}
}
}

View file

@ -3,7 +3,7 @@
use std::collections::{HashMap, HashSet};
use std::path::Path;
use futures::{pin_mut, AsyncReadExt, TryStreamExt};
use futures::{AsyncReadExt, TryStreamExt, pin_mut};
use interpulse::api::minecraft::{Argument, ArgumentValue, Library, VersionType};
use interpulse::api::modded::SidedDataEntry;
use interpulse::utils::get_path_from_artifact;
@ -11,8 +11,8 @@ use onelauncher_entity::resolution::Resolution;
use crate::constants::{self, DUMMY_REPLACE_NEWLINE};
use crate::error::LauncherResult;
use crate::store::credentials::MinecraftCredentials;
use crate::store::Core;
use crate::store::credentials::MinecraftCredentials;
use crate::utils::io;
#[allow(clippy::too_many_arguments)]
@ -178,23 +178,31 @@ pub async fn main_class(path: String) -> LauncherResult<Option<String>> {
let mut entry_reader = match reader.reader_without_entry(index).await {
Ok(reader) => reader,
Err(err) => {
tracing::error!("failed to get entry reader for {}: {}", entry.filename().as_str().unwrap_or_default(), err);
tracing::error!(
"failed to get entry reader for {}: {}",
entry.filename().as_str().unwrap_or_default(),
err
);
continue;
}
};
if let Err(err) = entry_reader.read_to_string(&mut buf).await {
tracing::error!("failed to read entry {}: {}", entry.filename().as_str().unwrap_or_default(), err);
tracing::error!(
"failed to read entry {}: {}",
entry.filename().as_str().unwrap_or_default(),
err
);
continue;
}
for line in buf.lines() {
let line = line.trim();
if line.starts_with("Main-Class:") {
if let Some(class) = line.split(':').nth(1) {
class_name = Some(class.trim().to_string());
break;
}
if line.starts_with("Main-Class:")
&& let Some(class) = line.split(':').nth(1)
{
class_name = Some(class.trim().to_string());
break;
}
}
}
@ -282,9 +290,10 @@ pub fn classpaths(
.iter()
.filter_map(|lib| {
if let Some(rules) = &lib.rules
&& !super::rules::validate_rules(rules, java_arch, updated) {
return None;
}
&& !super::rules::validate_rules(rules, java_arch, updated)
{
return None;
}
if !lib.include_in_classpath {
return None;
@ -294,11 +303,7 @@ pub fn classpaths(
})
.collect::<Result<HashSet<_>, _>>()?;
classpaths.insert(
io::canonicalize(client_path)?
.to_string_lossy()
.to_string(),
);
classpaths.insert(io::canonicalize(client_path)?.to_string_lossy().to_string());
Ok(classpaths
.into_iter()

View file

@ -6,7 +6,7 @@ use interpulse::api::modded::LoaderVersion;
use onelauncher_entity::loader::GameLoader;
use reqwest::Method;
use crate::api::ingress::{init_ingress, IngressSendExt};
use crate::api::ingress::{IngressSendExt, init_ingress};
use crate::error::{LauncherError, LauncherResult};
use crate::store::ingress::{IngressType, SubIngress, SubIngressExt};
use crate::store::metadata::MetadataError;
@ -27,7 +27,13 @@ pub async fn download_minecraft_ingressed(
)
.await?;
download_minecraft(version, java_arch, Some(&SubIngress::new(&id, 100.0)), force).await
download_minecraft(
version,
java_arch,
Some(&SubIngress::new(&id, 100.0)),
force,
)
.await
}
// MARK: Main
@ -38,7 +44,6 @@ pub async fn download_minecraft(
sub_ingress: Option<&SubIngress<'_>>,
force: Option<bool>,
) -> LauncherResult<()> {
let asset_index = download_assets_index(version, sub_ingress, force).await?;
tokio::try_join! {
download_assets(version.assets == "legacy", asset_index, sub_ingress, force),
@ -81,8 +86,7 @@ pub async fn download_version_info(
);
let mut info =
http::fetch_json_advanced(Method::GET, &version.url, None, None, None, ingress)
.await?;
http::fetch_json_advanced(Method::GET, &version.url, None, None, None, ingress).await?;
if let Some(loader) = loader {
let partial: interpulse::api::modded::PartialVersionInfo =
@ -104,7 +108,14 @@ pub async fn download_version_info(
info
};
ingress.send_ingress(ingress.ingress_total().map(|i| i / TASKS).unwrap_or_default()).await?;
ingress
.send_ingress(
ingress
.ingress_total()
.map(|i| i / TASKS)
.unwrap_or_default(),
)
.await?;
tracing::debug!("loaded minecraft version info for minecraft version {version_id}");
Ok(result)
@ -143,7 +154,9 @@ pub async fn download_assets_index(
&version.asset_index.url,
path,
None,
ingress.map(|i| SubIngress::from_sub(i, ingress_step)).as_ref(),
ingress
.map(|i| SubIngress::from_sub(i, ingress_step))
.as_ref(),
)
.await?,
)?;
@ -440,5 +453,5 @@ pub async fn get_loaders_for_version(mc_version: &str) -> LauncherResult<Vec<Gam
let state = State::get().await?;
let mut metadata = state.metadata.write().await;
Ok(metadata.get_loaders_for_version(mc_version).await?)
metadata.get_loaders_for_version(mc_version).await
}

View file

@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
use crate::api::ingress::{init_ingress, init_ingress_opt, send_ingress};
use crate::error::LauncherResult;
use crate::store::ingress::{IngressType, SubIngress};
use crate::store::Dirs;
use crate::store::ingress::{IngressType, SubIngress};
use crate::utils::http::{fetch_advanced, fetch_json};
use crate::utils::io::{self, IOError};
@ -94,7 +94,6 @@ pub async fn check_java_runtime(
io::write(&file, JAVA_INFO_CLASS).await?;
id.map(async |id| send_ingress(id, 25.0).await);
let java_info = tokio::process::Command::new(absolute_path)
.arg("-cp")
.arg(dir.path())
@ -179,7 +178,10 @@ pub async fn prepare_java(major: u32) -> LauncherResult<java_versions::Model> {
}
}
tracing::warn!("no java installations found on the system, attempting to download java {}", major);
tracing::warn!(
"no java installations found on the system, attempting to download java {}",
major
);
let java_path = install_java_from_major(major).await?;
let java_info = check_java_runtime(&java_path, false).await?;
@ -194,6 +196,20 @@ pub async fn prepare_java(major: u32) -> LauncherResult<java_versions::Model> {
Err(JavaError::MissingJava.into())
}
// share the code that I stole from the windows one anyway to use in UNIX settings
#[cfg(unix)]
fn find_java_in_path(mut found: Vec<PathBuf>) -> Vec<PathBuf> {
if let Ok(path) = std::env::var("PATH") {
for path_entry in path.split(':') {
let java = PathBuf::from(path_entry).join("java");
if java.exists() && java.is_file() && !found.contains(&java) {
found.push(java);
}
}
}
found
}
/// Attempts to scan common paths for Java installations.
///
/// **Can be heavy on I/O, don't run often!**
@ -219,7 +235,6 @@ pub async fn locate_java() -> LauncherResult<HashMap<PathBuf, JavaInfo>> {
tracing::warn!("java installation at '{}' is not valid", path.display());
continue;
};
valid.insert(path, info);
}
@ -299,6 +314,8 @@ fn internal_locate_java() -> Vec<PathBuf> {
}
}
found = find_java_in_path(found);
found
}
@ -333,6 +350,10 @@ fn internal_locate_java() -> Vec<PathBuf> {
}
}
// check env vars
// woowoooo mutation
found = find_java_in_path(found);
found
}
@ -353,12 +374,10 @@ pub async fn get_zulu_packages() -> LauncherResult<Vec<JavaPackage>> {
pub async fn install_java_from_major(version: u32) -> LauncherResult<PathBuf> {
let packages = get_zulu_packages().await?;
let package = packages.into_iter()
let package = packages
.into_iter()
.find(|p| p.java_version.contains(&version))
.ok_or_else(|| anyhow::anyhow!(
"Could not find a java package for version {}",
version
))?;
.ok_or_else(|| anyhow::anyhow!("Could not find a java package for version {}", version))?;
install_java_package(package).await
}
@ -388,19 +407,18 @@ pub async fn install_java_package(package: JavaPackage) -> LauncherResult<PathBu
.await
.map_err(IOError::AsyncZipError)?;
if let Some(entry) = archive.file().entries().first() {
if let Some(dir) = entry
if let Some(entry) = archive.file().entries().first()
&& let Some(dir) = entry
.filename()
.clone()
.into_string()
.unwrap()
.split('/')
.next()
{
let path = java_dir.join(dir);
if path.exists() {
io::remove_dir_all(&path).await?;
}
{
let path = java_dir.join(dir);
if path.exists() {
io::remove_dir_all(&path).await?;
}
}
@ -417,7 +435,12 @@ pub async fn install_java_package(package: JavaPackage) -> LauncherResult<PathBu
#[cfg(target_os = "macos")]
{
let java_version = package.java_version.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(".");
let java_version = package
.java_version
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(".");
base_path = base_path
.join(format!("zulu-{java_version}.jre"))
.join("Contents")

View file

@ -1,5 +1,6 @@
use chrono::{DateTime, Utc};
use onelauncher_entity::{loader::GameLoader, package::{PackageType, Provider}};
use onelauncher_entity::loader::GameLoader;
use onelauncher_entity::package::{PackageType, Provider};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
@ -23,17 +24,21 @@ pub enum Sort {
Relevance,
Downloads,
Newest,
Updated
Updated,
}
impl std::fmt::Display for Sort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
Self::Relevance => "relevance",
Self::Downloads => "downloads",
Self::Newest => "newest",
Self::Updated => "updated",
})
write!(
f,
"{}",
match self {
Self::Relevance => "relevance",
Self::Downloads => "downloads",
Self::Newest => "newest",
Self::Updated => "updated",
}
)
}
}
@ -48,28 +53,28 @@ pub struct SearchQuery {
}
#[onelauncher_macro::specta]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SearchResult {
pub project_id: String,
pub project_type: String,
pub slug: String,
pub author: String,
pub title: String,
pub description: String,
pub categories: Vec<String>,
pub display_categories: Vec<String>,
pub versions: Vec<String>,
pub downloads: usize,
pub icon_url: String,
pub date_created: DateTime<Utc>,
pub date_modified: DateTime<Utc>,
pub latest_version: String,
pub license: String,
pub client_side: PackageSide,
pub server_side: PackageSide,
pub project_id: String,
pub project_type: String,
pub slug: String,
pub author: String,
pub title: String,
pub description: String,
pub categories: Vec<String>,
pub display_categories: Vec<String>,
pub versions: Vec<String>,
pub downloads: usize,
pub icon_url: String,
pub date_created: DateTime<Utc>,
pub date_modified: DateTime<Utc>,
pub latest_version: String,
pub license: String,
pub client_side: PackageSide,
pub server_side: PackageSide,
/// List of URLs to images
pub gallery: Vec<String>,
// pub color: i64,
pub gallery: Vec<String>,
// pub color: i64,
}
#[onelauncher_macro::specta]
@ -98,7 +103,7 @@ pub struct ManagedPackage {
pub links: PackageLinks,
pub status: PackageStatus,
pub downloads: usize,
pub gallery: Vec<PackageGallery>
pub gallery: Vec<PackageGallery>,
}
#[onelauncher_macro::specta]
@ -179,7 +184,7 @@ pub enum PackageSide {
Unsupported,
}
/// https://spdx.org/licenses/
/// <https://spdx.org/licenses/>
#[onelauncher_macro::specta]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageLicense {
@ -188,7 +193,7 @@ pub struct PackageLicense {
pub url: Option<String>,
}
/// https://docs.curseforge.com/rest-api/#tocS_ModLinks
/// <https://docs.curseforge.com/rest-api/#tocS_ModLinks>
#[onelauncher_macro::specta]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageLinks {
@ -214,10 +219,10 @@ pub struct PackageGallery {
pub thumbnail_url: String,
pub title: Option<String>,
pub description: Option<String>,
pub featured: Option<bool>
pub featured: Option<bool>,
}
/// https://api.modrinth.com/v2/tag/donation_platform
/// <https://api.modrinth.com/v2/tag/donation_platform>
#[onelauncher_macro::specta]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
@ -262,4 +267,4 @@ pub enum PackageReleaseType {
Release,
Beta,
Alpha,
}
}

View file

@ -88,7 +88,7 @@ pub async fn download_package(
.await?;
let icon = if let Some(icon_url) = &package.icon_url {
if let Some(icon) = Icon::try_from_url(url::Url::parse(icon_url)?) {
if let Some(icon) = Icon::try_from_url(&url::Url::parse(icon_url)?) {
Some(icon::cache_icon(&icon).await?)
} else {
None
@ -142,12 +142,24 @@ pub async fn link_package(
tracing::trace!("checking compatibility of package with cluster");
if !skip_compatibility.unwrap_or(false) {
if !package.mc_loader.iter().any(|v| cluster.mc_loader.compatible_with(v)) {
return Err(LauncherError::from(PackageError::from(IncompatiblePackageType::Loader)));
if !package
.mc_loader
.iter()
.any(|v| cluster.mc_loader.compatible_with(v))
{
return Err(LauncherError::from(PackageError::from(
IncompatiblePackageType::Loader,
)));
}
if !package.mc_versions.iter().any(|v| cluster.mc_version.contains(v)) {
return Err(LauncherError::from(PackageError::from(IncompatiblePackageType::McVersion)));
if !package
.mc_versions
.iter()
.any(|v| cluster.mc_version.contains(v))
{
return Err(LauncherError::from(PackageError::from(
IncompatiblePackageType::McVersion,
)));
}
}
@ -160,7 +172,9 @@ pub async fn link_package(
io::create_dir_all(&dest_dir).await?;
let dest_pkg = dest_dir.join(package.file_name.as_str());
tokio::fs::hard_link(src_pkg, dest_pkg).await.map_err(IOError::from)?;
tokio::fs::hard_link(src_pkg, dest_pkg)
.await
.map_err(IOError::from)?;
dao::link_package_to_cluster(package, cluster).await?;
@ -174,9 +188,17 @@ pub async fn link_package(
}
fn path_from_model(dirs: &Dirs, model: &packages::Model) -> std::path::PathBuf {
join_package_file(dirs.package_dir(&model.package_type, &model.provider, &model.package_id), &model.version_id, model.file_name.as_str())
join_package_file(
dirs.package_dir(&model.package_type, &model.provider, &model.package_id),
&model.version_id,
model.file_name.as_str(),
)
}
fn join_package_file(path: impl AsRef<std::path::Path>, version_id: &str, file_name: &str) -> std::path::PathBuf {
fn join_package_file(
path: impl AsRef<std::path::Path>,
version_id: &str,
file_name: &str,
) -> std::path::PathBuf {
path.as_ref().join(format!("{version_id}-{file_name}"))
}
}

View file

@ -1,7 +1,11 @@
use tauri::{AppHandle, Manager};
use tracing::{error, warn};
use crate::{api::{proxy::{event::LauncherEvent, message::MessageLevel, LauncherProxy}, tauri::LauncherEventEmitter}, LauncherResult};
use crate::LauncherResult;
use crate::api::proxy::LauncherProxy;
use crate::api::proxy::event::LauncherEvent;
use crate::api::proxy::message::MessageLevel;
use crate::api::tauri::LauncherEventEmitter;
#[derive(Debug)]
pub struct ProxyTauri {
@ -13,7 +17,10 @@ impl ProxyTauri {
#[must_use]
pub fn new(handle: AppHandle) -> Self {
tracing::debug!("using tauri bridge");
tracing::debug!("webview version: {}", tauri::webview_version().unwrap_or("unknown".into()));
tracing::debug!(
"webview version: {}",
tauri::webview_version().unwrap_or_else(|_| "unknown".into())
);
Self {
emitter: LauncherEventEmitter::new(handle.clone()),
@ -27,7 +34,7 @@ impl LauncherProxy for ProxyTauri {
async fn send_event(&self, event: LauncherEvent) -> LauncherResult<()> {
if let LauncherEvent::Message(message) = &event {
match message.level {
MessageLevel::Info => {},
MessageLevel::Info => {}
MessageLevel::Warn => warn!("{}", message.message),
MessageLevel::Error => error!("{}", message.message),
}
@ -59,5 +66,4 @@ impl LauncherProxy for ProxyTauri {
Ok(())
}
}
}
}

View file

@ -1,16 +1,28 @@
use crate::api::packages::data::{
ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery, SearchResult,
};
use crate::api::packages::provider::ProviderExt;
use crate::store::{Settings, State};
use crate::utils::pagination::Paginated;
use interpulse::api::minecraft::Version;
use onelauncher_entity::{icon::Icon, loader::GameLoader, package::Provider, packages, resolution::Resolution};
use onelauncher_entity::icon::Icon;
use onelauncher_entity::loader::GameLoader;
use onelauncher_entity::package::Provider;
use onelauncher_entity::packages;
use onelauncher_entity::resolution::Resolution;
use sea_orm::ActiveValue::Set;
use crate::{api::packages::{data::{ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery, SearchResult}, provider::ProviderExt}, store::{Settings, State}, utils::pagination::Paginated};
use tauri::{AppHandle, Runtime};
use crate::{api::{self, cluster::dao::ClusterId}, error::{LauncherError, LauncherResult}, store::{credentials::MinecraftCredentials, Core}};
use crate::api::cluster::dao::ClusterId;
use crate::api::{self};
use crate::error::LauncherResult;
use crate::store::Core;
use crate::store::credentials::MinecraftCredentials;
use onelauncher_entity::prelude::model::*;
#[taurpc::procedures(path = "core")]
pub trait TauriLauncherApi {
// Clusters
#[taurpc(alias = "getClusters")]
async fn get_clusters() -> LauncherResult<Vec<Cluster>>;
@ -36,7 +48,6 @@ pub trait TauriLauncherApi {
#[taurpc(alias = "getWorlds")]
async fn get_worlds(id: ClusterId) -> LauncherResult<Vec<String>>;
// Setting Profiles
#[taurpc(alias = "getProfileOrDefault")]
async fn get_profile_or_default(name: Option<String>) -> LauncherResult<SettingsProfile>;
@ -45,7 +56,10 @@ pub trait TauriLauncherApi {
async fn get_global_profile() -> SettingsProfile;
#[taurpc(alias = "updateClusterProfile")]
async fn update_cluster_profile(name: String, profile: ProfileUpdate) -> LauncherResult<SettingsProfile>;
async fn update_cluster_profile(
name: String,
profile: ProfileUpdate,
) -> LauncherResult<SettingsProfile>;
// Game Metadata
#[taurpc(alias = "getGameVersions")]
@ -54,7 +68,6 @@ pub trait TauriLauncherApi {
#[taurpc(alias = "getLoadersForVersion")]
async fn get_loaders_for_version(mc_version: String) -> LauncherResult<Vec<GameLoader>>;
// Users
#[taurpc(alias = "getUsers")]
async fn get_users() -> LauncherResult<Vec<MinecraftCredentials>>;
@ -66,13 +79,17 @@ pub trait TauriLauncherApi {
async fn remove_user(uuid: uuid::Uuid) -> LauncherResult<()>;
#[taurpc(alias = "getDefaultUser")]
async fn get_default_user(fallback: Option<bool>) -> LauncherResult<Option<MinecraftCredentials>>;
async fn get_default_user(
fallback: Option<bool>,
) -> LauncherResult<Option<MinecraftCredentials>>;
#[taurpc(alias = "setDefaultUser")]
async fn set_default_user(uuid: Option<uuid::Uuid>) -> LauncherResult<()>;
#[taurpc(alias = "openMsaLogin")]
async fn open_msa_login<R: Runtime>(app_handle: AppHandle<R>) -> LauncherResult<Option<MinecraftCredentials>>;
async fn open_msa_login<R: Runtime>(
app_handle: AppHandle<R>,
) -> LauncherResult<Option<MinecraftCredentials>>;
// Settings
#[taurpc(alias = "readSettings")]
@ -83,28 +100,49 @@ pub trait TauriLauncherApi {
// Packages
#[taurpc(alias = "searchPackages")]
async fn search_packages(provider: Provider, query: SearchQuery) -> LauncherResult<Paginated<SearchResult>>;
async fn search_packages(
provider: Provider,
query: SearchQuery,
) -> LauncherResult<Paginated<SearchResult>>;
#[taurpc(alias = "getPackage")]
async fn get_package(provider: Provider, slug: String) -> LauncherResult<ManagedPackage>;
#[taurpc(alias = "getMultiplePackages")]
async fn get_multiple_packages(provider: Provider, slugs: Vec<String>) -> LauncherResult<Vec<ManagedPackage>>;
async fn get_multiple_packages(
provider: Provider,
slugs: Vec<String>,
) -> LauncherResult<Vec<ManagedPackage>>;
#[taurpc(alias = "getPackageVersions")]
async fn get_package_versions(provider: Provider, slug: String, mc_versions: Option<Vec<String>>, loaders: Option<Vec<GameLoader>>, offset: usize, limit: usize) -> LauncherResult<Paginated<ManagedVersion>>;
async fn get_package_versions(
provider: Provider,
slug: String,
mc_versions: Option<Vec<String>>,
loaders: Option<Vec<GameLoader>>,
offset: usize,
limit: usize,
) -> LauncherResult<Paginated<ManagedVersion>>;
#[taurpc(alias = "getPackageUser")]
async fn get_package_user(provider: Provider, slug: String) -> LauncherResult<ManagedUser>;
#[taurpc(alias = "downloadPackage")]
async fn download_package(provider: Provider, package_id: String, version_id: String, cluster_id: ClusterId, skip_compatibility: Option<bool>) -> LauncherResult<packages::Model>;
async fn download_package(
provider: Provider,
package_id: String,
version_id: String,
cluster_id: ClusterId,
skip_compatibility: Option<bool>,
) -> LauncherResult<packages::Model>;
#[taurpc(alias = "getUsersFromAuthor")]
async fn get_users_from_author(provider: Provider, author: PackageAuthor) -> LauncherResult<Vec<ManagedUser>>;
async fn get_users_from_author(
provider: Provider,
author: PackageAuthor,
) -> LauncherResult<Vec<ManagedUser>>;
}
#[derive(serde::Serialize, serde::Deserialize, specta::Type, Clone)]
pub struct CreateCluster {
name: String,
@ -137,7 +175,6 @@ pub struct TauriLauncherApiImpl;
#[taurpc::resolvers]
impl TauriLauncherApi for TauriLauncherApiImpl {
// Clusters
async fn get_clusters(self) -> LauncherResult<Vec<Cluster>> {
api::cluster::dao::get_all_clusters().await
@ -152,37 +189,54 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
}
async fn create_cluster(self, options: CreateCluster) -> LauncherResult<Cluster> {
let cluster = api::cluster::create_cluster(&options.name, &options.mc_version, options.mc_loader, options.mc_loader_version.as_deref(), options.icon).await?;
api::setting_profiles::create_profile(&options.name, |mut active_model: SettingsProfilePartial| async move {
active_model.force_fullscreen = Set(Some(false));
active_model.mem_max = Set(Some(2048));
Ok(active_model)
})
let cluster = api::cluster::create_cluster(
&options.name,
&options.mc_version,
options.mc_loader,
options.mc_loader_version.as_deref(),
options.icon,
)
.await?;
api::cluster::dao::update_cluster_by_id(cluster.id, |mut active_model: ClusterPartial| async move {
active_model.setting_profile_name = Set(Some(options.name.clone()));
api::setting_profiles::create_profile(
&options.name,
|mut active_model: SettingsProfilePartial| async move {
active_model.force_fullscreen = Set(Some(false));
Ok(active_model)
})
active_model.mem_max = Set(Some(2048));
Ok(active_model)
},
)
.await?;
api::cluster::dao::update_cluster_by_id(
cluster.id,
|mut active_model: ClusterPartial| async move {
active_model.setting_profile_name = Set(Some(options.name.clone()));
Ok(active_model)
},
)
.await?;
Ok(cluster)
}
async fn launch_cluster(self, id: ClusterId, uuid: Option<uuid::Uuid>) -> LauncherResult<()> {
let mut cluster = api::cluster::dao::get_cluster_by_id(id).await?
let mut cluster = api::cluster::dao::get_cluster_by_id(id)
.await?
.ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", id))?;
let uuid = match uuid {
Some(uuid) => uuid,
None => api::credentials::get_default_user().await?.ok_or_else(|| anyhow::anyhow!("no default user set"))?,
None => api::credentials::get_default_user()
.await?
.ok_or_else(|| anyhow::anyhow!("no default user set"))?,
};
let user = api::credentials::get_user(uuid).await?
let user = api::credentials::get_user(uuid)
.await?
.ok_or_else(|| anyhow::anyhow!("user with uuid {} not found", uuid))?;
// let user = api::credentials::get_fake_user();
@ -192,34 +246,42 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
Ok(())
}
async fn update_cluster_by_id(self, id: ClusterId, request: ClusterUpdate) -> LauncherResult<()> {
api::cluster::dao::update_cluster_by_id(id, |mut active_model: ClusterPartial| async move {
if let Some(name) = request.name {
active_model.name = Set(name)
}
async fn update_cluster_by_id(
self,
id: ClusterId,
request: ClusterUpdate,
) -> LauncherResult<()> {
api::cluster::dao::update_cluster_by_id(
id,
|mut active_model: ClusterPartial| async move {
if let Some(name) = request.name {
active_model.name = Set(name);
}
// ok i know this is so wrong but im not a rust guy
if let Some(icon_url) = request.icon_url {
api::cluster::dao::set_icon_by_id(id, &icon_url).await?;
}
// ok i know this is so wrong but im not a rust guy
if let Some(icon_url) = request.icon_url {
api::cluster::dao::set_icon_by_id(id, &icon_url).await?;
}
Ok(active_model)
})
.await
.map_err(|e| LauncherError::from(e))?;
Ok(active_model)
},
)
.await?;
Ok(())
Ok(())
}
async fn get_screenshots(self, id: ClusterId) -> LauncherResult<Vec<String>> {
let cluster = api::cluster::dao::get_cluster_by_id(id).await?
let cluster = api::cluster::dao::get_cluster_by_id(id)
.await?
.ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", id))?;
api::cluster::content::get_screenshots(&cluster).await
}
async fn get_worlds(self, id: ClusterId) -> LauncherResult<Vec<String>> {
let cluster = api::cluster::dao::get_cluster_by_id(id).await?
let cluster = api::cluster::dao::get_cluster_by_id(id)
.await?
.ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", id))?;
api::cluster::content::get_worlds(&cluster).await
@ -235,42 +297,49 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
}
// please kill this with fire
async fn update_cluster_profile(self, name: String, profile: ProfileUpdate) -> LauncherResult<SettingsProfile> {
let profile = api::setting_profiles::dao::update_profile_by_name(&name, |mut active_model: SettingsProfilePartial| async move {
if let Some(res) = profile.res {
active_model.res = Set(Some(res));
}
async fn update_cluster_profile(
self,
name: String,
profile: ProfileUpdate,
) -> LauncherResult<SettingsProfile> {
let profile = api::setting_profiles::dao::update_profile_by_name(
&name,
|mut active_model: SettingsProfilePartial| async move {
if let Some(res) = profile.res {
active_model.res = Set(Some(res));
}
if let Some(force_fullscreen) = profile.force_fullscreen {
active_model.force_fullscreen = Set(Some(force_fullscreen));
}
if let Some(force_fullscreen) = profile.force_fullscreen {
active_model.force_fullscreen = Set(Some(force_fullscreen));
}
if let Some(mem_max) = profile.mem_max {
active_model.mem_max = Set(Some(mem_max));
}
if let Some(mem_max) = profile.mem_max {
active_model.mem_max = Set(Some(mem_max));
}
if let Some(launch_args) = profile.launch_args {
active_model.launch_args = Set(Some(launch_args));
}
if let Some(launch_args) = profile.launch_args {
active_model.launch_args = Set(Some(launch_args));
}
if let Some(launch_env) = profile.launch_env {
active_model.launch_env = Set(Some(launch_env));
}
if let Some(launch_env) = profile.launch_env {
active_model.launch_env = Set(Some(launch_env));
}
if let Some(hook_pre) = profile.hook_pre {
active_model.hook_pre = Set(Some(hook_pre));
}
if let Some(hook_pre) = profile.hook_pre {
active_model.hook_pre = Set(Some(hook_pre));
}
if let Some(hook_wrapper) = profile.hook_wrapper {
active_model.hook_wrapper = Set(Some(hook_wrapper));
}
if let Some(hook_wrapper) = profile.hook_wrapper {
active_model.hook_wrapper = Set(Some(hook_wrapper));
}
if let Some(hook_post) = profile.hook_post {
active_model.hook_post = Set(Some(hook_post));
}
if let Some(hook_post) = profile.hook_post {
active_model.hook_post = Set(Some(hook_post));
}
Ok(active_model)
})
Ok(active_model)
},
)
.await?;
Ok(profile)
@ -285,7 +354,6 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
api::game::metadata::get_game_versions().await
}
// Users
async fn get_users(self) -> LauncherResult<Vec<MinecraftCredentials>> {
api::credentials::get_users().await
@ -299,7 +367,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
api::credentials::remove_user(uuid).await
}
async fn get_default_user(self, fallback: Option<bool>) -> LauncherResult<Option<MinecraftCredentials>> {
async fn get_default_user(
self,
fallback: Option<bool>,
) -> LauncherResult<Option<MinecraftCredentials>> {
let uuid = api::credentials::get_default_user().await?;
if fallback.is_some_and(|fallback| fallback) && uuid.is_none() {
@ -316,7 +387,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
api::credentials::set_default_user(uuid).await
}
async fn open_msa_login<R: Runtime>(self, app_handle: AppHandle<R>) -> LauncherResult<Option<MinecraftCredentials>> {
async fn open_msa_login<R: Runtime>(
self,
app_handle: AppHandle<R>,
) -> LauncherResult<Option<MinecraftCredentials>> {
use tauri::Manager;
let flow = api::credentials::begin().await?;
@ -336,10 +410,10 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
.map_err(|_| anyhow::anyhow!("failed to parse auth redirect url"))?,
),
)
.title(format!("Login to {}", Core::get().launcher_name))
.center()
.focused(true)
.build()?;
.title(format!("Login to {}", Core::get().launcher_name))
.center()
.focused(true)
.build()?;
win.request_user_attention(Some(tauri::UserAttentionType::Critical))?;
@ -352,17 +426,12 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
.url()?
.as_str()
.starts_with("https://login.live.com/oauth20_desktop.srf")
&& let Some((_, code)) = win.url()?.query_pairs().find(|x| x.0 == "code")
{
if let Some((_, code)) = win
.url()?
.query_pairs()
.find(|x| x.0 == "code")
{
win.close()?;
let value = api::credentials::finish(&code.clone(), flow).await?;
win.close()?;
let value = api::credentials::finish(&code.clone(), flow).await?;
return Ok(Some(value));
}
return Ok(Some(value));
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
@ -392,34 +461,66 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
}
// Packages
async fn search_packages(self, provider: Provider, query: SearchQuery) -> LauncherResult<Paginated<SearchResult>> {
Ok(provider.search(&query).await?)
async fn search_packages(
self,
provider: Provider,
query: SearchQuery,
) -> LauncherResult<Paginated<SearchResult>> {
provider.search(&query).await
}
async fn get_package(self, provider: Provider, slug: String) -> LauncherResult<ManagedPackage> {
Ok(provider.get(&slug).await?)
provider.get(&slug).await
}
async fn get_multiple_packages(self, provider: Provider, slugs: Vec<String>) -> LauncherResult<Vec<ManagedPackage>> {
Ok(provider.get_multiple(&slugs).await?)
async fn get_multiple_packages(
self,
provider: Provider,
slugs: Vec<String>,
) -> LauncherResult<Vec<ManagedPackage>> {
provider.get_multiple(&slugs).await
}
async fn get_package_versions(self, provider: Provider, slug: String, mc_versions: Option<Vec<String>>, loaders: Option<Vec<GameLoader>>, offset: usize, limit: usize) -> LauncherResult<Paginated<ManagedVersion>> {
Ok(provider.get_versions_paginated(&slug, mc_versions, loaders, offset, limit).await?)
async fn get_package_versions(
self,
provider: Provider,
slug: String,
mc_versions: Option<Vec<String>>,
loaders: Option<Vec<GameLoader>>,
offset: usize,
limit: usize,
) -> LauncherResult<Paginated<ManagedVersion>> {
provider
.get_versions_paginated(&slug, mc_versions, loaders, offset, limit)
.await
}
async fn get_package_user(self, provider: Provider, slug: String) -> LauncherResult<ManagedUser> {
Ok(provider.get_user(&slug).await?)
async fn get_package_user(
self,
provider: Provider,
slug: String,
) -> LauncherResult<ManagedUser> {
provider.get_user(&slug).await
}
async fn download_package(self, provider: Provider, package_id: String, version_id: String, cluster_id: ClusterId, skip_compatibility: Option<bool>) -> LauncherResult<packages::Model> {
let cluster = api::cluster::dao::get_cluster_by_id(cluster_id).await?
async fn download_package(
self,
provider: Provider,
package_id: String,
version_id: String,
cluster_id: ClusterId,
skip_compatibility: Option<bool>,
) -> LauncherResult<packages::Model> {
let cluster = api::cluster::dao::get_cluster_by_id(cluster_id)
.await?
.ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", cluster_id))?;
let package = provider.get(&package_id).await?;
let versions = provider.get_versions(&[version_id]).await?;
let version = versions.into_iter().next()
let version = versions
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("Version not found"))?;
let model = api::packages::download_package(&package, &version, None).await?;
@ -429,7 +530,11 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
Ok(model)
}
async fn get_users_from_author(self, provider: Provider, author: PackageAuthor) -> LauncherResult<Vec<ManagedUser>> {
Ok(provider.get_users_from_author(author).await?)
async fn get_users_from_author(
self,
provider: Provider,
author: PackageAuthor,
) -> LauncherResult<Vec<ManagedUser>> {
provider.get_users_from_author(author).await
}
}

View file

@ -1,10 +1,12 @@
#![feature(let_chains)]
#![feature(slice_as_array)]
#![allow(clippy::struct_excessive_bools)]
use api::proxy::LauncherProxy;
use store::{proxy::ProxyState, semaphore::SemaphoreStore, Core, CoreOptions, Dirs, State};
use error::LauncherResult;
pub use logger::start_logger;
use store::proxy::ProxyState;
use store::semaphore::SemaphoreStore;
use store::{Core, CoreOptions, Dirs, State};
pub mod api;
pub mod constants;
@ -15,10 +17,12 @@ pub mod utils;
mod logger;
pub use onelauncher_macro::*;
pub use onelauncher_entity as entity;
pub use onelauncher_migration as migration;
pub use {onelauncher_entity as entity, onelauncher_migration as migration};
pub async fn initialize_core(options: CoreOptions, proxy_backend: impl LauncherProxy + 'static) -> LauncherResult<()> {
pub async fn initialize_core(
options: CoreOptions,
proxy_backend: impl LauncherProxy + 'static,
) -> LauncherResult<()> {
Core::initialize(options).await?;
Dirs::get().await?;
start_logger().await;
@ -30,4 +34,4 @@ pub async fn initialize_core(options: CoreOptions, proxy_backend: impl LauncherP
tracing::info!("Core initialized successfully");
Ok(())
}
}

View file

@ -1,20 +1,19 @@
use tracing_subscriber::{prelude::*, util::SubscriberInitExt};
use crate::store::Core;
use tracing_subscriber::prelude::*;
use tracing_subscriber::util::SubscriberInitExt;
// Handling for the live development logging
// This will log to the console, and will not log to a file
#[cfg(debug_assertions)]
pub async fn start_logger() {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new(
Core::get().logger_filter
.as_ref()
.unwrap_or(
&format!("{}=info", env!("CARGO_PKG_NAME"))
)
)
});
let filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new(
Core::get()
.logger_filter
.clone()
.unwrap_or_else(|| format!("{}=info", env!("CARGO_PKG_NAME"))),
)
});
let mut fmt_layer = tracing_subscriber::fmt::layer()
.with_ansi(true)
@ -28,13 +27,14 @@ pub async fn start_logger() {
fmt_layer = fmt_layer.with_span_events(span);
}
if let Err(err) = tracing_subscriber::registry()
.with(fmt_layer)
.with(filter)
.with(tracing_error::ErrorLayer::default())
.try_init() {
eprintln!("Could not set default logger: {err}");
}
if let Err(err) = tracing_subscriber::registry()
.with(fmt_layer)
.with(filter)
.with(tracing_error::ErrorLayer::default())
.try_init()
{
eprintln!("Could not set default logger: {err}");
}
}
// Handling for the live production logging
@ -42,44 +42,41 @@ pub async fn start_logger() {
#[cfg(not(debug_assertions))]
pub async fn start_logger() {
use chrono::Local;
use std::fs::OpenOptions;
use tracing_subscriber::fmt::time::ChronoLocal;
use crate::store::Dirs;
use crate::store::Core;
use std::fs::OpenOptions;
use tracing_subscriber::fmt::time::ChronoLocal;
// Initialize and get logs directory path
let logs_dir = Dirs::get_launcher_logs_dir().await.unwrap_or_default();
use crate::store::{Core, Dirs};
let log_file_name = format!("launcher_{}.log", Local::now().format("%Y%m%d_%H%M%S"));
let log_file_path = logs_dir.join(log_file_name);
// Initialize and get logs directory path
let logs_dir = Dirs::get_launcher_logs_dir().await.unwrap_or_default();
if let Err(err) = std::fs::create_dir_all(&logs_dir) {
eprintln!("Could not create logs directory: {err}");
}
let log_file_name = format!("launcher_{}.log", Local::now().format("%Y%m%d_%H%M%S"));
let log_file_path = logs_dir.join(log_file_name);
let file = match OpenOptions::new()
.create(true)
.append(true)
.open(&log_file_path)
{
Ok(file) => file,
Err(e) => {
eprintln!("Could not start open log file: {e}");
return;
}
};
if let Err(err) = std::fs::create_dir_all(&logs_dir) {
eprintln!("Could not create logs directory: {err}");
}
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new(
Core::get().logger_filter
.as_ref()
.unwrap_or(
&format!("{}=info", env!("CARGO_PKG_NAME"))
)
)
});
let file = match OpenOptions::new()
.create(true)
.append(true)
.open(&log_file_path)
{
Ok(file) => file,
Err(e) => {
eprintln!("Could not start open log file: {e}");
return;
}
};
let filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new(
Core::get()
.logger_filter
.as_ref()
.unwrap_or(&format!("{}=info", env!("CARGO_PKG_NAME"))),
)
});
let mut fmt_layer = tracing_subscriber::fmt::layer()
.compact()
@ -91,9 +88,9 @@ pub async fn start_logger() {
fmt_layer = fmt_layer.with_span_events(span);
}
tracing_subscriber::registry()
.with(fmt_layer)
.with(filter)
.with(tracing_error::ErrorLayer::default())
tracing_subscriber::registry()
.with(fmt_layer)
.with(filter)
.with(tracing_error::ErrorLayer::default())
.init();
}

View file

@ -32,6 +32,7 @@ pub enum HashAlgorithm {
const BUFFER_SIZE: usize = 1024 * 256; // 16 KiB
impl HashAlgorithm {
#[allow(clippy::unused_async)]
pub async fn hash(&self, data: &[u8]) -> Result<String, IOError> {
let mut hasher = get_inner(self);
hasher.update(data);
@ -110,7 +111,7 @@ fn get_inner(algorithm: &HashAlgorithm) -> Box<dyn DynDigest + Send + Sync> {
match algorithm {
HashAlgorithm::Sha1 => Box::new(sha1::Sha1::default()),
HashAlgorithm::Sha256 => Box::new(sha2::Sha256::default()),
HashAlgorithm::Md5 => Box::new(md5::Md5::default())
HashAlgorithm::Md5 => Box::new(md5::Md5::default()),
}
}
@ -119,4 +120,4 @@ fn to_hex(data: &[u8]) -> String {
let _ = write!(output, "{b:02x}");
output
})
}
}

View file

@ -1,9 +1,11 @@
use onelauncher_entity::icon::{Icon, IconType};
use reqwest::Method;
use crate::{error::LauncherResult, store::Dirs};
use crate::error::LauncherResult;
use crate::store::Dirs;
use super::{crypto::HashAlgorithm, http, io};
use super::crypto::HashAlgorithm;
use super::{http, io};
/// Caches a icon on the filesystem. Name is the equivalent to the sha1 hash of the icon.
pub async fn cache_icon(icon: &Icon) -> LauncherResult<Icon> {
@ -12,17 +14,16 @@ pub async fn cache_icon(icon: &Icon) -> LauncherResult<Icon> {
IconType::Path => {
let bytes = io::read(&**icon).await?;
cache_icon_bytes(&bytes).await?
},
}
IconType::Url => {
let bytes = http::fetch(Method::GET, icon).await?;
cache_icon_bytes(&bytes).await?
},
}
IconType::Unknown => {
return Err(anyhow::anyhow!(
"icon type is unknown, cannot cache icon: {}",
icon
).into());
},
return Err(
anyhow::anyhow!("icon type is unknown, cannot cache icon: {}", icon).into(),
);
}
})
}
@ -36,5 +37,5 @@ pub async fn cache_icon_bytes(bytes: &[u8]) -> LauncherResult<Icon> {
let path = dir.join(&hash);
io::write(&path, bytes).await?;
Ok(Icon::from_hash(hash))
Ok(Icon::from_hash(&hash))
}

View file

@ -4,7 +4,9 @@ use sea_orm::{DeriveActiveEnum, EnumIter};
use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, EnumIter, DeriveActiveEnum)]
#[derive(
Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, EnumIter, DeriveActiveEnum,
)]
#[sea_orm(rs_type = "u8", db_type = "Integer")]
#[serde(rename_all = "lowercase")]
pub enum ClusterStage {
@ -16,18 +18,23 @@ pub enum ClusterStage {
}
impl ClusterStage {
pub fn is_downloading(&self) -> bool {
#[must_use]
pub const fn is_downloading(&self) -> bool {
matches!(self, Self::Downloading | Self::Repairing)
}
}
impl Display for ClusterStage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
ClusterStage::NotReady => "Not Ready",
ClusterStage::Downloading => "Downloading",
ClusterStage::Repairing => "Repairing",
ClusterStage::Ready => "Ready",
})
write!(
f,
"{}",
match self {
Self::NotReady => "Not Ready",
Self::Downloading => "Downloading",
Self::Repairing => "Repairing",
Self::Ready => "Ready",
}
)
}
}

View file

@ -1,10 +1,15 @@
use std::{fmt::Display, ops::Deref, str::FromStr};
use std::fmt::Display;
use std::ops::Deref;
use std::str::FromStr;
use sea_orm::{sea_query::Nullable, DeriveValueType};
use sea_orm::DeriveValueType;
use sea_orm::sea_query::Nullable;
use serde::{Deserialize, Serialize};
// #[onelauncher_macro::specta]
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType, specta::Type)]
#[derive(
Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, DeriveValueType, specta::Type,
)]
pub struct Icon(String);
impl Nullable for Icon {
@ -14,7 +19,8 @@ impl Nullable for Icon {
}
impl Icon {
pub fn from_hash(hash: String) -> Self {
#[must_use]
pub fn from_hash(hash: &str) -> Self {
Self(format!("cache://{hash}"))
}
@ -27,7 +33,8 @@ impl Icon {
Some(Self(format!("{}", path.display())))
}
pub fn try_from_url(url: url::Url) -> Option<Self> {
#[must_use]
pub fn try_from_url(url: &url::Url) -> Option<Self> {
if url.scheme() != "http" && url.scheme() != "https" {
return None;
}
@ -35,6 +42,7 @@ impl Icon {
Some(Self(url.as_str().to_string()))
}
#[must_use]
pub fn get_type(&self) -> IconType {
if self.is_path() {
IconType::Path
@ -48,16 +56,19 @@ impl Icon {
}
/// Returns true if the icon is a path to a file.
#[must_use]
pub fn is_path(&self) -> bool {
self.0.starts_with("file://")
}
/// Returns true if the icon is a HTTP URL.
#[must_use]
pub fn is_url(&self) -> bool {
self.0.starts_with("http://") || self.0.starts_with("https://")
}
/// Returns true if the icon is a cached icon. Relative to the cache directory.
#[must_use]
pub fn is_cached(&self) -> bool {
self.0.starts_with("cache://")
}

View file

@ -1,10 +1,23 @@
use std::{fmt::Display, str::FromStr};
use std::fmt::Display;
use std::str::FromStr;
use sea_orm::{DeriveActiveEnum, EnumIter};
use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter, DeriveActiveEnum)]
#[derive(
Default,
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
EnumIter,
DeriveActiveEnum,
)]
#[serde(rename_all = "lowercase")]
#[sea_orm(rs_type = "u8", db_type = "Integer")]
pub enum GameLoader {
@ -19,12 +32,12 @@ pub enum GameLoader {
}
impl GameLoader {
#[must_use]
pub const fn is_modded(&self) -> bool {
!matches!(self, Self::Vanilla)
}
#[must_use]
pub const fn get_format_version(&self) -> usize {
match self {
Self::Vanilla => interpulse::api::minecraft::CURRENT_FORMAT_VERSION,
@ -36,15 +49,17 @@ impl GameLoader {
}
}
#[must_use]
pub fn get_format_name(&self) -> String {
match self {
Self::Vanilla => String::from("minecraft"),
Self::NeoForge => String::from("neo"), // TODO(metadata): change to neoforge
_ => self.to_string().to_lowercase().replace(['_', '.', ' '], "")
_ => self.to_string().to_lowercase().replace(['_', '.', ' '], ""),
}
}
pub fn compatible_with(&self, other: &Self) -> bool {
#[must_use]
pub const fn compatible_with(&self, other: &Self) -> bool {
match self {
Self::Vanilla => matches!(other, Self::Vanilla),
Self::Forge => matches!(other, Self::Forge),
@ -55,6 +70,7 @@ impl GameLoader {
}
}
#[must_use]
pub const fn modded_loaders() -> &'static [Self] {
&[
Self::Forge,
@ -64,22 +80,23 @@ impl GameLoader {
Self::LegacyFabric,
]
}
}
impl TryFrom<String> for GameLoader {
type Error = String;
fn try_from(s: String) -> Result<GameLoader, String> {
Ok(match s.to_lowercase().replace(['_', '.', ' '], "").as_str() {
"vanilla" | "minecraft" => Self::Vanilla,
"forge" => Self::Forge,
"neoforge" | "neo" => Self::NeoForge,
"quilt" => Self::Quilt,
"fabric" => Self::Fabric,
"legacyfabric" => Self::LegacyFabric,
_ => return Err(format!("'{s}' is not a valid game loader")),
})
fn try_from(s: String) -> Result<Self, String> {
Ok(
match s.to_lowercase().replace(['_', '.', ' '], "").as_str() {
"vanilla" | "minecraft" => Self::Vanilla,
"forge" => Self::Forge,
"neoforge" | "neo" => Self::NeoForge,
"quilt" => Self::Quilt,
"fabric" => Self::Fabric,
"legacyfabric" => Self::LegacyFabric,
_ => return Err(format!("'{s}' is not a valid game loader")),
},
)
}
}
@ -93,15 +110,13 @@ impl FromStr for GameLoader {
impl Display for GameLoader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
match self {
Self::Vanilla => "Vanilla",
Self::Forge => "Forge",
Self::NeoForge => "NeoForge",
Self::Quilt => "Quilt",
Self::Fabric => "Fabric",
Self::LegacyFabric => "LegacyFabric",
}
)
f.write_str(match self {
Self::Vanilla => "Vanilla",
Self::Forge => "Forge",
Self::NeoForge => "NeoForge",
Self::Quilt => "Quilt",
Self::Fabric => "Fabric",
Self::LegacyFabric => "LegacyFabric",
})
}
}

View file

@ -24,7 +24,8 @@ impl PackageType {
Self::Shader => "shaders",
Self::DataPack => "datapacks",
Self::ModPack => "modpacks",
}.to_string()
}
.to_string()
}
}
@ -45,7 +46,7 @@ impl Display for Provider {
impl Provider {
#[must_use]
pub fn name(&self) -> &str {
pub const fn name(&self) -> &str {
match self {
Self::Modrinth => "Modrinth",
Self::Curseforge => "Curseforge",
@ -55,7 +56,7 @@ impl Provider {
/// Get the URL of the provider with a trailing slash
#[must_use]
pub fn website(&self) -> &str {
pub const fn website(&self) -> &str {
match self {
Self::Modrinth => "https://modrinth.com/",
Self::Curseforge => "https://curseforge.com/",
@ -67,4 +68,4 @@ impl Provider {
pub const fn get_providers() -> &'static [Self] {
&[Self::Modrinth, Self::Curseforge, Self::SkyClient]
}
}
}

View file

@ -1,4 +1,5 @@
use sea_orm::{sea_query::{Nullable, ValueType}, TryGetableFromJson};
use sea_orm::TryGetableFromJson;
use sea_orm::sea_query::{Nullable, ValueType};
use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
@ -15,14 +16,15 @@ impl Default for Resolution {
}
impl Resolution {
pub fn new(width: u32, height: u32) -> Self {
#[must_use]
pub const fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
}
impl From<Resolution> for sea_orm::Value {
fn from(value: Resolution) -> Self {
sea_orm::Value::Json(serde_json::to_value(value).ok().map(Box::new))
Self::Json(serde_json::to_value(value).ok().map(Box::new))
}
}
@ -31,9 +33,9 @@ impl TryGetableFromJson for Resolution {}
impl ValueType for Resolution {
fn try_from(v: sea_orm::Value) -> Result<Self, sea_orm::sea_query::ValueTypeErr> {
match v {
sea_orm::Value::Json(Some(json)) => Ok(
serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?,
),
sea_orm::Value::Json(Some(json)) => {
Ok(serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?)
}
_ => Err(sea_orm::sea_query::ValueTypeErr),
}
}
@ -56,4 +58,3 @@ impl Nullable for Resolution {
sea_orm::Value::Json(None)
}
}

View file

@ -1,4 +1,5 @@
use sea_orm::{sea_query::{self, Nullable, ValueType}, TryGetableFromJson};
use sea_orm::TryGetableFromJson;
use sea_orm::sea_query::{self, Nullable, ValueType};
use serde::{Deserialize, Serialize};
cfg_select! {
@ -51,7 +52,7 @@ cfg_select! {
impl From<SettingsOsExtra> for sea_query::Value {
fn from(value: SettingsOsExtra) -> Self {
sea_query::Value::Json(serde_json::to_value(value).ok().map(Box::new))
Self::Json(serde_json::to_value(value).ok().map(Box::new))
}
}
@ -60,11 +61,11 @@ impl TryGetableFromJson for SettingsOsExtra {}
impl ValueType for SettingsOsExtra {
fn try_from(v: sea_orm::Value) -> Result<Self, sea_query::ValueTypeErr> {
match v {
sea_orm::Value::Json(Some(json)) => Ok(
serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?,
),
_ => Err(sea_orm::sea_query::ValueTypeErr),
}
sea_orm::Value::Json(Some(json)) => {
Ok(serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?)
}
_ => Err(sea_orm::sea_query::ValueTypeErr),
}
}
fn type_name() -> String {
@ -84,4 +85,4 @@ impl Nullable for SettingsOsExtra {
fn null() -> sea_orm::Value {
sea_orm::Value::Json(None)
}
}
}

View file

@ -1,6 +1,7 @@
use std::ops::Deref;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
@ -25,45 +26,41 @@ impl<T> sea_orm::TryGetableFromJson for DbVec<T> where for<'de> T: Deserialize<'
impl<T> std::convert::From<DbVec<T>> for sea_orm::Value
where
DbVec<T>: Serialize,
DbVec<T>: Serialize,
{
fn from(source: DbVec<T>) -> Self {
sea_orm::Value::Json(
serde_json::to_value(&source)
.ok()
.map(std::boxed::Box::new),
)
}
fn from(source: DbVec<T>) -> Self {
Self::Json(serde_json::to_value(&source).ok().map(std::boxed::Box::new))
}
}
impl<T> sea_orm::sea_query::ValueType for DbVec<T>
where
DbVec<T>: DeserializeOwned,
Self: DeserializeOwned,
{
fn try_from(v: sea_orm::Value) -> Result<Self, sea_orm::sea_query::ValueTypeErr> {
match v {
sea_orm::Value::Json(Some(json)) => {
Ok(serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?)
}
_ => Err(sea_orm::sea_query::ValueTypeErr),
}
}
fn try_from(v: sea_orm::Value) -> Result<Self, sea_orm::sea_query::ValueTypeErr> {
match v {
sea_orm::Value::Json(Some(json)) => {
Ok(serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?)
}
_ => Err(sea_orm::sea_query::ValueTypeErr),
}
}
fn type_name() -> String {
stringify!(#ident).to_owned()
}
fn type_name() -> String {
stringify!(#ident).to_owned()
}
fn array_type() -> sea_orm::sea_query::ArrayType {
sea_orm::sea_query::ArrayType::Json
}
fn array_type() -> sea_orm::sea_query::ArrayType {
sea_orm::sea_query::ArrayType::Json
}
fn column_type() -> sea_orm::sea_query::ColumnType {
sea_orm::sea_query::ColumnType::Json
}
fn column_type() -> sea_orm::sea_query::ColumnType {
sea_orm::sea_query::ColumnType::Json
}
}
impl<T> sea_orm::sea_query::Nullable for DbVec<T> {
fn null() -> sea_orm::Value {
sea_orm::Value::Json(None)
}
}
fn null() -> sea_orm::Value {
sea_orm::Value::Json(None)
}
}

View file

@ -4,7 +4,8 @@ use merge::Merge;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use crate::{resolution::Resolution, settings::SettingsOsExtra};
use crate::resolution::Resolution;
use crate::settings::SettingsOsExtra;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, Merge)]
#[sea_orm(table_name = "setting_profiles")]
@ -31,7 +32,7 @@ pub struct Model {
#[sea_orm(nullable)]
pub hook_post: Option<String>,
#[sea_orm(nullable)]
pub os_extra: Option<SettingsOsExtra>
pub os_extra: Option<SettingsOsExtra>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@ -65,16 +66,21 @@ impl ActiveModelBehavior for ActiveModel {}
const GLOBAL_NAME: &str = "Global";
impl ActiveModel {
#[must_use]
pub fn is_global(&self) -> bool {
self.name.try_as_ref().map(|name| name == GLOBAL_NAME).unwrap_or(false)
self.name
.try_as_ref()
.is_some_and(|name| name == GLOBAL_NAME)
}
}
impl Model {
#[must_use]
pub fn is_global(&self) -> bool {
self.name == GLOBAL_NAME
}
#[must_use]
pub fn default_global_profile() -> Self {
Self {
name: GLOBAL_NAME.into(),
@ -87,7 +93,7 @@ impl Model {
launch_env: None,
mem_max: Some(2048),
res: None,
os_extra: Some(SettingsOsExtra::default())
os_extra: Some(SettingsOsExtra::default()),
}
}
}

View file

@ -24,7 +24,10 @@ mod pin;
/// ```
#[proc_macro_attribute]
#[cfg(feature = "specta")]
pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
pub fn specta(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
specta::specta(attr, item)
}
@ -36,12 +39,18 @@ pub fn serialized_error_derive(item: proc_macro::TokenStream) -> proc_macro::Tok
}
#[proc_macro_attribute]
pub fn error(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
pub fn error(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
error::error_attr(attr, item)
}
/// This macro Box::Pin a function
/// This macro `Box::Pin` a function
#[proc_macro_attribute]
pub fn pin(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
pub fn pin(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
pin::pin(attr, item)
}

View file

@ -1,21 +1,23 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{meta::ParseNestedMeta, parse_macro_input, Item};
use syn::meta::ParseNestedMeta;
use syn::{Item, parse_macro_input};
pub fn specta(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
pub fn specta(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let mut attrs = SpectaAttributes::default();
let parser = syn::meta::parser(|meta| attrs.parse(meta));
let parser = syn::meta::parser(|meta| attrs.parse(&meta));
parse_macro_input!(attr with parser);
let input = parse_macro_input!(item as Item);
let specta_rename = if let Some(rename) = attrs.rename {
quote! {
#[cfg_attr(feature = "specta", specta(rename = #rename))]
}
} else {
TokenStream::new()
};
let specta_rename = attrs.rename.map_or_else(TokenStream::new, |rename| {
quote! {
#[cfg_attr(feature = "specta", specta(rename = #rename))]
}
});
let expanded = quote! {
#[cfg_attr(feature = "specta", derive(specta::Type))]
@ -32,10 +34,10 @@ struct SpectaAttributes {
}
impl SpectaAttributes {
fn parse(&mut self, meta: ParseNestedMeta) -> syn::Result<()> {
fn parse(&mut self, meta: &ParseNestedMeta) -> syn::Result<()> {
if meta.path.is_ident("rename") {
self.rename = Some(meta.value()?.to_string());
}
Ok(())
}
}
Ok(())
}
}

View file

@ -7,7 +7,7 @@ interface MappedCategoryItem {
id: string;
}
type MappingList = Record<string, MappedCategoryItem[]>;
type MappingList = Record<string, Array<MappedCategoryItem>>;
const packageTypes = ['mod', 'modpack', 'shader', 'resourcepack'] as const;
type PackageType = typeof packageTypes[number];
@ -43,7 +43,7 @@ async function fetchCategoriesCurseforge(packageType: PackageType): Promise<Mapp
parentCategoryId?: number;
};
const data = (await response.json() as { data: CFCategory[] }).data;
const data = (await response.json() as { data: Array<CFCategory> }).data;
// TODO: Add more calssIds
const classIds: Record<PackageType, number> = {
@ -79,7 +79,7 @@ async function fetchCategoriesModrinth(packageType: PackageType): Promise<Mappin
header: string;
}
const data = await response.json() as ModrinthCategory[];
const data = await response.json() as Array<ModrinthCategory>;
const mapping: MappingList = {};

View file

@ -17,7 +17,6 @@
"scripts": {
"tauri": "pnpm tsx tauri.ts",
"prep": "pnpm tsx prepare.ts",
"prepare": "pnpm tsx prepare.ts",
"setup-win": "./setup.ps1",
"setup-nix": "./setup.sh",
"autoformat": "./format.sh",

View file

@ -1,4 +1,6 @@
import { exec } from 'node:child_process';
import { readFile, writeFile } from 'node:fs/promises';
import process from 'node:process';
import { consola } from 'consola';
import mustache from 'mustache';
import { join } from 'pathe';
@ -77,3 +79,22 @@ catch (error) {
consola.error(error);
env.__exit(1);
}
consola.start('building the web_commons package...');
exec('pnpm -w web_common build', {
cwd: env.__root,
env: {
...process.env,
NODE_ENV: 'production',
},
}, (error, stdout, stderr) => {
if (error) {
consola.error(`web_commons build failed: ${error.message}`);
env.__exit(1);
}
if (stderr)
consola.error(stderr);
consola.success('web_commons package built successfully!');
env.__exit(0);
});

View file

@ -29,7 +29,7 @@ if (cargoConfig.env && typeof cargoConfig.env === 'object')
function targetFilter(filter: 'b' | 't') {
const filters = filter === 'b' ? ['-b', '--bundles'] : ['-t', '--target'];
return (_: string, idx: number, args: string[]) => {
return (_: string, idx: number, args: Array<string>) => {
if (idx === 0)
return false;
const prev = args[idx - 1] ?? '';

View file

@ -45,9 +45,9 @@ export async function which(cmd: TemplateStringsArray): Promise<boolean> {
return type() === 'Windows_NT'
? where(cmd[0]!)
: Promise.any(
Array.from(new Set(process.env.PATH?.split(':')))
.map(p => fs.access(join(p, cmd[0]!), fs.constants.X_OK)),
).then(_ => true, _ => false);
Array.from(new Set(process.env.PATH?.split(':')))
.map(p => fs.access(join(p, cmd[0]!), fs.constants.X_OK)),
).then(_ => true, _ => false);
}
export async function awaitLock(file: string): Promise<void> {

View file

@ -47,7 +47,7 @@ export async function tauriUpdateKey(env: CheckedEnvironment): Promise<string |
return keys.publicKey;
}
export async function patchTauri(env: CheckedEnvironment, targets: string[], args: string[]): Promise<string[]> {
export async function patchTauri(env: CheckedEnvironment, targets: Array<string>, args: Array<string>): Promise<Array<string>> {
consola.start('patching tauri configuration...');
if (args.findIndex(a => ['--config', '-c'].includes(a)) !== -1)
throw new Error('custom tauri build configuration is not supported!');
@ -55,7 +55,7 @@ export async function patchTauri(env: CheckedEnvironment, targets: string[], arg
const osType = type();
const tauriPatch: {
build: {
features: string[];
features: Array<string>;
};
bundle: {
macOS: { minimumSystemVersion: string };
@ -107,10 +107,10 @@ export async function patchTauri(env: CheckedEnvironment, targets: string[], arg
if (
(targets.includes('aarch64-apple-darwin')
|| (targets.length === 0 && process.arch === 'arm64'))
&& (macOSStore.minimumVersion == null || semver.lt(
semver.coerce(macOSStore.minimumVersion)!,
semver.coerce(macOSStore.defaultArm64)!,
))
&& (macOSStore.minimumVersion == null || semver.lt(
semver.coerce(macOSStore.minimumVersion)!,
semver.coerce(macOSStore.defaultArm64)!,
))
) {
macOSStore.minimumVersion = macOSStore.defaultArm64;
consola.debug(`setting minimum macOS version to ${macOSStore.minimumVersion}...`);

View file

@ -1,13 +1,20 @@
[package]
name = "serde_json_path_to_error"
version = "0.2.0"
authors = [ "Polyfrost", "Ethan Brierley <ethanboxx@gmail.com>" ]
authors = [
"Polyfrost",
"Ethan Brierley <ethanboxx@gmail.com>"
]
license = "MIT/Apache-2.0"
readme = "README.md"
description = """
A drop in replacement for serde_json where detailed errors are the default
"""
keywords = [ "serde", "json", "serialization" ]
keywords = [
"serde",
"json",
"serialization"
]
documentation = "https://docs.rs/serde_json_path_to_error/"
repository = "https://github.com/eopb/serde_json_path_to_error"
edition = "2024"

View file

@ -1,4 +1,3 @@
/* eslint-disable eslint-comments/require-description -- i trust that one github discussion user on this */
/* eslint-disable react/no-unstable-default-props */
// Taken from https://github.com/TanStack/router/discussions/823#discussioncomment-12765059

View file

@ -1,5 +1,5 @@
import type { JSX, ReactNode } from 'react';
import React, { createContext, useContext } from 'react';
import type { JSX } from 'react';
import React, { createContext, useCallback, useContext, useMemo } from 'react';
interface SelectListContextType {
onSelect: (value: string) => void;
@ -30,7 +30,7 @@ export const SelectList: React.FC<SelectListProps> & { Item: React.FC<SelectList
multiple = false,
children,
}) => {
const handleSelect = (itemValue: string): void => {
const handleSelect = useCallback((itemValue: string): void => {
if (multiple) {
const currentValues = Array.isArray(value) ? value : [];
const newValues = currentValues.includes(itemValue)
@ -41,20 +41,20 @@ export const SelectList: React.FC<SelectListProps> & { Item: React.FC<SelectList
else {
onChange(itemValue);
}
};
}, [multiple, onChange, value]);
const isSelected = (itemValue: string): boolean => {
const isSelected = useCallback((itemValue: string): boolean => {
if (multiple)
return Array.isArray(value) && value.includes(itemValue);
return value === itemValue;
};
}, [multiple, value]);
const contextValue: SelectListContextType = {
const contextValue: SelectListContextType = useMemo(() => ({
onSelect: handleSelect,
isSelected,
multiple,
};
}), [handleSelect, isSelected, multiple]);
return (
<SelectListContext.Provider value={contextValue}>

View file

@ -34,7 +34,7 @@ export default defineConfig({
rollupOptions: {
external: [
'react',
'react/jsx-runtime',
// 'react/jsx-runtime',
'react-dom',
'react-aria-components',
'motion',

333
pnpm-lock.yaml generated
View file

@ -96,6 +96,9 @@ catalogs:
react-dom:
specifier: ^19.0.0
version: 19.1.0
react-just-parallax:
specifier: ^3.1.16
version: 3.1.16
rimraf:
specifier: ^6.0.1
version: 6.0.1
@ -161,6 +164,12 @@ importers:
eslint:
specifier: 'catalog:'
version: 9.25.1(jiti@2.4.2)
husky:
specifier: ^9.1.7
version: 9.1.7
lint-staged:
specifier: ^16.1.2
version: 16.1.2
rimraf:
specifier: 'catalog:'
version: 6.0.1
@ -172,7 +181,7 @@ importers:
version: 5.8.3
vite:
specifier: 'catalog:'
version: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)
version: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)
.github/actions/publish-artifacts:
dependencies:
@ -216,7 +225,7 @@ importers:
version: link:../../../packages/web_common
'@tailwindcss/vite':
specifier: 'catalog:'
version: 4.1.10(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
version: 4.1.10(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
'@tanstack/match-sorter-utils':
specifier: 'catalog:'
version: 8.19.4
@ -237,7 +246,7 @@ importers:
version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tanstack/router-plugin':
specifier: 'catalog:'
version: 1.119.0(@tanstack/react-router@1.119.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.6(solid-js@1.9.6)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)))(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
version: 1.119.0(@tanstack/react-router@1.119.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.6(solid-js@1.9.6)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)))(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
'@tauri-apps/api':
specifier: 'catalog:'
version: 2.5.0
@ -272,7 +281,7 @@ importers:
specifier: 'catalog:'
version: 19.1.0(react@19.1.0)
react-just-parallax:
specifier: ^3.1.16
specifier: 'catalog:'
version: 3.1.16(react@19.1.0)
tailwind-merge:
specifier: 'catalog:'
@ -307,16 +316,16 @@ importers:
version: 19.1.3(@types/react@19.1.2)
'@vitejs/plugin-react':
specifier: 'catalog:'
version: 4.4.1(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
version: 4.4.1(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
typescript:
specifier: 'catalog:'
version: 5.8.3
vite:
specifier: 'catalog:'
version: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)
version: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)
vite-plugin-svgr:
specifier: 'catalog:'
version: 4.3.0(rollup@4.41.1)(typescript@5.8.3)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
version: 4.3.0(rollup@4.41.1)(typescript@5.8.3)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
apps/onelauncher/desktop:
dependencies:
@ -338,7 +347,7 @@ importers:
version: link:../../../packages/web_common
'@tailwindcss/vite':
specifier: 'catalog:'
version: 4.1.10(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
version: 4.1.10(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
'@tanstack/match-sorter-utils':
specifier: 'catalog:'
version: 8.19.4
@ -359,7 +368,7 @@ importers:
version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tanstack/router-plugin':
specifier: 'catalog:'
version: 1.119.0(@tanstack/react-router@1.119.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.6(solid-js@1.9.6)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)))(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
version: 1.119.0(@tanstack/react-router@1.119.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.6(solid-js@1.9.6)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)))(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
'@tauri-apps/api':
specifier: 'catalog:'
version: 2.5.0
@ -441,13 +450,13 @@ importers:
version: 19.1.3(@types/react@19.1.2)
'@vitejs/plugin-react':
specifier: 'catalog:'
version: 4.4.1(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
version: 4.4.1(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
typescript:
specifier: 'catalog:'
version: 5.8.3
vite:
specifier: 'catalog:'
version: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)
version: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)
packages/core: {}
@ -530,25 +539,25 @@ importers:
devDependencies:
'@tailwindcss/vite':
specifier: 'catalog:'
version: 4.1.10(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
version: 4.1.10(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
'@types/react':
specifier: 'catalog:'
version: 19.1.2
'@vitejs/plugin-react':
specifier: 'catalog:'
version: 4.4.1(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
version: 4.4.1(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
typescript:
specifier: 'catalog:'
version: 5.8.3
vite:
specifier: 'catalog:'
version: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)
version: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)
vite-plugin-dts:
specifier: 'catalog:'
version: 4.5.4(@types/node@22.16.5)(rollup@4.41.1)(typescript@5.8.3)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
version: 4.5.4(@types/node@22.16.5)(rollup@4.41.1)(typescript@5.8.3)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
vite-plugin-static-copy:
specifier: 'catalog:'
version: 3.0.0(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
version: 3.0.0(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
packages:
@ -3159,6 +3168,7 @@ packages:
'@zod/mini@4.0.0-beta.20250430T185432':
resolution: {integrity: sha512-PLamkZ3BPeUXAYUhU5CaLY6ZaAQ4PVSoy2bh54UWXPx7fCC/+vWicLmgAt8+lwjaqpe2AW8FwiLXYoCb1UIJxA==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
@ -3211,6 +3221,10 @@ packages:
alien-signals@0.4.14:
resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==}
ansi-escapes@7.0.0:
resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
engines: {node: '>=18'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@ -3369,6 +3383,10 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
chalk@5.4.1:
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
character-entities-html4@2.1.0:
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
@ -3405,6 +3423,14 @@ packages:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
engines: {node: '>=4'}
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
cli-truncate@4.0.0:
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
engines: {node: '>=18'}
client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
@ -3419,6 +3445,9 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@ -3426,6 +3455,10 @@ packages:
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
commander@14.0.0:
resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==}
engines: {node: '>=20'}
commander@6.2.1:
resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
engines: {node: '>= 6'}
@ -3534,6 +3567,15 @@ packages:
supports-color:
optional: true
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
decimal.js@10.4.3:
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
@ -3591,6 +3633,9 @@ packages:
electron-to-chromium@1.5.149:
resolution: {integrity: sha512-UyiO82eb9dVOx8YO3ajDf9jz2kKyt98DEITRdeLPstOEuTlLzDA4Gyq5K9he71TQziU5jUVu2OAu5N48HmQiyQ==}
emoji-regex@10.4.0:
resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@ -3605,6 +3650,10 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
environment@1.1.0:
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
engines: {node: '>=18'}
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
@ -3905,6 +3954,9 @@ packages:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
@ -4035,6 +4087,10 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
get-east-asian-width@1.3.0:
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
engines: {node: '>=18'}
get-stream@9.0.1:
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
engines: {node: '>=18'}
@ -4155,6 +4211,11 @@ packages:
resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==}
engines: {node: '>=18.18.0'}
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
hasBin: true
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@ -4233,6 +4294,14 @@ packages:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-fullwidth-code-point@4.0.0:
resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
engines: {node: '>=12'}
is-fullwidth-code-point@5.0.0:
resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==}
engines: {node: '>=18'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
@ -4444,9 +4513,22 @@ packages:
resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
engines: {node: '>= 12.0.0'}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
lint-staged@16.1.2:
resolution: {integrity: sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==}
engines: {node: '>=20.17'}
hasBin: true
listr2@8.3.3:
resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==}
engines: {node: '>=18.0.0'}
local-pkg@1.1.1:
resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==}
engines: {node: '>=14'}
@ -4462,6 +4544,9 @@ packages:
lodash.castarray@4.4.0:
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
@ -4471,6 +4556,10 @@ packages:
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
log-update@6.1.0:
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
engines: {node: '>=18'}
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
@ -4655,6 +4744,10 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mimic-function@5.0.1:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
min-indent@1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
@ -4734,6 +4827,10 @@ packages:
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
hasBin: true
nano-spawn@1.0.2:
resolution: {integrity: sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==}
engines: {node: '>=20.17'}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -4786,6 +4883,10 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
onetime@7.0.0:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@ -4914,6 +5015,11 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
pidtree@0.6.0:
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
engines: {node: '>=0.10'}
hasBin: true
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
@ -5098,10 +5204,17 @@ packages:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
restore-cursor@5.1.0:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'}
reusify@1.0.4:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
rimraf@6.0.1:
resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==}
engines: {node: 20 || >=22}
@ -5198,6 +5311,14 @@ packages:
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
slice-ansi@5.0.0:
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
engines: {node: '>=12'}
slice-ansi@7.1.0:
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
engines: {node: '>=18'}
smol-toml@1.3.0:
resolution: {integrity: sha512-tWpi2TsODPScmi48b/OQZGi2lgUmBCHy6SZrhi/FdnnHiU1GwebbCfuQuxsC3nHaLwtYeJGPrDZDIeodDOc4pA==}
engines: {node: '>= 18'}
@ -5269,6 +5390,10 @@ packages:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
@ -5808,6 +5933,10 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
wrap-ansi@9.0.0:
resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
engines: {node: '>=18'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
@ -5853,6 +5982,11 @@ packages:
engines: {node: '>= 14'}
hasBin: true
yaml@2.8.0:
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
engines: {node: '>= 14.6'}
hasBin: true
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@ -8355,12 +8489,12 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 4.1.10
'@tailwindcss/vite@4.1.10(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))':
'@tailwindcss/vite@4.1.10(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))':
dependencies:
'@tailwindcss/node': 4.1.10
'@tailwindcss/oxide': 4.1.10
tailwindcss: 4.1.10
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)
'@tanstack/eslint-config@0.1.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
@ -8467,7 +8601,7 @@ snapshots:
optionalDependencies:
'@tanstack/react-router': 1.119.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tanstack/router-plugin@1.119.0(@tanstack/react-router@1.119.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.6(solid-js@1.9.6)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)))(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))':
'@tanstack/router-plugin@1.119.0(@tanstack/react-router@1.119.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.6(solid-js@1.9.6)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)))(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))':
dependencies:
'@babel/core': 7.27.1
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1)
@ -8488,8 +8622,8 @@ snapshots:
zod: 3.25.27
optionalDependencies:
'@tanstack/react-router': 1.119.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)
vite-plugin-solid: 2.11.6(solid-js@1.9.6)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)
vite-plugin-solid: 2.11.6(solid-js@1.9.6)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
transitivePeerDependencies:
- supports-color
@ -8788,14 +8922,14 @@ snapshots:
'@vercel/ncc@0.38.2': {}
'@vitejs/plugin-react@4.4.1(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))':
'@vitejs/plugin-react@4.4.1(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))':
dependencies:
'@babel/core': 7.27.1
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1)
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- supports-color
@ -8970,6 +9104,10 @@ snapshots:
alien-signals@0.4.14: {}
ansi-escapes@7.0.0:
dependencies:
environment: 1.1.0
ansi-regex@5.0.1: {}
ansi-regex@6.1.0: {}
@ -9138,6 +9276,8 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
chalk@5.4.1: {}
character-entities-html4@2.1.0: {}
character-entities-legacy@3.0.0: {}
@ -9174,6 +9314,15 @@ snapshots:
dependencies:
escape-string-regexp: 1.0.5
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
cli-truncate@4.0.0:
dependencies:
slice-ansi: 5.0.0
string-width: 7.2.0
client-only@0.0.1: {}
clsx@2.1.1: {}
@ -9184,6 +9333,8 @@ snapshots:
color-name@1.1.4: {}
colorette@2.0.20: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@ -9191,6 +9342,8 @@ snapshots:
comma-separated-tokens@2.0.3: {}
commander@14.0.0: {}
commander@6.2.1: {}
comment-parser@1.4.1: {}
@ -9278,6 +9431,10 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.1:
dependencies:
ms: 2.1.3
decimal.js@10.4.3: {}
decode-named-character-reference@1.1.0:
@ -9323,6 +9480,8 @@ snapshots:
electron-to-chromium@1.5.149: {}
emoji-regex@10.4.0: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
@ -9334,6 +9493,8 @@ snapshots:
entities@4.5.0: {}
environment@1.1.0: {}
error-ex@1.3.2:
dependencies:
is-arrayish: 0.2.1
@ -9853,6 +10014,8 @@ snapshots:
event-target-shim@5.0.1: {}
eventemitter3@5.0.1: {}
events@3.3.0: {}
execa@9.4.1:
@ -9982,6 +10145,8 @@ snapshots:
gensync@1.0.0-beta.2: {}
get-east-asian-width@1.3.0: {}
get-stream@9.0.1:
dependencies:
'@sec-ant/readable-stream': 0.4.1
@ -10156,6 +10321,8 @@ snapshots:
human-signals@8.0.0: {}
husky@9.1.7: {}
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
@ -10226,6 +10393,12 @@ snapshots:
is-fullwidth-code-point@3.0.0: {}
is-fullwidth-code-point@4.0.0: {}
is-fullwidth-code-point@5.0.0:
dependencies:
get-east-asian-width: 1.3.0
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
@ -10417,8 +10590,34 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.30.1
lightningcss-win32-x64-msvc: 1.30.1
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
lint-staged@16.1.2:
dependencies:
chalk: 5.4.1
commander: 14.0.0
debug: 4.4.1
lilconfig: 3.1.3
listr2: 8.3.3
micromatch: 4.0.8
nano-spawn: 1.0.2
pidtree: 0.6.0
string-argv: 0.3.2
yaml: 2.8.0
transitivePeerDependencies:
- supports-color
listr2@8.3.3:
dependencies:
cli-truncate: 4.0.0
colorette: 2.0.20
eventemitter3: 5.0.1
log-update: 6.1.0
rfdc: 1.4.1
wrap-ansi: 9.0.0
local-pkg@1.1.1:
dependencies:
mlly: 1.7.4
@ -10435,12 +10634,22 @@ snapshots:
lodash.castarray@4.4.0: {}
lodash.debounce@4.0.8: {}
lodash.isplainobject@4.0.6: {}
lodash.merge@4.6.2: {}
lodash@4.17.21: {}
log-update@6.1.0:
dependencies:
ansi-escapes: 7.0.0
cli-cursor: 5.0.0
slice-ansi: 7.1.0
strip-ansi: 7.1.0
wrap-ansi: 9.0.0
longest-streak@3.1.0: {}
loupe@3.1.2:
@ -10850,6 +11059,8 @@ snapshots:
mime-db: 1.52.0
optional: true
mimic-function@5.0.1: {}
min-indent@1.0.1: {}
minimatch@10.0.1:
@ -10916,6 +11127,8 @@ snapshots:
mustache@4.2.0: {}
nano-spawn@1.0.2: {}
nanoid@3.3.11: {}
napi-postinstall@0.2.3: {}
@ -10959,6 +11172,10 @@ snapshots:
dependencies:
wrappy: 1.0.2
onetime@7.0.0:
dependencies:
mimic-function: 5.0.1
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@ -11078,6 +11295,8 @@ snapshots:
picomatch@4.0.2: {}
pidtree@0.6.0: {}
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
@ -11218,6 +11437,13 @@ snapshots:
react: 19.1.0
scheduler: 0.26.0
react-just-parallax@3.1.16(react@19.1.0):
dependencies:
framesync: 6.1.0
lodash.debounce: 4.0.8
prefix: 1.0.0
react: 19.1.0
react-markdown@10.1.0(@types/react@19.1.2)(react@19.1.0):
dependencies:
'@types/hast': 3.0.4
@ -11381,8 +11607,15 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
restore-cursor@5.1.0:
dependencies:
onetime: 7.0.0
signal-exit: 4.1.0
reusify@1.0.4: {}
rfdc@1.4.1: {}
rimraf@6.0.1:
dependencies:
glob: 11.0.0
@ -11512,6 +11745,16 @@ snapshots:
sisteransi@1.0.5: {}
slice-ansi@5.0.0:
dependencies:
ansi-styles: 6.2.1
is-fullwidth-code-point: 4.0.0
slice-ansi@7.1.0:
dependencies:
ansi-styles: 6.2.1
is-fullwidth-code-point: 5.0.0
smol-toml@1.3.0: {}
snake-case@3.0.4:
@ -11594,6 +11837,12 @@ snapshots:
emoji-regex: 9.2.2
strip-ansi: 7.1.0
string-width@7.2.0:
dependencies:
emoji-regex: 10.4.0
get-east-asian-width: 1.3.0
strip-ansi: 7.1.0
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
@ -11948,7 +12197,7 @@ snapshots:
vite-node@2.1.3(@types/node@22.16.5)(lightningcss@1.30.1)(sass@1.80.3):
dependencies:
cac: 6.7.14
debug: 4.4.0
debug: 4.4.1
pathe: 1.1.2
vite: 5.4.10(@types/node@22.16.5)(lightningcss@1.30.1)(sass@1.80.3)
transitivePeerDependencies:
@ -11963,7 +12212,7 @@ snapshots:
- terser
optional: true
vite-plugin-dts@4.5.4(@types/node@22.16.5)(rollup@4.41.1)(typescript@5.8.3)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)):
vite-plugin-dts@4.5.4(@types/node@22.16.5)(rollup@4.41.1)(typescript@5.8.3)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)):
dependencies:
'@microsoft/api-extractor': 7.52.8(@types/node@22.16.5)
'@rollup/pluginutils': 5.1.4(rollup@4.41.1)
@ -11976,13 +12225,13 @@ snapshots:
magic-string: 0.30.17
typescript: 5.8.3
optionalDependencies:
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- '@types/node'
- rollup
- supports-color
vite-plugin-solid@2.11.6(solid-js@1.9.6)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)):
vite-plugin-solid@2.11.6(solid-js@1.9.6)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)):
dependencies:
'@babel/core': 7.27.1
'@types/babel__core': 7.20.5
@ -11990,27 +12239,27 @@ snapshots:
merge-anything: 5.1.7
solid-js: 1.9.6
solid-refresh: 0.6.3(solid-js@1.9.6)
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)
vitefu: 1.0.6(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0))
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)
vitefu: 1.0.6(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0))
transitivePeerDependencies:
- supports-color
optional: true
vite-plugin-static-copy@3.0.0(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)):
vite-plugin-static-copy@3.0.0(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)):
dependencies:
chokidar: 3.6.0
fs-extra: 11.3.0
p-map: 7.0.3
picocolors: 1.1.1
tinyglobby: 0.2.13
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)
vite-plugin-svgr@4.3.0(rollup@4.41.1)(typescript@5.8.3)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)):
vite-plugin-svgr@4.3.0(rollup@4.41.1)(typescript@5.8.3)(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)):
dependencies:
'@rollup/pluginutils': 5.1.4(rollup@4.41.1)
'@svgr/core': 8.1.0(typescript@5.8.3)
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3))
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)
transitivePeerDependencies:
- rollup
- supports-color
@ -12028,7 +12277,7 @@ snapshots:
sass: 1.80.3
optional: true
vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0):
vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0):
dependencies:
esbuild: 0.25.3
fdir: 6.4.4(picomatch@4.0.2)
@ -12043,11 +12292,11 @@ snapshots:
lightningcss: 1.30.1
sass: 1.80.3
tsx: 4.19.4
yaml: 2.6.0
yaml: 2.8.0
vitefu@1.0.6(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)):
vitefu@1.0.6(vite@6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)):
optionalDependencies:
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)
vite: 6.3.4(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.80.3)(tsx@4.19.4)(yaml@2.8.0)
optional: true
vitest@2.1.3(@types/node@22.16.5)(@vitest/ui@2.1.3)(jsdom@25.0.1)(lightningcss@1.30.1)(sass@1.80.3):
@ -12060,7 +12309,7 @@ snapshots:
'@vitest/spy': 2.1.3
'@vitest/utils': 2.1.3
chai: 5.1.2
debug: 4.4.0
debug: 4.4.1
magic-string: 0.30.17
pathe: 1.1.2
std-env: 3.7.0
@ -12159,6 +12408,12 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.1.0
wrap-ansi@9.0.0:
dependencies:
ansi-styles: 6.2.1
string-width: 7.2.0
strip-ansi: 7.1.0
wrappy@1.0.2: {}
ws@8.18.0:
@ -12185,6 +12440,8 @@ snapshots:
yaml@2.6.0: {}
yaml@2.8.0: {}
yocto-queue@0.1.0: {}
yoctocolors@2.1.1: {}

View file

@ -1,12 +1,11 @@
[toolchain]
channel = "nightly"
channel = "nightly-2025-07-23"
profile = "minimal"
components = [
"cargo",
"clippy",
"llvm-tools-preview",
"rustc",
"rust-src",
"rustfmt",
"rust-docs",
"rust-analyzer",