752 lines
24 KiB
Rust
752 lines
24 KiB
Rust
use serde::Serialize;
|
|
use std::{
|
|
collections::{HashSet, VecDeque},
|
|
sync::Mutex,
|
|
};
|
|
use tauri::{
|
|
AppHandle, Emitter, Listener, Manager, WebviewUrl, WebviewWindowBuilder,
|
|
menu::{
|
|
CheckMenuItem, IsMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu,
|
|
},
|
|
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
|
window::WindowBuilder,
|
|
};
|
|
use theseus::prelude::CommandPayload;
|
|
|
|
const MAIN_WINDOW_LABEL: &str = "main";
|
|
const LIGHTWEIGHT_HOST_WINDOW_LABEL: &str = "lightweight-host";
|
|
const TRAY_ID: &str = "main";
|
|
const LAUNCH_INSTANCE_PREFIX: &str = "launch-instance:";
|
|
|
|
struct TrayLabels {
|
|
show_launcher: &'static str,
|
|
launch_instance: &'static str,
|
|
lightweight_mode: &'static str,
|
|
quit: &'static str,
|
|
running_prefix: &'static str,
|
|
}
|
|
|
|
fn tray_labels(locale: &str) -> TrayLabels {
|
|
if locale.eq_ignore_ascii_case("zh-CN") {
|
|
TrayLabels {
|
|
show_launcher: "显示 Axolotl 启动器",
|
|
launch_instance: "启动实例",
|
|
lightweight_mode: "轻量模式",
|
|
quit: "退出",
|
|
running_prefix: "正在运行:",
|
|
}
|
|
} else if locale.eq_ignore_ascii_case("zh-TW") {
|
|
TrayLabels {
|
|
show_launcher: "顯示 Axolotl 啟動器",
|
|
launch_instance: "啟動實例",
|
|
lightweight_mode: "輕量模式",
|
|
quit: "結束",
|
|
running_prefix: "正在執行:",
|
|
}
|
|
} else {
|
|
TrayLabels {
|
|
show_launcher: "Show Axolotl Launcher",
|
|
launch_instance: "Launch instance",
|
|
lightweight_mode: "Lightweight mode",
|
|
quit: "Quit",
|
|
running_prefix: "Running: ",
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LightweightModeState {
|
|
active: bool,
|
|
route: String,
|
|
running_processes: usize,
|
|
pending_crashes: VecDeque<PendingCrash>,
|
|
pending_commands: VecDeque<CommandPayload>,
|
|
frontend_ready: bool,
|
|
restoring: bool,
|
|
running_instance_ids: HashSet<String>,
|
|
tray_update_generation: u64,
|
|
}
|
|
|
|
impl Default for LightweightModeState {
|
|
fn default() -> Self {
|
|
Self {
|
|
active: false,
|
|
route: "/".to_string(),
|
|
running_processes: 0,
|
|
pending_crashes: VecDeque::new(),
|
|
pending_commands: VecDeque::new(),
|
|
frontend_ready: false,
|
|
restoring: false,
|
|
running_instance_ids: HashSet::new(),
|
|
tray_update_generation: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct PendingCrash {
|
|
pub instance_id: String,
|
|
pub uuid: String,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct LightweightMode(Mutex<LightweightModeState>);
|
|
|
|
impl LightweightMode {
|
|
fn enter(&self, app: &AppHandle) -> Result<(), String> {
|
|
self.enter_internal(app, true)
|
|
}
|
|
|
|
pub fn enter_for_close(&self, app: &AppHandle) -> Result<(), String> {
|
|
self.enter_internal(app, false)
|
|
}
|
|
|
|
fn enter_internal(
|
|
&self,
|
|
app: &AppHandle,
|
|
require_running: bool,
|
|
) -> Result<(), String> {
|
|
let state = self.0.lock().map_err(|error| error.to_string())?;
|
|
if state.active {
|
|
return Ok(());
|
|
}
|
|
if require_running && state.running_processes == 0 {
|
|
return Err(
|
|
"Lightweight mode requires a running Minecraft instance"
|
|
.to_string(),
|
|
);
|
|
}
|
|
drop(state);
|
|
create_lightweight_host_window(app)?;
|
|
let mut state = self.0.lock().map_err(|error| error.to_string())?;
|
|
if state.active {
|
|
drop(state);
|
|
destroy_lightweight_host_window(app);
|
|
return Ok(());
|
|
}
|
|
|
|
state.active = true;
|
|
state.frontend_ready = false;
|
|
drop(state);
|
|
if let Err(error) = destroy_main_window(app) {
|
|
if let Ok(mut state) = self.0.lock() {
|
|
state.active = false;
|
|
}
|
|
destroy_lightweight_host_window(app);
|
|
return Err(error);
|
|
}
|
|
schedule_tray_menu_update(app);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn exit(&self, app: &AppHandle) -> Result<(), String> {
|
|
let state = self.0.lock().map_err(|error| error.to_string())?;
|
|
if !state.active {
|
|
return show_main_window(app);
|
|
}
|
|
|
|
let route = state.route.clone();
|
|
drop(state);
|
|
if let Ok(mut state) = self.0.lock() {
|
|
state.frontend_ready = false;
|
|
state.restoring = true;
|
|
}
|
|
if let Err(error) = create_main_window(app, &route) {
|
|
if let Ok(mut state) = self.0.lock() {
|
|
state.restoring = false;
|
|
}
|
|
return Err(error);
|
|
}
|
|
destroy_lightweight_host_window(app);
|
|
if let Ok(mut state) = self.0.lock() {
|
|
state.active = false;
|
|
}
|
|
schedule_tray_menu_update(app);
|
|
Ok(())
|
|
}
|
|
|
|
fn is_active(&self) -> bool {
|
|
self.0.lock().map(|state| state.active).unwrap_or(false)
|
|
}
|
|
|
|
fn process_event(&self, app: &AppHandle, payload: ProcessEventPayload) {
|
|
if payload.lightweight_replay {
|
|
return;
|
|
}
|
|
|
|
let restore_window = {
|
|
let mut state = match self.0.lock() {
|
|
Ok(state) => state,
|
|
Err(error) => {
|
|
tracing::error!(
|
|
"Failed to lock lightweight mode state: {error}"
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
match payload.event.as_str() {
|
|
"launched" => {
|
|
state.running_processes += 1;
|
|
state.running_instance_ids.insert(payload.instance_id);
|
|
None
|
|
}
|
|
"finished" => {
|
|
state.running_processes =
|
|
state.running_processes.saturating_sub(1);
|
|
state.running_instance_ids.remove(&payload.instance_id);
|
|
let crashed = payload.crashed == Some(true);
|
|
if state.active && crashed {
|
|
state.pending_crashes.push_back(PendingCrash {
|
|
instance_id: payload.instance_id,
|
|
uuid: payload.uuid,
|
|
});
|
|
}
|
|
let should_restore = if state.active {
|
|
crashed || state.running_processes == 0
|
|
} else {
|
|
state.running_processes == 0
|
|
};
|
|
if should_restore {
|
|
let was_lightweight = state.active;
|
|
state.active = false;
|
|
if was_lightweight {
|
|
state.frontend_ready = false;
|
|
state.restoring = true;
|
|
}
|
|
Some((state.route.clone(), was_lightweight))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
_ => None,
|
|
}
|
|
};
|
|
|
|
schedule_tray_menu_update(app);
|
|
match restore_window {
|
|
Some((route, was_lightweight)) => {
|
|
let app = app.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
let result = if was_lightweight {
|
|
create_main_window(&app, &route).map(|()| {
|
|
destroy_lightweight_host_window(&app);
|
|
})
|
|
} else {
|
|
show_main_window(&app)
|
|
};
|
|
if let Err(error) = result {
|
|
if was_lightweight {
|
|
if let Ok(mut state) =
|
|
app.state::<LightweightMode>().0.lock()
|
|
{
|
|
state.restoring = false;
|
|
}
|
|
}
|
|
tracing::error!(
|
|
"Failed to restore launcher after Minecraft exited: {error}"
|
|
);
|
|
}
|
|
});
|
|
}
|
|
None if payload.event == "launched" => {
|
|
let app = app.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
if payload.maximize_window {
|
|
maximize_minecraft_window(payload.pid).await;
|
|
}
|
|
let settings = match theseus::settings::get().await {
|
|
Ok(settings) => settings,
|
|
Err(error) => {
|
|
tracing::warn!(
|
|
"Failed to read lightweight mode setting: {error}"
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
if settings.enter_lightweight_mode_on_game_launch {
|
|
let state = app.state::<LightweightMode>();
|
|
if let Err(error) = state.enter(&app) {
|
|
tracing::warn!(
|
|
"Failed to enter lightweight mode: {error}"
|
|
);
|
|
}
|
|
} else if settings.hide_on_process_start
|
|
&& let Some(window) =
|
|
app.get_webview_window(MAIN_WINDOW_LABEL)
|
|
&& let Err(error) = window.minimize()
|
|
{
|
|
tracing::warn!(
|
|
"Failed to minimize launcher after Minecraft started: {error}"
|
|
);
|
|
}
|
|
});
|
|
}
|
|
None => {}
|
|
}
|
|
}
|
|
|
|
fn set_route(&self, route: String) {
|
|
if route.starts_with('/') {
|
|
if let Ok(mut state) = self.0.lock() {
|
|
state.route = route;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn mark_frontend_ready(&self) -> (Vec<PendingCrash>, Vec<CommandPayload>) {
|
|
self.0
|
|
.lock()
|
|
.map(|mut state| {
|
|
state.frontend_ready = true;
|
|
state.restoring = false;
|
|
(
|
|
state.pending_crashes.drain(..).collect(),
|
|
state.pending_commands.drain(..).collect(),
|
|
)
|
|
})
|
|
.unwrap_or_else(|_| (Vec::new(), Vec::new()))
|
|
}
|
|
|
|
fn queue_command(&self, command: CommandPayload) {
|
|
if let Ok(mut state) = self.0.lock() {
|
|
state.pending_commands.push_back(command);
|
|
}
|
|
}
|
|
|
|
fn is_frontend_ready(&self) -> bool {
|
|
self.0
|
|
.lock()
|
|
.map(|state| state.frontend_ready)
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn is_restoring(&self) -> bool {
|
|
self.0.lock().map(|state| state.restoring).unwrap_or(false)
|
|
}
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct ProcessEventPayload {
|
|
instance_id: String,
|
|
uuid: String,
|
|
#[serde(default)]
|
|
pid: u32,
|
|
#[serde(default)]
|
|
maximize_window: bool,
|
|
event: String,
|
|
crashed: Option<bool>,
|
|
#[serde(default)]
|
|
lightweight_replay: bool,
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
|
|
|
#[cfg(target_os = "windows")]
|
|
static MAXIMIZE_PROCESS_ID: AtomicU32 = AtomicU32::new(0);
|
|
#[cfg(target_os = "windows")]
|
|
static MAXIMIZE_WINDOW_FOUND: AtomicBool = AtomicBool::new(false);
|
|
#[cfg(target_os = "windows")]
|
|
static MAXIMIZE_WINDOW_ENUMERATION: Mutex<()> = Mutex::new(());
|
|
|
|
#[cfg(target_os = "windows")]
|
|
unsafe extern "system" fn maximize_if_owned_by_process(
|
|
hwnd: windows::Win32::Foundation::HWND,
|
|
_: windows::Win32::Foundation::LPARAM,
|
|
) -> windows::core::BOOL {
|
|
use windows::Win32::UI::WindowsAndMessaging::{
|
|
GetWindowThreadProcessId, IsWindowVisible, SW_MAXIMIZE, ShowWindow,
|
|
};
|
|
use windows::core::BOOL;
|
|
|
|
let mut window_pid = 0;
|
|
unsafe { GetWindowThreadProcessId(hwnd, Some(&mut window_pid)) };
|
|
if window_pid == MAXIMIZE_PROCESS_ID.load(Ordering::Relaxed)
|
|
&& unsafe { IsWindowVisible(hwnd).as_bool() }
|
|
{
|
|
let _ = unsafe { ShowWindow(hwnd, SW_MAXIMIZE) };
|
|
MAXIMIZE_WINDOW_FOUND.store(true, Ordering::Relaxed);
|
|
return BOOL(0);
|
|
}
|
|
BOOL(1)
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
async fn maximize_minecraft_window(pid: u32) {
|
|
if pid == 0 {
|
|
return;
|
|
}
|
|
|
|
for _ in 0..20 {
|
|
let found = {
|
|
let _guard = MAXIMIZE_WINDOW_ENUMERATION.lock();
|
|
MAXIMIZE_PROCESS_ID.store(pid, Ordering::Relaxed);
|
|
MAXIMIZE_WINDOW_FOUND.store(false, Ordering::Relaxed);
|
|
unsafe {
|
|
use windows::Win32::Foundation::LPARAM;
|
|
use windows::Win32::UI::WindowsAndMessaging::EnumWindows;
|
|
let _ =
|
|
EnumWindows(Some(maximize_if_owned_by_process), LPARAM(0));
|
|
}
|
|
MAXIMIZE_WINDOW_FOUND.load(Ordering::Relaxed)
|
|
};
|
|
if found {
|
|
return;
|
|
}
|
|
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
async fn maximize_minecraft_window(_pid: u32) {}
|
|
|
|
#[tauri::command]
|
|
pub fn lightweight_mode_frontend_ready(
|
|
app: AppHandle,
|
|
route: String,
|
|
) -> Result<FrontendReadyPayload, String> {
|
|
let state = app.state::<LightweightMode>();
|
|
state.set_route(route);
|
|
let (pending_crashes, pending_commands) = state.mark_frontend_ready();
|
|
schedule_tray_menu_update(&app);
|
|
Ok(FrontendReadyPayload {
|
|
pending_crashes,
|
|
pending_commands,
|
|
})
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct FrontendReadyPayload {
|
|
pub pending_crashes: Vec<PendingCrash>,
|
|
pub pending_commands: Vec<CommandPayload>,
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn lightweight_mode_set_route(app: AppHandle, route: String) {
|
|
app.state::<LightweightMode>().set_route(route);
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn lightweight_mode_enter(app: AppHandle) -> Result<(), String> {
|
|
// Schedule the window destruction after returning from the IPC command.
|
|
// Destroying the webview synchronously would leave the invoke caller
|
|
// waiting for a response from a window that is already being destroyed.
|
|
tauri::async_runtime::spawn(async move {
|
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
|
if let Err(error) = app.state::<LightweightMode>().enter_for_close(&app)
|
|
{
|
|
tracing::warn!(
|
|
"Failed to enter lightweight mode on close: {error}"
|
|
);
|
|
let _ = app.emit("lightweight-mode-error", error);
|
|
}
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
fn destroy_main_window(app: &AppHandle) -> Result<(), String> {
|
|
if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
|
|
window.destroy().map_err(|error| error.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn create_lightweight_host_window(app: &AppHandle) -> Result<(), String> {
|
|
if app.get_window(LIGHTWEIGHT_HOST_WINDOW_LABEL).is_none() {
|
|
WindowBuilder::new(app, LIGHTWEIGHT_HOST_WINDOW_LABEL)
|
|
.title("Axolotl Launcher")
|
|
.visible(false)
|
|
.focused(false)
|
|
.focusable(false)
|
|
.skip_taskbar(true)
|
|
.build()
|
|
.map_err(|error| error.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn destroy_lightweight_host_window(app: &AppHandle) {
|
|
if let Some(window) = app.get_window(LIGHTWEIGHT_HOST_WINDOW_LABEL)
|
|
&& let Err(error) = window.destroy()
|
|
{
|
|
tracing::warn!("Failed to destroy lightweight host window: {error}");
|
|
}
|
|
}
|
|
|
|
fn create_main_window(app: &AppHandle, route: &str) -> Result<(), String> {
|
|
if app.get_webview_window(MAIN_WINDOW_LABEL).is_none() {
|
|
// Only reassigned on non-macOS platforms to strip window decorations.
|
|
#[cfg_attr(target_os = "macos", allow(unused_mut))]
|
|
let mut builder = WebviewWindowBuilder::new(
|
|
app,
|
|
MAIN_WINDOW_LABEL,
|
|
WebviewUrl::App(route.into()),
|
|
)
|
|
.title("Axolotl Launcher")
|
|
.inner_size(1280.0, 800.0)
|
|
.min_inner_size(1100.0, 700.0)
|
|
.resizable(true)
|
|
.transparent(true)
|
|
.zoom_hotkeys_enabled(false)
|
|
.visible(false);
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
builder = builder.decorations(false);
|
|
}
|
|
builder.build().map_err(|error| error.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn show_main_window(app: &AppHandle) -> Result<(), String> {
|
|
if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
|
|
window.show().map_err(|error| error.to_string())?;
|
|
window.unminimize().map_err(|error| error.to_string())?;
|
|
window.set_focus().map_err(|error| error.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn schedule_tray_menu_update(app: &AppHandle) {
|
|
let generation = {
|
|
let state = app.state::<LightweightMode>();
|
|
let Ok(mut state) = state.0.lock() else {
|
|
return;
|
|
};
|
|
state.tray_update_generation =
|
|
state.tray_update_generation.wrapping_add(1);
|
|
state.tray_update_generation
|
|
};
|
|
let app = app.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
|
let (active, has_running_processes, running_instance_ids) = {
|
|
let state = app.state::<LightweightMode>();
|
|
let Ok(state) = state.0.lock() else {
|
|
return;
|
|
};
|
|
if state.tray_update_generation != generation {
|
|
return;
|
|
}
|
|
(
|
|
state.active,
|
|
state.running_processes > 0,
|
|
state.running_instance_ids.clone(),
|
|
)
|
|
};
|
|
if let Err(error) = rebuild_tray_menu(
|
|
&app,
|
|
active,
|
|
has_running_processes,
|
|
&running_instance_ids,
|
|
)
|
|
.await
|
|
{
|
|
tracing::warn!("Failed to update tray menu: {error}");
|
|
}
|
|
});
|
|
}
|
|
|
|
async fn rebuild_tray_menu(
|
|
app: &AppHandle,
|
|
active: bool,
|
|
has_running_processes: bool,
|
|
running_instance_ids: &HashSet<String>,
|
|
) -> Result<(), String> {
|
|
let locale = theseus::settings::get()
|
|
.await
|
|
.map(|settings| settings.locale)
|
|
.unwrap_or_default();
|
|
let labels = tray_labels(&locale);
|
|
let show_launcher = MenuItem::with_id(
|
|
app,
|
|
"show-launcher",
|
|
labels.show_launcher,
|
|
true,
|
|
None::<&str>,
|
|
)
|
|
.map_err(|error| error.to_string())?;
|
|
let lightweight_mode = CheckMenuItem::with_id(
|
|
app,
|
|
"lightweight-mode",
|
|
labels.lightweight_mode,
|
|
has_running_processes,
|
|
active,
|
|
None::<&str>,
|
|
)
|
|
.map_err(|error| error.to_string())?;
|
|
let quit = MenuItem::with_id(app, "quit", labels.quit, true, None::<&str>)
|
|
.map_err(|error| error.to_string())?;
|
|
let first_separator = PredefinedMenuItem::separator(app)
|
|
.map_err(|error| error.to_string())?;
|
|
let second_separator = PredefinedMenuItem::separator(app)
|
|
.map_err(|error| error.to_string())?;
|
|
let instances = theseus::instance::list().await.unwrap_or_else(|error| {
|
|
tracing::debug!("Tray instance list is unavailable: {error}");
|
|
Vec::new()
|
|
});
|
|
let mut instance_items = Vec::with_capacity(instances.len());
|
|
for instance in instances {
|
|
let running = running_instance_ids.contains(&instance.instance.id);
|
|
instance_items.push(
|
|
MenuItem::with_id(
|
|
app,
|
|
format!("{LAUNCH_INSTANCE_PREFIX}{}", instance.instance.id),
|
|
if running {
|
|
format!(
|
|
"{}{}",
|
|
labels.running_prefix, instance.instance.name
|
|
)
|
|
} else {
|
|
instance.instance.name.clone()
|
|
},
|
|
!running,
|
|
None::<&str>,
|
|
)
|
|
.map_err(|error| error.to_string())?,
|
|
);
|
|
}
|
|
let instance_references: Vec<&dyn IsMenuItem<_>> = instance_items
|
|
.iter()
|
|
.map(|item| item as &dyn IsMenuItem<_>)
|
|
.collect();
|
|
let launch_instances = Submenu::with_items(
|
|
app,
|
|
labels.launch_instance,
|
|
true,
|
|
&instance_references,
|
|
)
|
|
.map_err(|error| error.to_string())?;
|
|
let menu = Menu::with_items(
|
|
app,
|
|
&[
|
|
&show_launcher,
|
|
&first_separator,
|
|
&launch_instances,
|
|
&lightweight_mode,
|
|
&second_separator,
|
|
&quit,
|
|
],
|
|
)
|
|
.map_err(|error| error.to_string())?;
|
|
app.tray_by_id(TRAY_ID)
|
|
.ok_or_else(|| "Tray icon is unavailable".to_string())?
|
|
.set_menu(Some(menu))
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
|
|
fn handle_menu_event(app: &AppHandle, id: &str) {
|
|
match id {
|
|
"show-launcher" => {
|
|
let app = app.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
let _ = app.state::<LightweightMode>().exit(&app);
|
|
});
|
|
}
|
|
"lightweight-mode" => {
|
|
let app = app.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
let state = app.state::<LightweightMode>();
|
|
if state.is_active() {
|
|
let _ = state.exit(&app);
|
|
} else if let Err(error) = state.enter(&app) {
|
|
tracing::debug!(
|
|
"Lightweight mode was not entered from tray: {error}"
|
|
);
|
|
}
|
|
});
|
|
}
|
|
"quit" => app.exit(0),
|
|
instance_id if instance_id.starts_with(LAUNCH_INSTANCE_PREFIX) => {
|
|
let instance_id = instance_id
|
|
.trim_start_matches(LAUNCH_INSTANCE_PREFIX)
|
|
.to_string();
|
|
tauri::async_runtime::spawn(async move {
|
|
if let Err(error) = theseus::instance::run(
|
|
&instance_id,
|
|
theseus::instance::QuickPlayType::None,
|
|
false,
|
|
)
|
|
.await
|
|
{
|
|
tracing::error!(
|
|
"Failed to launch tray instance {instance_id}: {error}"
|
|
);
|
|
}
|
|
});
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub fn init(app: &AppHandle) {
|
|
app.manage(LightweightMode::default());
|
|
let tray = TrayIconBuilder::with_id(TRAY_ID)
|
|
.icon(
|
|
app.default_window_icon()
|
|
.expect("missing default app icon")
|
|
.clone(),
|
|
)
|
|
.show_menu_on_left_click(false)
|
|
.on_menu_event(|app, event| handle_menu_event(app, event.id.as_ref()))
|
|
.on_tray_icon_event(|tray, event| {
|
|
if matches!(
|
|
event,
|
|
TrayIconEvent::Click {
|
|
button: MouseButton::Left,
|
|
button_state: MouseButtonState::Up,
|
|
..
|
|
}
|
|
) {
|
|
let app = tray.app_handle().clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
let _ = app.state::<LightweightMode>().exit(&app);
|
|
});
|
|
}
|
|
})
|
|
.build(app)
|
|
.expect("failed to create system tray");
|
|
let _tray = tray;
|
|
schedule_tray_menu_update(app);
|
|
let app_handle = app.clone();
|
|
app.listen("process", move |event| {
|
|
let Ok(payload) =
|
|
serde_json::from_str::<ProcessEventPayload>(event.payload())
|
|
else {
|
|
return;
|
|
};
|
|
app_handle
|
|
.state::<LightweightMode>()
|
|
.process_event(&app_handle, payload);
|
|
});
|
|
let app_handle = app.clone();
|
|
app.listen("instance", move |_| {
|
|
schedule_tray_menu_update(&app_handle);
|
|
});
|
|
let app_handle = app.clone();
|
|
app.listen("settings", move |_| {
|
|
schedule_tray_menu_update(&app_handle);
|
|
});
|
|
let app_handle = app.clone();
|
|
app.listen("command", move |event| {
|
|
let Ok(command) =
|
|
serde_json::from_str::<CommandPayload>(event.payload())
|
|
else {
|
|
return;
|
|
};
|
|
let state = app_handle.state::<LightweightMode>();
|
|
if state.is_frontend_ready()
|
|
|| (!state.is_active() && !state.is_restoring())
|
|
{
|
|
return;
|
|
}
|
|
state.queue_command(command);
|
|
if !state.is_active() {
|
|
return;
|
|
}
|
|
let app = app_handle.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
let _ = app.state::<LightweightMode>().exit(&app);
|
|
});
|
|
});
|
|
}
|