ci / module boundaries / boundaries (push) Successful in 7s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m2s
ci / ts / ts (packages + web) (push) Successful in 3m8s
ci / desktop / rust (desktop proxy) (push) Successful in 4m21s
release / release (версия и заметки) (push) Successful in 6s
release / android (APK в релиз) (push) Successful in 4m0s
release / desktop (Linux и Windows в релиз) (push) Successful in 11m49s
release / prune (старые файлы и прогоны) (push) Successful in 5s
Clean release notes in Android and desktop update dialogs. Persist downloaded audio and offline playback events per server and user; add desktop update checks, checksum verification and installer launch.
161 lines
4.8 KiB
Rust
161 lines
4.8 KiB
Rust
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<Option<String>>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct Bootstrap {
|
|
proxy_base: String,
|
|
server: Option<String>,
|
|
}
|
|
|
|
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<AppState>) -> Result<Bootstrap, String> {
|
|
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<String, String> {
|
|
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<AppState>) -> 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<Option<String>, 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<Option<String>, 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<AppState>,
|
|
title: String,
|
|
artist: String,
|
|
album: String,
|
|
cover_url: Option<String>,
|
|
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");
|
|
}
|