diff --git a/.vscode/settings.json b/.vscode/settings.json index 75acb0da..8beb3b56 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,7 +23,7 @@ "**/.crates": true, "**/routeTree.gen.ts": true }, - "rust-analyzer.procMacro.enable": false, + "rust-analyzer.procMacro.enable": true, "rust-analyzer.procMacro.attributes.enable": false, "rust-analyzer.procMacro.ignored": { "async-trait": ["async_trait"], diff --git a/Cargo.lock b/Cargo.lock index d2d8816c..2108adba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3344,6 +3344,35 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "oneclient_gui" +version = "0.2.0-alpha.2" +dependencies = [ + "anyhow", + "chrono", + "interpulse", + "onelauncher_core", + "serde", + "serde_json", + "specta", + "specta-typescript", + "strum 0.27.1", + "tauri", + "tauri-build", + "tauri-plugin-clipboard-manager", + "tauri-plugin-deep-link", + "tauri-plugin-dialog", + "tauri-plugin-single-instance", + "tauri-plugin-updater", + "tauri-plugin-window-state", + "tauri-specta", + "thiserror 2.0.12", + "tokio", + "tracing", + "tracing-error", + "uuid 1.16.0", +] + [[package]] name = "onelauncher_core" version = "0.2.0-alpha.2" diff --git a/Cargo.toml b/Cargo.toml index 85c5c90d..84fb4494 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "apps/oneclient/desktop", "apps/onelauncher/desktop", "packages/core", "packages/entity", diff --git a/apps/oneclient/desktop/.gitignore b/apps/oneclient/desktop/.gitignore new file mode 100644 index 00000000..787194aa --- /dev/null +++ b/apps/oneclient/desktop/.gitignore @@ -0,0 +1,3 @@ +# Generated by Tauri +.cargo/ +/gen diff --git a/apps/oneclient/desktop/App.entitlements b/apps/oneclient/desktop/App.entitlements new file mode 100644 index 00000000..48f4264c --- /dev/null +++ b/apps/oneclient/desktop/App.entitlements @@ -0,0 +1,18 @@ + + + + + + com.apple.security.cs.disable-library-validation + + + com.apple.security.cs.allow-dyld-environment-variables + + + com.apple.security.device.audio-input + + + com.apple.security.device.camera + + + diff --git a/apps/oneclient/desktop/Cargo.toml b/apps/oneclient/desktop/Cargo.toml new file mode 100644 index 00000000..dc1368bf --- /dev/null +++ b/apps/oneclient/desktop/Cargo.toml @@ -0,0 +1,80 @@ +[package] +name = "oneclient_gui" +default-run = "oneclient_gui" +description = "Next-generation open source Minecraft launcher" +version = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } +documentation = { workspace = true } +readme = { workspace = true } +homepage = { workspace = true } +authors = { workspace = true } + +[lib] +name = "oneclient_gui" +crate-type = [ + "staticlib", + "cdylib", + "lib" +] + +[lints] +workspace = true + +[build-dependencies] +tauri-build = { workspace = true } + +[dependencies] +# core +onelauncher_core = { workspace = true, features = [ "tauri" ] } +interpulse = { workspace = true } + +# tauri +tauri-plugin-window-state = { workspace = true } +tauri-plugin-single-instance = { workspace = true } +tauri-plugin-updater = { workspace = true } +tauri-plugin-clipboard-manager = { workspace = true } +tauri-plugin-dialog = { workspace = true } +tauri-plugin-deep-link = { workspace = true } + +# code gen +tauri-specta = { workspace = true } +specta = { workspace = true } +specta-typescript = { workspace = true } +strum = { workspace = true } + +# logging +tracing = { workspace = true } +tracing-error = { workspace = true } + +# serialization & text +serde = { workspace = true } +serde_json = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } + +# io +tokio = { workspace = true } + +# utils +thiserror = { workspace = true } +anyhow = { workspace = true } + +[dependencies.tauri] +workspace = true +features = [ + "macos-private-api", + "image-ico", + "image-png", + "protocol-asset", + "specta" +] + +[features] +default = [ + "custom-protocol", + "devtools" +] +devtools = [ "tauri/devtools" ] +custom-protocol = [ "tauri/custom-protocol" ] diff --git a/apps/oneclient/desktop/Info.plist b/apps/oneclient/desktop/Info.plist new file mode 100644 index 00000000..041bdfca --- /dev/null +++ b/apps/oneclient/desktop/Info.plist @@ -0,0 +1,24 @@ + + + + + + CFBundleURLTypes + + + CFBundleURLName + OneClient + CFBundleURLSchemes + + oneclient + + + + + NSCameraUsageDescription + A Minecraft mod is requesting access to your camera. + + NSMicrophoneUsageDescription + A Minecraft mod is requesting access to your microphone. + + diff --git a/apps/oneclient/desktop/build.rs b/apps/oneclient/desktop/build.rs new file mode 100644 index 00000000..106b7123 --- /dev/null +++ b/apps/oneclient/desktop/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build(); +} diff --git a/apps/oneclient/desktop/capabilities/default.json b/apps/oneclient/desktop/capabilities/default.json new file mode 100644 index 00000000..40d2165e --- /dev/null +++ b/apps/oneclient/desktop/capabilities/default.json @@ -0,0 +1,36 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "capabilities for the main window", + "windows": ["*"], + "permissions": [ + "core:app:default", + "core:webview:default", + "core:image:default", + "core:menu:default", + "core:path:default", + "core:resources:default", + "core:tray:default", + "core:window:default", + + "core:window:allow-close", + "core:window:allow-create", + "core:window:allow-destroy", + "core:window:allow-start-dragging", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-toggle-maximize", + "core:window:allow-internal-toggle-maximize", + "core:window:allow-set-decorations", + "core:window:allow-set-title-bar-style", + "core:webview:allow-internal-toggle-devtools", + + "core:event:default", + "clipboard-manager:allow-write-text", + "dialog:allow-open", + "dialog:allow-save", + "dialog:allow-confirm", + "deep-link:default", + "updater:default" + ] +} diff --git a/apps/oneclient/desktop/package.json b/apps/oneclient/desktop/package.json new file mode 100644 index 00000000..bb196947 --- /dev/null +++ b/apps/oneclient/desktop/package.json @@ -0,0 +1,27 @@ +{ + "name": "@oneclient/desktop", + "type": "module", + "version": "0.1.0-alpha.2", + "private": true, + "description": "Next-generation open source Minecraft launcher", + "license": "GPL-3.0-only", + "repository": { + "type": "git", + "url": "git@github.com:polyfrost/onelauncher", + "directory": "apps/desktop" + }, + "bugs": "https://github.com/polyfrost/onelauncher/issues", + "sideEffects": false, + "scripts": { + "build": "tauri build", + "dev": "tauri dev", + "tauri": "tauri", + "tauri-ext": "pnpm --filter @onelauncher/scripts -- tauri" + }, + "dependencies": { + "@oneclient/frontend": "workspace:*" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.4" + } +} diff --git a/apps/oneclient/desktop/src/api/commands.rs b/apps/oneclient/desktop/src/api/commands.rs new file mode 100644 index 00000000..48fa8e68 --- /dev/null +++ b/apps/oneclient/desktop/src/api/commands.rs @@ -0,0 +1,30 @@ +pub mod cluster; +pub mod users; +pub mod processor; +pub mod package; +pub mod other; +pub mod settings; + +#[macro_export(local_inner_macros)] +macro_rules! collect_commands { + () => {{ + use $crate::api::commands::*; + tauri_specta::collect_commands![ + users::get_users, + users::get_user, + users::remove_user, + users::get_default_user, + users::set_default_user, + users::begin_ms_flow, + + cluster::create_cluster, + cluster::get_clusters, + cluster::get_cluster_by_id, + + settings::get_global_profile, + settings::get_profile_or_default, + + other::open_dev_tools, + ] + }}; +} diff --git a/apps/oneclient/desktop/src/api/commands/cluster.rs b/apps/oneclient/desktop/src/api/commands/cluster.rs new file mode 100644 index 00000000..1bb53684 --- /dev/null +++ b/apps/oneclient/desktop/src/api/commands/cluster.rs @@ -0,0 +1,40 @@ +use onelauncher_core::{api::{cluster, setting_profiles}, entity::{clusters::Model, icon::Icon, loader::GameLoader}, migration::sea_orm}; +use serde::{Deserialize, Serialize}; +use specta::Type; +use crate::api::error::SerializableResult; + +#[derive(Serialize, Deserialize, Type)] +pub struct CreateCluster { + name: String, + mc_version: String, + mc_loader: GameLoader, + mc_loader_version: Option, + icon_url: Option +} + +#[specta::specta] +#[tauri::command] +pub async fn create_cluster(options: CreateCluster) -> SerializableResult { + let thing = cluster::create_cluster(&options.name, &options.mc_version, options.mc_loader, options.mc_loader_version.as_deref(), options.icon_url).await?; + + if setting_profiles::dao::get_profile_by_name(&options.name).await?.is_none() { + setting_profiles::create_profile(&options.name, async |mut profile| { + profile.mem_max = sea_orm::ActiveValue::Set(Some(2048)); + Ok(profile) + }).await?; + } + + Ok(thing) +} + +#[specta::specta] +#[tauri::command] +pub async fn get_clusters() -> SerializableResult> { + Ok(cluster::dao::get_all_clusters().await?) +} + +#[specta::specta] +#[tauri::command] +pub async fn get_cluster_by_id(id: i64) -> SerializableResult> { + Ok(cluster::dao::get_cluster_by_id(id).await?) +} \ No newline at end of file diff --git a/apps/oneclient/desktop/src/api/commands/other.rs b/apps/oneclient/desktop/src/api/commands/other.rs new file mode 100644 index 00000000..8d3014b3 --- /dev/null +++ b/apps/oneclient/desktop/src/api/commands/other.rs @@ -0,0 +1,6 @@ +#[onelauncher_core::command] +#[allow(unused_parens)] +pub fn open_dev_tools(webview: tauri::WebviewWindow) { + #[cfg(feature = "devtools")] + webview.open_devtools(); +} \ No newline at end of file diff --git a/apps/oneclient/desktop/src/api/commands/package.rs b/apps/oneclient/desktop/src/api/commands/package.rs new file mode 100644 index 00000000..165a4b23 --- /dev/null +++ b/apps/oneclient/desktop/src/api/commands/package.rs @@ -0,0 +1,234 @@ +// use std::path::PathBuf; + +// use onelauncher::cluster::content::package; +// use onelauncher::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType}; +// use onelauncher::package::content::Providers; +// use onelauncher::package::import::ImportType; +// use onelauncher::store::{ +// Author, ClusterPath, Package, PackageBody, PackagePath, ProviderSearchResults, +// }; +// use onelauncher::utils::pagination::Pagination; +// use uuid::Uuid; + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_launcher_instances( +// launcher: ImportType, +// path: Option, +// ) -> Result<(String, Vec), String> { +// let (dir, instances) = onelauncher::api::package::import::get_launcher_instances(launcher, path).await?; +// Ok(( +// dir.to_str().ok_or("could not convert pathbuf to string")?.to_string(), +// instances +// )) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn import_instances( +// launcher: ImportType, +// base_path: PathBuf, +// instances: Vec, +// ) -> Result<(), String> { +// onelauncher::api::package::import::import_instances(launcher, base_path, instances).await?; +// Ok(()) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_provider_package( +// provider: Providers, +// project_id: String, +// ) -> Result { +// Ok(provider.get(&project_id).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_provider_packages( +// provider: Providers, +// project_ids: Vec, +// ) -> Result, String> { +// Ok(provider.get_multiple(&project_ids).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_all_provider_package_versions( +// provider: Providers, +// project_id: String, +// game_versions: Option>, +// loaders: Option>, +// page: Option, +// page_size: Option, +// ) -> Result<(Vec, Pagination), String> { +// Ok(provider +// .get_all_versions(&project_id, game_versions, loaders, page, page_size) +// .await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_provider_package_versions( +// provider: Providers, +// versions: Vec, +// ) -> Result, String> { +// Ok(provider.get_versions(versions).await?) +// } + +// #[derive(specta::Type, serde::Deserialize, serde::Serialize)] +// pub struct ProviderSearchQuery { +// query: Option, +// limit: Option, +// offset: Option, +// game_versions: Option>, +// categories: Option>, +// loaders: Option>, +// package_types: Option>, +// open_source: Option, +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn search_provider_packages( +// provider: Providers, +// query: ProviderSearchQuery, +// ) -> Result { +// Ok(provider +// .search( +// query.query, +// query.limit, +// query.offset, +// query.game_versions, +// query.categories, +// query.loaders, +// query.package_types, +// query.open_source, +// ) +// .await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_provider_authors( +// provider: Providers, +// author: Author, +// ) -> Result, String> { +// Ok(provider.get_authors(&author).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_package_body(provider: Providers, body: PackageBody) -> Result { +// Ok(provider.get_package_body(&body).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn download_provider_package( +// provider: Providers, +// package_id: String, +// cluster_id: Uuid, +// game_version: Option, +// loader: Option, +// package_version: Option, +// ) -> Result<(), String> { +// let mut cluster = onelauncher::cluster::get_by_uuid(cluster_id) +// .await? +// .ok_or("cluster not found")?; + +// let mgd_pkg = provider.get(&package_id).await?; + +// let (pkg_path, pkg) = package::download_package( +// &mgd_pkg, +// &mut cluster, +// game_version, +// loader, +// package_version, +// ) +// .await?; + +// package::add_package( +// &cluster.cluster_path(), +// pkg_path, +// pkg, +// ) +// .await?; + +// Ok(()) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_cluster_package( +// cluster_path: ClusterPath, +// package_path: PackagePath, +// package_type: PackageType, +// ) -> Result { +// Ok(package::get_package(&cluster_path, &package_path, package_type).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_cluster_packages( +// cluster_path: ClusterPath, +// package_type: PackageType, +// ) -> Result, String> { +// Ok(package::get_packages(&cluster_path, package_type).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn add_cluster_package( +// cluster_path: ClusterPath, +// file_name: String, +// package_type: PackageType, +// pkg: Package, +// ) -> Result<(), String> { +// let package_path = PackagePath::new(package_type, file_name)?; +// package::add_package(&cluster_path, package_path, pkg).await?; +// Ok(()) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn remove_cluster_package( +// cluster_path: ClusterPath, +// file_name: String, +// package_type: PackageType, +// ) -> Result<(), String> { +// let package_path = PackagePath::new(package_type, file_name)?; +// package::remove_package(&cluster_path, &package_path).await?; +// Ok(()) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn set_cluster_package_enabled( +// cluster_path: ClusterPath, +// file_name: String, +// package_type: PackageType, +// enabled: bool, +// ) -> Result<(), String> { +// let package_path = PackagePath::new(package_type, file_name)?; +// package::set_package_enabled(&cluster_path, &package_path, enabled).await?; +// Ok(()) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn sync_cluster_packages(cluster_path: ClusterPath) -> Result<(), String> { +// package::sync_packages(&cluster_path).await?; +// Ok(()) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn sync_cluster_packages_by_type( +// cluster_path: ClusterPath, +// package_type: PackageType, +// clear: Option, +// ) -> Result<(), String> { +// package::sync_packages_by_type(&cluster_path, package_type, clear).await?; +// Ok(()) +// } diff --git a/apps/oneclient/desktop/src/api/commands/processor.rs b/apps/oneclient/desktop/src/api/commands/processor.rs new file mode 100644 index 00000000..84a18027 --- /dev/null +++ b/apps/oneclient/desktop/src/api/commands/processor.rs @@ -0,0 +1,61 @@ +// use chrono::{DateTime, Utc}; +// use onelauncher::processor::{self, DetailedProcess}; +// use onelauncher::store::{Cluster, ClusterPath}; +// use uuid::Uuid; + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_running_clusters() -> Result, String> { +// Ok(processor::get_running_clusters().await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn is_cluster_running(uuid: Uuid) -> Result { +// Ok(processor::is_cluster_running(uuid).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_processes_by_path(path: ClusterPath) -> Result, String> { +// Ok(processor::get_uuids_by_cluster_path(path).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_processes_detailed_by_path( +// path: ClusterPath, +// ) -> Result, String> { +// Ok(processor::get_processes_detailed_by_path(path).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_process_detailed_by_id(uuid: Uuid) -> Result { +// Ok(processor::get_process_detailed_by_id(uuid).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_user_by_process(uuid: Uuid) -> Result, String> { +// Ok(processor::get_user_by_process(uuid).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_process_started_at(uuid: Uuid) -> Result, String> { +// Ok(processor::get_process_started_at(uuid).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn get_pid_by_uuid(uuid: Uuid) -> Result { +// Ok(processor::get_pid_by_uuid(uuid).await?) +// } + +// #[specta::specta] +// #[tauri::command] +// pub async fn kill_process(uuid: Uuid) -> Result<(), String> { +// processor::kill_by_uuid(uuid).await?; +// Ok(()) +// } diff --git a/apps/oneclient/desktop/src/api/commands/settings.rs b/apps/oneclient/desktop/src/api/commands/settings.rs new file mode 100644 index 00000000..3173c553 --- /dev/null +++ b/apps/oneclient/desktop/src/api/commands/settings.rs @@ -0,0 +1,18 @@ +use onelauncher_core::entity::setting_profiles::Model as SettingsModel; +use onelauncher_core::api::setting_profiles; + +use crate::api::error::SerializableResult; + +#[specta::specta] +#[tauri::command] +pub async fn get_global_profile() -> SerializableResult { + Ok(setting_profiles::get_global_profile().await) +} + +#[specta::specta] +#[tauri::command] +pub async fn get_profile_or_default( + name: Option, +) -> SerializableResult { + Ok(setting_profiles::dao::get_profile_or_default(name.as_ref()).await?) +} \ No newline at end of file diff --git a/apps/oneclient/desktop/src/api/commands/users.rs b/apps/oneclient/desktop/src/api/commands/users.rs new file mode 100644 index 00000000..6c4d4a04 --- /dev/null +++ b/apps/oneclient/desktop/src/api/commands/users.rs @@ -0,0 +1,112 @@ +use onelauncher_core::{api::credentials, error::LauncherResult, store::{credentials::{MinecraftCredentials, MinecraftLogin}, Core}}; +use tauri::{AppHandle, Manager}; +use uuid::Uuid; + +use crate::api::error::SerializableResult; + +#[specta::specta] +#[tauri::command] +pub async fn get_users() -> SerializableResult> { + Ok(credentials::users().await?) +} + +#[specta::specta] +#[tauri::command] +pub async fn get_user(uuid: Uuid) -> SerializableResult { + Ok(credentials::get_user(uuid).await?) +} + +#[specta::specta] +#[tauri::command] +pub async fn remove_user(uuid: Uuid) -> SerializableResult<()> { + Ok(credentials::remove_user(uuid).await?) +} + +#[specta::specta] +#[tauri::command] +pub async fn get_default_user( + fallback: Option, +) -> SerializableResult> { + let uuid = credentials::get_default_user().await?; + + if fallback.is_some_and(|fallback| fallback) && uuid.is_none() { + return Ok(credentials::users().await?.first().cloned()); + } + + match uuid { + Some(uuid) => Ok(Some(credentials::get_user(uuid).await?)), + None => Ok(None), + } +} + +#[specta::specta] +#[tauri::command] +pub async fn set_default_user(uuid: Option) -> SerializableResult<()> { + credentials::set_default_user(uuid).await?; + Ok(()) +} + +#[specta::specta] +#[tauri::command] +pub async fn begin_ms_flow(handle: tauri::AppHandle) -> SerializableResult> { + let flow = credentials::begin().await?; + let result = spawn_webview(handle, flow).await?; + + Ok(result) +} + +async fn spawn_webview( + handle: AppHandle, + flow: MinecraftLogin, +) -> LauncherResult> { + let now = chrono::Utc::now(); + + if let Some(win) = handle.get_webview_window("login") { + win.close()?; + } + + let win = tauri::WebviewWindowBuilder::new( + &handle, + "login", + tauri::WebviewUrl::External( + flow.redirect_uri + .parse() + .map_err(|_| anyhow::anyhow!("failed to parse auth redirect url"))?, + ), + ) + .title(format!("Login to {}", Core::get().launcher_name)) + .center() + .focused(true) + .build()?; + + win.request_user_attention(Some(tauri::UserAttentionType::Critical))?; + + while (chrono::Utc::now() - now) < chrono::Duration::minutes(10) { + if win.title().is_err() { + return Ok(None); + } + + if win + .url()? + .as_str() + .starts_with("https://login.live.com/oauth20_desktop.srf") + { + if let Some((_, code)) = win + .url()? + .query_pairs() + .find(|x| x.0 == "code") + { + win.close()?; + let value = credentials::finish(&code.clone(), flow).await?; + + return Ok(Some(value)); + } + } + + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + + win.close()?; + + Ok(None) +} diff --git a/apps/oneclient/desktop/src/api/error.rs b/apps/oneclient/desktop/src/api/error.rs new file mode 100644 index 00000000..3185836a --- /dev/null +++ b/apps/oneclient/desktop/src/api/error.rs @@ -0,0 +1,61 @@ +use onelauncher_core::error::LauncherError; +use serde::{Serialize, Serializer, ser::SerializeStruct}; +use specta::Type; + +#[derive(thiserror::Error, Debug, Type)] +#[serde(tag = "kind", content = "data")] +pub enum SerializableError { + #[error(transparent)] + CoreError(#[serde(skip)] #[from] onelauncher_core::error::LauncherError), +} + +pub type SerializableResult = std::result::Result; + +macro_rules! impl_serialize { + ($($variant:ident),* $(,)?) => { + impl Serialize for SerializableError { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + match self { + SerializableError::CoreError(error) => { + display_tracing_error(error); + + let mut state = serializer.serialize_struct("CoreError", 2)?; + state.serialize_field("field_name", "CoreError")?; + state.serialize_field("message", &error.to_string())?; + state.end() + } + $( + SerializableError::$variant(message) => { + let mut state = serializer.serialize_struct(stringify!($variant), 2)?; + state.serialize_field("field_name", stringify!($variant))?; + state.serialize_field("message", &message.to_string())?; + state.end() + }, + )* + } + } + } + }; +} + +impl_serialize! {} + +pub fn display_tracing_error(err: &LauncherError) { + match get_span_trace(err) { + Some(span_trace) => { + tracing::error!(error = %err, span_trace = %span_trace); + } + None => { + tracing::error!(error = %err); + } + } +} + +pub fn get_span_trace<'a>( + error: &'a (dyn std::error::Error + 'static), +) -> Option<&'a tracing_error::SpanTrace> { + error.source().and_then(tracing_error::ExtractSpanTrace::span_trace) +} diff --git a/apps/oneclient/desktop/src/api/events.rs b/apps/oneclient/desktop/src/api/events.rs new file mode 100644 index 00000000..42197de5 --- /dev/null +++ b/apps/oneclient/desktop/src/api/events.rs @@ -0,0 +1,8 @@ +#[macro_export] +macro_rules! collect_events { + () => {{ + tauri_specta::collect_events! { + onelauncher_core::api::proxy::event::LauncherEvent + } + }}; +} \ No newline at end of file diff --git a/apps/oneclient/desktop/src/api/mod.rs b/apps/oneclient/desktop/src/api/mod.rs new file mode 100644 index 00000000..4a937850 --- /dev/null +++ b/apps/oneclient/desktop/src/api/mod.rs @@ -0,0 +1,4 @@ +pub mod commands; +pub mod error; +pub mod events; +pub mod statics; diff --git a/apps/oneclient/desktop/src/api/statics.rs b/apps/oneclient/desktop/src/api/statics.rs new file mode 100644 index 00000000..396e6064 --- /dev/null +++ b/apps/oneclient/desktop/src/api/statics.rs @@ -0,0 +1,26 @@ +use onelauncher_core::{constants::{NATIVE_ARCH, TARGET_OS}, store::Core}; +use serde::Serialize; +use specta::Type; + +#[derive(Serialize, Type)] +pub struct ProgramInfo { + launcher_version: String, + webview_version: String, + tauri_version: String, + dev_build: bool, + platform: String, + arch: String, +} + +#[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()), + tauri_version: tauri::VERSION.into(), + dev_build: tauri::is_dev(), + platform: TARGET_OS.into(), + arch: NATIVE_ARCH.into(), + } +} diff --git a/apps/oneclient/desktop/src/constants.rs b/apps/oneclient/desktop/src/constants.rs new file mode 100644 index 00000000..bcc70c6d --- /dev/null +++ b/apps/oneclient/desktop/src/constants.rs @@ -0,0 +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 diff --git a/apps/oneclient/desktop/src/error.rs b/apps/oneclient/desktop/src/error.rs new file mode 100644 index 00000000..e69de29b diff --git a/apps/oneclient/desktop/src/ext/mod.rs b/apps/oneclient/desktop/src/ext/mod.rs new file mode 100644 index 00000000..864c0b2a --- /dev/null +++ b/apps/oneclient/desktop/src/ext/mod.rs @@ -0,0 +1,2 @@ +pub mod specta; +pub mod updater; diff --git a/apps/oneclient/desktop/src/ext/specta.rs b/apps/oneclient/desktop/src/ext/specta.rs new file mode 100644 index 00000000..951373d5 --- /dev/null +++ b/apps/oneclient/desktop/src/ext/specta.rs @@ -0,0 +1,17 @@ +use std::io; +use std::path::Path; +use std::process::Command; + +pub fn formatter(file: &Path) -> io::Result<()> { + Command::new("pnpm") + .arg("eslint") + .arg("--flag") + .arg("unstable_ts_config") + .arg("--fix") + .arg(file) + .output() + .map(|_| ()) + .map_err(io::Error::other) +} + +const _: specta_typescript::FormatterFn = formatter; diff --git a/apps/oneclient/desktop/src/ext/updater.rs b/apps/oneclient/desktop/src/ext/updater.rs new file mode 100644 index 00000000..eafc3c98 --- /dev/null +++ b/apps/oneclient/desktop/src/ext/updater.rs @@ -0,0 +1,117 @@ +use onelauncher_core::store::Core; +use tauri::plugin::TauriPlugin; +use tauri::{Emitter, Runtime}; +use tauri_plugin_updater::{Update as TauriPluginUpdate, UpdaterExt}; +use tokio::sync::Mutex; + +#[derive(Debug, Clone, specta::Type, serde::Serialize)] +pub struct Update { + pub version: String, +} + +impl Update { + fn new(update: &TauriPluginUpdate) -> Self { + Self { + version: update.version.clone(), + } + } +} + +#[derive(Default)] +pub struct State { + install_lock: Mutex<()>, +} + +async fn get_update(app: tauri::AppHandle) -> Result, String> { + app.updater_builder() + .header("X-OneClient-Version", "stable") + .map_err(|e| e.to_string())? + .build() + .map_err(|e| e.to_string())? + .check() + .await + .map_err(|e| e.to_string()) +} + +#[derive(Debug, Clone, serde::Serialize, specta::Type, tauri_specta::Event)] +#[serde(rename_all = "camelCase", tag = "status")] +pub enum UpdateEvent { + Loading, + Error(String), + UpdateAvailable { update: Update }, + NoUpdateAvailable, + Installing, +} + +#[tauri::command] +#[specta::specta] +pub async fn check_for_update(app: tauri::AppHandle) -> Result, String> { + app.emit("updater", UpdateEvent::Loading).ok(); + + let update = match get_update(app.clone()).await { + Ok(update) => update, + Err(e) => { + app.emit("updater", UpdateEvent::Error(e.clone())).ok(); + return Err(e); + } + }; + + let update = update.map(|u| Update::new(&u)); + + app.emit( + "updater", + update.clone().map_or(UpdateEvent::NoUpdateAvailable, |u| { + UpdateEvent::UpdateAvailable { update: u } + }), + ) + .ok(); + + Ok(update) +} + +#[tauri::command] +#[specta::specta] +pub async fn install_update( + app: tauri::AppHandle, + state: tauri::State<'_, State>, +) -> Result<(), String> { + let Ok(lock) = state.install_lock.try_lock() else { + return Err("Update already installing".into()); + }; + + app.emit("updater", UpdateEvent::Installing).ok(); + + get_update(app.clone()) + .await? + .ok_or_else(|| "No update required".to_string())? + .download_and_install(|_, _| {}, || {}) + .await + .map_err(|e| e.to_string())?; + + drop(lock); + + Ok(()) +} + +#[must_use] +pub fn plugin() -> TauriPlugin { + tauri::plugin::Builder::new("onelauncher-updater") + .on_page_load(|window, _| { + #[cfg(target_os = "linux")] + let updater_available = false; + + #[cfg(not(target_os = "linux"))] + let updater_available = true; + + if updater_available { + window + .eval("window.__LAUNCHER_UPDATER__ = true;") + .expect("Failed to inject updater JS"); + } + }) + .js_init_script(format!( + r#"window.__ONECLIENT_VERSION__ = "{}";"#, + Core::get().launcher_version, + )) + .build() +} diff --git a/apps/oneclient/desktop/src/lib.rs b/apps/oneclient/desktop/src/lib.rs new file mode 100644 index 00000000..603bccd2 --- /dev/null +++ b/apps/oneclient/desktop/src/lib.rs @@ -0,0 +1,120 @@ +use onelauncher_core::api::proxy::ProxyTauri; +use onelauncher_core::error::LauncherResult; +use onelauncher_core::store::proxy::ProxyState; +use onelauncher_core::store::semaphore::SemaphoreStore; +use onelauncher_core::store::{Core, CoreOptions, Dirs, State}; +use tauri::{Emitter, Manager}; + +pub mod api; +pub mod constants; +pub mod ext; + +#[derive(Clone, serde::Serialize)] +pub struct SingleInstancePayload { + args: Vec, + cwd: String, +} + +#[tracing::instrument] +async fn initialize_core() -> LauncherResult<()> { + let opts = CoreOptions { + curseforge_api_key: Some(constants::CURSEFORGE_API_KEY.to_string()), + launcher_name: "OneClient".to_string(), + launcher_version: env!("CARGO_PKG_VERSION").to_string(), + launcher_website: "https://polyfrost.org/".to_string(), + discord_client_id: None, //Some(constants::DISCORD_CLIENT_ID.to_string()), // disabled for now + fetch_attempts: 3, + ..Default::default() + }; + + Core::initialize(opts).await?; + Dirs::get().await?; + onelauncher_core::start_logger().await; + SemaphoreStore::get().await; + tracing::info!("initialized core modules"); + + Ok(()) +} + +#[tracing::instrument(skip_all)] +async fn initialize_tauri(builder: tauri::Builder) -> LauncherResult { + let prebuild = tauri_specta::Builder::::new() + .commands(collect_commands!()) + .events(collect_events!()); + + #[cfg(debug_assertions)] + prebuild + .export( + specta_typescript::Typescript::default() + .bigint(specta_typescript::BigIntExportBehavior::BigInt) + .formatter(ext::specta::formatter), + "../frontend/src/bindings.gen.ts", + ) + .expect("failed to export debug bindings!"); + + let builder = builder + .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { + println!("{}, {argv:?}, {cwd}", app.package_info().name); + app.emit("single-instance", SingleInstancePayload { args: argv, cwd }) + .unwrap(); + })) + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_clipboard_manager::init()) + .plugin(ext::updater::plugin()) + .manage(ext::updater::State::default()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_deep_link::init()) + // .plugin(api::init()) + .menu(tauri::menu::Menu::new) + .invoke_handler(prebuild.invoke_handler()) + .setup(move |app| { + prebuild.mount_events(app.handle()); + setup_window(app.handle()).expect("failed to setup main window"); + Ok(()) + }); + + let app = builder + .build(tauri::generate_context!()) + .expect("failed to build tauri application"); + + Ok(app) +} + +#[tracing::instrument(skip_all)] +async fn initialize_state(handle: &tauri::AppHandle) -> LauncherResult<()> { + let proxy = ProxyTauri::new(handle.clone()); + ProxyState::initialize(proxy).await?; + + State::get().await?; + + tracing::info!("initialized launcher successfully"); + Ok(()) +} + +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"); + + app.run(|_, _| {}); +} + +fn setup_window(handle: &tauri::AppHandle) -> Result<(), Box> { + let win = handle + .get_webview_window("main") + .ok_or_else(|| anyhow::anyhow!("no window called main was found"))?; + + // tokio::task::spawn(async move { + // // let state = State::get().await.expect("failed to get state"); + // // let settings = state.settings.read().await; + // // win.set_decorations(settings.); + // }); + + win.show()?; + + Ok(()) +} diff --git a/apps/oneclient/desktop/src/main.rs b/apps/oneclient/desktop/src/main.rs new file mode 100644 index 00000000..f639d4ae --- /dev/null +++ b/apps/oneclient/desktop/src/main.rs @@ -0,0 +1,7 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +#[tokio::main] +async fn main() { + oneclient_gui::run().await; +} diff --git a/apps/oneclient/desktop/tauri.conf.json b/apps/oneclient/desktop/tauri.conf.json new file mode 100644 index 00000000..cffb98da --- /dev/null +++ b/apps/oneclient/desktop/tauri.conf.json @@ -0,0 +1,125 @@ +{ + "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json", + "productName": "OneClient", + "identifier": "org.polyfrost.oneclient", + "app": { + "withGlobalTauri": true, + "macOSPrivateApi": true, + "security": { + "csp": null, + "assetProtocol": { + "enable": true, + "scope": { + "allow": [ + "$DATA/OneClient/**", + "$RESOURCE/**" + ] + } + }, + "freezePrototype": true, + "capabilities": [] + }, + "windows": [ + { + "title": "OneClient", + "titleBarStyle": "Overlay", + "hiddenTitle": true, + "width": 1280, + "height": 802, + "minWidth": 1020, + "minHeight": 600, + "resizable": true, + "center": true, + "decorations": false, + "fullscreen": false, + "shadow": true, + "visible": false + } + ] + }, + "build": { + "beforeDevCommand": "pnpm --filter @oneclient/frontend run dev", + "beforeBuildCommand": "pnpm --filter @oneclient/frontend run build", + "frontendDist": "../frontend/dist", + "devUrl": "http://localhost:8001" + }, + "bundle": { + "active": true, + "targets": [ + "app", + "dmg", + "deb", + "rpm", + "appimage", + "nsis" + ], + "category": "Game", + "icon": [ + "../distribution/icons/32x32.png", + "../distribution/icons/128x128.png", + "../distribution/icons/128x128@2x.png", + "../distribution/icons/icon.icns", + "../distribution/icons/icon.ico" + ], + "copyright": "Polyfrost", + "publisher": "Polyfrost", + "license": "GPL-3.0-only", + "homepage": "https://polyfrost.org/", + "longDescription": "Next-generation open source Minecraft launcher", + "createUpdaterArtifacts": true, + "resources": [], + "externalBin": [], + "linux": { + "deb": { + "depends": [], + "desktopTemplate": "../distribution/linux/org.polyfrost.oneclient.desktop.template" + }, + "rpm": { + "desktopTemplate": "../distribution/linux/org.polyfrost.oneclient.desktop.template" + }, + "appimage": { + + } + }, + "shortDescription": "OneClient", + "macOS": { + "entitlements": "App.entitlements", + "exceptionDomain": null, + "frameworks": [], + "dmg": { + "appPosition": { + "x": 190, + "y": 190 + }, + "applicationFolderPosition": { + "x": 470, + "y": 190 + } + }, + "providerShortName": null, + "signingIdentity": null, + "minimumSystemVersion": "10.15" + }, + "windows": { + "certificateThumbprint": null, + "webviewInstallMode": { + "silent": true, + "type": "downloadBootstrapper" + }, + "digestAlgorithm": "sha256", + "timestampUrl": "https://timestamp.digicert.com" + } + }, + "plugins": { + "updater": { + "pubkey": "", + "endpoints": ["https://polyfrost.org/releases/oneclient/{{current_version}}/{{target}}/{{arch}}"] + }, + "deep-link": { + "mobile": [], + "desktop": { + "schemes": ["oneclient"] + } + } + } +} diff --git a/apps/oneclient/distribution/aur/cli/PKGBUILD b/apps/oneclient/distribution/aur/cli/PKGBUILD new file mode 100644 index 00000000..281b42e3 --- /dev/null +++ b/apps/oneclient/distribution/aur/cli/PKGBUILD @@ -0,0 +1,29 @@ +pkgname=onelauncher-bin +pkgver=0.1.0 +pkgrel=2 +pkgdesc='Next-generation open source Minecraft launcher' +url='https://polyfrost.org/launcher' +arch=('x86_64') +license=('GPL3') + +makedepends=('rust' 'pnpm') +depends=( + 'openssl-1.1' 'dbus' 'freetype2' 'gtk3' 'libappindicator-gtk3' 'librsvg' 'libsoup' 'webkit2gtk' + 'libgl' 'libpulse' 'libx11' 'libxcursor' 'libxext' 'libxxf86vm' +) +optdepends=( + 'xorg-xrandr: very legacy minecraft versions' +) +provides=('onelauncher') +conflicts=('onelauncher') +source=("https://launcher.polyfrost.org/versions/${pkgver}/linux/onelauncher_${pkgver}_amd64.deb") +sha256sums=('') + +build() { + cd "$srcdir/" + tar xf data.tar.gz +} + +package() { + cp -r "${srcdir}/usr/" "${pkgdir}" +} diff --git a/apps/oneclient/distribution/aur/gui/PKGBUILD b/apps/oneclient/distribution/aur/gui/PKGBUILD new file mode 100644 index 00000000..d1975f4e --- /dev/null +++ b/apps/oneclient/distribution/aur/gui/PKGBUILD @@ -0,0 +1,50 @@ +pkgname=onelauncher +pkgver=0.1.0 +pkgrel=2 +pkgdesc='Next-generation open source Minecraft launcher' +url='https://polyfrost.org/launcher' +arch=('x86_64') +license=('GPL3') + +makedepends=('rust' 'pnpm') +depends=( + 'openssl-1.1' 'dbus' 'freetype2' 'gtk3' 'libappindicator-gtk3' 'librsvg' 'libsoup' 'webkit2gtk' + 'libgl' 'libpulse' 'libx11' 'libxcursor' 'libxext' 'libxxf86vm' +) +optdepends=( + 'xorg-xrandr: very legacy minecraft versions' +) +conflicts=('onelauncher-bin') +source=( + "$pkgname-$pkgver.tar.gz:https://github.com/polyfrost/onelauncher/archive/refs/tags/v${pkgver}.tar.gz" + "onelauncher.desktop" +) +sha256sums=('') +options=('!lto') + +prepare() { + cd "onelauncher-${pkgver}" + export RUSTUP_TOOLCHAIN=stable + cargo fetch --locked --target "$CARCH-unknown-linux-gnu" + pnpm install --frozen-lockfile --no-optional +} + +build() { + cd "onelauncher-${pkgver}" + pnpm --filter @onelauncher/gui build + export RUSTUP_TOOLCHAIN=stable + cargo build --frozen --release --all-features +} + +check() { + cd "onelauncher-${pkgver}" + export RUSTUP_TOOLCHAIN=stable + cargo test --frozen --all-features +} + +package() { + install -Dm755 "$srcdir"/onelauncher-"$pkgver"/target/release/onelauncher_gui "$pkgdir"/usr/bin/onelauncher + install -Dm644 "$srcdir"/onelauncher-"$pkgver"/packages/distribution/icons/128x128.png "$pkgdir"/usr/share/icons/hicolor/128x128/apps/OneLauncher.png + install -Dm644 "$srcdir"/onelauncher-"$pkgver"/packages/distribution/icons/icon.png "$pkgdir"/usr/share/icons/hicolor/256x256@2/apps/OneLauncher.png + install -Dm644 OneLauncher.desktop "$pkgdir"/usr/share/applications/OneLauncher.desktop +} diff --git a/apps/oneclient/distribution/brew/onelauncher.rb b/apps/oneclient/distribution/brew/onelauncher.rb new file mode 100644 index 00000000..f0d995a2 --- /dev/null +++ b/apps/oneclient/distribution/brew/onelauncher.rb @@ -0,0 +1,28 @@ +cask "onelauncher" do + version "0.1.0" + sha256 "" + + url "https://launcher.polyfrost.org/versions/#{version}/macos/OneLauncher_#{version}_universal.dmg" + name "OneLauncher" + desc "Next-generation open source Minecraft launcher" + homepage "https://polyfrost.org" + + livecheck do + url "https://polyfrost.org/launcher" + regex("/OneLauncher[._-]v(\d+(?:\.\d+)+)[._-]universal\.dmg/i") + end + + auto_updated true + depends_on macos: ">= :high_sierra" + + app "OneLauncher.app" + + uninstall quit: "OneLauncher" + + zap trash: [ + "~/Library/Application Support/OneLauncher", + "~/Library/Caches/OneLauncher", + "~/Library/Saved Application State/OneLauncher.savedState", + "~/Library/WebKit/OneLauncher", + ] +end diff --git a/apps/oneclient/distribution/flatpak/org.polyfrost.launcher.desktop b/apps/oneclient/distribution/flatpak/org.polyfrost.launcher.desktop new file mode 100644 index 00000000..ce1428e7 --- /dev/null +++ b/apps/oneclient/distribution/flatpak/org.polyfrost.launcher.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=OneLauncher +Comment=Next-generation open source Minecraft launcher +Exec=onelauncher +Icon=org.polyfrost.launcher +Type=Application +Terminal=false +StartupNotify=true +Categories=Game; +Keywords=game;launcher;mc;minecraft; +StartupWMClass=OneLauncher +Version=1.4 diff --git a/apps/oneclient/distribution/flatpak/org.polyfrost.launcher.metainfo.xml b/apps/oneclient/distribution/flatpak/org.polyfrost.launcher.metainfo.xml new file mode 100644 index 00000000..0483218d --- /dev/null +++ b/apps/oneclient/distribution/flatpak/org.polyfrost.launcher.metainfo.xml @@ -0,0 +1,4 @@ + + + + diff --git a/apps/oneclient/distribution/flatpak/org.polyfrost.launcher.yml b/apps/oneclient/distribution/flatpak/org.polyfrost.launcher.yml new file mode 100644 index 00000000..e69de29b diff --git a/apps/oneclient/distribution/icons/128x128.png b/apps/oneclient/distribution/icons/128x128.png new file mode 100644 index 00000000..da7f6efb Binary files /dev/null and b/apps/oneclient/distribution/icons/128x128.png differ diff --git a/apps/oneclient/distribution/icons/128x128@2x.png b/apps/oneclient/distribution/icons/128x128@2x.png new file mode 100644 index 00000000..928b5db3 Binary files /dev/null and b/apps/oneclient/distribution/icons/128x128@2x.png differ diff --git a/apps/oneclient/distribution/icons/32x32.png b/apps/oneclient/distribution/icons/32x32.png new file mode 100644 index 00000000..8f359046 Binary files /dev/null and b/apps/oneclient/distribution/icons/32x32.png differ diff --git a/apps/oneclient/distribution/icons/512x512.png b/apps/oneclient/distribution/icons/512x512.png new file mode 100644 index 00000000..bac8bde3 Binary files /dev/null and b/apps/oneclient/distribution/icons/512x512.png differ diff --git a/apps/oneclient/distribution/icons/Square107x107Logo.png b/apps/oneclient/distribution/icons/Square107x107Logo.png new file mode 100644 index 00000000..1c59cf41 Binary files /dev/null and b/apps/oneclient/distribution/icons/Square107x107Logo.png differ diff --git a/apps/oneclient/distribution/icons/Square142x142Logo.png b/apps/oneclient/distribution/icons/Square142x142Logo.png new file mode 100644 index 00000000..baec0a83 Binary files /dev/null and b/apps/oneclient/distribution/icons/Square142x142Logo.png differ diff --git a/apps/oneclient/distribution/icons/Square150x150Logo.png b/apps/oneclient/distribution/icons/Square150x150Logo.png new file mode 100644 index 00000000..94bcd0ba Binary files /dev/null and b/apps/oneclient/distribution/icons/Square150x150Logo.png differ diff --git a/apps/oneclient/distribution/icons/Square284x284Logo.png b/apps/oneclient/distribution/icons/Square284x284Logo.png new file mode 100644 index 00000000..84e5d051 Binary files /dev/null and b/apps/oneclient/distribution/icons/Square284x284Logo.png differ diff --git a/apps/oneclient/distribution/icons/Square30x30Logo.png b/apps/oneclient/distribution/icons/Square30x30Logo.png new file mode 100644 index 00000000..9d48b6b7 Binary files /dev/null and b/apps/oneclient/distribution/icons/Square30x30Logo.png differ diff --git a/apps/oneclient/distribution/icons/Square310x310Logo.png b/apps/oneclient/distribution/icons/Square310x310Logo.png new file mode 100644 index 00000000..e0f40fc8 Binary files /dev/null and b/apps/oneclient/distribution/icons/Square310x310Logo.png differ diff --git a/apps/oneclient/distribution/icons/Square44x44Logo.png b/apps/oneclient/distribution/icons/Square44x44Logo.png new file mode 100644 index 00000000..7ee77886 Binary files /dev/null and b/apps/oneclient/distribution/icons/Square44x44Logo.png differ diff --git a/apps/oneclient/distribution/icons/Square71x71Logo.png b/apps/oneclient/distribution/icons/Square71x71Logo.png new file mode 100644 index 00000000..b3bf70e8 Binary files /dev/null and b/apps/oneclient/distribution/icons/Square71x71Logo.png differ diff --git a/apps/oneclient/distribution/icons/Square89x89Logo.png b/apps/oneclient/distribution/icons/Square89x89Logo.png new file mode 100644 index 00000000..0552a2c8 Binary files /dev/null and b/apps/oneclient/distribution/icons/Square89x89Logo.png differ diff --git a/apps/oneclient/distribution/icons/StoreLogo.png b/apps/oneclient/distribution/icons/StoreLogo.png new file mode 100644 index 00000000..b863a641 Binary files /dev/null and b/apps/oneclient/distribution/icons/StoreLogo.png differ diff --git a/apps/oneclient/distribution/icons/icon.icns b/apps/oneclient/distribution/icons/icon.icns new file mode 100644 index 00000000..74c6b99a Binary files /dev/null and b/apps/oneclient/distribution/icons/icon.icns differ diff --git a/apps/oneclient/distribution/icons/icon.ico b/apps/oneclient/distribution/icons/icon.ico new file mode 100644 index 00000000..bbad83f3 Binary files /dev/null and b/apps/oneclient/distribution/icons/icon.ico differ diff --git a/apps/oneclient/distribution/icons/icon.png b/apps/oneclient/distribution/icons/icon.png new file mode 100644 index 00000000..00b8274e Binary files /dev/null and b/apps/oneclient/distribution/icons/icon.png differ diff --git a/apps/oneclient/distribution/linux/org.polyfrost.launcher.desktop.template b/apps/oneclient/distribution/linux/org.polyfrost.launcher.desktop.template new file mode 100644 index 00000000..7ab2d6d3 --- /dev/null +++ b/apps/oneclient/distribution/linux/org.polyfrost.launcher.desktop.template @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=OneLauncher +Comment=Next-generation open source Minecraft launcher +Exec={{exec}} +Icon={{icon}} +Type=Application +Terminal=false +StartupNotify=true +Categories=Game;ActionGame;AdventureGame;Simulation; +Keywords=mc;minecraft;game +StartupWMClass={{exec}} +Version=1.4 diff --git a/apps/oneclient/distribution/scoop/onelauncher.json b/apps/oneclient/distribution/scoop/onelauncher.json new file mode 100644 index 00000000..73bddd53 --- /dev/null +++ b/apps/oneclient/distribution/scoop/onelauncher.json @@ -0,0 +1,28 @@ +{ + "version": "0.1.0", + "homepage": "https://polyfrost.org/launcher", + "description": "Next-generation open source Minecraft launcher", + "license": "GPL-3.0-only", + "architecture": { + "64bit": { + "url": "https://launcher.polyfrost.org/versions/0.1.0/windows/OneLauncher_0.1.0_x64_en-US.msi", + "hash": "hhjgskdfghskfdlafjdshjadlk" + } + }, + "shortcuts": [ + [ + "PFiles\\OneLauncher\\OneLauncher.exe", + "OneLauncher" + ] + ], + "checkver": { + "github": "https://github.com/polyfrost/onelauncher" + }, + "autoupdat": { + "architecture": { + "64bit": { + "url": "https://launcher.polyfrost.org/versions/$version/windows/OneLauncher_$version_x64_en-US.msi" + } + } + } +} diff --git a/apps/oneclient/distribution/winget/Polyfrost.OneClient.installer.yaml b/apps/oneclient/distribution/winget/Polyfrost.OneClient.installer.yaml new file mode 100644 index 00000000..cfcc69e1 --- /dev/null +++ b/apps/oneclient/distribution/winget/Polyfrost.OneClient.installer.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.5.0.schema.json + +PackageIdentifier: Polyfrost.OneLauncher +PackageVersion: 0.1.0 +InstallerLocale: en-US +InstallerType: wix +ProductCode: '{}' +Installers: + - Architecture: x64 + InstallerUrl: https://launcher.polyfrost.org/versions/0.1.0/windows/OneLauncher_0.1.0_x64_en-US.msi + InstallerSha256: '1111111111111111111111111111318274823726424624666666666666666666666666666666661111E1235666' +ManifestType: installer +ManifestVersion: 1.5.0 diff --git a/apps/oneclient/distribution/winget/Polyfrost.OneClient.locale.en-US.yaml b/apps/oneclient/distribution/winget/Polyfrost.OneClient.locale.en-US.yaml new file mode 100644 index 00000000..603f13ab --- /dev/null +++ b/apps/oneclient/distribution/winget/Polyfrost.OneClient.locale.en-US.yaml @@ -0,0 +1,11 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.5.0.schema.json + +PackageIdentifier: Polyfrost.OneLauncher +PackageVersion: 0.1.0 +PackageLocale: en-US +Publisher: Polyfrost +PackageName: OneLauncher +License: GPL-3.0 +ShortDescription: Next-generation open source Minecraft launcher +ManifestType: defaultLocale +ManifestVersion: 1.5.0 diff --git a/apps/oneclient/distribution/winget/Polyfrost.OneClient.yaml b/apps/oneclient/distribution/winget/Polyfrost.OneClient.yaml new file mode 100644 index 00000000..09889a9c --- /dev/null +++ b/apps/oneclient/distribution/winget/Polyfrost.OneClient.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.5.0.schema.json + +PackageIdentifier: Polyfrost.OneLauncher +PackageVersion: 0.1.0 +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.5.0 diff --git a/apps/oneclient/frontend/package.json b/apps/oneclient/frontend/package.json index a9aad725..f6b6548d 100644 --- a/apps/oneclient/frontend/package.json +++ b/apps/oneclient/frontend/package.json @@ -3,8 +3,8 @@ "type": "module", "private": true, "scripts": { - "dev": "vite --port 3000", - "start": "vite --port 3000", + "dev": "vite", + "start": "vite", "build": "vite build && tsc", "serve": "vite preview", "lint": "eslint", @@ -12,6 +12,8 @@ "check": "prettier --write . && eslint --fix" }, "dependencies": { + "@fontsource/poppins": "^5.2.6", + "@formatjs/intl-durationformat": "^0.7.4", "@tailwindcss/vite": "^4.0.6", "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-query": "^5.66.5", @@ -20,16 +22,28 @@ "@tanstack/react-router-devtools": "^1.114.3", "@tanstack/react-table": "^8.21.2", "@tanstack/router-plugin": "^1.114.3", + "@tauri-apps/api": "^2.5.0", + "@tauri-apps/plugin-clipboard-manager": "^2.2.2", + "@tauri-apps/plugin-dialog": "^2.2.1", + "@tauri-apps/plugin-shell": "^2.2.1", + "@untitled-theme/icons-react": "^0.14.0", + "motion": "^12.10.5", + "overlayscrollbars": "^2.11.2", + "overlayscrollbars-react": "^0.5.6", "react": "^19.0.0", + "react-aria-components": "^1.8.0", "react-dom": "^19.0.0", - "tailwindcss": "^4.0.6" + "tailwind-merge": "^3.2.0", + "tailwind-variants": "^1.0.0", + "tailwindcss": "^4.0.6", + "tailwindcss-animated": "^2.0.0", + "tailwindcss-react-aria-components": "^2.0.0" }, "devDependencies": { "@tanstack/eslint-config": "^0.1.0", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.4", - "prettier": "^3.5.3", "typescript": "^5.7.2", "vite": "^6.1.0" } diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts new file mode 100644 index 00000000..c408de2e --- /dev/null +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -0,0 +1,233 @@ +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands */ + +/** tauri-specta globals */ + +import type { WebviewWindow as __WebviewWindow__ } from '@tauri-apps/api/webviewWindow'; +import { + invoke as TAURI_INVOKE, +} from '@tauri-apps/api/core'; +import * as TAURI_API_EVENT from '@tauri-apps/api/event'; + +export const commands = { + async getUsers(): Promise, SerializableError>> { + try { + return { status: 'ok', data: await TAURI_INVOKE('get_users') }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async getUser(uuid: string): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('get_user', { uuid }) }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async removeUser(uuid: string): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('remove_user', { uuid }) }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async getDefaultUser(fallback: boolean | null): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('get_default_user', { fallback }) }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async setDefaultUser(uuid: string | null): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('set_default_user', { uuid }) }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async beginMsFlow(): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('begin_ms_flow') }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async createCluster(options: CreateCluster): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('create_cluster', { options }) }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async getClusters(): Promise, SerializableError>> { + try { + return { status: 'ok', data: await TAURI_INVOKE('get_clusters') }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async getClusterById(id: bigint): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('get_cluster_by_id', { id }) }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async getGlobalProfile(): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('get_global_profile') }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async getProfileOrDefault(name: string | null): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('get_profile_or_default', { name }) }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async openDevTools(): Promise { + await TAURI_INVOKE('open_dev_tools'); + }, +}; + +/** user-defined events */ + +export const events = __makeEvents__<{ + launcherEvent: LauncherEvent; +}>({ + launcherEvent: 'launcher-event', +}); + +/** user-defined constants */ + +/** user-defined types */ + +export type ClusterStage = 'notready' | 'downloading' | 'repairing' | 'ready'; +export interface CreateCluster { name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; icon_url: Icon | null } +export type GameLoader = 'vanilla' | 'forge' | 'neoforge' | 'quilt' | 'fabric' | 'legacyfabric'; +export type Icon = string; +export interface IngressPayload { id: string; message: string; ingress_type: IngressType; percent: number | null; total: number } +export type IngressType = { Download: { file_name: string } } | 'JavaPrepare' | 'JavaCheck' | 'JavaLocate' | 'MinecraftDownload' | { PrepareCluster: { cluster_name: string } }; +export type LauncherEvent = { Ingress: IngressPayload } | { Message: MessagePayload } | { Process: ProcessPayload }; +export type MessageLevel = 'Info' | 'Warn' | 'Error'; +export interface MessagePayload { level: MessageLevel; message: string } +/** + * A structure of all needed Minecraft credentials for logging in and account management. + */ +export interface 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; + +} +export interface Model { 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 interface Process { pid: number; started_at: string; cluster_id: bigint; post_hook: string | null; account_id: string } +export type ProcessPayload = { Starting: { command: string } } | { Started: { process: Process } } | { Stopped: { pid: number; exit_code: number } } | { Output: { pid: number; output: string } }; +export interface Resolution { width: number; height: number } +export interface SerializableError { kind: 'CoreError' } +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 interface SettingsOsExtra { enable_gamemode: boolean | null } + +interface __EventObj__ { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +} + +export type Result = + | { status: 'ok'; data: T } + | { status: 'error'; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case 'listen': + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case 'once': + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case 'emit': + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/apps/oneclient/frontend/src/routeTree.gen.ts b/apps/oneclient/frontend/src/routeTree.gen.ts new file mode 100644 index 00000000..5e682936 --- /dev/null +++ b/apps/oneclient/frontend/src/routeTree.gen.ts @@ -0,0 +1,88 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +// Import Routes + +import { Route as rootRoute } from './routes/__root' +import { Route as IndexImport } from './routes/index' + +// Create/Update Routes + +const IndexRoute = IndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRoute, +} as any) + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + } +} + +// Create and export the route tree + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute +} + +export interface FileRoutesByTo { + '/': typeof IndexRoute +} + +export interface FileRoutesById { + __root__: typeof rootRoute + '/': typeof IndexRoute +} + +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' + fileRoutesByTo: FileRoutesByTo + to: '/' + id: '__root__' | '/' + fileRoutesById: FileRoutesById +} + +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, +} + +export const routeTree = rootRoute + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +/* ROUTE_MANIFEST_START +{ + "routes": { + "__root__": { + "filePath": "__root.tsx", + "children": [ + "/" + ] + }, + "/": { + "filePath": "index.tsx" + } + } +} +ROUTE_MANIFEST_END */ diff --git a/apps/oneclient/frontend/vite.config.js b/apps/oneclient/frontend/vite.config.js index 393040ff..5fbb4ac3 100644 --- a/apps/oneclient/frontend/vite.config.js +++ b/apps/oneclient/frontend/vite.config.js @@ -1,20 +1,32 @@ -import { defineConfig } from "vite"; -import viteReact from "@vitejs/plugin-react"; -import tailwindcss from "@tailwindcss/vite"; +import { resolve } from 'node:path'; +import process from 'node:process'; +import tailwindcss from '@tailwindcss/vite'; -import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; -import { resolve } from "node:path"; +import { TanStackRouterVite } from '@tanstack/router-plugin/vite'; +import viteReact from '@vitejs/plugin-react'; + +import { defineConfig } from 'vite'; -// https://vitejs.dev/config/ export default defineConfig({ - plugins: [TanStackRouterVite({ autoCodeSplitting: true }), viteReact(), tailwindcss()], - test: { - globals: true, - environment: "jsdom", - }, - resolve: { - alias: { - '@': resolve(__dirname, './src'), - }, - } + plugins: [ + TanStackRouterVite({ autoCodeSplitting: true }), + viteReact(), + tailwindcss(), + ], + clearScreen: false, + server: { + port: 8001, + strictPort: true, + }, + resolve: { + alias: { + '@': resolve(__dirname, './src'), + }, + }, + envPrefix: ['VITE_', 'TAURI_ENV_'], + build: { + target: process.env.TAURI_ENV_PLATFORM === 'windows' ? 'chrome105' : 'safari13', + minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false, + sourcemap: !!process.env.TAURI_ENV_DEBUG, + }, }); diff --git a/apps/onelauncher/frontend/src/bindings.gen.ts b/apps/onelauncher/frontend/src/bindings.gen.ts index 465176fd..c408de2e 100644 --- a/apps/onelauncher/frontend/src/bindings.gen.ts +++ b/apps/onelauncher/frontend/src/bindings.gen.ts @@ -71,6 +71,56 @@ export const commands = { else return { status: 'error', error: e as any }; } }, + async createCluster(options: CreateCluster): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('create_cluster', { options }) }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async getClusters(): Promise, SerializableError>> { + try { + return { status: 'ok', data: await TAURI_INVOKE('get_clusters') }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async getClusterById(id: bigint): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('get_cluster_by_id', { id }) }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async getGlobalProfile(): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('get_global_profile') }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, + async getProfileOrDefault(name: string | null): Promise> { + try { + return { status: 'ok', data: await TAURI_INVOKE('get_profile_or_default', { name }) }; + } + catch (e) { + if (e instanceof Error) + throw e; + else return { status: 'error', error: e as any }; + } + }, async openDevTools(): Promise { await TAURI_INVOKE('open_dev_tools'); }, @@ -88,6 +138,10 @@ export const events = __makeEvents__<{ /** user-defined types */ +export type ClusterStage = 'notready' | 'downloading' | 'repairing' | 'ready'; +export interface CreateCluster { name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; icon_url: Icon | null } +export type GameLoader = 'vanilla' | 'forge' | 'neoforge' | 'quilt' | 'fabric' | 'legacyfabric'; +export type Icon = string; export interface IngressPayload { id: string; message: string; ingress_type: IngressType; percent: number | null; total: number } export type IngressType = { Download: { file_name: string } } | 'JavaPrepare' | 'JavaCheck' | 'JavaLocate' | 'MinecraftDownload' | { PrepareCluster: { cluster_name: string } }; export type LauncherEvent = { Ingress: IngressPayload } | { Message: MessagePayload } | { Process: ProcessPayload }; @@ -119,9 +173,13 @@ export interface MinecraftCredentials { expires: string; } +export interface Model { 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 interface Process { pid: number; started_at: string; cluster_id: bigint; post_hook: string | null; account_id: string } export type ProcessPayload = { Starting: { command: string } } | { Started: { process: Process } } | { Stopped: { pid: number; exit_code: number } } | { Output: { pid: number; output: string } }; +export interface Resolution { width: number; height: number } export interface SerializableError { kind: 'CoreError' } +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 interface SettingsOsExtra { enable_gamemode: boolean | null } interface __EventObj__ { listen: ( diff --git a/apps/onelauncher/frontend/src/routeTree.gen.ts b/apps/onelauncher/frontend/src/routeTree.gen.ts index b105d8d3..1ba59c25 100644 --- a/apps/onelauncher/frontend/src/routeTree.gen.ts +++ b/apps/onelauncher/frontend/src/routeTree.gen.ts @@ -11,13 +11,36 @@ // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as OnboardingRouteImport } from './routes/onboarding/route' import { Route as AppRouteImport } from './routes/app/route' import { Route as IndexImport } from './routes/index' +import { Route as OnboardingIndexImport } from './routes/onboarding/index' import { Route as AppIndexImport } from './routes/app/index' +import { Route as OnboardingSummaryImport } from './routes/onboarding/summary' +import { Route as OnboardingLoginImport } from './routes/onboarding/login' +import { Route as OnboardingLanguageImport } from './routes/onboarding/language' +import { Route as OnboardingImportImport } from './routes/onboarding/import' +import { Route as OnboardingCompleteImport } from './routes/onboarding/complete' +import { Route as AppSettingsRouteImport } from './routes/app/settings/route' +import { Route as AppClusterRouteImport } from './routes/app/cluster/route' +import { Route as AppSettingsIndexImport } from './routes/app/settings/index' +import { Route as AppClusterIndexImport } from './routes/app/cluster/index' import { Route as AppBrowserIndexImport } from './routes/app/browser/index' +import { Route as AppSettingsMinecraftImport } from './routes/app/settings/minecraft' +import { Route as AppSettingsAppearanceImport } from './routes/app/settings/appearance' +import { Route as AppSettingsAccountsImport } from './routes/app/settings/accounts' +import { Route as AppClusterWorldsImport } from './routes/app/cluster/worlds' +import { Route as AppClusterSettingsImport } from './routes/app/cluster/settings' +import { Route as AppClusterScreenshotsImport } from './routes/app/cluster/screenshots' // Create/Update Routes +const OnboardingRouteRoute = OnboardingRouteImport.update({ + id: '/onboarding', + path: '/onboarding', + getParentRoute: () => rootRoute, +} as any) + const AppRouteRoute = AppRouteImport.update({ id: '/app', path: '/app', @@ -30,18 +53,114 @@ const IndexRoute = IndexImport.update({ getParentRoute: () => rootRoute, } as any) +const OnboardingIndexRoute = OnboardingIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => OnboardingRouteRoute, +} as any) + const AppIndexRoute = AppIndexImport.update({ id: '/', path: '/', getParentRoute: () => AppRouteRoute, } as any) +const OnboardingSummaryRoute = OnboardingSummaryImport.update({ + id: '/summary', + path: '/summary', + getParentRoute: () => OnboardingRouteRoute, +} as any) + +const OnboardingLoginRoute = OnboardingLoginImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => OnboardingRouteRoute, +} as any) + +const OnboardingLanguageRoute = OnboardingLanguageImport.update({ + id: '/language', + path: '/language', + getParentRoute: () => OnboardingRouteRoute, +} as any) + +const OnboardingImportRoute = OnboardingImportImport.update({ + id: '/import', + path: '/import', + getParentRoute: () => OnboardingRouteRoute, +} as any) + +const OnboardingCompleteRoute = OnboardingCompleteImport.update({ + id: '/complete', + path: '/complete', + getParentRoute: () => OnboardingRouteRoute, +} as any) + +const AppSettingsRouteRoute = AppSettingsRouteImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => AppRouteRoute, +} as any) + +const AppClusterRouteRoute = AppClusterRouteImport.update({ + id: '/cluster', + path: '/cluster', + getParentRoute: () => AppRouteRoute, +} as any) + +const AppSettingsIndexRoute = AppSettingsIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => AppSettingsRouteRoute, +} as any) + +const AppClusterIndexRoute = AppClusterIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => AppClusterRouteRoute, +} as any) + const AppBrowserIndexRoute = AppBrowserIndexImport.update({ id: '/browser/', path: '/browser/', getParentRoute: () => AppRouteRoute, } as any) +const AppSettingsMinecraftRoute = AppSettingsMinecraftImport.update({ + id: '/minecraft', + path: '/minecraft', + getParentRoute: () => AppSettingsRouteRoute, +} as any) + +const AppSettingsAppearanceRoute = AppSettingsAppearanceImport.update({ + id: '/appearance', + path: '/appearance', + getParentRoute: () => AppSettingsRouteRoute, +} as any) + +const AppSettingsAccountsRoute = AppSettingsAccountsImport.update({ + id: '/accounts', + path: '/accounts', + getParentRoute: () => AppSettingsRouteRoute, +} as any) + +const AppClusterWorldsRoute = AppClusterWorldsImport.update({ + id: '/worlds', + path: '/worlds', + getParentRoute: () => AppClusterRouteRoute, +} as any) + +const AppClusterSettingsRoute = AppClusterSettingsImport.update({ + id: '/settings', + path: '/settings', + getParentRoute: () => AppClusterRouteRoute, +} as any) + +const AppClusterScreenshotsRoute = AppClusterScreenshotsImport.update({ + id: '/screenshots', + path: '/screenshots', + getParentRoute: () => AppClusterRouteRoute, +} as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -60,6 +179,62 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AppRouteImport parentRoute: typeof rootRoute } + '/onboarding': { + id: '/onboarding' + path: '/onboarding' + fullPath: '/onboarding' + preLoaderRoute: typeof OnboardingRouteImport + parentRoute: typeof rootRoute + } + '/app/cluster': { + id: '/app/cluster' + path: '/cluster' + fullPath: '/app/cluster' + preLoaderRoute: typeof AppClusterRouteImport + parentRoute: typeof AppRouteImport + } + '/app/settings': { + id: '/app/settings' + path: '/settings' + fullPath: '/app/settings' + preLoaderRoute: typeof AppSettingsRouteImport + parentRoute: typeof AppRouteImport + } + '/onboarding/complete': { + id: '/onboarding/complete' + path: '/complete' + fullPath: '/onboarding/complete' + preLoaderRoute: typeof OnboardingCompleteImport + parentRoute: typeof OnboardingRouteImport + } + '/onboarding/import': { + id: '/onboarding/import' + path: '/import' + fullPath: '/onboarding/import' + preLoaderRoute: typeof OnboardingImportImport + parentRoute: typeof OnboardingRouteImport + } + '/onboarding/language': { + id: '/onboarding/language' + path: '/language' + fullPath: '/onboarding/language' + preLoaderRoute: typeof OnboardingLanguageImport + parentRoute: typeof OnboardingRouteImport + } + '/onboarding/login': { + id: '/onboarding/login' + path: '/login' + fullPath: '/onboarding/login' + preLoaderRoute: typeof OnboardingLoginImport + parentRoute: typeof OnboardingRouteImport + } + '/onboarding/summary': { + id: '/onboarding/summary' + path: '/summary' + fullPath: '/onboarding/summary' + preLoaderRoute: typeof OnboardingSummaryImport + parentRoute: typeof OnboardingRouteImport + } '/app/': { id: '/app/' path: '/' @@ -67,6 +242,55 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AppIndexImport parentRoute: typeof AppRouteImport } + '/onboarding/': { + id: '/onboarding/' + path: '/' + fullPath: '/onboarding/' + preLoaderRoute: typeof OnboardingIndexImport + parentRoute: typeof OnboardingRouteImport + } + '/app/cluster/screenshots': { + id: '/app/cluster/screenshots' + path: '/screenshots' + fullPath: '/app/cluster/screenshots' + preLoaderRoute: typeof AppClusterScreenshotsImport + parentRoute: typeof AppClusterRouteImport + } + '/app/cluster/settings': { + id: '/app/cluster/settings' + path: '/settings' + fullPath: '/app/cluster/settings' + preLoaderRoute: typeof AppClusterSettingsImport + parentRoute: typeof AppClusterRouteImport + } + '/app/cluster/worlds': { + id: '/app/cluster/worlds' + path: '/worlds' + fullPath: '/app/cluster/worlds' + preLoaderRoute: typeof AppClusterWorldsImport + parentRoute: typeof AppClusterRouteImport + } + '/app/settings/accounts': { + id: '/app/settings/accounts' + path: '/accounts' + fullPath: '/app/settings/accounts' + preLoaderRoute: typeof AppSettingsAccountsImport + parentRoute: typeof AppSettingsRouteImport + } + '/app/settings/appearance': { + id: '/app/settings/appearance' + path: '/appearance' + fullPath: '/app/settings/appearance' + preLoaderRoute: typeof AppSettingsAppearanceImport + parentRoute: typeof AppSettingsRouteImport + } + '/app/settings/minecraft': { + id: '/app/settings/minecraft' + path: '/minecraft' + fullPath: '/app/settings/minecraft' + preLoaderRoute: typeof AppSettingsMinecraftImport + parentRoute: typeof AppSettingsRouteImport + } '/app/browser/': { id: '/app/browser/' path: '/browser' @@ -74,17 +298,70 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AppBrowserIndexImport parentRoute: typeof AppRouteImport } + '/app/cluster/': { + id: '/app/cluster/' + path: '/' + fullPath: '/app/cluster/' + preLoaderRoute: typeof AppClusterIndexImport + parentRoute: typeof AppClusterRouteImport + } + '/app/settings/': { + id: '/app/settings/' + path: '/' + fullPath: '/app/settings/' + preLoaderRoute: typeof AppSettingsIndexImport + parentRoute: typeof AppSettingsRouteImport + } } } // Create and export the route tree +interface AppClusterRouteRouteChildren { + AppClusterScreenshotsRoute: typeof AppClusterScreenshotsRoute + AppClusterSettingsRoute: typeof AppClusterSettingsRoute + AppClusterWorldsRoute: typeof AppClusterWorldsRoute + AppClusterIndexRoute: typeof AppClusterIndexRoute +} + +const AppClusterRouteRouteChildren: AppClusterRouteRouteChildren = { + AppClusterScreenshotsRoute: AppClusterScreenshotsRoute, + AppClusterSettingsRoute: AppClusterSettingsRoute, + AppClusterWorldsRoute: AppClusterWorldsRoute, + AppClusterIndexRoute: AppClusterIndexRoute, +} + +const AppClusterRouteRouteWithChildren = AppClusterRouteRoute._addFileChildren( + AppClusterRouteRouteChildren, +) + +interface AppSettingsRouteRouteChildren { + AppSettingsAccountsRoute: typeof AppSettingsAccountsRoute + AppSettingsAppearanceRoute: typeof AppSettingsAppearanceRoute + AppSettingsMinecraftRoute: typeof AppSettingsMinecraftRoute + AppSettingsIndexRoute: typeof AppSettingsIndexRoute +} + +const AppSettingsRouteRouteChildren: AppSettingsRouteRouteChildren = { + AppSettingsAccountsRoute: AppSettingsAccountsRoute, + AppSettingsAppearanceRoute: AppSettingsAppearanceRoute, + AppSettingsMinecraftRoute: AppSettingsMinecraftRoute, + AppSettingsIndexRoute: AppSettingsIndexRoute, +} + +const AppSettingsRouteRouteWithChildren = + AppSettingsRouteRoute._addFileChildren(AppSettingsRouteRouteChildren) + interface AppRouteRouteChildren { + AppClusterRouteRoute: typeof AppClusterRouteRouteWithChildren + AppSettingsRouteRoute: typeof AppSettingsRouteRouteWithChildren AppIndexRoute: typeof AppIndexRoute AppBrowserIndexRoute: typeof AppBrowserIndexRoute } const AppRouteRouteChildren: AppRouteRouteChildren = { + AppClusterRouteRoute: AppClusterRouteRouteWithChildren, + AppSettingsRouteRoute: AppSettingsRouteRouteWithChildren, AppIndexRoute: AppIndexRoute, AppBrowserIndexRoute: AppBrowserIndexRoute, } @@ -93,44 +370,176 @@ const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren( AppRouteRouteChildren, ) +interface OnboardingRouteRouteChildren { + OnboardingCompleteRoute: typeof OnboardingCompleteRoute + OnboardingImportRoute: typeof OnboardingImportRoute + OnboardingLanguageRoute: typeof OnboardingLanguageRoute + OnboardingLoginRoute: typeof OnboardingLoginRoute + OnboardingSummaryRoute: typeof OnboardingSummaryRoute + OnboardingIndexRoute: typeof OnboardingIndexRoute +} + +const OnboardingRouteRouteChildren: OnboardingRouteRouteChildren = { + OnboardingCompleteRoute: OnboardingCompleteRoute, + OnboardingImportRoute: OnboardingImportRoute, + OnboardingLanguageRoute: OnboardingLanguageRoute, + OnboardingLoginRoute: OnboardingLoginRoute, + OnboardingSummaryRoute: OnboardingSummaryRoute, + OnboardingIndexRoute: OnboardingIndexRoute, +} + +const OnboardingRouteRouteWithChildren = OnboardingRouteRoute._addFileChildren( + OnboardingRouteRouteChildren, +) + export interface FileRoutesByFullPath { '/': typeof IndexRoute '/app': typeof AppRouteRouteWithChildren + '/onboarding': typeof OnboardingRouteRouteWithChildren + '/app/cluster': typeof AppClusterRouteRouteWithChildren + '/app/settings': typeof AppSettingsRouteRouteWithChildren + '/onboarding/complete': typeof OnboardingCompleteRoute + '/onboarding/import': typeof OnboardingImportRoute + '/onboarding/language': typeof OnboardingLanguageRoute + '/onboarding/login': typeof OnboardingLoginRoute + '/onboarding/summary': typeof OnboardingSummaryRoute '/app/': typeof AppIndexRoute + '/onboarding/': typeof OnboardingIndexRoute + '/app/cluster/screenshots': typeof AppClusterScreenshotsRoute + '/app/cluster/settings': typeof AppClusterSettingsRoute + '/app/cluster/worlds': typeof AppClusterWorldsRoute + '/app/settings/accounts': typeof AppSettingsAccountsRoute + '/app/settings/appearance': typeof AppSettingsAppearanceRoute + '/app/settings/minecraft': typeof AppSettingsMinecraftRoute '/app/browser': typeof AppBrowserIndexRoute + '/app/cluster/': typeof AppClusterIndexRoute + '/app/settings/': typeof AppSettingsIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/onboarding/complete': typeof OnboardingCompleteRoute + '/onboarding/import': typeof OnboardingImportRoute + '/onboarding/language': typeof OnboardingLanguageRoute + '/onboarding/login': typeof OnboardingLoginRoute + '/onboarding/summary': typeof OnboardingSummaryRoute '/app': typeof AppIndexRoute + '/onboarding': typeof OnboardingIndexRoute + '/app/cluster/screenshots': typeof AppClusterScreenshotsRoute + '/app/cluster/settings': typeof AppClusterSettingsRoute + '/app/cluster/worlds': typeof AppClusterWorldsRoute + '/app/settings/accounts': typeof AppSettingsAccountsRoute + '/app/settings/appearance': typeof AppSettingsAppearanceRoute + '/app/settings/minecraft': typeof AppSettingsMinecraftRoute '/app/browser': typeof AppBrowserIndexRoute + '/app/cluster': typeof AppClusterIndexRoute + '/app/settings': typeof AppSettingsIndexRoute } export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute '/app': typeof AppRouteRouteWithChildren + '/onboarding': typeof OnboardingRouteRouteWithChildren + '/app/cluster': typeof AppClusterRouteRouteWithChildren + '/app/settings': typeof AppSettingsRouteRouteWithChildren + '/onboarding/complete': typeof OnboardingCompleteRoute + '/onboarding/import': typeof OnboardingImportRoute + '/onboarding/language': typeof OnboardingLanguageRoute + '/onboarding/login': typeof OnboardingLoginRoute + '/onboarding/summary': typeof OnboardingSummaryRoute '/app/': typeof AppIndexRoute + '/onboarding/': typeof OnboardingIndexRoute + '/app/cluster/screenshots': typeof AppClusterScreenshotsRoute + '/app/cluster/settings': typeof AppClusterSettingsRoute + '/app/cluster/worlds': typeof AppClusterWorldsRoute + '/app/settings/accounts': typeof AppSettingsAccountsRoute + '/app/settings/appearance': typeof AppSettingsAppearanceRoute + '/app/settings/minecraft': typeof AppSettingsMinecraftRoute '/app/browser/': typeof AppBrowserIndexRoute + '/app/cluster/': typeof AppClusterIndexRoute + '/app/settings/': typeof AppSettingsIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/app' | '/app/' | '/app/browser' + fullPaths: + | '/' + | '/app' + | '/onboarding' + | '/app/cluster' + | '/app/settings' + | '/onboarding/complete' + | '/onboarding/import' + | '/onboarding/language' + | '/onboarding/login' + | '/onboarding/summary' + | '/app/' + | '/onboarding/' + | '/app/cluster/screenshots' + | '/app/cluster/settings' + | '/app/cluster/worlds' + | '/app/settings/accounts' + | '/app/settings/appearance' + | '/app/settings/minecraft' + | '/app/browser' + | '/app/cluster/' + | '/app/settings/' fileRoutesByTo: FileRoutesByTo - to: '/' | '/app' | '/app/browser' - id: '__root__' | '/' | '/app' | '/app/' | '/app/browser/' + to: + | '/' + | '/onboarding/complete' + | '/onboarding/import' + | '/onboarding/language' + | '/onboarding/login' + | '/onboarding/summary' + | '/app' + | '/onboarding' + | '/app/cluster/screenshots' + | '/app/cluster/settings' + | '/app/cluster/worlds' + | '/app/settings/accounts' + | '/app/settings/appearance' + | '/app/settings/minecraft' + | '/app/browser' + | '/app/cluster' + | '/app/settings' + id: + | '__root__' + | '/' + | '/app' + | '/onboarding' + | '/app/cluster' + | '/app/settings' + | '/onboarding/complete' + | '/onboarding/import' + | '/onboarding/language' + | '/onboarding/login' + | '/onboarding/summary' + | '/app/' + | '/onboarding/' + | '/app/cluster/screenshots' + | '/app/cluster/settings' + | '/app/cluster/worlds' + | '/app/settings/accounts' + | '/app/settings/appearance' + | '/app/settings/minecraft' + | '/app/browser/' + | '/app/cluster/' + | '/app/settings/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute AppRouteRoute: typeof AppRouteRouteWithChildren + OnboardingRouteRoute: typeof OnboardingRouteRouteWithChildren } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AppRouteRoute: AppRouteRouteWithChildren, + OnboardingRouteRoute: OnboardingRouteRouteWithChildren, } export const routeTree = rootRoute @@ -144,7 +553,8 @@ export const routeTree = rootRoute "filePath": "__root.tsx", "children": [ "/", - "/app" + "/app", + "/onboarding" ] }, "/": { @@ -153,17 +563,106 @@ export const routeTree = rootRoute "/app": { "filePath": "app/route.tsx", "children": [ + "/app/cluster", + "/app/settings", "/app/", "/app/browser/" ] }, + "/onboarding": { + "filePath": "onboarding/route.tsx", + "children": [ + "/onboarding/complete", + "/onboarding/import", + "/onboarding/language", + "/onboarding/login", + "/onboarding/summary", + "/onboarding/" + ] + }, + "/app/cluster": { + "filePath": "app/cluster/route.tsx", + "parent": "/app", + "children": [ + "/app/cluster/screenshots", + "/app/cluster/settings", + "/app/cluster/worlds", + "/app/cluster/" + ] + }, + "/app/settings": { + "filePath": "app/settings/route.tsx", + "parent": "/app", + "children": [ + "/app/settings/accounts", + "/app/settings/appearance", + "/app/settings/minecraft", + "/app/settings/" + ] + }, + "/onboarding/complete": { + "filePath": "onboarding/complete.tsx", + "parent": "/onboarding" + }, + "/onboarding/import": { + "filePath": "onboarding/import.tsx", + "parent": "/onboarding" + }, + "/onboarding/language": { + "filePath": "onboarding/language.tsx", + "parent": "/onboarding" + }, + "/onboarding/login": { + "filePath": "onboarding/login.tsx", + "parent": "/onboarding" + }, + "/onboarding/summary": { + "filePath": "onboarding/summary.tsx", + "parent": "/onboarding" + }, "/app/": { "filePath": "app/index.tsx", "parent": "/app" }, + "/onboarding/": { + "filePath": "onboarding/index.tsx", + "parent": "/onboarding" + }, + "/app/cluster/screenshots": { + "filePath": "app/cluster/screenshots.tsx", + "parent": "/app/cluster" + }, + "/app/cluster/settings": { + "filePath": "app/cluster/settings.tsx", + "parent": "/app/cluster" + }, + "/app/cluster/worlds": { + "filePath": "app/cluster/worlds.tsx", + "parent": "/app/cluster" + }, + "/app/settings/accounts": { + "filePath": "app/settings/accounts.tsx", + "parent": "/app/settings" + }, + "/app/settings/appearance": { + "filePath": "app/settings/appearance.tsx", + "parent": "/app/settings" + }, + "/app/settings/minecraft": { + "filePath": "app/settings/minecraft.tsx", + "parent": "/app/settings" + }, "/app/browser/": { "filePath": "app/browser/index.tsx", "parent": "/app" + }, + "/app/cluster/": { + "filePath": "app/cluster/index.tsx", + "parent": "/app/cluster" + }, + "/app/settings/": { + "filePath": "app/settings/index.tsx", + "parent": "/app/settings" } } } diff --git a/packages/core/src/store/db.rs b/packages/core/src/store/db.rs index ab96e24e..3b5560a2 100644 --- a/packages/core/src/store/db.rs +++ b/packages/core/src/store/db.rs @@ -3,7 +3,7 @@ use std::{path::PathBuf, time::Duration}; use onelauncher_migration::MigratorTrait; use sea_orm::{ConnectOptions, Database, DatabaseConnection}; -use crate::{utils::io::IOError, LauncherResult}; +use crate::{utils::io::{self, IOError}, LauncherResult}; use super::Dirs; diff --git a/packages/core/src/store/state.rs b/packages/core/src/store/state.rs index 9467f622..c3920114 100644 --- a/packages/core/src/store/state.rs +++ b/packages/core/src/store/state.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use sea_orm::DatabaseConnection; use tokio::sync::{OnceCell, RwLock}; +use crate::store::Dirs; use crate::LauncherResult; use super::credentials::CredentialsStore; @@ -41,6 +42,8 @@ impl State { #[tracing::instrument] async fn initialize() -> LauncherResult> { + crate::utils::io::create_dir_all(Dirs::get().await?.base_dir()).await?; + let ingress_processor = IngressProcessor::new(); let settings = Settings::new().await; let db = super::db::create_pool().await?; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e90db4f..de577ba4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,12 @@ importers: apps/oneclient/frontend: dependencies: + '@fontsource/poppins': + specifier: ^5.2.6 + version: 5.2.6 + '@formatjs/intl-durationformat': + specifier: ^0.7.4 + version: 0.7.4 '@tailwindcss/vite': specifier: ^4.0.6 version: 4.1.5(vite@6.3.4(@types/node@22.7.9)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)) @@ -99,15 +105,54 @@ importers: '@tanstack/router-plugin': specifier: ^1.114.3 version: 1.119.0(@tanstack/react-router@1.119.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.6(solid-js@1.9.6)(vite@6.3.4(@types/node@22.7.9)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)))(vite@6.3.4(@types/node@22.7.9)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)) + '@tauri-apps/api': + specifier: ^2.5.0 + version: 2.5.0 + '@tauri-apps/plugin-clipboard-manager': + specifier: ^2.2.2 + version: 2.2.2 + '@tauri-apps/plugin-dialog': + specifier: ^2.2.1 + version: 2.2.1 + '@tauri-apps/plugin-shell': + specifier: ^2.2.1 + version: 2.2.1 + '@untitled-theme/icons-react': + specifier: ^0.14.0 + version: 0.14.0(@types/react@19.1.2)(react@19.1.0) + motion: + specifier: ^12.10.5 + version: 12.10.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + overlayscrollbars: + specifier: ^2.11.2 + version: 2.11.2 + overlayscrollbars-react: + specifier: ^0.5.6 + version: 0.5.6(overlayscrollbars@2.11.2)(react@19.1.0) react: specifier: ^19.0.0 version: 19.1.0 + react-aria-components: + specifier: ^1.8.0 + version: 1.8.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + tailwind-merge: + specifier: ^3.2.0 + version: 3.2.0 + tailwind-variants: + specifier: ^1.0.0 + version: 1.0.0(tailwindcss@4.1.5) tailwindcss: specifier: ^4.0.6 version: 4.1.5 + tailwindcss-animated: + specifier: ^2.0.0 + version: 2.0.0(tailwindcss@4.1.5) + tailwindcss-react-aria-components: + specifier: ^2.0.0 + version: 2.0.0(tailwindcss@4.1.5) devDependencies: '@tanstack/eslint-config': specifier: ^0.1.0 @@ -121,9 +166,6 @@ importers: '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.4.1(vite@6.3.4(@types/node@22.7.9)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.80.3)(tsx@4.19.4)(yaml@2.6.0)) - prettier: - specifier: ^3.5.3 - version: 3.5.3 typescript: specifier: ^5.7.2 version: 5.8.3