mirror of
https://github.com/flectar/mail.git
synced 2026-11-12 02:05:59 +00:00
611 lines
22 KiB
Rust
611 lines
22 KiB
Rust
//! Native window and system-tray lifecycle wiring.
|
|
//!
|
|
//! Window close behavior, tray actions, background resource release, and
|
|
//! persistence of the close-to-tray, sign-in and window-frame preferences form
|
|
//! one lifecycle boundary. Keeping that boundary here makes the application
|
|
//! composition root independent of platform window policy.
|
|
|
|
use super::*;
|
|
use crate::tray_ui::FlectarTray;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
mod macos;
|
|
|
|
/// How long the window must stay minimized before its rendering buffers are
|
|
/// released. Shorter interruptions keep the rendered message ready.
|
|
#[cfg(desktop)]
|
|
const MINIMIZED_RELEASE_DELAY: Duration = Duration::from_secs(10);
|
|
/// Let Slint destroy the native surface and process pending property changes
|
|
/// that may still own image references before returning memory.
|
|
pub(super) const TRIM_DELAY: Duration = Duration::from_millis(250);
|
|
/// Coalesces the burst of startup updates applied while the window is hidden.
|
|
const HIDDEN_TRIM_DELAY: Duration = Duration::from_secs(1);
|
|
|
|
thread_local! {
|
|
static LIFECYCLE: Lifecycle = Lifecycle::default();
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Lifecycle {
|
|
minimized_release: Timer,
|
|
hidden_trim: Timer,
|
|
/// The last minimized state reported by the platform.
|
|
minimized: Cell<bool>,
|
|
/// Rendering was suspended because the window stayed minimized.
|
|
released_while_minimized: Cell<bool>,
|
|
/// Deferred until the window is first shown after a background start.
|
|
first_show: RefCell<Vec<Box<dyn FnOnce()>>>,
|
|
/// Maximized and full-screen state when the window was hidden.
|
|
hidden_state: Cell<(bool, bool)>,
|
|
/// The main window was closed while messages were being written in
|
|
/// their own windows; the application ends when the last one closes.
|
|
quit_after_composers: Cell<bool>,
|
|
}
|
|
|
|
pub(super) fn create_and_register_window_lifecycle(
|
|
app: &AppWindow,
|
|
#[cfg_attr(not(feature = "flatpak"), allow(unused_variables))] runtime: &tokio::runtime::Handle,
|
|
) -> Result<Option<FlectarTray>, slint::PlatformError> {
|
|
#[cfg(not(any(mobile, feature = "flatpak")))]
|
|
let tray = Some(FlectarTray::new()?);
|
|
#[cfg(any(mobile, feature = "flatpak"))]
|
|
let tray: Option<FlectarTray> = None;
|
|
|
|
#[cfg(mobile)]
|
|
app.set_close_to_tray(false);
|
|
// The Flatpak has no tray icon: closing keeps it running in the
|
|
// background, and Settings says so.
|
|
#[cfg(feature = "flatpak")]
|
|
app.set_tray_supported(false);
|
|
|
|
if let Some(tray) = tray.as_ref() {
|
|
tray.set_enabled(app.get_close_to_tray());
|
|
crate::app_icon::attach_tray(app, tray);
|
|
|
|
let app_for_tray = app.as_weak();
|
|
tray.on_show_window(move || {
|
|
if let Some(app) = app_for_tray.upgrade() {
|
|
activate(&app, None);
|
|
}
|
|
});
|
|
let app_for_tray_quit = app.as_weak();
|
|
tray.on_quit_app(move || match app_for_tray_quit.upgrade() {
|
|
Some(app) => quit(&app),
|
|
None => {
|
|
let _ = slint::quit_event_loop();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Opening the running app from the Dock or Finder shows its window.
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let app = app.as_weak();
|
|
slint::Timer::single_shot(Duration::ZERO, move || {
|
|
macos::on_reopen(move || {
|
|
if let Some(app) = app.upgrade() {
|
|
activate(&app, None);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
let app_for_close = app.as_weak();
|
|
#[cfg(feature = "flatpak")]
|
|
let runtime = runtime.clone();
|
|
app.window().on_close_requested(move || {
|
|
let Some(app) = app_for_close.upgrade() else {
|
|
return slint::CloseRequestResponse::HideWindow;
|
|
};
|
|
if app.invoke_system_back_requested() {
|
|
return slint::CloseRequestResponse::KeepWindowShown;
|
|
}
|
|
if app.get_close_to_tray() {
|
|
prepare_for_tray(&app);
|
|
#[cfg(feature = "flatpak")]
|
|
describe_background(&app, &runtime);
|
|
slint::CloseRequestResponse::HideWindow
|
|
} else if composer::open_windows() > 0 {
|
|
LIFECYCLE.with(|lifecycle| lifecycle.quit_after_composers.set(true));
|
|
prepare_for_tray(&app);
|
|
slint::CloseRequestResponse::HideWindow
|
|
} else {
|
|
let _ = slint::quit_event_loop();
|
|
slint::CloseRequestResponse::HideWindow
|
|
}
|
|
});
|
|
|
|
Ok(tray)
|
|
}
|
|
|
|
/// End the app, as Quit in the tray menu or a desktop's background-apps list
|
|
/// asks.
|
|
pub(super) fn quit(app: &AppWindow) {
|
|
let _ = app.hide();
|
|
let _ = slint::quit_event_loop();
|
|
}
|
|
|
|
/// Tell the desktop, in the current language, what the Flatpak does while
|
|
/// its window is closed: GNOME lists it under Background Apps with this line.
|
|
#[cfg(feature = "flatpak")]
|
|
pub(super) fn describe_background(app: &AppWindow, runtime: &tokio::runtime::Handle) {
|
|
let status = app.global::<I18n>().invoke_background_status().to_string();
|
|
runtime.spawn(crate::autostart::set_background_status(status));
|
|
}
|
|
|
|
pub(super) fn prepare_for_tray(app: &AppWindow) {
|
|
let window = app.window();
|
|
let state = (window.is_maximized(), window.is_fullscreen());
|
|
LIFECYCLE.with(|lifecycle| {
|
|
lifecycle.minimized_release.stop();
|
|
lifecycle.released_while_minimized.set(false);
|
|
lifecycle.hidden_state.set(state);
|
|
});
|
|
app.invoke_suspend_rendering(true);
|
|
slint::Timer::single_shot(TRIM_DELAY, release_free_memory);
|
|
}
|
|
|
|
/// A composer window closed. Once the last one is gone, a main window that
|
|
/// was closed while they were open ends the application.
|
|
pub(super) fn composer_closed() {
|
|
if LIFECYCLE.with(|lifecycle| lifecycle.quit_after_composers.get())
|
|
&& composer::open_windows() == 0
|
|
{
|
|
let _ = slint::quit_event_loop();
|
|
}
|
|
}
|
|
|
|
pub(super) fn show_from_tray(app: &AppWindow) {
|
|
LIFECYCLE.with(|lifecycle| lifecycle.quit_after_composers.set(false));
|
|
crate::window_chrome::before_show();
|
|
if let Err(error) = app.show() {
|
|
eprintln!("could not restore window: {error}");
|
|
return;
|
|
}
|
|
let (maximized, fullscreen) = LIFECYCLE.with(|lifecycle| {
|
|
lifecycle.minimized.set(false);
|
|
lifecycle.hidden_state.take()
|
|
});
|
|
restore_window_state(app, maximized, fullscreen);
|
|
app.invoke_suspend_rendering(false);
|
|
app.window().request_redraw();
|
|
for hook in LIFECYCLE.with(|lifecycle| lifecycle.first_show.take()) {
|
|
hook();
|
|
}
|
|
}
|
|
|
|
/// The recreated native window starts out normal, while Slint still records
|
|
/// the old state and so ignores a request to set it again. Apply it to the
|
|
/// native window directly; Slint follows the resulting resize.
|
|
fn restore_window_state(app: &AppWindow, maximized: bool, fullscreen: bool) {
|
|
#[cfg(desktop)]
|
|
if maximized || fullscreen {
|
|
use slint::winit_030::{WinitWindowAccessor, winit::window::Fullscreen};
|
|
let app = app.as_weak();
|
|
// The native window is created again after `show` returns.
|
|
let _ = slint::spawn_local(async move {
|
|
let Some(app) = app.upgrade() else {
|
|
return;
|
|
};
|
|
let Ok(window) = app.window().winit_window().await else {
|
|
return;
|
|
};
|
|
if fullscreen {
|
|
window.set_fullscreen(Some(Fullscreen::Borderless(None)));
|
|
} else {
|
|
window.set_maximized(true);
|
|
}
|
|
});
|
|
}
|
|
#[cfg(mobile)]
|
|
let _ = (app, maximized, fullscreen);
|
|
}
|
|
|
|
/// Bring the window to the user from the tray or another launch: show it if
|
|
/// it is hidden, restore it if it is minimized, and ask for focus with the
|
|
/// launcher's activation token when one was forwarded.
|
|
pub(super) fn activate(app: &AppWindow, activation_token: Option<String>) {
|
|
let window = app.window();
|
|
if !window.is_visible() {
|
|
show_from_tray(app);
|
|
} else if window.is_minimized() {
|
|
window.set_minimized(false);
|
|
}
|
|
#[cfg(desktop)]
|
|
crate::window_chrome::focus(app, activation_token);
|
|
#[cfg(mobile)]
|
|
let _ = activation_token;
|
|
}
|
|
|
|
/// Return memory freed by startup work that ran while the window is hidden,
|
|
/// as after a start in the tray. Hiding the window trims on its own.
|
|
pub(super) fn trim_while_hidden(app: &AppWindow) {
|
|
if app.window().is_visible() {
|
|
return;
|
|
}
|
|
LIFECYCLE.with(|lifecycle| {
|
|
lifecycle.hidden_trim.start(
|
|
slint::TimerMode::SingleShot,
|
|
HIDDEN_TRIM_DELAY,
|
|
release_free_memory,
|
|
);
|
|
});
|
|
}
|
|
|
|
/// Run `hook` once the window is visible: now, or when a background start
|
|
/// first shows it.
|
|
pub(super) fn when_first_shown(app: &AppWindow, hook: impl FnOnce() + 'static) {
|
|
if app.window().is_visible() {
|
|
hook();
|
|
} else {
|
|
LIFECYCLE.with(|lifecycle| lifecycle.first_show.borrow_mut().push(Box::new(hook)));
|
|
}
|
|
}
|
|
|
|
/// Follow platform minimize reports. A window that stays minimized releases
|
|
/// the same rendering buffers as one closed to the tray, and renders them
|
|
/// again as soon as it is restored. Platforms that cannot report minimization
|
|
/// (Wayland) keep everything rendered.
|
|
#[cfg(desktop)]
|
|
pub(crate) fn minimized_changed(app: &AppWindow, minimized: bool) {
|
|
let changed = LIFECYCLE.with(|lifecycle| lifecycle.minimized.replace(minimized) != minimized);
|
|
if !changed {
|
|
return;
|
|
}
|
|
if minimized {
|
|
let app = app.as_weak();
|
|
LIFECYCLE.with(|lifecycle| {
|
|
lifecycle.minimized_release.start(
|
|
slint::TimerMode::SingleShot,
|
|
MINIMIZED_RELEASE_DELAY,
|
|
move || {
|
|
let Some(app) = app.upgrade() else {
|
|
return;
|
|
};
|
|
// Hiding to the tray already released everything.
|
|
if !LIFECYCLE.with(|lifecycle| lifecycle.minimized.get())
|
|
|| !app.window().is_visible()
|
|
|| app.get_render_suspended()
|
|
{
|
|
return;
|
|
}
|
|
LIFECYCLE.with(|lifecycle| lifecycle.released_while_minimized.set(true));
|
|
app.invoke_suspend_rendering(true);
|
|
slint::Timer::single_shot(TRIM_DELAY, release_free_memory);
|
|
},
|
|
);
|
|
});
|
|
return;
|
|
}
|
|
let resume = LIFECYCLE.with(|lifecycle| {
|
|
lifecycle.minimized_release.stop();
|
|
lifecycle.released_while_minimized.replace(false)
|
|
});
|
|
if resume {
|
|
app.invoke_suspend_rendering(false);
|
|
app.window().request_redraw();
|
|
}
|
|
}
|
|
|
|
/// Whether the window was last reported minimized. Lets the platform event
|
|
/// filter skip minimize queries on ordinary frames and resizes.
|
|
#[cfg(desktop)]
|
|
pub(crate) fn reported_minimized() -> bool {
|
|
LIFECYCLE.with(|lifecycle| lifecycle.minimized.get())
|
|
}
|
|
|
|
/// Whether a background start leaves the user a way back to the window. A
|
|
/// tray icon is one: Linux needs a StatusNotifier host for it, which some
|
|
/// desktops (GNOME without an extension) do not provide, while Windows and
|
|
/// macOS always have one. The Flatpak has no tray icon; opening it again
|
|
/// brings the window back, and GNOME lists it under Background Apps meanwhile.
|
|
#[cfg(desktop)]
|
|
pub(super) fn can_start_hidden(runtime: &tokio::runtime::Handle) -> bool {
|
|
if cfg!(feature = "flatpak") {
|
|
return true;
|
|
}
|
|
#[cfg(linux_desktop)]
|
|
return runtime.block_on(status_notifier_host_registered());
|
|
#[cfg(not(linux_desktop))]
|
|
{
|
|
let _ = runtime;
|
|
true
|
|
}
|
|
}
|
|
|
|
#[cfg(linux_desktop)]
|
|
async fn status_notifier_host_registered() -> bool {
|
|
use ashpd::zbus;
|
|
let query = async {
|
|
let connection = zbus::Connection::session().await.ok()?;
|
|
let watcher = zbus::Proxy::new(
|
|
&connection,
|
|
"org.kde.StatusNotifierWatcher",
|
|
"/StatusNotifierWatcher",
|
|
"org.kde.StatusNotifierWatcher",
|
|
)
|
|
.await
|
|
.ok()?;
|
|
watcher
|
|
.get_property::<bool>("IsStatusNotifierHostRegistered")
|
|
.await
|
|
.ok()
|
|
};
|
|
matches!(
|
|
tokio::time::timeout(Duration::from_secs(1), query).await,
|
|
Ok(Some(true))
|
|
)
|
|
}
|
|
|
|
/// Return memory once the window has gone to the tray or stayed minimized.
|
|
fn release_free_memory() {
|
|
crate::memory::release_free_heap();
|
|
#[cfg(target_os = "windows")]
|
|
crate::memory::trim_working_set();
|
|
}
|
|
|
|
pub(super) fn register_window_preference_callbacks(
|
|
app: &AppWindow,
|
|
tray: Option<&FlectarTray>,
|
|
state: &Rc<RefCell<InboxState>>,
|
|
runtime: &tokio::runtime::Handle,
|
|
ui_task_tx: &UiSender<UiTaskUpdate>,
|
|
) {
|
|
let app_weak = app.as_weak();
|
|
app.on_toggle_settings(move || {
|
|
if let Some(app) = app_weak.upgrade() {
|
|
app.set_settings_open(!app.get_settings_open());
|
|
}
|
|
});
|
|
// The startup entry and the default email app can also change in the
|
|
// system's own settings, so read them again whenever Settings appears:
|
|
// from the rail, a tab or a shortcut into a settings page.
|
|
#[cfg(desktop)]
|
|
{
|
|
let launch_at_login = LaunchAtLogin::register(app, runtime);
|
|
let default_email_app =
|
|
crate::default_email_app::DefaultEmailAppRow::register(app, runtime);
|
|
let app_weak = app.as_weak();
|
|
app.on_settings_shown(move || {
|
|
if let Some(app) = app_weak.upgrade() {
|
|
launch_at_login.refresh(&app, false);
|
|
if let Some(default_email_app) = &default_email_app {
|
|
default_email_app.refresh(&app);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
let app_weak = app.as_weak();
|
|
let state_for_frame = Rc::clone(state);
|
|
let runtime_for_frame = runtime.clone();
|
|
app.global::<WindowChrome>().on_choose(move |integrated| {
|
|
let Some(app) = app_weak.upgrade() else {
|
|
return;
|
|
};
|
|
let frame = if integrated {
|
|
window_chrome::WindowFrame::Integrated
|
|
} else {
|
|
window_chrome::WindowFrame::System
|
|
};
|
|
window_chrome::apply_preference(&app, frame);
|
|
let Some(core) = state_for_frame.borrow().core.clone() else {
|
|
app.set_sync_status(UiMessage::plain(
|
|
"Window frame updated for this session.",
|
|
));
|
|
return;
|
|
};
|
|
match runtime_for_frame.block_on(core.set_window_frame(frame.key())) {
|
|
Ok(()) => app.set_sync_status(UiMessage::plain("Window frame preference saved.")),
|
|
Err(error) => app.set_sync_status(UiMessage::detail(
|
|
"Could not save window frame preference: {}",
|
|
error,
|
|
)),
|
|
}
|
|
});
|
|
|
|
let app_weak = app.as_weak();
|
|
let tray_for_close_setting = tray.map(FlectarTray::as_weak);
|
|
let state_for_close_setting = Rc::clone(state);
|
|
let runtime_for_close_setting = runtime.clone();
|
|
let ui_task_tx_for_close_setting = ui_task_tx.clone();
|
|
app.on_set_close_to_tray(move |enabled| {
|
|
let Some(app) = app_weak.upgrade() else {
|
|
return;
|
|
};
|
|
if cfg!(mobile) {
|
|
app.set_close_to_tray(false);
|
|
app.set_sync_status(UiMessage::plain(
|
|
"System tray behavior is unavailable in this package.",
|
|
));
|
|
return;
|
|
}
|
|
app.set_close_to_tray(enabled);
|
|
if let Some(tray) = tray_for_close_setting
|
|
.as_ref()
|
|
.and_then(slint::Weak::upgrade)
|
|
{
|
|
tray.set_enabled(enabled);
|
|
}
|
|
|
|
let core = state_for_close_setting.borrow().core.clone();
|
|
app.set_sync_status(UiMessage::plain("Saving close behavior…"));
|
|
let updates = ui_task_tx_for_close_setting.clone();
|
|
runtime_for_close_setting.spawn(async move {
|
|
// The Flatpak may run with its window closed only while the
|
|
// desktop allows it; a refusal turns the switch back off.
|
|
#[cfg(feature = "flatpak")]
|
|
if enabled && let Some(message) = background_refusal().await {
|
|
let _ = updates
|
|
.send(close_behavior_update(message, Some(false)))
|
|
.await;
|
|
return;
|
|
}
|
|
let update = match core {
|
|
None => close_behavior_update(
|
|
UiMessage::plain("Close-to-tray is active for this session."),
|
|
None,
|
|
),
|
|
Some(core) => match core.set_close_to_tray(enabled).await {
|
|
Ok(()) => close_behavior_update(close_behavior_saved(enabled), None),
|
|
// Restore both the setting and tray visibility if the
|
|
// preference could not be persisted.
|
|
Err(error) => close_behavior_update(
|
|
UiMessage::detail("Could not save close behavior: {}", error),
|
|
Some(!enabled),
|
|
),
|
|
},
|
|
};
|
|
let _ = updates.send(update).await;
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Ask the desktop to let the Flatpak run with its window closed; what to
|
|
/// tell the user when it does not.
|
|
#[cfg(feature = "flatpak")]
|
|
async fn background_refusal() -> Option<UiMessage> {
|
|
match tokio::task::spawn_blocking(crate::autostart::allow_background).await {
|
|
Ok(Ok(true)) => None,
|
|
Ok(Ok(false)) => Some(UiMessage::plain(
|
|
"Your desktop did not allow Flectar Mail to run in the background.",
|
|
)),
|
|
Ok(Err(error)) => Some(UiMessage::detail(
|
|
"Could not save close behavior: {}",
|
|
error,
|
|
)),
|
|
Err(error) => Some(UiMessage::detail(
|
|
"Could not save close behavior: {}",
|
|
error,
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// What closing the window does now.
|
|
fn close_behavior_saved(enabled: bool) -> UiMessage {
|
|
if !enabled {
|
|
UiMessage::plain("Closing the window will now quit Flectar Mail.")
|
|
} else if cfg!(feature = "flatpak") {
|
|
UiMessage::plain("Closing the window will now keep Flectar Mail running in the background.")
|
|
} else {
|
|
UiMessage::plain("Closing the window will now keep Flectar Mail in the tray.")
|
|
}
|
|
}
|
|
|
|
/// A close-behavior result for the window: what to tell the user, and the
|
|
/// switch to restore when the change did not take.
|
|
fn close_behavior_update(message: UiMessage, close_to_tray: Option<bool>) -> UiTaskUpdate {
|
|
UiTaskUpdate {
|
|
message,
|
|
accounts: None,
|
|
calendar_connections: None,
|
|
carddav_connections: None,
|
|
calendar_error: None,
|
|
account_removal: None,
|
|
clear_account_form: false,
|
|
finishes_oauth: false,
|
|
finishes_account_setup: false,
|
|
close_to_tray,
|
|
}
|
|
}
|
|
|
|
/// The "Start automatically" switch. The operating system's startup list is
|
|
/// read and written on the blocking pool; one lock and a request counter keep
|
|
/// overlapping toggles and refreshes from applying out of order.
|
|
#[cfg(desktop)]
|
|
#[derive(Clone)]
|
|
struct LaunchAtLogin {
|
|
supported: bool,
|
|
runtime: tokio::runtime::Handle,
|
|
lock: Arc<Mutex<()>>,
|
|
requests: Arc<std::sync::atomic::AtomicU64>,
|
|
}
|
|
|
|
#[cfg(desktop)]
|
|
impl LaunchAtLogin {
|
|
fn register(app: &AppWindow, runtime: &tokio::runtime::Handle) -> Self {
|
|
let this = Self {
|
|
supported: crate::autostart::supported(),
|
|
runtime: runtime.clone(),
|
|
lock: Arc::default(),
|
|
requests: Arc::default(),
|
|
};
|
|
app.set_launch_at_login_available(this.supported);
|
|
if !this.supported {
|
|
return this;
|
|
}
|
|
this.refresh(app, true);
|
|
let toggle = this.clone();
|
|
let app_weak = app.as_weak();
|
|
app.on_set_launch_at_login(move |enabled| {
|
|
if app_weak.upgrade().is_some() {
|
|
toggle.set(app_weak.clone(), enabled);
|
|
}
|
|
});
|
|
this
|
|
}
|
|
|
|
/// Read the entry into the switch. At startup, also re-point an entry
|
|
/// whose executable was moved or replaced.
|
|
fn refresh(&self, app: &AppWindow, repair: bool) {
|
|
if !self.supported {
|
|
return;
|
|
}
|
|
let this = self.clone();
|
|
let app = app.as_weak();
|
|
self.runtime.spawn_blocking(move || {
|
|
use std::sync::atomic::Ordering;
|
|
let _guard = this.lock.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let requests = this.requests.load(Ordering::SeqCst);
|
|
if repair && let Err(error) = crate::autostart::repair() {
|
|
eprintln!("could not update the sign-in entry: {error}");
|
|
}
|
|
match crate::autostart::is_enabled() {
|
|
// A toggle requested meanwhile reports its own result.
|
|
Ok(enabled) if this.requests.load(Ordering::SeqCst) == requests => {
|
|
let _ = app.upgrade_in_event_loop(move |app| app.set_launch_at_login(enabled));
|
|
}
|
|
Ok(_) => {}
|
|
Err(error) => eprintln!("could not read the sign-in entry: {error}"),
|
|
}
|
|
});
|
|
}
|
|
|
|
fn set(&self, app: slint::Weak<AppWindow>, enabled: bool) {
|
|
use std::sync::atomic::Ordering;
|
|
let request = self.requests.fetch_add(1, Ordering::SeqCst) + 1;
|
|
let this = self.clone();
|
|
self.runtime.spawn_blocking(move || {
|
|
let _guard = this.lock.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
// Only the latest request is applied; it supersedes this one.
|
|
if this.requests.load(Ordering::SeqCst) != request {
|
|
return;
|
|
}
|
|
use crate::autostart::Applied;
|
|
let result = crate::autostart::set_enabled(enabled).map_err(|error| error.to_string());
|
|
let actual = crate::autostart::is_enabled()
|
|
.unwrap_or(enabled == matches!(result, Ok(Applied::Done)));
|
|
let _ = app.upgrade_in_event_loop(move |app| {
|
|
app.set_launch_at_login(actual);
|
|
app.set_sync_status(match result {
|
|
Ok(Applied::Done) if enabled => {
|
|
UiMessage::plain("Flectar Mail will start when you sign in.")
|
|
}
|
|
Ok(Applied::Done) => UiMessage::plain(
|
|
"Flectar Mail will no longer start when you sign in.",
|
|
),
|
|
Ok(Applied::NeedsApproval) if cfg!(target_os = "windows") => UiMessage::plain(
|
|
"Turn on Flectar Mail under Startup apps in Windows Settings to finish.",
|
|
),
|
|
Ok(Applied::NeedsApproval) => UiMessage::plain(
|
|
"Allow Flectar Mail under Login Items in System Settings to finish.",
|
|
),
|
|
Err(error) => {
|
|
UiMessage::detail("Could not change startup behavior: {}", error)
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|