mod media; mod proxy; mod store; mod update; use media::Media; use proxy::Proxy; use serde::Serialize; use std::sync::Mutex; use tauri::{ image::Image, include_image, menu::{Menu, MenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, AppHandle, Manager, State, }; struct AppState { proxy: Proxy, media: Media, server: Mutex>, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct Bootstrap { proxy_base: String, server: Option, } fn show_main(app: &AppHandle) { if let Some(window) = app.get_webview_window("main") { let _ = window.unminimize(); let _ = window.show(); let _ = window.set_focus(); } } #[tauri::command] fn bootstrap(app: AppHandle, state: State) -> Result { Ok(Bootstrap { proxy_base: state.proxy.base.clone(), server: store::load_server(&app)?, }) } #[tauri::command] async fn set_server(app: AppHandle, state: State<'_, AppState>, url: String) -> Result { let server = proxy::validate_server(&url)?; state.proxy.check(&server).await?; store::save_server(&app, Some(&server))?; *state.proxy.server.write().map_err(|e| e.to_string())? = Some(server.clone()); *state.server.lock().map_err(|e| e.to_string())? = Some(server.clone()); Ok(server) } #[tauri::command] fn clear_server(app: AppHandle, state: State) -> Result<(), String> { store::save_server(&app, None)?; store::save_user(&app, None)?; store::save_refresh(None)?; *state.proxy.server.write().map_err(|e| e.to_string())? = None; *state.server.lock().map_err(|e| e.to_string())? = None; Ok(()) } #[tauri::command] fn token_get() -> Result, String> { store::load_refresh() } #[tauri::command] fn token_set(token: String) -> Result<(), String> { store::save_refresh(Some(&token)) } #[tauri::command] fn token_clear() -> Result<(), String> { store::save_refresh(None) } #[tauri::command] fn user_get(app: AppHandle) -> Result, String> { store::load_user(&app) } #[tauri::command] fn user_set(app: AppHandle, json: String) -> Result<(), String> { store::save_user(&app, Some(&json)) } #[tauri::command] fn user_clear(app: AppHandle) -> Result<(), String> { store::save_user(&app, None) } #[tauri::command] fn media_set( state: State, title: String, artist: String, album: String, cover_url: Option, playing: bool, ) -> Result<(), String> { state.media.set(&title, &artist, &album, cover_url.as_deref(), playing) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_single_instance::init(|app, _, _| show_main(app))) .plugin(tauri_plugin_window_state::Builder::default().build()) .manage(update::Updater::default()) .setup(|app| { let saved = store::load_server(app.handle())?; let proxy = tauri::async_runtime::block_on(Proxy::start(saved.clone())).map_err(|e| e.to_string())?; let media = Media::new(); if let Some(window) = app.get_webview_window("main") { let _ = media.attach(app.handle().clone(), &window); } app.manage(AppState { proxy, media, server: Mutex::new(saved), }); let open = MenuItem::with_id(app, "open", "Открыть", true, None::<&str>)?; let quit = MenuItem::with_id(app, "quit", "Выход", true, None::<&str>)?; let menu = Menu::with_items(app, &[&open, &quit])?; let icon: Image = include_image!("icons/32x32.png"); TrayIconBuilder::new() .icon(icon) .menu(&menu) .show_menu_on_left_click(false) .on_menu_event(|app, event| match event.id.as_ref() { "open" => show_main(app), "quit" => app.exit(0), _ => {} }) .on_tray_icon_event(|tray, event| { if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event { show_main(tray.app_handle()); } }) .build(app)?; Ok(()) }) .invoke_handler(tauri::generate_handler![ bootstrap, set_server, clear_server, token_get, token_set, token_clear, user_get, user_set, user_clear, media_set, update::update_check, update::update_install ]) .run(tauri::generate_context!()) .expect("desktop app failed to start"); }