Merge branch 'oneclient/main' into onelauncher/main
This commit is contained in:
commit
0d77715a78
102 changed files with 2430 additions and 1235 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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(|_, _| {});
|
||||
}
|
||||
|
||||
|
|
|
|||
74
apps/oneclient/desktop/src/oneclient/mod.rs
Normal file
74
apps/oneclient/desktop/src/oneclient/mod.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
52
apps/oneclient/frontend/src/components/Tabs.tsx
Normal file
52
apps/oneclient/frontend/src/components/Tabs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,3 +2,4 @@ export * from './GameBackground';
|
|||
export * from './Loader';
|
||||
export * from './Navbar';
|
||||
export * from './Spinner';
|
||||
export * from './Tabs';
|
||||
|
|
|
|||
27
apps/oneclient/frontend/src/routes/app/cluster/overview.tsx
Normal file
27
apps/oneclient/frontend/src/routes/app/cluster/overview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
132
apps/oneclient/frontend/src/routes/app/cluster/route.tsx
Normal file
132
apps/oneclient/frontend/src/routes/app/cluster/route.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
269
apps/oneclient/frontend/src/routes/app/clusters.tsx
Normal file
269
apps/oneclient/frontend/src/routes/app/clusters.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
45
apps/oneclient/frontend/src/stores/clusterStore.ts
Normal file
45
apps/oneclient/frontend/src/stores/clusterStore.ts
Normal 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;
|
||||
18
apps/oneclient/frontend/src/utils/loaders.ts
Normal file
18
apps/oneclient/frontend/src/utils/loaders.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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: '';
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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?)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@ mod events;
|
|||
pub use events::*;
|
||||
|
||||
mod commands;
|
||||
pub use commands::*;
|
||||
pub use commands::*;
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {:?}",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -45,4 +45,4 @@ impl OsExt for Os {
|
|||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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://")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue