Files
Starlight_Launcher/packages/app-lib/src/event/mod.rs
T
Disy bc904065c3
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
feat: complete hosted mod sync and launcher interface updates
Add pack sync markers, tagged mod updates, parallel progress, JWT downloads and retry recovery. Include pending onboarding, about scene, compatibility data pack and download fixes.
2026-09-15 19:06:56 +08:00

429 lines
11 KiB
Rust

//! Theseus state management system
use ariadne::ids::UserId;
use ariadne::users::UserStatus;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
#[cfg(feature = "tauri")]
use tauri::Emitter;
use tokio::sync::OnceCell;
use uuid::Uuid;
pub mod emit;
// Global event state
// Stores the Tauri app handle and other event-related state variables
static EVENT_STATE: OnceCell<Arc<EventState>> = OnceCell::const_new();
pub struct EventState {
/// Tauri app
#[cfg(feature = "tauri")]
pub app: tauri::AppHandle,
pub loading_bars: DashMap<Uuid, LoadingBar>,
}
impl EventState {
#[cfg(feature = "tauri")]
pub async fn init(app: tauri::AppHandle) -> crate::Result<Arc<Self>> {
EVENT_STATE
.get_or_try_init(|| async {
Ok(Arc::new(Self {
app,
loading_bars: DashMap::new(),
}))
})
.await
.cloned()
}
#[cfg(not(feature = "tauri"))]
pub async fn init() -> crate::Result<Arc<Self>> {
EVENT_STATE
.get_or_try_init(|| async {
Ok(Arc::new(Self {
loading_bars: DashMap::new(),
}))
})
.await
.cloned()
}
pub fn get() -> crate::Result<Arc<Self>> {
Ok(EVENT_STATE.get().ok_or(EventError::NotInitialized)?.clone())
}
// Values provided should not be used directly, as they are clones and are not guaranteed to be up-to-date
pub async fn list_progress_bars() -> crate::Result<DashMap<Uuid, LoadingBar>>
{
let value = Self::get()?;
Ok(value.loading_bars.clone())
}
#[cfg(feature = "tauri")]
pub async fn get_main_window() -> crate::Result<Option<tauri::WebviewWindow>>
{
use tauri::Manager;
let value = Self::get()?;
Ok(value.app.get_webview_window("main"))
}
}
#[derive(Serialize, Debug, Clone)]
pub struct LoadingBar {
// loading_bar_uuid not be used directly by external functions as it may not reflect the current state of the loading bar/hashmap
pub loading_bar_uuid: Uuid,
pub message: String,
pub total: f64,
pub current: f64,
#[serde(skip)]
pub last_sent: f64,
pub bar_type: LoadingBarType,
#[cfg(feature = "cli")]
#[serde(skip)]
pub cli_progress_bar: indicatif::ProgressBar,
}
#[derive(Serialize, Debug, Clone)]
pub struct LoadingBarId(Uuid);
// When Loading bar id is dropped, we should remove it from the hashmap
impl Drop for LoadingBarId {
fn drop(&mut self) {
let loader_uuid = self.0;
tokio::spawn(async move {
if let Ok(event_state) = EventState::get() {
#[cfg(any(feature = "tauri", feature = "cli"))]
if let Some((_, bar)) =
event_state.loading_bars.remove(&loader_uuid)
{
#[cfg(feature = "tauri")]
{
let loader_uuid = bar.loading_bar_uuid;
let event = bar.bar_type.clone();
let fraction = bar.current / bar.total;
let _ = event_state.app.emit(
"loading",
LoadingPayload {
fraction: None,
message: "Completed".to_string(),
event,
loader_uuid,
total: Some(bar.total),
},
);
tracing::trace!(
"Exited at {fraction} for loading bar: {:?}",
loader_uuid
);
}
// Emit event to indicatif progress bar arc
#[cfg(feature = "cli")]
{
let cli_progress_bar = bar.cli_progress_bar;
cli_progress_bar.finish();
}
}
#[cfg(not(any(feature = "tauri", feature = "cli")))]
event_state.loading_bars.remove(&loader_uuid);
}
});
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum LoadingBarType {
LegacyDataMigration,
HostedPackSync {
instance_id: String,
instance_name: String,
error: Option<String>,
},
HostedModDownload {
instance_id: String,
instance_name: String,
batch_id: String,
file_name: String,
error: Option<String>,
},
DirectoryMove {
old: PathBuf,
new: PathBuf,
},
JavaDownload {
version: u32,
},
PackFileDownload {
instance_id: String,
pack_name: String,
icon: Option<String>,
pack_version: String,
},
PackDownload {
instance_id: String,
pack_name: String,
icon: Option<PathBuf>,
pack_id: Option<String>,
pack_version: Option<String>,
},
MinecraftDownload {
instance_id: String,
instance_name: String,
},
InstanceUpdate {
instance_id: String,
instance_name: String,
},
ZipExtract {
instance_id: String,
instance_name: String,
},
ConfigChange {
new_path: PathBuf,
},
CopyInstance {
import_location: PathBuf,
instance_name: String,
},
LauncherUpdate {
version: String,
current_version: String,
},
}
#[derive(Serialize, Clone)]
#[cfg(feature = "tauri")]
pub struct LoadingPayload {
pub event: LoadingBarType,
pub loader_uuid: Uuid,
pub fraction: Option<f64>, // by convention, if optional, it means the loading is done
pub message: String,
pub total: Option<f64>,
}
#[derive(Serialize, Clone)]
#[cfg(feature = "tauri")]
pub struct WarningPayload {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instance_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instance_name: Option<String>,
}
#[derive(Serialize, Clone)]
#[cfg(feature = "tauri")]
pub struct JavaDiscoveryPayload {
pub count: usize,
}
#[derive(Serialize, Clone)]
#[cfg(feature = "tauri")]
#[serde(rename_all = "camelCase")]
pub struct JavaDownloadConfirmationPayload {
pub request_id: Uuid,
pub version: u32,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct InstanceBulkUpdateProgressPayload {
pub instance_id: String,
pub stage: InstanceBulkUpdateProgressStage,
pub current: usize,
pub total: usize,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum InstanceBulkUpdateProgressStage {
ResolvingVersions,
Downloading,
Finishing,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(tag = "event")]
pub enum CommandPayload {
InstallMod {
id: String,
},
InstallVersion {
id: String,
},
InstallModpack {
id: String,
},
InstallServer {
id: String,
},
LaunchInstance {
id: String,
server: Option<String>,
singleplayer_world: Option<String>,
},
RunMRPack {
// run or install .mrpack
path: PathBuf,
},
OpenSeedMap {
// URL query string describing the shared seed-map state
query: String,
},
}
#[derive(Serialize, Clone)]
#[cfg(feature = "tauri")]
pub struct ProcessPayload {
pub instance_id: String,
pub uuid: Uuid,
pub pid: u32,
pub maximize_window: bool,
pub event: ProcessPayloadType,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub crashed: Option<bool>,
}
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ProcessPayloadType {
Launched,
Finished,
}
#[derive(Serialize, Clone)]
#[cfg(feature = "tauri")]
pub struct ServerPayload {
#[serde(rename = "serverId")]
pub server_id: String,
#[serde(flatten)]
pub event: ServerPayloadType,
}
/// Classifies why a server process exited on its own, derived from the tail
/// of its console output so the UI can react (e.g. offer the EULA dialog)
/// instead of just reporting a dead process.
#[derive(Serialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ExitReason {
Eula,
}
#[derive(Serialize, Clone, Debug)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum ServerPayloadType {
Log {
line: String,
},
ConsoleOutput {
data: String,
},
DownloadProgress {
downloaded: u64,
#[serde(skip_serializing_if = "Option::is_none")]
total: Option<u64>,
},
Started,
Stopped {
crashed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
reason: Option<ExitReason>,
},
#[allow(dead_code)]
EulaRequired {
server_id: String,
eula_text: String,
},
}
#[derive(Serialize, Clone)]
#[cfg(feature = "tauri")]
pub struct InstancePayload {
pub instance_id: String,
#[serde(flatten)]
pub event: InstancePayloadType,
}
#[derive(Serialize, Clone)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum InstancePayloadType {
Created,
Synced,
ContentChanged {
revision: u64,
},
ServersUpdated,
WorldUpdated {
world: String,
},
ServerJoined {
host: String,
port: u16,
timestamp: DateTime<Utc>,
},
Edited,
ContentInstallFinished {
project_ids: Vec<String>,
#[serde(default)]
dependency_project_ids: Vec<String>,
},
ContentInstallFailed {
project_ids: Vec<String>,
message: String,
},
Removed,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "event")]
pub enum FriendPayload {
FriendRequest { from: UserId },
UserOffline { id: UserId },
StatusUpdate { user_status: UserStatus },
StatusSync,
}
#[cfg(feature = "tauri")]
pub use self::log_types::*;
#[cfg(feature = "tauri")]
mod log_types {
use crate::state::Log4jEvent;
use serde::Serialize;
#[derive(Serialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum LogEvent {
Log4j(Log4jEvent),
Legacy { message: String },
}
#[derive(Serialize, Clone)]
pub struct LogPayload {
pub instance_id: String,
#[serde(flatten)]
pub event: LogEvent,
}
}
#[derive(Debug, thiserror::Error)]
pub enum EventError {
#[error("Event state was not properly initialized")]
NotInitialized,
#[error("Non-existent loading bar of key: {0}")]
NoLoadingBar(Uuid),
#[cfg(feature = "tauri")]
#[error("Tauri error: {0}")]
TauriError(#[from] tauri::Error),
}