//! Settings preference callback wiring. //! //! These callbacks share one lifecycle and persistence boundary: update the //! session immediately where needed, then write through the core when it is //! available. Keeping them together prevents the application composition root //! from accumulating one closure per setting. use super::*; use flectar_mail_core::models::CustomTheme; fn apply_account_presentation_settings( app: &AppWindow, state: &Rc>, runtime: &tokio::runtime::Runtime, settings: &flectar_mail_core::models::Settings, ) { state.borrow_mut().account_presentation = AccountPresentationSettings::from_settings(settings); refresh_connected_accounts(app, state); refresh_rows_only(app, state, runtime); // A profile change can change which accounts the mail view shows. app.invoke_mail_profile_changed(); } /// Persists the download location and only then reflects it in the UI, so a /// failed write never leaves Settings showing a directory that was not saved. #[cfg(desktop)] fn save_download_directory( app: &AppWindow, state: &Rc>, runtime: &tokio::runtime::Runtime, directory: String, ) { let Some(core) = state.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Download location is saved once an account is set up.", )); return; }; match runtime.block_on(core.set_download_directory(&directory)) { Ok(()) => { app.set_download_directory(directory.into()); app.set_sync_status(UiMessage::plain("Download location saved.")); } Err(error) => app.set_sync_status(UiMessage::detail( "Could not save the download location: {}", error, )), } } pub(super) fn register_settings_preference_callbacks( app: &AppWindow, state: &Rc>, runtime: &Rc, ) { let app_weak = app.as_weak(); let state_for_notifications = Rc::clone(state); let runtime_for_notifications = Rc::clone(runtime); app.on_save_notification_settings(move |enabled, sound, scope| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_notifications.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Notification preferences are active for this session.", )); return; }; match runtime_for_notifications.block_on(core.set_notification_settings( enabled, sound, scope.as_str(), )) { Ok(()) => app.set_sync_status(UiMessage::plain("Notification preferences saved.")), Err(error) => { app.set_sync_status(UiMessage::detail("Could not save notifications: {}", error)) } } }); let app_weak = app.as_weak(); let state_for_sync_interval = Rc::clone(state); let runtime_for_sync_interval = Rc::clone(runtime); app.on_save_sync_interval(move |minutes| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_sync_interval.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Sync interval is active for this session.", )); return; }; match runtime_for_sync_interval.block_on(core.set_sync_interval_minutes(i64::from(minutes))) { Ok(()) => app.set_sync_status(UiMessage::detail( "Sync interval set to {} minutes.", minutes, )), Err(error) => { app.set_sync_status(UiMessage::detail("Could not save sync interval: {}", error)) } } }); let app_weak = app.as_weak(); let state_for_read_on_open = Rc::clone(state); let runtime_for_read_on_open = Rc::clone(runtime); app.on_save_mark_read_on_open(move |enabled| { let Some(app) = app_weak.upgrade() else { return; }; state_for_read_on_open.borrow_mut().mark_read_on_open = enabled; let Some(core) = state_for_read_on_open.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Message-opening preference updated for this session.", )); return; }; match runtime_for_read_on_open.block_on(core.set_mark_read_on_open(enabled)) { Ok(()) => app.set_sync_status(UiMessage::plain("Message-opening preference saved.")), Err(error) => app.set_sync_status(UiMessage::detail( "Could not save message-opening preference: {}", error, )), } }); let app_weak = app.as_weak(); let state_for_contacts = Rc::clone(state); let runtime_for_contacts = Rc::clone(runtime); app.on_save_contact_discovery_settings(move |outgoing, incoming, all_accounts, suggest| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_contacts.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Contact suggestion preferences are active for this session.", )); return; }; match runtime_for_contacts.block_on(core.set_contact_discovery_settings( outgoing, incoming, all_accounts, suggest, )) { Ok(()) => { app.set_sync_status(UiMessage::plain("Contact suggestion preferences saved.")) } Err(error) => { if let Ok(settings) = runtime_for_contacts.block_on(core.load_settings()) { app.set_collect_outgoing_contacts(settings.collect_outgoing_contacts); app.set_collect_incoming_contacts(settings.collect_incoming_contacts); app.set_contact_suggest_all_accounts(settings.contact_suggest_all_accounts); app.set_suggest_learned_contacts(settings.suggest_learned_contacts); } app.set_sync_status(UiMessage::detail( "Could not save contact suggestion preferences: {}", error, )); } } }); let app_weak = app.as_weak(); let state_for_clear_contacts = Rc::clone(state); let runtime_for_clear_contacts = Rc::clone(runtime); app.on_clear_contact_suggestions(move || { let Some(app) = app_weak.upgrade() else { return false; }; let Some(core) = state_for_clear_contacts.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Contact storage is still starting. Try again shortly.", )); return false; }; match runtime_for_clear_contacts.block_on(core.clear_contact_suggestions()) { Ok(removed) => { app.set_sync_status(UiMessage::detail("Cleared {} suggested people.", removed)); app.invoke_search_contacts(app.get_contact_search_query()); true } Err(error) => { app.set_sync_status(UiMessage::detail( "Could not clear suggested people: {}", error, )); false } } }); let app_weak = app.as_weak(); let state_for_theme = Rc::clone(state); let runtime_for_theme = Rc::clone(runtime); app.on_save_theme(move |theme| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_theme.borrow().core.clone() else { app.set_sync_status(UiMessage::plain("Theme updated for this session.")); return; }; match runtime_for_theme.block_on(core.set_theme(&theme)) { Ok(()) => app.set_sync_status(UiMessage::plain("Theme preference saved.")), Err(error) => app.set_sync_status(UiMessage::detail("Could not save theme: {}", error)), } }); let app_weak = app.as_weak(); // Only desktop hosts pick a filesystem directory; mobile hosts export // through their own document provider and hide the setting entirely. #[cfg(desktop)] { let state_for_download = Rc::clone(state); let runtime_for_download = Rc::clone(runtime); let app_weak_for_download = app_weak.clone(); let picking_download = Rc::new(Cell::new(false)); app.on_choose_download_directory(move || { let Some(app) = app_weak_for_download.upgrade() else { return; }; if picking_download.replace(true) { return; } let title = translated(&app, &UiMessage::plain("Choose download location")).to_string(); let current = crate::documents::resolve_download_directory(&app.get_download_directory()); // The portal picker needs the runtime: rfd's blocking dialog // panics on the UI thread, which has no Tokio context. let request: crate::documents::DocumentFuture> = Box::pin(async move { let dialog = rfd::AsyncFileDialog::new().set_title(title); let dialog = match current { Some(directory) => dialog.set_directory(directory), None => dialog, }; Ok(dialog .pick_folder() .await .map(|folder| folder.path().to_owned())) }); let state = Rc::clone(&state_for_download); let runtime = Rc::clone(&runtime_for_download); let picking = Rc::clone(&picking_download); let app_weak = app.as_weak(); crate::documents::run_request(&runtime_for_download, request, move |picked| { picking.set(false); let Some(app) = app_weak.upgrade() else { return; }; match picked { Ok(Some(directory)) => save_download_directory( &app, &state, &runtime, directory.to_string_lossy().into_owned(), ), Ok(None) => {} Err(error) => app.set_sync_status(UiMessage::detail( "Could not save the download location: {}", error, )), } }); }); let state_for_clear = Rc::clone(state); let runtime_for_clear = Rc::clone(runtime); let app_weak_for_clear = app_weak.clone(); app.on_clear_download_directory(move || { let Some(app) = app_weak_for_clear.upgrade() else { return; }; save_download_directory(&app, &state_for_clear, &runtime_for_clear, String::new()); }); } let state_for_theme_preset = Rc::clone(state); let runtime_for_theme_preset = Rc::clone(runtime); app.on_save_theme_preset(move |preset| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_theme_preset.borrow().core.clone() else { app.set_sync_status(UiMessage::plain("Theme palette updated for this session.")); return; }; match runtime_for_theme_preset.block_on(core.set_theme_preset(preset.as_str())) { Ok(()) => app.set_sync_status(UiMessage::plain("Theme palette saved.")), Err(error) => { app.set_sync_status(UiMessage::detail("Could not save theme palette: {}", error)) } } }); let app_weak = app.as_weak(); let state_for_appearance = Rc::clone(state); let runtime_for_appearance = Rc::clone(runtime); app.global::().on_choose(move |preference| { let Some(app) = app_weak.upgrade() else { return; }; if flectar_mail_core::models::MessageAppearance::parse(&preference).is_none() { return; } let colors = app.global::(); let previous = colors.get_preference(); colors.set_preference(preference.clone()); let Some(core) = state_for_appearance.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Message appearance updated for this session.", )); return; }; match runtime_for_appearance.block_on(core.set_message_appearance(&preference)) { Ok(()) => app.set_sync_status(UiMessage::plain("Message appearance preference saved.")), Err(error) => { colors.set_preference(previous); app.set_sync_status(UiMessage::detail( "Could not save message appearance: {}", error, )); } } }); let app_weak = app.as_weak(); let state_for_custom_theme = Rc::clone(state); let runtime_for_custom_theme = Rc::clone(runtime); app.on_save_custom_theme( move |light_primary, light_page, light_surface, light_text, light_border, dark_primary, dark_page, dark_surface, dark_text, dark_border| { let Some(app) = app_weak.upgrade() else { return false; }; let custom_theme = CustomTheme { light_primary: theme::color_to_hex(light_primary), light_page_background: theme::color_to_hex(light_page), light_surface: theme::color_to_hex(light_surface), light_text: theme::color_to_hex(light_text), light_border: theme::color_to_hex(light_border), dark_primary: theme::color_to_hex(dark_primary), dark_page_background: theme::color_to_hex(dark_page), dark_surface: theme::color_to_hex(dark_surface), dark_text: theme::color_to_hex(dark_text), dark_border: theme::color_to_hex(dark_border), }; let Some(core) = state_for_custom_theme.borrow().core.clone() else { app.set_sync_status(UiMessage::plain("Custom theme updated for this session.")); return false; }; match runtime_for_custom_theme.block_on(core.set_custom_theme(custom_theme)) { Ok(()) => { app.set_sync_status(UiMessage::plain("Custom theme saved.")); true } Err(error) => { app.set_sync_status(UiMessage::detail( "Could not save custom theme: {}", error, )); false } } }, ); let app_weak = app.as_weak(); let state_for_language = Rc::clone(state); let runtime_for_language = Rc::clone(runtime); app.on_save_language(move |language| { let Some(app) = app_weak.upgrade() else { return; }; apply_language(&app, language.as_str()); let Some(core) = state_for_language.borrow().core.clone() else { app.set_sync_status(UiMessage::plain("Language updated for this session.")); return; }; match runtime_for_language.block_on(core.set_language(language.as_str())) { Ok(()) => app.set_sync_status(UiMessage::plain("Language preference saved.")), Err(error) => { app.set_sync_status(UiMessage::detail("Could not save language: {}", error)) } } }); let app_weak = app.as_weak(); let state_for_time_format = Rc::clone(state); let runtime_for_time_format = Rc::clone(runtime); app.on_save_time_format(move |value| { let Some(app) = app_weak.upgrade() else { return; }; let Some(format) = flectar_mail_core::models::TimeFormat::parse(&value) else { return; }; date_format::reload(&app); let Some(core) = state_for_time_format.borrow().core.clone() else { app.set_sync_status(UiMessage::plain("Time format updated for this session.")); return; }; match runtime_for_time_format.block_on(core.set_time_format(format)) { Ok(()) => app.set_sync_status(UiMessage::plain("Time format preference saved.")), Err(error) => { app.set_sync_status(UiMessage::detail("Could not save time format: {}", error)) } } }); let app_weak = app.as_weak(); let state_for_app_icon = Rc::clone(state); let runtime_for_app_icon = Rc::clone(runtime); app.global::().on_choose(move |variant| { let Some(app) = app_weak.upgrade() else { return; }; let previous = crate::app_icon::choice(app.global::().get_variant()); let icon = crate::app_icon::choice(variant); crate::app_icon::apply(&app, icon); let Some(core) = state_for_app_icon.borrow().core.clone() else { app.set_sync_status(UiMessage::plain("App icon updated for this session.")); return; }; match runtime_for_app_icon.block_on(core.set_app_icon(icon)) { Ok(()) => app.set_sync_status(UiMessage::plain("App icon saved.")), Err(error) => { crate::app_icon::apply(&app, previous); app.set_sync_status(UiMessage::detail("Could not save app icon: {}", error)); } } }); let app_weak = app.as_weak(); let state_for_sidebar_icons = Rc::clone(state); let runtime_for_sidebar_icons = Rc::clone(runtime); app.on_save_sidebar_icon_style(move |monochrome| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_sidebar_icons.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Sidebar icon style updated for this session.", )); return; }; match runtime_for_sidebar_icons.block_on(core.set_monochrome_sidebar_icons(monochrome)) { Ok(()) => app.set_sync_status(UiMessage::plain("Sidebar icon preference saved.")), Err(error) => app.set_sync_status(UiMessage::detail( "Could not save sidebar icon preference: {}", error, )), } }); let app_weak = app.as_weak(); let state_for_avatars = Rc::clone(state); let runtime_for_avatars = Rc::clone(runtime); app.on_save_avatar_visibility(move |enabled| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_avatars.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Avatar preference updated for this session.", )); return; }; match runtime_for_avatars.block_on(core.set_show_avatars(enabled)) { Ok(()) => app.set_sync_status(UiMessage::plain("Avatar preference saved.")), Err(error) => app.set_sync_status(UiMessage::detail( "Could not save avatar preference: {}", error, )), } }); let app_weak = app.as_weak(); let state_for_tabs = Rc::clone(state); let runtime_for_tabs = Rc::clone(runtime); app.on_save_show_tabs(move |enabled| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_tabs.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Tabs preference updated for this session.", )); return; }; match runtime_for_tabs.block_on(core.set_show_tabs(enabled)) { Ok(()) => app.set_sync_status(UiMessage::plain("Tabs preference saved.")), Err(error) => app.set_sync_status(UiMessage::detail( "Could not save tabs preference: {}", error, )), } }); let app_weak = app.as_weak(); let state_for_date_groups = Rc::clone(state); let runtime_for_date_groups = Rc::clone(runtime); app.on_save_group_mail_by_date(move |enabled| { let Some(app) = app_weak.upgrade() else { return; }; state_for_date_groups.borrow_mut().mail_groups.clear(); refresh_rows_only(&app, &state_for_date_groups, &runtime_for_date_groups); let Some(core) = state_for_date_groups.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Mail grouping is active for this session.", )); return; }; match runtime_for_date_groups.block_on(core.set_group_mail_by_date(enabled)) { Ok(()) => app.set_sync_status(UiMessage::plain("Mail grouping preference saved.")), Err(error) => { app.set_group_mail_by_date(!enabled); refresh_rows_only(&app, &state_for_date_groups, &runtime_for_date_groups); app.set_sync_status(UiMessage::detail( "Could not save mail grouping preference: {}", error, )); } } }); let app_weak = app.as_weak(); let state_for_profiles = Rc::clone(state); let runtime_for_profiles = Rc::clone(runtime); app.on_save_mail_profile(move |profile_id, name, color| { let Some(app) = app_weak.upgrade() else { return Default::default(); }; let Some(core) = state_for_profiles.borrow().core.clone() else { app.set_sync_status(UiMessage::plain("Profile changes require local mail data.")); return Default::default(); }; let profile_id = (!profile_id.trim().is_empty()).then(|| profile_id.to_string()); let (mut settings, profile) = match runtime_for_profiles.block_on(core.save_mail_profile( profile_id, name.to_string(), theme::color_to_hex(color), )) { Ok(saved) => saved, Err(error) => { app.set_sync_status(UiMessage::detail("Could not save profile: {}", error)); return Default::default(); } }; // The editor's checked accounts become the profile's accounts. let members = app.get_profile_members(); let mut failure = None; for member in members.iter() { let account_id = i64::from(member.id); let in_profile = profile.account_ids.contains(&account_id); let assignment = match (member.checked, in_profile) { (true, false) => Some(profile.id.clone()), (false, true) => None, _ => continue, }; match runtime_for_profiles.block_on(core.assign_account_profile(account_id, assignment)) { Ok(updated) => settings = updated, Err(error) => failure = Some(error), } } apply_account_presentation_settings( &app, &state_for_profiles, &runtime_for_profiles, &settings, ); match failure { Some(error) => app.set_sync_status(UiMessage::detail( "Could not update account profile: {}", error, )), None => app.set_sync_status(UiMessage::plain("Profile saved.")), } profile.id.into() }); let app_weak = app.as_weak(); app.on_prepare_profile_members(move |profile_id, checked_account| { let Some(app) = app_weak.upgrade() else { return; }; let rows = connected_account_rows(&app) .into_iter() .map(|account| { let member = !profile_id.is_empty() && account.profile_id == profile_id; ProfileMemberRow { id: account.id, name: account.name, email: account.email, initials: account.initials, avatar: account.avatar_small, has_avatar: account.has_avatar, other_profile: if member { Default::default() } else { account.profile_name }, checked: member || account.id == checked_account, } }) .collect::>(); app.set_profile_members(ModelRc::new(VecModel::from(rows))); }); let app_weak = app.as_weak(); let state_for_profile_assignment = Rc::clone(state); let runtime_for_profile_assignment = Rc::clone(runtime); app.on_assign_account_profile(move |account_id, profile_id| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_profile_assignment.borrow().core.clone() else { app.set_sync_status(UiMessage::plain("Profile changes require local mail data.")); return; }; let profile_id = (!profile_id.trim().is_empty()).then(|| profile_id.to_string()); match runtime_for_profile_assignment .block_on(core.assign_account_profile(i64::from(account_id), profile_id)) { Ok(settings) => { apply_account_presentation_settings( &app, &state_for_profile_assignment, &runtime_for_profile_assignment, &settings, ); app.set_sync_status(UiMessage::plain("Account profile updated.")); } Err(error) => app.set_sync_status(UiMessage::detail( "Could not update account profile: {}", error, )), } }); let app_weak = app.as_weak(); let state_for_account_color = Rc::clone(state); let runtime_for_account_color = Rc::clone(runtime); app.on_save_account_color(move |account_id, color, generated| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_account_color.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Account color changes require local mail data.", )); return; }; let color = (!generated).then(|| theme::color_to_hex(color)); match runtime_for_account_color .block_on(core.set_account_color(i64::from(account_id), color)) { Ok(settings) => { apply_account_presentation_settings( &app, &state_for_account_color, &runtime_for_account_color, &settings, ); app.set_sync_status(UiMessage::plain("Account color updated.")); } Err(error) => app.set_sync_status(UiMessage::detail( "Could not update account color: {}", error, )), } }); let app_weak = app.as_weak(); let state_for_active_profile = Rc::clone(state); let runtime_for_active_profile = Rc::clone(runtime); app.on_select_mail_profile(move |profile_id| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_active_profile.borrow().core.clone() else { app.set_sync_status(UiMessage::plain("Profile changes require local mail data.")); return; }; let profile_id = (!profile_id.trim().is_empty()).then(|| profile_id.to_string()); match runtime_for_active_profile.block_on(core.set_active_mail_profile(profile_id)) { Ok(settings) => apply_account_presentation_settings( &app, &state_for_active_profile, &runtime_for_active_profile, &settings, ), Err(error) => { app.set_sync_status(UiMessage::detail("Could not switch profile: {}", error)) } } }); let app_weak = app.as_weak(); let state_for_profile_delete = Rc::clone(state); let runtime_for_profile_delete = Rc::clone(runtime); app.on_delete_mail_profile(move |profile_id| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_profile_delete.borrow().core.clone() else { app.set_sync_status(UiMessage::plain("Profile changes require local mail data.")); return; }; match runtime_for_profile_delete.block_on(core.delete_mail_profile(profile_id.to_string())) { Ok(settings) => { apply_account_presentation_settings( &app, &state_for_profile_delete, &runtime_for_profile_delete, &settings, ); app.set_sync_status(UiMessage::plain("Profile deleted.")); } Err(error) => { app.set_sync_status(UiMessage::detail("Could not delete profile: {}", error)) } } }); let app_weak = app.as_weak(); let state_for_marker_visibility = Rc::clone(state); let runtime_for_marker_visibility = Rc::clone(runtime); app.on_save_account_markers_visibility(move |enabled| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_marker_visibility.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Marker preference is active for this session.", )); state_for_marker_visibility .borrow_mut() .account_presentation .show_markers = enabled; refresh_rows_only( &app, &state_for_marker_visibility, &runtime_for_marker_visibility, ); return; }; match runtime_for_marker_visibility.block_on(core.set_show_account_badges(enabled)) { Ok(settings) => { apply_account_presentation_settings( &app, &state_for_marker_visibility, &runtime_for_marker_visibility, &settings, ); app.set_sync_status(UiMessage::plain("Mail-list marker preference saved.")); } Err(error) => app.set_sync_status(UiMessage::detail( "Could not save mail-list marker preference: {}", error, )), } }); let app_weak = app.as_weak(); let state_for_workspace_layout = Rc::clone(state); let runtime_for_workspace_layout = Rc::clone(runtime); app.on_save_workspace_layout(move |layout| { let Some(app) = app_weak.upgrade() else { return; }; let Some(core) = state_for_workspace_layout.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Workspace layout updated for this session.", )); return; }; match runtime_for_workspace_layout.block_on(core.set_workspace_layout(layout.as_str())) { Ok(()) => app.set_sync_status(UiMessage::plain("Workspace layout preference saved.")), Err(error) => app.set_sync_status(UiMessage::detail( "Could not save workspace layout: {}", error, )), } }); let app_weak = app.as_weak(); let state_for_list_pane_width = Rc::clone(state); let runtime_for_list_pane_width = Rc::clone(runtime); app.on_save_workspace_list_pane_width(move |width| { let Some(app) = app_weak.upgrade() else { return; }; let width = flectar_mail_core::models::normalized_workspace_list_pane_width(width.round() as i64); app.set_workspace_list_pane_width(width as f32); let Some(core) = state_for_list_pane_width.borrow().core.clone() else { return; }; if let Err(error) = runtime_for_list_pane_width.block_on(core.set_workspace_list_pane_width(width)) { app.set_sync_status(UiMessage::detail( "Could not save list pane width: {}", error, )); } }); } /// Both entry points share one asynchronous persistence path and busy state. pub(super) fn register_oauth_settings_callbacks( app: &AppWindow, state: &Rc>, runtime: &Rc, ) { let weak = app.as_weak(); let single_state = Rc::clone(state); let single_runtime = Rc::clone(runtime); app.on_save_oauth_app(move |provider, client_id, client_secret| { let Some(app) = weak.upgrade() else { return }; let (google, microsoft) = match provider.as_str() { "gmail" => ( Some((client_id.to_string(), client_secret.to_string())), None, ), "microsoft" => (None, Some(client_id.to_string())), _ => return, }; save_oauth_settings( &app, &single_state, &single_runtime, google, microsoft, false, ); }); let weak = app.as_weak(); let state = Rc::clone(state); let runtime = Rc::clone(runtime); app.on_save_oauth_apps(move |google_id, google_secret, microsoft_id| { let Some(app) = weak.upgrade() else { return }; save_oauth_settings( &app, &state, &runtime, Some((google_id.to_string(), google_secret.to_string())), Some(microsoft_id.to_string()), true, ); }); } fn save_oauth_settings( app: &AppWindow, state: &Rc>, runtime: &tokio::runtime::Runtime, google: Option<(String, String)>, microsoft: Option, close_dialog: bool, ) { if app.get_oauth_settings_saving() || app.get_oauth_in_progress() { return; } let Some(core) = state.borrow().core.clone() else { let message = UiMessage::plain("Local mail data is unavailable. Retry startup."); app.set_sync_status(message.clone()); app.set_oauth_settings_error(ui_message::translated(app, &message)); return; }; let update_google = google.is_some(); let update_microsoft = microsoft.is_some(); app.set_oauth_settings_error("".into()); app.set_oauth_settings_saving(true); let weak = app.as_weak(); runtime.spawn(async move { let result = core.set_oauth_apps(google, microsoft).await; let _ = weak.upgrade_in_event_loop(move |app| { app.set_oauth_settings_saving(false); match result { Ok(settings) => { if update_google { app.set_google_client_id(settings.google_client_id.clone().into()); app.set_google_client_secret(settings.google_client_secret.clone().into()); } if update_microsoft { app.set_ms_client_id(settings.ms_client_id.clone().into()); } app.set_custom_oauth_configured( !settings.google_client_id.is_empty() || !settings.ms_client_id.is_empty(), ); startup::refresh_oauth_availability(&app); app.set_sync_status(UiMessage::plain("OAuth app keys saved.")); if close_dialog { app.set_oauth_setup_open(false); } } Err(error) => { let message = UiMessage::detail("OAuth settings failed: {}", error); app.set_sync_status(message.clone()); app.set_oauth_settings_error(ui_message::translated(&app, &message)); } } }); }); } #[cfg(test)] mod tests { use super::*; #[test] fn hiding_settings_discards_an_unsaved_palette() { use slint::platform::software_renderer::{MinimalSoftwareWindow, RepaintBufferType}; struct Headless(Rc); impl slint::platform::Platform for Headless { fn create_window_adapter( &self, ) -> Result, slint::PlatformError> { Ok(self.0.clone()) } } let window = MinimalSoftwareWindow::new(RepaintBufferType::NewBuffer); slint::platform::set_platform(Box::new(Headless(window))).unwrap(); let app = AppWindow::new().unwrap(); let theme = app.global::(); let draft = app.global::(); let saved = theme.get_custom_light_primary(); app.set_settings_open(true); slint::platform::update_timers_and_animations(); // The editor previews a color before it is saved. draft.invoke_begin(); theme.set_custom_light_primary(slint::Color::from_rgb_u8(0xff, 0, 0)); app.set_settings_open(false); slint::platform::update_timers_and_animations(); assert_eq!(theme.get_custom_light_primary(), saved); assert!(!draft.get_dirty()); // A saved palette stays. app.set_settings_open(true); slint::platform::update_timers_and_animations(); draft.invoke_begin(); let chosen = slint::Color::from_rgb_u8(0, 0x80, 0); theme.set_custom_light_primary(chosen); draft.set_dirty(false); app.set_settings_open(false); slint::platform::update_timers_and_animations(); assert_eq!(theme.get_custom_light_primary(), chosen); } }