Merge branch 'oneclient/main' into onelauncher/main

This commit is contained in:
LynithDev 2025-07-25 12:16:37 +02:00
commit 0d77715a78
102 changed files with 2430 additions and 1235 deletions

View file

@ -1,11 +1,16 @@
use onelauncher_core::error::{DaoError, LauncherResult};
use std::collections::HashMap;
use onelauncher_core::entity::clusters;
use onelauncher_core::error::LauncherResult;
use tauri::Runtime;
#[taurpc::procedures(path = "oneclient", export_to = "../frontend/src/bindings.gen.ts")]
pub trait OneClientApi {
async fn return_error() -> LauncherResult<()>;
#[taurpc(alias = "openDevTools")]
async fn open_dev_tools<R: Runtime>(webview_window: tauri::WebviewWindow<R>);
#[taurpc(alias = "getClustersGroupedByMajor")]
async fn get_clusters_grouped_by_major() -> LauncherResult<HashMap<u32, Vec<clusters::Model>>>;
}
#[taurpc::ipc_type]
@ -13,15 +18,33 @@ pub struct OneClientApiImpl;
#[taurpc::resolvers]
impl OneClientApi for OneClientApiImpl {
async fn return_error(self) -> LauncherResult<()> {
let err = DaoError::NotFound.into();
Err(err)
}
async fn open_dev_tools<R: Runtime>(self, webview_window: tauri::WebviewWindow<R>) {
#[cfg(feature = "devtools")]
webview_window.open_devtools();
}
async fn get_clusters_grouped_by_major(
self,
) -> LauncherResult<HashMap<u32, Vec<clusters::Model>>> {
let clusters = onelauncher_core::api::cluster::dao::get_all_clusters().await?;
let mut mapped: HashMap<u32, Vec<clusters::Model>> = HashMap::new();
for cluster in clusters {
// Assuming `mc_version` is a String and you have a function to parse major version
let split = &mut cluster.mc_version.split('.');
split.next();
if let Some(major) = split.next() {
// Convert the major version to a u32
let major: u32 = match major.parse() {
Ok(v) => v,
Err(_) => continue, // Skip if parsing fails
};
mapped.entry(major).or_default().push(cluster);
}
}
Ok(mapped)
}
}

View file

