diff --git a/apps/oneclient/desktop/src/api/commands.rs b/apps/oneclient/desktop/src/api/commands.rs index 436ecb42..6a61bf58 100644 --- a/apps/oneclient/desktop/src/api/commands.rs +++ b/apps/oneclient/desktop/src/api/commands.rs @@ -1,11 +1,16 @@ -use onelauncher_core::error::{DaoError, LauncherResult}; +use std::collections::HashMap; + +use onelauncher_core::entity::clusters; +use onelauncher_core::error::LauncherResult; use tauri::Runtime; #[taurpc::procedures(path = "oneclient", export_to = "../frontend/src/bindings.gen.ts")] pub trait OneClientApi { - async fn return_error() -> LauncherResult<()>; - + #[taurpc(alias = "openDevTools")] async fn open_dev_tools(webview_window: tauri::WebviewWindow); + + #[taurpc(alias = "getClustersGroupedByMajor")] + async fn get_clusters_grouped_by_major() -> LauncherResult>>; } #[taurpc::ipc_type] @@ -13,15 +18,33 @@ pub struct OneClientApiImpl; #[taurpc::resolvers] impl OneClientApi for OneClientApiImpl { - - async fn return_error(self) -> LauncherResult<()> { - let err = DaoError::NotFound.into(); - Err(err) - } - async fn open_dev_tools(self, webview_window: tauri::WebviewWindow) { #[cfg(feature = "devtools")] webview_window.open_devtools(); } + async fn get_clusters_grouped_by_major( + self, + ) -> LauncherResult>> { + let clusters = onelauncher_core::api::cluster::dao::get_all_clusters().await?; + + let mut mapped: HashMap> = HashMap::new(); + + for cluster in clusters { + // Assuming `mc_version` is a String and you have a function to parse major version + let split = &mut cluster.mc_version.split('.'); + split.next(); + if let Some(major) = split.next() { + // Convert the major version to a u32 + let major: u32 = match major.parse() { + Ok(v) => v, + Err(_) => continue, // Skip if parsing fails + }; + + mapped.entry(major).or_default().push(cluster); + } + } + + Ok(mapped) + } } diff --git a/apps/oneclient/desktop/src/api/statics.rs b/apps/oneclient/desktop/src/api/statics.rs index 396e6064..ea54474d 100644 --- a/apps/oneclient/desktop/src/api/statics.rs +++ b/apps/oneclient/desktop/src/api/statics.rs @@ -1,4 +1,5 @@ -use onelauncher_core::{constants::{NATIVE_ARCH, TARGET_OS}, store::Core}; +use onelauncher_core::constants::{NATIVE_ARCH, TARGET_OS}; +use onelauncher_core::store::Core; use serde::Serialize; use specta::Type; @@ -14,7 +15,6 @@ pub struct ProgramInfo { #[must_use] pub fn get_program_info() -> ProgramInfo { - ProgramInfo { launcher_version: Core::get().launcher_version.clone(), webview_version: tauri::webview_version().unwrap_or_else(|_| "UNKNOWN".into()), diff --git a/apps/oneclient/desktop/src/constants.rs b/apps/oneclient/desktop/src/constants.rs index bcc70c6d..6ccdf0c6 100644 --- a/apps/oneclient/desktop/src/constants.rs +++ b/apps/oneclient/desktop/src/constants.rs @@ -1,3 +1,3 @@ // === API === pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe"; -pub const DISCORD_CLIENT_ID: &str = "1274084193193955408"; \ No newline at end of file +pub const DISCORD_CLIENT_ID: &str = "1274084193193955408"; diff --git a/apps/oneclient/desktop/src/lib.rs b/apps/oneclient/desktop/src/lib.rs index 06b3d0f8..bab5afdc 100644 --- a/apps/oneclient/desktop/src/lib.rs +++ b/apps/oneclient/desktop/src/lib.rs @@ -7,10 +7,12 @@ use onelauncher_core::store::{Core, CoreOptions, Dirs, State}; use tauri::{Emitter, Manager}; use crate::api::commands::OneClientApi; +use crate::oneclient::initialize_oneclient; pub mod api; pub mod constants; pub mod ext; +pub mod oneclient; #[derive(Clone, serde::Serialize)] pub struct SingleInstancePayload { @@ -33,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() }; @@ -97,13 +100,17 @@ async fn initialize_state(handle: &tauri::AppHandle) -> LauncherResult<()> { pub async fn run() { initialize_core().await.expect("failed to initialize core"); + let app = initialize_tauri(tauri::Builder::default()) .await .expect("failed to initialize tauri"); + initialize_state(app.handle()) .await .expect("failed to initialize state"); + initialize_oneclient().await; + app.run(|_, _| {}); } diff --git a/apps/oneclient/desktop/src/oneclient/mod.rs b/apps/oneclient/desktop/src/oneclient/mod.rs new file mode 100644 index 00000000..d0be6292 --- /dev/null +++ b/apps/oneclient/desktop/src/oneclient/mod.rs @@ -0,0 +1,74 @@ +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; +} + +struct OnlineCluster { + mc_major_version: u8, + mc_minor_versions: Vec, + mc_loaders: Vec, +} + +async fn init_clusters() { + // TODO: use online metadata stuff + let online_clusters: Vec = vec![ + OnlineCluster { + mc_major_version: 21, + mc_minor_versions: vec![1, 2, 4, 7], + mc_loaders: vec![GameLoader::Fabric, GameLoader::Forge], + }, + OnlineCluster { + mc_major_version: 8, + mc_minor_versions: vec![9], + mc_loaders: vec![GameLoader::Forge], + }, + ]; + + for cluster in online_clusters { + for minor_version in cluster.mc_minor_versions { + let mc_version = format!("1.{}.{}", cluster.mc_major_version, minor_version); + + for loader in &cluster.mc_loaders { + if let Err(e) = create_cluster_if_not_exist( + &format!("{mc_version} {loader}"), + &mc_version, + *loader, + None, + None, + ) + .await + { + send_error!("failed to create cluster for {}: {}", mc_version, e); + } + } + } + } +} + +async fn create_cluster_if_not_exist( + name: &str, + mc_version: &str, + mc_loader: GameLoader, + mc_loader_version: Option<&str>, + icon_url: Option, +) -> LauncherResult> { + let clusters = onelauncher_core::api::cluster::dao::get_clusters_by_version(mc_version).await?; + if clusters.len() > 1 { + return Ok(None); + } + + onelauncher_core::api::cluster::create_cluster( + name, + mc_version, + mc_loader, + mc_loader_version, + icon_url, + ) + .await + .map(Some) +} diff --git a/apps/oneclient/frontend/package.json b/apps/oneclient/frontend/package.json index 7a5ea661..a32c6add 100644 --- a/apps/oneclient/frontend/package.json +++ b/apps/oneclient/frontend/package.json @@ -33,6 +33,7 @@ "react": "catalog:", "react-aria-components": "catalog:", "react-dom": "catalog:", + "react-just-parallax": "catalog:", "tailwind-merge": "catalog:", "tailwind-variants": "catalog:", "tailwindcss": "catalog:", diff --git a/apps/oneclient/frontend/src/assets/backgrounds/CavesAndCliffs.jpg b/apps/oneclient/frontend/src/assets/backgrounds/CavesAndCliffs.jpg new file mode 100644 index 00000000..58fe22ff Binary files /dev/null and b/apps/oneclient/frontend/src/assets/backgrounds/CavesAndCliffs.jpg differ diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts index 6ba1b768..7c685d6f 100644 --- a/apps/oneclient/frontend/src/bindings.gen.ts +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -1,164 +1,251 @@ // This file has been generated by Specta. DO NOT EDIT. -import type { InferCommandOutput } from 'taurpc'; -import { createTauRPCProxy as createProxy } from 'taurpc'; +import { createTauRPCProxy as createProxy, type InferCommandOutput } from 'taurpc' +type TAURI_CHANNEL = (response: T) => void -type TAURI_CHANNEL = (response: T) => void; -export type ClusterError = { type: 'InvalidVersion'; data: string } | { type: 'MissingJavaVersion'; data: string } | { type: 'ClusterDownloading'; data: string } | { type: 'ClusterAlreadyRunning'; data: string }; +export type ClusterError = { type: "InvalidVersion"; data: string } | { type: "MissingJavaVersion"; data: string } | { type: "ClusterDownloading"; data: string } | { type: "ClusterAlreadyRunning"; data: string } -export interface 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 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 ClusterStage = "notready" | "downloading" | "repairing" | "ready" -export interface ClusterUpdate { name: string | null; icon_url: Icon | null } +export type ClusterUpdate = { name: string | null; icon_url: Icon | null } -export interface CreateCluster { name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; icon: 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 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 DaoError = { type: "NotFound"; data: string } | { type: "AlreadyExists"; data: string } | { type: "InvalidValue"; data: string } -export interface DirectoryError { type: 'BaseDir'; data: string } +export type DbVec = T[] -export type DiscordError = { type: 'MissingClientId'; data: string } | { type: 'ConnectError'; data: string }; +export type DirectoryError = { type: "BaseDir"; data: string } -export type GameLoader = 'vanilla' | 'forge' | 'neoforge' | 'quilt' | 'fabric' | 'legacyfabric'; +export type DiscordError = { type: "MissingClientId"; data: string } | { type: "ConnectError"; data: string } -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 Filters = { game_versions: string[] | null; loaders: GameLoader[] | null; categories: string[] | null; package_types: PackageType[] | null } -export type Icon = string; +export type GameLoader = "vanilla" | "forge" | "neoforge" | "quilt" | "fabric" | "legacyfabric" -export type IncompatiblePackageType = { type: 'McVersion'; data: string } | { type: 'Loader'; data: string }; +export type IOError = { type: "InvalidAbsolutePath"; data: string } | { type: "IOErrorWrapper"; data: string } | { type: "IOError"; data: string } | { type: "DeserializeError"; data: string } | { type: "AsyncZipError"; data: string } -export interface IngressError { type: 'NotFound'; data: string } +export type Icon = string -export interface IngressPayload { id: string; message: string; ingress_type: IngressType; percent: number | null; total: number } +export type IncompatiblePackageType = { type: "McVersion"; data: string } | { type: "Loader"; data: string } -export type IngressType = { Download: { file_name: string } } | 'JavaPrepare' | 'JavaCheck' | 'JavaLocate' | 'MinecraftDownload' | { PrepareCluster: { cluster_name: string } }; +export type IngressError = { type: "NotFound"; data: string } -export type JavaError = { type: 'ParseVersion'; data: string } | { type: 'Execute'; data: string } | { type: 'MissingJava'; data: string }; +export type IngressPayload = { id: string; message: string; ingress_type: IngressType; percent: number | null; total: number } -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 IngressType = { Download: { file_name: string } } | "JavaPrepare" | "JavaCheck" | "JavaLocate" | "MinecraftDownload" | { PrepareCluster: { cluster_name: string } } -export type MessageLevel = 'Info' | 'Warn' | 'Error'; +export type JavaError = { type: "ParseVersion"; data: string } | { type: "Execute"; data: string } | { type: "MissingJava"; data: string } -export interface MessagePayload { level: MessageLevel; message: 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 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 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 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 }; +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 interface MinecraftCredentials { +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`]. - */ - expires: string; +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`]. + */ +expires: string } -} +export type PackageAuthor = { Team: { team_id: string; org_id: string | null } } | { Users: ManagedUser[] } -export type PackageError = { type: 'NoPrimaryFile'; data: string } | { type: 'IsModPack'; data: string } | { type: 'Incompatible'; data: IncompatiblePackageType }; +export type PackageDependencyType = "required" | "optional" | "embedded" | "incompatible" -export interface Process { pid: number; started_at: string; cluster_id: bigint; post_hook: string | null; account_id: string } +/** + * + */ +export type PackageDonationPlatform = "patreon" | "buymeacoffee" | "paypal" | "github" | "kofi" | "other" -export type ProcessError = { type: 'HookUnsuccessful'; data: string } | { type: 'NoPid'; data: string }; +export type PackageDonationUrl = { id: PackageDonationPlatform; url: 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 PackageError = { type: "NoPrimaryFile"; data: string } | { type: "IsModPack"; data: string } | { type: "Incompatible"; data: IncompatiblePackageType } -export interface Resolution { width: number; height: number } +export type PackageGallery = { url: string; thumbnail_url: string; title: string | null; description: string | null; featured: boolean | null } -export interface 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 PackageLicense = { id: string; name: string; url: string | null } -export interface Settings { global_game_settings: SettingProfileModel; allow_parallel_running_clusters: boolean; enable_gamemode: boolean; discord_enabled: boolean; max_concurrent_requests: bigint; settings_version: number } +/** + * + */ +export type PackageLinks = { website: string | null; issues: string | null; source: string | null; wiki: string | null; donation: PackageDonationUrl[] | null; discord: string | null } -export type SettingsOsExtra = Record; +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; mc_loader: DbVec; 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 = { 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 interface Version { +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; - -} +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 = +export type VersionType = /** * A major version, which is stable for all players to use */ - 'release' | +"release" | /** * An experimental version, which is unstable and used for feature previews and beta testing */ - 'snapshot' | +"snapshot" | /** * The oldest versions before the game was released */ - 'old_alpha' | +"old_alpha" | /** * Early versions of the game */ - 'old_beta'; +"old_beta" -const ARGS_MAP = { oneclient: '{"open_dev_tools":[],"return_error":[]}', core: '{"getClusterById":["id"],"getProfileOrDefault":["name"],"readSettings":[],"getClusters":[],"removeUser":["uuid"],"writeSettings":["setting"],"removeCluster":["id"],"createCluster":["options"],"getGameVersions":[],"openMsaLogin":[],"launchCluster":["id","uuid"],"setDefaultUser":["uuid"],"getDefaultUser":["fallback"],"updateClusterById":["id","request"],"getLoadersForVersion":["mc_version"],"getUser":["uuid"],"getUsers":[],"getGlobalProfile":[]}', events: '{"ingress":["event"],"message":["event"],"process":["event"]}' }; -export interface Router { - core: { getClusters: () => Promise>; getClusterById: (id: bigint) => Promise; removeCluster: (id: bigint) => Promise; createCluster: (options: CreateCluster) => Promise; launchCluster: (id: bigint, uuid: string | null) => Promise; updateClusterById: (id: bigint, request: ClusterUpdate) => Promise; getProfileOrDefault: (name: string | null) => Promise; getGlobalProfile: () => Promise; getGameVersions: () => Promise>; getLoadersForVersion: (mcVersion: string) => Promise>; getUsers: () => Promise>; getUser: (uuid: string) => Promise; removeUser: (uuid: string) => Promise; getDefaultUser: (fallback: boolean | null) => Promise; setDefaultUser: (uuid: string | null) => Promise; openMsaLogin: () => Promise; readSettings: () => Promise; writeSettings: (setting: Settings) => Promise }; - oneclient: { return_error: () => Promise; open_dev_tools: () => Promise }; - events: { ingress: (event: IngressPayload) => Promise; message: (event: MessagePayload) => Promise; process: (event: ProcessPayload) => Promise }; -} +const ARGS_MAP = { 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}', 'core':'{"setDefaultUser":["uuid"],"openMsaLogin":[],"getClusters":[],"removeUser":["uuid"],"getWorlds":["id"],"getPackage":["provider","slug"],"getMultiplePackages":["provider","slugs"],"updateClusterById":["id","request"],"readSettings":[],"searchPackages":["provider","query"],"getPackageVersions":["provider","slug","mc_versions","loaders","offset","limit"],"getPackageUser":["provider","slug"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getLoadersForVersion":["mc_version"],"getUsersFromAuthor":["provider","author"],"getUsers":[],"launchCluster":["id","uuid"],"getUser":["uuid"],"getProfileOrDefault":["name"],"getClusterById":["id"],"getDefaultUser":["fallback"],"writeSettings":["setting"],"createCluster":["options"],"removeCluster":["id"],"getGlobalProfile":[],"getGameVersions":[],"getScreenshots":["id"],"updateClusterProfile":["name","profile"]}', 'events':'{"message":["event"],"process":["event"],"ingress":["event"]}' } +export type Router = { 'core': { getClusters: () => Promise, +getClusterById: (id: bigint) => Promise, +removeCluster: (id: bigint) => Promise, +createCluster: (options: CreateCluster) => Promise, +launchCluster: (id: bigint, uuid: string | null) => Promise, +updateClusterById: (id: bigint, request: ClusterUpdate) => Promise, +getScreenshots: (id: bigint) => Promise, +getWorlds: (id: bigint) => Promise, +getProfileOrDefault: (name: string | null) => Promise, +getGlobalProfile: () => Promise, +updateClusterProfile: (name: string, profile: ProfileUpdate) => Promise, +getGameVersions: () => Promise, +getLoadersForVersion: (mcVersion: string) => Promise, +getUsers: () => Promise, +getUser: (uuid: string) => Promise, +removeUser: (uuid: string) => Promise, +getDefaultUser: (fallback: boolean | null) => Promise, +setDefaultUser: (uuid: string | null) => Promise, +openMsaLogin: () => Promise, +readSettings: () => Promise, +writeSettings: (setting: Settings) => Promise, +searchPackages: (provider: Provider, query: SearchQuery) => Promise>, +getPackage: (provider: Provider, slug: string) => Promise, +getMultiplePackages: (provider: Provider, slugs: string[]) => Promise, +getPackageVersions: (provider: Provider, slug: string, mcVersions: string[] | null, loaders: GameLoader[] | null, offset: bigint, limit: bigint) => Promise>, +getPackageUser: (provider: Provider, slug: string) => Promise, +downloadPackage: (provider: Provider, packageId: string, versionId: string, clusterId: bigint, skipCompatibility: boolean | null) => Promise, +getUsersFromAuthor: (provider: Provider, author: PackageAuthor) => Promise }, +'events': { ingress: (event: IngressPayload) => Promise, +message: (event: MessagePayload) => Promise, +process: (event: ProcessPayload) => Promise }, +'oneclient': { openDevTools: () => Promise, +getClustersGroupedByMajor: () => Promise> } }; -export type { InferCommandOutput }; -export const createTauRPCProxy = () => createProxy(ARGS_MAP); + +export type { InferCommandOutput } +export const createTauRPCProxy = () => createProxy(ARGS_MAP) diff --git a/apps/oneclient/frontend/src/components/GameBackground.tsx b/apps/oneclient/frontend/src/components/GameBackground.tsx index ef7cebd1..6dca5458 100644 --- a/apps/oneclient/frontend/src/components/GameBackground.tsx +++ b/apps/oneclient/frontend/src/components/GameBackground.tsx @@ -1,9 +1,13 @@ +/* eslint-disable perfectionist/sort-imports -- nah */ import type { CSSProperties } from 'react'; import { twMerge } from 'tailwind-merge'; -import HypixelSkyblockHub from '../assets/backgrounds/HypixelSkyblockHub.png'; + +import CavesAndCliffs from '@/assets/backgrounds/CavesAndCliffs.jpg'; +import HypixelSkyblockHub from '@/assets/backgrounds/HypixelSkyblockHub.png'; const GameBackgrounds = { HypixelSkyblockHub, + CavesAndCliffs, }; export type GameBackgroundName = keyof typeof GameBackgrounds; @@ -19,7 +23,7 @@ export function GameBackground({ name, className = '', style = undefined }: Game return (
); diff --git a/apps/oneclient/frontend/src/components/Navbar.tsx b/apps/oneclient/frontend/src/components/Navbar.tsx index 005a075c..3b81258b 100644 --- a/apps/oneclient/frontend/src/components/Navbar.tsx +++ b/apps/oneclient/frontend/src/components/Navbar.tsx @@ -1,5 +1,5 @@ import type { ButtonProps } from '@onelauncher/common/components'; -import type { LinkProps } from '@tanstack/react-router'; +import type { LinkProps, RegisteredRouter } from '@tanstack/react-router'; import LauncherLogo from '@/assets/logos/oneclient.svg?react'; import { Button } from '@onelauncher/common/components'; import { Link } from '@tanstack/react-router'; @@ -19,9 +19,9 @@ export function Navbar() {
- Home - Clusters - Accounts + Home + Clusters + Accounts
@@ -54,7 +54,7 @@ export function Navbar() { function NavbarLink({ to, children, -}: LinkProps) { +}: LinkProps<'a', RegisteredRouter, '/app'>) { return (
diff --git a/apps/oneclient/frontend/src/components/SheetPage.tsx b/apps/oneclient/frontend/src/components/SheetPage.tsx index a66e0b32..f226abf3 100644 --- a/apps/oneclient/frontend/src/components/SheetPage.tsx +++ b/apps/oneclient/frontend/src/components/SheetPage.tsx @@ -1,23 +1,32 @@ -import type { MotionValue } from 'motion/react'; -import useAppShellStore from '@/stores/appShellStore'; -import { getLocationName } from '@/utils/locationMapping'; +import type { HTMLMotionProps, MotionValue } from 'motion/react'; import { Button } from '@onelauncher/common/components'; -import { Outlet, useNavigate } from '@tanstack/react-router'; +import { useCanGoBack, useRouter } from '@tanstack/react-router'; import { ArrowLeftIcon } from '@untitled-theme/icons-react'; import { motion, useScroll, useTransform } from 'motion/react'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; -import React, { useRef } from 'react'; +import React, { useCallback, useRef } from 'react'; +import { twMerge } from 'tailwind-merge'; export function SheetPage({ headerSmall, headerLarge, + children, + scrollContainerRef, }: { headerSmall: React.ReactNode; headerLarge: React.ReactNode; + children?: React.ReactNode; + scrollContainerRef?: React.RefObject; }) { const scrollContainer = useRef(null); const headerLargeRef = useRef(null); + const setScrollContainerRef = useCallback((el: HTMLElement | null) => { + scrollContainer.current = el; + if (scrollContainerRef) + scrollContainerRef.current = el; + }, [scrollContainerRef]); + const { scrollYProgress } = useScroll({ axis: 'y', container: scrollContainer, @@ -48,20 +57,13 @@ export function SheetPage({
- scrollContainer.current = ref?.osInstance()?.elements().content ?? null}> -
+ setScrollContainerRef(ref?.osInstance()?.elements().content ?? null)}> +
- {/* } /> */} {headerLarge} - - - + {children}
@@ -69,29 +71,45 @@ export function SheetPage({ ); } +SheetPage.Content = ({ + children, + className, + ...rest +}: Omit, 'animate' | 'initial'>) => ( + + {children} + +); + function GoBackButton() { - const prevLocation = useAppShellStore(state => state.prevLocation); - const navigate = useNavigate(); + const router = useRouter(); + const canGoBack = useCanGoBack(); - const onClick = () => { - if (!prevLocation) - return; - - navigate({ - to: prevLocation.pathname, - search: prevLocation.search, - hash: prevLocation.hash, - state: prevLocation.state, - }); - }; - - if (!prevLocation) + if (!canGoBack) return undefined; + const onClick = () => { + router.history.back(); + }; + return ( - ); } @@ -111,21 +129,21 @@ function HeaderBackgrounds({ }} >
-
+
); } diff --git a/apps/oneclient/frontend/src/components/Tabs.tsx b/apps/oneclient/frontend/src/components/Tabs.tsx new file mode 100644 index 00000000..4104769c --- /dev/null +++ b/apps/oneclient/frontend/src/components/Tabs.tsx @@ -0,0 +1,52 @@ +import type { LinkProps } from '@tanstack/react-router'; +import { Link } from '@tanstack/react-router'; +import { twMerge } from 'tailwind-merge'; + +export function TabList({ + className, + children, + floating = false, + ref, +}: React.HTMLAttributes & { + floating?: boolean; + ref?: React.Ref; +}) { + return ( +
+ {children} +
+ ); +} + +export function Tab({ + to, + children, +}: Omit) { + return ( +
+ + {children} + +
+ ); +} diff --git a/apps/oneclient/frontend/src/components/index.ts b/apps/oneclient/frontend/src/components/index.ts index 9ca292a7..97466938 100644 --- a/apps/oneclient/frontend/src/components/index.ts +++ b/apps/oneclient/frontend/src/components/index.ts @@ -2,3 +2,4 @@ export * from './GameBackground'; export * from './Loader'; export * from './Navbar'; export * from './Spinner'; +export * from './Tabs'; diff --git a/apps/oneclient/frontend/src/routes/app/cluster/overview.tsx b/apps/oneclient/frontend/src/routes/app/cluster/overview.tsx new file mode 100644 index 00000000..00c23208 --- /dev/null +++ b/apps/oneclient/frontend/src/routes/app/cluster/overview.tsx @@ -0,0 +1,27 @@ +import { SheetPage } from '@/components/SheetPage'; +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/app/cluster/overview')({ + component: RouteComponent, + beforeLoad(ctx) { + console.log('beforeLoad overview', ctx.search); + }, +}); + +function RouteComponent() { + const { cluster } = Route.useRouteContext(); + + return ( + +
+

Cluster Overview

+

Welcome to the cluster overview page. Here you can find information about your clusters.

+
+ +
+

test

+ {/* Additional content can be added here */} +
+
+ ); +} diff --git a/apps/oneclient/frontend/src/routes/app/cluster/route.tsx b/apps/oneclient/frontend/src/routes/app/cluster/route.tsx new file mode 100644 index 00000000..8423918b --- /dev/null +++ b/apps/oneclient/frontend/src/routes/app/cluster/route.tsx @@ -0,0 +1,132 @@ +import { Tab, TabList } from '@/components'; +import { SheetPage } from '@/components/SheetPage'; +import { bindings } from '@/main'; +import { prettifyLoader } from '@/utils/loaders'; +import { getVersionInfoOrDefault } from '@/utils/versionMap'; +import { Button } from '@onelauncher/common/components'; +import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'; +import { useMotionValueEvent, useScroll } from 'motion/react'; +import { useRef, useState } from 'react'; + +export interface ClusterRouteSearchParams { + clusterId: number; +} + +export const Route = createFileRoute('/app/cluster')({ + component: RouteComponent, + validateSearch: (search): ClusterRouteSearchParams => { + console.log('validateSearch', search); + return { + clusterId: Number(search.clusterId), + }; + }, + async beforeLoad({ context, search }) { + console.log('beforeLoad', search); + if (!search.clusterId) + throw redirect({ to: '/app/clusters', from: '/app/cluster' }); + + const query = context.queryClient.ensureQueryData({ + queryKey: ['getClusterById', search.clusterId], + queryFn: () => bindings.core.getClusterById(search.clusterId as unknown as bigint), + }); + + const cluster = await query; + if (!cluster) + throw redirect({ to: '/app/clusters', from: '/app/cluster' }); + + return { + cluster, + }; + }, +}); + +function RouteComponent() { + const tabListRef = useRef(null); + const scrollContainerRef = useRef(null); + + const { scrollYProgress } = useScroll({ + axis: 'y', + container: scrollContainerRef, + target: tabListRef, + layoutEffect: false, + offset: ['end end', 'start start'], + }); + + const [floating, setFloating] = useState(false); + useMotionValueEvent(scrollYProgress, 'change', (v) => { + setFloating(v >= 0.9); + }); + + const search = Route.useSearch(); + + return ( + } + headerSmall={} + scrollContainerRef={scrollContainerRef} + > + + Overview + Logs + + +
+ +
+
+ ); +} + +function HeaderLarge() { + const { cluster } = Route.useRouteContext(); + const versionInfo = getVersionInfoOrDefault(cluster.mc_version); + + const launch = () => { + bindings.core.launchCluster(cluster.id as unknown as bigint, null); + }; + + return ( +
+
+
+ {versionInfo.tags.map(tag => ( + + {tag} + + ))} +
+

{prettifyLoader(cluster.mc_loader)} {cluster.mc_version}

+

{versionInfo.longDescription}

+
+ + +
+ ); +} + +function HeaderSmall() { + const { cluster } = Route.useRouteContext(); + + const launch = () => { + bindings.core.launchCluster(cluster.id as unknown as bigint, null); + }; + + return ( +
+

{prettifyLoader(cluster.mc_loader)} {cluster.mc_version}

+ + +
+ ); +} diff --git a/apps/oneclient/frontend/src/routes/app/clusters.tsx b/apps/oneclient/frontend/src/routes/app/clusters.tsx new file mode 100644 index 00000000..a003a87d --- /dev/null +++ b/apps/oneclient/frontend/src/routes/app/clusters.tsx @@ -0,0 +1,269 @@ +import type { GameLoader } from '@/bindings.gen'; +import { GameBackground } from '@/components'; +import { SheetPage } from '@/components/SheetPage'; +import { bindings } from '@/main'; +import useClusterStore from '@/stores/clusterStore'; +import { prettifyLoader } from '@/utils/loaders'; +import { getVersionInfo, getVersionInfoOrDefault, parseMcVersion } from '@/utils/versionMap'; +import { useCommandSuspense } from '@onelauncher/common'; +import { Button, Dropdown } from '@onelauncher/common/components'; +import { createFileRoute } from '@tanstack/react-router'; +import { ArrowRightIcon } from '@untitled-theme/icons-react'; +import { useCallback, useMemo, useState } from 'react'; +import { twMerge } from 'tailwind-merge'; + +export const Route = createFileRoute('/app/clusters')({ + component: RouteComponent, +}); + +function RouteComponent() { + const prevMinorVersions = useClusterStore(state => state.minorVersions); + const setMinorVersion = useClusterStore(state => state.setMinorVersion); + const modLoaders = useClusterStore(state => state.modLoaders); + const setModLoader = useClusterStore(state => state.setModLoader); + + const navigate = Route.useNavigate(); + + const { data: clusters } = useCommandSuspense('getClustersGroupedByMajor', bindings.oneclient.getClustersGroupedByMajor); + + const loadMinorVersion = useCallback((major: number) => { + const existing = prevMinorVersions.find(v => v.major === major); + if (existing) + return existing.minor; + return undefined; + }, [prevMinorVersions]); + + const [majorVersion, setMajorVersion] = useState(() => { + const keys = Object.keys(clusters); + if (keys.length === 0) + return 0; + + return Number.parseInt(keys[0]); + }); + + const [activeMinorVersion, setActiveMinorVersion] = useState(); + + const minorVersions = useMemo(() => { + const list = clusters[majorVersion]; + if (!list || list.length === 0) + return []; + + const versions = list.map(c => parseMcVersion(c.mc_version)?.minor).filter((v): v is number => v !== undefined); + const sorted = Array.from(new Set(versions)).sort((a, b) => a - b); + + // attempt to load the minor version from localStorage + const storedMinor = loadMinorVersion(majorVersion) || sorted[0]; + setActiveMinorVersion(storedMinor); + + return sorted; + }, [clusters, loadMinorVersion, majorVersion]); + + const modLoader = useMemo(() => { + if (!activeMinorVersion) + return undefined; + + return modLoaders[`${majorVersion}.${activeMinorVersion}`]; + }, [modLoaders, majorVersion, activeMinorVersion]); + + const loaders = useMemo(() => { + const list = clusters[majorVersion]; + if (!list || list.length === 0) + return []; + + const loadersSet: Set = new Set(); + for (const cluster of list) + if (cluster.mc_loader !== 'vanilla') + loadersSet.add(cluster.mc_loader); + + return Array.from(loadersSet).sort(); + }, [clusters, majorVersion]); + + const cluster = useMemo(() => { + const list = clusters[majorVersion]; + if (!activeMinorVersion) + return list?.[0]; + + return list?.find((c) => { + const version = c.mc_version.endsWith(activeMinorVersion.toString()); + const loaderCheck = modLoader ? c.mc_loader === modLoader : true; + return version && loaderCheck; + }); + }, [clusters, majorVersion, activeMinorVersion, modLoader]); + + const versionInfo = useMemo(() => getVersionInfoOrDefault(cluster?.mc_version), [cluster]); + + return ( + } + headerSmall={} + > +
+
+ +
+ {Object.keys(clusters).map((majorStr) => { + const major = Number.parseInt(majorStr, 10); + const isSelected = major === majorVersion; + const info = getVersionInfo(major); + + if (!info) + return undefined; + + return ( + setMajorVersion(major)} + tags={[...new Set(clusters[major]?.flatMap(c => prettifyLoader(c.mc_loader)))]} + /> + ); + })} +
+
+ + + + +
+

Version {cluster?.mc_version}

+

{versionInfo.longDescription}

+ + {minorVersions.length > 1 && ( +
+

Minor Version

+ + { + setActiveMinorVersion(Number(selected)); + setMinorVersion(majorVersion, Number(selected)); + }} + selectedKey={activeMinorVersion} + > + {minorVersions.map((minorVersion) => { + const fullVer = `${versionInfo.prettyName}.${minorVersion}`; + return ( + + {fullVer} + + ); + })} + +
+ )} + + {loaders.length > 1 && ( +
+

Mod Loader

+ { + if (!activeMinorVersion) + return; + + setModLoader(`${majorVersion}.${activeMinorVersion}`, selected as GameLoader); + }} + selectedKey={activeMinorVersion ? (modLoaders[`${majorVersion}.${activeMinorVersion}`] ?? loaders[0]) : loaders[0]} + > + {loaders.map(loader => ( + + {prettifyLoader(loader)} + + ))} + +
+ )} + +
+ + +
+ +
+
+
+
+ ); +} + +function HeaderLarge({ ref }: { ref?: React.Ref }) { + return ( +
+
+

Clusters

+

Something something in corporate style fashion about picking your preferred gamemodes and versions and optionally loader so that oneclient can pick something for them

+
+
+ ); +} + +function HeaderSmall() { + return ( +

Clusters

+ ); +} + +function ClusterEntry({ + isSelected, + major_version, + onClick, + tags, +}: { + isSelected: boolean; + major_version: number; + onClick: () => unknown; + tags: Array; +}) { + const info = getVersionInfo(major_version); + + if (!info) + return undefined; + + return ( +
+ +
+
+ +
+ {info.tags.concat(tags).map(tag => ( +
+ {tag} +
+ ))} +
+ +

Version {info.prettyName}

+
+ ); +} diff --git a/apps/oneclient/frontend/src/routes/app/clusters/index.tsx b/apps/oneclient/frontend/src/routes/app/clusters/index.tsx deleted file mode 100644 index 74c9235d..00000000 --- a/apps/oneclient/frontend/src/routes/app/clusters/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; - -export const Route = createFileRoute('/app/clusters/')({ - component: RouteComponent, -}); - -function RouteComponent() { - return ( -
-
Hello "/app/clusters/"!
- -
- -
Hello "/app/clusters/"!
-
- ); -} diff --git a/apps/oneclient/frontend/src/routes/app/clusters/route.tsx b/apps/oneclient/frontend/src/routes/app/clusters/route.tsx deleted file mode 100644 index 6625ff2c..00000000 --- a/apps/oneclient/frontend/src/routes/app/clusters/route.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { SheetPage } from '@/components/SheetPage'; -import { Button } from '@onelauncher/common/components'; -import { createFileRoute } from '@tanstack/react-router'; - -export const Route = createFileRoute('/app/clusters')({ - component: RouteComponent, -}); - -function RouteComponent() { - return ( - } - headerSmall={} - /> - ); -} - -function HeaderLarge({ ref }: { ref?: React.Ref }) { - return ( -
-
-

Header

-

Tailored for Hypixel Skyblock.

-
- - -
- ); -} - -function HeaderSmall() { - return ( -
-
-

Header

-
- - -
- ); -} diff --git a/apps/oneclient/frontend/src/routes/app/index.tsx b/apps/oneclient/frontend/src/routes/app/index.tsx index fd6def29..c15fc4e0 100644 --- a/apps/oneclient/frontend/src/routes/app/index.tsx +++ b/apps/oneclient/frontend/src/routes/app/index.tsx @@ -1,14 +1,16 @@ -import type { ClusterModel } from '@/bindings.gen'; +import type { GameLoader } from '@/bindings.gen'; import type { HTMLAttributes } from 'react'; import { GameBackground } from '@/components'; import { bindings } from '@/main'; +import { prettifyLoader } from '@/utils/loaders'; import { animations, transitions } from '@/utils/motion'; import { getVersionInfo } from '@/utils/versionMap'; import { useCommandSuspense } from '@onelauncher/common'; import { Button } from '@onelauncher/common/components'; import { createFileRoute } from '@tanstack/react-router'; -import { DotsGridIcon, Settings04Icon } from '@untitled-theme/icons-react'; +import { DotsGridIcon, PlusIcon, Settings04Icon } from '@untitled-theme/icons-react'; import { motion } from 'motion/react'; +import { MouseParallax } from 'react-just-parallax'; import { twMerge } from 'tailwind-merge'; export const Route = createFileRoute('/app/')({ @@ -16,30 +18,32 @@ export const Route = createFileRoute('/app/')({ }); function RouteComponent() { - const { data: clusters } = useCommandSuspense>( + const { data: clusters } = useCommandSuspense( 'getClusters', bindings.core.getClusters, { - select: ((data: Array) => { - const sorted = data.sort((a, b) => { - if (a.last_played && b.last_played) - return new Date(b.last_played).getTime() - new Date(a.last_played).getTime(); - else if (a.last_played) - return -1; // a has last_played, b does not - else if (b.last_played) - return 1; // b has last_played, a does not + select(data) { + const sorted = data + .filter(cluster => cluster.last_played !== null) + .sort((a, b) => { + if (a.last_played && b.last_played) + return new Date(b.last_played).getTime() - new Date(a.last_played).getTime(); + else if (a.last_played) + return -1; // a has last_played, b does not + else if (b.last_played) + return 1; // b has last_played, a does not - const aVersion = a.mc_version.replaceAll('.', ''); - const bVersion = b.mc_version.replaceAll('.', ''); - return Number.parseInt(bVersion) - Number.parseInt(aVersion); - }); + const aVersion = a.mc_version.replaceAll('.', ''); + const bVersion = b.mc_version.replaceAll('.', ''); + return Number.parseInt(bVersion) - Number.parseInt(aVersion); + }); return [ sorted[0], sorted[1], - undefined, + sorted[3], ]; - }) as (data: Array) => Array, + }, }, ); @@ -51,14 +55,25 @@ function RouteComponent() {
- +
- {clusters.map((cluster, index) => ( - - ))} + {clusters.map((cluster) => { + // eslint-disable-next-line ts/no-unnecessary-condition -- rule broken + return cluster === undefined + ? ( + + + + ) + : ( + + ); + })} @@ -69,30 +84,34 @@ function RouteComponent() { } interface RecentsCardProps { - version: string | undefined; + version: string; + loader: GameLoader; } const BLUR = '30px'; -function RecentsCard({ version }: RecentsCardProps) { +function RecentsCard({ version, loader }: RecentsCardProps) { const versionInfo = getVersionInfo(version); + if (!versionInfo) + return Unknown Version; + return ( - - {versionInfo && ( -
- + +
+ + + -
-
- -

{version}

+
- )} + +

{version} {prettifyLoader(loader)}

+
); } diff --git a/apps/oneclient/frontend/src/routes/app/route.tsx b/apps/oneclient/frontend/src/routes/app/route.tsx index 5de18320..b173dd03 100644 --- a/apps/oneclient/frontend/src/routes/app/route.tsx +++ b/apps/oneclient/frontend/src/routes/app/route.tsx @@ -3,9 +3,8 @@ import { LoaderSuspense, Navbar } from '@/components'; import { GameBackground } from '@/components/GameBackground'; import useAppShellStore from '@/stores/appShellStore'; import { AnimatedOutlet } from '@onelauncher/common/components'; -import { createFileRoute, useRouter } from '@tanstack/react-router'; -import { AnimatePresence } from 'motion/react'; -import { useEffect } from 'react'; +import { createFileRoute } from '@tanstack/react-router'; +import { MouseParallax } from 'react-just-parallax'; export const Route = createFileRoute('/app')({ component: RouteComponent, @@ -26,7 +25,7 @@ function RouteComponent() { animate: { opacity: 0 }, }} from={Route.id} - transition={{ duration: 0.3, bounce: 0.1, power: 0.2, type: 'spring' }} + transition={{ duration: 0.25, bounce: 0.1, power: 0.2, type: 'spring' }} />
@@ -37,22 +36,9 @@ function RouteComponent() { function AppShell({ children, }: PropsWithChildren) { - const router = useRouter(); - const setPrevLocation = useAppShellStore(state => state.setPrevLocation); - - useEffect(() => { - const unsub = router.subscribe('onBeforeNavigate', (e) => { - setPrevLocation(e.fromLocation ?? null); - }); - - return () => unsub(); - }, [router, setPrevLocation]); - return (
- - - + @@ -70,7 +56,7 @@ function BackgroundGradient() { return undefined; return ( -
+
{/* Linear black gradient: left -> right */}
- + + +
); } diff --git a/apps/oneclient/frontend/src/stores/appShellStore.ts b/apps/oneclient/frontend/src/stores/appShellStore.ts index 5dbe305e..e9380efe 100644 --- a/apps/oneclient/frontend/src/stores/appShellStore.ts +++ b/apps/oneclient/frontend/src/stores/appShellStore.ts @@ -1,22 +1,17 @@ -import type { ParsedLocation } from '@tanstack/react-router'; import { create } from 'zustand'; export interface AppShellStore { background: 'gradientOverlay' | 'none'; - prevLocation: ParsedLocation | null; } export interface AppShellStoreActions { setBackground: (background: AppShellStore['background']) => void; - setPrevLocation: (location: ParsedLocation | null) => void; } export const useAppShellStore = create()(set => ({ background: 'gradientOverlay', - prevLocation: null, setBackground: background => set({ background }), - setPrevLocation: location => set({ prevLocation: location }), })); export default useAppShellStore; diff --git a/apps/oneclient/frontend/src/stores/clusterStore.ts b/apps/oneclient/frontend/src/stores/clusterStore.ts new file mode 100644 index 00000000..2e6535be --- /dev/null +++ b/apps/oneclient/frontend/src/stores/clusterStore.ts @@ -0,0 +1,45 @@ +import type { GameLoader } from '@/bindings.gen'; +import type { ParsedMcVersion } from '@/utils/versionMap'; +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +export type MajorVersion = number; +export type MinorVersion = number; + +export interface ClusterStore { + modLoaders: Record<`${MajorVersion}.${MinorVersion}`, GameLoader>; + minorVersions: Array; +} + +export interface ClusterStoreActions { + setModLoader: (version: `${MajorVersion}.${MinorVersion}`, loader: GameLoader) => void; + setMinorVersion: (major: number, minor: number) => void; +} + +export const useClusterStore = create()(persist( + set => ({ + modLoaders: {}, + minorVersions: [], + + setModLoader: (version, loader) => set(state => ({ + modLoaders: { + ...state.modLoaders, + [version]: loader, + }, + })), + setMinorVersion: (major, minor) => set((state) => { + const existing = state.minorVersions.find(v => v.major === major); + if (existing) + existing.minor = minor; + else + state.minorVersions.push({ major, minor }); + + return { minorVersions: [...state.minorVersions] }; + }), + }), + { + name: 'oneclient:cluster_list', + }, +)); + +export default useClusterStore; diff --git a/apps/oneclient/frontend/src/utils/loaders.ts b/apps/oneclient/frontend/src/utils/loaders.ts new file mode 100644 index 00000000..27a433e7 --- /dev/null +++ b/apps/oneclient/frontend/src/utils/loaders.ts @@ -0,0 +1,18 @@ +import type { GameLoader } from '@/bindings.gen'; + +export function prettifyLoader(loader: GameLoader): string { + switch (loader) { + case 'fabric': + return 'Fabric'; + case 'forge': + return 'Forge'; + case 'neoforge': + return 'NeoForge'; + case 'quilt': + return 'Quilt'; + case 'vanilla': + return 'Vanilla'; + case 'legacyfabric': + return 'Legacy Fabric'; + } +} diff --git a/apps/oneclient/frontend/src/utils/locationMapping.ts b/apps/oneclient/frontend/src/utils/locationMapping.ts deleted file mode 100644 index 925b3e7f..00000000 --- a/apps/oneclient/frontend/src/utils/locationMapping.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { FileRouteTypes } from '@/routeTree.gen'; - -const mapping: Partial> = { - '/app': 'Home', - '/app/clusters': 'Clusters', -}; - -export function getLocationName( - to: FileRouteTypes['to'] | (string & {}) | undefined | null, -): string | undefined { - if (!to) - return undefined; - - if (to in mapping) - return mapping[to as keyof typeof mapping]; - - const mostSimilarPath = getMostSimilarPathMapping(to); - return mostSimilarPath; -} - -function getMostSimilarPathMapping( - to: string, -): FileRouteTypes['to'] | undefined { - let path = to; - - while (path !== '') { - if (Object.prototype.hasOwnProperty.call(mapping, path)) - return path as FileRouteTypes['to']; - - const lastSlash = path.lastIndexOf('/'); - - if (lastSlash === 0) - return Object.prototype.hasOwnProperty.call(mapping, '/') ? '/' : undefined; - - path = path.substring(0, lastSlash); - } - - return undefined; -} diff --git a/apps/oneclient/frontend/src/utils/versionMap.ts b/apps/oneclient/frontend/src/utils/versionMap.ts index e232f3db..ecf5b963 100644 --- a/apps/oneclient/frontend/src/utils/versionMap.ts +++ b/apps/oneclient/frontend/src/utils/versionMap.ts @@ -2,17 +2,33 @@ import type { GameBackgroundName } from '@/components'; export interface VersionInfo { backgroundName: GameBackgroundName; - description: string; + prettyName: string; + shortDescription: string; + longDescription: string; + tags: Array; } const VERSION_MAP: Record = { [-1]: { backgroundName: 'HypixelSkyblockHub', - description: 'Unknown Version', + shortDescription: 'Unknown Version', + longDescription: 'This version is not recognized or does not have specific information available.', + prettyName: '0.0', + tags: [], }, 8: { backgroundName: 'HypixelSkyblockHub', - description: 'The Bountiful Update', + prettyName: '1.8', + shortDescription: 'The Bountiful Update', + longDescription: 'The Bountiful Update is a major update that introduces new features, items, and gameplay mechanics to enhance the Minecraft experience. It focuses on expanding the game world and providing players with new challenges and adventures.', + tags: ['Legacy', 'Bountiful Update'], + }, + 21: { + backgroundName: 'CavesAndCliffs', + prettyName: '1.21', + shortDescription: 'The Tricky Trials Update', + longDescription: `Minecraft's 1.21 update, known as "Tricky Trials," primarily focuses on combat adventures and tinkering, introducing trial chambers, new copper block variants, a new crafting tool, and a new weapon. It also features new hostile mobs, paintings, and gameplay enhancements.`, + tags: ['Tricky Trials'], }, } as const; @@ -20,13 +36,12 @@ export function getVersionInfo(version: number | string | null | undefined): Ver if (version === undefined || version === null) return undefined; - let majorVersion; + let majorVersion: number | undefined; - if (typeof version === 'string') { - const parts = version.split('.'); - if (parts.length >= 2) - majorVersion = Number.parseInt(parts[1], 10); - } + if (typeof version === 'string') + majorVersion = parseMcVersion(version)?.major; + else if (typeof version === 'number') + majorVersion = version; if (majorVersion) return VERSION_MAP[majorVersion]; @@ -34,7 +49,29 @@ export function getVersionInfo(version: number | string | null | undefined): Ver return undefined; } -export function getVersionInfoOrDefault(version: number | string): VersionInfo { +export function getVersionInfoOrDefault(version: number | string | null | undefined): VersionInfo { const info = getVersionInfo(version); return info ?? VERSION_MAP[-1]; } + +export interface ParsedMcVersion { + major: number; + minor: number | undefined; +} + +export function parseMcVersion(version: string | null | undefined): ParsedMcVersion | undefined { + if (version === undefined || version === null) + return undefined; + + const parts = version.split('.'); + if (parts.length <= 1) // we need a.b.c where c is optional + return undefined; + + const major = Number.parseInt(parts[1], 10); + const minor = parts.length > 1 ? Number.parseInt(parts[2], 10) : undefined; + + return { + major, + minor: Number.isNaN(minor) ? undefined : minor, + }; +} diff --git a/apps/onelauncher/desktop/src/api/commands.rs b/apps/onelauncher/desktop/src/api/commands.rs index 58d7ddb5..0b73e846 100644 --- a/apps/onelauncher/desktop/src/api/commands.rs +++ b/apps/onelauncher/desktop/src/api/commands.rs @@ -7,7 +7,10 @@ pub trait OneLauncherApi { async fn open_dev_tools(webview_window: tauri::WebviewWindow); - async fn set_window_style(webview_window: tauri::WebviewWindow, decorations: bool); + async fn set_window_style( + webview_window: tauri::WebviewWindow, + decorations: bool, + ); } #[taurpc::ipc_type] @@ -15,7 +18,6 @@ pub struct OneLauncherApiImpl; #[taurpc::resolvers] impl OneLauncherApi for OneLauncherApiImpl { - async fn return_error(self) -> LauncherResult<()> { let err = DaoError::NotFound.into(); Err(err) @@ -26,7 +28,11 @@ impl OneLauncherApi for OneLauncherApiImpl { webview_window.open_devtools(); } - async fn set_window_style(self, webview_window: tauri::WebviewWindow, decorations: bool) { + async fn set_window_style( + self, + webview_window: tauri::WebviewWindow, + decorations: bool, + ) { webview_window.set_decorations(decorations).ok(); } } diff --git a/apps/onelauncher/desktop/src/api/statics.rs b/apps/onelauncher/desktop/src/api/statics.rs index 396e6064..ea54474d 100644 --- a/apps/onelauncher/desktop/src/api/statics.rs +++ b/apps/onelauncher/desktop/src/api/statics.rs @@ -1,4 +1,5 @@ -use onelauncher_core::{constants::{NATIVE_ARCH, TARGET_OS}, store::Core}; +use onelauncher_core::constants::{NATIVE_ARCH, TARGET_OS}; +use onelauncher_core::store::Core; use serde::Serialize; use specta::Type; @@ -14,7 +15,6 @@ pub struct ProgramInfo { #[must_use] pub fn get_program_info() -> ProgramInfo { - ProgramInfo { launcher_version: Core::get().launcher_version.clone(), webview_version: tauri::webview_version().unwrap_or_else(|_| "UNKNOWN".into()), diff --git a/apps/onelauncher/desktop/src/constants.rs b/apps/onelauncher/desktop/src/constants.rs index bcc70c6d..6ccdf0c6 100644 --- a/apps/onelauncher/desktop/src/constants.rs +++ b/apps/onelauncher/desktop/src/constants.rs @@ -1,3 +1,3 @@ // === API === pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe"; -pub const DISCORD_CLIENT_ID: &str = "1274084193193955408"; \ No newline at end of file +pub const DISCORD_CLIENT_ID: &str = "1274084193193955408"; diff --git a/apps/onelauncher/desktop/src/lib.rs b/apps/onelauncher/desktop/src/lib.rs index 2a3eb545..18498af3 100644 --- a/apps/onelauncher/desktop/src/lib.rs +++ b/apps/onelauncher/desktop/src/lib.rs @@ -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,6 +62,7 @@ async fn initialize_tauri(builder: tauri::Builder) -> 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); @@ -75,10 +77,8 @@ async fn initialize_tauri(builder: tauri::Builder) -> 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"); diff --git a/apps/onelauncher/frontend/src/styles/global.css b/apps/onelauncher/frontend/src/styles/global.css index 178da43f..b4e1abe8 100644 --- a/apps/onelauncher/frontend/src/styles/global.css +++ b/apps/onelauncher/frontend/src/styles/global.css @@ -15,6 +15,17 @@ --radius-4xl: 2rem; } +h1 { @apply text-[2rem] font-semibold; line-height: 1.5 !important; } +h2 { @apply text-[1.5rem] font-semibold; line-height: 1.25 !important; } +h3 { @apply text-[1.25rem] font-medium; } +h4 { @apply text-[1rem] font-medium; } +h5 { @apply text-[0.875rem] text-fg-secondary font-bold uppercase; } +h6 { @apply text-[0.75rem] text-fg-secondary font-medium uppercase; } + +code { + @apply rounded-md p-2 text-left; +} + /* i dont know where to put this */ .linearBlur::after { content: ''; diff --git a/eslint.config.js b/eslint.config.js index 4f8eb329..b2ce183f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -47,7 +47,9 @@ 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 + '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: [ diff --git a/lint-staged.config.mjs b/lint-staged.config.mjs index 55f002ac..f45ad4e5 100644 --- a/lint-staged.config.mjs +++ b/lint-staged.config.mjs @@ -8,7 +8,7 @@ const config = { '*.{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 + // 'cargo clippy -- -D warnings', // sadly clippy can't do this ], }; export default config; diff --git a/packages/core/src/api/cluster/content.rs b/packages/core/src/api/cluster/content.rs index d7bacaf5..97411227 100644 --- a/packages/core/src/api/cluster/content.rs +++ b/packages/core/src/api/cluster/content.rs @@ -1,39 +1,44 @@ use onelauncher_entity::clusters; -use crate::{error::LauncherResult, store::Dirs, utils::io}; +use crate::error::LauncherResult; +use crate::store::Dirs; +use crate::utils::io; pub async fn get_screenshots(cluster: &clusters::Model) -> LauncherResult> { - let dir = cluster.folder_name.clone(); - let path = Dirs::get_clusters_dir().await?.join(dir).join("screenshots"); + let dir = cluster.folder_name.clone(); + let path = Dirs::get_clusters_dir() + .await? + .join(dir) + .join("screenshots"); - if !path.exists() { - io::create_dir(&path).await?; - return Ok(Vec::new()); - } + if !path.exists() { + io::create_dir(&path).await?; + return Ok(Vec::new()); + } - let mut list = vec![]; - let mut files = io::read_dir(path).await?; - while let Ok(Some(entry)) = files.next_entry().await { - list.push(entry.file_name().to_string_lossy().to_string()); - } + let mut list = vec![]; + let mut files = io::read_dir(path).await?; + while let Ok(Some(entry)) = files.next_entry().await { + list.push(entry.file_name().to_string_lossy().to_string()); + } Ok(list) } pub async fn get_worlds(cluster: &clusters::Model) -> LauncherResult> { - let dir = cluster.folder_name.clone(); - let path = Dirs::get_clusters_dir().await?.join(dir).join("saves"); + let dir = cluster.folder_name.clone(); + let path = Dirs::get_clusters_dir().await?.join(dir).join("saves"); - if !path.exists() { - io::create_dir(&path).await?; - return Ok(Vec::new()); - } + if !path.exists() { + io::create_dir(&path).await?; + return Ok(Vec::new()); + } - let mut list = vec![]; - let mut files = io::read_dir(path).await?; - while let Ok(Some(entry)) = files.next_entry().await { - list.push(entry.file_name().to_string_lossy().to_string()); - } + let mut list = vec![]; + let mut files = io::read_dir(path).await?; + while let Ok(Some(entry)) = files.next_entry().await { + list.push(entry.file_name().to_string_lossy().to_string()); + } Ok(list) } diff --git a/packages/core/src/api/cluster/dao.rs b/packages/core/src/api/cluster/dao.rs index 4672be45..b248bca2 100644 --- a/packages/core/src/api/cluster/dao.rs +++ b/packages/core/src/api/cluster/dao.rs @@ -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( - id: ClusterId, - block: B, -) -> LauncherResult -where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult { +pub async fn update_cluster_by_id(id: ClusterId, block: B) -> LauncherResult +where + B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult, +{ let state = State::get().await?; let db = &state.db; @@ -54,7 +59,9 @@ pub async fn update_cluster( cluster: &mut clusters::Model, block: B, ) -> LauncherResult<&mut clusters::Model> -where B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult { +where + B: AsyncFnOnce(clusters::ActiveModel) -> LauncherResult, +{ 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 LauncherResult> { +pub async fn get_cluster_by_folder_name( + folder_name: &Path, +) -> LauncherResult> { let state = State::get().await?; let db = &state.db; @@ -108,34 +113,44 @@ pub async fn get_cluster_by_folder_name(folder_name: &Path) -> LauncherResult LauncherResult> { +/// Gets all clusters by version from the database. +pub async fn get_clusters_by_version(version: &str) -> LauncherResult> { let state = State::get().await?; let db = &state.db; let clusters = clusters::Entity::find() + .filter(clusters::Column::McVersion.eq(version)) .all(db) .await?; Ok(clusters) } -/// Gets all clusters grouped by their group from the database. -pub async fn get_clusters_grouped() -> LauncherResult)>> { +/// Gets all clusters from the database. +pub async fn get_all_clusters() -> LauncherResult> { let state = State::get().await?; let db = &state.db; - let groups = cluster_groups::Entity::find() - .all(db) - .await?; + let clusters = clusters::Entity::find().all(db).await?; - let clusters = groups.load_many(clusters::Entity, db) - .await?; + Ok(clusters) +} + +/// Gets all clusters grouped by their group from the database. +pub async fn get_clusters_grouped() +-> LauncherResult)>> { + let state = State::get().await?; + let db = &state.db; + + let groups = cluster_groups::Entity::find().all(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.get(index).cloned().unwrap_or_else(Vec::new); + let group_clusters: Vec = + clusters.get(index).cloned().unwrap_or_else(Vec::new); grouped.push((group.clone(), group_clusters)); } @@ -144,7 +159,8 @@ pub async fn get_clusters_grouped() -> LauncherResult) -> LauncherResult<&clusters::Model> { +pub async fn prepare_cluster( + cluster: &mut clusters::Model, + force: Option, +) -> LauncherResult<&clusters::Model> { tracing::debug!("preparing cluster {}", cluster.name); // if cluster.stage == ClusterStage::Ready { @@ -117,7 +120,6 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model, force: Option) // return Ok(cluster); // } - dao::update_cluster(cluster, async |mut cluster| { cluster.stage = Set(ClusterStage::Downloading); Ok(cluster) @@ -161,7 +163,8 @@ async fn install_cluster(cluster: &clusters::Model, force: Option) -> Laun // TASK 1 ingress_id.set_ingress_message("fetching data").await?; let setting_profile = - setting_profiles::dao::get_profile_or_default(cluster.setting_profile_name.as_ref()).await?; + setting_profiles::dao::get_profile_or_default(cluster.setting_profile_name.as_ref()) + .await?; let state = State::get().await?; let mut metadata = state.metadata.write().await; @@ -193,7 +196,9 @@ async fn install_cluster(cluster: &clusters::Model, force: Option) -> Laun ) .await?; - let java_version = if let Some(version) = java::get_recommended_java(&version_info, Some(&setting_profile)).await? { + let java_version = if let Some(version) = + java::get_recommended_java(&version_info, Some(&setting_profile)).await? + { version } else { let Some(ver) = &version_info.java_version else { @@ -204,10 +209,22 @@ async fn install_cluster(cluster: &clusters::Model, force: Option) -> Laun }; // TASK 3 - download_minecraft(&version_info, &java_version.arch, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), force).await?; + download_minecraft( + &version_info, + &java_version.arch, + Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), + force, + ) + .await?; // TASK 4 - run_forge_processors(cluster, &mut version_info, java_version, &SubIngress::new(&ingress_id, INGRESS_STEP)).await?; + run_forge_processors( + cluster, + &mut version_info, + java_version, + &SubIngress::new(&ingress_id, INGRESS_STEP), + ) + .await?; Ok(()) } @@ -265,23 +282,23 @@ async fn run_forge_processors( server => ""; } - ingress.set_ingress_message("running forge processors").await?; + ingress + .set_ingress_message("running forge processors") + .await?; let total_length = processors.len(); for (index, processor) in processors.iter().enumerate() { if let Some(sides) = &processor.sides - && !sides.contains(&String::from("client")) { - continue; - } + && !sides.contains(&String::from("client")) + { + continue; + } let mut cp = processor.classpath.clone(); cp.push(processor.jar.clone()); let child = Command::new(&java_version.absolute_path) .arg("-cp") - .arg(arguments::get_classpath_library( - &libraries, - &cp, - )?) + .arg(arguments::get_classpath_library(&libraries, &cp)?) .arg( arguments::main_class(arguments::get_library(&libraries, &processor.jar, false)?) .await? @@ -307,7 +324,9 @@ async fn run_forge_processors( .into()); } - ingress.set_ingress_message(&format!("running forge processors {index}/{total_length}")).await?; + ingress + .set_ingress_message(&format!("running forge processors {index}/{total_length}")) + .await?; } Ok(()) @@ -315,11 +334,16 @@ async fn run_forge_processors( pub async fn update_playtime(id: ClusterId, duration: i64) -> LauncherResult { dao::update_cluster_by_id(id, async |mut cluster| { - let overall_played = cluster.overall_played.take().unwrap_or_default().unwrap_or_default(); + let overall_played = cluster + .overall_played + .take() + .unwrap_or_default() + .unwrap_or_default(); cluster.overall_played = Set(Some(overall_played + duration)); cluster.last_played = Set(Some(Utc::now())); Ok(cluster) - }).await -} \ No newline at end of file + }) + .await +} diff --git a/packages/core/src/api/cluster/sync.rs b/packages/core/src/api/cluster/sync.rs index 4c901664..4303eb47 100644 --- a/packages/core/src/api/cluster/sync.rs +++ b/packages/core/src/api/cluster/sync.rs @@ -1,9 +1,13 @@ -use std::{collections::HashSet, path::PathBuf}; +use std::collections::HashSet; +use std::path::PathBuf; use onelauncher_entity::clusters; use serde::Serialize; -use crate::{error::{DaoError, LauncherResult}, send_error, store::Dirs, utils::io}; +use crate::error::{DaoError, LauncherResult}; +use crate::send_error; +use crate::store::Dirs; +use crate::utils::io; use super::dao::{self, ClusterId}; @@ -42,10 +46,7 @@ pub async fn sync_clusters() -> LauncherResult> { missing_ids.push(cluster.id); let path = cluster.folder_name.clone(); - send_error!( - "cluster {} is missing from the filesystem", - path.clone() - ); + send_error!("cluster {} is missing from the filesystem", path.clone()); } checked_folders.insert(dirs.clusters_dir().join(cluster.folder_name.clone())); @@ -56,22 +57,22 @@ pub async fn sync_clusters() -> LauncherResult> { // and checks if they are in the database let cluster_dir = Dirs::get_clusters_dir().await?; if cluster_dir.exists() { - let mut stream = io::read_dir(cluster_dir).await?; - while let Ok(Some(entry)) = stream.next_entry().await { - let path = entry.path(); - if !path.is_dir() || checked_folders.contains(&path) { - // Skip if it's not a directory or if we've already checked it - continue; - } + let mut stream = io::read_dir(cluster_dir).await?; + while let Ok(Some(entry)) = stream.next_entry().await { + let path = entry.path(); + if !path.is_dir() || checked_folders.contains(&path) { + // Skip if it's not a directory or if we've already checked it + continue; + } - // We have a directory that is not in the database - if let Err(err) = sync_from_fs_to_db(&path).await { - tracing::error!("failed to sync cluster from fs to db: {}", err); - } - } - } else { - io::create_dir_all(cluster_dir).await?; - } + // We have a directory that is not in the database + if let Err(err) = sync_from_fs_to_db(&path).await { + tracing::error!("failed to sync cluster from fs to db: {}", err); + } + } + } else { + io::create_dir_all(cluster_dir).await?; + } Ok(missing_ids) } @@ -80,7 +81,9 @@ pub async fn sync_clusters() -> LauncherResult> { /// Checks if the cluster in the database exists and if the directory exists. #[tracing::instrument] pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult { - let path = Dirs::get_clusters_dir().await?.join(cluster.folder_name.clone()); + let path = Dirs::get_clusters_dir() + .await? + .join(cluster.folder_name.clone()); if !path.exists() { return Ok(SyncAction::MissingFs); } @@ -119,5 +122,6 @@ async fn sync_from_fs_to_db(path: &PathBuf) -> LauncherResult { Err(anyhow::anyhow!( "TODO: failed to sync cluster from fs to db: {}", path.display() - ).into()) -} \ No newline at end of file + ) + .into()) +} diff --git a/packages/core/src/api/credentials.rs b/packages/core/src/api/credentials.rs index 733d639b..e3c4a26b 100644 --- a/packages/core/src/api/credentials.rs +++ b/packages/core/src/api/credentials.rs @@ -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 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), } -} \ No newline at end of file +} diff --git a/packages/core/src/api/game/arguments.rs b/packages/core/src/api/game/arguments.rs index 5a75a74a..96cd7b27 100644 --- a/packages/core/src/api/game/arguments.rs +++ b/packages/core/src/api/game/arguments.rs @@ -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> { 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::, _>>()?; - 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() diff --git a/packages/core/src/api/game/launch.rs b/packages/core/src/api/game/launch.rs index 007c080b..4df851f8 100644 --- a/packages/core/src/api/game/launch.rs +++ b/packages/core/src/api/game/launch.rs @@ -8,7 +8,7 @@ use onelauncher_entity::prelude::model::*; use tokio::process::Command; use tokio::sync::RwLock; -use crate::api::cluster::{prepare_cluster, update_playtime, ClusterError}; +use crate::api::cluster::{ClusterError, prepare_cluster, update_playtime}; use crate::api::game::{arguments, log, metadata}; use crate::api::java::{self, JavaError}; use crate::api::setting_profiles::dao::get_profile_by_name; @@ -32,9 +32,10 @@ pub async fn launch_minecraft( prepare_cluster(cluster, force).await?; let mut settings = get_global_profile().await; if let Some(name) = &cluster.setting_profile_name - && let Some(profile) = get_profile_by_name(name).await? { - settings.merge(profile); - } + && let Some(profile) = get_profile_by_name(name).await? + { + settings.merge(profile); + } let state = State::get().await?; let dirs = Dirs::get().await?; @@ -68,9 +69,7 @@ pub async fn launch_minecraft( let version_name = loader_version .as_ref() - .map_or(version.id.clone(), |it| { - format!("{}-{}", version.id, it.id) - }); + .map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id)); let version_info = metadata::download_version_info(&version, loader_version.as_ref(), None, None).await?; @@ -188,11 +187,16 @@ pub async fn launch_minecraft( let censors = log::create_censors(&creds); if let Some(discord) = &state.rpc { - discord.set_message( - &format!("Playing {}", cluster.name), - Some(Timestamps::new().start(Utc::now().timestamp())), - ).await; + discord + .set_message( + &format!("Playing {}", cluster.name), + Some(Timestamps::new().start(Utc::now().timestamp())), + ) + .await; } - state.processes.spawn(cluster.id, cwd.clone(), creds, censors, &settings, command).await + state + .processes + .spawn(cluster.id, cwd.clone(), creds, censors, &settings, command) + .await } diff --git a/packages/core/src/api/game/log.rs b/packages/core/src/api/game/log.rs index 7b26d635..dc437ace 100644 --- a/packages/core/src/api/game/log.rs +++ b/packages/core/src/api/game/log.rs @@ -5,9 +5,7 @@ use crate::store::credentials::MinecraftCredentials; pub type CensorMap = HashMap; #[must_use] -pub fn create_censors( - creds: &MinecraftCredentials, -) -> CensorMap { +pub fn create_censors(creds: &MinecraftCredentials) -> CensorMap { let mut map = HashMap::new(); let username = whoami::username(); @@ -24,7 +22,10 @@ pub fn create_censors( map.insert(creds.username.clone(), "{MC_USERNAME}".to_string()); map.insert(creds.id.as_simple().to_string(), "{MC_UUID}".to_string()); - map.insert(creds.id.as_hyphenated().to_string(), "{MC_UUID}".to_string()); + map.insert( + creds.id.as_hyphenated().to_string(), + "{MC_UUID}".to_string(), + ); map } @@ -33,4 +34,4 @@ pub fn censor_line(censors: &CensorMap, line: &mut String) { for (key, value) in censors { *line = line.replace(key, value); } -} \ No newline at end of file +} diff --git a/packages/core/src/api/game/metadata.rs b/packages/core/src/api/game/metadata.rs index b1e8af0d..6c5bf8b9 100644 --- a/packages/core/src/api/game/metadata.rs +++ b/packages/core/src/api/game/metadata.rs @@ -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, ) -> 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 bool { } if let Some(version) = &rule.version - && let Ok(regex) = Regex::new(version.as_str()) { - rule_match &= regex.is_match(&sysinfo::System::os_version().unwrap_or_default()); - } + && let Ok(regex) = Regex::new(version.as_str()) + { + rule_match &= regex.is_match(&sysinfo::System::os_version().unwrap_or_default()); + } rule_match -} \ No newline at end of file +} diff --git a/packages/core/src/api/ingress.rs b/packages/core/src/api/ingress.rs index 83655452..57d2e336 100644 --- a/packages/core/src/api/ingress.rs +++ b/packages/core/src/api/ingress.rs @@ -67,31 +67,33 @@ pub async fn send_ingress_message( } /// Sends an ingress update with the given increment -pub async fn send_ingress( - id: &IngressId, - increment: f64, -) -> LauncherResult<()> { +pub async fn send_ingress(id: &IngressId, increment: f64) -> LauncherResult<()> { send_ingress_message(id, increment, None).await } /// Sends an ingress update with the given message (does not increment) -pub async fn set_ingress_message( - id: &IngressId, - message: &str, -) -> LauncherResult<()> { +pub async fn set_ingress_message(id: &IngressId, message: &str) -> LauncherResult<()> { send_ingress_message(id, 0.0, Some(message)).await } #[async_trait::async_trait] pub trait IngressSendExt { - async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()>; + async fn send_ingress_message( + &self, + increment: f64, + message: Option<&str>, + ) -> LauncherResult<()>; async fn send_ingress(&self, increment: f64) -> LauncherResult<()>; async fn set_ingress_message(&self, message: &str) -> LauncherResult<()>; } #[async_trait::async_trait] impl IngressSendExt for IngressId { - async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> { + async fn send_ingress_message( + &self, + increment: f64, + message: Option<&str>, + ) -> LauncherResult<()> { send_ingress_message(self, increment, message).await } @@ -106,7 +108,11 @@ impl IngressSendExt for IngressId { #[async_trait::async_trait] impl IngressSendExt for Option<&IngressId> { - async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> { + async fn send_ingress_message( + &self, + increment: f64, + message: Option<&str>, + ) -> LauncherResult<()> { if let Some(id) = self { id.send_ingress_message(increment, message).await } else { @@ -133,7 +139,11 @@ impl IngressSendExt for Option<&IngressId> { #[async_trait::async_trait] impl IngressSendExt for &SubIngress<'_> { - async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> { + async fn send_ingress_message( + &self, + increment: f64, + message: Option<&str>, + ) -> LauncherResult<()> { self.id.send_ingress_message(increment, message).await } @@ -148,7 +158,11 @@ impl IngressSendExt for &SubIngress<'_> { #[async_trait::async_trait] impl IngressSendExt for Option<&SubIngress<'_>> { - async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> { + async fn send_ingress_message( + &self, + increment: f64, + message: Option<&str>, + ) -> LauncherResult<()> { if let Some(id) = self { id.send_ingress_message(increment, message).await } else { @@ -175,7 +189,7 @@ impl IngressSendExt for Option<&SubIngress<'_>> { #[cfg(test)] pub mod tests { - use crate::api::ingress::{init_ingress, IngressSendExt}; + use crate::api::ingress::{IngressSendExt, init_ingress}; use crate::api::proxy::ProxyDynamic; use crate::initialize_core; use crate::store::CoreOptions; diff --git a/packages/core/src/api/java/dao.rs b/packages/core/src/api/java/dao.rs index 394fd20c..a34b849d 100644 --- a/packages/core/src/api/java/dao.rs +++ b/packages/core/src/api/java/dao.rs @@ -1,9 +1,11 @@ use std::path::{Path, PathBuf}; use onelauncher_entity::java_versions; -use sea_orm::{ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder}; +use sea_orm::ActiveValue::Set; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder}; -use crate::{error::LauncherResult, store::State}; +use crate::error::LauncherResult; +use crate::store::State; use super::{JavaError, JavaInfo}; @@ -21,15 +23,18 @@ pub async fn get_latest_java() -> LauncherResult> { } /// Accepts a path to a JRE folder and a [`JavaInfo`] and converts it to a [`java_versions::ActiveModel`] -pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult { +pub fn get_java_model( + absolute_path: &Path, + info: &JavaInfo, +) -> LauncherResult { let major_version: u32 = if info.java_version[0..2].eq("1.") { info.java_version[2..3].parse() } else { let split = info.java_version.split_once('.').unwrap_or_default(); split.0.parse() - }.or_else( - |_| info.java_version.parse() - ).map_err(|e| JavaError::ParseVersion(info.java_version.clone(), e))?; + } + .or_else(|_| info.java_version.parse()) + .map_err(|e| JavaError::ParseVersion(info.java_version.clone(), e))?; Ok(java_versions::ActiveModel { absolute_path: Set(absolute_path.to_string_lossy().to_string()), @@ -84,7 +89,10 @@ pub async fn get_latest_java_by_major(major: u32) -> LauncherResult LauncherResult { +pub async fn insert_java( + absolute_path: PathBuf, + info: JavaInfo, +) -> LauncherResult { let db = &State::get().await?.db; let model = get_java_model(&absolute_path, &info)?; @@ -93,7 +101,9 @@ pub async fn insert_java(absolute_path: PathBuf, info: JavaInfo) -> LauncherResu } /// Accepts a path to JRE folders and returns the inserted entries -pub async fn insert_java_many(java: Vec<(PathBuf, JavaInfo)>) -> LauncherResult> { +pub async fn insert_java_many( + java: Vec<(PathBuf, JavaInfo)>, +) -> LauncherResult> { let db = &State::get().await?.db; let mut models = Vec::new(); @@ -105,4 +115,4 @@ pub async fn insert_java_many(java: Vec<(PathBuf, JavaInfo)>) -> LauncherResult< Ok(java_versions::Entity::insert_many(models) .exec_with_returning_many(db) .await?) -} \ No newline at end of file +} diff --git a/packages/core/src/api/java/mod.rs b/packages/core/src/api/java/mod.rs index ed990728..511d647e 100644 --- a/packages/core/src/api/java/mod.rs +++ b/packages/core/src/api/java/mod.rs @@ -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}; @@ -178,7 +178,10 @@ pub async fn prepare_java(major: u32) -> LauncherResult { } } - 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?; @@ -197,19 +200,15 @@ pub async fn prepare_java(major: u32) -> LauncherResult { #[cfg(unix)] fn find_java_in_path(mut found: Vec) -> Vec { - 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() { - if java.is_file() { - if !found.contains(&java) { - found.push(java); - } - } - } - } - } - found + 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. /// @@ -315,7 +314,6 @@ fn internal_locate_java() -> Vec { } } - found = find_java_in_path(found); found @@ -376,12 +374,10 @@ pub async fn get_zulu_packages() -> LauncherResult> { pub async fn install_java_from_major(version: u32) -> LauncherResult { 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 } @@ -411,19 +407,18 @@ pub async fn install_java_package(package: JavaPackage) -> LauncherResult LauncherResult>().join("."); + let java_version = package + .java_version + .iter() + .map(|v| v.to_string()) + .collect::>() + .join("."); base_path = base_path .join(format!("zulu-{java_version}.jre")) .join("Contents") diff --git a/packages/core/src/api/packages/dao.rs b/packages/core/src/api/packages/dao.rs index 94195019..1127c59b 100644 --- a/packages/core/src/api/packages/dao.rs +++ b/packages/core/src/api/packages/dao.rs @@ -1,9 +1,7 @@ use onelauncher_entity::loader::GameLoader; use onelauncher_entity::package::PackageType; use onelauncher_entity::{cluster_packages, clusters, packages}; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, -}; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter}; use crate::error::{DaoError, LauncherResult}; use crate::store::State; diff --git a/packages/core/src/api/packages/data.rs b/packages/core/src/api/packages/data.rs index 5a25a91a..b2f80fa6 100644 --- a/packages/core/src/api/packages/data.rs +++ b/packages/core/src/api/packages/data.rs @@ -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, - pub display_categories: Vec, - pub versions: Vec, - pub downloads: usize, - pub icon_url: String, - pub date_created: DateTime, - pub date_modified: DateTime, - 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, + pub display_categories: Vec, + pub versions: Vec, + pub downloads: usize, + pub icon_url: String, + pub date_created: DateTime, + pub date_modified: DateTime, + pub latest_version: String, + pub license: String, + pub client_side: PackageSide, + pub server_side: PackageSide, /// List of URLs to images - pub gallery: Vec, - // pub color: i64, + pub gallery: Vec, + // 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 + pub gallery: Vec, } #[onelauncher_macro::specta] @@ -179,7 +184,7 @@ pub enum PackageSide { Unsupported, } -/// 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, } -/// 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, pub description: Option, - pub featured: Option + pub featured: Option, } -/// 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, -} \ No newline at end of file +} diff --git a/packages/core/src/api/packages/mod.rs b/packages/core/src/api/packages/mod.rs index b8c384c0..5a92fdc4 100644 --- a/packages/core/src/api/packages/mod.rs +++ b/packages/core/src/api/packages/mod.rs @@ -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, version_id: &str, file_name: &str) -> std::path::PathBuf { +fn join_package_file( + path: impl AsRef, + version_id: &str, + file_name: &str, +) -> std::path::PathBuf { path.as_ref().join(format!("{version_id}-{file_name}")) -} \ No newline at end of file +} diff --git a/packages/core/src/api/packages/provider/mod.rs b/packages/core/src/api/packages/provider/mod.rs index 375fea85..201be575 100644 --- a/packages/core/src/api/packages/provider/mod.rs +++ b/packages/core/src/api/packages/provider/mod.rs @@ -1,7 +1,8 @@ use onelauncher_entity::loader::GameLoader; use onelauncher_entity::package::Provider; -use crate::{api::packages::data::SearchResult, error::LauncherResult}; +use crate::api::packages::data::SearchResult; +use crate::error::LauncherResult; use crate::utils::pagination::Paginated; use super::data::{ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery}; @@ -70,11 +71,7 @@ impl ProviderExt for Provider { hashes: &[String], ) -> LauncherResult> { match self { - Self::Modrinth => { - ModrinthProviderImpl - .get_versions_by_hashes(hashes) - .await - } + Self::Modrinth => ModrinthProviderImpl.get_versions_by_hashes(hashes).await, _ => todo!("unimplemented provider"), } } @@ -91,11 +88,7 @@ impl ProviderExt for Provider { author: PackageAuthor, ) -> LauncherResult> { match self { - Self::Modrinth => { - ModrinthProviderImpl - .get_users_from_author(author) - .await - } + Self::Modrinth => ModrinthProviderImpl.get_users_from_author(author).await, _ => todo!("unimplemented provider"), } } diff --git a/packages/core/src/api/packages/provider/modrinth.rs b/packages/core/src/api/packages/provider/modrinth.rs index 00961c4b..cf8673c7 100644 --- a/packages/core/src/api/packages/provider/modrinth.rs +++ b/packages/core/src/api/packages/provider/modrinth.rs @@ -1,6 +1,9 @@ use super::ProviderExt; use crate::api::packages::data::{ - ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency, ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageGallery, PackageLicense, PackageLinks, PackageReleaseType, PackageSide, PackageStatus, SearchQuery, SearchResult, DEFAULT_LIMIT + DEFAULT_LIMIT, ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency, + ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageGallery, + PackageLicense, PackageLinks, PackageReleaseType, PackageSide, PackageStatus, SearchQuery, + SearchResult, }; use crate::error::LauncherResult; use crate::utils::http; @@ -52,25 +55,41 @@ impl ProviderExt for ModrinthProviderImpl { if let Some(categories) = &filters.categories { for category in categories { - builder.and(Facet("categories".to_string(), FacetOperation::Eq, category.to_string())); + builder.and(Facet( + "categories".to_string(), + FacetOperation::Eq, + category.to_string(), + )); } } if let Some(mc_versions) = &filters.game_versions { for mc_version in mc_versions { - builder.and(Facet("versions".to_string(), FacetOperation::Eq, mc_version.to_string())); + builder.and(Facet( + "versions".to_string(), + FacetOperation::Eq, + mc_version.to_string(), + )); } } if let Some(package_types) = &filters.package_types { for package_type in package_types { - builder.and(Facet("project_type".to_string(), FacetOperation::Eq, package_type_to_string(package_type))); + builder.and(Facet( + "project_type".to_string(), + FacetOperation::Eq, + package_type_to_string(package_type), + )); } } if let Some(loaders) = &filters.loaders { for loader in loaders { - builder.and(Facet("categories".to_string(), FacetOperation::Eq, loader.to_string())); + builder.and(Facet( + "categories".to_string(), + FacetOperation::Eq, + loader.to_string(), + )); } } @@ -166,7 +185,10 @@ impl ProviderExt for ModrinthProviderImpl { // Ok(http::fetch_json(Method::GET, url_v3!("/organizations/{slug}/projects"), None, None).await?) // } - async fn get_users_from_author(&self, author: PackageAuthor) -> LauncherResult> { + async fn get_users_from_author( + &self, + author: PackageAuthor, + ) -> LauncherResult> { let (team_id, org_id) = match author { PackageAuthor::Team { team_id, org_id } => (team_id, org_id), PackageAuthor::Users(users) => { @@ -188,14 +210,24 @@ impl ProviderExt for ModrinthProviderImpl { // pub members: Vec, } - let organization = http::fetch_json::(Method::GET, url_v3!("/organization/{org_id}"), None, None).await?; + let organization = http::fetch_json::( + Method::GET, + url_v3!("/organization/{org_id}"), + None, + None, + ) + .await?; let org_user = ManagedUser { id: organization.id.clone(), username: organization.name, avatar_url: organization.icon_url, bio: Some(organization.description), - url: Some(format!("{}/organization/{}", Provider::Modrinth.website(), organization.id)), + url: Some(format!( + "{}/organization/{}", + Provider::Modrinth.website(), + organization.id + )), is_organization_user: true, role: None, }; @@ -407,7 +439,7 @@ impl From for ManagedPackage { }, status: value.status, downloads: value.downloads, - gallery: value.gallery.into_iter().map(Into::into).collect() + gallery: value.gallery.into_iter().map(Into::into).collect(), } } } @@ -522,11 +554,7 @@ impl From for ManagedUser { Self { id: value.id.clone(), username: value.username, - url: Some(format!( - "{}user/{}", - Provider::Modrinth.website(), - value.id - )), + url: Some(format!("{}user/{}", Provider::Modrinth.website(), value.id)), avatar_url: value.avatar_url, bio: value.bio, is_organization_user: false, @@ -538,24 +566,24 @@ impl From for ManagedUser { #[derive(Deserialize)] pub struct ModrinthGallery { #[serde(rename = "raw_url")] - pub url: String, + pub url: String, #[serde(rename = "url")] - pub thumbnail_url: String, - pub title: Option, - pub description: Option, - pub featured: Option + pub thumbnail_url: String, + pub title: Option, + pub description: Option, + pub featured: Option, } impl From for PackageGallery { - fn from(value: ModrinthGallery) -> Self { - Self { - url: value.url, - thumbnail_url: value.thumbnail_url, - title: value.title, - description: value.description, - featured: value.featured - } - } + fn from(value: ModrinthGallery) -> Self { + Self { + url: value.url, + thumbnail_url: value.thumbnail_url, + title: value.title, + description: value.description, + featured: value.featured, + } + } } #[allow(dead_code)] diff --git a/packages/core/src/api/packages/tests.rs b/packages/core/src/api/packages/tests.rs index 06595ec6..4e07d944 100644 --- a/packages/core/src/api/packages/tests.rs +++ b/packages/core/src/api/packages/tests.rs @@ -1,7 +1,9 @@ - use onelauncher_entity::package::Provider; -use crate::{api::packages::{self, provider::ProviderExt}, error::LauncherResult, store::{Core, CoreOptions, Dirs}}; +use crate::api::packages::provider::ProviderExt; +use crate::api::packages::{self}; +use crate::error::LauncherResult; +use crate::store::{Core, CoreOptions, Dirs}; #[tokio::test] pub async fn test_get_provider() -> LauncherResult<()> { @@ -22,7 +24,9 @@ pub async fn test_get_multiple() -> LauncherResult<()> { let provider = Provider::Modrinth; - let res = provider.get_multiple(&["oneconfig".to_string(), "chatting".to_string()]).await?; + let res = provider + .get_multiple(&["oneconfig".to_string(), "chatting".to_string()]) + .await?; assert_eq!(res.len(), 2); @@ -35,7 +39,9 @@ pub async fn test_get_versions() -> LauncherResult<()> { let provider = Provider::Modrinth; - let res = provider.get_versions(&["ZvlCAdEF".to_string(), "GQANlg7p".to_string()]).await?; + let res = provider + .get_versions(&["ZvlCAdEF".to_string(), "GQANlg7p".to_string()]) + .await?; assert_eq!(res.len(), 2); @@ -51,7 +57,7 @@ pub async fn test_download_chatting() -> LauncherResult<()> { let pkg = provider.get("chatting").await?; let ver_id = pkg.version_ids.first().expect("No version found"); let versions = provider.get_versions(&[ver_id.clone()]).await?; - let ver = versions.first().expect("No version found"); + let ver = versions.first().expect("No version found"); let db_model = packages::download_package(&pkg, ver, None).await; assert!(db_model.is_ok_and(|m| m.display_name == pkg.name)); diff --git a/packages/core/src/api/processes.rs b/packages/core/src/api/processes.rs index c4ecccf4..aa055aba 100644 --- a/packages/core/src/api/processes.rs +++ b/packages/core/src/api/processes.rs @@ -6,18 +6,8 @@ use crate::store::processes::Process; #[derive(Debug, Clone, Serialize)] #[serde(tag = "type")] pub enum ProcessPayload { - Starting { - command: String, - }, - Started { - process: Process, - }, - Stopped { - pid: u32, - exit_code: i32, - }, - Output { - pid: u32, - output: String, - }, -} \ No newline at end of file + Starting { command: String }, + Started { process: Process }, + Stopped { pid: u32, exit_code: i32 }, + Output { pid: u32, output: String }, +} diff --git a/packages/core/src/api/proxy/event.rs b/packages/core/src/api/proxy/event.rs index fec2d154..75a6f321 100644 --- a/packages/core/src/api/proxy/event.rs +++ b/packages/core/src/api/proxy/event.rs @@ -1,6 +1,7 @@ use serde::Serialize; -use crate::{error::LauncherResult, store::proxy::ProxyState}; +use crate::error::LauncherResult; +use crate::store::proxy::ProxyState; #[onelauncher_macro::specta] #[derive(Debug, Clone, Serialize)] diff --git a/packages/core/src/api/proxy/impl/mod.rs b/packages/core/src/api/proxy/impl/mod.rs index 73e243a0..84cb8be7 100644 --- a/packages/core/src/api/proxy/impl/mod.rs +++ b/packages/core/src/api/proxy/impl/mod.rs @@ -1,7 +1,11 @@ -#[cfg(feature = "tauri")] pub mod proxy_tauri; -#[cfg(feature = "cli")] pub mod proxy_cli; +#[cfg(feature = "cli")] +pub mod proxy_cli; pub mod proxy_empty; +#[cfg(feature = "tauri")] +pub mod proxy_tauri; -#[cfg(feature = "tauri")] pub use proxy_tauri::ProxyTauri; -#[cfg(feature = "cli")] pub use proxy_cli::ProxyCli; -pub use proxy_empty::ProxyEmpty; \ No newline at end of file +#[cfg(feature = "cli")] +pub use proxy_cli::ProxyCli; +pub use proxy_empty::ProxyEmpty; +#[cfg(feature = "tauri")] +pub use proxy_tauri::ProxyTauri; diff --git a/packages/core/src/api/proxy/impl/proxy_cli.rs b/packages/core/src/api/proxy/impl/proxy_cli.rs index 0a72b9ca..33910fa8 100644 --- a/packages/core/src/api/proxy/impl/proxy_cli.rs +++ b/packages/core/src/api/proxy/impl/proxy_cli.rs @@ -4,7 +4,11 @@ use indicatif::ProgressBar; use tokio::sync::RwLock; use uuid::Uuid; -use crate::{api::{processes::ProcessPayload, proxy::{event::LauncherEvent, LauncherProxy}}, constants::CLI_TOTAL_INGRESS, LauncherResult}; +use crate::LauncherResult; +use crate::api::processes::ProcessPayload; +use crate::api::proxy::LauncherProxy; +use crate::api::proxy::event::LauncherEvent; +use crate::constants::CLI_TOTAL_INGRESS; #[derive(Default, Debug)] pub struct ProxyCli { @@ -39,35 +43,37 @@ impl LauncherProxy for ProxyCli { let progress = ProgressBar::new(CLI_TOTAL_INGRESS); progress.set_message(ingress.message.clone()); progress.set_position(0); - progress.set_style(indicatif::ProgressStyle::default_bar().template( - "{spinner:.green}, [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}" - ).unwrap().progress_chars("#>-")); + progress.set_style( + indicatif::ProgressStyle::default_bar() + .template( + "{spinner:.green}, [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}", + ) + .unwrap() + .progress_chars("#>-"), + ); feeds.insert(ingress.id, progress); } - }, + } LauncherEvent::Message(message) => { println!("[{:?}] {}", message.level, message.message); - }, - LauncherEvent::Process(process) => { - match process { - ProcessPayload::Starting { command } => { - println!("Starting process: {command}"); - }, - ProcessPayload::Started { process } => { - println!("Process started: {process:#?}"); - }, - ProcessPayload::Stopped { pid, exit_code } => { - println!("Process {pid} exited with code {exit_code}"); - }, - ProcessPayload::Output { pid, output } => { - println!("Process {pid}: {output}"); - }, - } } + LauncherEvent::Process(process) => match process { + ProcessPayload::Starting { command } => { + println!("Starting process: {command}"); + } + ProcessPayload::Started { process } => { + println!("Process started: {process:#?}"); + } + ProcessPayload::Stopped { pid, exit_code } => { + println!("Process {pid} exited with code {exit_code}"); + } + ProcessPayload::Output { pid, output } => { + println!("Process {pid}: {output}"); + } + }, } Ok(()) } - } diff --git a/packages/core/src/api/proxy/impl/proxy_empty.rs b/packages/core/src/api/proxy/impl/proxy_empty.rs index d4b0e5af..72d7b2c0 100644 --- a/packages/core/src/api/proxy/impl/proxy_empty.rs +++ b/packages/core/src/api/proxy/impl/proxy_empty.rs @@ -1,4 +1,6 @@ -use crate::{api::proxy::{event::LauncherEvent, LauncherProxy}, LauncherResult}; +use crate::LauncherResult; +use crate::api::proxy::LauncherProxy; +use crate::api::proxy::event::LauncherEvent; #[derive(Default, Debug)] pub struct ProxyEmpty; @@ -17,17 +19,19 @@ impl LauncherProxy for ProxyEmpty { LauncherEvent::Ingress(ingress) => { println!( "{} [{}] {}", - ingress.percent.map_or("100%".to_string(), |p| format!("{:.2}%", p * 100.0)), + ingress + .percent + .map_or("100%".to_string(), |p| format!("{:.2}%", p * 100.0)), ingress.id, ingress.message ); - }, + } LauncherEvent::Message(message) => { println!("[{:?}] {}", message.level, message.message); - }, + } LauncherEvent::Process(payload) => { println!("{payload:#?}"); - }, + } } Ok(()) @@ -44,5 +48,4 @@ impl LauncherProxy for ProxyEmpty { println!("shown window"); Ok(()) } - } diff --git a/packages/core/src/api/proxy/impl/proxy_tauri.rs b/packages/core/src/api/proxy/impl/proxy_tauri.rs index 46d89fe1..3e09503c 100644 --- a/packages/core/src/api/proxy/impl/proxy_tauri.rs +++ b/packages/core/src/api/proxy/impl/proxy_tauri.rs @@ -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(()) } } - -} \ No newline at end of file +} diff --git a/packages/core/src/api/proxy/message.rs b/packages/core/src/api/proxy/message.rs index 2bc8585c..dd5cb318 100644 --- a/packages/core/src/api/proxy/message.rs +++ b/packages/core/src/api/proxy/message.rs @@ -19,10 +19,7 @@ pub enum MessageLevel { pub fn send_message(level: MessageLevel, message: String) { tokio::spawn(async move { - let payload = MessagePayload { - level, - message, - }; + let payload = MessagePayload { level, message }; send_event(super::event::LauncherEvent::Message(payload)).await; }); diff --git a/packages/core/src/api/proxy/mod.rs b/packages/core/src/api/proxy/mod.rs index 416e428d..828a9140 100644 --- a/packages/core/src/api/proxy/mod.rs +++ b/packages/core/src/api/proxy/mod.rs @@ -1,6 +1,6 @@ +pub mod event; mod r#impl; pub mod message; -pub mod event; pub use r#impl::*; @@ -15,5 +15,7 @@ pub trait LauncherProxy: Send + Sync + std::fmt::Debug { fn show_main_window(&self) -> crate::LauncherResult<()>; } -#[cfg(feature = "cli")] pub type ProxyDynamic = ProxyCli; -#[cfg(not(feature = "cli"))] pub type ProxyDynamic = ProxyEmpty; \ No newline at end of file +#[cfg(feature = "cli")] +pub type ProxyDynamic = ProxyCli; +#[cfg(not(feature = "cli"))] +pub type ProxyDynamic = ProxyEmpty; diff --git a/packages/core/src/api/setting_profiles.rs b/packages/core/src/api/setting_profiles.rs index 81b1ef18..707c880e 100644 --- a/packages/core/src/api/setting_profiles.rs +++ b/packages/core/src/api/setting_profiles.rs @@ -6,8 +6,7 @@ use crate::store::State; pub mod dao { use onelauncher_entity::setting_profiles; - use sea_orm::IntoActiveModel; - use sea_orm::{ActiveModelTrait, EntityTrait}; + use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel}; use crate::error::{DaoError, LauncherResult}; use crate::store::State; @@ -59,37 +58,45 @@ pub mod dao { } pub async fn update_profile_by_name( - name: &str, - block: B, - ) -> LauncherResult - where B: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult { - let state = State::get().await?; - let db = &state.db; + name: &str, + block: B, + ) -> LauncherResult + where + B: AsyncFnOnce( + setting_profiles::ActiveModel, + ) -> LauncherResult, + { + let state = State::get().await?; + let db = &state.db; - let model = get_profile_by_name(name).await?.ok_or(DaoError::NotFound)?; - let model = block(model.into_active_model()).await?; - let model = model.update(db).await?; + let model = get_profile_by_name(name).await?.ok_or(DaoError::NotFound)?; + let model = block(model.into_active_model()).await?; + let model = model.update(db).await?; - Ok(model) - } + Ok(model) + } - /// Updates an existing profile in the database. - pub async fn update_profile( - profile: &mut setting_profiles::Model, - block: B, - ) -> LauncherResult<&mut setting_profiles::Model> - where B: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult { - let state = State::get().await?; - let db = &state.db; + /// Updates an existing profile in the database. + pub async fn update_profile( + profile: &mut setting_profiles::Model, + block: B, + ) -> LauncherResult<&mut setting_profiles::Model> + where + B: AsyncFnOnce( + setting_profiles::ActiveModel, + ) -> LauncherResult, + { + let state = State::get().await?; + let db = &state.db; - let model = profile.clone().into_active_model(); - let model = block(model).await?; - let model = model.update(db).await?; + let model = profile.clone().into_active_model(); + let model = block(model).await?; + let model = model.update(db).await?; - *profile = model; + *profile = model; - Ok(profile) - } + Ok(profile) + } pub async fn delete_profile_by_name(name: &str) -> LauncherResult<()> { let state = State::get().await?; diff --git a/packages/core/src/api/tauri/commands.rs b/packages/core/src/api/tauri/commands.rs index 4fa6576e..24e57e9a 100644 --- a/packages/core/src/api/tauri/commands.rs +++ b/packages/core/src/api/tauri/commands.rs @@ -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>; @@ -36,7 +48,6 @@ pub trait TauriLauncherApi { #[taurpc(alias = "getWorlds")] async fn get_worlds(id: ClusterId) -> LauncherResult>; - // Setting Profiles #[taurpc(alias = "getProfileOrDefault")] async fn get_profile_or_default(name: Option) -> LauncherResult; @@ -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; + async fn update_cluster_profile( + name: String, + profile: ProfileUpdate, + ) -> LauncherResult; // 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>; - // Users #[taurpc(alias = "getUsers")] async fn get_users() -> LauncherResult>; @@ -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) -> LauncherResult>; + async fn get_default_user( + fallback: Option, + ) -> LauncherResult>; #[taurpc(alias = "setDefaultUser")] async fn set_default_user(uuid: Option) -> LauncherResult<()>; #[taurpc(alias = "openMsaLogin")] - async fn open_msa_login(app_handle: AppHandle) -> LauncherResult>; + async fn open_msa_login( + app_handle: AppHandle, + ) -> LauncherResult>; // Settings #[taurpc(alias = "readSettings")] @@ -83,28 +100,49 @@ pub trait TauriLauncherApi { // Packages #[taurpc(alias = "searchPackages")] - async fn search_packages(provider: Provider, query: SearchQuery) -> LauncherResult>; + async fn search_packages( + provider: Provider, + query: SearchQuery, + ) -> LauncherResult>; #[taurpc(alias = "getPackage")] async fn get_package(provider: Provider, slug: String) -> LauncherResult; #[taurpc(alias = "getMultiplePackages")] - async fn get_multiple_packages(provider: Provider, slugs: Vec) -> LauncherResult>; + async fn get_multiple_packages( + provider: Provider, + slugs: Vec, + ) -> LauncherResult>; #[taurpc(alias = "getPackageVersions")] - async fn get_package_versions(provider: Provider, slug: String, mc_versions: Option>, loaders: Option>, offset: usize, limit: usize) -> LauncherResult>; + async fn get_package_versions( + provider: Provider, + slug: String, + mc_versions: Option>, + loaders: Option>, + offset: usize, + limit: usize, + ) -> LauncherResult>; #[taurpc(alias = "getPackageUser")] async fn get_package_user(provider: Provider, slug: String) -> LauncherResult; #[taurpc(alias = "downloadPackage")] - async fn download_package(provider: Provider, package_id: String, version_id: String, cluster_id: ClusterId, skip_compatibility: Option) -> LauncherResult; + async fn download_package( + provider: Provider, + package_id: String, + version_id: String, + cluster_id: ClusterId, + skip_compatibility: Option, + ) -> LauncherResult; #[taurpc(alias = "getUsersFromAuthor")] - async fn get_users_from_author(provider: Provider, author: PackageAuthor) -> LauncherResult>; + async fn get_users_from_author( + provider: Provider, + author: PackageAuthor, + ) -> LauncherResult>; } - #[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> { api::cluster::dao::get_all_clusters().await @@ -152,37 +189,54 @@ impl TauriLauncherApi for TauriLauncherApiImpl { } async fn create_cluster(self, options: CreateCluster) -> LauncherResult { - 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) -> 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> { - 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> { - 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 { - 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 { + 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> { 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) -> LauncherResult> { + async fn get_default_user( + self, + fallback: Option, + ) -> LauncherResult> { 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(self, app_handle: AppHandle) -> LauncherResult> { + async fn open_msa_login( + self, + app_handle: AppHandle, + ) -> LauncherResult> { 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> { - Ok(provider.search(&query).await?) + async fn search_packages( + self, + provider: Provider, + query: SearchQuery, + ) -> LauncherResult> { + provider.search(&query).await } async fn get_package(self, provider: Provider, slug: String) -> LauncherResult { - Ok(provider.get(&slug).await?) + provider.get(&slug).await } - async fn get_multiple_packages(self, provider: Provider, slugs: Vec) -> LauncherResult> { - Ok(provider.get_multiple(&slugs).await?) + async fn get_multiple_packages( + self, + provider: Provider, + slugs: Vec, + ) -> LauncherResult> { + provider.get_multiple(&slugs).await } - async fn get_package_versions(self, provider: Provider, slug: String, mc_versions: Option>, loaders: Option>, offset: usize, limit: usize) -> LauncherResult> { - 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>, + loaders: Option>, + offset: usize, + limit: usize, + ) -> LauncherResult> { + provider + .get_versions_paginated(&slug, mc_versions, loaders, offset, limit) + .await } - async fn get_package_user(self, provider: Provider, slug: String) -> LauncherResult { - Ok(provider.get_user(&slug).await?) + async fn get_package_user( + self, + provider: Provider, + slug: String, + ) -> LauncherResult { + provider.get_user(&slug).await } - async fn download_package(self, provider: Provider, package_id: String, version_id: String, cluster_id: ClusterId, skip_compatibility: Option) -> LauncherResult { - 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, + ) -> LauncherResult { + 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> { - Ok(provider.get_users_from_author(author).await?) + async fn get_users_from_author( + self, + provider: Provider, + author: PackageAuthor, + ) -> LauncherResult> { + provider.get_users_from_author(author).await } } diff --git a/packages/core/src/api/tauri/mod.rs b/packages/core/src/api/tauri/mod.rs index 8df55897..5a467e3c 100644 --- a/packages/core/src/api/tauri/mod.rs +++ b/packages/core/src/api/tauri/mod.rs @@ -2,4 +2,4 @@ mod events; pub use events::*; mod commands; -pub use commands::*; \ No newline at end of file +pub use commands::*; diff --git a/packages/core/src/constants.rs b/packages/core/src/constants.rs index eff6c4ce..108835b3 100644 --- a/packages/core/src/constants.rs +++ b/packages/core/src/constants.rs @@ -30,7 +30,8 @@ pub const FEATURED_PACKAGES_URL: &str = "https://polyfrost.org/meta/onelauncher/ /// / API base url. pub const MCLOGS_API_URL: &str = "https://api.mclo.gs/1"; /// metadata base url. -pub const SKYCLIENT_BASE_URL: &str = "https://raw.githubusercontent.com/SkyblockClient/SkyblockClient-REPO/refs/heads/main/v1"; +pub const SKYCLIENT_BASE_URL: &str = + "https://raw.githubusercontent.com/SkyblockClient/SkyblockClient-REPO/refs/heads/main/v1"; // =========== Hacky Mojang-spec OS constants =========== #[cfg(target_os = "windows")] diff --git a/packages/core/src/error.rs b/packages/core/src/error.rs index a2760b19..51daba92 100644 --- a/packages/core/src/error.rs +++ b/packages/core/src/error.rs @@ -27,25 +27,61 @@ pub enum LauncherError { DaoError(#[from] DaoError), #[error("json error: {0}")] - SerdeError(#[from] #[skip] serde_json::Error), + SerdeError( + #[from] + #[skip] + serde_json::Error, + ), #[error(transparent)] - AnyhowError(#[from] #[skip] anyhow::Error), + AnyhowError( + #[from] + #[skip] + anyhow::Error, + ), #[error("database error: {0}")] - DbError(#[from] #[skip] sea_orm::DbErr), + DbError( + #[from] + #[skip] + sea_orm::DbErr, + ), #[error("http error: {0}")] - ReqwestError(#[from] #[skip] reqwest::Error), + ReqwestError( + #[from] + #[skip] + reqwest::Error, + ), #[error("meta error: {0}")] - InterpulseError(#[from] #[skip] interpulse::Error), + InterpulseError( + #[from] + #[skip] + interpulse::Error, + ), #[error(transparent)] - RegexError(#[from] #[skip] regex::Error), + RegexError( + #[from] + #[skip] + regex::Error, + ), #[error("couldn't acquire semaphore: {0}")] - SemaphoreError(#[from] #[skip] tokio::sync::AcquireError), + SemaphoreError( + #[from] + #[skip] + tokio::sync::AcquireError, + ), #[error(transparent)] - UrlError(#[from] #[skip] url::ParseError), + UrlError( + #[from] + #[skip] + url::ParseError, + ), #[cfg(feature = "tauri")] #[error(transparent)] - TauriError(#[from] #[skip] tauri::Error), + TauriError( + #[from] + #[skip] + tauri::Error, + ), } pub type LauncherResult = Result; diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index c877e859..9bdd2e23 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,9 +1,12 @@ #![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; @@ -14,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; @@ -29,4 +34,4 @@ pub async fn initialize_core(options: CoreOptions, proxy_backend: impl LauncherP tracing::info!("Core initialized successfully"); Ok(()) -} \ No newline at end of file +} diff --git a/packages/core/src/logger.rs b/packages/core/src/logger.rs index 3b52511c..54333806 100644 --- a/packages/core/src/logger.rs +++ b/packages/core/src/logger.rs @@ -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(); } diff --git a/packages/core/src/store/core_options.rs b/packages/core/src/store/core_options.rs index e44c23a5..9941f807 100644 --- a/packages/core/src/store/core_options.rs +++ b/packages/core/src/store/core_options.rs @@ -1,4 +1,5 @@ -use std::{ops::Deref, sync::Arc}; +use std::ops::Deref; +use std::sync::Arc; use tokio::sync::OnceCell; use tracing_subscriber::fmt::format::FmtSpan; @@ -48,9 +49,9 @@ impl Core { } pub async fn initialize(options: CoreOptions) -> LauncherResult<()> { - CORE_STATE.get_or_try_init(|| async move { - Ok::<_, LauncherError>(Self::new(options)) - }).await?; + CORE_STATE + .get_or_try_init(|| async move { Ok::<_, LauncherError>(Self::new(options)) }) + .await?; tracing::info!("core initialized successfully"); @@ -58,10 +59,7 @@ impl Core { } pub fn get() -> Arc { - CORE_STATE - .get() - .expect("core was not initialized") - .clone() + CORE_STATE.get().expect("core was not initialized").clone() } } @@ -72,4 +70,3 @@ impl Deref for Core { &self.0 } } - diff --git a/packages/core/src/store/credentials.rs b/packages/core/src/store/credentials.rs index 822c7349..9cd0c08c 100644 --- a/packages/core/src/store/credentials.rs +++ b/packages/core/src/store/credentials.rs @@ -1019,7 +1019,7 @@ pub enum MinecraftAuthError { PKCS8Error( #[from] #[skip] - p256::pkcs8::Error + p256::pkcs8::Error, ), #[error("failed to serialize JSON during MSA step {step:?}: {source}")] SerializeError { diff --git a/packages/core/src/store/db.rs b/packages/core/src/store/db.rs index ab96e24e..48306e38 100644 --- a/packages/core/src/store/db.rs +++ b/packages/core/src/store/db.rs @@ -1,9 +1,11 @@ -use std::{path::PathBuf, time::Duration}; +use std::path::PathBuf; +use std::time::Duration; use onelauncher_migration::MigratorTrait; use sea_orm::{ConnectOptions, Database, DatabaseConnection}; -use crate::{utils::io::IOError, LauncherResult}; +use crate::LauncherResult; +use crate::utils::io::IOError; use super::Dirs; @@ -30,4 +32,4 @@ pub async fn create_pool_from_path(path: PathBuf) -> LauncherResult LauncherResult { let path = Dirs::get_db_file().await?; create_pool_from_path(path).await -} \ No newline at end of file +} diff --git a/packages/core/src/store/dirs.rs b/packages/core/src/store/dirs.rs index 009705cc..1c4b76ce 100644 --- a/packages/core/src/store/dirs.rs +++ b/packages/core/src/store/dirs.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use onelauncher_entity::package::{PackageType, Provider}; use tokio::sync::OnceCell; -use crate::{utils, LauncherResult}; +use crate::{LauncherResult, utils}; use super::Core; @@ -24,9 +24,9 @@ pub enum DirectoryError { impl Dirs { pub async fn get() -> LauncherResult<&'static Self> { - DIRS_GLOBAL.get_or_try_init(async || { - Self::initialize() - }).await + DIRS_GLOBAL + .get_or_try_init(async || Self::initialize()) + .await } fn initialize() -> LauncherResult { @@ -36,9 +36,7 @@ impl Dirs { tracing::info!("using base directory '{}'", base_dir.display()); - Ok(Self { - base_dir - }) + Ok(Self { base_dir }) } /// Get the base directory for the launcher. @@ -46,7 +44,6 @@ impl Dirs { pub const fn base_dir(&self) -> &PathBuf { &self.base_dir } - } macro_rules! dirs_impl { diff --git a/packages/core/src/store/discord.rs b/packages/core/src/store/discord.rs index d8e58b15..e27ace7e 100644 --- a/packages/core/src/store/discord.rs +++ b/packages/core/src/store/discord.rs @@ -1,6 +1,8 @@ -use std::sync::{atomic::AtomicBool, Arc}; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; -use discord_rich_presence::{activity::{Activity, Assets, Button, Timestamps}, DiscordIpc, DiscordIpcClient}; +use discord_rich_presence::activity::{Activity, Assets, Button, Timestamps}; +use discord_rich_presence::{DiscordIpc, DiscordIpcClient}; use tokio::sync::RwLock; use crate::error::LauncherResult; @@ -15,11 +17,13 @@ pub struct DiscordRPC { impl DiscordRPC { pub fn initialize() -> LauncherResult { - let client_id = Core::get().discord_client_id.clone() + let client_id = Core::get() + .discord_client_id + .clone() .ok_or(DiscordError::MissingClientId)?; - let mut client = DiscordIpcClient::new(client_id.as_str()) - .map_err(|_| DiscordError::ConnectError)?; + let mut client = + DiscordIpcClient::new(client_id.as_str()).map_err(|_| DiscordError::ConnectError)?; let connected = client.connect().is_ok(); @@ -34,18 +38,15 @@ impl DiscordRPC { self.set_activity( Activity::new() .state(msg) - .buttons( - vec![Button::new("Website", "https://polyfrost.org/")] - ) - .timestamps( - timestamp.unwrap_or_else(|| Timestamps::new().start(self.started_at)) - ) + .buttons(vec![Button::new("Website", "https://polyfrost.org/")]) + .timestamps(timestamp.unwrap_or_else(|| Timestamps::new().start(self.started_at))) .assets( Assets::new() .large_image("onelauncher_logo_512") .large_text("OneLauncher"), - ) - ).await + ), + ) + .await } pub async fn set_activity(&self, activity: Activity<'_>) -> bool { @@ -102,7 +103,8 @@ impl DiscordRPC { } fn set_connected(&self, connected: bool) { - self.connected.store(connected, std::sync::atomic::Ordering::Relaxed); + self.connected + .store(connected, std::sync::atomic::Ordering::Relaxed); } } @@ -113,4 +115,4 @@ pub enum DiscordError { MissingClientId, #[error("couldn't connect to Discord IPC")] ConnectError, -} \ No newline at end of file +} diff --git a/packages/core/src/store/ingress.rs b/packages/core/src/store/ingress.rs index 10184951..84756d3c 100644 --- a/packages/core/src/store/ingress.rs +++ b/packages/core/src/store/ingress.rs @@ -4,9 +4,9 @@ use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use uuid::Uuid; -use crate::api::proxy::event::try_send_event; use crate::LauncherResult; use crate::api::ingress::IngressPayload; +use crate::api::proxy::event::try_send_event; use super::State; @@ -88,8 +88,7 @@ impl IngressProcessor { total: ingress.total, }; - try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload)) - .await?; + try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload)).await?; Ok(()) } @@ -219,8 +218,7 @@ impl Drop for IngressId { total: ingress.total, }; - try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload)) - .await?; + try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload)).await?; tracing::trace!( "exited at {}% for ingress {:?}", diff --git a/packages/core/src/store/metadata.rs b/packages/core/src/store/metadata.rs index e05b6e92..79929730 100644 --- a/packages/core/src/store/metadata.rs +++ b/packages/core/src/store/metadata.rs @@ -20,7 +20,7 @@ use super::Dirs; pub struct Metadata { initialized: bool, inner: MetadataInner, - version_loader_cache: HashMap> + version_loader_cache: HashMap>, } #[derive(Debug, Default, Serialize, Deserialize)] @@ -46,7 +46,7 @@ pub enum MetadataError { ParseError( #[from] #[skip] - serde_json::Error + serde_json::Error, ), #[error("no matching loader found")] NoMatchingLoader, @@ -65,7 +65,6 @@ impl Metadata { Self::default() } - #[tracing::instrument(skip(self))] pub async fn get_vanilla_or_fetch(&mut self) -> LauncherResult<&VanillaManifest> { if !self.initialized() { @@ -76,7 +75,10 @@ impl Metadata { } #[tracing::instrument(skip(self))] - pub async fn get_modded_or_fetch(&mut self, loader: &GameLoader) -> LauncherResult<&ModdedManifest> { + pub async fn get_modded_or_fetch( + &mut self, + loader: &GameLoader, + ) -> LauncherResult<&ModdedManifest> { if !loader.is_modded() { return Err(MetadataError::NotModdedManifest(*loader).into()); } @@ -89,7 +91,10 @@ impl Metadata { } pub fn get_vanilla(&self) -> LauncherResult<&VanillaManifest> { - self.inner.minecraft.as_ref().ok_or_else(|| MetadataError::FetchError.into()) + self.inner + .minecraft + .as_ref() + .ok_or_else(|| MetadataError::FetchError.into()) } pub fn get_modded(&self, loader: &GameLoader) -> LauncherResult<&ModdedManifest> { @@ -103,7 +108,8 @@ impl Metadata { GameLoader::Fabric => self.inner.fabric.as_ref(), GameLoader::Quilt => self.inner.quilt.as_ref(), _ => None, - }.ok_or_else(|| MetadataError::FetchError.into()) + } + .ok_or_else(|| MetadataError::FetchError.into()) } #[tracing::instrument(skip_all)] @@ -144,18 +150,16 @@ impl Metadata { ($var:tt, $is_modded:tt) => { if self.inner.$var.is_none() { match GameLoader::from_str(stringify!($var)) { - Ok(loader) => { - match check!(_fetch, loader, $is_modded) { - Ok(data) => { - self.inner.$var = Some(data); - changed += 1; - } - Err(err) => { - tracing::error!("failed to fetch manifest for {}: {}", loader, err); - } + Ok(loader) => match check!(_fetch, loader, $is_modded) { + Ok(data) => { + self.inner.$var = Some(data); + changed += 1; + } + Err(err) => { + tracing::error!("failed to fetch manifest for {}: {}", loader, err); } }, - Err(err) => tracing::error!("{err}") + Err(err) => tracing::error!("{err}"), }; } }; @@ -225,7 +229,10 @@ impl Metadata { /// Returns a list of loaders that are compatible with the given Minecraft version. /// /// WARNING: Can be heavy in certain cases! - pub async fn get_loaders_for_version(&mut self, mc_version: &str) -> LauncherResult> { + pub async fn get_loaders_for_version( + &mut self, + mc_version: &str, + ) -> LauncherResult> { if !self.initialized() { self.initialize().await?; } @@ -248,7 +255,8 @@ impl Metadata { } } - self.version_loader_cache.insert(mc_version.to_owned(), loaders.clone()); + self.version_loader_cache + .insert(mc_version.to_owned(), loaders.clone()); Ok(loaders) } diff --git a/packages/core/src/store/mod.rs b/packages/core/src/store/mod.rs index f49cecaf..df767b21 100644 --- a/packages/core/src/store/mod.rs +++ b/packages/core/src/store/mod.rs @@ -1,17 +1,17 @@ +mod core_options; +pub mod credentials; pub(super) mod db; +mod dirs; pub mod discord; pub mod ingress; +pub mod metadata; +pub mod processes; pub mod proxy; pub mod semaphore; -pub mod metadata; -pub mod credentials; -pub mod processes; -mod core_options; -mod dirs; mod settings; mod state; +pub use core_options::*; pub use dirs::*; pub use settings::*; pub use state::*; -pub use core_options::*; \ No newline at end of file diff --git a/packages/core/src/store/processes.rs b/packages/core/src/store/processes.rs index a3790d2c..8d8c7f7b 100644 --- a/packages/core/src/store/processes.rs +++ b/packages/core/src/store/processes.rs @@ -187,7 +187,8 @@ impl ProcessStore { send_error!("{err:?}"); } - command.stdout(Stdio::piped()) + command + .stdout(Stdio::piped()) .stderr(Stdio::piped()) .stdin(Stdio::null()); @@ -324,7 +325,10 @@ async fn run_hook(hook: &str, cwd: PathBuf) -> LauncherResult> { } } -async fn wait_for_exit(cluster_id: ClusterId, child: &Arc>) -> LauncherResult { +async fn wait_for_exit( + cluster_id: ClusterId, + child: &Arc>, +) -> LauncherResult { let mut last_updated = Utc::now(); loop { diff --git a/packages/core/src/store/proxy.rs b/packages/core/src/store/proxy.rs index 7a449173..3046edab 100644 --- a/packages/core/src/store/proxy.rs +++ b/packages/core/src/store/proxy.rs @@ -1,9 +1,11 @@ -use std::{ops::Deref, sync::Arc}; +use std::ops::Deref; +use std::sync::Arc; use anyhow::anyhow; use tokio::sync::OnceCell; -use crate::{api::proxy::LauncherProxy, LauncherResult}; +use crate::LauncherResult; +use crate::api::proxy::LauncherProxy; static PROXY_STATE: OnceCell> = OnceCell::const_new(); @@ -38,4 +40,4 @@ impl ProxyState { .ok_or_else(|| anyhow!("proxy state not initialized")) .cloned()?) } -} \ No newline at end of file +} diff --git a/packages/core/src/store/semaphore.rs b/packages/core/src/store/semaphore.rs index 3b60ee5d..050633bc 100644 --- a/packages/core/src/store/semaphore.rs +++ b/packages/core/src/store/semaphore.rs @@ -5,12 +5,15 @@ use tokio::sync::{OnceCell, Semaphore}; static STORE_STATE: OnceCell> = OnceCell::const_new(); pub struct SemaphoreStore { - pub fetch: Arc + pub fetch: Arc, } impl SemaphoreStore { pub async fn get() -> Arc { - STORE_STATE.get_or_init(|| async { Self::initialize() }).await.clone() + STORE_STATE + .get_or_init(|| async { Self::initialize() }) + .await + .clone() } #[tracing::instrument] @@ -23,4 +26,4 @@ impl SemaphoreStore { pub async fn fetch() -> Arc { Self::get().await.fetch.clone() } -} \ No newline at end of file +} diff --git a/packages/core/src/store/settings.rs b/packages/core/src/store/settings.rs index 6382f235..72d6732a 100644 --- a/packages/core/src/store/settings.rs +++ b/packages/core/src/store/settings.rs @@ -1,7 +1,8 @@ use onelauncher_entity::setting_profiles; use serde::{Deserialize, Serialize}; -use crate::{send_warning, utils::io, LauncherResult}; +use crate::utils::io; +use crate::{LauncherResult, send_warning}; use super::Dirs; @@ -15,7 +16,7 @@ pub struct Settings { pub discord_enabled: bool, pub max_concurrent_requests: usize, pub settings_version: u32, - pub native_window_frame: bool + pub native_window_frame: bool, } impl Default for Settings { @@ -40,7 +41,7 @@ impl Settings { Err(err) => { send_warning!("Failed to read settings file: {}", err); Self::default() - }, + } } } @@ -57,4 +58,4 @@ impl Settings { Ok(()) } -} \ No newline at end of file +} diff --git a/packages/core/src/store/state.rs b/packages/core/src/store/state.rs index c3920114..e6c422e3 100644 --- a/packages/core/src/store/state.rs +++ b/packages/core/src/store/state.rs @@ -3,15 +3,15 @@ use std::sync::Arc; use sea_orm::DatabaseConnection; use tokio::sync::{OnceCell, RwLock}; -use crate::store::Dirs; use crate::LauncherResult; +use crate::store::Dirs; -use super::credentials::CredentialsStore; -use super::processes::ProcessStore; use super::Settings; +use super::credentials::CredentialsStore; use super::discord::DiscordRPC; use super::ingress::IngressProcessor; use super::metadata::Metadata; +use super::processes::ProcessStore; /// The static [`OnceCell>`] for storing the global runtime launcher state. static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); @@ -32,8 +32,7 @@ impl State { Ok(LAUNCHER_STATE .get_or_try_init(Self::initialize) .await? - .clone() - ) + .clone()) } pub fn initialized() -> bool { diff --git a/packages/core/src/utils/crypto.rs b/packages/core/src/utils/crypto.rs index 5687c906..5df68e74 100644 --- a/packages/core/src/utils/crypto.rs +++ b/packages/core/src/utils/crypto.rs @@ -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 { let mut hasher = get_inner(self); hasher.update(data); @@ -110,7 +111,7 @@ fn get_inner(algorithm: &HashAlgorithm) -> Box { 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 }) -} \ No newline at end of file +} diff --git a/packages/core/src/utils/http.rs b/packages/core/src/utils/http.rs index 3ddd9784..dea14d51 100644 --- a/packages/core/src/utils/http.rs +++ b/packages/core/src/utils/http.rs @@ -12,9 +12,9 @@ use tokio_stream::StreamExt; use crate::api::ingress::IngressSendExt; use crate::error::LauncherResult; +use crate::store::Core; use crate::store::ingress::{SubIngress, SubIngressExt}; use crate::store::semaphore::SemaphoreStore; -use crate::store::Core; use super::crypto::{CryptoError, HashAlgorithm}; use super::io; @@ -82,13 +82,15 @@ pub async fn fetch_advanced( match req.send().await { Err(err) => { if attempt <= Core::get().fetch_attempts { - tracing::error!("error occurred whilst fetching on attempt {attempt}: {err} retrying request..."); + tracing::error!( + "error occurred whilst fetching on attempt {attempt}: {err} retrying request..." + ); continue; } - return Err(err.into()) - }, - Ok(res) => break res + return Err(err.into()); + } + Ok(res) => break res, } }; @@ -102,13 +104,12 @@ pub async fn fetch_advanced( let mut bytes = Vec::new(); while let Some(item) = stream.next().await { - let chunk = - item.or(Err(anyhow::anyhow!("no value for fetch bytes")))?; + let chunk = item.or(Err(anyhow::anyhow!("no value for fetch bytes")))?; bytes.append(&mut chunk.to_vec()); - ingress.send_ingress( - (chunk.len() as f64 / total_size as f64) * ingress.total, - ).await?; + ingress + .send_ingress((chunk.len() as f64 / total_size as f64) * ingress.total) + .await?; } Ok(Bytes::from(bytes)) @@ -131,7 +132,7 @@ pub async fn fetch_advanced( } .into()); } - }, + } Err(err) => { tracing::error!("failed to calculate hash for {url}: {err}"); return Err(err.into()); @@ -178,7 +179,10 @@ pub async fn download_advanced( ingress: Option<&SubIngress<'_>>, ) -> LauncherResult { const TASKS: f64 = 3.0; - let ingress_step = ingress.ingress_total().map(|total| total / TASKS).unwrap_or_default(); + let ingress_step = ingress + .ingress_total() + .map(|total| total / TASKS) + .unwrap_or_default(); let bytes = fetch_advanced( method, @@ -209,7 +213,7 @@ pub async fn download( url: &str, path: impl AsRef, hash: Option<(HashAlgorithm, &str)>, - ingress: Option<&SubIngress<'_>> + ingress: Option<&SubIngress<'_>>, ) -> LauncherResult { download_advanced(method, url, path, None, None, hash, ingress).await } diff --git a/packages/core/src/utils/icon.rs b/packages/core/src/utils/icon.rs index 15b28b43..02e5bb30 100644 --- a/packages/core/src/utils/icon.rs +++ b/packages/core/src/utils/icon.rs @@ -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 { @@ -12,17 +14,16 @@ pub async fn cache_icon(icon: &Icon) -> LauncherResult { 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 { let path = dir.join(&hash); io::write(&path, bytes).await?; - Ok(Icon::from_hash(hash)) + Ok(Icon::from_hash(&hash)) } diff --git a/packages/core/src/utils/io.rs b/packages/core/src/utils/io.rs index 34702c61..bbad400e 100644 --- a/packages/core/src/utils/io.rs +++ b/packages/core/src/utils/io.rs @@ -1,10 +1,11 @@ -use std::{path::PathBuf, sync::Arc}; +use std::path::PathBuf; +use std::sync::Arc; use async_compression::tokio::bufread::GzipDecoder; use async_stream::try_stream; use async_zip::StoredZipEntry; use async_zip::base::read::WithoutEntry; -use futures::{pin_mut, Stream, TryStreamExt}; +use futures::{Stream, TryStreamExt, pin_mut}; use serde::Serialize; use serde::de::DeserializeOwned; use tempfile::TempDir; @@ -27,11 +28,23 @@ pub enum IOError { path: String, }, #[error(transparent)] - IOError(#[from] #[skip] std::io::Error), + IOError( + #[from] + #[skip] + std::io::Error, + ), #[error("json deserialization error: {0}")] - DeserializeError(#[from] #[skip] serde_json::Error), + DeserializeError( + #[from] + #[skip] + serde_json::Error, + ), #[error(transparent)] - AsyncZipError(#[from] #[skip] async_zip::error::ZipError), + AsyncZipError( + #[from] + #[skip] + async_zip::error::ZipError, + ), } impl> From<(P, std::io::Error)> for IOError { @@ -283,9 +296,16 @@ where /// Reads a zip archive from a byte array and returns a stream of entries. pub fn stream_zip_entries_bytes( data: Vec, -) -> impl Stream -), IOError>> { +) -> impl Stream< + Item = Result< + ( + usize, + StoredZipEntry, + Arc, + ), + IOError, + >, +> { try_stream! { let reader = Arc::new(async_zip::base::read::mem::ZipFileReader::new(data).await?); let entries = reader.file().entries(); @@ -319,9 +339,10 @@ pub async fn unzip_bytes_filtered( let file_name = entry.filename().as_str()?; if let Some(filter) = &filter_entries - && !filter(file_name) { - continue; - } + && !filter(file_name) + { + continue; + } let path = dest_path.as_ref().join(sanitize_path(file_name)); diff --git a/packages/core/src/utils/mod.rs b/packages/core/src/utils/mod.rs index 54a197b5..6ea42db3 100644 --- a/packages/core/src/utils/mod.rs +++ b/packages/core/src/utils/mod.rs @@ -1,6 +1,6 @@ pub mod crypto; -pub mod os_ext; pub mod http; pub mod icon; pub mod io; -pub mod pagination; \ No newline at end of file +pub mod os_ext; +pub mod pagination; diff --git a/packages/core/src/utils/os_ext.rs b/packages/core/src/utils/os_ext.rs index 9c078ecd..fdc65fab 100644 --- a/packages/core/src/utils/os_ext.rs +++ b/packages/core/src/utils/os_ext.rs @@ -45,4 +45,4 @@ impl OsExt for Os { _ => Self::Unknown, } } -} \ No newline at end of file +} diff --git a/packages/core/src/utils/pagination.rs b/packages/core/src/utils/pagination.rs index 467aafe9..249d039c 100644 --- a/packages/core/src/utils/pagination.rs +++ b/packages/core/src/utils/pagination.rs @@ -1,11 +1,13 @@ +use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::marker::Send; -use serde::{Deserialize, Serialize}; #[onelauncher_macro::specta] #[derive(Clone, Serialize, Deserialize)] pub struct Paginated -where T: Clone + Serialize + Send + Sync { +where + T: Clone + Serialize + Send + Sync, +{ pub total: usize, pub offset: usize, pub limit: usize, @@ -23,9 +25,14 @@ impl Debug for Paginated { } } -impl Paginated { +impl Paginated { #[must_use] pub const fn new(total: usize, offset: usize, limit: usize, items: Vec) -> Self { - Self { total, offset, limit, items } + Self { + total, + offset, + limit, + items, + } } } diff --git a/packages/entity/src/domain/cluster_stage.rs b/packages/entity/src/domain/cluster_stage.rs index 5d2228cc..4da82160 100644 --- a/packages/entity/src/domain/cluster_stage.rs +++ b/packages/entity/src/domain/cluster_stage.rs @@ -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", + } + ) } } diff --git a/packages/entity/src/domain/icon.rs b/packages/entity/src/domain/icon.rs index 3c57e784..efb30bfe 100644 --- a/packages/entity/src/domain/icon.rs +++ b/packages/entity/src/domain/icon.rs @@ -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 { + #[must_use] + pub fn try_from_url(url: &url::Url) -> Option { 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://") } diff --git a/packages/entity/src/domain/loader.rs b/packages/entity/src/domain/loader.rs index fa4dca20..a2551aef 100644 --- a/packages/entity/src/domain/loader.rs +++ b/packages/entity/src/domain/loader.rs @@ -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 for GameLoader { type Error = String; - fn try_from(s: String) -> Result { - 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 { + 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", + }) } } diff --git a/packages/entity/src/domain/package.rs b/packages/entity/src/domain/package.rs index 4f99c3c8..44efdda7 100644 --- a/packages/entity/src/domain/package.rs +++ b/packages/entity/src/domain/package.rs @@ -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] } -} \ No newline at end of file +} diff --git a/packages/entity/src/domain/resolution.rs b/packages/entity/src/domain/resolution.rs index 5ab9d27a..5a71df8b 100644 --- a/packages/entity/src/domain/resolution.rs +++ b/packages/entity/src/domain/resolution.rs @@ -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 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 { 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) } } - diff --git a/packages/entity/src/domain/settings.rs b/packages/entity/src/domain/settings.rs index ed8c337a..19a3d2f7 100644 --- a/packages/entity/src/domain/settings.rs +++ b/packages/entity/src/domain/settings.rs @@ -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 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 { 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) } -} \ No newline at end of file +} diff --git a/packages/entity/src/domain/utility.rs b/packages/entity/src/domain/utility.rs index 97cd515b..3589178a 100644 --- a/packages/entity/src/domain/utility.rs +++ b/packages/entity/src/domain/utility.rs @@ -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 sea_orm::TryGetableFromJson for DbVec where for<'de> T: Deserialize<' impl std::convert::From> for sea_orm::Value where - DbVec: Serialize, + DbVec: Serialize, { - fn from(source: DbVec) -> Self { - sea_orm::Value::Json( - serde_json::to_value(&source) - .ok() - .map(std::boxed::Box::new), - ) - } + fn from(source: DbVec) -> Self { + Self::Json(serde_json::to_value(&source).ok().map(std::boxed::Box::new)) + } } impl sea_orm::sea_query::ValueType for DbVec where - DbVec: DeserializeOwned, + Self: DeserializeOwned, { - fn try_from(v: sea_orm::Value) -> Result { - 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 { + 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 sea_orm::sea_query::Nullable for DbVec { - fn null() -> sea_orm::Value { - sea_orm::Value::Json(None) - } -} \ No newline at end of file + fn null() -> sea_orm::Value { + sea_orm::Value::Json(None) + } +} diff --git a/packages/entity/src/entities/setting_profiles.rs b/packages/entity/src/entities/setting_profiles.rs index 387b414d..5db59f06 100644 --- a/packages/entity/src/entities/setting_profiles.rs +++ b/packages/entity/src/entities/setting_profiles.rs @@ -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, #[sea_orm(nullable)] - pub os_extra: Option + pub os_extra: Option, } #[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()), } } } diff --git a/packages/macro/src/error.rs b/packages/macro/src/error.rs index 0d98cbd3..4512a5a9 100644 --- a/packages/macro/src/error.rs +++ b/packages/macro/src/error.rs @@ -1,9 +1,9 @@ use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_macro_input, Data, DeriveInput, Fields}; +use syn::{Data, DeriveInput, Fields, parse_macro_input}; pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream { - let input = parse_macro_input!(item as DeriveInput); + let input = parse_macro_input!(item as DeriveInput); let Data::Enum(data_enum) = &input.data else { return syn::Error::new_spanned(&input.ident, "Only enums are supported") @@ -98,82 +98,76 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream { } }; - let output = quote! { - #[cfg_attr(feature = "specta", derive(onelauncher_macro::SerializedError))] - #input + let output = quote! { + #[cfg_attr(feature = "specta", derive(onelauncher_macro::SerializedError))] + #input #serialize_impl - }; + }; - output.into() + output.into() } pub fn error_derive(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); + let input = parse_macro_input!(input as DeriveInput); - let enum_name = &input.ident; - let specta_name = format_ident!("{}SpectaType", enum_name); - let mut variants = vec![]; + let enum_name = &input.ident; + let specta_name = format_ident!("{}SpectaType", enum_name); + let mut variants = vec![]; - let Data::Enum(data_enum) = &input.data else { - return syn::Error::new_spanned(enum_name, "Only enums are supported") - .to_compile_error() - .into(); - }; + let Data::Enum(data_enum) = &input.data else { + return syn::Error::new_spanned(enum_name, "Only enums are supported") + .to_compile_error() + .into(); + }; - for variant in &data_enum.variants { - let name = &variant.ident; - let ty = match &variant.fields { - Fields::Unit => quote! { String }, + for variant in &data_enum.variants { + let name = &variant.ident; + let ty = match &variant.fields { + Fields::Unit => quote! { String }, - Fields::Unnamed(fields) => { - // Handle newtype-style or tuple-like enums - if fields.unnamed.len() == 1 { - let field = &fields.unnamed[0]; - let field_ty = &field.ty; + Fields::Unnamed(fields) => { + // Handle newtype-style or tuple-like enums + if fields.unnamed.len() == 1 { + let field = &fields.unnamed[0]; + let field_ty = &field.ty; - let has_from = field - .attrs - .iter() - .any(|a| a.path().is_ident("from")); + let has_from = field.attrs.iter().any(|a| a.path().is_ident("from")); - let has_skip = field - .attrs - .iter() - .any(|a| a.path().is_ident("skip")); + let has_skip = field.attrs.iter().any(|a| a.path().is_ident("skip")); - if has_from && !has_skip { - quote! { + if has_from && !has_skip { + quote! { #field_ty } - } else { - quote! { String } - } - } else { - quote! { String } - } - } + } else { + quote! { String } + } + } else { + quote! { String } + } + } - Fields::Named(_) => { - quote! { String } // Simplification: treat all struct-like variants as String - } - }; + Fields::Named(_) => { + quote! { String } // Simplification: treat all struct-like variants as String + } + }; - variants.push(quote! { - #name(#ty) - }); - } + variants.push(quote! { + #name(#ty) + }); + } let enum_name_str = enum_name.to_string(); let output = quote! { - #[derive(Debug, specta::Type, serde::Serialize)] - #[specta(remote = #enum_name)] + #[derive(Debug, specta::Type, serde::Serialize)] + #[specta(remote = #enum_name)] #[specta(tag = "type", content = "data")] #[specta(rename = #enum_name_str)] - pub enum #specta_name { - #(#variants,)* - } - }; + pub enum #specta_name { + #(#variants,)* + } + }; - output.into() -} \ No newline at end of file + output.into() +} diff --git a/packages/macro/src/lib.rs b/packages/macro/src/lib.rs index 89cd0997..20138247 100644 --- a/packages/macro/src/lib.rs +++ b/packages/macro/src/lib.rs @@ -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) } diff --git a/packages/macro/src/pin.rs b/packages/macro/src/pin.rs index ac1ae23a..eba1a30a 100644 --- a/packages/macro/src/pin.rs +++ b/packages/macro/src/pin.rs @@ -1,7 +1,10 @@ use quote::quote; -use syn::{parse_macro_input, ItemFn}; +use syn::{ItemFn, parse_macro_input}; -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 { let input = parse_macro_input!(item as ItemFn); let attrs = &input.attrs; @@ -19,4 +22,4 @@ pub fn pin(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> pro }; result.into() -} \ No newline at end of file +} diff --git a/packages/macro/src/specta.rs b/packages/macro/src/specta.rs index 9eb2aa2f..6ee917b5 100644 --- a/packages/macro/src/specta.rs +++ b/packages/macro/src/specta.rs @@ -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(()) - } -} \ No newline at end of file + Ok(()) + } +} diff --git a/packages/web_common/src/components/AnimatedOutlet.tsx b/packages/web_common/src/components/AnimatedOutlet.tsx index cd2f90ff..79886ba6 100644 --- a/packages/web_common/src/components/AnimatedOutlet.tsx +++ b/packages/web_common/src/components/AnimatedOutlet.tsx @@ -15,8 +15,8 @@ interface TransitionProps { } interface AnimatedOutletProps { - enter: TransitionProps; - exit: TransitionProps; + enter?: TransitionProps; + exit?: TransitionProps; transition?: MotionProps['transition']; from: AnyRoute['id']; clone?: boolean; @@ -126,10 +126,10 @@ export function AnimatedOutlet({
{snapshots.map(snapshot => (