511 lines
24 KiB
Rust
511 lines
24 KiB
Rust
//! storebound desktop: a Tauri shell around the storebound CLI, which runs as a sidecar (`storebound serve --stdio`).
|
|
//! The GUI never talks to a store itself; every store call is a storebound command.
|
|
pub mod accounts;
|
|
pub mod ai_fix;
|
|
pub mod cache;
|
|
pub mod card;
|
|
pub mod commands;
|
|
pub mod confirm;
|
|
pub mod downloads;
|
|
pub mod logs;
|
|
pub mod menu_panel;
|
|
pub mod policy;
|
|
pub mod profiles;
|
|
pub mod registry;
|
|
pub mod selftest;
|
|
pub mod updates;
|
|
pub mod sidecar;
|
|
pub mod store;
|
|
pub mod tray;
|
|
pub mod workspace;
|
|
|
|
include!("commands_list.rs");
|
|
|
|
use std::sync::Mutex;
|
|
|
|
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu, WINDOW_SUBMENU_ID};
|
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
|
use tauri::{AppHandle, Emitter, Manager, RunEvent, Runtime, WindowEvent};
|
|
use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
|
|
use tauri_plugin_deep_link::DeepLinkExt;
|
|
|
|
/// WebKitGTK's DMABUF renderer leaves the window blank on some Linux GPU / driver / VM combinations. Turn it off unless
|
|
/// the user set the variable (WEBKIT_DISABLE_DMABUF_RENDERER=0 keeps DMABUF). Must run before any webview exists.
|
|
pub fn linux_webkit_env() {
|
|
#[cfg(target_os = "linux")]
|
|
if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() {
|
|
// SAFETY: called first thing in main, before any thread is started.
|
|
unsafe { std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1") };
|
|
}
|
|
}
|
|
|
|
/// Started by the logon entry (`--hidden`): the app runs in the tray and shows no window.
|
|
fn started_hidden() -> bool {
|
|
std::env::args().any(|a| a == "--hidden")
|
|
}
|
|
|
|
/// Tray icon: a left click shows the tray card (card.rs), a right click the app's own menu panels (menu_panel.rs, with
|
|
/// the tree from tray.rs: open the window, read the stores, the apps with their state, mute, updates, Settings and Quit,
|
|
/// the only real quit on Windows and Linux, where closing the window keeps the app running in the tray). The window
|
|
/// refines tooltip, menu and icon after every read (`tray_update`). Linux trays send no clicks: the native menu,
|
|
/// rendered from the same tree, stays on the icon there (and anywhere the panels fail).
|
|
fn tray(app: &tauri::App) -> tauri::Result<()> {
|
|
let handle = app.handle();
|
|
let panels = handle.state::<menu_panel::MenuPanels>();
|
|
if let Ok(mut n) = panels.nodes.lock() {
|
|
*n = tray::initial_nodes();
|
|
}
|
|
let mut b = TrayIconBuilder::with_id("main").tooltip("storebound").show_menu_on_left_click(false);
|
|
if cfg!(target_os = "linux") {
|
|
panels.native.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
b = b.menu(&tray::native_menu(handle, &tray::initial_nodes())?);
|
|
}
|
|
if let Some(icon) = app.default_window_icon() {
|
|
b = b.icon(icon.clone());
|
|
}
|
|
b.on_menu_event(|app, e| dispatch_menu(app, e.id().as_ref()))
|
|
.on_tray_icon_event(|t, e| {
|
|
let TrayIconEvent::Click { button, button_state: MouseButtonState::Up, rect, position, .. } = e else { return };
|
|
let app = t.app_handle();
|
|
// both open from the icon's rectangle (not the click point): their edge sits next to the icon, above a
|
|
// bottom taskbar, so nothing starts inside the taskbar
|
|
let scale = app.get_webview_window(policy::MAIN).and_then(|w| w.scale_factor().ok()).unwrap_or(1.0);
|
|
let p = rect.position.to_physical::<f64>(scale);
|
|
let s = rect.size.to_physical::<f64>(scale);
|
|
let icon = if s.width > 0.0 && s.height > 0.0 { card::Rect { x: p.x, y: p.y, w: s.width, h: s.height } } else { card::Rect { x: position.x, y: position.y, w: 1.0, h: 1.0 } };
|
|
match button {
|
|
MouseButton::Right => menu_panel::open_root(app, menu_panel::Anchor::Tray { x: icon.x, y: icon.y, w: icon.w, h: icon.h }),
|
|
MouseButton::Left => {
|
|
if app.get_webview_window(card::LABEL).is_none() {
|
|
show_main(app);
|
|
return;
|
|
}
|
|
card::toggle(app, icon);
|
|
}
|
|
_ => {}
|
|
}
|
|
})
|
|
.build(app)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// A tray menu pick (a menu panel row or the native fallback menu): tray.rs maps the id to what it does.
|
|
pub fn dispatch_menu<R: Runtime>(app: &AppHandle<R>, id: &str) {
|
|
match tray::action_of(id) {
|
|
Some(tray::Action::Open) => show_main(app),
|
|
Some(tray::Action::Quit) => app.exit(0),
|
|
Some(tray::Action::Event(word)) => {
|
|
let _ = app.emit_to(policy::MAIN, "tray-action", word);
|
|
}
|
|
Some(tray::Action::Route(route)) => {
|
|
show_main(app);
|
|
let _ = app.emit_to(policy::MAIN, "tray-route", route);
|
|
}
|
|
None => {}
|
|
}
|
|
}
|
|
|
|
/// Silent self-update: at start (after a minute) and every 30 minutes, a signed update from storebound.dev is
|
|
/// downloaded, installed and the app restarts with the same arguments (`--hidden` stays hidden). Errors are logged.
|
|
/// Background updater (docs/desktop-release.md, "Dev channel"): asks the chosen channel's latest.json at start, every
|
|
/// 15 minutes, right after a resume from sleep and when Settings switched the channel; installs a strictly newer, signed
|
|
/// build silently (the plugin checks the minisign signature first), deferred while the main window is in use (at most
|
|
/// updates::MAX_DEFERS checks in a row). Every step goes to host.log as `auto-update ...`.
|
|
fn auto_update(app: AppHandle) {
|
|
tauri::async_runtime::spawn(async move {
|
|
use std::time::{Duration, Instant, SystemTime};
|
|
tokio::time::sleep(Duration::from_secs(10)).await;
|
|
let mut first = true;
|
|
let mut last_check = Instant::now();
|
|
let mut last_wall = SystemTime::now();
|
|
let mut defers = 0u32;
|
|
loop {
|
|
let now_wall = SystemTime::now();
|
|
let gap = now_wall.duration_since(last_wall).map(|d| d.as_secs()).unwrap_or(0);
|
|
last_wall = now_wall;
|
|
let kicked = updates::take_kick();
|
|
if updates::check_due(first, kicked, gap, last_check.elapsed().as_secs()) {
|
|
let why = if first { "start" } else if kicked { "channel switched" } else if gap > updates::TICK_SECS + 60 { "resume" } else { "every 15 min" };
|
|
first = false;
|
|
last_check = Instant::now();
|
|
defers = auto_update_once(&app, why, defers).await;
|
|
}
|
|
tokio::time::sleep(Duration::from_secs(updates::TICK_SECS)).await;
|
|
}
|
|
});
|
|
}
|
|
|
|
/// One check of the background updater; returns the new count of deferred installs.
|
|
async fn auto_update_once(app: &AppHandle, why: &str, defers: u32) -> u32 {
|
|
let state = app.state::<commands::AppState>();
|
|
let channel = match state.saved.lock() {
|
|
Ok(s) => s.settings.channel.clone(),
|
|
Err(_) => return defers,
|
|
};
|
|
let current = env!("CARGO_PKG_VERSION");
|
|
let update = match commands::find_update(app, &channel).await {
|
|
Ok(Some(u)) => u,
|
|
Ok(None) => {
|
|
logs::host("info", &format!("auto-update ({why}): {current} is current on {channel}"));
|
|
return 0;
|
|
}
|
|
Err(e) => {
|
|
logs::host("warn", &format!("auto-update ({why}): check of {channel} failed: {}", e.message));
|
|
return defers;
|
|
}
|
|
};
|
|
if !updates::should_install(current, &update.version) {
|
|
logs::host("info", &format!("auto-update ({why}): {channel} offers {}, not newer than {current}: no downgrade", update.version));
|
|
return 0;
|
|
}
|
|
let main = app.get_webview_window(policy::MAIN);
|
|
let visible = main.as_ref().and_then(|w| w.is_visible().ok()).unwrap_or(false);
|
|
let focused = main.as_ref().and_then(|w| w.is_focused().ok()).unwrap_or(false);
|
|
if updates::defer_install(visible, focused, defers) {
|
|
logs::host("info", &format!("auto-update ({why}): {} on {channel} waits, the window is in use ({}/{})", update.version, defers + 1, updates::MAX_DEFERS));
|
|
return defers + 1;
|
|
}
|
|
logs::host("info", &format!("auto-update ({why}): installing {} from {channel} (this is {current})", update.version));
|
|
// the sidecar binary lives inside the app bundle that is about to be replaced
|
|
state.sidecar.shutdown().await;
|
|
match update.download_and_install(|_, _| {}, || {}).await {
|
|
Ok(()) => {
|
|
logs::host("info", &format!("auto-update: {} installed, restarting", update.version));
|
|
app.restart()
|
|
}
|
|
Err(e) => {
|
|
logs::host("warn", &format!("auto-update: install of {} failed: {}", update.version, updates::error_text(&e.to_string())));
|
|
0
|
|
}
|
|
}
|
|
}
|
|
|
|
fn show_main<R: Runtime>(app: &AppHandle<R>) {
|
|
if let Some(w) = app.get_webview_window(policy::MAIN) {
|
|
let _ = w.unminimize();
|
|
let _ = w.show();
|
|
let _ = w.set_focus();
|
|
// the last notification's target (the plugin has no click callback on desktop): the window goes there now
|
|
if let Some(route) = tray::take_pending() {
|
|
let _ = app.emit_to(policy::MAIN, "tray-route", route);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// macOS menu: app menu with Quit (Cmd+Q really quits), Edit (copy / paste in text fields), and a Window menu that
|
|
/// keeps a permanent entry for the main window, since the automatic list drops a hidden window.
|
|
fn menu<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
|
|
let name = "storebound";
|
|
let app_menu = Submenu::with_items(
|
|
app,
|
|
name,
|
|
true,
|
|
&[
|
|
&PredefinedMenuItem::about(app, Some("About storebound"), None)?,
|
|
&PredefinedMenuItem::separator(app)?,
|
|
&PredefinedMenuItem::services(app, None)?,
|
|
&PredefinedMenuItem::separator(app)?,
|
|
&PredefinedMenuItem::hide(app, None)?,
|
|
&PredefinedMenuItem::hide_others(app, None)?,
|
|
&PredefinedMenuItem::show_all(app, None)?,
|
|
&PredefinedMenuItem::separator(app)?,
|
|
&PredefinedMenuItem::quit(app, Some("Quit storebound"))?,
|
|
],
|
|
)?;
|
|
let edit = Submenu::with_items(
|
|
app,
|
|
"Edit",
|
|
true,
|
|
&[
|
|
&PredefinedMenuItem::undo(app, None)?,
|
|
&PredefinedMenuItem::redo(app, None)?,
|
|
&PredefinedMenuItem::separator(app)?,
|
|
&PredefinedMenuItem::cut(app, None)?,
|
|
&PredefinedMenuItem::copy(app, None)?,
|
|
&PredefinedMenuItem::paste(app, None)?,
|
|
&PredefinedMenuItem::select_all(app, None)?,
|
|
],
|
|
)?;
|
|
let window = Submenu::with_id_and_items(
|
|
app,
|
|
WINDOW_SUBMENU_ID,
|
|
"Window",
|
|
true,
|
|
&[
|
|
&PredefinedMenuItem::minimize(app, None)?,
|
|
&PredefinedMenuItem::maximize(app, Some("Zoom"))?,
|
|
&PredefinedMenuItem::separator(app)?,
|
|
&MenuItem::with_id(app, "show-main", "storebound", true, Some("CmdOrCtrl+0"))?,
|
|
&PredefinedMenuItem::separator(app)?,
|
|
&PredefinedMenuItem::close_window(app, None)?,
|
|
],
|
|
)?;
|
|
Menu::with_items(app, &[&app_menu, &edit, &window])
|
|
}
|
|
|
|
fn handle_deep_link(app: &AppHandle, urls: &[url::Url]) {
|
|
if let Some(dir) = urls.iter().find_map(policy::deep_link_open_path) {
|
|
show_main(app);
|
|
tauri::async_runtime::spawn(commands::open_folder_link(app.clone(), dir));
|
|
} else if let Some(screen) = urls.iter().find_map(policy::deep_link_screen) {
|
|
show_main(app);
|
|
let _ = app.emit_to(policy::MAIN, "deep-link", screen);
|
|
}
|
|
}
|
|
|
|
/// Debug builds only: STOREBOUND_DEV_PROJECTS (folders separated by the OS path separator) seeds the app folder list
|
|
/// once, for screenshots and manual checks with the demo apps. Release builds ignore it.
|
|
fn dev_seed(data_dir: &std::path::Path) {
|
|
if !cfg!(debug_assertions) {
|
|
return;
|
|
}
|
|
let Some(v) = std::env::var_os("STOREBOUND_DEV_PROJECTS") else { return };
|
|
let mut st = store::load(data_dir);
|
|
let mut changed = false;
|
|
for dir in std::env::split_paths(&v) {
|
|
let dir = std::fs::canonicalize(&dir).unwrap_or(dir);
|
|
let path = dir.to_string_lossy().to_string();
|
|
if !commands::has_config(&dir) || st.projects.iter().any(|p| p.path == path) {
|
|
continue;
|
|
}
|
|
let name = dir.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
|
|
if let Ok(id) = store::new_id() {
|
|
st.projects.push(store::Project { id, path, name, play_account: None, apple_account: None, added: store::now_iso() });
|
|
changed = true;
|
|
}
|
|
}
|
|
if changed {
|
|
let _ = store::save(data_dir, &st);
|
|
}
|
|
}
|
|
|
|
/// One line before Tauri starts: a start at logon that dies early (before the host log exists) still leaves a trace
|
|
/// with its arguments and working folder (a logon start once left no line at all). Best effort, never fails.
|
|
fn early_trace() {
|
|
use std::io::Write;
|
|
let base = std::env::var_os("APPDATA").or_else(|| std::env::var_os("XDG_DATA_HOME")).map(std::path::PathBuf::from)
|
|
.or_else(|| std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".local/share")));
|
|
let Some(base) = base else { return };
|
|
let dir = base.join("dev.storebound.desktop").join("logs");
|
|
if std::fs::create_dir_all(&dir).is_err() { return }
|
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
let cwd = std::env::current_dir().map(|d| d.display().to_string()).unwrap_or_default();
|
|
let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
|
|
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(dir.join("start.log")) {
|
|
let _ = writeln!(f, "{secs} start {} args={args:?} cwd={cwd} pid={}", env!("CARGO_PKG_VERSION"), std::process::id());
|
|
}
|
|
}
|
|
|
|
pub fn run() {
|
|
early_trace();
|
|
linux_webkit_env();
|
|
let app = tauri::Builder::default()
|
|
// single-instance first: a second start (or a storebound:// link) focuses the running app instead
|
|
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| show_main(app)))
|
|
.plugin(tauri_plugin_deep_link::init())
|
|
// never restore "visible": a --hidden start would otherwise pop the window up
|
|
.plugin(tauri_plugin_window_state::Builder::new().with_filter(|label| !label.starts_with(menu_panel::LABEL_PREFIX)).with_state_flags(tauri_plugin_window_state::StateFlags::all() & !tauri_plugin_window_state::StateFlags::VISIBLE).build())
|
|
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec!["--hidden"])))
|
|
.plugin(tauri_plugin_shell::init())
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.plugin(tauri_plugin_notification::init())
|
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
|
.plugin(tauri_plugin_process::init())
|
|
.setup(|app| {
|
|
#[cfg(all(windows, not(debug_assertions)))]
|
|
if let Some(w) = app.get_webview_window(policy::MAIN) {
|
|
harden_webview(&w);
|
|
}
|
|
let data_dir = app.path().app_data_dir()?;
|
|
logs::init(&data_dir, app.path().home_dir().ok().map(|h| h.to_string_lossy().to_string()));
|
|
logs::panic_hook();
|
|
logs::host("info", &format!("storebound desktop {} starting ({} {}{})", env!("CARGO_PKG_VERSION"), std::env::consts::OS, std::env::consts::ARCH, if cfg!(debug_assertions) { ", debug" } else { "" }));
|
|
dev_seed(&data_dir);
|
|
let cache = cache::Cache::open(&data_dir).or_else(|_| cache::Cache::in_memory()).map_err(std::io::Error::other)?;
|
|
let (saved, report) = store::load_report(&data_dir);
|
|
let home = app.path().home_dir().ok();
|
|
logs::host("info", &format!("saved state: {} app folders ({} not found), {} accounts, {} studios, format {}", saved.projects.len(), saved.projects.iter().filter(|p| !std::path::Path::new(&p.path).is_dir()).count(), saved.accounts.len(), saved.studios.len(), saved.version));
|
|
if let Some(from) = report.migrated_from {
|
|
logs::host("info", &format!("saved state migrated from format {from} to {}", store::VERSION));
|
|
}
|
|
for n in &report.notes {
|
|
logs::host("warn", &format!("saved state: {n}"));
|
|
}
|
|
if let Some(b) = &report.backup {
|
|
logs::host("info", &format!("saved state: original kept as {}", b.display()));
|
|
}
|
|
let keep_days = saved.settings.cache.keep_days;
|
|
let state = commands::AppState {
|
|
sidecar: sidecar::Sidecar::default(),
|
|
saved: Mutex::new(saved),
|
|
data_dir,
|
|
profiles: profiles::ProfileStore::new(profiles::default_dir(home.as_deref()), home.clone()),
|
|
legacy: Box::new(accounts::Keychain),
|
|
registry: registry::default_file(home.as_deref()),
|
|
cache,
|
|
downloads: downloads::Jobs::default(),
|
|
};
|
|
// Settings > Offline cache: drop saved reads older than the chosen number of days
|
|
commands::prune_cache(&state, keep_days);
|
|
// the machine's shared accounts and app folders (CLI and app); old keychain accounts move into profiles once
|
|
if let Err(e) = commands::sync_machine(&state) {
|
|
logs::host("warn", &format!("accounts / app folders not synced at start: {}", e.message));
|
|
}
|
|
// start at logon, in the tray: rewritten on every start (Windows: the HKCU Run entry then points at the
|
|
// running exe, so a moved or updated install fixes itself)
|
|
match app.autolaunch().enable() {
|
|
Ok(()) => logs::host("info", "autostart at logon set (--hidden, tray)"),
|
|
Err(e) => logs::host("warn", &format!("autostart not set: {e}")),
|
|
}
|
|
app.manage(state);
|
|
app.manage(menu_panel::MenuPanels::default());
|
|
tray(app)?;
|
|
// pinned tray menu panels of the last session come back
|
|
menu_panel::restore(app.handle());
|
|
if let Err(e) = card::create(app.handle()) {
|
|
logs::host("warn", &format!("tray card not created: {e}"));
|
|
}
|
|
if !started_hidden() {
|
|
show_main(app.handle());
|
|
}
|
|
auto_update(app.handle().clone());
|
|
#[cfg(target_os = "macos")]
|
|
app.set_menu(menu(app.handle())?)?;
|
|
#[cfg(not(target_os = "macos"))]
|
|
let _ = menu::<tauri::Wry>;
|
|
app.on_menu_event(|app, e| {
|
|
if e.id() == "show-main" {
|
|
show_main(app);
|
|
}
|
|
});
|
|
// Linux: registering runs update-desktop-database and xdg-mime, which a minimal system may not have; a
|
|
// missing tool must not stop the app (it then only misses storebound:// links)
|
|
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
|
|
if let Err(e) = app.deep_link().register_all() {
|
|
logs::host("warn", &format!("storebound:// links not registered: {e}"));
|
|
}
|
|
let handle = app.handle().clone();
|
|
app.deep_link().on_open_url(move |e| handle_deep_link(&handle, &e.urls()));
|
|
if let Ok(Some(urls)) = app.deep_link().get_current() {
|
|
handle_deep_link(app.handle(), &urls);
|
|
}
|
|
Ok(())
|
|
})
|
|
.on_window_event(|w, e| {
|
|
// the close button hides the window: the app keeps running in the tray (macOS: also the Dock); the tray's
|
|
// Quit (macOS: Cmd+Q) really quits
|
|
if w.label() == card::LABEL {
|
|
match e {
|
|
WindowEvent::Focused(false) => { let _ = w.hide(); card::note_hidden(); }
|
|
WindowEvent::CloseRequested { api, .. } => { api.prevent_close(); let _ = w.hide(); card::note_hidden(); }
|
|
_ => {}
|
|
}
|
|
}
|
|
if w.label() == policy::MAIN {
|
|
if let WindowEvent::CloseRequested { api, .. } = e {
|
|
api.prevent_close();
|
|
let _ = w.hide();
|
|
}
|
|
}
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
commands::app_info,
|
|
commands::sidecar_status,
|
|
commands::sidecar_call,
|
|
commands::cache_get,
|
|
commands::cache_clear,
|
|
commands::job_cancel,
|
|
commands::write_with_confirm,
|
|
commands::projects_list,
|
|
commands::project_add,
|
|
commands::project_init,
|
|
commands::project_remove,
|
|
commands::project_set_accounts,
|
|
commands::accounts_list,
|
|
commands::account_add_play,
|
|
commands::account_add_apple,
|
|
commands::account_remove,
|
|
commands::portfolio_status,
|
|
commands::open_external,
|
|
commands::tools_info,
|
|
commands::tools_install,
|
|
commands::tools_uninstall,
|
|
commands::workspace_get,
|
|
commands::studio_save,
|
|
commands::studio_remove,
|
|
commands::studio_logo,
|
|
commands::account_update,
|
|
commands::account_set_play_developer,
|
|
commands::account_check,
|
|
commands::store_apps_refresh,
|
|
commands::store_apps_cached,
|
|
commands::overrides_set,
|
|
commands::settings_set,
|
|
commands::cache_info,
|
|
commands::data_reveal,
|
|
commands::notify,
|
|
commands::tray_update,
|
|
commands::card_snapshot,
|
|
commands::app_reset,
|
|
commands::update_check,
|
|
commands::update_install,
|
|
commands::project_import_fastlane,
|
|
commands::export_save,
|
|
commands::price_sheet_pick,
|
|
commands::image_pick,
|
|
commands::project_reveal,
|
|
commands::project_locate,
|
|
commands::state_reset,
|
|
commands::log_webview,
|
|
commands::ai_fix_info,
|
|
commands::ai_fix_run,
|
|
commands::ai_fix_undo,
|
|
commands::card_get,
|
|
commands::card_action,
|
|
commands::menu_state,
|
|
commands::menu_ready,
|
|
commands::menu_open_sub,
|
|
commands::menu_pin_sub,
|
|
commands::menu_close_subs,
|
|
commands::menu_close,
|
|
commands::menu_dismiss,
|
|
commands::menu_pin,
|
|
commands::menu_select,
|
|
])
|
|
.build(tauri::generate_context!())
|
|
.expect("storebound desktop failed to start");
|
|
|
|
app.run(|app, e| match e {
|
|
#[cfg(target_os = "macos")]
|
|
RunEvent::Reopen { .. } => show_main(app),
|
|
// no window left is not a reason to quit (tray app); an explicit exit or the updater's restart carries a code
|
|
RunEvent::ExitRequested { code: None, api, .. } => api.prevent_exit(),
|
|
RunEvent::Exit => {
|
|
if let Some(s) = app.try_state::<commands::AppState>() {
|
|
s.sidecar.shutdown_now();
|
|
}
|
|
ai_fix::kill_all();
|
|
}
|
|
_ => {}
|
|
});
|
|
}
|
|
|
|
/// Release builds on Windows: no WebView2 browser context menu (Back, Refresh, Save as, Print, More tools) and no
|
|
/// browser accelerator keys (Ctrl+R, Ctrl+P, F5, Ctrl+Shift+I...). The page draws its own menus; dev keeps devtools.
|
|
#[cfg(all(windows, not(debug_assertions)))]
|
|
fn harden_webview(w: &tauri::WebviewWindow) {
|
|
use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Settings3;
|
|
use windows_core::Interface;
|
|
let r = w.with_webview(|wv| unsafe {
|
|
let Ok(core) = wv.controller().CoreWebView2() else { return };
|
|
let Ok(s) = core.Settings() else { return };
|
|
let _ = s.SetAreDefaultContextMenusEnabled(false);
|
|
if let Ok(s3) = s.cast::<ICoreWebView2Settings3>() {
|
|
let _ = s3.SetAreBrowserAcceleratorKeysEnabled(false);
|
|
}
|
|
});
|
|
if let Err(e) = r {
|
|
logs::host("warn", &format!("webview menus not turned off: {e}"));
|
|
}
|
|
}
|