diff --git a/.github/actions/publish-artifacts/src/index.ts b/.github/actions/publish-artifacts/src/index.ts index d9a30ec8..48c5ba53 100644 --- a/.github/actions/publish-artifacts/src/index.ts +++ b/.github/actions/publish-artifacts/src/index.ts @@ -20,7 +20,7 @@ interface TargetConfig { } interface BuildTarget { updater: false | Updater; - standalone: TargetConfig[]; + standalone: Array; } const osTargets = { @@ -67,7 +67,7 @@ const frontendBundle = 'apps/frontend/dist.tar.xz'; const updaterName = `OneLauncher-Updater-${os}-${arch}`; const frontendName = `OneLauncher-Frontend-${os}-${arch}`; -const globFiles = async (pattern: string): Promise => await (await glob.create(pattern)).glob(); +const globFiles = async (pattern: string): Promise> => await (await glob.create(pattern)).glob(); async function uploadFrontend() { if (!(await exists(frontendBundle))) { diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml index b32fed1d..e4417b9b 100644 --- a/.github/actions/setup-pnpm/action.yml +++ b/.github/actions/setup-pnpm/action.yml @@ -9,10 +9,12 @@ runs: using: composite steps: - name: Install pnpm - uses: pnpm/action-setup@ac5bf11548bf5e19b8aadb8182072616590fa4a6 + uses: pnpm/action-setup@v4 + with: + version: 10 - name: Install Node.js - uses: actions/setup-node@26961cf329f22f6837d5f54c3efd76b480300ace + uses: actions/setup-node@v4 with: token: ${{ inputs.token }} check-latest: true diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index 3fd88d7d..4b67d02c 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -15,23 +15,23 @@ inputs: runs: using: composite steps: - - name: Install Rust Stable - uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a - with: - toolchain: stable - targets: ${{ inputs.target }} - components: clippy, rustfmt + # - name: Install Rust Stable + # uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a + # with: + # toolchain: stable + # targets: ${{ inputs.target }} + # components: clippy, rustfmt - name: Install Rust Nightly - uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a + uses: dtolnay/rust-toolchain@master with: - toolchain: nightly + toolchain: nightly-2025-07-23 targets: ${{ inputs.target }} - components: clippy, rustfmt + components: cargo, clippy, rustc, rustfmt - name: Cache Rust if: ${{ inputs.restore-cache == 'true' }} - uses: Swatinem/rust-cache@9bdad043e88c75890e36ad3bbc8d27f0090dd609 + uses: Swatinem/rust-cache@master with: key: ${{ inputs.target }} save-if: ${{ inputs.save-cache }} diff --git a/.github/workflows/oneclient_release.yml b/.github/workflows/oneclient_release.yml index 15f36428..70040ff5 100644 --- a/.github/workflows/oneclient_release.yml +++ b/.github/workflows/oneclient_release.yml @@ -43,5 +43,6 @@ jobs: releaseName: OneClient __VERSION__ releaseDraft: true prerelease: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-alpha') || false }} - tauriScript: pnpm oneclient:desktop + tauriScript: pnpm -w oneclient:desktop args: ${{ matrix.args }} + projectPath: ./apps/oneclient/desktop/ diff --git a/.github/workflows/onelauncher_release.yml b/.github/workflows/onelauncher_release.yml index 0b2e4a14..7062001b 100644 --- a/.github/workflows/onelauncher_release.yml +++ b/.github/workflows/onelauncher_release.yml @@ -43,5 +43,6 @@ jobs: releaseName: OneLauncher __VERSION__ releaseDraft: true prerelease: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-alpha') || false }} - tauriScript: pnpm onelauncher:desktop + tauriScript: pnpm -w onelauncher:desktop args: ${{ matrix.args }} + projectPath: ./apps/onelauncher/desktop/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..cb2c84d5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm lint-staged diff --git a/.nvmrc b/.nvmrc index 209e3ef4..a45fd52c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +24 diff --git a/README.md b/README.md index 99041200..89f27dc0 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ We welcome contributions! Please read our [contributing guidelines](CONTRIBUTING If you encounter any issues, ensure that you are using the following versions (or later) of Rust, Node and Pnpm: -- `rustc` version: **1.89** +- `rustc` version: **nightly-2025-07-23** - `node` version: **22.15** - `pnpm` version: **10.13** diff --git a/apps/oneclient/desktop/src/api/commands.rs b/apps/oneclient/desktop/src/api/commands.rs index 33da7cb8..6a61bf58 100644 --- a/apps/oneclient/desktop/src/api/commands.rs +++ b/apps/oneclient/desktop/src/api/commands.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; -use onelauncher_core::{entity::clusters, error::LauncherResult}; +use onelauncher_core::entity::clusters; +use onelauncher_core::error::LauncherResult; use tauri::Runtime; #[taurpc::procedures(path = "oneclient", export_to = "../frontend/src/bindings.gen.ts")] @@ -22,7 +23,9 @@ impl OneClientApi for OneClientApiImpl { webview_window.open_devtools(); } - async fn get_clusters_grouped_by_major(self) -> LauncherResult>> { + 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(); @@ -38,13 +41,10 @@ impl OneClientApi for OneClientApiImpl { Err(_) => continue, // Skip if parsing fails }; - mapped.entry(major) - .or_insert_with(Vec::new) - .push(cluster); + mapped.entry(major).or_default().push(cluster); } } Ok(mapped) } - } diff --git a/apps/oneclient/desktop/src/lib.rs b/apps/oneclient/desktop/src/lib.rs index 43d9a265..bab5afdc 100644 --- a/apps/oneclient/desktop/src/lib.rs +++ b/apps/oneclient/desktop/src/lib.rs @@ -35,9 +35,10 @@ async fn initialize_core() -> LauncherResult<()> { launcher_website: "https://polyfrost.org/".to_string(), discord_client_id: None, //Some(constants::DISCORD_CLIENT_ID.to_string()), // disabled for now fetch_attempts: 3, - logger_filter: Some( - format!("{}={level},onelauncher_core={level}", env!("CARGO_PKG_NAME")), - ), + logger_filter: Some(format!( + "{}={level},onelauncher_core={level}", + env!("CARGO_PKG_NAME") + )), ..Default::default() }; @@ -68,7 +69,7 @@ async fn initialize_tauri(builder: tauri::Builder) -> LauncherResult app.emit("single-instance", SingleInstancePayload { args: argv, cwd }) .unwrap(); })) - .plugin(tauri_plugin_updater::Builder::new().build()) + // .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_deep_link::init()) @@ -98,9 +99,7 @@ async fn initialize_state(handle: &tauri::AppHandle) -> LauncherResult<()> { } pub async fn run() { - initialize_core() - .await - .expect("failed to initialize core"); + initialize_core().await.expect("failed to initialize core"); let app = initialize_tauri(tauri::Builder::default()) .await @@ -110,8 +109,7 @@ pub async fn run() { .await .expect("failed to initialize state"); - initialize_oneclient() - .await; + initialize_oneclient().await; app.run(|_, _| {}); } diff --git a/apps/oneclient/desktop/src/oneclient/mod.rs b/apps/oneclient/desktop/src/oneclient/mod.rs index 41cde7c6..d0be6292 100644 --- a/apps/oneclient/desktop/src/oneclient/mod.rs +++ b/apps/oneclient/desktop/src/oneclient/mod.rs @@ -1,4 +1,8 @@ -use onelauncher_core::{entity::{clusters::Model as ClusterModel, icon::Icon, loader::GameLoader}, error::LauncherResult, send_error}; +use onelauncher_core::entity::clusters::Model as ClusterModel; +use onelauncher_core::entity::icon::Icon; +use onelauncher_core::entity::loader::GameLoader; +use onelauncher_core::error::LauncherResult; +use onelauncher_core::send_error; pub async fn initialize_oneclient() { init_clusters().await; @@ -31,12 +35,14 @@ async fn init_clusters() { for loader in &cluster.mc_loaders { if let Err(e) = create_cluster_if_not_exist( - &format!("{} {}", mc_version, loader), + &format!("{mc_version} {loader}"), &mc_version, *loader, None, None, - ).await { + ) + .await + { send_error!("failed to create cluster for {}: {}", mc_version, e); } } @@ -56,7 +62,13 @@ async fn create_cluster_if_not_exist( return Ok(None); } - onelauncher_core::api::cluster::create_cluster(name, mc_version, mc_loader, mc_loader_version, icon_url) - .await - .map(Some) -} \ No newline at end of file + onelauncher_core::api::cluster::create_cluster( + name, + mc_version, + mc_loader, + mc_loader_version, + icon_url, + ) + .await + .map(Some) +} diff --git a/apps/oneclient/desktop/tauri.conf.json b/apps/oneclient/desktop/tauri.conf.json index cffb98da..4e718061 100644 --- a/apps/oneclient/desktop/tauri.conf.json +++ b/apps/oneclient/desktop/tauri.conf.json @@ -45,6 +45,7 @@ }, "bundle": { "active": true, + "createUpdaterArtifacts": false, "targets": [ "app", "dmg", @@ -66,7 +67,6 @@ "license": "GPL-3.0-only", "homepage": "https://polyfrost.org/", "longDescription": "Next-generation open source Minecraft launcher", - "createUpdaterArtifacts": true, "resources": [], "externalBin": [], "linux": { diff --git a/apps/oneclient/frontend/.gitignore b/apps/oneclient/frontend/.gitignore index 0b0b1a38..ed5e068a 100644 --- a/apps/oneclient/frontend/.gitignore +++ b/apps/oneclient/frontend/.gitignore @@ -4,4 +4,5 @@ dist dist-ssr *.local -**/*.gen.ts +# **/*.gen.ts +routeTree.gen.ts diff --git a/apps/oneclient/frontend/package.json b/apps/oneclient/frontend/package.json index 4b94dc85..a32c6add 100644 --- a/apps/oneclient/frontend/package.json +++ b/apps/oneclient/frontend/package.json @@ -8,8 +8,7 @@ "build": "vite build && tsc", "serve": "vite preview", "lint": "eslint", - "format": "prettier", - "check": "prettier --write . && eslint --fix" + "check": "eslint --fix" }, "dependencies": { "@fontsource/poppins": "catalog:", diff --git a/apps/oneclient/frontend/prettier.config.js b/apps/oneclient/frontend/prettier.config.js deleted file mode 100644 index adcd2f4d..00000000 --- a/apps/oneclient/frontend/prettier.config.js +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-check - -/** @type {import('prettier').Config} */ -const config = { - semi: true, - singleQuote: true, - trailingComma: 'all', -}; - -export default config; diff --git a/apps/oneclient/frontend/src/bindings.gen.ts b/apps/oneclient/frontend/src/bindings.gen.ts new file mode 100644 index 00000000..ed7cc5bd --- /dev/null +++ b/apps/oneclient/frontend/src/bindings.gen.ts @@ -0,0 +1,251 @@ +// This file has been generated by Specta. DO NOT EDIT. + +import { createTauRPCProxy as createProxy, type InferCommandOutput } from 'taurpc' +type TAURI_CHANNEL = (response: T) => void + + +export type ClusterError = { type: "InvalidVersion"; data: string } | { type: "MissingJavaVersion"; data: string } | { type: "ClusterDownloading"; data: string } | { type: "ClusterAlreadyRunning"; data: string } + +export type ClusterModel = { id: bigint; folder_name: string; stage: ClusterStage; created_at: string; group_id: bigint | null; name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; last_played: string | null; overall_played: bigint | null; icon_url: Icon | null; setting_profile_name: string | null; linked_pack_id: string | null; linked_pack_version: string | null } + +export type ClusterStage = "notready" | "downloading" | "repairing" | "ready" + +export type ClusterUpdate = { name: string | null; icon_url: Icon | null } + +export type CreateCluster = { name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; icon: Icon | null } + +export type CryptoError = { type: "InvalidHash"; data: string } | { type: "InvalidAlgorithm"; data: string } + +export type DaoError = { type: "NotFound"; data: string } | { type: "AlreadyExists"; data: string } | { type: "InvalidValue"; data: string } + +export type DbVec = T[] + +export type DirectoryError = { type: "BaseDir"; data: string } + +export type DiscordError = { type: "MissingClientId"; data: string } | { type: "ConnectError"; data: string } + +export type Filters = { game_versions: string[] | null; loaders: GameLoader[] | null; categories: string[] | null; package_types: PackageType[] | null } + +export type GameLoader = "vanilla" | "forge" | "neoforge" | "quilt" | "fabric" | "legacyfabric" + +export type IOError = { type: "InvalidAbsolutePath"; data: string } | { type: "IOErrorWrapper"; data: string } | { type: "IOError"; data: string } | { type: "DeserializeError"; data: string } | { type: "AsyncZipError"; data: string } + +export type Icon = string + +export type IncompatiblePackageType = { type: "McVersion"; data: string } | { type: "Loader"; data: string } + +export type IngressError = { type: "NotFound"; data: string } + +export type IngressPayload = { id: string; message: string; ingress_type: IngressType; percent: number | null; total: number } + +export type IngressType = { Download: { file_name: string } } | "JavaPrepare" | "JavaCheck" | "JavaLocate" | "MinecraftDownload" | { PrepareCluster: { cluster_name: string } } + +export type JavaError = { type: "ParseVersion"; data: string } | { type: "Execute"; data: string } | { type: "MissingJava"; data: string } + +export type LauncherError = { type: "DirError"; data: DirectoryError } | { type: "IOError"; data: IOError } | { type: "IngressError"; data: IngressError } | { type: "JavaError"; data: JavaError } | { type: "CryptoError"; data: CryptoError } | { type: "DiscordError"; data: DiscordError } | { type: "MetadataError"; data: MetadataError } | { type: "ClusterError"; data: ClusterError } | { type: "MinecraftAuthError"; data: MinecraftAuthError } | { type: "ProcessError"; data: ProcessError } | { type: "PackageError"; data: PackageError } | { type: "DaoError"; data: DaoError } | { type: "SerdeError"; data: string } | { type: "AnyhowError"; data: string } | { type: "DbError"; data: string } | { type: "ReqwestError"; data: string } | { type: "InterpulseError"; data: string } | { type: "RegexError"; data: string } | { type: "SemaphoreError"; data: string } | { type: "UrlError"; data: string } | { type: "TauriError"; data: string } + +export type ManagedPackage = { id: string; slug: string; provider: Provider; package_type: PackageType; name: string; short_desc: string; body: string; version_ids: string[]; mc_versions: string[]; loaders: GameLoader[]; icon_url: string | null; created: string; updated: string; client: PackageSide; server: PackageSide; categories: string[]; license: PackageLicense | null; author: PackageAuthor; links: PackageLinks; status: PackageStatus; downloads: bigint; gallery: PackageGallery[] } + +export type ManagedUser = { id: string; username: string; url?: string | null; avatar_url?: string | null; bio?: string | null; is_organization_user?: boolean; role?: string | null } + +export type ManagedVersion = { version_id: string; project_id: string; display_name: string; display_version: string; changelog: string | null; dependencies: ManagedVersionDependency[]; mc_versions: string[]; release_type: PackageReleaseType; loaders: GameLoader[]; published: string; downloads: bigint; files: ManagedVersionFile[] } + +export type ManagedVersionDependency = { version_id: string | null; project_id: string | null; file_name: string | null; dependency_type: PackageDependencyType } + +export type ManagedVersionFile = { sha1: string; url: string; file_name: string; primary: boolean; size: bigint } + +export type MessageLevel = "Info" | "Warn" | "Error" + +export type MessagePayload = { level: MessageLevel; message: string } + +export type MetadataError = { type: "FetchError"; data: string } | { type: "NotModdedManifest"; data: string } | { type: "NotVanillaManifest"; data: string } | { type: "ParseError"; data: string } | { type: "NoMatchingLoader"; data: string } | { type: "NoMatchingVersion"; data: string } + +export type MinecraftAuthError = { type: "PublicKeyReading"; data: string } | { type: "PKCS8Error"; data: string } | { type: "SerializeError"; data: string } | { type: "DeserializeError"; data: string } | { type: "RequestError"; data: string } | { type: "SigningError"; data: string } | { type: "HashError"; data: string } | { type: "SessionIdError"; data: string } + +/** + * A structure of all needed Minecraft credentials for logging in and account management. + */ +export type MinecraftCredentials = { +/** + * The uuid of the credentials. + */ +id: string; +/** + * The username of the Minecraft account. + */ +username: string; +/** + * The access token as a String. + */ +access_token: string; +/** + * The refresh token as a string for [`MinecraftState#refresh`]. + */ +refresh_token: string; +/** + * The time that the access token expires as a [`DateTime`]. + */ +expires: string } + +export type PackageAuthor = { Team: { team_id: string; org_id: string | null } } | { Users: ManagedUser[] } + +export type PackageDependencyType = "required" | "optional" | "embedded" | "incompatible" + +/** + * https://api.modrinth.com/v2/tag/donation_platform + */ +export type PackageDonationPlatform = "patreon" | "buymeacoffee" | "paypal" | "github" | "kofi" | "other" + +export type PackageDonationUrl = { id: PackageDonationPlatform; url: string } + +export type PackageError = { type: "NoPrimaryFile"; data: string } | { type: "IsModPack"; data: string } | { type: "Incompatible"; data: IncompatiblePackageType } + +export type PackageGallery = { url: string; thumbnail_url: string; title: string | null; description: string | null; featured: boolean | null } + +/** + * https://spdx.org/licenses/ + */ +export type PackageLicense = { id: string; name: string; url: string | null } + +/** + * https://docs.curseforge.com/rest-api/#tocS_ModLinks + */ +export type PackageLinks = { website: string | null; issues: string | null; source: string | null; wiki: string | null; donation: PackageDonationUrl[] | null; discord: string | null } + +export type PackageModel = { hash: string; file_name: string; version_id: string; published_at: string; display_name: string; display_version: string; package_type: PackageType; provider: Provider; package_id: string; mc_versions: DbVec; 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 type Version = { +/** + * A unique identifier of the version + */ +id: string; +/** + * The release type of the version + */ +type: VersionType; +/** + * A link to additional information about the version + */ +url: string; +/** + * The latest time a file in this version was updated + */ +time: string; +/** + * The time this version was released + */ +releaseTime: string; +/** + * The SHA1 hash of the additional information about the version + */ +sha1: string; +/** + * Whether the version supports the latest player safety features + */ +complianceLevel: number; +/** + * The SHA1 hash of the original unmodified Minecraft version's JSON + * This is only available when using the Pulseflow mirror + */ +originalSha1?: string | null } + +/** + * The version type of Minecraft + */ +export type VersionType = +/** + * A major version, which is stable for all players to use + */ +"release" | +/** + * An experimental version, which is unstable and used for feature previews and beta testing + */ +"snapshot" | +/** + * The oldest versions before the game was released + */ +"old_alpha" | +/** + * Early versions of the game + */ +"old_beta" + +const ARGS_MAP = { 'events':'{"process":["event"],"ingress":["event"],"message":["event"]}', 'core':'{"getPackageVersions":["provider","slug","mc_versions","loaders","offset","limit"],"readSettings":[],"getDefaultUser":["fallback"],"getPackageUser":["provider","slug"],"getClusterById":["id"],"updateClusterById":["id","request"],"getClusters":[],"getLoadersForVersion":["mc_version"],"searchPackages":["provider","query"],"removeCluster":["id"],"getPackage":["provider","slug"],"launchCluster":["id","uuid"],"openMsaLogin":[],"getMultiplePackages":["provider","slugs"],"getUsersFromAuthor":["provider","author"],"getGlobalProfile":[],"removeUser":["uuid"],"getWorlds":["id"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getUsers":[],"getGameVersions":[],"getUser":["uuid"],"setDefaultUser":["uuid"],"updateClusterProfile":["name","profile"],"getProfileOrDefault":["name"],"createCluster":["options"],"getScreenshots":["id"],"writeSettings":["setting"]}', 'oneclient':'{"getClustersGroupedByMajor":[],"openDevTools":[]}' } +export type Router = { 'oneclient': { openDevTools: () => Promise, +getClustersGroupedByMajor: () => Promise> }, +'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 } }; + + +export type { InferCommandOutput } +export const createTauRPCProxy = () => createProxy(ARGS_MAP) diff --git a/apps/oneclient/frontend/tsconfig.json b/apps/oneclient/frontend/tsconfig.json index 3dd88644..e442b494 100644 --- a/apps/oneclient/frontend/tsconfig.json +++ b/apps/oneclient/frontend/tsconfig.json @@ -25,5 +25,5 @@ "skipLibCheck": true, "noUncheckedSideEffectImports": true }, - "include": ["**/*.ts", "**/*.tsx", "prettier.config.js", "vite.config.js"] + "include": ["**/*.ts", "**/*.tsx", "vite.config.ts"] } diff --git a/apps/oneclient/frontend/vite.config.js b/apps/oneclient/frontend/vite.config.ts similarity index 100% rename from apps/oneclient/frontend/vite.config.js rename to apps/oneclient/frontend/vite.config.ts diff --git a/apps/onelauncher/desktop/src/lib.rs b/apps/onelauncher/desktop/src/lib.rs index 5aaa252c..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,13 +62,14 @@ 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); app.emit("single-instance", SingleInstancePayload { args: argv, cwd }) .unwrap(); })) - .plugin(tauri_plugin_updater::Builder::new().build()) + // .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_deep_link::init()) @@ -75,10 +77,8 @@ async fn initialize_tauri(builder: tauri::Builder) -> 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/desktop/tauri.conf.json b/apps/onelauncher/desktop/tauri.conf.json index 2db398b7..1e957a67 100644 --- a/apps/onelauncher/desktop/tauri.conf.json +++ b/apps/onelauncher/desktop/tauri.conf.json @@ -45,6 +45,7 @@ }, "bundle": { "active": true, + "createUpdaterArtifacts": false, "targets": [ "app", "dmg", @@ -66,7 +67,6 @@ "license": "GPL-3.0-only", "homepage": "https://polyfrost.org/", "longDescription": "Next-generation open source Minecraft launcher", - "createUpdaterArtifacts": true, "resources": [], "externalBin": [], "linux": { @@ -112,7 +112,7 @@ }, "plugins": { "updater": { - "pubkey": "", + "active": false, "endpoints": ["https://polyfrost.org/releases/tauri/{{current_version}}/{{target}}/{{arch}}"] }, "deep-link": { diff --git a/apps/onelauncher/frontend/.gitignore b/apps/onelauncher/frontend/.gitignore index 0b0b1a38..ed5e068a 100644 --- a/apps/onelauncher/frontend/.gitignore +++ b/apps/onelauncher/frontend/.gitignore @@ -4,4 +4,5 @@ dist dist-ssr *.local -**/*.gen.ts +# **/*.gen.ts +routeTree.gen.ts diff --git a/apps/onelauncher/frontend/src/bindings.gen.ts b/apps/onelauncher/frontend/src/bindings.gen.ts new file mode 100644 index 00000000..c3765171 --- /dev/null +++ b/apps/onelauncher/frontend/src/bindings.gen.ts @@ -0,0 +1,253 @@ +// @ts-nocheck +// This file has been generated by Specta. DO NOT EDIT. + +import { createTauRPCProxy as createProxy, type InferCommandOutput } from 'taurpc' +type TAURI_CHANNEL = (response: T) => void + + +export type ClusterError = { type: "InvalidVersion"; data: string } | { type: "MissingJavaVersion"; data: string } | { type: "ClusterDownloading"; data: string } | { type: "ClusterAlreadyRunning"; data: string } + +export type ClusterModel = { id: bigint; folder_name: string; stage: ClusterStage; created_at: string; group_id: bigint | null; name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; last_played: string | null; overall_played: bigint | null; icon_url: Icon | null; setting_profile_name: string | null; linked_pack_id: string | null; linked_pack_version: string | null } + +export type ClusterStage = "notready" | "downloading" | "repairing" | "ready" + +export type ClusterUpdate = { name: string | null; icon_url: Icon | null } + +export type CreateCluster = { name: string; mc_version: string; mc_loader: GameLoader; mc_loader_version: string | null; icon: Icon | null } + +export type CryptoError = { type: "InvalidHash"; data: string } | { type: "InvalidAlgorithm"; data: string } + +export type DaoError = { type: "NotFound"; data: string } | { type: "AlreadyExists"; data: string } | { type: "InvalidValue"; data: string } + +export type DbVec = T[] + +export type DirectoryError = { type: "BaseDir"; data: string } + +export type DiscordError = { type: "MissingClientId"; data: string } | { type: "ConnectError"; data: string } + +export type Filters = { game_versions: string[] | null; loaders: GameLoader[] | null; categories: string[] | null; package_types: PackageType[] | null } + +export type GameLoader = "vanilla" | "forge" | "neoforge" | "quilt" | "fabric" | "legacyfabric" + +export type IOError = { type: "InvalidAbsolutePath"; data: string } | { type: "IOErrorWrapper"; data: string } | { type: "IOError"; data: string } | { type: "DeserializeError"; data: string } | { type: "AsyncZipError"; data: string } + +export type Icon = string + +export type IncompatiblePackageType = { type: "McVersion"; data: string } | { type: "Loader"; data: string } + +export type IngressError = { type: "NotFound"; data: string } + +export type IngressPayload = { id: string; message: string; ingress_type: IngressType; percent: number | null; total: number } + +export type IngressType = { Download: { file_name: string } } | "JavaPrepare" | "JavaCheck" | "JavaLocate" | "MinecraftDownload" | { PrepareCluster: { cluster_name: string } } + +export type JavaError = { type: "ParseVersion"; data: string } | { type: "Execute"; data: string } | { type: "MissingJava"; data: string } + +export type LauncherError = { type: "DirError"; data: DirectoryError } | { type: "IOError"; data: IOError } | { type: "IngressError"; data: IngressError } | { type: "JavaError"; data: JavaError } | { type: "CryptoError"; data: CryptoError } | { type: "DiscordError"; data: DiscordError } | { type: "MetadataError"; data: MetadataError } | { type: "ClusterError"; data: ClusterError } | { type: "MinecraftAuthError"; data: MinecraftAuthError } | { type: "ProcessError"; data: ProcessError } | { type: "PackageError"; data: PackageError } | { type: "DaoError"; data: DaoError } | { type: "SerdeError"; data: string } | { type: "AnyhowError"; data: string } | { type: "DbError"; data: string } | { type: "ReqwestError"; data: string } | { type: "InterpulseError"; data: string } | { type: "RegexError"; data: string } | { type: "SemaphoreError"; data: string } | { type: "UrlError"; data: string } | { type: "TauriError"; data: string } + +export type ManagedPackage = { id: string; slug: string; provider: Provider; package_type: PackageType; name: string; short_desc: string; body: string; version_ids: string[]; mc_versions: string[]; loaders: GameLoader[]; icon_url: string | null; created: string; updated: string; client: PackageSide; server: PackageSide; categories: string[]; license: PackageLicense | null; author: PackageAuthor; links: PackageLinks; status: PackageStatus; downloads: bigint; gallery: PackageGallery[] } + +export type ManagedUser = { id: string; username: string; url?: string | null; avatar_url?: string | null; bio?: string | null; is_organization_user?: boolean; role?: string | null } + +export type ManagedVersion = { version_id: string; project_id: string; display_name: string; display_version: string; changelog: string | null; dependencies: ManagedVersionDependency[]; mc_versions: string[]; release_type: PackageReleaseType; loaders: GameLoader[]; published: string; downloads: bigint; files: ManagedVersionFile[] } + +export type ManagedVersionDependency = { version_id: string | null; project_id: string | null; file_name: string | null; dependency_type: PackageDependencyType } + +export type ManagedVersionFile = { sha1: string; url: string; file_name: string; primary: boolean; size: bigint } + +export type MessageLevel = "Info" | "Warn" | "Error" + +export type MessagePayload = { level: MessageLevel; message: string } + +export type MetadataError = { type: "FetchError"; data: string } | { type: "NotModdedManifest"; data: string } | { type: "NotVanillaManifest"; data: string } | { type: "ParseError"; data: string } | { type: "NoMatchingLoader"; data: string } | { type: "NoMatchingVersion"; data: string } + +export type MinecraftAuthError = { type: "PublicKeyReading"; data: string } | { type: "PKCS8Error"; data: string } | { type: "SerializeError"; data: string } | { type: "DeserializeError"; data: string } | { type: "RequestError"; data: string } | { type: "SigningError"; data: string } | { type: "HashError"; data: string } | { type: "SessionIdError"; data: string } + +/** + * A structure of all needed Minecraft credentials for logging in and account management. + */ +export type MinecraftCredentials = { +/** + * The uuid of the credentials. + */ +id: string; +/** + * The username of the Minecraft account. + */ +username: string; +/** + * The access token as a String. + */ +access_token: string; +/** + * The refresh token as a string for [`MinecraftState#refresh`]. + */ +refresh_token: string; +/** + * The time that the access token expires as a [`DateTime`]. + */ +expires: string } + +export type PackageAuthor = { Team: { team_id: string; org_id: string | null } } | { Users: ManagedUser[] } + +export type PackageDependencyType = "required" | "optional" | "embedded" | "incompatible" + +/** + * https://api.modrinth.com/v2/tag/donation_platform + */ +export type PackageDonationPlatform = "patreon" | "buymeacoffee" | "paypal" | "github" | "kofi" | "other" + +export type PackageDonationUrl = { id: PackageDonationPlatform; url: string } + +export type PackageError = { type: "NoPrimaryFile"; data: string } | { type: "IsModPack"; data: string } | { type: "Incompatible"; data: IncompatiblePackageType } + +export type PackageGallery = { url: string; thumbnail_url: string; title: string | null; description: string | null; featured: boolean | null } + +/** + * https://spdx.org/licenses/ + */ +export type PackageLicense = { id: string; name: string; url: string | null } + +/** + * https://docs.curseforge.com/rest-api/#tocS_ModLinks + */ +export type PackageLinks = { website: string | null; issues: string | null; source: string | null; wiki: string | null; donation: PackageDonationUrl[] | null; discord: string | null } + +export type PackageModel = { hash: string; file_name: string; version_id: string; published_at: string; display_name: string; display_version: string; package_type: PackageType; provider: Provider; package_id: string; mc_versions: DbVec; 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 = Record + +export type Sort = "Relevance" | "Downloads" | "Newest" | "Updated" + +/** + * A game version of Minecraft + */ +export type Version = { +/** + * A unique identifier of the version + */ +id: string; +/** + * The release type of the version + */ +type: VersionType; +/** + * A link to additional information about the version + */ +url: string; +/** + * The latest time a file in this version was updated + */ +time: string; +/** + * The time this version was released + */ +releaseTime: string; +/** + * The SHA1 hash of the additional information about the version + */ +sha1: string; +/** + * Whether the version supports the latest player safety features + */ +complianceLevel: number; +/** + * The SHA1 hash of the original unmodified Minecraft version's JSON + * This is only available when using the Pulseflow mirror + */ +originalSha1?: string | null } + +/** + * The version type of Minecraft + */ +export type VersionType = +/** + * A major version, which is stable for all players to use + */ +"release" | +/** + * An experimental version, which is unstable and used for feature previews and beta testing + */ +"snapshot" | +/** + * The oldest versions before the game was released + */ +"old_alpha" | +/** + * Early versions of the game + */ +"old_beta" + +const ARGS_MAP = { 'events':'{"ingress":["event"],"message":["event"],"process":["event"]}', 'core':'{"removeUser":["uuid"],"writeSettings":["setting"],"updateClusterById":["id","request"],"getScreenshots":["id"],"getClusters":[],"getWorlds":["id"],"getGameVersions":[],"getPackage":["provider","slug"],"getLoadersForVersion":["mc_version"],"getMultiplePackages":["provider","slugs"],"getPackageUser":["provider","slug"],"downloadPackage":["provider","package_id","version_id","cluster_id","skip_compatibility"],"getUsersFromAuthor":["provider","author"],"searchPackages":["provider","query"],"getUser":["uuid"],"getProfileOrDefault":["name"],"updateClusterProfile":["name","profile"],"getPackageVersions":["provider","slug","mc_versions","loaders","offset","limit"],"launchCluster":["id","uuid"],"setDefaultUser":["uuid"],"openMsaLogin":[],"getGlobalProfile":[],"getClusterById":["id"],"getDefaultUser":["fallback"],"readSettings":[],"removeCluster":["id"],"createCluster":["options"],"getUsers":[]}', 'onelauncher':'{"set_window_style":["decorations"],"return_error":[],"open_dev_tools":[]}' } +export type Router = { 'core': { getClusters: () => Promise, +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 }, +'onelauncher': { return_error: () => Promise, +open_dev_tools: () => Promise, +set_window_style: (decorations: boolean) => Promise } }; + + +export type { InferCommandOutput } +export const createTauRPCProxy = () => createProxy(ARGS_MAP) diff --git a/apps/onelauncher/frontend/src/components/content/PackageItem.tsx b/apps/onelauncher/frontend/src/components/content/PackageItem.tsx index ef7c9acf..1a1ef302 100644 --- a/apps/onelauncher/frontend/src/components/content/PackageItem.tsx +++ b/apps/onelauncher/frontend/src/components/content/PackageItem.tsx @@ -1,7 +1,7 @@ import type { Provider, SearchResult } from '@/bindings.gen'; import { abbreviateNumber, LOADERS, upperFirst } from '@/utils'; import { Show, Tooltip } from '@onelauncher/common/components'; -import { Link, useNavigate } from '@tanstack/react-router'; +import { Link } from '@tanstack/react-router'; import { Download01Icon } from '@untitled-theme/icons-react'; import { useMemo } from 'react'; import { Focusable } from 'react-aria-components'; @@ -13,7 +13,7 @@ function includes(list: { includes: (arg0: TArray) => boole export function PackageGrid({ items, provider }: { items: Array; provider: Provider }) { return ( -
+
{items.map(item => ( ))} @@ -22,7 +22,7 @@ export function PackageGrid({ items, provider }: { items: Array; p } export function PackageItem({ provider, ...item }: SearchResult & { provider: Provider }) { - const loaders = useMemo(() => item.categories.filter(cat => includes(LOADERS, cat)), []); + const loaders = useMemo(() => item.categories.filter(cat => includes(LOADERS, cat)), [item.categories]); return ( void; + setCluster: Dispatch>; provider: Provider; - setProvider: (provider: Provider) => void; + setProvider: Dispatch>; query: SearchQuery; - setQuery: (query: SearchQuery) => void; + setQuery: Dispatch>; search: () => void; } @@ -36,7 +37,7 @@ export function BrowserProvider(props: any) { categories: null, game_versions: null, loaders: null, - package_types: ["mod"] + package_types: ['mod'], }, query: null, limit: 18 as unknown as bigint, @@ -47,7 +48,7 @@ export function BrowserProvider(props: any) { useEffect(() => { if (query.filters?.categories || query.query) navigate({ to: '/app/browser/search' }); - }, [query]); + }, [navigate, query]); const context = useMemo(() => ({ cluster, @@ -78,3 +79,7 @@ export function usePackageData(provider: Provider, slug: string, options?: Omit< export function usePackageVersions(provider: Provider, slug: string, { mc_versions, loaders, offset, limit, ...options }: { mc_versions?: Array | null; loaders?: Array | null; offset?: number; limit: number } & Omit>, 'queryKey' | 'queryFn'>, key: false | BindingCommands | (string & {}) = `getPackageVersions.${provider}.${slug}.${mc_versions?.join('')}.${loaders?.join('')}`) { return useCommand(key, () => bindings.core.getPackageVersions(provider, slug, mc_versions ?? null, loaders ?? null, (offset ?? 0) as unknown as bigint, limit as unknown as bigint), options); } + +export function useDownloadPackage(cluster: ClusterModel, provider: Provider, version: ManagedVersion, skipCompatibility = false) { + return useCommandMut(() => bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility)); +} diff --git a/apps/onelauncher/frontend/src/hooks/useCluster.tsx b/apps/onelauncher/frontend/src/hooks/useCluster.tsx index c188f7c1..23220181 100644 --- a/apps/onelauncher/frontend/src/hooks/useCluster.tsx +++ b/apps/onelauncher/frontend/src/hooks/useCluster.tsx @@ -1,10 +1,10 @@ import type { ClusterModel } from '@/bindings.gen'; import type { ModalProps } from '@/components/overlay/Modal'; +import Modal from '@/components/overlay/Modal'; import { bindings } from '@/main'; import { useCommand } from '@onelauncher/common'; -import { useEffect, useState } from 'react'; -import Modal from '@/components/overlay/Modal'; import { Button, Dropdown } from '@onelauncher/common/components'; +import { useEffect, useState } from 'react'; import { Heading } from 'react-aria-components'; export function useRecentCluster() { @@ -58,26 +58,29 @@ export function useClusters() { } type ChooseClusterModalProps = ModalProps & { - clusters?: ClusterModel[]; + clusters?: Array; confirmText?: string; - selected?: (clusters: ClusterModel[]) => number; + selected?: (clusters: Array) => number; onSelected?: (cluster: ClusterModel) => void; }; - -export function ChooseClusterModal({clusters: clusterList, selected: defaultSelected, onSelected, confirmText, ...props}: ChooseClusterModalProps) { - const clusters = useClusters() +export function ChooseClusterModal({ clusters: clusterList, selected: defaultSelected, onSelected, confirmText, ...props }: ChooseClusterModalProps) { + const clusters = useClusters(); const [selected, setSelected] = useState(0); useEffect(() => { - if (!clusters) return; - const newSelected = defaultSelected?.(clusters) - if (newSelected) setSelected(newSelected) - }, [clusters]) + if (!clusters) + return; + + const newSelected = defaultSelected?.(clusters); + + if (newSelected) + setSelected(newSelected); + }, [clusters, defaultSelected]); function chooseCluster() { - if (clusters !== undefined && selected !== undefined) { - const cluster = clusters[selected]; + if (clusters !== undefined) { + const cluster = clusters[selected] as ClusterModel | undefined; if (cluster !== undefined) onSelected?.(cluster); } @@ -87,35 +90,35 @@ export function ChooseClusterModal({clusters: clusterList, selected: defaultSele
- Select a Cluster - { - setSelected(index as number) - }} - selectedKey={selected} - > - {clusters?.map((cluster, index) => ( - - {cluster.name} - - ))} - + Select a Cluster + { + setSelected(index as number); + }} + selectedKey={selected} + > + {clusters?.map((cluster, index) => ( + + {cluster.name} + + ))} + -
-
+
+
- ) + ); // return ( // { - if (props.page === page) + if (pageIndex === page) return; - setPage(props.page); + setPage(pageIndex); }} > - {props.page} + {pageIndex} ); } diff --git a/apps/onelauncher/frontend/src/hooks/useSettings.tsx b/apps/onelauncher/frontend/src/hooks/useSettings.tsx index cf820eee..927ea61b 100644 --- a/apps/onelauncher/frontend/src/hooks/useSettings.tsx +++ b/apps/onelauncher/frontend/src/hooks/useSettings.tsx @@ -3,7 +3,7 @@ import type { Settings } from '@/bindings.gen'; import { bindings } from '@/main'; import { useCommand } from '@onelauncher/common'; -import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; +import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; interface SettingsControllerType { settings: Settings | undefined; @@ -12,7 +12,7 @@ interface SettingsControllerType { setSettingsToSave: (settings: Partial | null) => void; settingsChanged: boolean; setSettingsChanged: (changed: boolean) => void; - createSetting: (name: K, value: V) => [V, (value: V) => void]; + createSetting: (name: K, value: V) => [V, (v: V) => void]; } const SettingsContext = createContext(null); @@ -51,14 +51,14 @@ export function SettingsProvider({ children }: SettingsProviderProps) { return () => window.removeEventListener('beforeunload', handleBeforeUnload); }, []); - const saveChangedSettings = () => { + const saveChangedSettings = useCallback(() => { if (!settings) return; writeQuery.refetch(); setSettingsChanged(false); - }; + }, [settings, writeQuery]); const handleSetSettingsToSave = useCallback((newSettings: Partial | null) => { setSettingsToSave(newSettings); @@ -86,7 +86,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) { return [value, setValue]; }, [settings]); - const controller: SettingsControllerType = { + const controller: SettingsControllerType = useMemo(() => ({ settings, saveChangedSettings, settingsToSave, @@ -94,7 +94,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) { settingsChanged, setSettingsChanged, createSetting, - }; + }), [createSetting, handleSetSettingsToSave, saveChangedSettings, settings, settingsChanged, settingsToSave]); if (settings === undefined) return null; diff --git a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx index f376a604..d1cb8ab7 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx @@ -32,6 +32,7 @@ function Featured() { const navigate = useNavigate(); const featuredImageIndex = useMemo(() => featuredPackage.data?.gallery.findIndex(image => image.featured), [featuredPackage.data]); const [selectedImage, setSelectedImage] = useState(featuredImageIndex ?? 0); + return (
@@ -40,7 +41,7 @@ function Featured() {
thumbnail
-
+

{featuredPackage.data?.name}

diff --git a/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx index e6a763b2..11788629 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx @@ -1,8 +1,9 @@ import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, PackageDonationUrl, Provider } from '@/bindings.gen'; import type { HTMLProps } from 'react'; import Modal from '@/components/overlay/Modal'; -import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser'; +import { useBrowserContext, useDownloadPackage, usePackageData, usePackageVersions } from '@/hooks/useBrowser'; import { ChooseClusterModal, useClusters } from '@/hooks/useCluster'; +import usePagination from '@/hooks/usePagination'; import { bindings } from '@/main'; import { abbreviateNumber, formatAsRelative, PROVIDERS, upperFirst } from '@/utils'; import { useCommand } from '@onelauncher/common'; @@ -16,7 +17,6 @@ import Markdown from 'react-markdown'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; import { twMerge } from 'tailwind-merge'; -import usePagination from '@/hooks/usePagination'; export const Route = createFileRoute('/app/browser/package/$provider/$slug')({ component: RouteComponent, @@ -26,10 +26,10 @@ function includes(list: { includes: (arg0: TArray) => boole return list.includes(element as unknown as TArray); } -function CustomA({ href, children, includeIcon, className, ...rest }: { href: string; children: any, includeIcon?: boolean } & HTMLProps) { +function CustomA({ href, children, includeIcon, className, ...rest }: { href: string; children: any; includeIcon?: boolean } & HTMLProps) { return ( { e.preventDefault(); @@ -38,7 +38,7 @@ function CustomA({ href, children, includeIcon, className, ...rest }: { href: st {...rest} > {children} - {includeIcon && } + {includeIcon && } ); } @@ -56,10 +56,12 @@ function RouteComponent() { if (!includes(PROVIDERS, provider)) throw new Error('Invalid provider'); const packageData = usePackageData(provider, slug, {}); - const browserContext = useBrowserContext(); + const packageContextValue = useMemo(() => ({ + pkg: packageData.data, + }), [packageData.data]); return ( - +
@@ -84,14 +86,14 @@ function RouteComponent() {
- + {packageData.data?.gallery.map(item => ( -
+
{item.title &&
{item.title}
}
@@ -113,17 +115,17 @@ function RouteComponent() { function getPackageUrl(pkg: ManagedPackage): string { switch (pkg.provider) { - case 'Modrinth': return `https://modrinth.com/project/${pkg.slug}` - case 'Curseforge': return `https://www.curseforge.com/minecraft/${pkg.package_type}s/${pkg.slug}` - case 'SkyClient': return `` + case 'Modrinth': return `https://modrinth.com/project/${pkg.slug}`; + case 'Curseforge': return `https://www.curseforge.com/minecraft/${pkg.package_type}s/${pkg.slug}`; + case 'SkyClient': return ``; } } function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { const { provider } = Route.useParams(); - const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, []); - const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, []); + const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, [pkg.created]); + const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, [pkg.updated]); const authors = useCommand('getUsersFromAuthor', () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author)); @@ -163,18 +165,32 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {

Links

-
- +
+ {provider} {' '} Page - {(Object.entries(pkg.links) as [keyof typeof pkg.links, typeof pkg.links[keyof typeof pkg.links]][]).filter(a => a[1]).map(link => - typeof link[1] == "string" - ? - : (link[1] as PackageDonationUrl[]).map(donationLink => ) - - )} + {(Object.entries(pkg.links) as Array<[keyof typeof pkg.links, typeof pkg.links[keyof typeof pkg.links]]>).filter(a => a[1]).map(link => + typeof link[1] == 'string' + ? ( + + ) + : (link[1] as Array).map(donationLink => ( + + )))}
@@ -232,26 +248,33 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { function InstallButton() { const triggerRef = useRef(null); const { provider, slug } = Route.useParams(); - if(!includes(PROVIDERS, provider)) throw new Error("invalid provider") + if (!includes(PROVIDERS, provider)) + throw new Error('invalid provider'); const [open, setOpen] = useState(false); const clusters = useClusters(); const browserContext = useBrowserContext(); - const { data: versions } = usePackageVersions(provider, slug, { + const { data: versions, isLoading: versionsLoading } = usePackageVersions(provider, slug, { mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : [], loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : [], - limit: 1 + limit: 1, }); const version = useMemo(() => { if (!versions || !browserContext.cluster) return undefined; - return versions.items[0] + return versions.items[0]; }, [browserContext.cluster, versions]); - function download() { - if (!version || !browserContext.cluster || !includes(PROVIDERS, provider)) - return false; - downloadPackage(browserContext.cluster, provider, version); - } + const download = useDownloadPackage(browserContext.cluster!, provider, version!, true); + const downloadRef = useRef(download); + useEffect(() => { + downloadRef.current.reset(); + }, [browserContext.cluster]); + + // function download() { + // if (!version || !browserContext.cluster || !includes(PROVIDERS, provider)) + // return false; + // downloadPackage(browserContext.cluster, provider, version); + // } return ( @@ -259,24 +282,39 @@ function InstallButton() { @@ -289,7 +327,7 @@ function InstallButton() { aria-label="cluster" isOpen={open} onOpenChange={setOpen} onSelectionChange={(e) => { - const cluster = clusters?.find(cluster => cluster.id as unknown as number === e); + const cluster = clusters?.find(item => item.id as unknown as number === e); if (cluster) browserContext.setCluster(cluster); }} @@ -359,52 +397,57 @@ function colorForType(type: string) { } } -function Versions(){ - const {provider, slug} = Route.useParams() - if(!includes(PROVIDERS, provider)) throw new Error("invalid provider") - const [offset, setOffset] = useState(0) +function Versions() { + const { provider, slug } = Route.useParams(); + if (!includes(PROVIDERS, provider)) + throw new Error('invalid provider'); + const [offset, setOffset] = useState(0); const { data: versions } = usePackageVersions(provider, slug, { limit: 20, - offset + offset, }); + const pagination = usePagination({ itemsCount: versions?.total as unknown as number, - itemsPerPage: versions?.limit as unknown as number - }) - useEffect(()=>{ - setOffset(pagination.offset) - },[pagination.offset]) - useEffect(()=>{ - pagination.reset() - },[versions?.total]) - return
-
- + itemsPerPage: versions?.limit as unknown as number, + }); + const paginationRef = useRef(pagination); + useEffect(() => { + setOffset(pagination.offset); + }, [pagination.offset]); + useEffect(() => { + paginationRef.current.reset(); + }, [versions?.total]); + return ( +
+
+ +
+ + + + Name + Game Versions + Loaders + Created + Downloads + + + + {versions?.items.map(item => + )} + +
- - - - Name - Game Versions - Loaders - Created - Downloads - - - - {versions?.items.map(item=> - - )} - -
-
+ ); } -function VersionRow({version}:{version:ManagedVersion}){ - const {provider} = Route.useParams() - if(!includes(PROVIDERS, provider)) throw new Error("invalid provider") +function VersionRow({ version }: { version: ManagedVersion }) { + const { provider } = Route.useParams(); + if (!includes(PROVIDERS, provider)) + throw new Error('invalid provider'); return ( - +
@@ -413,22 +456,22 @@ function VersionRow({version}:{version:ManagedVersion}){ {version.display_name} - {version.mc_versions.join(", ")} - {version.loaders.map(upperFirst).join(", ")} + {version.mc_versions.join(', ')} + {version.loaders.map(upperFirst).join(', ')} {formatAsRelative(Date.parse(version.published))} {version.downloads} -