Files
Starlight_Lancher/packages/app-lib/src/event/emit.rs
Disy bc904065c3
Some checks failed
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

577 lines
18 KiB
Rust

use super::{FriendPayload, LoadingBarId};
use crate::event::{
CommandPayload, EventError, InstanceBulkUpdateProgressPayload,
InstancePayloadType, LoadingBar, LoadingBarType, ProcessPayloadType,
ServerPayloadType,
};
#[cfg(feature = "tauri")]
use crate::event::{
InstancePayload, JavaDiscoveryPayload, JavaDownloadConfirmationPayload,
LoadingPayload, ProcessPayload, ServerPayload, WarningPayload,
};
use futures::prelude::*;
use serde_json::Value;
#[cfg(feature = "tauri")]
use std::sync::LazyLock;
#[cfg(feature = "tauri")]
use tauri::{Emitter, Manager};
#[cfg(feature = "tauri")]
use tokio::sync::oneshot;
use uuid::Uuid;
#[cfg(feature = "cli")]
const CLI_PROGRESS_BAR_TOTAL: u64 = 1000;
#[cfg(feature = "tauri")]
static JAVA_DOWNLOAD_CONFIRMATIONS: LazyLock<
dashmap::DashMap<Uuid, oneshot::Sender<bool>>,
> = LazyLock::new(dashmap::DashMap::new);
#[cfg(feature = "tauri")]
struct PendingJavaDownloadConfirmation(Uuid);
#[cfg(feature = "tauri")]
impl Drop for PendingJavaDownloadConfirmation {
fn drop(&mut self) {
JAVA_DOWNLOAD_CONFIRMATIONS.remove(&self.0);
}
}
/*
Events are a way we can communicate with the Tauri frontend from the Rust backend.
We include a feature flag for Tauri, so that we can compile this code without Tauri.
To use events, we need to do the following:
1) Make sure we are using the tauri feature flag
2) Initialize the EventState with EventState::init() *before* initializing the theseus State
3) Call emit_x functions to send events to the frontend
For emit_loading() specifically, we need to initialize the loading bar with init_loading() first and pass the received loader in
For example:
pub async fn loading_function() -> crate::Result<()> {
loading_function()).await;
}
pub async fn loading_function() -> crate::Result<()> {
let loading_bar = init_loading(LoadingBarType::StateInit, 100.0, "Loading something long...").await;
for i in 0..100 {
emit_loading(&loading_bar, 1.0, None)?;
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
*/
/// Initialize a loading bar for use in emit_loading
/// This will generate a LoadingBarId, which is used to refer to the loading bar uniquely.
/// total is the total amount of work to be done- all emissions will be considered a fraction of this value (should be 1 or 100 for simplicity)
/// title is the title of the loading bar
/// The app will wait for this loading bar to finish before exiting, as it is considered safe.
pub async fn init_loading(
bar_type: LoadingBarType,
total: f64,
title: &str,
) -> crate::Result<LoadingBarId> {
let key = init_loading_unsafe(bar_type, total, title).await?;
Ok(key)
}
/// An unsafe loading bar can be created without adding it to the SafeProcesses list,
/// meaning that the app won't ask to wait for it to finish before exiting.
pub async fn init_loading_unsafe(
bar_type: LoadingBarType,
total: f64,
title: &str,
) -> crate::Result<LoadingBarId> {
let event_state = crate::EventState::get()?;
let key = LoadingBarId(Uuid::new_v4());
event_state.loading_bars.insert(
key.0,
LoadingBar {
loading_bar_uuid: key.0,
message: title.to_string(),
total,
current: 0.0,
last_sent: 0.0,
bar_type,
#[cfg(feature = "cli")]
cli_progress_bar: {
let pb = indicatif::ProgressBar::new(CLI_PROGRESS_BAR_TOTAL);
pb.set_position(0);
pb.set_style(
indicatif::ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}",
).unwrap()
.progress_chars("#>-"),
);
pb
},
},
);
// attempt an initial loading_emit event to the frontend
emit_loading(&key, 0.0, None)?;
Ok(key)
}
// emit_loading emits a loading event to the frontend
// key refers to the loading bar to update
// increment refers to by what relative increment to the loading struct's total to update
// message is the message to display on the loading bar- if None, use the loading bar's default one
// By convention, fraction is the fraction of the progress bar that is filled
#[tracing::instrument(level = "debug")]
pub fn emit_loading(
key: &LoadingBarId,
increment_frac: f64,
message: Option<&str>,
) -> crate::Result<()> {
let event_state = crate::EventState::get()?;
let Some(mut loading_bar) = event_state.loading_bars.get_mut(&key.0) else {
return Err(EventError::NoLoadingBar(key.0).into());
};
// Tick up loading bar
loading_bar.current += increment_frac;
let display_frac = loading_bar.current / loading_bar.total;
if f64::abs(display_frac - loading_bar.last_sent) > 0.005 {
// Emit event to indicatif progress bar
#[cfg(feature = "cli")]
{
loading_bar.cli_progress_bar.set_message(
message
.map(|x| x.to_string())
.unwrap_or(loading_bar.message.clone()),
);
loading_bar.cli_progress_bar.set_position(
(display_frac * CLI_PROGRESS_BAR_TOTAL as f64).round() as u64,
);
}
//Emit event to tauri
#[cfg(feature = "tauri")]
event_state
.app
.emit(
"loading",
LoadingPayload {
fraction: if display_frac >= 1.0 {
None // by convention, when its done, we submit None
// any further updates will be ignored (also sending None)
} else {
Some(display_frac)
},
message: message
.unwrap_or(&loading_bar.message)
.to_string(),
event: loading_bar.bar_type.clone(),
loader_uuid: loading_bar.loading_bar_uuid,
total: Some(loading_bar.total),
},
)
.map_err(EventError::from)?;
#[cfg(not(any(feature = "cli", feature = "tauri")))]
let _ = message;
loading_bar.last_sent = display_frac;
}
Ok(())
}
/// Set measured phase progress without completing the task at a phase boundary.
/// A zero total represents a phase whose amount of work is not yet known.
pub fn set_loading(
key: &LoadingBarId,
current: u64,
total: u64,
message: &str,
) -> crate::Result<()> {
let event_state = crate::EventState::get()?;
let Some(mut bar) = event_state.loading_bars.get_mut(&key.0) else {
return Err(EventError::NoLoadingBar(key.0).into());
};
bar.current = current.min(total) as f64;
bar.total = total as f64;
bar.message = message.to_owned();
let fraction = if total == 0 {
0.0
} else {
bar.current / bar.total
};
bar.last_sent = fraction;
#[cfg(feature = "tauri")]
event_state
.app
.emit(
"loading",
LoadingPayload {
fraction: Some(fraction),
message: bar.message.clone(),
event: bar.bar_type.clone(),
loader_uuid: bar.loading_bar_uuid,
total: Some(bar.total),
},
)
.map_err(EventError::from)?;
#[cfg(feature = "cli")]
{
bar.cli_progress_bar.set_message(bar.message.clone());
bar.cli_progress_bar
.set_position((fraction * CLI_PROGRESS_BAR_TOTAL as f64) as u64);
}
Ok(())
}
pub fn fail_hosted_loading(key: &LoadingBarId, error: &str) {
if let Ok(state) = crate::EventState::get()
&& let Some(mut bar) = state.loading_bars.get_mut(&key.0)
{
match &mut bar.bar_type {
LoadingBarType::HostedPackSync { error: failure, .. }
| LoadingBarType::HostedModDownload { error: failure, .. } => {
*failure = Some(error.to_owned())
}
_ => {}
}
}
}
// emit_warning(message)
pub async fn emit_warning(message: &str) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit(
"warning",
WarningPayload {
message: message.to_string(),
kind: None,
instance_id: None,
instance_name: None,
},
)
.map_err(EventError::from)?;
}
tracing::warn!("{}", message);
Ok(())
}
pub async fn emit_minecraft_crash_warning(
instance_id: &str,
instance_name: &str,
) -> crate::Result<()> {
let message = format!("Instance {instance_name} has crashed");
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit(
"warning",
WarningPayload {
message: message.clone(),
kind: Some("minecraft_crash".to_string()),
instance_id: Some(instance_id.to_string()),
instance_name: Some(instance_name.to_string()),
},
)
.map_err(EventError::from)?;
}
tracing::warn!(instance_id = instance_id, "{}", message);
Ok(())
}
// emit_java_discovery_update(count)
// Fired when a Java rescan changed the set of discovered installations
#[allow(unused_variables)]
pub async fn emit_java_discovery_update(count: usize) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit("java_discovery_update", JavaDiscoveryPayload { count })
.map_err(EventError::from)?;
}
tracing::debug!("Java discovery updated: {count} installations");
Ok(())
}
#[allow(unused_variables)]
pub async fn request_java_download_confirmation(
version: u32,
) -> crate::Result<bool> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
let request_id = Uuid::new_v4();
let (sender, receiver) = oneshot::channel();
JAVA_DOWNLOAD_CONFIRMATIONS.insert(request_id, sender);
let _pending = PendingJavaDownloadConfirmation(request_id);
event_state
.app
.emit(
"java_download_confirmation",
JavaDownloadConfirmationPayload {
request_id,
version,
},
)
.map_err(EventError::from)?;
Ok(receiver.await.unwrap_or(false))
}
#[cfg(not(feature = "tauri"))]
Ok(true)
}
#[cfg(feature = "tauri")]
pub fn respond_to_java_download_confirmation(
request_id: Uuid,
approved: bool,
) -> bool {
JAVA_DOWNLOAD_CONFIRMATIONS
.remove(&request_id)
.is_some_and(|(_, sender)| sender.send(approved).is_ok())
}
#[allow(unused_variables)]
pub async fn emit_instance_bulk_update_progress(
payload: InstanceBulkUpdateProgressPayload,
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit("instance_bulk_update_progress", payload)
.map_err(EventError::from)?;
}
Ok(())
}
// emit_command(CommandPayload::Something { something })
// ie: installing a pack, opening an .mrpack, etc
// Generally used for url deep links and file opens that we want to handle in the frontend
pub async fn emit_command(command: CommandPayload) -> crate::Result<()> {
tracing::debug!("Command: {}", serde_json::to_string(&command)?);
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit("command", command)
.map_err(EventError::from)?;
if let Some(window) = event_state.app.get_window("main") {
let _ = window.set_focus();
}
}
Ok(())
}
// emit_process(instance_id, uuid, event, message, crashed)
#[allow(unused_variables)]
pub async fn emit_process(
instance_id: &str,
uuid: Uuid,
pid: u32,
maximize_window: bool,
event: ProcessPayloadType,
message: &str,
crashed: Option<bool>,
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit(
"process",
ProcessPayload {
instance_id: instance_id.to_string(),
uuid,
pid,
maximize_window,
event,
message: message.to_string(),
crashed,
},
)
.map_err(EventError::from)?;
}
Ok(())
}
// emit_instance(path, event)
#[allow(unused_variables)]
pub async fn emit_instance(
instance_id: &str,
event: InstancePayloadType,
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit(
"instance",
InstancePayload {
instance_id: instance_id.to_string(),
event,
},
)
.map_err(EventError::from)?;
}
Ok(())
}
// emit_server(server_id, event)
#[allow(unused_variables)]
pub async fn emit_server(
server_id: &str,
event: ServerPayloadType,
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit(
"server",
ServerPayload {
server_id: server_id.to_string(),
event,
},
)
.map_err(EventError::from)?;
}
Ok(())
}
#[allow(unused_variables)]
pub async fn emit_friend(payload: FriendPayload) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit("friend", payload)
.map_err(EventError::from)?;
}
Ok(())
}
#[allow(unused_variables)]
pub async fn emit_notification(payload: Value) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit("notification", payload)
.map_err(EventError::from)?;
}
Ok(())
}
// loading_join! macro
// loading_join!(key: Option<&LoadingBarId>, total: f64, message: Option<&str>; task1, task2, task3...)
// This will submit a loading event with the given message for each task as they complete
// task1, task2, task3 are async tasks that you want to to join on await on
// Key is the key to use for which loading bar to submit these results to- a LoadingBarId. If None, it does nothing
// Total is the total amount of progress that the loading bar should take up by all futures in this (will be split evenly amongst them).
// If message is Some(t) you will overwrite this loading bar's message with a custom one
// For example, if you want the tasks to range as 0.1, 0.2, 0.3 (of the progress bar), you would do:
// loading_join!(loading_bar, 0.1; task1, task2, task3)
// This will await on each of the tasks, and as each completes, it will emit a loading event for 0.033, 0.066, 0.099, etc
// This should function as a drop-in replacement for tokio::try_join_all! in most cases- except the function *itself* calls ? rather than needing it.
#[macro_export]
macro_rules! count {
() => (0usize);
( $x:tt $($xs:tt)* ) => (1usize + $crate::count!($($xs)*));
}
#[macro_export]
macro_rules! loading_join {
($key:expr, $total:expr, $message:expr; $($task:expr $(,)?)+) => {
{
let key = $key;
let message : Option<&str> = $message;
let num_futures = $crate::count!($($task)*);
let increment = $total / num_futures as f64;
paste::paste! {
$( let [ <unique_name $task>] = {
{
let key = key.clone();
let message = message.clone();
async move {
let res = $task.await;
if let Some(key) = key {
$crate::event::emit::emit_loading(key, increment, message)?;
}
res
}
}
};)+
}
paste::paste! {
tokio::try_join! (
$( [ <unique_name $task>] ),+
)
}
}
};
}
// A drop in replacement to try_for_each_concurrent that emits loading events as it goes
// Key is the key to use for which loading bar- a LoadingBarId. If None, does nothing
// Total is the total amount of progress that the loading bar should take up by all futures in this (will be split evenly amongst them).
// If message is Some(t) you will overwrite this loading bar's message with a custom one
// num_futs is the number of futures that will be run, which is needed as we allow Iterator to be passed in, which doesn't have a size
#[tracing::instrument(skip(stream, f))]
pub async fn loading_try_for_each_concurrent<I, F, Fut, T>(
stream: I,
limit: Option<usize>,
key: Option<&LoadingBarId>,
total: f64,
num_futs: usize, // num is in here as we allow Iterator to be passed in, which doesn't have a size
message: Option<&str>,
f: F,
) -> crate::Result<()>
where
I: futures::TryStreamExt<Error = crate::Error> + TryStream<Ok = T>,
F: FnMut(T) -> Fut + Send,
Fut: Future<Output = crate::Result<()>> + Send,
T: Send,
{
let mut f = f;
stream
.try_for_each_concurrent(limit, |item| {
let f = f(item);
async move {
f.await?;
if let Some(key) = key {
emit_loading(key, total / (num_futs as f64), message)?;
}
Ok(())
}
})
.await
}