@ -1,4 +1,5 @@
use onelauncher_core::{constants::{NATIVE_ARCH, TARGET_OS}, store::Core};
use onelauncher_core::constants::{NATIVE_ARCH, TARGET_OS};
use onelauncher_core::store::Core;
use serde::Serialize;
use specta::Type;
@ -14,7 +15,6 @@ pub struct ProgramInfo {
#[must_use]
pub fn get_program_info() -> ProgramInfo {
ProgramInfo {
launcher_version: Core::get().launcher_version.clone(),
webview_version: tauri::webview_version().unwrap_or_else(|_| "UNKNOWN".into()),

View file

@ -1,3 +1,3 @@
// === API ===
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
pub const DISCORD_CLIENT_ID: &str = "1274084193193955408";
pub const DISCORD_CLIENT_ID: &str = "1274084193193955408";

View file

@ -7,10 +7,12 @@ use onelauncher_core::store::{Core, CoreOptions, Dirs, State};
use tauri::{Emitter, Manager};
use crate::api::commands::OneClientApi;
use crate::oneclient::initialize_oneclient;
pub mod api;
pub mod constants;
pub mod ext;
pub mod oneclient;
#[derive(Clone, serde::Serialize)]
pub struct SingleInstancePayload {
@ -33,9 +35,10 @@ async fn initialize_core() -> LauncherResult<()> {
launcher_website: "https://polyfrost.org/".to_string(),
discord_client_id: None, //Some(constants::DISCORD_CLIENT_ID.to_string()), // disabled for now
fetch_attempts: 3,
logger_filter: Some(
format!("{}={level},onelauncher_core={level}", env!("CARGO_PKG_NAME")),
),
logger_filter: Some(format!(
"{}={level},onelauncher_core={level}",
env!("CARGO_PKG_NAME")
)),
..Default::default()
};
@ -97,13 +100,17 @@ async fn initialize_state(handle: &tauri::AppHandle) -> LauncherResult<()> {
pub async fn run() {
initialize_core().await.expect("failed to initialize core");
let app = initialize_tauri(tauri::Builder::default())
.await
.expect("failed to initialize tauri");
initialize_state(app.handle())
.await
.expect("failed to initialize state");
initialize_oneclient().await;
app.run(|_, _| {});
}

View file

@ -0,0 +1,74 @@
use onelauncher_core::entity::clusters::Model as ClusterModel;
use onelauncher_core::entity::icon::Icon;
use onelauncher_core::entity::loader::GameLoader;
use onelauncher_core::error::LauncherResult;
use onelauncher_core::send_error;
pub async fn initialize_oneclient() {
init_clusters().await;
}
struct OnlineCluster {
mc_major_version: u8,
mc_minor_versions: Vec<u8>,
mc_loaders: Vec<GameLoader>,
}
async fn init_clusters() {
// TODO: use online metadata stuff
let online_clusters: Vec<OnlineCluster> = vec![
OnlineCluster {
mc_major_version: 21,
mc_minor_versions: vec![1, 2, 4, 7],
mc_loaders: vec![GameLoader::Fabric, GameLoader::Forge],
},
OnlineCluster {
mc_major_version: 8,
mc_minor_versions: vec![9],
mc_loaders: vec![GameLoader::Forge],
},
];
for cluster in online_clusters {
for minor_version in cluster.mc_minor_versions {
let mc_version = format!("1.{}.{}", cluster.mc_major_version, minor_version);
for loader in &cluster.mc_loaders {
if let Err(e) = create_cluster_if_not_exist(
&format!("{mc_version} {loader}"),
&mc_version,
*loader,
None,
None,
)
.await
{
send_error!("failed to create cluster for {}: {}", mc_version, e);
}
}
}
}
}
async fn create_cluster_if_not_exist(
name: &str,
mc_version: &str,
mc_loader: GameLoader,
mc_loader_version: Option<&str>,
icon_url: Option<Icon>,
) -> LauncherResult<Option<ClusterModel>> {
let clusters = onelauncher_core::api::cluster::dao::get_clusters_by_version(mc_version).await?;
if clusters.len() > 1 {
return Ok(None);
}
onelauncher_core::api::cluster::create_cluster(
name,
mc_version,
mc_loader,
mc_loader_version,
icon_url,
)
.await
.map(Some)
}

View file

@ -33,6 +33,7 @@
"react": "catalog:",
"react-aria-components": "catalog:",
"react-dom": "catalog:",
"react-just-parallax": "catalog:",
"tailwind-merge": "catalog:",
"tailwind-variants": "catalog:",
"tailwindcss": "catalog:",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

View file

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

View file

@ -1,9 +1,13 @@
/* eslint-disable perfectionist/sort-imports -- nah */
import type { CSSProperties } from 'react';
import { twMerge } from 'tailwind-merge';
import HypixelSkyblockHub from '../assets/backgrounds/HypixelSkyblockHub.png';
import CavesAndCliffs from '@/assets/backgrounds/CavesAndCliffs.jpg';
import HypixelSkyblockHub from '@/assets/backgrounds/HypixelSkyblockHub.png';
const GameBackgrounds = {
HypixelSkyblockHub,
CavesAndCliffs,
};
export type GameBackgroundName = keyof typeof GameBackgrounds;
@ -19,7 +23,7 @@ export function GameBackground({ name, className = '', style = undefined }: Game
return (
<div
className={twMerge(`absolute -z-50 pointer-events-none inset-0 bg-cover bg-center`, className)}
className={twMerge(`pointer-events-none inset-0 bg-cover bg-center bg-no-repeat`, className)}
style={{ backgroundImage: `url(${BackgroundImage})`, ...style }}
/>
);

View file

@ -1,5 +1,5 @@
import type { ButtonProps } from '@onelauncher/common/components';
import type { LinkProps } from '@tanstack/react-router';
import type { LinkProps, RegisteredRouter } from '@tanstack/react-router';
import LauncherLogo from '@/assets/logos/oneclient.svg?react';
import { Button } from '@onelauncher/common/components';
import { Link } from '@tanstack/react-router';
@ -19,9 +19,9 @@ export function Navbar() {
</div>
<div className="flex flex-1 items-center justify-center pointer-events-none gap-6">
<NavbarLink to="/app">Home</NavbarLink>
<NavbarLink to="/app/clusters">Clusters</NavbarLink>
<NavbarLink to="/">Accounts</NavbarLink>
<NavbarLink to=".">Home</NavbarLink>
<NavbarLink to="./clusters">Clusters</NavbarLink>
<NavbarLink to=".">Accounts</NavbarLink>
</div>
<div className="flex flex-1 items-center justify-end gap-2 pointer-events-none">
@ -54,7 +54,7 @@ export function Navbar() {
function NavbarLink({
to,
children,
}: LinkProps) {
}: LinkProps<'a', RegisteredRouter, '/app'>) {
return (
<div className="flex-1 flex justify-center items-center">
<Link
@ -65,8 +65,9 @@ function NavbarLink({
className: 'text-fg-primary font-semibold partial-underline-75% pointer-events-none',
}}
className="text-center text-2lg transition-all duration-100 after:duration-100 after:transition-all"
from="/app"
inactiveProps={{
className: 'text-fg-secondary font-normal partial-underline-0% hover:partial-underline-50% hover:font-medium hover:text-fg-secondary-hover after:text-fg-secondary-hover pointer-events-auto',
className: 'text-fg-secondary font-normal partial-underline-0% hover:partial-underline-60% hover:text-fg-secondary-hover after:text-fg-secondary-hover pointer-events-auto',
}}
to={to}
>

View file

@ -1,23 +1,32 @@
import type { MotionValue } from 'motion/react';
import useAppShellStore from '@/stores/appShellStore';
import { getLocationName } from '@/utils/locationMapping';
import type { HTMLMotionProps, MotionValue } from 'motion/react';
import { Button } from '@onelauncher/common/components';
import { Outlet, useNavigate } from '@tanstack/react-router';
import { useCanGoBack, useRouter } from '@tanstack/react-router';
import { ArrowLeftIcon } from '@untitled-theme/icons-react';
import { motion, useScroll, useTransform } from 'motion/react';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import React, { useRef } from 'react';
import React, { useCallback, useRef } from 'react';
import { twMerge } from 'tailwind-merge';
export function SheetPage({
headerSmall,
headerLarge,
children,
scrollContainerRef,
}: {
headerSmall: React.ReactNode;
headerLarge: React.ReactNode;
children?: React.ReactNode;
scrollContainerRef?: React.RefObject<HTMLElement | null>;
}) {
const scrollContainer = useRef<HTMLElement>(null);
const headerLargeRef = useRef<HTMLDivElement>(null);
const setScrollContainerRef = useCallback((el: HTMLElement | null) => {
scrollContainer.current = el;
if (scrollContainerRef)
scrollContainerRef.current = el;
}, [scrollContainerRef]);
const { scrollYProgress } = useScroll({
axis: 'y',
container: scrollContainer,
@ -48,20 +57,13 @@ export function SheetPage({
</motion.div>
</div>
<OverlayScrollbarsComponent className="flex flex-col flex-1 mask-t-from-97% pt-4 relative" ref={ref => scrollContainer.current = ref?.osInstance()?.elements().content ?? null}>
<div className="flex flex-col mx-12 gap-4">
<OverlayScrollbarsComponent className="flex flex-col flex-1 mask-t-from-97% pt-4 relative" ref={ref => setScrollContainerRef(ref?.osInstance()?.elements().content ?? null)}>
<div className="flex flex-col mx-12 gap-4 flex-1 min-h-full">
<motion.div className="w-full" ref={headerLargeRef} style={{ opacity: largeOpacity }}>
{/* <headerLarge ref={headerLargeRef as React.RefObject<HTMLDivElement>} /> */}
{headerLarge}
</motion.div>
<motion.div
animate={{ bottom: 0 }}
className="relative flex flex-col px-10 py-6 mb-8 bg-page-elevated rounded-2xl"
initial={{ bottom: '-80px' }}
>
<Outlet />
</motion.div>
{children}
</div>
</OverlayScrollbarsComponent>
</div>
@ -69,29 +71,45 @@ export function SheetPage({
);
}
SheetPage.Content = ({
children,
className,
...rest
}: Omit<HTMLMotionProps<'div'>, 'animate' | 'initial'>) => (
<motion.div
animate={{
bottom: 0,
opacity: 1,
}}
className={twMerge('relative flex flex-col px-10 py-6 last:mb-8 h-full bg-page-elevated rounded-2xl', className)}
exit={{
opacity: 0,
}}
initial={{
opacity: 0,
}}
transition={{ duration: 0.25 }}
{...rest}
>
{children}
</motion.div>
);
function GoBackButton() {
const prevLocation = useAppShellStore(state => state.prevLocation);
const navigate = useNavigate();
const router = useRouter();
const canGoBack = useCanGoBack();
const onClick = () => {
if (!prevLocation)
return;
navigate({
to: prevLocation.pathname,
search: prevLocation.search,
hash: prevLocation.hash,
state: prevLocation.state,
});
};
if (!prevLocation)
if (!canGoBack)
return undefined;
const onClick = () => {
router.history.back();
};
return (
<Button color="ghost" onClick={onClick}>
<Button color="ghost" onPress={onClick}>
<ArrowLeftIcon height={20} />
<span className="h-5">{getLocationName(prevLocation.pathname)}</span>
<span className="h-5">Back</span>
</Button>
);
}
@ -111,21 +129,21 @@ function HeaderBackgrounds({
}}
>
<div
className="absolute -z-10 -top-20 h-50 left-0 w-screen animate-fade animate-duration-200" style={{
className="absolute -z-10 -top-20 h-50 left-0 w-screen" style={{
background: 'rgba(0, 0, 0, 0.20)',
}}
>
</div>
<div
className="relative h-50 top-0 left-0 w-screen animate-fade animate-duration-200"
className="relative h-50 top-0 left-0 w-screen"
style={{
background: 'linear-gradient(180deg, rgba(17, 23, 28, 0.00) 0%, #11171C 60.1%)',
}}
>
</div>
<div className="relative top-0 left-0 w-full h-full bg-page animate-fade animate-duration-200"></div>
<div className="relative top-0 left-0 w-full h-full bg-page "></div>
</motion.div>
);
}

View file

@ -0,0 +1,52 @@
import type { LinkProps } from '@tanstack/react-router';
import { Link } from '@tanstack/react-router';
import { twMerge } from 'tailwind-merge';
export function TabList({
className,
children,
floating = false,
ref,
}: React.HTMLAttributes<HTMLDivElement> & {
floating?: boolean;
ref?: React.Ref<HTMLDivElement>;
}) {
return (
<div
className={twMerge(
'flex flex-row gap-2 border border-transparent bg-page-elevated transition-all',
floating
? 'px-6 mx-4 py-3 shadow-lg border-ghost-overlay rounded-xl'
: 'px-10 py-6 rounded-2xl',
className,
)} ref={ref}
>
{children}
</div>
);
}
export function Tab({
to,
children,
}: Omit<LinkProps, 'activeOptions' | 'activeProps' | 'inactiveProps' | 'className'>) {
return (
<div className="relative flex justify-center items-center">
<Link
activeOptions={{
exact: true,
}}
activeProps={{
className: 'text-fg-primary font-semibold partial-underline-75% pointer-events-none',
}}
className="text-center text-lg transition-all duration-100 after:duration-100 after:transition-all"
inactiveProps={{
className: 'text-fg-secondary font-normal partial-underline-0% hover:partial-underline-60% hover:text-fg-secondary-hover after:text-fg-secondary-hover pointer-events-auto',
}}
to={to}
>
{children}
</Link>
</div>
);
}

View file

@ -2,3 +2,4 @@ export * from './GameBackground';
export * from './Loader';
export * from './Navbar';
export * from './Spinner';
export * from './Tabs';

View file

@ -0,0 +1,27 @@
import { SheetPage } from '@/components/SheetPage';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/app/cluster/overview')({
component: RouteComponent,
beforeLoad(ctx) {
console.log('beforeLoad overview', ctx.search);
},
});
function RouteComponent() {
const { cluster } = Route.useRouteContext();
return (
<SheetPage.Content>
<div className="flex flex-col gap-4">
<h1 className="text-2xl font-semibold">Cluster Overview</h1>
<p>Welcome to the cluster overview page. Here you can find information about your clusters.</p>
<div style={{ height: '1500px' }}>
</div>
<p>test</p>
{/* Additional content can be added here */}
</div>
</SheetPage.Content>
);
}

View file

@ -0,0 +1,132 @@
import { Tab, TabList } from '@/components';
import { SheetPage } from '@/components/SheetPage';
import { bindings } from '@/main';
import { prettifyLoader } from '@/utils/loaders';
import { getVersionInfoOrDefault } from '@/utils/versionMap';
import { Button } from '@onelauncher/common/components';
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
import { useMotionValueEvent, useScroll } from 'motion/react';
import { useRef, useState } from 'react';
export interface ClusterRouteSearchParams {
clusterId: number;
}
export const Route = createFileRoute('/app/cluster')({
component: RouteComponent,
validateSearch: (search): ClusterRouteSearchParams => {
console.log('validateSearch', search);
return {
clusterId: Number(search.clusterId),
};
},
async beforeLoad({ context, search }) {
console.log('beforeLoad', search);
if (!search.clusterId)
throw redirect({ to: '/app/clusters', from: '/app/cluster' });
const query = context.queryClient.ensureQueryData({
queryKey: ['getClusterById', search.clusterId],
queryFn: () => bindings.core.getClusterById(search.clusterId as unknown as bigint),
});
const cluster = await query;
if (!cluster)
throw redirect({ to: '/app/clusters', from: '/app/cluster' });
return {
cluster,
};
},
});
function RouteComponent() {
const tabListRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLElement>(null);
const { scrollYProgress } = useScroll({
axis: 'y',
container: scrollContainerRef,
target: tabListRef,
layoutEffect: false,
offset: ['end end', 'start start'],
});
const [floating, setFloating] = useState(false);
useMotionValueEvent(scrollYProgress, 'change', (v) => {
setFloating(v >= 0.9);
});
const search = Route.useSearch();
return (
<SheetPage
headerLarge={<HeaderLarge />}
headerSmall={<HeaderSmall />}
scrollContainerRef={scrollContainerRef}
>
<TabList className="sticky top-0 z-10 shadow-lg" floating={floating} ref={tabListRef}>
<Tab from={Route.id} search={search} to="/app/cluster/overview">Overview</Tab>
<Tab>Logs</Tab>
</TabList>
<div className="relative">
<Outlet />
</div>
</SheetPage>
);
}
function HeaderLarge() {
const { cluster } = Route.useRouteContext();
const versionInfo = getVersionInfoOrDefault(cluster.mc_version);
const launch = () => {
bindings.core.launchCluster(cluster.id as unknown as bigint, null);
};
return (
<div className="flex flex-row justify-between items-end gap-16">
<div className="flex flex-col gap-2">
<div className="flex flex-row gap-2 flex-wrap">
{versionInfo.tags.map(tag => (
<span className="text-sm font-medium text-fg-secondary bg-component-bg/40 px-2 py-1 rounded" key={tag}>
{tag}
</span>
))}
</div>
<h1 className="text-3xl font-semibold">{prettifyLoader(cluster.mc_loader)} {cluster.mc_version}</h1>
<p className="text-md font-medium text-fg-secondary">{versionInfo.longDescription}</p>
</div>
<Button
color="primary"
onPress={launch}
size="large"
>
Launch
</Button>
</div>
);
}
function HeaderSmall() {
const { cluster } = Route.useRouteContext();
const launch = () => {
bindings.core.launchCluster(cluster.id as unknown as bigint, null);
};
return (
<div className="flex flex-row justify-between items-center h-full">
<h1 className="text-2lg h-full font-medium">{prettifyLoader(cluster.mc_loader)} {cluster.mc_version}</h1>
<Button
color="primary"
onPress={launch}
>
Launch
</Button>
</div>
);
}

View file

@ -0,0 +1,269 @@
import type { GameLoader } from '@/bindings.gen';
import { GameBackground } from '@/components';
import { SheetPage } from '@/components/SheetPage';
import { bindings } from '@/main';
import useClusterStore from '@/stores/clusterStore';
import { prettifyLoader } from '@/utils/loaders';
import { getVersionInfo, getVersionInfoOrDefault, parseMcVersion } from '@/utils/versionMap';
import { useCommandSuspense } from '@onelauncher/common';
import { Button, Dropdown } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { ArrowRightIcon } from '@untitled-theme/icons-react';
import { useCallback, useMemo, useState } from 'react';
import { twMerge } from 'tailwind-merge';
export const Route = createFileRoute('/app/clusters')({
component: RouteComponent,
});
function RouteComponent() {
const prevMinorVersions = useClusterStore(state => state.minorVersions);
const setMinorVersion = useClusterStore(state => state.setMinorVersion);
const modLoaders = useClusterStore(state => state.modLoaders);
const setModLoader = useClusterStore(state => state.setModLoader);
const navigate = Route.useNavigate();
const { data: clusters } = useCommandSuspense('getClustersGroupedByMajor', bindings.oneclient.getClustersGroupedByMajor);
const loadMinorVersion = useCallback((major: number) => {
const existing = prevMinorVersions.find(v => v.major === major);
if (existing)
return existing.minor;
return undefined;
}, [prevMinorVersions]);
const [majorVersion, setMajorVersion] = useState<number>(() => {
const keys = Object.keys(clusters);
if (keys.length === 0)
return 0;
return Number.parseInt(keys[0]);
});
const [activeMinorVersion, setActiveMinorVersion] = useState<number | undefined>();
const minorVersions = useMemo(() => {
const list = clusters[majorVersion];
if (!list || list.length === 0)
return [];
const versions = list.map(c => parseMcVersion(c.mc_version)?.minor).filter((v): v is number => v !== undefined);
const sorted = Array.from(new Set(versions)).sort((a, b) => a - b);
// attempt to load the minor version from localStorage
const storedMinor = loadMinorVersion(majorVersion) || sorted[0];
setActiveMinorVersion(storedMinor);
return sorted;
}, [clusters, loadMinorVersion, majorVersion]);
const modLoader = useMemo(() => {
if (!activeMinorVersion)
return undefined;
return modLoaders[`${majorVersion}.${activeMinorVersion}`];
}, [modLoaders, majorVersion, activeMinorVersion]);
const loaders = useMemo(() => {
const list = clusters[majorVersion];
if (!list || list.length === 0)
return [];
const loadersSet: Set<GameLoader> = new Set();
for (const cluster of list)
if (cluster.mc_loader !== 'vanilla')
loadersSet.add(cluster.mc_loader);
return Array.from(loadersSet).sort();
}, [clusters, majorVersion]);
const cluster = useMemo(() => {
const list = clusters[majorVersion];
if (!activeMinorVersion)
return list?.[0];
return list?.find((c) => {
const version = c.mc_version.endsWith(activeMinorVersion.toString());
const loaderCheck = modLoader ? c.mc_loader === modLoader : true;
return version && loaderCheck;
});
}, [clusters, majorVersion, activeMinorVersion, modLoader]);
const versionInfo = useMemo(() => getVersionInfoOrDefault(cluster?.mc_version), [cluster]);
return (
<SheetPage
headerLarge={<HeaderLarge />}
headerSmall={<HeaderSmall />}
>
<div className="relative flex flex-row gap-4">
<div className="flex flex-col flex-1">
<div className="grid grid-cols-2 2xl:grid-cols-3 gap-4">
{Object.keys(clusters).map((majorStr) => {
const major = Number.parseInt(majorStr, 10);
const isSelected = major === majorVersion;
const info = getVersionInfo(major);
if (!info)
return undefined;
return (
<ClusterEntry
isSelected={isSelected}
key={major}
major_version={major}
onClick={() => setMajorVersion(major)}
tags={[...new Set(clusters[major]?.flatMap(c => prettifyLoader(c.mc_loader)))]}
/>
);
})}
</div>
</div>
<SheetPage.Content className="sticky top-8 w-86 h-full flex flex-col p-2 gap-2 outline outline-ghost-overlay">
<GameBackground className="aspect-video w-full rounded-xl outline-2 outline-ghost-overlay" name={versionInfo.backgroundName} />
<div className="flex flex-col px-4 pt-2 pb-4 gap-2">
<h2 className="text-xxl font-medium">Version {cluster?.mc_version}</h2>
<p className="text-sm text-fg-secondary">{versionInfo.longDescription}</p>
{minorVersions.length > 1 && (
<div className="flex flex-row items-center justify-between mt-3">
<p>Minor Version</p>
<Dropdown
aria-label="Minor Version Dropdown"
onSelectionChange={(selected) => {
setActiveMinorVersion(Number(selected));
setMinorVersion(majorVersion, Number(selected));
}}
selectedKey={activeMinorVersion}
>
{minorVersions.map((minorVersion) => {
const fullVer = `${versionInfo.prettyName}.${minorVersion}`;
return (
<Dropdown.Item
id={minorVersion}
key={minorVersion}
textValue={fullVer}
>
{fullVer}
</Dropdown.Item>
);
})}
</Dropdown>
</div>
)}
{loaders.length > 1 && (
<div className="flex flex-row items-center justify-between mb-2">
<p>Mod Loader</p>
<Dropdown
aria-label="Mod Loader Dropdown"
onSelectionChange={(selected) => {
if (!activeMinorVersion)
return;
setModLoader(`${majorVersion}.${activeMinorVersion}`, selected as GameLoader);
}}
selectedKey={activeMinorVersion ? (modLoaders[`${majorVersion}.${activeMinorVersion}`] ?? loaders[0]) : loaders[0]}
>
{loaders.map(loader => (
<Dropdown.Item
id={loader}
key={loader}
>
{prettifyLoader(loader)}
</Dropdown.Item>
))}
</Dropdown>
</div>
)}
<div className="w-full flex flex-row gap-4 mt-2">
<Button className="flex-1" size="large">Launch</Button>
<Button
className="flex-1"
color="secondary"
onPress={() => {
if (!cluster)
return;
navigate({
to: `/app/cluster/overview`,
search: {
clusterId: cluster.id as unknown as number,
},
});
}}
size="large"
>
View
<ArrowRightIcon />
</Button>
</div>
</div>
</SheetPage.Content>
</div>
</SheetPage>
);
}
function HeaderLarge({ ref }: { ref?: React.Ref<HTMLDivElement> }) {
return (
<div className="flex flex-row justify-between items-end gap-8" ref={ref}>
<div className="flex flex-col">
<h1 className="text-3xl font-semibold">Clusters</h1>
<p className="text-md font-medium text-fg-secondary">Something something in corporate style fashion about picking your preferred gamemodes and versions and optionally loader so that oneclient can pick something for them</p>
</div>
</div>
);
}
function HeaderSmall() {
return (
<h1 className="text-2lg h-full font-medium">Clusters</h1>
);
}
function ClusterEntry({
isSelected,
major_version,
onClick,
tags,
}: {
isSelected: boolean;
major_version: number;
onClick: () => unknown;
tags: Array<string>;
}) {
const info = getVersionInfo(major_version);
if (!info)
return undefined;
return (
<div className={twMerge('flex flex-col justify-between relative aspect-video transition-[filter] px-4', !isSelected && 'brightness-70 grayscale-25 hover:brightness-80 hover:grayscale-0')} key={major_version} onClick={onClick}>
<GameBackground className={twMerge('opacity-90 absolute w-full h-full -z-10 rounded-md transition-[outline] outline-2', isSelected ? 'outline-brand' : 'outline-ghost-overlay')} name={info.backgroundName} />
<div
className="absolute -z-10 top-0 left-0 w-full h-full rounded-md overflow-hidden " style={{
background: 'radial-gradient(48.93% 47.95% at 49.92% 28.42%, rgba(0, 0, 0, 0.00) 0%, rgba(0, 0, 0, 0.30) 52.26%, rgba(0, 0, 0, 0.60) 100%)',
}}
>
</div>
<div className="flex flex-row flex-wrap gap-2 mt-3">
{info.tags.concat(tags).map(tag => (
<div className="bg-fg-primary text-brand px-2 text-sm py-0.5 rounded-full" key={tag}>
{tag}
</div>
))}
</div>
<h3 className="text-fg-primary text-2xl font-semibold mb-2">Version {info.prettyName}</h3>
</div>
);
}

View file

@ -1,17 +0,0 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/app/clusters/')({
component: RouteComponent,
});
function RouteComponent() {
return (
<div>
<div>Hello "/app/clusters/"!</div>
<div className="w-px h-screen bg-red-100/10"></div>
<div>Hello "/app/clusters/"!</div>
</div>
);
}

View file

@ -1,45 +0,0 @@
import { SheetPage } from '@/components/SheetPage';
import { Button } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/app/clusters')({
component: RouteComponent,
});
function RouteComponent() {
return (
<SheetPage
headerLarge={<HeaderLarge />}
headerSmall={<HeaderSmall />}
/>
);
}
function HeaderLarge({ ref }: { ref?: React.Ref<HTMLDivElement> }) {
return (
<div className="flex flex-row justify-between items-end" ref={ref}>
<div className="flex flex-col">
<h1 className="text-3xl font-semibold">Header</h1>
<p className="text-md font-medium text-fg-secondary">Tailored for Hypixel Skyblock.</p>
</div>
<Button color="primary" size="large">
Launch
</Button>
</div>
);
}
function HeaderSmall() {
return (
<div className="flex flex-row w-full items-center justify-between">
<div className="flex flex-row items-center gap-4">
<h1 className="text-2lg h-full font-medium">Header</h1>
</div>
<Button color="primary">
Launch
</Button>
</div>
);
}

View file

@ -1,14 +1,16 @@
import type { ClusterModel } from '@/bindings.gen';
import type { GameLoader } from '@/bindings.gen';
import type { HTMLAttributes } from 'react';
import { GameBackground } from '@/components';
import { bindings } from '@/main';
import { prettifyLoader } from '@/utils/loaders';
import { animations, transitions } from '@/utils/motion';
import { getVersionInfo } from '@/utils/versionMap';
import { useCommandSuspense } from '@onelauncher/common';
import { Button } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { DotsGridIcon, Settings04Icon } from '@untitled-theme/icons-react';
import { DotsGridIcon, PlusIcon, Settings04Icon } from '@untitled-theme/icons-react';
import { motion } from 'motion/react';
import { MouseParallax } from 'react-just-parallax';
import { twMerge } from 'tailwind-merge';
export const Route = createFileRoute('/app/')({
@ -16,30 +18,32 @@ export const Route = createFileRoute('/app/')({
});
function RouteComponent() {
const { data: clusters } = useCommandSuspense<Array<ClusterModel | undefined>>(
const { data: clusters } = useCommandSuspense(
'getClusters',
bindings.core.getClusters,
{
select: ((data: Array<ClusterModel>) => {
const sorted = data.sort((a, b) => {
if (a.last_played && b.last_played)
return new Date(b.last_played).getTime() - new Date(a.last_played).getTime();
else if (a.last_played)
return -1; // a has last_played, b does not
else if (b.last_played)
return 1; // b has last_played, a does not
select(data) {
const sorted = data
.filter(cluster => cluster.last_played !== null)
.sort((a, b) => {
if (a.last_played && b.last_played)
return new Date(b.last_played).getTime() - new Date(a.last_played).getTime();
else if (a.last_played)
return -1; // a has last_played, b does not
else if (b.last_played)
return 1; // b has last_played, a does not
const aVersion = a.mc_version.replaceAll('.', '');
const bVersion = b.mc_version.replaceAll('.', '');
return Number.parseInt(bVersion) - Number.parseInt(aVersion);
});
const aVersion = a.mc_version.replaceAll('.', '');
const bVersion = b.mc_version.replaceAll('.', '');
return Number.parseInt(bVersion) - Number.parseInt(aVersion);
});
return [
sorted[0],
sorted[1],
undefined,
sorted[3],
];
}) as (data: Array<ClusterModel | undefined>) => Array<ClusterModel | undefined>,
},
},
);
@ -51,14 +55,25 @@ function RouteComponent() {
<div className="flex flex-row justify-center items-center gap-2">
<Button size="large">Launch</Button>
<Button color="ghost" size="iconLarge"><Settings04Icon /></Button>
<Button color="ghost" size="iconLarge">
<Settings04Icon />
</Button>
</div>
</motion.div>
<motion.div {...animations.slideInUp} className="flex flex-row transition-[height] h-52 gap-6">
{clusters.map((cluster, index) => (
<RecentsCard key={cluster?.folder_name ?? index} version={cluster?.mc_version} />
))}
{clusters.map((cluster) => {
// eslint-disable-next-line ts/no-unnecessary-condition -- rule broken
return cluster === undefined
? (
<Card blur className="group flex justify-center items-center" key={`undefined_${Math.random()}`}>
<PlusIcon className="text-fg-secondary opacity-40 stroke-2 group-hover:text-fg-primary group-hover:opacity-90" height={64} width={64} />
</Card>
)
: (
<RecentsCard key={cluster.folder_name} loader={cluster.mc_loader} version={cluster.mc_version} />
);
})}
<Card blur className="flex flex-col justify-center items-center max-w-24">
<DotsGridIcon height={48} width={48} />
@ -69,30 +84,34 @@ function RouteComponent() {
}
interface RecentsCardProps {
version: string | undefined;
version: string;
loader: GameLoader;
}
const BLUR = '30px';
function RecentsCard({ version }: RecentsCardProps) {
function RecentsCard({ version, loader }: RecentsCardProps) {
const versionInfo = getVersionInfo(version);
if (!versionInfo)
return <Card blur>Unknown Version</Card>;
return (
<Card blur={versionInfo === undefined}>
{versionInfo && (
<div className="flex w-full h-full justify-start items-end px-6 py-3">
<GameBackground className="-z-10" name={versionInfo.backgroundName} />
<Card>
<div className="flex w-full h-full justify-start items-end px-6 py-3">
<MouseParallax isAbsolutelyPositioned strength={0.005} zIndex={-10}>
<GameBackground className="absolute left-0 top-0 w-full h-full scale-110" name={versionInfo.backgroundName} />
</MouseParallax>
<div
className="absolute top-0 left-0 -z-10 w-full h-full"
style={{
background: 'linear-gradient(180deg, rgba(25, 25, 25, 0.00) 24.52%, rgba(17, 17, 21, 0.75) 65%)',
}}
>
</div>
<h4 className="text-3xl font-semibold">{version}</h4>
<div
className="absolute top-0 left-0 -z-10 w-full h-full"
style={{
background: 'linear-gradient(180deg, rgba(25, 25, 25, 0.00) 24.52%, rgba(17, 17, 21, 0.75) 65%)',
}}
>
</div>
)}
<h4 className="text-3xl font-semibold">{version} {prettifyLoader(loader)}</h4>
</div>
</Card>
);
}

View file

@ -3,9 +3,8 @@ import { LoaderSuspense, Navbar } from '@/components';
import { GameBackground } from '@/components/GameBackground';
import useAppShellStore from '@/stores/appShellStore';
import { AnimatedOutlet } from '@onelauncher/common/components';
import { createFileRoute, useRouter } from '@tanstack/react-router';
import { AnimatePresence } from 'motion/react';
import { useEffect } from 'react';
import { createFileRoute } from '@tanstack/react-router';
import { MouseParallax } from 'react-just-parallax';
export const Route = createFileRoute('/app')({
component: RouteComponent,
@ -26,7 +25,7 @@ function RouteComponent() {
animate: { opacity: 0 },
}}
from={Route.id}
transition={{ duration: 0.3, bounce: 0.1, power: 0.2, type: 'spring' }}
transition={{ duration: 0.25, bounce: 0.1, power: 0.2, type: 'spring' }}
/>
</div>
</AppShell>
@ -37,22 +36,9 @@ function RouteComponent() {
function AppShell({
children,
}: PropsWithChildren) {
const router = useRouter();
const setPrevLocation = useAppShellStore(state => state.setPrevLocation);
useEffect(() => {
const unsub = router.subscribe('onBeforeNavigate', (e) => {
setPrevLocation(e.fromLocation ?? null);
});
return () => unsub();
}, [router, setPrevLocation]);
return (
<div className="flex flex-col h-full w-full">
<AnimatePresence>
<BackgroundGradient />
</AnimatePresence>
<BackgroundGradient />
<Navbar />
@ -70,7 +56,7 @@ function BackgroundGradient() {
return undefined;
return (
<div>
<div className="relative">
{/* Linear black gradient: left -> right */}
<div
className="absolute top-0 left-0 w-screen h-screen -z-10"
@ -96,7 +82,12 @@ function BackgroundGradient() {
>
</div>
<GameBackground name="HypixelSkyblockHub" />
<MouseParallax isAbsolutelyPositioned strength={0.01} zIndex={-50}>
<GameBackground
className="absolute left-0 top-0 w-screen h-screen scale-110"
name="HypixelSkyblockHub"
/>
</MouseParallax>
</div>
);
}

View file

@ -1,22 +1,17 @@
import type { ParsedLocation } from '@tanstack/react-router';
import { create } from 'zustand';
export interface AppShellStore {
background: 'gradientOverlay' | 'none';
prevLocation: ParsedLocation | null;
}
export interface AppShellStoreActions {
setBackground: (background: AppShellStore['background']) => void;
setPrevLocation: (location: ParsedLocation | null) => void;
}
export const useAppShellStore = create<AppShellStore & AppShellStoreActions>()(set => ({
background: 'gradientOverlay',
prevLocation: null,
setBackground: background => set({ background }),
setPrevLocation: location => set({ prevLocation: location }),
}));
export default useAppShellStore;

View file

@ -0,0 +1,45 @@
import type { GameLoader } from '@/bindings.gen';
import type { ParsedMcVersion } from '@/utils/versionMap';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type MajorVersion = number;
export type MinorVersion = number;
export interface ClusterStore {
modLoaders: Record<`${MajorVersion}.${MinorVersion}`, GameLoader>;
minorVersions: Array<ParsedMcVersion>;
}
export interface ClusterStoreActions {
setModLoader: (version: `${MajorVersion}.${MinorVersion}`, loader: GameLoader) => void;
setMinorVersion: (major: number, minor: number) => void;
}
export const useClusterStore = create<ClusterStore & ClusterStoreActions>()(persist(
set => ({
modLoaders: {},
minorVersions: [],
setModLoader: (version, loader) => set(state => ({
modLoaders: {
...state.modLoaders,
[version]: loader,
},
})),
setMinorVersion: (major, minor) => set((state) => {
const existing = state.minorVersions.find(v => v.major === major);
if (existing)
existing.minor = minor;
else
state.minorVersions.push({ major, minor });
return { minorVersions: [...state.minorVersions] };
}),
}),
{
name: 'oneclient:cluster_list',
},
));
export default useClusterStore;

View file

@ -0,0 +1,18 @@
import type { GameLoader } from '@/bindings.gen';
export function prettifyLoader(loader: GameLoader): string {
switch (loader) {
case 'fabric':
return 'Fabric';
case 'forge':
return 'Forge';
case 'neoforge':
return 'NeoForge';
case 'quilt':
return 'Quilt';
case 'vanilla':
return 'Vanilla';
case 'legacyfabric':
return 'Legacy Fabric';
}
}

View file

@ -1,39 +0,0 @@
import type { FileRouteTypes } from '@/routeTree.gen';
const mapping: Partial<Record<FileRouteTypes['to'], string>> = {
'/app': 'Home',
'/app/clusters': 'Clusters',
};
export function getLocationName(
to: FileRouteTypes['to'] | (string & {}) | undefined | null,
): string | undefined {
if (!to)
return undefined;
if (to in mapping)
return mapping[to as keyof typeof mapping];
const mostSimilarPath = getMostSimilarPathMapping(to);
return mostSimilarPath;
}
function getMostSimilarPathMapping(
to: string,
): FileRouteTypes['to'] | undefined {
let path = to;
while (path !== '') {
if (Object.prototype.hasOwnProperty.call(mapping, path))
return path as FileRouteTypes['to'];
const lastSlash = path.lastIndexOf('/');
if (lastSlash === 0)
return Object.prototype.hasOwnProperty.call(mapping, '/') ? '/' : undefined;
path = path.substring(0, lastSlash);
}
return undefined;
}

View file

@ -2,17 +2,33 @@ import type { GameBackgroundName } from '@/components';
export interface VersionInfo {
backgroundName: GameBackgroundName;
description: string;
prettyName: string;
shortDescription: string;
longDescription: string;
tags: Array<string>;
}
const VERSION_MAP: Record<number, VersionInfo> = {
[-1]: {
backgroundName: 'HypixelSkyblockHub',
description: 'Unknown Version',
shortDescription: 'Unknown Version',
longDescription: 'This version is not recognized or does not have specific information available.',
prettyName: '0.0',
tags: [],
},
8: {
backgroundName: 'HypixelSkyblockHub',
description: 'The Bountiful Update',
prettyName: '1.8',
shortDescription: 'The Bountiful Update',
longDescription: 'The Bountiful Update is a major update that introduces new features, items, and gameplay mechanics to enhance the Minecraft experience. It focuses on expanding the game world and providing players with new challenges and adventures.',
tags: ['Legacy', 'Bountiful Update'],
},
21: {
backgroundName: 'CavesAndCliffs',
prettyName: '1.21',
shortDescription: 'The Tricky Trials Update',
longDescription: `Minecraft's 1.21 update, known as "Tricky Trials," primarily focuses on combat adventures and tinkering, introducing trial chambers, new copper block variants, a new crafting tool, and a new weapon. It also features new hostile mobs, paintings, and gameplay enhancements.`,
tags: ['Tricky Trials'],
},
} as const;
@ -20,13 +36,12 @@ export function getVersionInfo(version: number | string | null | undefined): Ver
if (version === undefined || version === null)
return undefined;
let majorVersion;
let majorVersion: number | undefined;
if (typeof version === 'string') {
const parts = version.split('.');
if (parts.length >= 2)
majorVersion = Number.parseInt(parts[1], 10);
}
if (typeof version === 'string')
majorVersion = parseMcVersion(version)?.major;
else if (typeof version === 'number')
majorVersion = version;
if (majorVersion)
return VERSION_MAP[majorVersion];
@ -34,7 +49,29 @@ export function getVersionInfo(version: number | string | null | undefined): Ver
return undefined;
}
export function getVersionInfoOrDefault(version: number | string): VersionInfo {
export function getVersionInfoOrDefault(version: number | string | null | undefined): VersionInfo {
const info = getVersionInfo(version);
return info ?? VERSION_MAP[-1];
}
export interface ParsedMcVersion {
major: number;
minor: number | undefined;
}
export function parseMcVersion(version: string | null | undefined): ParsedMcVersion | undefined {
if (version === undefined || version === null)
return undefined;
const parts = version.split('.');
if (parts.length <= 1) // we need a.b.c where c is optional
return undefined;
const major = Number.parseInt(parts[1], 10);
const minor = parts.length > 1 ? Number.parseInt(parts[2], 10) : undefined;
return {
major,
minor: Number.isNaN(minor) ? undefined : minor,
};
}

View file

@ -7,7 +7,10 @@ pub trait OneLauncherApi {
async fn open_dev_tools<R: Runtime>(webview_window: tauri::WebviewWindow<R>);
async fn set_window_style<R: Runtime>(webview_window: tauri::WebviewWindow<R>, decorations: bool);
async fn set_window_style<R: Runtime>(
webview_window: tauri::WebviewWindow<R>,
decorations: bool,
);
}
#[taurpc::ipc_type]
@ -15,7 +18,6 @@ pub struct OneLauncherApiImpl;
#[taurpc::resolvers]
impl OneLauncherApi for OneLauncherApiImpl {
async fn return_error(self) -> LauncherResult<()> {
let err = DaoError::NotFound.into();
Err(err)
@ -26,7 +28,11 @@ impl OneLauncherApi for OneLauncherApiImpl {
webview_window.open_devtools();
}
async fn set_window_style<R: Runtime>(self, webview_window: tauri::WebviewWindow<R>, decorations: bool) {
async fn set_window_style<R: Runtime>(
self,
webview_window: tauri::WebviewWindow<R>,
decorations: bool,
) {
webview_window.set_decorations(decorations).ok();
}
}

View file

@ -1,4 +1,5 @@
use onelauncher_core::{constants::{NATIVE_ARCH, TARGET_OS}, store::Core};
use onelauncher_core::constants::{NATIVE_ARCH, TARGET_OS};
use onelauncher_core::store::Core;
use serde::Serialize;
use specta::Type;
@ -14,7 +15,6 @@ pub struct ProgramInfo {
#[must_use]
pub fn get_program_info() -> ProgramInfo {
ProgramInfo {
launcher_version: Core::get().launcher_version.clone(),
webview_version: tauri::webview_version().unwrap_or_else(|_| "UNKNOWN".into()),

View file

@ -1,3 +1,3 @@
// === API ===
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
pub const DISCORD_CLIENT_ID: &str = "1274084193193955408";
pub const DISCORD_CLIENT_ID: &str = "1274084193193955408";

View file

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

View file

@ -15,6 +15,17 @@
--radius-4xl: 2rem;
}
h1 { @apply text-[2rem] font-semibold; line-height: 1.5 !important; }
h2 { @apply text-[1.5rem] font-semibold; line-height: 1.25 !important; }
h3 { @apply text-[1.25rem] font-medium; }
h4 { @apply text-[1rem] font-medium; }
h5 { @apply text-[0.875rem] text-fg-secondary font-bold uppercase; }
h6 { @apply text-[0.75rem] text-fg-secondary font-medium uppercase; }
code {
@apply rounded-md p-2 text-left;
}
/* i dont know where to put this */
.linearBlur::after {
content: '';

View file

@ -47,7 +47,9 @@ export default defineConfig(
'ts/no-use-before-define': 'off',
'style/function-paren-newline': ['error', 'consistent'],
'react/no-context-provider': 'off',
'prefer-const': 'off', // disabled due to maximum call stack size
'prefer-const': 'off', // disabled due to maximum call stack size,
'style/jsx-one-expression-per-line': ['error', { allow: 'non-jsx' }],
'unused-imports/no-unused-imports': 'off',
'eslint-comments/no-unlimited-disable': 'off',
'eslint-comments/require-description': ['error', {
ignore: [

View file

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

View file

@ -1,39 +1,44 @@
use onelauncher_entity::clusters;
use crate::{error::LauncherResult, store::Dirs, utils::io};
use crate::error::LauncherResult;
use crate::store::Dirs;
use crate::utils::io;
pub async fn get_screenshots(cluster: &clusters::Model) -> LauncherResult<Vec<String>> {
let dir = cluster.folder_name.clone();
let path = Dirs::get_clusters_dir().await?.join(dir).join("screenshots");
let dir = cluster.folder_name.clone();
let path = Dirs::get_clusters_dir()
.await?
.join(dir)
.join("screenshots");
if !path.exists() {
io::create_dir(&path).await?;
return Ok(Vec::new());
}
if !path.exists() {
io::create_dir(&path).await?;
return Ok(Vec::new());
}
let mut list = vec![];
let mut files = io::read_dir(path).await?;
while let Ok(Some(entry)) = files.next_entry().await {
list.push(entry.file_name().to_string_lossy().to_string());
}
let mut list = vec![];
let mut files = io::read_dir(path).await?;
while let Ok(Some(entry)) = files.next_entry().await {
list.push(entry.file_name().to_string_lossy().to_string());
}
Ok(list)
}
pub async fn get_worlds(cluster: &clusters::Model) -> LauncherResult<Vec<String>> {
let dir = cluster.folder_name.clone();
let path = Dirs::get_clusters_dir().await?.join(dir).join("saves");
let dir = cluster.folder_name.clone();
let path = Dirs::get_clusters_dir().await?.join(dir).join("saves");
if !path.exists() {
io::create_dir(&path).await?;
return Ok(Vec::new());
}
if !path.exists() {
io::create_dir(&path).await?;
return Ok(Vec::new());
}
let mut list = vec![];
let mut files = io::read_dir(path).await?;
while let Ok(Some(entry)) = files.next_entry().await {
list.push(entry.file_name().to_string_lossy().to_string());
}
let mut list = vec![];
let mut files = io::read_dir(path).await?;
while let Ok(Some(entry)) = files.next_entry().await {
list.push(entry.file_name().to_string_lossy().to_string());
}
Ok(list)
}

View file

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

View file

@ -2,9 +2,9 @@ use chrono::Utc;
use dao::ClusterId;
use interpulse::api::minecraft::VersionInfo;
use onelauncher_entity::cluster_stage::ClusterStage;
use onelauncher_entity::{clusters, java_versions};
use onelauncher_entity::icon::Icon;
use onelauncher_entity::loader::GameLoader;
use onelauncher_entity::{clusters, java_versions};
use sea_orm::ActiveValue::Set;
use tokio::process::Command;
@ -17,8 +17,8 @@ use crate::store::ingress::{IngressType, SubIngress};
use crate::store::{Dirs, State};
use crate::utils::io::{self, IOError};
pub mod dao;
pub mod content;
pub mod dao;
mod sync;
pub use sync::*;
@ -109,7 +109,10 @@ pub async fn create_cluster(
}
/// Make a cluster playable (installs all necessary game files and dependencies required to play)
pub async fn prepare_cluster(cluster: &mut clusters::Model, force: Option<bool>) -> LauncherResult<&clusters::Model> {
pub async fn prepare_cluster(
cluster: &mut clusters::Model,
force: Option<bool>,
) -> LauncherResult<&clusters::Model> {
tracing::debug!("preparing cluster {}", cluster.name);
// if cluster.stage == ClusterStage::Ready {
@ -117,7 +120,6 @@ pub async fn prepare_cluster(cluster: &mut clusters::Model, force: Option<bool>)
// return Ok(cluster);
// }
dao::update_cluster(cluster, async |mut cluster| {
cluster.stage = Set(ClusterStage::Downloading);
Ok(cluster)
@ -161,7 +163,8 @@ async fn install_cluster(cluster: &clusters::Model, force: Option<bool>) -> Laun
// TASK 1
ingress_id.set_ingress_message("fetching data").await?;
let setting_profile =
setting_profiles::dao::get_profile_or_default(cluster.setting_profile_name.as_ref()).await?;
setting_profiles::dao::get_profile_or_default(cluster.setting_profile_name.as_ref())
.await?;
let state = State::get().await?;
let mut metadata = state.metadata.write().await;
@ -193,7 +196,9 @@ async fn install_cluster(cluster: &clusters::Model, force: Option<bool>) -> Laun
)
.await?;
let java_version = if let Some(version) = java::get_recommended_java(&version_info, Some(&setting_profile)).await? {
let java_version = if let Some(version) =
java::get_recommended_java(&version_info, Some(&setting_profile)).await?
{
version
} else {
let Some(ver) = &version_info.java_version else {
@ -204,10 +209,22 @@ async fn install_cluster(cluster: &clusters::Model, force: Option<bool>) -> Laun
};
// TASK 3
download_minecraft(&version_info, &java_version.arch, Some(&SubIngress::new(&ingress_id, INGRESS_STEP)), force).await?;
download_minecraft(
&version_info,
&java_version.arch,
Some(&SubIngress::new(&ingress_id, INGRESS_STEP)),
force,
)
.await?;
// TASK 4
run_forge_processors(cluster, &mut version_info, java_version, &SubIngress::new(&ingress_id, INGRESS_STEP)).await?;
run_forge_processors(
cluster,
&mut version_info,
java_version,
&SubIngress::new(&ingress_id, INGRESS_STEP),
)
.await?;
Ok(())
}
@ -265,23 +282,23 @@ async fn run_forge_processors(
server => "";
}
ingress.set_ingress_message("running forge processors").await?;
ingress
.set_ingress_message("running forge processors")
.await?;
let total_length = processors.len();
for (index, processor) in processors.iter().enumerate() {
if let Some(sides) = &processor.sides
&& !sides.contains(&String::from("client")) {
continue;
}
&& !sides.contains(&String::from("client"))
{
continue;
}
let mut cp = processor.classpath.clone();
cp.push(processor.jar.clone());
let child = Command::new(&java_version.absolute_path)
.arg("-cp")
.arg(arguments::get_classpath_library(
&libraries,
&cp,
)?)
.arg(arguments::get_classpath_library(&libraries, &cp)?)
.arg(
arguments::main_class(arguments::get_library(&libraries, &processor.jar, false)?)
.await?
@ -307,7 +324,9 @@ async fn run_forge_processors(
.into());
}
ingress.set_ingress_message(&format!("running forge processors {index}/{total_length}")).await?;
ingress
.set_ingress_message(&format!("running forge processors {index}/{total_length}"))
.await?;
}
Ok(())
@ -315,11 +334,16 @@ async fn run_forge_processors(
pub async fn update_playtime(id: ClusterId, duration: i64) -> LauncherResult<clusters::Model> {
dao::update_cluster_by_id(id, async |mut cluster| {
let overall_played = cluster.overall_played.take().unwrap_or_default().unwrap_or_default();
let overall_played = cluster
.overall_played
.take()
.unwrap_or_default()
.unwrap_or_default();
cluster.overall_played = Set(Some(overall_played + duration));
cluster.last_played = Set(Some(Utc::now()));
Ok(cluster)
}).await
}
})
.await
}

View file

@ -1,9 +1,13 @@
use std::{collections::HashSet, path::PathBuf};
use std::collections::HashSet;
use std::path::PathBuf;
use onelauncher_entity::clusters;
use serde::Serialize;
use crate::{error::{DaoError, LauncherResult}, send_error, store::Dirs, utils::io};
use crate::error::{DaoError, LauncherResult};
use crate::send_error;
use crate::store::Dirs;
use crate::utils::io;
use super::dao::{self, ClusterId};
@ -42,10 +46,7 @@ pub async fn sync_clusters() -> LauncherResult<Vec<ClusterId>> {
missing_ids.push(cluster.id);
let path = cluster.folder_name.clone();
send_error!(
"cluster {} is missing from the filesystem",
path.clone()
);
send_error!("cluster {} is missing from the filesystem", path.clone());
}
checked_folders.insert(dirs.clusters_dir().join(cluster.folder_name.clone()));
@ -56,22 +57,22 @@ pub async fn sync_clusters() -> LauncherResult<Vec<ClusterId>> {
// and checks if they are in the database
let cluster_dir = Dirs::get_clusters_dir().await?;
if cluster_dir.exists() {
let mut stream = io::read_dir(cluster_dir).await?;
while let Ok(Some(entry)) = stream.next_entry().await {
let path = entry.path();
if !path.is_dir() || checked_folders.contains(&path) {
// Skip if it's not a directory or if we've already checked it
continue;
}
let mut stream = io::read_dir(cluster_dir).await?;
while let Ok(Some(entry)) = stream.next_entry().await {
let path = entry.path();
if !path.is_dir() || checked_folders.contains(&path) {
// Skip if it's not a directory or if we've already checked it
continue;
}
// We have a directory that is not in the database
if let Err(err) = sync_from_fs_to_db(&path).await {
tracing::error!("failed to sync cluster from fs to db: {}", err);
}
}
} else {
io::create_dir_all(cluster_dir).await?;
}
// We have a directory that is not in the database
if let Err(err) = sync_from_fs_to_db(&path).await {
tracing::error!("failed to sync cluster from fs to db: {}", err);
}
}
} else {
io::create_dir_all(cluster_dir).await?;
}
Ok(missing_ids)
}
@ -80,7 +81,9 @@ pub async fn sync_clusters() -> LauncherResult<Vec<ClusterId>> {
/// Checks if the cluster in the database exists and if the directory exists.
#[tracing::instrument]
pub async fn sync_cluster(cluster: &clusters::Model) -> LauncherResult<SyncAction> {
let path = Dirs::get_clusters_dir().await?.join(cluster.folder_name.clone());
let path = Dirs::get_clusters_dir()
.await?
.join(cluster.folder_name.clone());
if !path.exists() {
return Ok(SyncAction::MissingFs);
}
@ -119,5 +122,6 @@ async fn sync_from_fs_to_db(path: &PathBuf) -> LauncherResult<clusters::Model> {
Err(anyhow::anyhow!(
"TODO: failed to sync cluster from fs to db: {}",
path.display()
).into())
}
)
.into())
}

View file

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

View file

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

View file

@ -8,7 +8,7 @@ use onelauncher_entity::prelude::model::*;
use tokio::process::Command;
use tokio::sync::RwLock;
use crate::api::cluster::{prepare_cluster, update_playtime, ClusterError};
use crate::api::cluster::{ClusterError, prepare_cluster, update_playtime};
use crate::api::game::{arguments, log, metadata};
use crate::api::java::{self, JavaError};
use crate::api::setting_profiles::dao::get_profile_by_name;
@ -32,9 +32,10 @@ pub async fn launch_minecraft(
prepare_cluster(cluster, force).await?;
let mut settings = get_global_profile().await;
if let Some(name) = &cluster.setting_profile_name
&& let Some(profile) = get_profile_by_name(name).await? {
settings.merge(profile);
}
&& let Some(profile) = get_profile_by_name(name).await?
{
settings.merge(profile);
}
let state = State::get().await?;
let dirs = Dirs::get().await?;
@ -68,9 +69,7 @@ pub async fn launch_minecraft(
let version_name = loader_version
.as_ref()
.map_or(version.id.clone(), |it| {
format!("{}-{}", version.id, it.id)
});
.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
let version_info =
metadata::download_version_info(&version, loader_version.as_ref(), None, None).await?;
@ -188,11 +187,16 @@ pub async fn launch_minecraft(
let censors = log::create_censors(&creds);
if let Some(discord) = &state.rpc {
discord.set_message(
&format!("Playing {}", cluster.name),
Some(Timestamps::new().start(Utc::now().timestamp())),
).await;
discord
.set_message(
&format!("Playing {}", cluster.name),
Some(Timestamps::new().start(Utc::now().timestamp())),
)
.await;
}
state.processes.spawn(cluster.id, cwd.clone(), creds, censors, &settings, command).await
state
.processes
.spawn(cluster.id, cwd.clone(), creds, censors, &settings, command)
.await
}

View file

@ -5,9 +5,7 @@ use crate::store::credentials::MinecraftCredentials;
pub type CensorMap = HashMap<String, String>;
#[must_use]
pub fn create_censors(
creds: &MinecraftCredentials,
) -> CensorMap {
pub fn create_censors(creds: &MinecraftCredentials) -> CensorMap {
let mut map = HashMap::new();
let username = whoami::username();
@ -24,7 +22,10 @@ pub fn create_censors(
map.insert(creds.username.clone(), "{MC_USERNAME}".to_string());
map.insert(creds.id.as_simple().to_string(), "{MC_UUID}".to_string());
map.insert(creds.id.as_hyphenated().to_string(), "{MC_UUID}".to_string());
map.insert(
creds.id.as_hyphenated().to_string(),
"{MC_UUID}".to_string(),
);
map
}
@ -33,4 +34,4 @@ pub fn censor_line(censors: &CensorMap, line: &mut String) {
for (key, value) in censors {
*line = line.replace(key, value);
}
}
}

View file

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

View file

@ -76,9 +76,10 @@ pub fn validate_os_rule(rule: &OsRule, java_arch: &str, updated: bool) -> bool {
}
if let Some(version) = &rule.version
&& let Ok(regex) = Regex::new(version.as_str()) {
rule_match &= regex.is_match(&sysinfo::System::os_version().unwrap_or_default());
}
&& let Ok(regex) = Regex::new(version.as_str())
{
rule_match &= regex.is_match(&sysinfo::System::os_version().unwrap_or_default());
}
rule_match
}
}

View file

@ -67,31 +67,33 @@ pub async fn send_ingress_message(
}
/// Sends an ingress update with the given increment
pub async fn send_ingress(
id: &IngressId,
increment: f64,
) -> LauncherResult<()> {
pub async fn send_ingress(id: &IngressId, increment: f64) -> LauncherResult<()> {
send_ingress_message(id, increment, None).await
}
/// Sends an ingress update with the given message (does not increment)
pub async fn set_ingress_message(
id: &IngressId,
message: &str,
) -> LauncherResult<()> {
pub async fn set_ingress_message(id: &IngressId, message: &str) -> LauncherResult<()> {
send_ingress_message(id, 0.0, Some(message)).await
}
#[async_trait::async_trait]
pub trait IngressSendExt {
async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()>;
async fn send_ingress_message(
&self,
increment: f64,
message: Option<&str>,
) -> LauncherResult<()>;
async fn send_ingress(&self, increment: f64) -> LauncherResult<()>;
async fn set_ingress_message(&self, message: &str) -> LauncherResult<()>;
}
#[async_trait::async_trait]
impl IngressSendExt for IngressId {
async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> {
async fn send_ingress_message(
&self,
increment: f64,
message: Option<&str>,
) -> LauncherResult<()> {
send_ingress_message(self, increment, message).await
}
@ -106,7 +108,11 @@ impl IngressSendExt for IngressId {
#[async_trait::async_trait]
impl IngressSendExt for Option<&IngressId> {
async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> {
async fn send_ingress_message(
&self,
increment: f64,
message: Option<&str>,
) -> LauncherResult<()> {
if let Some(id) = self {
id.send_ingress_message(increment, message).await
} else {
@ -133,7 +139,11 @@ impl IngressSendExt for Option<&IngressId> {
#[async_trait::async_trait]
impl IngressSendExt for &SubIngress<'_> {
async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> {
async fn send_ingress_message(
&self,
increment: f64,
message: Option<&str>,
) -> LauncherResult<()> {
self.id.send_ingress_message(increment, message).await
}
@ -148,7 +158,11 @@ impl IngressSendExt for &SubIngress<'_> {
#[async_trait::async_trait]
impl IngressSendExt for Option<&SubIngress<'_>> {
async fn send_ingress_message(&self, increment: f64, message: Option<&str>) -> LauncherResult<()> {
async fn send_ingress_message(
&self,
increment: f64,
message: Option<&str>,
) -> LauncherResult<()> {
if let Some(id) = self {
id.send_ingress_message(increment, message).await
} else {
@ -175,7 +189,7 @@ impl IngressSendExt for Option<&SubIngress<'_>> {
#[cfg(test)]
pub mod tests {
use crate::api::ingress::{init_ingress, IngressSendExt};
use crate::api::ingress::{IngressSendExt, init_ingress};
use crate::api::proxy::ProxyDynamic;
use crate::initialize_core;
use crate::store::CoreOptions;

View file

@ -1,9 +1,11 @@
use std::path::{Path, PathBuf};
use onelauncher_entity::java_versions;
use sea_orm::{ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder};
use sea_orm::ActiveValue::Set;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder};
use crate::{error::LauncherResult, store::State};
use crate::error::LauncherResult;
use crate::store::State;
use super::{JavaError, JavaInfo};
@ -21,15 +23,18 @@ pub async fn get_latest_java() -> LauncherResult<Option<java_versions::Model>> {
}
/// Accepts a path to a JRE folder and a [`JavaInfo`] and converts it to a [`java_versions::ActiveModel`]
pub fn get_java_model(absolute_path: &Path, info: &JavaInfo) -> LauncherResult<java_versions::ActiveModel> {
pub fn get_java_model(
absolute_path: &Path,
info: &JavaInfo,
) -> LauncherResult<java_versions::ActiveModel> {
let major_version: u32 = if info.java_version[0..2].eq("1.") {
info.java_version[2..3].parse()
} else {
let split = info.java_version.split_once('.').unwrap_or_default();
split.0.parse()
}.or_else(
|_| info.java_version.parse()
).map_err(|e| JavaError::ParseVersion(info.java_version.clone(), e))?;
}
.or_else(|_| info.java_version.parse())
.map_err(|e| JavaError::ParseVersion(info.java_version.clone(), e))?;
Ok(java_versions::ActiveModel {
absolute_path: Set(absolute_path.to_string_lossy().to_string()),
@ -84,7 +89,10 @@ pub async fn get_latest_java_by_major(major: u32) -> LauncherResult<Option<java_
}
/// Accepts a path to a JRE folder and returns the inserted entry
pub async fn insert_java(absolute_path: PathBuf, info: JavaInfo) -> LauncherResult<java_versions::Model> {
pub async fn insert_java(
absolute_path: PathBuf,
info: JavaInfo,
) -> LauncherResult<java_versions::Model> {
let db = &State::get().await?.db;
let model = get_java_model(&absolute_path, &info)?;
@ -93,7 +101,9 @@ pub async fn insert_java(absolute_path: PathBuf, info: JavaInfo) -> LauncherResu
}
/// Accepts a path to JRE folders and returns the inserted entries
pub async fn insert_java_many(java: Vec<(PathBuf, JavaInfo)>) -> LauncherResult<Vec<java_versions::Model>> {
pub async fn insert_java_many(
java: Vec<(PathBuf, JavaInfo)>,
) -> LauncherResult<Vec<java_versions::Model>> {
let db = &State::get().await?.db;
let mut models = Vec::new();
@ -105,4 +115,4 @@ pub async fn insert_java_many(java: Vec<(PathBuf, JavaInfo)>) -> LauncherResult<
Ok(java_versions::Entity::insert_many(models)
.exec_with_returning_many(db)
.await?)
}
}

View file

@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
use crate::api::ingress::{init_ingress, init_ingress_opt, send_ingress};
use crate::error::LauncherResult;
use crate::store::ingress::{IngressType, SubIngress};
use crate::store::Dirs;
use crate::store::ingress::{IngressType, SubIngress};
use crate::utils::http::{fetch_advanced, fetch_json};
use crate::utils::io::{self, IOError};
@ -178,7 +178,10 @@ pub async fn prepare_java(major: u32) -> LauncherResult<java_versions::Model> {
}
}
tracing::warn!("no java installations found on the system, attempting to download java {}", major);
tracing::warn!(
"no java installations found on the system, attempting to download java {}",
major
);
let java_path = install_java_from_major(major).await?;
let java_info = check_java_runtime(&java_path, false).await?;
@ -197,19 +200,15 @@ pub async fn prepare_java(major: u32) -> LauncherResult<java_versions::Model> {
#[cfg(unix)]
fn find_java_in_path(mut found: Vec<PathBuf>) -> Vec<PathBuf> {
if let Ok(path) = std::env::var("PATH") {
for path_entry in path.split(':') {
let java = PathBuf::from(path_entry).join("java");
if java.exists() {
if java.is_file() {
if !found.contains(&java) {
found.push(java);
}
}
}
}
}
found
if let Ok(path) = std::env::var("PATH") {
for path_entry in path.split(':') {
let java = PathBuf::from(path_entry).join("java");
if java.exists() && java.is_file() && !found.contains(&java) {
found.push(java);
}
}
}
found
}
/// Attempts to scan common paths for Java installations.
///
@ -315,7 +314,6 @@ fn internal_locate_java() -> Vec<PathBuf> {
}
}
found = find_java_in_path(found);
found
@ -376,12 +374,10 @@ pub async fn get_zulu_packages() -> LauncherResult<Vec<JavaPackage>> {
pub async fn install_java_from_major(version: u32) -> LauncherResult<PathBuf> {
let packages = get_zulu_packages().await?;
let package = packages.into_iter()
let package = packages
.into_iter()
.find(|p| p.java_version.contains(&version))
.ok_or_else(|| anyhow::anyhow!(
"Could not find a java package for version {}",
version
))?;
.ok_or_else(|| anyhow::anyhow!("Could not find a java package for version {}", version))?;
install_java_package(package).await
}
@ -411,19 +407,18 @@ pub async fn install_java_package(package: JavaPackage) -> LauncherResult<PathBu
.await
.map_err(IOError::AsyncZipError)?;
if let Some(entry) = archive.file().entries().first() {
if let Some(dir) = entry
if let Some(entry) = archive.file().entries().first()
&& let Some(dir) = entry
.filename()
.clone()
.into_string()
.unwrap()
.split('/')
.next()
{
let path = java_dir.join(dir);
if path.exists() {
io::remove_dir_all(&path).await?;
}
{
let path = java_dir.join(dir);
if path.exists() {
io::remove_dir_all(&path).await?;
}
}
@ -440,7 +435,12 @@ pub async fn install_java_package(package: JavaPackage) -> LauncherResult<PathBu
#[cfg(target_os = "macos")]
{
let java_version = package.java_version.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(".");
let java_version = package
.java_version
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(".");
base_path = base_path
.join(format!("zulu-{java_version}.jre"))
.join("Contents")

View file

@ -1,9 +1,7 @@
use onelauncher_entity::loader::GameLoader;
use onelauncher_entity::package::PackageType;
use onelauncher_entity::{cluster_packages, clusters, packages};
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter,
};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
use crate::error::{DaoError, LauncherResult};
use crate::store::State;

View file

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

View file

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

View file

@ -1,7 +1,8 @@
use onelauncher_entity::loader::GameLoader;
use onelauncher_entity::package::Provider;
use crate::{api::packages::data::SearchResult, error::LauncherResult};
use crate::api::packages::data::SearchResult;
use crate::error::LauncherResult;
use crate::utils::pagination::Paginated;
use super::data::{ManagedPackage, ManagedUser, ManagedVersion, PackageAuthor, SearchQuery};
@ -70,11 +71,7 @@ impl ProviderExt for Provider {
hashes: &[String],
) -> LauncherResult<Vec<ManagedVersion>> {
match self {
Self::Modrinth => {
ModrinthProviderImpl
.get_versions_by_hashes(hashes)
.await
}
Self::Modrinth => ModrinthProviderImpl.get_versions_by_hashes(hashes).await,
_ => todo!("unimplemented provider"),
}
}
@ -91,11 +88,7 @@ impl ProviderExt for Provider {
author: PackageAuthor,
) -> LauncherResult<Vec<ManagedUser>> {
match self {
Self::Modrinth => {
ModrinthProviderImpl
.get_users_from_author(author)
.await
}
Self::Modrinth => ModrinthProviderImpl.get_users_from_author(author).await,
_ => todo!("unimplemented provider"),
}
}

View file

@ -1,6 +1,9 @@
use super::ProviderExt;
use crate::api::packages::data::{
ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency, ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageGallery, PackageLicense, PackageLinks, PackageReleaseType, PackageSide, PackageStatus, SearchQuery, SearchResult, DEFAULT_LIMIT
DEFAULT_LIMIT, ManagedPackage, ManagedUser, ManagedVersion, ManagedVersionDependency,
ManagedVersionFile, PackageAuthor, PackageDependencyType, PackageDonationUrl, PackageGallery,
PackageLicense, PackageLinks, PackageReleaseType, PackageSide, PackageStatus, SearchQuery,
SearchResult,
};
use crate::error::LauncherResult;
use crate::utils::http;
@ -52,25 +55,41 @@ impl ProviderExt for ModrinthProviderImpl {
if let Some(categories) = &filters.categories {
for category in categories {
builder.and(Facet("categories".to_string(), FacetOperation::Eq, category.to_string()));
builder.and(Facet(
"categories".to_string(),
FacetOperation::Eq,
category.to_string(),
));
}
}
if let Some(mc_versions) = &filters.game_versions {
for mc_version in mc_versions {
builder.and(Facet("versions".to_string(), FacetOperation::Eq, mc_version.to_string()));
builder.and(Facet(
"versions".to_string(),
FacetOperation::Eq,
mc_version.to_string(),
));
}
}
if let Some(package_types) = &filters.package_types {
for package_type in package_types {
builder.and(Facet("project_type".to_string(), FacetOperation::Eq, package_type_to_string(package_type)));
builder.and(Facet(
"project_type".to_string(),
FacetOperation::Eq,
package_type_to_string(package_type),
));
}
}
if let Some(loaders) = &filters.loaders {
for loader in loaders {
builder.and(Facet("categories".to_string(), FacetOperation::Eq, loader.to_string()));
builder.and(Facet(
"categories".to_string(),
FacetOperation::Eq,
loader.to_string(),
));
}
}
@ -166,7 +185,10 @@ impl ProviderExt for ModrinthProviderImpl {
// Ok(http::fetch_json(Method::GET, url_v3!("/organizations/{slug}/projects"), None, None).await?)
// }
async fn get_users_from_author(&self, author: PackageAuthor) -> LauncherResult<Vec<ManagedUser>> {
async fn get_users_from_author(
&self,
author: PackageAuthor,
) -> LauncherResult<Vec<ManagedUser>> {
let (team_id, org_id) = match author {
PackageAuthor::Team { team_id, org_id } => (team_id, org_id),
PackageAuthor::Users(users) => {
@ -188,14 +210,24 @@ impl ProviderExt for ModrinthProviderImpl {
// pub members: Vec<TeamMember>,
}
let organization = http::fetch_json::<Organization>(Method::GET, url_v3!("/organization/{org_id}"), None, None).await?;
let organization = http::fetch_json::<Organization>(
Method::GET,
url_v3!("/organization/{org_id}"),
None,
None,
)
.await?;
let org_user = ManagedUser {
id: organization.id.clone(),
username: organization.name,
avatar_url: organization.icon_url,
bio: Some(organization.description),
url: Some(format!("{}/organization/{}", Provider::Modrinth.website(), organization.id)),
url: Some(format!(
"{}/organization/{}",
Provider::Modrinth.website(),
organization.id
)),
is_organization_user: true,
role: None,
};
@ -407,7 +439,7 @@ impl From<ModrinthPackage> for ManagedPackage {
},
status: value.status,
downloads: value.downloads,
gallery: value.gallery.into_iter().map(Into::into).collect()
gallery: value.gallery.into_iter().map(Into::into).collect(),
}
}
}
@ -522,11 +554,7 @@ impl From<ModrinthUser> for ManagedUser {
Self {
id: value.id.clone(),
username: value.username,
url: Some(format!(
"{}user/{}",
Provider::Modrinth.website(),
value.id
)),
url: Some(format!("{}user/{}", Provider::Modrinth.website(), value.id)),
avatar_url: value.avatar_url,
bio: value.bio,
is_organization_user: false,
@ -538,24 +566,24 @@ impl From<ModrinthUser> for ManagedUser {
#[derive(Deserialize)]
pub struct ModrinthGallery {
#[serde(rename = "raw_url")]
pub url: String,
pub url: String,
#[serde(rename = "url")]
pub thumbnail_url: String,
pub title: Option<String>,
pub description: Option<String>,
pub featured: Option<bool>
pub thumbnail_url: String,
pub title: Option<String>,
pub description: Option<String>,
pub featured: Option<bool>,
}
impl From<ModrinthGallery> for PackageGallery {
fn from(value: ModrinthGallery) -> Self {
Self {
url: value.url,
thumbnail_url: value.thumbnail_url,
title: value.title,
description: value.description,
featured: value.featured
}
}
fn from(value: ModrinthGallery) -> Self {
Self {
url: value.url,
thumbnail_url: value.thumbnail_url,
title: value.title,
description: value.description,
featured: value.featured,
}
}
}
#[allow(dead_code)]

View file

@ -1,7 +1,9 @@
use onelauncher_entity::package::Provider;
use crate::{api::packages::{self, provider::ProviderExt}, error::LauncherResult, store::{Core, CoreOptions, Dirs}};
use crate::api::packages::provider::ProviderExt;
use crate::api::packages::{self};
use crate::error::LauncherResult;
use crate::store::{Core, CoreOptions, Dirs};
#[tokio::test]
pub async fn test_get_provider() -> LauncherResult<()> {
@ -22,7 +24,9 @@ pub async fn test_get_multiple() -> LauncherResult<()> {
let provider = Provider::Modrinth;
let res = provider.get_multiple(&["oneconfig".to_string(), "chatting".to_string()]).await?;
let res = provider
.get_multiple(&["oneconfig".to_string(), "chatting".to_string()])
.await?;
assert_eq!(res.len(), 2);
@ -35,7 +39,9 @@ pub async fn test_get_versions() -> LauncherResult<()> {
let provider = Provider::Modrinth;
let res = provider.get_versions(&["ZvlCAdEF".to_string(), "GQANlg7p".to_string()]).await?;
let res = provider
.get_versions(&["ZvlCAdEF".to_string(), "GQANlg7p".to_string()])
.await?;
assert_eq!(res.len(), 2);
@ -51,7 +57,7 @@ pub async fn test_download_chatting() -> LauncherResult<()> {
let pkg = provider.get("chatting").await?;
let ver_id = pkg.version_ids.first().expect("No version found");
let versions = provider.get_versions(&[ver_id.clone()]).await?;
let ver = versions.first().expect("No version found");
let ver = versions.first().expect("No version found");
let db_model = packages::download_package(&pkg, ver, None).await;
assert!(db_model.is_ok_and(|m| m.display_name == pkg.name));

View file

@ -6,18 +6,8 @@ use crate::store::processes::Process;
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
pub enum ProcessPayload {
Starting {
command: String,
},
Started {
process: Process,
},
Stopped {
pid: u32,
exit_code: i32,
},
Output {
pid: u32,
output: String,
},
}
Starting { command: String },
Started { process: Process },
Stopped { pid: u32, exit_code: i32 },
Output { pid: u32, output: String },
}

View file

@ -1,6 +1,7 @@
use serde::Serialize;
use crate::{error::LauncherResult, store::proxy::ProxyState};
use crate::error::LauncherResult;
use crate::store::proxy::ProxyState;
#[onelauncher_macro::specta]
#[derive(Debug, Clone, Serialize)]

View file

@ -1,7 +1,11 @@
#[cfg(feature = "tauri")] pub mod proxy_tauri;
#[cfg(feature = "cli")] pub mod proxy_cli;
#[cfg(feature = "cli")]
pub mod proxy_cli;
pub mod proxy_empty;
#[cfg(feature = "tauri")]
pub mod proxy_tauri;
#[cfg(feature = "tauri")] pub use proxy_tauri::ProxyTauri;
#[cfg(feature = "cli")] pub use proxy_cli::ProxyCli;
pub use proxy_empty::ProxyEmpty;
#[cfg(feature = "cli")]
pub use proxy_cli::ProxyCli;
pub use proxy_empty::ProxyEmpty;
#[cfg(feature = "tauri")]
pub use proxy_tauri::ProxyTauri;

View file

@ -4,7 +4,11 @@ use indicatif::ProgressBar;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::{api::{processes::ProcessPayload, proxy::{event::LauncherEvent, LauncherProxy}}, constants::CLI_TOTAL_INGRESS, LauncherResult};
use crate::LauncherResult;
use crate::api::processes::ProcessPayload;
use crate::api::proxy::LauncherProxy;
use crate::api::proxy::event::LauncherEvent;
use crate::constants::CLI_TOTAL_INGRESS;
#[derive(Default, Debug)]
pub struct ProxyCli {
@ -39,35 +43,37 @@ impl LauncherProxy for ProxyCli {
let progress = ProgressBar::new(CLI_TOTAL_INGRESS);
progress.set_message(ingress.message.clone());
progress.set_position(0);
progress.set_style(indicatif::ProgressStyle::default_bar().template(
"{spinner:.green}, [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}"
).unwrap().progress_chars("#>-"));
progress.set_style(
indicatif::ProgressStyle::default_bar()
.template(
"{spinner:.green}, [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}",
)
.unwrap()
.progress_chars("#>-"),
);
feeds.insert(ingress.id, progress);
}
},
}
LauncherEvent::Message(message) => {
println!("[{:?}] {}", message.level, message.message);
},
LauncherEvent::Process(process) => {
match process {
ProcessPayload::Starting { command } => {
println!("Starting process: {command}");
},
ProcessPayload::Started { process } => {
println!("Process started: {process:#?}");
},
ProcessPayload::Stopped { pid, exit_code } => {
println!("Process {pid} exited with code {exit_code}");
},
ProcessPayload::Output { pid, output } => {
println!("Process {pid}: {output}");
},
}
}
LauncherEvent::Process(process) => match process {
ProcessPayload::Starting { command } => {
println!("Starting process: {command}");
}
ProcessPayload::Started { process } => {
println!("Process started: {process:#?}");
}
ProcessPayload::Stopped { pid, exit_code } => {
println!("Process {pid} exited with code {exit_code}");
}
ProcessPayload::Output { pid, output } => {
println!("Process {pid}: {output}");
}
},
}
Ok(())
}
}

View file

@ -1,4 +1,6 @@
use crate::{api::proxy::{event::LauncherEvent, LauncherProxy}, LauncherResult};
use crate::LauncherResult;
use crate::api::proxy::LauncherProxy;
use crate::api::proxy::event::LauncherEvent;
#[derive(Default, Debug)]
pub struct ProxyEmpty;
@ -17,17 +19,19 @@ impl LauncherProxy for ProxyEmpty {
LauncherEvent::Ingress(ingress) => {
println!(
"{} [{}] {}",
ingress.percent.map_or("100%".to_string(), |p| format!("{:.2}%", p * 100.0)),
ingress
.percent
.map_or("100%".to_string(), |p| format!("{:.2}%", p * 100.0)),
ingress.id,
ingress.message
);
},
}
LauncherEvent::Message(message) => {
println!("[{:?}] {}", message.level, message.message);
},
}
LauncherEvent::Process(payload) => {
println!("{payload:#?}");
},
}
}
Ok(())
@ -44,5 +48,4 @@ impl LauncherProxy for ProxyEmpty {
println!("shown window");
Ok(())
}
}

View file

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

View file

@ -19,10 +19,7 @@ pub enum MessageLevel {
pub fn send_message(level: MessageLevel, message: String) {
tokio::spawn(async move {
let payload = MessagePayload {
level,
message,
};
let payload = MessagePayload { level, message };
send_event(super::event::LauncherEvent::Message(payload)).await;
});

View file

@ -1,6 +1,6 @@
pub mod event;
mod r#impl;
pub mod message;
pub mod event;
pub use r#impl::*;
@ -15,5 +15,7 @@ pub trait LauncherProxy: Send + Sync + std::fmt::Debug {
fn show_main_window(&self) -> crate::LauncherResult<()>;
}
#[cfg(feature = "cli")] pub type ProxyDynamic = ProxyCli;
#[cfg(not(feature = "cli"))] pub type ProxyDynamic = ProxyEmpty;
#[cfg(feature = "cli")]
pub type ProxyDynamic = ProxyCli;
#[cfg(not(feature = "cli"))]
pub type ProxyDynamic = ProxyEmpty;

View file

@ -6,8 +6,7 @@ use crate::store::State;
pub mod dao {
use onelauncher_entity::setting_profiles;
use sea_orm::IntoActiveModel;
use sea_orm::{ActiveModelTrait, EntityTrait};
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel};
use crate::error::{DaoError, LauncherResult};
use crate::store::State;
@ -59,37 +58,45 @@ pub mod dao {
}
pub async fn update_profile_by_name<B>(
name: &str,
block: B,
) -> LauncherResult<setting_profiles::Model>
where B: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::ActiveModel> {
let state = State::get().await?;
let db = &state.db;
name: &str,
block: B,
) -> LauncherResult<setting_profiles::Model>
where
B: AsyncFnOnce(
setting_profiles::ActiveModel,
) -> LauncherResult<setting_profiles::ActiveModel>,
{
let state = State::get().await?;
let db = &state.db;
let model = get_profile_by_name(name).await?.ok_or(DaoError::NotFound)?;
let model = block(model.into_active_model()).await?;
let model = model.update(db).await?;
let model = get_profile_by_name(name).await?.ok_or(DaoError::NotFound)?;
let model = block(model.into_active_model()).await?;
let model = model.update(db).await?;
Ok(model)
}
Ok(model)
}
/// Updates an existing profile in the database.
pub async fn update_profile<B>(
profile: &mut setting_profiles::Model,
block: B,
) -> LauncherResult<&mut setting_profiles::Model>
where B: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult<setting_profiles::ActiveModel> {
let state = State::get().await?;
let db = &state.db;
/// Updates an existing profile in the database.
pub async fn update_profile<B>(
profile: &mut setting_profiles::Model,
block: B,
) -> LauncherResult<&mut setting_profiles::Model>
where
B: AsyncFnOnce(
setting_profiles::ActiveModel,
) -> LauncherResult<setting_profiles::ActiveModel>,
{
let state = State::get().await?;
let db = &state.db;
let model = profile.clone().into_active_model();
let model = block(model).await?;
let model = model.update(db).await?;
let model = profile.clone().into_active_model();
let model = block(model).await?;
let model = model.update(db).await?;
*profile = model;
*profile = model;
Ok(profile)
}
Ok(profile)
}
pub async fn delete_profile_by_name(name: &str) -> LauncherResult<()> {
let state = State::get().await?;

View file

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

View file

@ -2,4 +2,4 @@ mod events;
pub use events::*;
mod commands;
pub use commands::*;
pub use commands::*;

View file

@ -30,7 +30,8 @@ pub const FEATURED_PACKAGES_URL: &str = "https://polyfrost.org/meta/onelauncher/
/// <https://mclo.gs>/ API base url.
pub const MCLOGS_API_URL: &str = "https://api.mclo.gs/1";
/// <https://skyclient.co/> metadata base url.
pub const SKYCLIENT_BASE_URL: &str = "https://raw.githubusercontent.com/SkyblockClient/SkyblockClient-REPO/refs/heads/main/v1";
pub const SKYCLIENT_BASE_URL: &str =
"https://raw.githubusercontent.com/SkyblockClient/SkyblockClient-REPO/refs/heads/main/v1";
// =========== Hacky Mojang-spec OS constants ===========
#[cfg(target_os = "windows")]

View file

@ -27,25 +27,61 @@ pub enum LauncherError {
DaoError(#[from] DaoError),
#[error("json error: {0}")]
SerdeError(#[from] #[skip] serde_json::Error),
SerdeError(
#[from]
#[skip]
serde_json::Error,
),
#[error(transparent)]
AnyhowError(#[from] #[skip] anyhow::Error),
AnyhowError(
#[from]
#[skip]
anyhow::Error,
),
#[error("database error: {0}")]
DbError(#[from] #[skip] sea_orm::DbErr),
DbError(
#[from]
#[skip]
sea_orm::DbErr,
),
#[error("http error: {0}")]
ReqwestError(#[from] #[skip] reqwest::Error),
ReqwestError(
#[from]
#[skip]
reqwest::Error,
),
#[error("meta error: {0}")]
InterpulseError(#[from] #[skip] interpulse::Error),
InterpulseError(
#[from]
#[skip]
interpulse::Error,
),
#[error(transparent)]
RegexError(#[from] #[skip] regex::Error),
RegexError(
#[from]
#[skip]
regex::Error,
),
#[error("couldn't acquire semaphore: {0}")]
SemaphoreError(#[from] #[skip] tokio::sync::AcquireError),
SemaphoreError(
#[from]
#[skip]
tokio::sync::AcquireError,
),
#[error(transparent)]
UrlError(#[from] #[skip] url::ParseError),
UrlError(
#[from]
#[skip]
url::ParseError,
),
#[cfg(feature = "tauri")]
#[error(transparent)]
TauriError(#[from] #[skip] tauri::Error),
TauriError(
#[from]
#[skip]
tauri::Error,
),
}
pub type LauncherResult<T> = Result<T, LauncherError>;

View file

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

View file

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

View file

@ -1,4 +1,5 @@
use std::{ops::Deref, sync::Arc};
use std::ops::Deref;
use std::sync::Arc;
use tokio::sync::OnceCell;
use tracing_subscriber::fmt::format::FmtSpan;
@ -48,9 +49,9 @@ impl Core {
}
pub async fn initialize(options: CoreOptions) -> LauncherResult<()> {
CORE_STATE.get_or_try_init(|| async move {
Ok::<_, LauncherError>(Self::new(options))
}).await?;
CORE_STATE
.get_or_try_init(|| async move { Ok::<_, LauncherError>(Self::new(options)) })
.await?;
tracing::info!("core initialized successfully");
@ -58,10 +59,7 @@ impl Core {
}
pub fn get() -> Arc<Self> {
CORE_STATE
.get()
.expect("core was not initialized")
.clone()
CORE_STATE.get().expect("core was not initialized").clone()
}
}
@ -72,4 +70,3 @@ impl Deref for Core {
&self.0
}
}

View file

@ -1019,7 +1019,7 @@ pub enum MinecraftAuthError {
PKCS8Error(
#[from]
#[skip]
p256::pkcs8::Error
p256::pkcs8::Error,
),
#[error("failed to serialize JSON during MSA step {step:?}: {source}")]
SerializeError {

View file

@ -1,9 +1,11 @@
use std::{path::PathBuf, time::Duration};
use std::path::PathBuf;
use std::time::Duration;
use onelauncher_migration::MigratorTrait;
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use crate::{utils::io::IOError, LauncherResult};
use crate::LauncherResult;
use crate::utils::io::IOError;
use super::Dirs;
@ -30,4 +32,4 @@ pub async fn create_pool_from_path(path: PathBuf) -> LauncherResult<DatabaseConn
pub async fn create_pool() -> LauncherResult<DatabaseConnection> {
let path = Dirs::get_db_file().await?;
create_pool_from_path(path).await
}
}

View file

@ -3,7 +3,7 @@ use std::path::PathBuf;
use onelauncher_entity::package::{PackageType, Provider};
use tokio::sync::OnceCell;
use crate::{utils, LauncherResult};
use crate::{LauncherResult, utils};
use super::Core;
@ -24,9 +24,9 @@ pub enum DirectoryError {
impl Dirs {
pub async fn get() -> LauncherResult<&'static Self> {
DIRS_GLOBAL.get_or_try_init(async || {
Self::initialize()
}).await
DIRS_GLOBAL
.get_or_try_init(async || Self::initialize())
.await
}
fn initialize() -> LauncherResult<Self> {
@ -36,9 +36,7 @@ impl Dirs {
tracing::info!("using base directory '{}'", base_dir.display());
Ok(Self {
base_dir
})
Ok(Self { base_dir })
}
/// Get the base directory for the launcher.
@ -46,7 +44,6 @@ impl Dirs {
pub const fn base_dir(&self) -> &PathBuf {
&self.base_dir
}
}
macro_rules! dirs_impl {

View file

@ -1,6 +1,8 @@
use std::sync::{atomic::AtomicBool, Arc};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use discord_rich_presence::{activity::{Activity, Assets, Button, Timestamps}, DiscordIpc, DiscordIpcClient};
use discord_rich_presence::activity::{Activity, Assets, Button, Timestamps};
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
use tokio::sync::RwLock;
use crate::error::LauncherResult;
@ -15,11 +17,13 @@ pub struct DiscordRPC {
impl DiscordRPC {
pub fn initialize() -> LauncherResult<Self> {
let client_id = Core::get().discord_client_id.clone()
let client_id = Core::get()
.discord_client_id
.clone()
.ok_or(DiscordError::MissingClientId)?;
let mut client = DiscordIpcClient::new(client_id.as_str())
.map_err(|_| DiscordError::ConnectError)?;
let mut client =
DiscordIpcClient::new(client_id.as_str()).map_err(|_| DiscordError::ConnectError)?;
let connected = client.connect().is_ok();
@ -34,18 +38,15 @@ impl DiscordRPC {
self.set_activity(
Activity::new()
.state(msg)
.buttons(
vec![Button::new("Website", "https://polyfrost.org/")]
)
.timestamps(
timestamp.unwrap_or_else(|| Timestamps::new().start(self.started_at))
)
.buttons(vec![Button::new("Website", "https://polyfrost.org/")])
.timestamps(timestamp.unwrap_or_else(|| Timestamps::new().start(self.started_at)))
.assets(
Assets::new()
.large_image("onelauncher_logo_512")
.large_text("OneLauncher"),
)
).await
),
)
.await
}
pub async fn set_activity(&self, activity: Activity<'_>) -> bool {
@ -102,7 +103,8 @@ impl DiscordRPC {
}
fn set_connected(&self, connected: bool) {
self.connected.store(connected, std::sync::atomic::Ordering::Relaxed);
self.connected
.store(connected, std::sync::atomic::Ordering::Relaxed);
}
}
@ -113,4 +115,4 @@ pub enum DiscordError {
MissingClientId,
#[error("couldn't connect to Discord IPC")]
ConnectError,
}
}

View file

@ -4,9 +4,9 @@ use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::api::proxy::event::try_send_event;
use crate::LauncherResult;
use crate::api::ingress::IngressPayload;
use crate::api::proxy::event::try_send_event;
use super::State;
@ -88,8 +88,7 @@ impl IngressProcessor {
total: ingress.total,
};
try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload))
.await?;
try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload)).await?;
Ok(())
}
@ -219,8 +218,7 @@ impl Drop for IngressId {
total: ingress.total,
};
try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload))
.await?;
try_send_event(crate::api::proxy::event::LauncherEvent::Ingress(payload)).await?;
tracing::trace!(
"exited at {}% for ingress {:?}",

View file

@ -20,7 +20,7 @@ use super::Dirs;
pub struct Metadata {
initialized: bool,
inner: MetadataInner,
version_loader_cache: HashMap<String, Vec<GameLoader>>
version_loader_cache: HashMap<String, Vec<GameLoader>>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
@ -46,7 +46,7 @@ pub enum MetadataError {
ParseError(
#[from]
#[skip]
serde_json::Error
serde_json::Error,
),
#[error("no matching loader found")]
NoMatchingLoader,
@ -65,7 +65,6 @@ impl Metadata {
Self::default()
}
#[tracing::instrument(skip(self))]
pub async fn get_vanilla_or_fetch(&mut self) -> LauncherResult<&VanillaManifest> {
if !self.initialized() {
@ -76,7 +75,10 @@ impl Metadata {
}
#[tracing::instrument(skip(self))]
pub async fn get_modded_or_fetch(&mut self, loader: &GameLoader) -> LauncherResult<&ModdedManifest> {
pub async fn get_modded_or_fetch(
&mut self,
loader: &GameLoader,
) -> LauncherResult<&ModdedManifest> {
if !loader.is_modded() {
return Err(MetadataError::NotModdedManifest(*loader).into());
}
@ -89,7 +91,10 @@ impl Metadata {
}
pub fn get_vanilla(&self) -> LauncherResult<&VanillaManifest> {
self.inner.minecraft.as_ref().ok_or_else(|| MetadataError::FetchError.into())
self.inner
.minecraft
.as_ref()
.ok_or_else(|| MetadataError::FetchError.into())
}
pub fn get_modded(&self, loader: &GameLoader) -> LauncherResult<&ModdedManifest> {
@ -103,7 +108,8 @@ impl Metadata {
GameLoader::Fabric => self.inner.fabric.as_ref(),
GameLoader::Quilt => self.inner.quilt.as_ref(),
_ => None,
}.ok_or_else(|| MetadataError::FetchError.into())
}
.ok_or_else(|| MetadataError::FetchError.into())
}
#[tracing::instrument(skip_all)]
@ -144,18 +150,16 @@ impl Metadata {
($var:tt, $is_modded:tt) => {
if self.inner.$var.is_none() {
match GameLoader::from_str(stringify!($var)) {
Ok(loader) => {
match check!(_fetch, loader, $is_modded) {
Ok(data) => {
self.inner.$var = Some(data);
changed += 1;
}
Err(err) => {
tracing::error!("failed to fetch manifest for {}: {}", loader, err);
}
Ok(loader) => match check!(_fetch, loader, $is_modded) {
Ok(data) => {
self.inner.$var = Some(data);
changed += 1;
}
Err(err) => {
tracing::error!("failed to fetch manifest for {}: {}", loader, err);
}
},
Err(err) => tracing::error!("{err}")
Err(err) => tracing::error!("{err}"),
};
}
};
@ -225,7 +229,10 @@ impl Metadata {
/// Returns a list of loaders that are compatible with the given Minecraft version.
///
/// WARNING: Can be heavy in certain cases!
pub async fn get_loaders_for_version(&mut self, mc_version: &str) -> LauncherResult<Vec<GameLoader>> {
pub async fn get_loaders_for_version(
&mut self,
mc_version: &str,
) -> LauncherResult<Vec<GameLoader>> {
if !self.initialized() {
self.initialize().await?;
}
@ -248,7 +255,8 @@ impl Metadata {
}
}
self.version_loader_cache.insert(mc_version.to_owned(), loaders.clone());
self.version_loader_cache
.insert(mc_version.to_owned(), loaders.clone());
Ok(loaders)
}

View file

@ -1,17 +1,17 @@
mod core_options;
pub mod credentials;
pub(super) mod db;
mod dirs;
pub mod discord;
pub mod ingress;
pub mod metadata;
pub mod processes;
pub mod proxy;
pub mod semaphore;
pub mod metadata;
pub mod credentials;
pub mod processes;
mod core_options;
mod dirs;
mod settings;
mod state;
pub use core_options::*;
pub use dirs::*;
pub use settings::*;
pub use state::*;
pub use core_options::*;

View file

@ -187,7 +187,8 @@ impl ProcessStore {
send_error!("{err:?}");
}
command.stdout(Stdio::piped())
command
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null());
@ -324,7 +325,10 @@ async fn run_hook(hook: &str, cwd: PathBuf) -> LauncherResult<Option<i32>> {
}
}
async fn wait_for_exit(cluster_id: ClusterId, child: &Arc<RwLock<ChildType>>) -> LauncherResult<i32> {
async fn wait_for_exit(
cluster_id: ClusterId,
child: &Arc<RwLock<ChildType>>,
) -> LauncherResult<i32> {
let mut last_updated = Utc::now();
loop {

View file

@ -1,9 +1,11 @@
use std::{ops::Deref, sync::Arc};
use std::ops::Deref;
use std::sync::Arc;
use anyhow::anyhow;
use tokio::sync::OnceCell;
use crate::{api::proxy::LauncherProxy, LauncherResult};
use crate::LauncherResult;
use crate::api::proxy::LauncherProxy;
static PROXY_STATE: OnceCell<Arc<ProxyState>> = OnceCell::const_new();
@ -38,4 +40,4 @@ impl ProxyState {
.ok_or_else(|| anyhow!("proxy state not initialized"))
.cloned()?)
}
}
}

View file

@ -5,12 +5,15 @@ use tokio::sync::{OnceCell, Semaphore};
static STORE_STATE: OnceCell<Arc<SemaphoreStore>> = OnceCell::const_new();
pub struct SemaphoreStore {
pub fetch: Arc<Semaphore>
pub fetch: Arc<Semaphore>,
}
impl SemaphoreStore {
pub async fn get() -> Arc<Self> {
STORE_STATE.get_or_init(|| async { Self::initialize() }).await.clone()
STORE_STATE
.get_or_init(|| async { Self::initialize() })
.await
.clone()
}
#[tracing::instrument]
@ -23,4 +26,4 @@ impl SemaphoreStore {
pub async fn fetch() -> Arc<Semaphore> {
Self::get().await.fetch.clone()
}
}
}

View file

@ -1,7 +1,8 @@
use onelauncher_entity::setting_profiles;
use serde::{Deserialize, Serialize};
use crate::{send_warning, utils::io, LauncherResult};
use crate::utils::io;
use crate::{LauncherResult, send_warning};
use super::Dirs;
@ -15,7 +16,7 @@ pub struct Settings {
pub discord_enabled: bool,
pub max_concurrent_requests: usize,
pub settings_version: u32,
pub native_window_frame: bool
pub native_window_frame: bool,
}
impl Default for Settings {
@ -40,7 +41,7 @@ impl Settings {
Err(err) => {
send_warning!("Failed to read settings file: {}", err);
Self::default()
},
}
}
}
@ -57,4 +58,4 @@ impl Settings {
Ok(())
}
}
}

View file

@ -3,15 +3,15 @@ use std::sync::Arc;
use sea_orm::DatabaseConnection;
use tokio::sync::{OnceCell, RwLock};
use crate::store::Dirs;
use crate::LauncherResult;
use crate::store::Dirs;
use super::credentials::CredentialsStore;
use super::processes::ProcessStore;
use super::Settings;
use super::credentials::CredentialsStore;
use super::discord::DiscordRPC;
use super::ingress::IngressProcessor;
use super::metadata::Metadata;
use super::processes::ProcessStore;
/// The static [`OnceCell<RwLock<State>>`] for storing the global runtime launcher state.
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
@ -32,8 +32,7 @@ impl State {
Ok(LAUNCHER_STATE
.get_or_try_init(Self::initialize)
.await?
.clone()
)
.clone())
}
pub fn initialized() -> bool {

View file

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

View file

@ -12,9 +12,9 @@ use tokio_stream::StreamExt;
use crate::api::ingress::IngressSendExt;
use crate::error::LauncherResult;
use crate::store::Core;
use crate::store::ingress::{SubIngress, SubIngressExt};
use crate::store::semaphore::SemaphoreStore;
use crate::store::Core;
use super::crypto::{CryptoError, HashAlgorithm};
use super::io;
@ -82,13 +82,15 @@ pub async fn fetch_advanced(
match req.send().await {
Err(err) => {
if attempt <= Core::get().fetch_attempts {
tracing::error!("error occurred whilst fetching on attempt {attempt}: {err} retrying request...");
tracing::error!(
"error occurred whilst fetching on attempt {attempt}: {err} retrying request..."
);
continue;
}
return Err(err.into())
},
Ok(res) => break res
return Err(err.into());
}
Ok(res) => break res,
}
};
@ -102,13 +104,12 @@ pub async fn fetch_advanced(
let mut bytes = Vec::new();
while let Some(item) = stream.next().await {
let chunk =
item.or(Err(anyhow::anyhow!("no value for fetch bytes")))?;
let chunk = item.or(Err(anyhow::anyhow!("no value for fetch bytes")))?;
bytes.append(&mut chunk.to_vec());
ingress.send_ingress(
(chunk.len() as f64 / total_size as f64) * ingress.total,
).await?;
ingress
.send_ingress((chunk.len() as f64 / total_size as f64) * ingress.total)
.await?;
}
Ok(Bytes::from(bytes))
@ -131,7 +132,7 @@ pub async fn fetch_advanced(
}
.into());
}
},
}
Err(err) => {
tracing::error!("failed to calculate hash for {url}: {err}");
return Err(err.into());
@ -178,7 +179,10 @@ pub async fn download_advanced(
ingress: Option<&SubIngress<'_>>,
) -> LauncherResult<Bytes> {
const TASKS: f64 = 3.0;
let ingress_step = ingress.ingress_total().map(|total| total / TASKS).unwrap_or_default();
let ingress_step = ingress
.ingress_total()
.map(|total| total / TASKS)
.unwrap_or_default();
let bytes = fetch_advanced(
method,
@ -209,7 +213,7 @@ pub async fn download(
url: &str,
path: impl AsRef<Path>,
hash: Option<(HashAlgorithm, &str)>,
ingress: Option<&SubIngress<'_>>
ingress: Option<&SubIngress<'_>>,
) -> LauncherResult<Bytes> {
download_advanced(method, url, path, None, None, hash, ingress).await
}

View file

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

View file

@ -1,10 +1,11 @@
use std::{path::PathBuf, sync::Arc};
use std::path::PathBuf;
use std::sync::Arc;
use async_compression::tokio::bufread::GzipDecoder;
use async_stream::try_stream;
use async_zip::StoredZipEntry;
use async_zip::base::read::WithoutEntry;
use futures::{pin_mut, Stream, TryStreamExt};
use futures::{Stream, TryStreamExt, pin_mut};
use serde::Serialize;
use serde::de::DeserializeOwned;
use tempfile::TempDir;
@ -27,11 +28,23 @@ pub enum IOError {
path: String,
},
#[error(transparent)]
IOError(#[from] #[skip] std::io::Error),
IOError(
#[from]
#[skip]
std::io::Error,
),
#[error("json deserialization error: {0}")]
DeserializeError(#[from] #[skip] serde_json::Error),
DeserializeError(
#[from]
#[skip]
serde_json::Error,
),
#[error(transparent)]
AsyncZipError(#[from] #[skip] async_zip::error::ZipError),
AsyncZipError(
#[from]
#[skip]
async_zip::error::ZipError,
),
}
impl<P: AsRef<std::path::Path>> From<(P, std::io::Error)> for IOError {
@ -283,9 +296,16 @@ where
/// Reads a zip archive from a byte array and returns a stream of entries.
pub fn stream_zip_entries_bytes(
data: Vec<u8>,
) -> impl Stream<Item = Result<(
usize, StoredZipEntry, Arc<async_zip::base::read::mem::ZipFileReader>
), IOError>> {
) -> impl Stream<
Item = Result<
(
usize,
StoredZipEntry,
Arc<async_zip::base::read::mem::ZipFileReader>,
),
IOError,
>,
> {
try_stream! {
let reader = Arc::new(async_zip::base::read::mem::ZipFileReader::new(data).await?);
let entries = reader.file().entries();
@ -319,9 +339,10 @@ pub async fn unzip_bytes_filtered(
let file_name = entry.filename().as_str()?;
if let Some(filter) = &filter_entries
&& !filter(file_name) {
continue;
}
&& !filter(file_name)
{
continue;
}
let path = dest_path.as_ref().join(sanitize_path(file_name));

View file

@ -1,6 +1,6 @@
pub mod crypto;
pub mod os_ext;
pub mod http;
pub mod icon;
pub mod io;
pub mod pagination;
pub mod os_ext;
pub mod pagination;

View file

@ -45,4 +45,4 @@ impl OsExt for Os {
_ => Self::Unknown,
}
}
}
}

View file

@ -1,11 +1,13 @@
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::marker::Send;
use serde::{Deserialize, Serialize};
#[onelauncher_macro::specta]
#[derive(Clone, Serialize, Deserialize)]
pub struct Paginated<T>
where T: Clone + Serialize + Send + Sync {
where
T: Clone + Serialize + Send + Sync,
{
pub total: usize,
pub offset: usize,
pub limit: usize,
@ -23,9 +25,14 @@ impl<T: Clone + Serialize + Send + Sync> Debug for Paginated<T> {
}
}
impl <T: Clone + Serialize + Send + Sync> Paginated<T> {
impl<T: Clone + Serialize + Send + Sync> Paginated<T> {
#[must_use]
pub const fn new(total: usize, offset: usize, limit: usize, items: Vec<T>) -> Self {
Self { total, offset, limit, items }
Self {
total,
offset,
limit,
items,
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields};
use syn::{Data, DeriveInput, Fields, parse_macro_input};
pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let input = parse_macro_input!(item as DeriveInput);
let Data::Enum(data_enum) = &input.data else {
return syn::Error::new_spanned(&input.ident, "Only enums are supported")
@ -98,82 +98,76 @@ pub fn error_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
};
let output = quote! {
#[cfg_attr(feature = "specta", derive(onelauncher_macro::SerializedError))]
#input
let output = quote! {
#[cfg_attr(feature = "specta", derive(onelauncher_macro::SerializedError))]
#input
#serialize_impl
};
};
output.into()
output.into()
}
pub fn error_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let input = parse_macro_input!(input as DeriveInput);
let enum_name = &input.ident;
let specta_name = format_ident!("{}SpectaType", enum_name);
let mut variants = vec![];
let enum_name = &input.ident;
let specta_name = format_ident!("{}SpectaType", enum_name);
let mut variants = vec![];
let Data::Enum(data_enum) = &input.data else {
return syn::Error::new_spanned(enum_name, "Only enums are supported")
.to_compile_error()
.into();
};
let Data::Enum(data_enum) = &input.data else {
return syn::Error::new_spanned(enum_name, "Only enums are supported")
.to_compile_error()
.into();
};
for variant in &data_enum.variants {
let name = &variant.ident;
let ty = match &variant.fields {
Fields::Unit => quote! { String },
for variant in &data_enum.variants {
let name = &variant.ident;
let ty = match &variant.fields {
Fields::Unit => quote! { String },
Fields::Unnamed(fields) => {
// Handle newtype-style or tuple-like enums
if fields.unnamed.len() == 1 {
let field = &fields.unnamed[0];
let field_ty = &field.ty;
Fields::Unnamed(fields) => {
// Handle newtype-style or tuple-like enums
if fields.unnamed.len() == 1 {
let field = &fields.unnamed[0];
let field_ty = &field.ty;
let has_from = field
.attrs
.iter()
.any(|a| a.path().is_ident("from"));
let has_from = field.attrs.iter().any(|a| a.path().is_ident("from"));
let has_skip = field
.attrs
.iter()
.any(|a| a.path().is_ident("skip"));
let has_skip = field.attrs.iter().any(|a| a.path().is_ident("skip"));
if has_from && !has_skip {
quote! {
if has_from && !has_skip {
quote! {
#field_ty
}
} else {
quote! { String }
}
} else {
quote! { String }
}
}
} else {
quote! { String }
}
} else {
quote! { String }
}
}
Fields::Named(_) => {
quote! { String } // Simplification: treat all struct-like variants as String
}
};
Fields::Named(_) => {
quote! { String } // Simplification: treat all struct-like variants as String
}
};
variants.push(quote! {
#name(#ty)
});
}
variants.push(quote! {
#name(#ty)
});
}
let enum_name_str = enum_name.to_string();
let output = quote! {
#[derive(Debug, specta::Type, serde::Serialize)]
#[specta(remote = #enum_name)]
#[derive(Debug, specta::Type, serde::Serialize)]
#[specta(remote = #enum_name)]
#[specta(tag = "type", content = "data")]
#[specta(rename = #enum_name_str)]
pub enum #specta_name {
#(#variants,)*
}
};
pub enum #specta_name {
#(#variants,)*
}
};
output.into()
}
output.into()
}

View file

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

View file

@ -1,7 +1,10 @@
use quote::quote;
use syn::{parse_macro_input, ItemFn};
use syn::{ItemFn, parse_macro_input};
pub fn pin(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
pub fn pin(
_attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let input = parse_macro_input!(item as ItemFn);
let attrs = &input.attrs;
@ -19,4 +22,4 @@ pub fn pin(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> pro
};
result.into()
}
}

View file

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

View file

@ -15,8 +15,8 @@ interface TransitionProps {
}
interface AnimatedOutletProps {
enter: TransitionProps;
exit: TransitionProps;
enter?: TransitionProps;
exit?: TransitionProps;
transition?: MotionProps['transition'];
from: AnyRoute['id'];
clone?: boolean;
@ -126,10 +126,10 @@ export function AnimatedOutlet({
<div className="relative w-full h-full">
{snapshots.map(snapshot => (
<motion.div
animate={exit.animate}
animate={exit?.animate || { opacity: 1 }}
aria-hidden="true"
className="absolute inset-0 pointer-events-none w-full h-full"
initial={exit.initial}
initial={exit?.initial || { opacity: 1 }}
key={snapshot.id}
onAnimationComplete={() => handleAnimationComplete(snapshot.id)}
ref={(el) => {
@ -141,9 +141,9 @@ export function AnimatedOutlet({
))}
<motion.div
animate={enter.animate}
animate={enter?.animate}
className="relative w-full h-full"
initial={enter.initial}
initial={enter?.initial}
key={nextId.current}
ref={outletRef}
transition={transition}

View file

@ -11,6 +11,8 @@ type RegisterConfig = Register extends {
commands: infer Keys;
} ? Keys : never;
type CommandCacheKey = RegisterConfig[number] | (string & {}) | false;
async function fetchCommand<T>(command: () => Promise<T>): Promise<T> {
try {
const result = await command();
@ -26,12 +28,15 @@ async function fetchCommand<T>(command: () => Promise<T>): Promise<T> {
}
}
export function useCommand<T>(
cacheKey: RegisterConfig[number] | (string & {}) | false,
command: () => Promise<T>,
options?: Omit<UndefinedInitialDataOptions<T>, 'queryKey' | 'queryFn'>,
): UseQueryResult<T, Error> {
return useQuery({
export function useCommand<TQueryFnData, TSelected = TQueryFnData>(
cacheKey: CommandCacheKey,
command: () => Promise<TQueryFnData>,
options?: Omit<
UndefinedInitialDataOptions<TQueryFnData, Error, TSelected, [CommandCacheKey] | []>,
'queryKey' | 'queryFn'
>,
): UseQueryResult<TSelected, Error> {
return useQuery<TQueryFnData, Error, TSelected, [CommandCacheKey] | []>({
queryKey: cacheKey === false ? [] : [cacheKey],
queryFn: () => fetchCommand(command),
enabled: true,
@ -39,24 +44,35 @@ export function useCommand<T>(
});
}
export function useCommandSuspense<T>(
cacheKey: RegisterConfig[number] | (string & {}) | false,
command: () => Promise<T>,
options?: Omit<UseSuspenseQueryOptions<T>, 'queryKey' | 'queryFn'>,
): UseSuspenseQueryResult<T, Error> {
return useSuspenseQuery({
export function useCommandSuspense<TQueryFnData, TSelected = TQueryFnData>(
cacheKey: CommandCacheKey,
command: () => Promise<TQueryFnData>,
options?: Omit<
UseSuspenseQueryOptions<TQueryFnData, Error, TSelected, [CommandCacheKey] | []>,
'queryKey' | 'queryFn'
>,
): UseSuspenseQueryResult<TSelected, Error> {
return useSuspenseQuery<TQueryFnData, Error, TSelected, [CommandCacheKey] | []>({
queryKey: cacheKey === false ? [] : [cacheKey],
queryFn: () => fetchCommand(command),
...options,
});
}
export function useCommandMut<TData, TError = Error, TVariables = void, TContext = unknown>(
command: () => Promise<TData>,
options?: Omit<UseMutationOptions<TData, TError, TVariables, TContext>, 'mutationFn'>,
export function useCommandMut<
TData,
TError = Error,
TVariables = void,
TContext = unknown,
>(
command: (variables: TVariables) => Promise<TData>,
options?: Omit<
UseMutationOptions<TData, TError, TVariables, TContext>,
'mutationFn'
>,
): UseMutationResult<TData, TError, TVariables, TContext> {
return useMutation<TData, TError, TVariables, TContext>({
mutationFn: () => fetchCommand(command),
mutationFn: command,
...options,
});
}

Some files were not shown because too many files have changed in this diff Show more