From 21169095e6b723f26a298f33f994a0c32c90f1d6 Mon Sep 17 00:00:00 2001 From: springmin Date: Sat, 17 Oct 2026 12:29:36 +0800 Subject: [PATCH 01/11] documents: route attachments, backups and snapshots through the host provider Compose attachments, account-backup export/import and database-snapshot export still opened desktop file dialogs directly, so on mobile they reported "no picker" even though the host provides a DocumentProvider. - DocumentProvider::import_many() imports several documents for one action (default: repeat import() until the picker is cancelled; the desktop provider uses one multi-select dialog). Imported documents stay alive in the composer until Core has staged the attachments. - Compose attachments use import_many() instead of rfd::FileDialog. - Backup export/import stage one app-private file and hand it to the provider; the existing import logic keeps its synchronous shape on the UI thread executor. - Database snapshots keep Core's verified snapshot, archive it as one zip (hosts export files, not folders) and pass it to the provider. Staging stays outside the data/cache roots Core requires. - Desktop behavior is unchanged: same dialogs and file names. --- Cargo.lock | 1 + Cargo.toml | 2 + platform/README.md | 4 - platform/android/Cargo.lock | 20 + src/compose_controller.rs | 4 + src/data_controller.rs | 919 +++++++++++++++++++++--------------- src/documents.rs | 86 +++- src/main.rs | 178 ++++--- 8 files changed, 750 insertions(+), 464 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fd93ea10..7ca2cdae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2214,6 +2214,7 @@ dependencies = [ "windows-sys 0.61.2", "winresource", "x11rb", + "zip", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3f3a10ec..36cd74f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,8 @@ base64 = "0.22" percent-encoding = "2" data-url = "0.3" html5ever = "0.40" +# Snapshot archives one verified database snapshot for the document provider. +zip = { version = "9.0.4", default-features = false } cssparser = "0.38" # Match Blitz's Stylo version when adapting resolved CSS colors and gradients. style = { package = "stylo", version = "=0.22.0" } diff --git a/platform/README.md b/platform/README.md index 11e7b4d9..a9244ee3 100644 --- a/platform/README.md +++ b/platform/README.md @@ -73,10 +73,6 @@ Hosts that launch through `src/desktop.rs` (desktop and iOS) also provide ### Known gaps -- Compose attachments, account backups and database snapshots still use - desktop file dialogs directly. On `mobile` they report that the host has no - picker. Routing them through `DocumentProvider` will make them available to - every host. - iOS has no Keychain-backed `CredentialStore` yet and uses `UnavailableCredentialStore`. diff --git a/platform/android/Cargo.lock b/platform/android/Cargo.lock index 01f78309..e05111b8 100644 --- a/platform/android/Cargo.lock +++ b/platform/android/Cargo.lock @@ -1794,6 +1794,7 @@ dependencies = [ "windows-sys 0.61.2", "winresource", "x11rb", + "zip", ] [[package]] @@ -6945,6 +6946,12 @@ dependencies = [ "serde", ] +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typenum" version = "1.20.1" @@ -8501,6 +8508,19 @@ dependencies = [ "syn 3.0.4", ] +[[package]] +name = "zip" +version = "9.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22" +dependencies = [ + "crc32fast", + "indexmap", + "memchr", + "nix", + "typed-path", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/src/compose_controller.rs b/src/compose_controller.rs index 433a12d2..6cb45405 100644 --- a/src/compose_controller.rs +++ b/src/compose_controller.rs @@ -542,11 +542,15 @@ pub(super) fn complete_compose_recipient( pub(super) fn clear_compose( app: &AppWindow, files: &Rc>>, + imports: &Rc>>>, document: &Rc>, editor: &Rc>, contacts: &Rc>>, ) { files.borrow_mut().clear(); + // Releases host-owned import copies once Core has staged the attachments + // (drafts and sends copy the files before this runs). + imports.borrow_mut().clear(); app.global::() .invoke_composer_reset(); document.borrow_mut().reset(); diff --git a/src/data_controller.rs b/src/data_controller.rs index e59d910f..75fd6aab 100644 --- a/src/data_controller.rs +++ b/src/data_controller.rs @@ -341,58 +341,475 @@ fn read_account_backup(path: &std::path::Path) -> Result, String> { Ok(bytes) } -#[cfg(desktop)] -fn pick_backup_export_path(title: String, filter: String) -> Option { - rfd::FileDialog::new() - .set_title(title) - .set_file_name(format!( - "flectar-mail-backup-{}.json", - Local::now().format("%Y-%m-%d") - )) - .add_filter(filter, &["json"]) - .save_file() -} -#[cfg(mobile)] -fn pick_backup_export_path(_title: String, _filter: String) -> Option { - None +/// Applies an account backup the host already imported. The caller keeps the +/// imported document alive until this returns. +fn import_account_backup_document( + app: &AppWindow, + path: &std::path::Path, + core: &CoreMailSource, + state: &Rc>, + runtime: &Rc, +) { + let result = (|| -> Result<(usize, Settings, Vec, Vec), String> { + let bytes = read_account_backup(path)?; + let backup: serde_json::Value = + serde_json::from_slice(&bytes).map_err(|error| format!("invalid JSON: {error}"))?; + if backup.get("format").and_then(|value| value.as_str()) + != Some("flectar-mail-account-backup") + { + return Err("this is not a Flectar Mail account backup".into()); + } + if backup.get("version").and_then(|value| value.as_u64()) != Some(1) { + return Err("this backup version is not supported".into()); + } + if backup + .get("includesCredentials") + .and_then(|value| value.as_bool()) + == Some(true) + { + return Err("backups containing credentials are not accepted".into()); + } + let configs = serde_json::from_value::>( + backup + .get("accounts") + .cloned() + .ok_or_else(|| "backup has no accounts list".to_owned())?, + ) + .map_err(|error| format!("invalid account data: {error}"))?; + if configs.len() > 100 { + return Err("backup contains more than 100 accounts".into()); + } + let imported_emails = configs + .iter() + .map(|config| config.email.trim().to_ascii_lowercase()) + .collect::>(); + let portable_profiles = portable_mail_profiles(&backup, &imported_emails)?; + let has_portable_profiles = portable_profiles.is_some(); + let portable_profiles = portable_profiles.unwrap_or_default(); + let portable_colors = portable_account_colors(&backup, &imported_emails)?; + let has_portable_colors = portable_colors.is_some(); + let portable_colors = portable_colors.unwrap_or_default(); + + let mut settings = runtime.block_on(core.load_settings())?; + if let Some(preferences) = backup.get("preferences") { + if let Some(theme) = preferences.get("theme").and_then(|value| value.as_str()) + && matches!(theme, "system" | "carbon" | "snow" | "dark" | "light") + { + settings.theme = theme.to_owned(); + } + if let Some(enabled) = preferences + .get("monochromeSidebarIcons") + .and_then(|value| value.as_bool()) + { + settings.monochrome_sidebar_icons = enabled; + } + if let Some(enabled) = preferences + .get("showAvatars") + .and_then(|value| value.as_bool()) + { + settings.show_avatars = enabled; + } + if let Some(frame) = preferences + .get("windowFrame") + .and_then(|value| value.as_str()) + && matches!(frame, "system" | "integrated") + { + settings.window_frame = frame.to_owned(); + } + if let Some(layout) = preferences + .get("workspaceLayout") + .or_else(|| preferences.get("mailLayout")) + .and_then(|value| value.as_str()) + && matches!(layout, "default" | "minimal") + { + settings.workspace_layout = layout.to_owned(); + } + if let Some(preference) = preferences + .get("messageAppearance") + .and_then(|value| value.as_str()) + .and_then(flectar_mail_core::models::MessageAppearance::parse) + { + settings.message_appearance = preference; + } + if let Some(width) = preferences + .get("workspaceListPaneWidth") + .and_then(|value| value.as_i64()) + { + settings.workspace_list_pane_width = + normalized_workspace_list_pane_width(width); + } + if let Some(language) = preferences + .get("language") + .and_then(|value| value.as_str()) + && language.len() <= 32 + { + settings.language = language.to_owned(); + } + if let Some(week_start) = preferences + .get("calendarWeekStart") + .and_then(|value| value.as_str()) + && matches!(week_start, "sunday" | "monday") + { + settings.calendar_week_start = week_start.to_owned(); + } + if let Some(enabled) = preferences + .get("loadRemoteImages") + .and_then(|value| value.as_bool()) + { + settings.load_remote_images = enabled; + } + if let Some(enabled) = preferences + .get("markReadOnOpen") + .and_then(|value| value.as_bool()) + { + settings.mark_read_on_open = enabled; + } + if let Some(enabled) = preferences + .get("collectOutgoingContacts") + .and_then(|value| value.as_bool()) + { + settings.collect_outgoing_contacts = enabled; + } + if let Some(enabled) = preferences + .get("collectIncomingContacts") + .and_then(|value| value.as_bool()) + { + settings.collect_incoming_contacts = enabled; + } + if let Some(enabled) = preferences + .get("contactSuggestAllAccounts") + .and_then(|value| value.as_bool()) + { + settings.contact_suggest_all_accounts = enabled; + } + if let Some(enabled) = preferences + .get("suggestLearnedContacts") + .and_then(|value| value.as_bool()) + { + settings.suggest_learned_contacts = enabled; + } + if let Some(enabled) = preferences + .get("notificationsEnabled") + .and_then(|value| value.as_bool()) + { + settings.notifications_enabled = enabled; + } + if let Some(scope) = preferences + .get("notificationScope") + .and_then(|value| value.as_str()) + && matches!(scope, "important" | "all") + { + settings.notification_scope = scope.to_owned(); + } + if let Some(enabled) = preferences + .get("soundEnabled") + .and_then(|value| value.as_bool()) + { + settings.sound_enabled = enabled; + } + if let Some(minutes) = preferences + .get("syncIntervalMinutes") + .and_then(|value| value.as_i64()) + && matches!(minutes, 1 | 5 | 15) + { + settings.sync_interval_minutes = minutes; + } + if let Some(enabled) = preferences + .get("closeToTray") + .and_then(|value| value.as_bool()) + { + settings.close_to_tray = enabled; + } + if let Some(enabled) = preferences + .get("showAccountMarkers") + .and_then(|value| value.as_bool()) + { + settings.show_account_badges = enabled; + } + } + + let imported = runtime + .block_on(core.import_account_configs(configs))?; + runtime.block_on(core.save_settings(settings.clone()))?; + let accounts = runtime.block_on(core.load_accounts())?; + let configs = runtime.block_on(core.load_account_configs())?; + let account_ids = accounts + .iter() + .map(|account| (account.email.trim().to_ascii_lowercase(), account.id)) + .collect::>(); + if has_portable_colors { + for email in &imported_emails { + if let Some(account_id) = account_ids.get(email) { + runtime + .block_on(core.set_account_color(*account_id, None))?; + } + } + for (email, color) in portable_colors { + if let Some(account_id) = account_ids.get(&email) { + runtime + .block_on(core.set_account_color(*account_id, Some(color)))?; + } + } + } + if has_portable_profiles { + for email in &imported_emails { + if let Some(account_id) = account_ids.get(email) { + runtime + .block_on(core.assign_account_profile(*account_id, None))?; + } + } + } + for profile in portable_profiles { + let current = runtime.block_on(core.load_settings())?; + let existing_id = current + .mail_profiles + .iter() + .find(|candidate| candidate.name.eq_ignore_ascii_case(profile.name.trim())) + .map(|candidate| candidate.id.clone()); + let (_, saved) = runtime.block_on(core.save_mail_profile( + existing_id, + profile.name, + profile.color, + ))?; + for email in profile.account_emails { + if let Some(account_id) = account_ids.get(&email.trim().to_ascii_lowercase()) { + runtime.block_on(core.assign_account_profile( + *account_id, + Some(saved.id.clone()), + ))?; + } + } + } + settings = runtime.block_on(core.load_settings())?; + Ok((imported, settings, accounts, configs)) + })(); + + match result { + Ok((imported, settings, accounts, configs)) => { + let total = accounts.len(); + apply_language(app, &settings.language); + state.borrow_mut().account_presentation = + AccountPresentationSettings::from_settings(&settings); + update_connected_accounts( + app, + state, + runtime, + accounts, + configs, + ); + refresh_rows_only(app, state, runtime); + app.set_theme_mode( + match settings.theme.as_str() { + "carbon" | "dark" => "dark", + "snow" | "light" => "light", + _ => "system", + } + .into(), + ); + app.set_monochrome_sidebar_icons(settings.monochrome_sidebar_icons); + app.global::() + .set_preference(settings.message_appearance.as_str().into()); + app.set_show_avatars(settings.show_avatars); + app.set_show_account_markers(settings.show_account_badges); + crate::window_chrome::apply_preference( + app, + crate::window_chrome::WindowFrame::parse(&settings.window_frame), + ); + app.set_workspace_layout(settings.workspace_layout.clone().into()); + app.set_workspace_list_pane_width( + normalized_workspace_list_pane_width(settings.workspace_list_pane_width) + as f32, + ); + app.set_notifications_enabled(settings.notifications_enabled); + app.set_notification_sound_enabled(settings.sound_enabled); + app.set_notification_scope(settings.notification_scope.clone().into()); + app.set_sync_interval_minutes(settings.sync_interval_minutes as i32); + app.set_mark_read_on_open(settings.mark_read_on_open); + 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); + state.borrow_mut().mark_read_on_open = + settings.mark_read_on_open; + app.set_remote_images_enabled( + settings.load_remote_images && cfg!(feature = "remote-content"), + ); + #[cfg(not(any(mobile, feature = "flatpak")))] + app.set_close_to_tray(settings.close_to_tray); + #[cfg(any(mobile, feature = "flatpak"))] + app.set_close_to_tray(false); + app.set_sync_status(if imported == 0 { + UiMessage::detail( + "Backup imported. All {} account configurations already existed.", + total, + ) + } else { + UiMessage::detail( + "Imported {} account configurations. Open Accounts and sign in to each one.", + imported, + ) + }); + } + Err(error) => { + app.set_sync_status(UiMessage::detail("Could not import backup: {}", error)) + } + } } -#[cfg(desktop)] -fn pick_backup_import_path(title: String, filter: String) -> Option { - rfd::FileDialog::new() - .set_title(title) - .add_filter(filter, &["json"]) - .pick_file() +// --- staging helpers -------------------------------------------------------- + +/// A provider export reads from an app-private staging file; the host copies +/// it to whatever destination its picker offers. +async fn export_document_bytes( + documents: &std::sync::Arc, + staging_dir: PathBuf, + bytes: Vec, + name: String, +) -> Result { + let staging = staging_dir.join(&name); + let result = async { + tokio::fs::create_dir_all(&staging_dir) + .await + .map_err(|error| format!("creating {}: {error}", staging_dir.display()))?; + tokio::fs::write(&staging, bytes) + .await + .map_err(|error| format!("writing {}: {error}", staging.display()))?; + documents.export(staging.clone(), name).await + } + .await; + let _ = tokio::fs::remove_file(&staging).await; + result } -#[cfg(mobile)] -fn pick_backup_import_path(_title: String, _filter: String) -> Option { - None +/// Database snapshot staging must stay outside the application data and cache +/// roots: Core refuses to write a snapshot inside them. +fn snapshot_staging_root(paths: &Paths) -> PathBuf { + let temporary = std::env::temp_dir(); + if temporary.starts_with(&paths.data_dir) || temporary.starts_with(&paths.cache_dir) { + // Android points TMPDIR at the app cache; stage beside the data root. + return paths + .data_dir + .parent() + .map(|parent| parent.join("snapshot-staging")) + .unwrap_or_else(|| temporary.join("flectar-mail-snapshot")); + } + temporary.join("flectar-mail-snapshot") } -#[cfg(desktop)] -fn pick_database_snapshot_path(title: String) -> Option { - rfd::FileDialog::new() - .set_title(title) - .pick_folder() - .map(|directory| { - directory.join(format!( - "flectar-mail-snapshot-{}", - Local::now().format("%Y-%m-%d-%H%M%S") - )) +/// Creates the verified snapshot in staging, archives it as one file and hands +/// it to the document provider (mobile hosts export files, not folders). +async fn export_database_snapshot( + core: &CoreMailSource, + documents: std::sync::Arc, + staging_root: PathBuf, +) -> Result, String> { + // Unique per export (pid + sequence): the staging root may live in a + // shared temporary directory, so a predictable name could be pre-created. + static SEQUENCE: AtomicU64 = AtomicU64::new(0); + let sequence = SEQUENCE.fetch_add(1, Ordering::Relaxed); + let unique = format!( + "flectar-mail-snapshot-{}-{sequence}-{}", + std::process::id(), + Local::now().format("%Y-%m-%d-%H%M%S") + ); + let name = format!("{unique}.zip"); + let directory = staging_root.join(&unique); + let archive = staging_root.join(&name); + let result = async { + tokio::fs::create_dir_all(&staging_root) + .await + .map_err(|error| format!("creating {}: {error}", staging_root.display()))?; + // The snapshot is private mailbox data; keep it unreadable to other + // users of a shared temporary directory. + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions( + &staging_root, + std::fs::Permissions::from_mode(0o700), + ); + } + let _ = tokio::fs::remove_dir_all(&directory).await; + core.create_database_snapshot(directory.clone()) + .await + .map_err(|error| error.to_string())?; + let directory_for_archive = directory.clone(); + let archive_for_zip = archive.clone(); + tokio::task::spawn_blocking(move || { + archive_snapshot_directory(&directory_for_archive, &archive_for_zip) }) + .await + .map_err(|error| error.to_string())??; + let exported = documents.export(archive.clone(), name.clone()).await?; + Ok(exported.then_some(name)) + } + .await; + let _ = tokio::fs::remove_dir_all(&directory).await; + let _ = tokio::fs::remove_file(&archive).await; + result } -#[cfg(mobile)] -fn pick_database_snapshot_path(_title: String) -> Option { - None +/// The zip crate runs without compression features: snapshots are dense +/// SQLite stores plus already-compressed attachments. +fn archive_snapshot_directory( + directory: &std::path::Path, + archive: &std::path::Path, +) -> Result<(), String> { + fn append( + zip: &mut zip::ZipWriter, + root: &std::path::Path, + directory: &std::path::Path, + options: zip::write::SimpleFileOptions, + ) -> Result<(), String> { + let mut entries = std::fs::read_dir(directory) + .map_err(|error| format!("reading {}: {error}", directory.display()))? + .collect::, _>>() + .map_err(|error| error.to_string())?; + entries.sort_by_key(|entry| entry.file_name()); + for entry in entries { + let path = entry.path(); + if entry + .file_type() + .map(|kind| kind.is_symlink()) + .unwrap_or(true) + { + continue; + } + let name = path + .strip_prefix(root) + .map_err(|error| error.to_string())? + .to_string_lossy() + .replace('\\', "/"); + if path.is_dir() { + zip.add_directory(format!("{name}/"), options) + .map_err(|error| error.to_string())?; + append(zip, root, &path, options)?; + } else { + zip.start_file(name, options) + .map_err(|error| error.to_string())?; + let mut source = std::fs::File::open(&path) + .map_err(|error| format!("reading {}: {error}", path.display()))?; + std::io::copy(&mut source, zip).map_err(|error| error.to_string())?; + } + } + Ok(()) + } + let file = std::fs::File::create(archive) + .map_err(|error| format!("creating {}: {error}", archive.display()))?; + let mut zip = zip::ZipWriter::new(file); + let options = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Stored); + append(&mut zip, directory, directory, options)?; + zip.finish().map_err(|error| error.to_string())?; + Ok(()) } #[allow(clippy::too_many_arguments)] pub(super) fn register_data_management_callbacks( app: &AppWindow, paths: &Paths, + documents: &std::sync::Arc, state: &Rc>, runtime: &Rc, ui_task_tx: &UiSender, @@ -466,6 +883,8 @@ pub(super) fn register_data_management_callbacks( let app_weak = app.as_weak(); let state_for_backup = Rc::clone(state); let runtime_for_backup = Rc::clone(runtime); + let documents_for_backup = documents.clone(); + let paths_for_backup = paths.clone(); app.on_export_account_backup(move || { let Some(app) = app_weak.upgrade() else { return; @@ -480,21 +899,6 @@ pub(super) fn register_data_management_callbacks( )); return; } - let Some(path) = pick_backup_export_path( - translated( - &app, - &UiMessage::plain("Export Flectar Mail account backup"), - ) - .to_string(), - translated(&app, &UiMessage::plain("Flectar Mail backup")).to_string(), - ) else { - #[cfg(mobile)] - app.set_sync_status(UiMessage::plain( - "The platform host does not provide document export.", - )); - return; - }; - let preferences = core .as_ref() .and_then(|core| runtime_for_backup.block_on(core.load_settings()).ok()) @@ -575,16 +979,39 @@ pub(super) fn register_data_management_callbacks( "accounts": accounts, "preferences": preferences }); - match serde_json::to_vec_pretty(&backup) - .map_err(|error| error.to_string()) - .and_then(|bytes| std::fs::write(&path, bytes).map_err(|error| error.to_string())) - { - Ok(()) => app.set_sync_status(UiMessage::plain( - "Backup exported. Passwords and OAuth tokens were not included.", - )), + let bytes = match serde_json::to_vec_pretty(&backup) { + Ok(bytes) => bytes, Err(error) => { - app.set_sync_status(UiMessage::detail("Could not export backup: {}", error)) + app.set_sync_status(UiMessage::detail("Could not export backup: {}", error)); + return; + } + }; + let name = format!( + "flectar-mail-backup-{}.json", + Local::now().format("%Y-%m-%d") + ); + let documents = documents_for_backup.clone(); + let staging_dir = paths_for_backup.temp_dir(); + let app_weak = app.as_weak(); + let spawned = slint::spawn_local(async move { + let result = export_document_bytes(&documents, staging_dir, bytes, name).await; + let Some(app) = app_weak.upgrade() else { + return; + }; + match result { + Ok(true) => app.set_sync_status(UiMessage::plain( + "Backup exported. Passwords and OAuth tokens were not included.", + )), + Ok(false) => {} + Err(error) => { + app.set_sync_status(UiMessage::detail("Could not export backup: {}", error)) + } } + }); + if spawned.is_err() { + app.set_sync_status(UiMessage::plain( + "The platform host does not provide document export.", + )); } }); @@ -592,6 +1019,8 @@ pub(super) fn register_data_management_callbacks( let state_for_database_snapshot = Rc::clone(state); let runtime_for_database_snapshot = Rc::clone(runtime); let updates_for_database_snapshot = ui_task_tx.clone(); + let documents_for_database_snapshot = documents.clone(); + let paths_for_database_snapshot = paths.clone(); app.on_export_database_snapshot(move || { let Some(app) = app_weak.upgrade() else { return; @@ -602,27 +1031,20 @@ pub(super) fn register_data_management_callbacks( )); return; }; - let Some(path) = pick_database_snapshot_path( - translated( - &app, - &UiMessage::plain("Choose where to export the database snapshot"), - ) - .to_string(), - ) else { - #[cfg(mobile)] - app.set_sync_status(UiMessage::plain( - "The platform host does not provide document export.", - )); - return; - }; - app.set_sync_status(UiMessage::plain("Creating verified database snapshot…")); let updates = updates_for_database_snapshot.clone(); + let documents = documents_for_database_snapshot.clone(); + let staging_root = snapshot_staging_root(&paths_for_database_snapshot); runtime_for_database_snapshot.spawn(async move { - let display_path = path.display().to_string(); - let message = match core.create_database_snapshot(path).await { - Ok(_) => UiMessage::detail("Database snapshot exported to {}.", display_path), - Err(error) => UiMessage::detail("Could not export database snapshot: {}", error), + let message = match export_database_snapshot(&core, documents, staging_root).await { + Ok(Some(name)) => { + UiMessage::detail("Database snapshot exported to {}.", name) + } + // Cancelled at the picker: return the status line to idle. + Ok(None) => UiMessage::plain("Local data ready."), + Err(error) => { + UiMessage::detail("Could not export database snapshot: {}", error) + } }; let _ = updates .send(UiTaskUpdate { @@ -644,6 +1066,7 @@ pub(super) fn register_data_management_callbacks( let app_weak = app.as_weak(); let state_for_backup_import = Rc::clone(state); let runtime_for_backup_import = Rc::clone(runtime); + let documents_for_backup_import = documents.clone(); app.on_import_account_backup(move || { let Some(app) = app_weak.upgrade() else { return; @@ -654,325 +1077,37 @@ pub(super) fn register_data_management_callbacks( )); return; }; - let Some(path) = pick_backup_import_path( - translated( - &app, - &UiMessage::plain("Import Flectar Mail account backup"), - ) - .to_string(), - translated(&app, &UiMessage::plain("Flectar Mail backup")).to_string(), - ) else { - #[cfg(mobile)] + let documents = documents_for_backup_import.clone(); + let app_weak = app.as_weak(); + let state = Rc::clone(&state_for_backup_import); + let runtime = Rc::clone(&runtime_for_backup_import); + // The provider handshake runs on the UI thread's executor; the import + // keeps its synchronous shape so the existing state updates stay on + // the main loop. + let spawned = slint::spawn_local(async move { + let imported = documents.import().await; + let Some(app) = app_weak.upgrade() else { + return; + }; + let document = match imported { + Ok(Some(document)) => document, + Ok(None) => return, + Err(error) => { + app.set_sync_status(UiMessage::detail( + "Could not import backup: {}", + error, + )); + return; + } + }; + import_account_backup_document(&app, document.path(), &core, &state, &runtime); + }); + if spawned.is_err() { app.set_sync_status(UiMessage::plain( "The platform host does not provide document import.", )); - return; - }; - - let result = (|| -> Result<(usize, Settings, Vec, Vec), String> { - let bytes = read_account_backup(&path)?; - let backup: serde_json::Value = - serde_json::from_slice(&bytes).map_err(|error| format!("invalid JSON: {error}"))?; - if backup.get("format").and_then(|value| value.as_str()) - != Some("flectar-mail-account-backup") - { - return Err("this is not a Flectar Mail account backup".into()); - } - if backup.get("version").and_then(|value| value.as_u64()) != Some(1) { - return Err("this backup version is not supported".into()); - } - if backup - .get("includesCredentials") - .and_then(|value| value.as_bool()) - == Some(true) - { - return Err("backups containing credentials are not accepted".into()); - } - let configs = serde_json::from_value::>( - backup - .get("accounts") - .cloned() - .ok_or_else(|| "backup has no accounts list".to_owned())?, - ) - .map_err(|error| format!("invalid account data: {error}"))?; - if configs.len() > 100 { - return Err("backup contains more than 100 accounts".into()); - } - let imported_emails = configs - .iter() - .map(|config| config.email.trim().to_ascii_lowercase()) - .collect::>(); - let portable_profiles = portable_mail_profiles(&backup, &imported_emails)?; - let has_portable_profiles = portable_profiles.is_some(); - let portable_profiles = portable_profiles.unwrap_or_default(); - let portable_colors = portable_account_colors(&backup, &imported_emails)?; - let has_portable_colors = portable_colors.is_some(); - let portable_colors = portable_colors.unwrap_or_default(); - - let mut settings = runtime_for_backup_import.block_on(core.load_settings())?; - if let Some(preferences) = backup.get("preferences") { - if let Some(theme) = preferences.get("theme").and_then(|value| value.as_str()) - && matches!(theme, "system" | "carbon" | "snow" | "dark" | "light") - { - settings.theme = theme.to_owned(); - } - if let Some(enabled) = preferences - .get("monochromeSidebarIcons") - .and_then(|value| value.as_bool()) - { - settings.monochrome_sidebar_icons = enabled; - } - if let Some(enabled) = preferences - .get("showAvatars") - .and_then(|value| value.as_bool()) - { - settings.show_avatars = enabled; - } - if let Some(frame) = preferences - .get("windowFrame") - .and_then(|value| value.as_str()) - && matches!(frame, "system" | "integrated") - { - settings.window_frame = frame.to_owned(); - } - if let Some(layout) = preferences - .get("workspaceLayout") - .or_else(|| preferences.get("mailLayout")) - .and_then(|value| value.as_str()) - && matches!(layout, "default" | "minimal") - { - settings.workspace_layout = layout.to_owned(); - } - if let Some(preference) = preferences - .get("messageAppearance") - .and_then(|value| value.as_str()) - .and_then(flectar_mail_core::models::MessageAppearance::parse) - { - settings.message_appearance = preference; - } - if let Some(width) = preferences - .get("workspaceListPaneWidth") - .and_then(|value| value.as_i64()) - { - settings.workspace_list_pane_width = - normalized_workspace_list_pane_width(width); - } - if let Some(language) = preferences - .get("language") - .and_then(|value| value.as_str()) - && language.len() <= 32 - { - settings.language = language.to_owned(); - } - if let Some(week_start) = preferences - .get("calendarWeekStart") - .and_then(|value| value.as_str()) - && matches!(week_start, "sunday" | "monday") - { - settings.calendar_week_start = week_start.to_owned(); - } - if let Some(enabled) = preferences - .get("loadRemoteImages") - .and_then(|value| value.as_bool()) - { - settings.load_remote_images = enabled; - } - if let Some(enabled) = preferences - .get("markReadOnOpen") - .and_then(|value| value.as_bool()) - { - settings.mark_read_on_open = enabled; - } - if let Some(enabled) = preferences - .get("collectOutgoingContacts") - .and_then(|value| value.as_bool()) - { - settings.collect_outgoing_contacts = enabled; - } - if let Some(enabled) = preferences - .get("collectIncomingContacts") - .and_then(|value| value.as_bool()) - { - settings.collect_incoming_contacts = enabled; - } - if let Some(enabled) = preferences - .get("contactSuggestAllAccounts") - .and_then(|value| value.as_bool()) - { - settings.contact_suggest_all_accounts = enabled; - } - if let Some(enabled) = preferences - .get("suggestLearnedContacts") - .and_then(|value| value.as_bool()) - { - settings.suggest_learned_contacts = enabled; - } - if let Some(enabled) = preferences - .get("notificationsEnabled") - .and_then(|value| value.as_bool()) - { - settings.notifications_enabled = enabled; - } - if let Some(scope) = preferences - .get("notificationScope") - .and_then(|value| value.as_str()) - && matches!(scope, "important" | "all") - { - settings.notification_scope = scope.to_owned(); - } - if let Some(enabled) = preferences - .get("soundEnabled") - .and_then(|value| value.as_bool()) - { - settings.sound_enabled = enabled; - } - if let Some(minutes) = preferences - .get("syncIntervalMinutes") - .and_then(|value| value.as_i64()) - && matches!(minutes, 1 | 5 | 15) - { - settings.sync_interval_minutes = minutes; - } - if let Some(enabled) = preferences - .get("closeToTray") - .and_then(|value| value.as_bool()) - { - settings.close_to_tray = enabled; - } - if let Some(enabled) = preferences - .get("showAccountMarkers") - .and_then(|value| value.as_bool()) - { - settings.show_account_badges = enabled; - } - } - - let imported = runtime_for_backup_import - .block_on(core.import_account_configs(configs))?; - runtime_for_backup_import.block_on(core.save_settings(settings.clone()))?; - let accounts = runtime_for_backup_import.block_on(core.load_accounts())?; - let configs = runtime_for_backup_import.block_on(core.load_account_configs())?; - let account_ids = accounts - .iter() - .map(|account| (account.email.trim().to_ascii_lowercase(), account.id)) - .collect::>(); - if has_portable_colors { - for email in &imported_emails { - if let Some(account_id) = account_ids.get(email) { - runtime_for_backup_import - .block_on(core.set_account_color(*account_id, None))?; - } - } - for (email, color) in portable_colors { - if let Some(account_id) = account_ids.get(&email) { - runtime_for_backup_import - .block_on(core.set_account_color(*account_id, Some(color)))?; - } - } - } - if has_portable_profiles { - for email in &imported_emails { - if let Some(account_id) = account_ids.get(email) { - runtime_for_backup_import - .block_on(core.assign_account_profile(*account_id, None))?; - } - } - } - for profile in portable_profiles { - let current = runtime_for_backup_import.block_on(core.load_settings())?; - let existing_id = current - .mail_profiles - .iter() - .find(|candidate| candidate.name.eq_ignore_ascii_case(profile.name.trim())) - .map(|candidate| candidate.id.clone()); - let (_, saved) = runtime_for_backup_import.block_on(core.save_mail_profile( - existing_id, - profile.name, - profile.color, - ))?; - for email in profile.account_emails { - if let Some(account_id) = account_ids.get(&email.trim().to_ascii_lowercase()) { - runtime_for_backup_import.block_on(core.assign_account_profile( - *account_id, - Some(saved.id.clone()), - ))?; - } - } - } - settings = runtime_for_backup_import.block_on(core.load_settings())?; - Ok((imported, settings, accounts, configs)) - })(); - - match result { - Ok((imported, settings, accounts, configs)) => { - let total = accounts.len(); - apply_language(&app, &settings.language); - state_for_backup_import.borrow_mut().account_presentation = - AccountPresentationSettings::from_settings(&settings); - update_connected_accounts( - &app, - &state_for_backup_import, - &runtime_for_backup_import, - accounts, - configs, - ); - refresh_rows_only(&app, &state_for_backup_import, &runtime_for_backup_import); - app.set_theme_mode( - match settings.theme.as_str() { - "carbon" | "dark" => "dark", - "snow" | "light" => "light", - _ => "system", - } - .into(), - ); - app.set_monochrome_sidebar_icons(settings.monochrome_sidebar_icons); - app.global::() - .set_preference(settings.message_appearance.as_str().into()); - app.set_show_avatars(settings.show_avatars); - app.set_show_account_markers(settings.show_account_badges); - crate::window_chrome::apply_preference( - &app, - crate::window_chrome::WindowFrame::parse(&settings.window_frame), - ); - app.set_workspace_layout(settings.workspace_layout.clone().into()); - app.set_workspace_list_pane_width( - normalized_workspace_list_pane_width(settings.workspace_list_pane_width) - as f32, - ); - app.set_notifications_enabled(settings.notifications_enabled); - app.set_notification_sound_enabled(settings.sound_enabled); - app.set_notification_scope(settings.notification_scope.clone().into()); - app.set_sync_interval_minutes(settings.sync_interval_minutes as i32); - app.set_mark_read_on_open(settings.mark_read_on_open); - 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); - state_for_backup_import.borrow_mut().mark_read_on_open = - settings.mark_read_on_open; - app.set_remote_images_enabled( - settings.load_remote_images && cfg!(feature = "remote-content"), - ); - #[cfg(not(any(mobile, feature = "flatpak")))] - app.set_close_to_tray(settings.close_to_tray); - #[cfg(any(mobile, feature = "flatpak"))] - app.set_close_to_tray(false); - app.set_sync_status(if imported == 0 { - UiMessage::detail( - "Backup imported. All {} account configurations already existed.", - total, - ) - } else { - UiMessage::detail( - "Imported {} account configurations. Open Accounts and sign in to each one.", - imported, - ) - }); - } - Err(error) => { - app.set_sync_status(UiMessage::detail("Could not import backup: {}", error)) - } } + }); let (data_reset_raw_tx, data_reset_rx) = bounded_ui_channel::>(); diff --git a/src/documents.rs b/src/documents.rs index 884839a8..3483d4bd 100644 --- a/src/documents.rs +++ b/src/documents.rs @@ -39,9 +39,20 @@ impl Drop for DocumentRequestGuard { (std::mem::replace(&mut self.0, Box::new(|| {})))(); } } -pub trait DocumentProvider: Send + Sync { +pub trait DocumentProvider: Send + Sync + 'static { fn import(&self) -> DocumentFuture>; fn export(&self, path: PathBuf, name: String) -> DocumentFuture; + /// Import several documents for one user action (compose attachments). + /// Hosts whose picker selects multiple files at once override this; the + /// default imports one document per request, so a single-document picker + /// never opens twice for one action. Each returned document owns its copy, + /// so the caller must keep them alive until Core has staged the files. + fn import_many( + self: std::sync::Arc, + _limit: usize, + ) -> DocumentFuture> { + Box::pin(async move { Ok(self.import().await?.into_iter().collect()) }) + } } /// Desktop file dialogs. Mobile hosts inject their own provider through /// `PlatformContext::app_private`. @@ -70,6 +81,25 @@ impl DocumentProvider for DesktopDocuments { .map(|f| ImportedDocument::source(f.path().to_owned()))) }) } + fn import_many( + self: std::sync::Arc, + limit: usize, + ) -> DocumentFuture> { + Box::pin(async move { + Ok(rfd::AsyncFileDialog::new() + .set_title("Upload files") + .pick_files() + .await + .map(|files| { + files + .into_iter() + .take(limit) + .map(|file| ImportedDocument::source(file.path().to_owned())) + .collect() + }) + .unwrap_or_default()) + }) + } fn export(&self, path: PathBuf, name: String) -> DocumentFuture { Box::pin(async move { let Some(file) = rfd::AsyncFileDialog::new() @@ -156,6 +186,60 @@ mod tests { assert_eq!(cancelled.load(Ordering::SeqCst), 1); } + struct FakeDocuments { + directory: PathBuf, + imports: AtomicUsize, + cancel_after: usize, + } + + impl DocumentProvider for FakeDocuments { + fn import(&self) -> DocumentFuture> { + let seen = self.imports.fetch_add(1, Ordering::SeqCst); + let cancel = seen >= self.cancel_after; + let path = self.directory.join(format!("import-{seen}")); + Box::pin(async move { + if cancel { + return Ok(None); + } + std::fs::write(&path, b"attachment").unwrap(); + Ok(Some(ImportedDocument::temporary(path, |path| { + let _ = std::fs::remove_file(path); + }))) + }) + } + fn export(&self, _path: PathBuf, _name: String) -> DocumentFuture { + Box::pin(async { Ok(false) }) + } + } + + #[tokio::test] + async fn import_many_uses_one_picker_request() { + let directory = tempfile::tempdir().unwrap(); + let provider = Arc::new(FakeDocuments { + directory: directory.path().to_owned(), + imports: AtomicUsize::new(0), + cancel_after: usize::MAX, + }); + let documents = provider.clone().import_many(20).await.unwrap(); + assert_eq!(documents.len(), 1); + assert_eq!(provider.imports.load(Ordering::SeqCst), 1); + drop(documents); + assert_eq!(std::fs::read_dir(directory.path()).unwrap().count(), 0); + } + + #[tokio::test] + async fn import_many_returns_empty_when_the_picker_is_cancelled() { + let directory = tempfile::tempdir().unwrap(); + let provider = Arc::new(FakeDocuments { + directory: directory.path().to_owned(), + imports: AtomicUsize::new(0), + cancel_after: 0, + }); + let documents = provider.clone().import_many(20).await.unwrap(); + assert!(documents.is_empty()); + assert_eq!(provider.imports.load(Ordering::SeqCst), 1); + } + #[test] fn desktop_source_survives_document_drop() { let dir = tempfile::tempdir().unwrap(); diff --git a/src/main.rs b/src/main.rs index 541a902e..b7029667 100644 --- a/src/main.rs +++ b/src/main.rs @@ -132,6 +132,7 @@ const SENDER_AVATAR_REGULAR_SIDE: f32 = 38.0; const ACCOUNT_AVATAR_SMALL_SIDE: f32 = 22.0; const ACCOUNT_AVATAR_REGULAR_SIDE: f32 = 38.0; const MAX_COMPOSE_ATTACHMENT_BYTES: u64 = 25 * 1024 * 1024; +const MAX_COMPOSE_ATTACHMENT_FILES: usize = 20; fn favicon_retry_delay(attempts: u8) -> Duration { let exponent = attempts.saturating_sub(1).min(6); @@ -326,16 +327,6 @@ fn reconcile_model_rows_by( } } -#[cfg(desktop)] -fn pick_compose_attachment_paths(title: String) -> Option> { - rfd::FileDialog::new().set_title(title).pick_files() -} - -#[cfg(mobile)] -fn pick_compose_attachment_paths(_title: String) -> Option> { - None -} - #[derive(Clone, Debug)] struct ComposeFile { path: PathBuf, @@ -1402,7 +1393,10 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> }); theme::register_theme_utilities(&app); app.set_print_supported(!cfg!(mobile)); - app.set_document_apis_supported(!cfg!(mobile)); + // Every host provides a document provider now: desktop dialogs on desktop, + // the system picker on mobile. The document entry points (compose + // attachments, account backups, database snapshots) stay available. + app.set_document_apis_supported(true); renderer_preferences::initialize_step(use_wgpu, || app.show())?; startup_metrics.emit( "window_shown", @@ -5206,6 +5200,9 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> register_settings_preference_callbacks(&app, &state, &runtime); let compose_files = Rc::new(RefCell::new(Vec::::new())); + let compose_imports = Rc::new(RefCell::new( + Vec::>::new(), + )); let compose_document = Rc::new(RefCell::new(RichComposeDocument::default())); let compose_editor = Rc::new(RefCell::new(LazyComposeEditor::default())); let compose_contacts = Rc::new(RefCell::new( @@ -5459,6 +5456,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let state_for_message_compose = Rc::clone(&state); let runtime_for_message_compose = Rc::clone(&runtime); let files_for_message_compose = Rc::clone(&compose_files); + let imports_for_message_compose = Rc::clone(&compose_imports); let document_for_message_compose = Rc::clone(&compose_document); let editor_for_message_compose = Rc::clone(&compose_editor); let contacts_for_message_compose = Rc::clone(&compose_contacts); @@ -5544,6 +5542,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> clear_compose( &app, &files_for_message_compose, + &imports_for_message_compose, &document_for_message_compose, &editor_for_message_compose, &contacts_for_message_compose, @@ -5570,6 +5569,8 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> } }) .collect::>(); + *imports_for_message_compose.borrow_mut() = + loaded_files.iter().map(|_| None).collect(); *files_for_message_compose.borrow_mut() = loaded_files; apply_compose_files(&app, &files_for_message_compose.borrow()); @@ -5644,6 +5645,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> clear_compose( &app, &files_for_message_compose, + &imports_for_message_compose, &document_for_message_compose, &editor_for_message_compose, &contacts_for_message_compose, @@ -5683,6 +5685,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let app_weak = app.as_weak(); let files_for_close = Rc::clone(&compose_files); + let imports_for_close = Rc::clone(&compose_imports); let document_for_close = Rc::clone(&compose_document); let editor_for_close = Rc::clone(&compose_editor); let contacts_for_close = Rc::clone(&compose_contacts); @@ -5696,6 +5699,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> clear_compose( &app, &files_for_close, + &imports_for_close, &document_for_close, &editor_for_close, &contacts_for_close, @@ -5706,76 +5710,107 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let app_weak = app.as_weak(); let files_for_picker = Rc::clone(&compose_files); + let imports_for_picker = Rc::clone(&compose_imports); + let documents_for_picker = platform.documents.clone(); + let picking_for_picker = Rc::new(Cell::new(false)); app.on_pick_compose_attachments(move || { let Some(app) = app_weak.upgrade() else { return; }; - let Some(paths) = pick_compose_attachment_paths( - translated(&app, &UiMessage::plain("Attach files to this email")).to_string(), - ) else { - #[cfg(mobile)] - { - app.set_compose_notice(UiMessage::plain( - "The platform host does not provide a document picker.", - )); - app.set_compose_notice_is_error(true); - } + if picking_for_picker.replace(true) { return; - }; - - let mut compose_files = files_for_picker.borrow_mut(); - let mut total_size = compose_files.iter().map(|file| file.size).sum::(); - let mut added = 0usize; - let mut skipped = 0usize; - for path in paths { - let path = std::fs::canonicalize(&path).unwrap_or(path); - if compose_files.iter().any(|file| file.path == path) { - skipped += 1; - continue; - } - let Ok(metadata) = std::fs::metadata(&path) else { - skipped += 1; - continue; + } + let documents = documents_for_picker.clone(); + let app_weak = app.as_weak(); + let files = Rc::clone(&files_for_picker); + let imports = Rc::clone(&imports_for_picker); + let picking = Rc::clone(&picking_for_picker); + let picking_for_error = Rc::clone(&picking_for_picker); + let spawned = slint::spawn_local(async move { + let imported = documents.import_many(MAX_COMPOSE_ATTACHMENT_FILES).await; + picking.set(false); + let Some(app) = app_weak.upgrade() else { + return; }; - if !metadata.is_file() - || total_size.saturating_add(metadata.len()) > MAX_COMPOSE_ATTACHMENT_BYTES - { - skipped += 1; - continue; - } - let Some(filename) = path.file_name().map(|name| name.to_string_lossy().into_owned()) - else { - skipped += 1; - continue; + let documents = match imported { + Ok(documents) => documents, + Err(error) => { + tracing::warn!(%error, "compose attachment picker failed"); + app.set_compose_notice(UiMessage::plain( + "The platform host does not provide a document picker.", + )); + app.set_compose_notice_is_error(true); + return; + } }; - total_size += metadata.len(); - compose_files.push(ComposeFile { - path, - filename, - size: metadata.len(), - }); - added += 1; - } - apply_compose_files(&app, &compose_files); - if skipped == 0 { - app.set_compose_notice(if added == 1 { - UiMessage::plain("Added 1 attachment.") + if documents.is_empty() { + return; + } + let mut compose_files = files.borrow_mut(); + let mut compose_imports = imports.borrow_mut(); + let mut total_size = compose_files.iter().map(|file| file.size).sum::(); + let mut added = 0usize; + let mut skipped = 0usize; + for document in documents { + let path = std::fs::canonicalize(document.path()) + .unwrap_or_else(|_| document.path().to_path_buf()); + if compose_files.iter().any(|file| file.path == path) { + skipped += 1; + continue; + } + let Ok(metadata) = std::fs::metadata(&path) else { + skipped += 1; + continue; + }; + if !metadata.is_file() + || total_size.saturating_add(metadata.len()) > MAX_COMPOSE_ATTACHMENT_BYTES + { + skipped += 1; + continue; + } + let Some(filename) = path.file_name().map(|name| name.to_string_lossy().into_owned()) + else { + skipped += 1; + continue; + }; + total_size += metadata.len(); + compose_files.push(ComposeFile { + path, + filename, + size: metadata.len(), + }); + compose_imports.push(Some(document)); + added += 1; + } + apply_compose_files(&app, &compose_files); + if skipped == 0 { + app.set_compose_notice(if added == 1 { + UiMessage::plain("Added 1 attachment.") + } else { + UiMessage::detail("Added {} attachments.", added) + }); + app.set_compose_notice_is_error(false); } else { - UiMessage::detail("Added {} attachments.", added) - }); - app.set_compose_notice_is_error(false); - } else { - app.set_compose_notice(UiMessage::arguments( - "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB total limit).", - added, - skipped, + app.set_compose_notice(UiMessage::arguments( + "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB total limit).", + added, + skipped, + )); + app.set_compose_notice_is_error(added == 0); + } + }); + if spawned.is_err() { + picking_for_error.set(false); + app.set_compose_notice(UiMessage::plain( + "The platform host does not provide a document picker.", )); - app.set_compose_notice_is_error(added == 0); + app.set_compose_notice_is_error(true); } }); let app_weak = app.as_weak(); let files_for_remove = Rc::clone(&compose_files); + let imports_for_remove = Rc::clone(&compose_imports); app.on_remove_compose_attachment(move |index| { let Some(app) = app_weak.upgrade() else { return; @@ -5786,6 +5821,10 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let mut files = files_for_remove.borrow_mut(); if index < files.len() { files.remove(index); + let mut imports = imports_for_remove.borrow_mut(); + if index < imports.len() { + imports.remove(index); + } apply_compose_files(&app, &files); app.set_compose_notice(UiMessage::plain("Attachment removed.")); app.set_compose_notice_is_error(false); @@ -6040,6 +6079,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let state_for_compose = Rc::clone(&state); let runtime_for_compose = Rc::clone(&runtime); let files_for_save = Rc::clone(&compose_files); + let imports_for_save = Rc::clone(&compose_imports); let document_for_save = Rc::clone(&compose_document); let editor_for_save = Rc::clone(&compose_editor); let contacts_for_save = Rc::clone(&compose_contacts); @@ -6164,6 +6204,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> clear_compose( &app, &files_for_save, + &imports_for_save, &document_for_save, &editor_for_save, &contacts_for_save, @@ -6187,6 +6228,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let state_for_send_result = Rc::clone(&state); let runtime_for_send_result = Rc::clone(&runtime); let files_for_send_result = Rc::clone(&compose_files); + let imports_for_send_result = Rc::clone(&compose_imports); let document_for_send_result = Rc::clone(&compose_document); let editor_for_send_result = Rc::clone(&compose_editor); let contacts_for_send_result = Rc::clone(&compose_contacts); @@ -6207,6 +6249,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> clear_compose( &app, &files_for_send_result, + &imports_for_send_result, &document_for_send_result, &editor_for_send_result, &contacts_for_send_result, @@ -6888,6 +6931,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> register_data_management_callbacks( &app, &platform.paths, + &platform.documents, &state, &runtime, &ui_task_tx, From c124518b232887491471993c56a85994d690fa55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Sun, 18 Oct 2026 03:36:22 +0200 Subject: [PATCH 02/11] refactor: simplify import_many --- src/documents.rs | 42 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/documents.rs b/src/documents.rs index 3483d4bd..72a5baa6 100644 --- a/src/documents.rs +++ b/src/documents.rs @@ -39,19 +39,16 @@ impl Drop for DocumentRequestGuard { (std::mem::replace(&mut self.0, Box::new(|| {})))(); } } -pub trait DocumentProvider: Send + Sync + 'static { +pub trait DocumentProvider: Send + Sync { fn import(&self) -> DocumentFuture>; fn export(&self, path: PathBuf, name: String) -> DocumentFuture; - /// Import several documents for one user action (compose attachments). - /// Hosts whose picker selects multiple files at once override this; the - /// default imports one document per request, so a single-document picker - /// never opens twice for one action. Each returned document owns its copy, - /// so the caller must keep them alive until Core has staged the files. - fn import_many( - self: std::sync::Arc, - _limit: usize, - ) -> DocumentFuture> { - Box::pin(async move { Ok(self.import().await?.into_iter().collect()) }) + /// Imports the documents for one user action (compose attachments). The + /// default opens a single-document picker once; hosts with a multi-select + /// picker override it. `limit` bounds how many copies a host makes; + /// hosts that return source paths need not apply it. + fn import_many(&self, _limit: usize) -> DocumentFuture> { + let import = self.import(); + Box::pin(async move { Ok(import.await?.into_iter().collect()) }) } } /// Desktop file dialogs. Mobile hosts inject their own provider through @@ -81,23 +78,16 @@ impl DocumentProvider for DesktopDocuments { .map(|f| ImportedDocument::source(f.path().to_owned()))) }) } - fn import_many( - self: std::sync::Arc, - limit: usize, - ) -> DocumentFuture> { - Box::pin(async move { + fn import_many(&self, _limit: usize) -> DocumentFuture> { + Box::pin(async { Ok(rfd::AsyncFileDialog::new() .set_title("Upload files") .pick_files() .await - .map(|files| { - files - .into_iter() - .take(limit) - .map(|file| ImportedDocument::source(file.path().to_owned())) - .collect() - }) - .unwrap_or_default()) + .unwrap_or_default() + .into_iter() + .map(|file| ImportedDocument::source(file.path().to_owned())) + .collect()) }) } fn export(&self, path: PathBuf, name: String) -> DocumentFuture { @@ -220,7 +210,7 @@ mod tests { imports: AtomicUsize::new(0), cancel_after: usize::MAX, }); - let documents = provider.clone().import_many(20).await.unwrap(); + let documents = provider.import_many(20).await.unwrap(); assert_eq!(documents.len(), 1); assert_eq!(provider.imports.load(Ordering::SeqCst), 1); drop(documents); @@ -235,7 +225,7 @@ mod tests { imports: AtomicUsize::new(0), cancel_after: 0, }); - let documents = provider.clone().import_many(20).await.unwrap(); + let documents = provider.import_many(20).await.unwrap(); assert!(documents.is_empty()); assert_eq!(provider.imports.load(Ordering::SeqCst), 1); } From a41a7d8b50985473e128123274e50d9aa3876c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Sun, 18 Oct 2026 03:36:22 +0200 Subject: [PATCH 03/11] fix: run compose picker on the runtime --- src/compose_controller.rs | 7 ++--- src/main.rs | 62 +++++++++++++++++++++++---------------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/compose_controller.rs b/src/compose_controller.rs index 6cb45405..2effde40 100644 --- a/src/compose_controller.rs +++ b/src/compose_controller.rs @@ -542,15 +542,14 @@ pub(super) fn complete_compose_recipient( pub(super) fn clear_compose( app: &AppWindow, files: &Rc>>, - imports: &Rc>>>, + session: &Cell, document: &Rc>, editor: &Rc>, contacts: &Rc>>, ) { files.borrow_mut().clear(); - // Releases host-owned import copies once Core has staged the attachments - // (drafts and sends copy the files before this runs). - imports.borrow_mut().clear(); + // A picker still open for this composer must not attach to the next one. + session.set(session.get().wrapping_add(1)); app.global::() .invoke_composer_reset(); document.borrow_mut().reset(); diff --git a/src/main.rs b/src/main.rs index b7029667..8bc27af1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -327,11 +327,15 @@ fn reconcile_model_rows_by( } } -#[derive(Clone, Debug)] +#[derive(Clone)] struct ComposeFile { path: PathBuf, filename: String, size: u64, + /// Host-owned import copy behind `path`. Removing the attachment or + /// clearing the composer releases it; drafts and sends stage the file in + /// Core first. + _import: Option>, } struct FaviconUpdate { @@ -5200,9 +5204,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> register_settings_preference_callbacks(&app, &state, &runtime); let compose_files = Rc::new(RefCell::new(Vec::::new())); - let compose_imports = Rc::new(RefCell::new( - Vec::>::new(), - )); + let compose_session = Rc::new(Cell::new(0u64)); let compose_document = Rc::new(RefCell::new(RichComposeDocument::default())); let compose_editor = Rc::new(RefCell::new(LazyComposeEditor::default())); let compose_contacts = Rc::new(RefCell::new( @@ -5456,7 +5458,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let state_for_message_compose = Rc::clone(&state); let runtime_for_message_compose = Rc::clone(&runtime); let files_for_message_compose = Rc::clone(&compose_files); - let imports_for_message_compose = Rc::clone(&compose_imports); + let session_for_message_compose = Rc::clone(&compose_session); let document_for_message_compose = Rc::clone(&compose_document); let editor_for_message_compose = Rc::clone(&compose_editor); let contacts_for_message_compose = Rc::clone(&compose_contacts); @@ -5542,7 +5544,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> clear_compose( &app, &files_for_message_compose, - &imports_for_message_compose, + &session_for_message_compose, &document_for_message_compose, &editor_for_message_compose, &contacts_for_message_compose, @@ -5566,11 +5568,10 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> path, filename: attachment.filename.clone(), size, + _import: None, } }) .collect::>(); - *imports_for_message_compose.borrow_mut() = - loaded_files.iter().map(|_| None).collect(); *files_for_message_compose.borrow_mut() = loaded_files; apply_compose_files(&app, &files_for_message_compose.borrow()); @@ -5645,7 +5646,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> clear_compose( &app, &files_for_message_compose, - &imports_for_message_compose, + &session_for_message_compose, &document_for_message_compose, &editor_for_message_compose, &contacts_for_message_compose, @@ -5685,7 +5686,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let app_weak = app.as_weak(); let files_for_close = Rc::clone(&compose_files); - let imports_for_close = Rc::clone(&compose_imports); + let session_for_close = Rc::clone(&compose_session); let document_for_close = Rc::clone(&compose_document); let editor_for_close = Rc::clone(&compose_editor); let contacts_for_close = Rc::clone(&compose_contacts); @@ -5699,7 +5700,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> clear_compose( &app, &files_for_close, - &imports_for_close, + &session_for_close, &document_for_close, &editor_for_close, &contacts_for_close, @@ -5710,8 +5711,9 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let app_weak = app.as_weak(); let files_for_picker = Rc::clone(&compose_files); - let imports_for_picker = Rc::clone(&compose_imports); + let session_for_picker = Rc::clone(&compose_session); let documents_for_picker = platform.documents.clone(); + let runtime_for_picker = Rc::clone(&runtime); let picking_for_picker = Rc::new(Cell::new(false)); app.on_pick_compose_attachments(move || { let Some(app) = app_weak.upgrade() else { @@ -5720,18 +5722,31 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> if picking_for_picker.replace(true) { return; } - let documents = documents_for_picker.clone(); + // Providers rely on Tokio timers and file I/O, so the request runs on + // the runtime and the composer awaits it on the UI thread. + let request = runtime_for_picker + .spawn(documents_for_picker.import_many(MAX_COMPOSE_ATTACHMENT_FILES)); + let abort_request = request.abort_handle(); let app_weak = app.as_weak(); let files = Rc::clone(&files_for_picker); - let imports = Rc::clone(&imports_for_picker); + let session = Rc::clone(&session_for_picker); + let picked_in = session.get(); let picking = Rc::clone(&picking_for_picker); let picking_for_error = Rc::clone(&picking_for_picker); let spawned = slint::spawn_local(async move { - let imported = documents.import_many(MAX_COMPOSE_ATTACHMENT_FILES).await; + let imported = match request.await { + Ok(result) => result, + Err(error) => Err(error.to_string()), + }; picking.set(false); let Some(app) = app_weak.upgrade() else { return; }; + // The composer was sent, discarded or replaced while the picker + // was open; dropping the documents releases their copies. + if session.get() != picked_in { + return; + } let documents = match imported { Ok(documents) => documents, Err(error) => { @@ -5747,7 +5762,6 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> return; } let mut compose_files = files.borrow_mut(); - let mut compose_imports = imports.borrow_mut(); let mut total_size = compose_files.iter().map(|file| file.size).sum::(); let mut added = 0usize; let mut skipped = 0usize; @@ -5778,8 +5792,8 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> path, filename, size: metadata.len(), + _import: Some(Rc::new(document)), }); - compose_imports.push(Some(document)); added += 1; } apply_compose_files(&app, &compose_files); @@ -5800,6 +5814,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> } }); if spawned.is_err() { + abort_request.abort(); picking_for_error.set(false); app.set_compose_notice(UiMessage::plain( "The platform host does not provide a document picker.", @@ -5810,7 +5825,6 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let app_weak = app.as_weak(); let files_for_remove = Rc::clone(&compose_files); - let imports_for_remove = Rc::clone(&compose_imports); app.on_remove_compose_attachment(move |index| { let Some(app) = app_weak.upgrade() else { return; @@ -5821,10 +5835,6 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let mut files = files_for_remove.borrow_mut(); if index < files.len() { files.remove(index); - let mut imports = imports_for_remove.borrow_mut(); - if index < imports.len() { - imports.remove(index); - } apply_compose_files(&app, &files); app.set_compose_notice(UiMessage::plain("Attachment removed.")); app.set_compose_notice_is_error(false); @@ -6079,7 +6089,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let state_for_compose = Rc::clone(&state); let runtime_for_compose = Rc::clone(&runtime); let files_for_save = Rc::clone(&compose_files); - let imports_for_save = Rc::clone(&compose_imports); + let session_for_save = Rc::clone(&compose_session); let document_for_save = Rc::clone(&compose_document); let editor_for_save = Rc::clone(&compose_editor); let contacts_for_save = Rc::clone(&compose_contacts); @@ -6204,7 +6214,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> clear_compose( &app, &files_for_save, - &imports_for_save, + &session_for_save, &document_for_save, &editor_for_save, &contacts_for_save, @@ -6228,7 +6238,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> let state_for_send_result = Rc::clone(&state); let runtime_for_send_result = Rc::clone(&runtime); let files_for_send_result = Rc::clone(&compose_files); - let imports_for_send_result = Rc::clone(&compose_imports); + let session_for_send_result = Rc::clone(&compose_session); let document_for_send_result = Rc::clone(&compose_document); let editor_for_send_result = Rc::clone(&compose_editor); let contacts_for_send_result = Rc::clone(&compose_contacts); @@ -6249,7 +6259,7 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> clear_compose( &app, &files_for_send_result, - &imports_for_send_result, + &session_for_send_result, &document_for_send_result, &editor_for_send_result, &contacts_for_send_result, From 2f759a0488053164528d08a5a6d03fb516e9bea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Sun, 18 Oct 2026 03:36:22 +0200 Subject: [PATCH 04/11] fix: run backup and snapshot exports on the runtime --- src/data_controller.rs | 258 +++++++++++++++++++++++++---------------- 1 file changed, 158 insertions(+), 100 deletions(-) diff --git a/src/data_controller.rs b/src/data_controller.rs index 75fd6aab..eaed179c 100644 --- a/src/data_controller.rs +++ b/src/data_controller.rs @@ -12,7 +12,7 @@ use flectar_mail_core::models::{ normalized_workspace_list_pane_width, }; use serde::{Deserialize, Serialize}; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; const MAX_ACCOUNT_BACKUP_BYTES: u64 = 1024 * 1024; const STORAGE_CATEGORY_COUNT: usize = 5; @@ -657,101 +657,86 @@ fn import_account_backup_document( } } -// --- staging helpers -------------------------------------------------------- - -/// A provider export reads from an app-private staging file; the host copies -/// it to whatever destination its picker offers. +/// Hands generated bytes to the document provider through a private staging +/// file; the host copies it to the destination its picker offers. async fn export_document_bytes( documents: &std::sync::Arc, - staging_dir: PathBuf, + staging_root: PathBuf, bytes: Vec, name: String, ) -> Result { - let staging = staging_dir.join(&name); - let result = async { - tokio::fs::create_dir_all(&staging_dir) - .await - .map_err(|error| format!("creating {}: {error}", staging_dir.display()))?; - tokio::fs::write(&staging, bytes) - .await - .map_err(|error| format!("writing {}: {error}", staging.display()))?; - documents.export(staging.clone(), name).await - } - .await; - let _ = tokio::fs::remove_file(&staging).await; - result + tokio::fs::create_dir_all(&staging_root) + .await + .map_err(|error| format!("creating {}: {error}", staging_root.display()))?; + let staging = tempfile::tempdir_in(&staging_root) + .map_err(|error| format!("creating staging in {}: {error}", staging_root.display()))?; + let path = staging.path().join(&name); + tokio::fs::write(&path, bytes) + .await + .map_err(|error| format!("writing {}: {error}", path.display()))?; + documents.export(path, name).await } -/// Database snapshot staging must stay outside the application data and cache -/// roots: Core refuses to write a snapshot inside them. -fn snapshot_staging_root(paths: &Paths) -> PathBuf { - let temporary = std::env::temp_dir(); - if temporary.starts_with(&paths.data_dir) || temporary.starts_with(&paths.cache_dir) { - // Android points TMPDIR at the app cache; stage beside the data root. - return paths - .data_dir - .parent() - .map(|parent| parent.join("snapshot-staging")) - .unwrap_or_else(|| temporary.join("flectar-mail-snapshot")); - } - temporary.join("flectar-mail-snapshot") +#[cfg(desktop)] +fn pick_database_snapshot_path(title: String) -> Option { + rfd::FileDialog::new() + .set_title(title) + .pick_folder() + .map(|directory| { + directory.join(format!( + "flectar-mail-snapshot-{}", + Local::now().format("%Y-%m-%d-%H%M%S") + )) + }) } -/// Creates the verified snapshot in staging, archives it as one file and hands -/// it to the document provider (mobile hosts export files, not folders). +/// Mobile hosts export single files from their app-private storage, so the +/// verified snapshot is staged beside the data directory (Core refuses +/// destinations inside the data and cache roots) and archived as one zip. +#[cfg(mobile)] async fn export_database_snapshot( core: &CoreMailSource, - documents: std::sync::Arc, - staging_root: PathBuf, + documents: &std::sync::Arc, + paths: &Paths, ) -> Result, String> { - // Unique per export (pid + sequence): the staging root may live in a - // shared temporary directory, so a predictable name could be pre-created. - static SEQUENCE: AtomicU64 = AtomicU64::new(0); - let sequence = SEQUENCE.fetch_add(1, Ordering::Relaxed); - let unique = format!( - "flectar-mail-snapshot-{}-{sequence}-{}", - std::process::id(), - Local::now().format("%Y-%m-%d-%H%M%S") - ); - let name = format!("{unique}.zip"); - let directory = staging_root.join(&unique); - let archive = staging_root.join(&name); + let root = paths + .data_dir + .parent() + .ok_or("the data directory has no parent for snapshot staging")? + .join("snapshot-staging"); + // The root only ever holds this export, so clearing it also removes what + // an interrupted export left behind. + let _ = tokio::fs::remove_dir_all(&root).await; let result = async { - tokio::fs::create_dir_all(&staging_root) + tokio::fs::create_dir_all(&root) .await - .map_err(|error| format!("creating {}: {error}", staging_root.display()))?; - // The snapshot is private mailbox data; keep it unreadable to other - // users of a shared temporary directory. - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let _ = std::fs::set_permissions( - &staging_root, - std::fs::Permissions::from_mode(0o700), - ); - } - let _ = tokio::fs::remove_dir_all(&directory).await; + .map_err(|error| format!("creating {}: {error}", root.display()))?; + let name = format!( + "flectar-mail-snapshot-{}", + Local::now().format("%Y-%m-%d-%H%M%S") + ); + let directory = root.join(&name); core.create_database_snapshot(directory.clone()) .await .map_err(|error| error.to_string())?; - let directory_for_archive = directory.clone(); + let name = format!("{name}.zip"); + let archive = root.join(&name); let archive_for_zip = archive.clone(); - tokio::task::spawn_blocking(move || { - archive_snapshot_directory(&directory_for_archive, &archive_for_zip) - }) - .await - .map_err(|error| error.to_string())??; - let exported = documents.export(archive.clone(), name.clone()).await?; + tokio::task::spawn_blocking(move || archive_snapshot_directory(&directory, &archive_for_zip)) + .await + .map_err(|error| error.to_string())??; + let exported = documents.export(archive, name.clone()).await?; Ok(exported.then_some(name)) } .await; - let _ = tokio::fs::remove_dir_all(&directory).await; - let _ = tokio::fs::remove_file(&archive).await; + let _ = tokio::fs::remove_dir_all(&root).await; result } -/// The zip crate runs without compression features: snapshots are dense -/// SQLite stores plus already-compressed attachments. +/// Archives a snapshot directory without compression: the stores are dense +/// SQLite files plus already-compressed attachments. Symlinks are skipped so +/// nothing outside the snapshot enters the archive. +#[cfg(any(mobile, test))] fn archive_snapshot_directory( directory: &std::path::Path, archive: &std::path::Path, @@ -760,7 +745,6 @@ fn archive_snapshot_directory( zip: &mut zip::ZipWriter, root: &std::path::Path, directory: &std::path::Path, - options: zip::write::SimpleFileOptions, ) -> Result<(), String> { let mut entries = std::fs::read_dir(directory) .map_err(|error| format!("reading {}: {error}", directory.display()))? @@ -769,11 +753,8 @@ fn archive_snapshot_directory( entries.sort_by_key(|entry| entry.file_name()); for entry in entries { let path = entry.path(); - if entry - .file_type() - .map(|kind| kind.is_symlink()) - .unwrap_or(true) - { + let kind = entry.file_type().map_err(|error| error.to_string())?; + if kind.is_symlink() { continue; } let name = path @@ -781,15 +762,19 @@ fn archive_snapshot_directory( .map_err(|error| error.to_string())? .to_string_lossy() .replace('\\', "/"); - if path.is_dir() { + let options = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Stored); + if kind.is_dir() { zip.add_directory(format!("{name}/"), options) .map_err(|error| error.to_string())?; - append(zip, root, &path, options)?; + append(zip, root, &path)?; } else { - zip.start_file(name, options) - .map_err(|error| error.to_string())?; let mut source = std::fs::File::open(&path) .map_err(|error| format!("reading {}: {error}", path.display()))?; + let size = source.metadata().map_err(|error| error.to_string())?.len(); + // Stores past 4 GiB need ZIP64 entries. + zip.start_file(name, options.large_file(size >= u64::from(u32::MAX))) + .map_err(|error| error.to_string())?; std::io::copy(&mut source, zip).map_err(|error| error.to_string())?; } } @@ -798,9 +783,7 @@ fn archive_snapshot_directory( let file = std::fs::File::create(archive) .map_err(|error| format!("creating {}: {error}", archive.display()))?; let mut zip = zip::ZipWriter::new(file); - let options = zip::write::SimpleFileOptions::default() - .compression_method(zip::CompressionMethod::Stored); - append(&mut zip, directory, directory, options)?; + append(&mut zip, directory, directory)?; zip.finish().map_err(|error| error.to_string())?; Ok(()) } @@ -991,10 +974,19 @@ pub(super) fn register_data_management_callbacks( Local::now().format("%Y-%m-%d") ); let documents = documents_for_backup.clone(); - let staging_dir = paths_for_backup.temp_dir(); + let staging_root = paths_for_backup.temp_dir(); + // Providers rely on Tokio timers and file I/O, so the export runs on + // the runtime and the result is reported from the UI thread. + let export = runtime_for_backup.spawn(async move { + export_document_bytes(&documents, staging_root, bytes, name).await + }); + let abort_export = export.abort_handle(); let app_weak = app.as_weak(); let spawned = slint::spawn_local(async move { - let result = export_document_bytes(&documents, staging_dir, bytes, name).await; + let result = match export.await { + Ok(result) => result, + Err(error) => Err(error.to_string()), + }; let Some(app) = app_weak.upgrade() else { return; }; @@ -1009,6 +1001,7 @@ pub(super) fn register_data_management_callbacks( } }); if spawned.is_err() { + abort_export.abort(); app.set_sync_status(UiMessage::plain( "The platform host does not provide document export.", )); @@ -1019,8 +1012,11 @@ pub(super) fn register_data_management_callbacks( let state_for_database_snapshot = Rc::clone(state); let runtime_for_database_snapshot = Rc::clone(runtime); let updates_for_database_snapshot = ui_task_tx.clone(); + #[cfg(mobile)] let documents_for_database_snapshot = documents.clone(); + #[cfg(mobile)] let paths_for_database_snapshot = paths.clone(); + let database_snapshot_running = Arc::new(AtomicBool::new(false)); app.on_export_database_snapshot(move || { let Some(app) = app_weak.upgrade() else { return; @@ -1031,21 +1027,48 @@ pub(super) fn register_data_management_callbacks( )); return; }; + if database_snapshot_running.load(Ordering::Acquire) { + app.set_sync_status(UiMessage::plain("Creating verified database snapshot…")); + return; + } + #[cfg(desktop)] + let Some(path) = pick_database_snapshot_path( + translated( + &app, + &UiMessage::plain("Choose where to export the database snapshot"), + ) + .to_string(), + ) else { + return; + }; + + database_snapshot_running.store(true, Ordering::Release); app.set_sync_status(UiMessage::plain("Creating verified database snapshot…")); let updates = updates_for_database_snapshot.clone(); + #[cfg(mobile)] let documents = documents_for_database_snapshot.clone(); - let staging_root = snapshot_staging_root(&paths_for_database_snapshot); + #[cfg(mobile)] + let paths = paths_for_database_snapshot.clone(); + let running = Arc::clone(&database_snapshot_running); runtime_for_database_snapshot.spawn(async move { - let message = match export_database_snapshot(&core, documents, staging_root).await { - Ok(Some(name)) => { - UiMessage::detail("Database snapshot exported to {}.", name) + #[cfg(desktop)] + let message = { + let display_path = path.display().to_string(); + match core.create_database_snapshot(path).await { + Ok(_) => UiMessage::detail("Database snapshot exported to {}.", display_path), + Err(error) => { + UiMessage::detail("Could not export database snapshot: {}", error) + } } + }; + #[cfg(mobile)] + let message = match export_database_snapshot(&core, &documents, &paths).await { + Ok(Some(name)) => UiMessage::detail("Database snapshot exported to {}.", name), // Cancelled at the picker: return the status line to idle. Ok(None) => UiMessage::plain("Local data ready."), - Err(error) => { - UiMessage::detail("Could not export database snapshot: {}", error) - } + Err(error) => UiMessage::detail("Could not export database snapshot: {}", error), }; + running.store(false, Ordering::Release); let _ = updates .send(UiTaskUpdate { message, @@ -1077,15 +1100,19 @@ pub(super) fn register_data_management_callbacks( )); return; }; - let documents = documents_for_backup_import.clone(); + // The picker runs on the runtime (providers rely on Tokio timers); + // the import keeps its synchronous shape on the UI thread so the + // existing state updates stay on the main loop. + let request = runtime_for_backup_import.spawn(documents_for_backup_import.import()); + let abort_request = request.abort_handle(); let app_weak = app.as_weak(); let state = Rc::clone(&state_for_backup_import); let runtime = Rc::clone(&runtime_for_backup_import); - // The provider handshake runs on the UI thread's executor; the import - // keeps its synchronous shape so the existing state updates stay on - // the main loop. let spawned = slint::spawn_local(async move { - let imported = documents.import().await; + let imported = match request.await { + Ok(result) => result, + Err(error) => Err(error.to_string()), + }; let Some(app) = app_weak.upgrade() else { return; }; @@ -1103,11 +1130,11 @@ pub(super) fn register_data_management_callbacks( import_account_backup_document(&app, document.path(), &core, &state, &runtime); }); if spawned.is_err() { + abort_request.abort(); app.set_sync_status(UiMessage::plain( "The platform host does not provide document import.", )); } - }); let (data_reset_raw_tx, data_reset_rx) = bounded_ui_channel::>(); @@ -1457,4 +1484,35 @@ mod tests { }); assert!(portable_account_colors(&outside, &emails).is_err()); } + + #[test] + fn snapshot_archive_keeps_the_tree_and_skips_symlinks() { + let staging = tempfile::tempdir().unwrap(); + let snapshot = staging.path().join("snapshot"); + std::fs::create_dir_all(snapshot.join("file_transfers")).unwrap(); + std::fs::write(snapshot.join("mail.sqlite3"), b"mail").unwrap(); + std::fs::write(snapshot.join("file_transfers/part"), b"part").unwrap(); + let outside = staging.path().join("outside"); + std::fs::write(&outside, b"private").unwrap(); + #[cfg(unix)] + std::os::unix::fs::symlink(&outside, snapshot.join("link")).unwrap(); + let archive = staging.path().join("snapshot.zip"); + + archive_snapshot_directory(&snapshot, &archive).unwrap(); + + let mut zip = zip::ZipArchive::new(std::fs::File::open(&archive).unwrap()).unwrap(); + let mut names = zip + .file_names() + .map(|name| name.unwrap().into_owned()) + .collect::>(); + names.sort(); + assert_eq!( + names, + ["file_transfers/", "file_transfers/part", "mail.sqlite3"] + ); + let mut contents = String::new(); + std::io::Read::read_to_string(&mut zip.by_name("mail.sqlite3").unwrap(), &mut contents) + .unwrap(); + assert_eq!(contents, "mail"); + } } From 3ffe140c4d5e4d7ee23edd97b539941975ac974f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Sun, 18 Oct 2026 03:36:22 +0200 Subject: [PATCH 05/11] docs: note document export staging --- platform/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/platform/README.md b/platform/README.md index a9244ee3..da9e1deb 100644 --- a/platform/README.md +++ b/platform/README.md @@ -71,10 +71,18 @@ provides these crate-internal items, which shared code calls without a `cfg`: Hosts that launch through `src/desktop.rs` (desktop and iOS) also provide `platform::launch_context()`. +`DocumentProvider::export()` receives files from two app-private places: +generated files below the cache directory, and database snapshots, which are +staged as one zip in `snapshot-staging` beside the data directory because Core +refuses to write snapshots inside the data and cache roots. A host that +restricts export sources must accept both. + ### Known gaps - iOS has no Keychain-backed `CredentialStore` yet and uses `UnavailableCredentialStore`. +- The Android document bridge caps each transfer at 512 MiB, so it cannot + export database snapshots of larger mailboxes yet. ## Adding a platform From f185ffa812ac60d6f213a3a66cefc823a8bbaab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Sun, 18 Oct 2026 03:36:26 +0200 Subject: [PATCH 06/11] style: format data_controller --- src/data_controller.rs | 62 ++++++++++++++---------------------------- 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/src/data_controller.rs b/src/data_controller.rs index eaed179c..3b985630 100644 --- a/src/data_controller.rs +++ b/src/data_controller.rs @@ -341,7 +341,6 @@ fn read_account_backup(path: &std::path::Path) -> Result, String> { Ok(bytes) } - /// Applies an account backup the host already imported. The caller keeps the /// imported document alive until this returns. fn import_account_backup_document( @@ -436,12 +435,9 @@ fn import_account_backup_document( .get("workspaceListPaneWidth") .and_then(|value| value.as_i64()) { - settings.workspace_list_pane_width = - normalized_workspace_list_pane_width(width); + settings.workspace_list_pane_width = normalized_workspace_list_pane_width(width); } - if let Some(language) = preferences - .get("language") - .and_then(|value| value.as_str()) + if let Some(language) = preferences.get("language").and_then(|value| value.as_str()) && language.len() <= 32 { settings.language = language.to_owned(); @@ -529,8 +525,7 @@ fn import_account_backup_document( } } - let imported = runtime - .block_on(core.import_account_configs(configs))?; + let imported = runtime.block_on(core.import_account_configs(configs))?; runtime.block_on(core.save_settings(settings.clone()))?; let accounts = runtime.block_on(core.load_accounts())?; let configs = runtime.block_on(core.load_account_configs())?; @@ -541,22 +536,19 @@ fn import_account_backup_document( if has_portable_colors { for email in &imported_emails { if let Some(account_id) = account_ids.get(email) { - runtime - .block_on(core.set_account_color(*account_id, None))?; + runtime.block_on(core.set_account_color(*account_id, None))?; } } for (email, color) in portable_colors { if let Some(account_id) = account_ids.get(&email) { - runtime - .block_on(core.set_account_color(*account_id, Some(color)))?; + runtime.block_on(core.set_account_color(*account_id, Some(color)))?; } } } if has_portable_profiles { for email in &imported_emails { if let Some(account_id) = account_ids.get(email) { - runtime - .block_on(core.assign_account_profile(*account_id, None))?; + runtime.block_on(core.assign_account_profile(*account_id, None))?; } } } @@ -574,10 +566,9 @@ fn import_account_backup_document( ))?; for email in profile.account_emails { if let Some(account_id) = account_ids.get(&email.trim().to_ascii_lowercase()) { - runtime.block_on(core.assign_account_profile( - *account_id, - Some(saved.id.clone()), - ))?; + runtime.block_on( + core.assign_account_profile(*account_id, Some(saved.id.clone())), + )?; } } } @@ -591,13 +582,7 @@ fn import_account_backup_document( apply_language(app, &settings.language); state.borrow_mut().account_presentation = AccountPresentationSettings::from_settings(&settings); - update_connected_accounts( - app, - state, - runtime, - accounts, - configs, - ); + update_connected_accounts(app, state, runtime, accounts, configs); refresh_rows_only(app, state, runtime); app.set_theme_mode( match settings.theme.as_str() { @@ -617,10 +602,9 @@ fn import_account_backup_document( crate::window_chrome::WindowFrame::parse(&settings.window_frame), ); app.set_workspace_layout(settings.workspace_layout.clone().into()); - app.set_workspace_list_pane_width( - normalized_workspace_list_pane_width(settings.workspace_list_pane_width) - as f32, - ); + app.set_workspace_list_pane_width(normalized_workspace_list_pane_width( + settings.workspace_list_pane_width, + ) as f32); app.set_notifications_enabled(settings.notifications_enabled); app.set_notification_sound_enabled(settings.sound_enabled); app.set_notification_scope(settings.notification_scope.clone().into()); @@ -630,8 +614,7 @@ fn import_account_backup_document( 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); - state.borrow_mut().mark_read_on_open = - settings.mark_read_on_open; + state.borrow_mut().mark_read_on_open = settings.mark_read_on_open; app.set_remote_images_enabled( settings.load_remote_images && cfg!(feature = "remote-content"), ); @@ -651,9 +634,7 @@ fn import_account_backup_document( ) }); } - Err(error) => { - app.set_sync_status(UiMessage::detail("Could not import backup: {}", error)) - } + Err(error) => app.set_sync_status(UiMessage::detail("Could not import backup: {}", error)), } } @@ -722,9 +703,11 @@ async fn export_database_snapshot( let name = format!("{name}.zip"); let archive = root.join(&name); let archive_for_zip = archive.clone(); - tokio::task::spawn_blocking(move || archive_snapshot_directory(&directory, &archive_for_zip)) - .await - .map_err(|error| error.to_string())??; + tokio::task::spawn_blocking(move || { + archive_snapshot_directory(&directory, &archive_for_zip) + }) + .await + .map_err(|error| error.to_string())??; let exported = documents.export(archive, name.clone()).await?; Ok(exported.then_some(name)) } @@ -1120,10 +1103,7 @@ pub(super) fn register_data_management_callbacks( Ok(Some(document)) => document, Ok(None) => return, Err(error) => { - app.set_sync_status(UiMessage::detail( - "Could not import backup: {}", - error, - )); + app.set_sync_status(UiMessage::detail("Could not import backup: {}", error)); return; } }; From f1f9364b90857c407a6f62c07bb49278b8bd42bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Sun, 18 Oct 2026 03:40:02 +0200 Subject: [PATCH 07/11] fix: restore desktop dialog titles and filters --- src/data_controller.rs | 20 ++++++++-- src/documents.rs | 87 +++++++++++++++++++++++++++++++++--------- src/main.rs | 9 ++++- 3 files changed, 93 insertions(+), 23 deletions(-) diff --git a/src/data_controller.rs b/src/data_controller.rs index 3b985630..710b45a8 100644 --- a/src/data_controller.rs +++ b/src/data_controller.rs @@ -645,6 +645,7 @@ async fn export_document_bytes( staging_root: PathBuf, bytes: Vec, name: String, + dialog: documents::DocumentDialog, ) -> Result { tokio::fs::create_dir_all(&staging_root) .await @@ -655,7 +656,17 @@ async fn export_document_bytes( tokio::fs::write(&path, bytes) .await .map_err(|error| format!("writing {}: {error}", path.display()))?; - documents.export(path, name).await + documents.export_with(path, name, dialog).await +} + +fn backup_dialog(app: &AppWindow, title: UiMessage) -> documents::DocumentDialog { + documents::DocumentDialog { + title: translated(app, &title).to_string(), + filter: Some(( + translated(app, &UiMessage::plain("Flectar Mail backup")).to_string(), + &["json"], + )), + } } #[cfg(desktop)] @@ -958,10 +969,11 @@ pub(super) fn register_data_management_callbacks( ); let documents = documents_for_backup.clone(); let staging_root = paths_for_backup.temp_dir(); + let dialog = backup_dialog(&app, UiMessage::plain("Export Flectar Mail account backup")); // Providers rely on Tokio timers and file I/O, so the export runs on // the runtime and the result is reported from the UI thread. let export = runtime_for_backup.spawn(async move { - export_document_bytes(&documents, staging_root, bytes, name).await + export_document_bytes(&documents, staging_root, bytes, name, dialog).await }); let abort_export = export.abort_handle(); let app_weak = app.as_weak(); @@ -1086,7 +1098,9 @@ pub(super) fn register_data_management_callbacks( // The picker runs on the runtime (providers rely on Tokio timers); // the import keeps its synchronous shape on the UI thread so the // existing state updates stay on the main loop. - let request = runtime_for_backup_import.spawn(documents_for_backup_import.import()); + let dialog = backup_dialog(&app, UiMessage::plain("Import Flectar Mail account backup")); + let request = + runtime_for_backup_import.spawn(documents_for_backup_import.import_with(dialog)); let abort_request = request.abort_handle(); let app_weak = app.as_weak(); let state = Rc::clone(&state_for_backup_import); diff --git a/src/documents.rs b/src/documents.rs index 72a5baa6..f9b52400 100644 --- a/src/documents.rs +++ b/src/documents.rs @@ -39,15 +39,46 @@ impl Drop for DocumentRequestGuard { (std::mem::replace(&mut self.0, Box::new(|| {})))(); } } +/// How a desktop file dialog presents one request. Hosts whose system picker +/// has no title or file-type filter ignore it. +#[derive(Clone, Default)] +pub struct DocumentDialog { + pub title: String, + /// Filter name and the extensions it offers. + pub filter: Option<(String, &'static [&'static str])>, +} +impl DocumentDialog { + pub fn titled(title: impl Into) -> Self { + Self { + title: title.into(), + filter: None, + } + } +} pub trait DocumentProvider: Send + Sync { fn import(&self) -> DocumentFuture>; fn export(&self, path: PathBuf, name: String) -> DocumentFuture; + fn import_with(&self, _dialog: DocumentDialog) -> DocumentFuture> { + self.import() + } + fn export_with( + &self, + path: PathBuf, + name: String, + _dialog: DocumentDialog, + ) -> DocumentFuture { + self.export(path, name) + } /// Imports the documents for one user action (compose attachments). The /// default opens a single-document picker once; hosts with a multi-select /// picker override it. `limit` bounds how many copies a host makes; /// hosts that return source paths need not apply it. - fn import_many(&self, _limit: usize) -> DocumentFuture> { - let import = self.import(); + fn import_many( + &self, + _limit: usize, + dialog: DocumentDialog, + ) -> DocumentFuture> { + let import = self.import_with(dialog); Box::pin(async move { Ok(import.await?.into_iter().collect()) }) } } @@ -68,20 +99,40 @@ pub(crate) fn safe_name(name: &str) -> String { #[cfg(desktop)] struct DesktopDocuments; #[cfg(desktop)] +impl DocumentDialog { + fn file_dialog(&self) -> rfd::AsyncFileDialog { + let dialog = rfd::AsyncFileDialog::new().set_title(&self.title); + match &self.filter { + Some((name, extensions)) => dialog.add_filter(name, extensions), + None => dialog, + } + } +} +#[cfg(desktop)] impl DocumentProvider for DesktopDocuments { fn import(&self) -> DocumentFuture> { - Box::pin(async { - Ok(rfd::AsyncFileDialog::new() - .set_title("Upload file") + self.import_with(DocumentDialog::titled("Upload file")) + } + fn export(&self, path: PathBuf, name: String) -> DocumentFuture { + self.export_with(path, name, DocumentDialog::titled("Download file")) + } + fn import_with(&self, dialog: DocumentDialog) -> DocumentFuture> { + Box::pin(async move { + Ok(dialog + .file_dialog() .pick_file() .await .map(|f| ImportedDocument::source(f.path().to_owned()))) }) } - fn import_many(&self, _limit: usize) -> DocumentFuture> { - Box::pin(async { - Ok(rfd::AsyncFileDialog::new() - .set_title("Upload files") + fn import_many( + &self, + _limit: usize, + dialog: DocumentDialog, + ) -> DocumentFuture> { + Box::pin(async move { + Ok(dialog + .file_dialog() .pick_files() .await .unwrap_or_default() @@ -90,14 +141,14 @@ impl DocumentProvider for DesktopDocuments { .collect()) }) } - fn export(&self, path: PathBuf, name: String) -> DocumentFuture { + fn export_with( + &self, + path: PathBuf, + name: String, + dialog: DocumentDialog, + ) -> DocumentFuture { Box::pin(async move { - let Some(file) = rfd::AsyncFileDialog::new() - .set_title("Download file") - .set_file_name(name) - .save_file() - .await - else { + let Some(file) = dialog.file_dialog().set_file_name(name).save_file().await else { return Ok(false); }; flectar_mail_core::files::save_cached_file(&path, file.path()) @@ -210,7 +261,7 @@ mod tests { imports: AtomicUsize::new(0), cancel_after: usize::MAX, }); - let documents = provider.import_many(20).await.unwrap(); + let documents = provider.import_many(20, DocumentDialog::default()).await.unwrap(); assert_eq!(documents.len(), 1); assert_eq!(provider.imports.load(Ordering::SeqCst), 1); drop(documents); @@ -225,7 +276,7 @@ mod tests { imports: AtomicUsize::new(0), cancel_after: 0, }); - let documents = provider.import_many(20).await.unwrap(); + let documents = provider.import_many(20, DocumentDialog::default()).await.unwrap(); assert!(documents.is_empty()); assert_eq!(provider.imports.load(Ordering::SeqCst), 1); } diff --git a/src/main.rs b/src/main.rs index 8bc27af1..d5baa901 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5724,8 +5724,13 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> } // Providers rely on Tokio timers and file I/O, so the request runs on // the runtime and the composer awaits it on the UI thread. - let request = runtime_for_picker - .spawn(documents_for_picker.import_many(MAX_COMPOSE_ATTACHMENT_FILES)); + let dialog = documents::DocumentDialog::titled(translated( + &app, + &UiMessage::plain("Attach files to this email"), + )); + let request = runtime_for_picker.spawn( + documents_for_picker.import_many(MAX_COMPOSE_ATTACHMENT_FILES, dialog), + ); let abort_request = request.abort_handle(); let app_weak = app.as_weak(); let files = Rc::clone(&files_for_picker); From decaaa0900ed27b28eb8f8b3d0473f9048ba5306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Sun, 18 Oct 2026 03:46:11 +0200 Subject: [PATCH 08/11] fix: guard document exports and cap mobile snapshots --- platform/README.md | 5 +- src/data_controller.rs | 166 ++++++++++++++++++++++++++++++++--------- 2 files changed, 135 insertions(+), 36 deletions(-) diff --git a/platform/README.md b/platform/README.md index da9e1deb..5879769e 100644 --- a/platform/README.md +++ b/platform/README.md @@ -81,8 +81,9 @@ restricts export sources must accept both. - iOS has no Keychain-backed `CredentialStore` yet and uses `UnavailableCredentialStore`. -- The Android document bridge caps each transfer at 512 MiB, so it cannot - export database snapshots of larger mailboxes yet. +- Mobile document transfers are capped at 512 MiB, so Android and iOS cannot + export database snapshots of larger mailboxes yet. The export fails before + the picker opens. ## Adding a platform diff --git a/src/data_controller.rs b/src/data_controller.rs index 710b45a8..f1e78278 100644 --- a/src/data_controller.rs +++ b/src/data_controller.rs @@ -685,17 +685,24 @@ fn pick_database_snapshot_path(title: String) -> Option { /// Mobile hosts export single files from their app-private storage, so the /// verified snapshot is staged beside the data directory (Core refuses /// destinations inside the data and cache roots) and archived as one zip. +#[cfg(mobile)] +fn snapshot_staging_root(paths: &Paths) -> Result { + Ok(paths + .data_dir + .parent() + .ok_or("the data directory has no parent for snapshot staging")? + .join("snapshot-staging")) +} + #[cfg(mobile)] async fn export_database_snapshot( core: &CoreMailSource, documents: &std::sync::Arc, paths: &Paths, + staging: &tokio::sync::Mutex<()>, ) -> Result, String> { - let root = paths - .data_dir - .parent() - .ok_or("the data directory has no parent for snapshot staging")? - .join("snapshot-staging"); + let _staging = staging.lock().await; + let root = snapshot_staging_root(paths)?; // The root only ever holds this export, so clearing it also removes what // an interrupted export left behind. let _ = tokio::fs::remove_dir_all(&root).await; @@ -715,7 +722,11 @@ async fn export_database_snapshot( let archive = root.join(&name); let archive_for_zip = archive.clone(); tokio::task::spawn_blocking(move || { - archive_snapshot_directory(&directory, &archive_for_zip) + archive_snapshot_directory( + &directory, + &archive_for_zip, + flectar_mail_core::files::MAX_TRANSFER as u64, + ) }) .await .map_err(|error| error.to_string())??; @@ -729,59 +740,95 @@ async fn export_database_snapshot( /// Archives a snapshot directory without compression: the stores are dense /// SQLite files plus already-compressed attachments. Symlinks are skipped so -/// nothing outside the snapshot enters the archive. +/// nothing outside the snapshot enters the archive. Hosts transfer at most +/// `limit` bytes, so a larger snapshot fails before the archive is written. #[cfg(any(mobile, test))] fn archive_snapshot_directory( directory: &std::path::Path, archive: &std::path::Path, + limit: u64, ) -> Result<(), String> { - fn append( - zip: &mut zip::ZipWriter, + /// Archive name, source path and, for files, the size. + type Entry = (String, PathBuf, Option); + fn collect( root: &std::path::Path, directory: &std::path::Path, + entries: &mut Vec, ) -> Result<(), String> { - let mut entries = std::fs::read_dir(directory) + let mut children = std::fs::read_dir(directory) .map_err(|error| format!("reading {}: {error}", directory.display()))? .collect::, _>>() .map_err(|error| error.to_string())?; - entries.sort_by_key(|entry| entry.file_name()); - for entry in entries { + children.sort_by_key(|entry| entry.file_name()); + for entry in children { let path = entry.path(); let kind = entry.file_type().map_err(|error| error.to_string())?; - if kind.is_symlink() { - continue; - } let name = path .strip_prefix(root) .map_err(|error| error.to_string())? .to_string_lossy() .replace('\\', "/"); - let options = zip::write::SimpleFileOptions::default() - .compression_method(zip::CompressionMethod::Stored); if kind.is_dir() { - zip.add_directory(format!("{name}/"), options) - .map_err(|error| error.to_string())?; - append(zip, root, &path)?; - } else { - let mut source = std::fs::File::open(&path) - .map_err(|error| format!("reading {}: {error}", path.display()))?; - let size = source.metadata().map_err(|error| error.to_string())?.len(); - // Stores past 4 GiB need ZIP64 entries. - zip.start_file(name, options.large_file(size >= u64::from(u32::MAX))) - .map_err(|error| error.to_string())?; - std::io::copy(&mut source, zip).map_err(|error| error.to_string())?; + entries.push((format!("{name}/"), path.clone(), None)); + collect(root, &path, entries)?; + } else if kind.is_file() { + let size = entry.metadata().map_err(|error| error.to_string())?.len(); + entries.push((name, path, Some(size))); } } Ok(()) } + let too_large = |size| { + format!( + "the snapshot is {}, more than the {} this device can export", + format_storage_size(size), + format_storage_size(limit) + ) + }; + let mut entries = Vec::new(); + collect(directory, directory, &mut entries)?; + let size = entries.iter().filter_map(|(_, _, size)| *size).sum::(); + if size > limit { + return Err(too_large(size)); + } let file = std::fs::File::create(archive) .map_err(|error| format!("creating {}: {error}", archive.display()))?; let mut zip = zip::ZipWriter::new(file); - append(&mut zip, directory, directory)?; - zip.finish().map_err(|error| error.to_string())?; + let options = + zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored); + for (name, path, size) in entries { + let Some(size) = size else { + zip.add_directory(name, options) + .map_err(|error| error.to_string())?; + continue; + }; + let mut source = std::fs::File::open(&path) + .map_err(|error| format!("reading {}: {error}", path.display()))?; + // Stores past 4 GiB need ZIP64 entries. + zip.start_file(name, options.large_file(size >= u64::from(u32::MAX))) + .map_err(|error| error.to_string())?; + std::io::copy(&mut source, &mut zip).map_err(|error| error.to_string())?; + } + let size = zip + .finish() + .and_then(|file| Ok(file.metadata()?.len())) + .map_err(|error| error.to_string())?; + // Entry headers can still push the archive past the limit. + if size > limit { + return Err(too_large(size)); + } Ok(()) } +/// Clears the in-progress flag when the snapshot task ends, even by panic. +struct SnapshotRunning(Arc); + +impl Drop for SnapshotRunning { + fn drop(&mut self) { + self.0.store(false, Ordering::Release); + } +} + #[allow(clippy::too_many_arguments)] pub(super) fn register_data_management_callbacks( app: &AppWindow, @@ -862,10 +909,16 @@ pub(super) fn register_data_management_callbacks( let runtime_for_backup = Rc::clone(runtime); let documents_for_backup = documents.clone(); let paths_for_backup = paths.clone(); + let exporting_backup = Rc::new(Cell::new(false)); app.on_export_account_backup(move || { let Some(app) = app_weak.upgrade() else { return; }; + // The dialog does not block the window; a second click while it is + // open would start a second export. + if exporting_backup.get() { + return; + } let (core, configs) = { let state = state_for_backup.borrow(); (state.core.clone(), state.account_configs.clone()) @@ -977,11 +1030,14 @@ pub(super) fn register_data_management_callbacks( }); let abort_export = export.abort_handle(); let app_weak = app.as_weak(); + let exporting = Rc::clone(&exporting_backup); + exporting.set(true); let spawned = slint::spawn_local(async move { let result = match export.await { Ok(result) => result, Err(error) => Err(error.to_string()), }; + exporting.set(false); let Some(app) = app_weak.upgrade() else { return; }; @@ -997,6 +1053,7 @@ pub(super) fn register_data_management_callbacks( }); if spawned.is_err() { abort_export.abort(); + exporting_backup.set(false); app.set_sync_status(UiMessage::plain( "The platform host does not provide document export.", )); @@ -1011,6 +1068,18 @@ pub(super) fn register_data_management_callbacks( let documents_for_database_snapshot = documents.clone(); #[cfg(mobile)] let paths_for_database_snapshot = paths.clone(); + #[cfg(mobile)] + let snapshot_staging = Arc::new(tokio::sync::Mutex::new(())); + // A killed export leaves a full copy of the stores in staging; reclaim it + // now instead of at the next export. + #[cfg(mobile)] + if let Ok(root) = snapshot_staging_root(paths) { + let staging = Arc::clone(&snapshot_staging); + runtime.spawn(async move { + let _staging = staging.lock().await; + let _ = tokio::fs::remove_dir_all(root).await; + }); + } let database_snapshot_running = Arc::new(AtomicBool::new(false)); app.on_export_database_snapshot(move || { let Some(app) = app_weak.upgrade() else { @@ -1044,7 +1113,9 @@ pub(super) fn register_data_management_callbacks( let documents = documents_for_database_snapshot.clone(); #[cfg(mobile)] let paths = paths_for_database_snapshot.clone(); - let running = Arc::clone(&database_snapshot_running); + #[cfg(mobile)] + let staging = Arc::clone(&snapshot_staging); + let running = SnapshotRunning(Arc::clone(&database_snapshot_running)); runtime_for_database_snapshot.spawn(async move { #[cfg(desktop)] let message = { @@ -1057,13 +1128,14 @@ pub(super) fn register_data_management_callbacks( } }; #[cfg(mobile)] - let message = match export_database_snapshot(&core, &documents, &paths).await { + let message = match export_database_snapshot(&core, &documents, &paths, &staging).await + { Ok(Some(name)) => UiMessage::detail("Database snapshot exported to {}.", name), // Cancelled at the picker: return the status line to idle. Ok(None) => UiMessage::plain("Local data ready."), Err(error) => UiMessage::detail("Could not export database snapshot: {}", error), }; - running.store(false, Ordering::Release); + drop(running); let _ = updates .send(UiTaskUpdate { message, @@ -1085,10 +1157,15 @@ pub(super) fn register_data_management_callbacks( let state_for_backup_import = Rc::clone(state); let runtime_for_backup_import = Rc::clone(runtime); let documents_for_backup_import = documents.clone(); + let importing_backup = Rc::new(Cell::new(false)); app.on_import_account_backup(move || { let Some(app) = app_weak.upgrade() else { return; }; + // A second click while the picker is open would apply two backups. + if importing_backup.get() { + return; + } let Some(core) = state_for_backup_import.borrow().core.clone() else { app.set_sync_status(UiMessage::plain( "Local data storage is unavailable. Retry startup.", @@ -1105,11 +1182,14 @@ pub(super) fn register_data_management_callbacks( let app_weak = app.as_weak(); let state = Rc::clone(&state_for_backup_import); let runtime = Rc::clone(&runtime_for_backup_import); + let importing = Rc::clone(&importing_backup); + importing.set(true); let spawned = slint::spawn_local(async move { let imported = match request.await { Ok(result) => result, Err(error) => Err(error.to_string()), }; + importing.set(false); let Some(app) = app_weak.upgrade() else { return; }; @@ -1125,6 +1205,7 @@ pub(super) fn register_data_management_callbacks( }); if spawned.is_err() { abort_request.abort(); + importing_backup.set(false); app.set_sync_status(UiMessage::plain( "The platform host does not provide document import.", )); @@ -1492,7 +1573,7 @@ mod tests { std::os::unix::fs::symlink(&outside, snapshot.join("link")).unwrap(); let archive = staging.path().join("snapshot.zip"); - archive_snapshot_directory(&snapshot, &archive).unwrap(); + archive_snapshot_directory(&snapshot, &archive, u64::MAX).unwrap(); let mut zip = zip::ZipArchive::new(std::fs::File::open(&archive).unwrap()).unwrap(); let mut names = zip @@ -1509,4 +1590,21 @@ mod tests { .unwrap(); assert_eq!(contents, "mail"); } + + #[test] + fn snapshot_archive_refuses_stores_over_the_transfer_limit() { + let staging = tempfile::tempdir().unwrap(); + let snapshot = staging.path().join("snapshot"); + std::fs::create_dir_all(&snapshot).unwrap(); + std::fs::write(snapshot.join("mail.sqlite3"), [0; 64]).unwrap(); + let archive = staging.path().join("snapshot.zip"); + + let error = archive_snapshot_directory(&snapshot, &archive, 63).unwrap_err(); + + assert!(error.contains("64 B"), "{error}"); + assert!(!archive.exists()); + // Headers alone push an archive of a store at the limit past it. + assert!(archive_snapshot_directory(&snapshot, &archive, 64).is_err()); + archive_snapshot_directory(&snapshot, &archive, 4096).unwrap(); + } } From adaca97c43054d12f7f70684ad8b2fd9bb6b1e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Sun, 18 Oct 2026 03:46:15 +0200 Subject: [PATCH 09/11] fix: report document picker errors --- lang/de/LC_MESSAGES/flectar-mail.po | 376 ++++++++++----------- lang/es/LC_MESSAGES/flectar-mail.po | 378 ++++++++++----------- lang/flectar-mail.pot | 408 +++++++++++------------ lang/fr/LC_MESSAGES/flectar-mail.po | 376 ++++++++++----------- lang/ru/LC_MESSAGES/flectar-mail.po | 378 ++++++++++----------- lang/tr/LC_MESSAGES/flectar-mail.po | 378 ++++++++++----------- lang/zh_Hans/LC_MESSAGES/flectar-mail.po | 378 ++++++++++----------- src/data_controller.rs | 12 +- src/main.rs | 12 +- ui/app.slint | 4 +- 10 files changed, 1320 insertions(+), 1380 deletions(-) diff --git a/lang/de/LC_MESSAGES/flectar-mail.po b/lang/de/LC_MESSAGES/flectar-mail.po index f1808c7a..c45ba670 100644 --- a/lang/de/LC_MESSAGES/flectar-mail.po +++ b/lang/de/LC_MESSAGES/flectar-mail.po @@ -186,7 +186,7 @@ msgstr "Gesprächsverlauf verfügbar" msgid "New address-book entry" msgstr "Neuer Adressbuch-Eintrag" -#: ui/app.slint:121 ui/app.slint:847 ui/components/composer-fields.slint:66 +#: ui/app.slint:121 ui/app.slint:845 ui/components/composer-fields.slint:66 #: ui/dialogs/compose-dialog.slint:37 msgid "No connected account" msgstr "Kein verknüpftes Konto" @@ -201,11 +201,11 @@ msgstr[1] "{n} Interaktionen" msgid "Last contacted {} {}, {}" msgstr "" -#: ui/app.slint:128 ui/app.slint:361 +#: ui/app.slint:128 ui/app.slint:359 msgid "Loading local data…" msgstr "Lade lokale Daten…" -#: ui/app.slint:129 ui/app.slint:362 +#: ui/app.slint:129 ui/app.slint:360 msgid "Local data ready." msgstr "Lokale Daten bereit." @@ -666,750 +666,742 @@ msgid "Choose where to export the database snapshot" msgstr "" #: ui/app.slint:289 -msgid "The platform host does not provide document export." -msgstr "" - -#: ui/app.slint:290 -msgid "The platform host does not provide document import." -msgstr "" - -#: ui/app.slint:291 msgid "Backup exported. Passwords and OAuth tokens were not included." msgstr "" -#: ui/app.slint:292 +#: ui/app.slint:290 msgid "Could not export backup: {}" msgstr "" -#: ui/app.slint:293 +#: ui/app.slint:291 msgid "Local data storage is unavailable. Retry startup." msgstr "" -#: ui/app.slint:294 +#: ui/app.slint:292 msgid "Creating verified database snapshot…" msgstr "" -#: ui/app.slint:295 +#: ui/app.slint:293 msgid "Backup imported. All {} account configurations already existed." msgstr "" -#: ui/app.slint:296 +#: ui/app.slint:294 msgid "" "Imported {} account configurations. Open Accounts and sign in to each one." msgstr "" -#: ui/app.slint:297 +#: ui/app.slint:295 msgid "Could not import backup: {}" msgstr "" -#: ui/app.slint:298 +#: ui/app.slint:296 msgid "All local Flectar Mail data was deleted." msgstr "" -#: ui/app.slint:299 +#: ui/app.slint:297 msgid "Could not delete all local data: {}" msgstr "" -#: ui/app.slint:300 +#: ui/app.slint:298 msgid "Deleting local accounts and cached data…" msgstr "" -#: ui/app.slint:305 +#: ui/app.slint:303 msgid "Background mail refresh failed: {}" msgstr "" -#: ui/app.slint:306 +#: ui/app.slint:304 msgid "No conversations in this folder" msgstr "" -#: ui/app.slint:307 +#: ui/app.slint:305 msgid "No conversations match this search" msgstr "" -#: ui/app.slint:308 +#: ui/app.slint:306 msgid "Mail core is ready for account synchronization and message actions." msgstr "" -#: ui/app.slint:309 +#: ui/app.slint:307 msgid "Large message rendered in compatibility mode." msgstr "" -#: ui/app.slint:310 +#: ui/app.slint:308 msgid "Software email render failed: {}" msgstr "" -#: ui/app.slint:311 +#: ui/app.slint:309 msgid "Rendered with software fallback after GPU error: {}" msgstr "" -#: ui/app.slint:312 +#: ui/app.slint:310 msgid "Blitz render failed (GPU: {}; software: {})" msgstr "" -#: ui/app.slint:317 +#: ui/app.slint:315 msgid "Add an email address, then save the contact." msgstr "" -#: ui/app.slint:318 +#: ui/app.slint:316 msgid "Enter a valid email address before saving." msgstr "" -#: ui/app.slint:319 +#: ui/app.slint:317 msgid "Could not save: contact storage is unavailable." msgstr "" -#: ui/app.slint:320 +#: ui/app.slint:318 msgid "Could not save contact: {}" msgstr "" -#: ui/app.slint:321 +#: ui/app.slint:319 msgid "Contact saved." msgstr "" -#: ui/app.slint:322 +#: ui/app.slint:320 msgid "Saved to contacts." msgstr "" -#: ui/app.slint:323 +#: ui/app.slint:321 msgid "Could not delete: contact storage is unavailable." msgstr "" -#: ui/app.slint:324 +#: ui/app.slint:322 msgid "Could not delete contact: {}" msgstr "" -#: ui/app.slint:325 +#: ui/app.slint:323 msgid "Contact deleted." msgstr "" -#: ui/app.slint:326 +#: ui/app.slint:324 msgid "Could not load contacts: {}" msgstr "" -#: ui/app.slint:327 +#: ui/app.slint:325 msgid "Local contact storage is still starting…" msgstr "" -#: ui/app.slint:328 +#: ui/app.slint:326 msgid "Loading contacts…" msgstr "" -#: ui/app.slint:333 ui/components/calendar-agenda.slint:233 +#: ui/app.slint:331 ui/components/calendar-agenda.slint:233 msgid "Local calendar" msgstr "Lokaler Kalender" -#: ui/app.slint:334 ui/components/calendar.slint:271 +#: ui/app.slint:332 ui/components/calendar.slint:271 #: ui/components/calendar.slint:319 ui/components/calendar.slint:434 msgid "Calendar" msgstr "" -#: ui/app.slint:335 +#: ui/app.slint:333 msgid "Could not load calendar: {}" msgstr "" -#: ui/app.slint:336 +#: ui/app.slint:334 msgid "Calendar shown and syncing." msgstr "" -#: ui/app.slint:337 +#: ui/app.slint:335 msgid "Calendar hidden and sync paused." msgstr "" -#: ui/app.slint:338 +#: ui/app.slint:336 msgid "Calendar update failed: {}" msgstr "" -#: ui/app.slint:339 +#: ui/app.slint:337 msgid "Only events created in Flectar Mail can be edited." msgstr "" -#: ui/app.slint:340 +#: ui/app.slint:338 msgid "Only events created in Flectar Mail can be deleted." msgstr "" -#: ui/app.slint:341 +#: ui/app.slint:339 msgid "Calendar storage is unavailable." msgstr "" -#: ui/app.slint:342 +#: ui/app.slint:340 msgid "Could not delete event: {}" msgstr "" -#: ui/app.slint:343 +#: ui/app.slint:341 msgid "Event deleted, but refresh failed: {}" msgstr "" -#: ui/app.slint:344 +#: ui/app.slint:342 msgid "Event deleted." msgstr "" -#: ui/app.slint:345 +#: ui/app.slint:343 msgid "Use YYYY-MM-DD and a 24-hour time such as 09:30." msgstr "" -#: ui/app.slint:346 +#: ui/app.slint:344 msgid "Event duration must be a number of minutes." msgstr "" -#: ui/app.slint:347 +#: ui/app.slint:345 msgid "Add a title and choose a duration from 15 to 1440 minutes." msgstr "" -#: ui/app.slint:348 +#: ui/app.slint:346 msgid "That local time does not exist in the current time zone." msgstr "" -#: ui/app.slint:349 +#: ui/app.slint:347 msgid "Could not save event: {}" msgstr "" -#: ui/app.slint:350 +#: ui/app.slint:348 msgid "Event saved, but refresh failed: {}" msgstr "" -#: ui/app.slint:351 +#: ui/app.slint:349 msgid "Event saved. Provider sync will run when enabled." msgstr "" -#: ui/app.slint:356 +#: ui/app.slint:354 msgid "Email resource render failed: {}" msgstr "" -#: ui/app.slint:357 +#: ui/app.slint:355 msgid "Message refresh failed: {}" msgstr "" -#: ui/app.slint:358 +#: ui/app.slint:356 msgid "Message load failed: {}" msgstr "" -#: ui/app.slint:359 +#: ui/app.slint:357 msgid "" "Your local mailbox could not be opened. No account operation was started. " "Retry, or copy the diagnostics for support." msgstr "" -#: ui/app.slint:360 +#: ui/app.slint:358 msgid "Local storage startup failed." msgstr "" -#: ui/app.slint:367 +#: ui/app.slint:365 msgid "Could not identify the dragged account." msgstr "" -#: ui/app.slint:368 +#: ui/app.slint:366 msgid "Account ordering requires local mail data." msgstr "" -#: ui/app.slint:369 +#: ui/app.slint:367 msgid "Could not find the account to reorder." msgstr "" -#: ui/app.slint:370 +#: ui/app.slint:368 msgid "Saving account order…" msgstr "" -#: ui/app.slint:371 +#: ui/app.slint:369 msgid "Sent copy settings require local mail data." msgstr "Die Einstellungen für gesendete Kopien erfordern lokale E-Mail-Daten." -#: ui/app.slint:372 +#: ui/app.slint:370 msgid "Updating Sent copy settings…" msgstr "Einstellungen für gesendete Kopien werden aktualisiert…" -#: ui/app.slint:373 +#: ui/app.slint:371 msgid "Sent copy settings updated." msgstr "Einstellungen für gesendete Kopien aktualisiert." -#: ui/app.slint:374 +#: ui/app.slint:372 msgid "Could not update Sent copy settings: {}" msgstr "" "Einstellungen für gesendete Kopien konnten nicht aktualisiert werden: {}" -#: ui/app.slint:375 +#: ui/app.slint:373 msgid "Mail history requires local mail data." msgstr "" -#: ui/app.slint:376 +#: ui/app.slint:374 msgid "Updating mail history…" msgstr "" -#: ui/app.slint:377 +#: ui/app.slint:375 msgid "Remote images are unavailable in this minimal build." msgstr "" -#: ui/app.slint:378 +#: ui/app.slint:376 msgid "Could not save remote-image setting: {}" msgstr "" -#: ui/app.slint:379 +#: ui/app.slint:377 msgid "Remote sender icons unavailable: {}" msgstr "" -#: ui/app.slint:380 +#: ui/app.slint:378 msgid "Remote images unavailable: {}" msgstr "" -#: ui/app.slint:381 +#: ui/app.slint:379 msgid "Remote body images and sender favicons enabled." msgstr "" -#: ui/app.slint:382 +#: ui/app.slint:380 msgid "Remote images blocked." msgstr "" -#: ui/app.slint:383 +#: ui/app.slint:381 msgid "Could not refresh message images: {}" msgstr "" -#: ui/app.slint:384 +#: ui/app.slint:382 msgid "Remote-image loading is disabled." msgstr "" -#: ui/app.slint:385 +#: ui/app.slint:383 msgid "Could not load message images: {}" msgstr "" -#: ui/app.slint:386 +#: ui/app.slint:384 msgid "Sync finished, refresh failed: {}" msgstr "" -#: ui/app.slint:387 +#: ui/app.slint:385 msgid "Sync complete." msgstr "" -#: ui/app.slint:388 +#: ui/app.slint:386 msgid "Sync failed: {}" msgstr "" -#: ui/app.slint:389 +#: ui/app.slint:387 msgid "Local mail data is unavailable; preview data is read-only." msgstr "" -#: ui/app.slint:390 +#: ui/app.slint:388 msgid "Synchronizing accounts…" msgstr "" -#: ui/app.slint:395 +#: ui/app.slint:393 msgid "Message action completed." msgstr "" -#: ui/app.slint:396 +#: ui/app.slint:394 msgid "" "Permanent deletion queued. Mail will disappear after the server confirms." msgstr "" -#: ui/app.slint:397 +#: ui/app.slint:395 msgid "Message action failed: {}" msgstr "" -#: ui/app.slint:398 +#: ui/app.slint:396 msgid "Message action failed: message is no longer available" msgstr "" -#: ui/app.slint:399 +#: ui/app.slint:397 msgid "Label added." msgstr "" -#: ui/app.slint:400 +#: ui/app.slint:398 msgid "Label removed." msgstr "" -#: ui/app.slint:401 +#: ui/app.slint:399 msgid "Label created and added." msgstr "" -#: ui/app.slint:402 +#: ui/app.slint:400 msgid "Label created." msgstr "" -#: ui/app.slint:403 +#: ui/app.slint:401 msgid "Label updated." msgstr "" -#: ui/app.slint:404 +#: ui/app.slint:402 msgid "Label deleted." msgstr "" -#: ui/app.slint:405 +#: ui/app.slint:403 msgid "Could not update label: {}" msgstr "" -#: ui/app.slint:406 +#: ui/app.slint:404 msgid "Could not save label: {}" msgstr "" -#: ui/app.slint:407 +#: ui/app.slint:405 msgid "Could not delete label: {}" msgstr "" -#: ui/app.slint:408 +#: ui/app.slint:406 msgid "Folder created." msgstr "" -#: ui/app.slint:409 +#: ui/app.slint:407 msgid "Folder renamed." msgstr "" -#: ui/app.slint:410 +#: ui/app.slint:408 msgid "Folder deleted." msgstr "" -#: ui/app.slint:411 +#: ui/app.slint:409 msgid "Could not create folder: {}" msgstr "" -#: ui/app.slint:412 +#: ui/app.slint:410 msgid "Could not rename folder: {}" msgstr "" -#: ui/app.slint:413 +#: ui/app.slint:411 msgid "Could not delete folder: {}" msgstr "" -#: ui/app.slint:414 +#: ui/app.slint:412 msgid "Could not close preview: {}" msgstr "" -#: ui/app.slint:415 +#: ui/app.slint:413 msgid "Opened the system print view." msgstr "" -#: ui/app.slint:416 +#: ui/app.slint:414 msgid "Could not print message: {}" msgstr "" -#: ui/app.slint:417 +#: ui/app.slint:415 msgid "Opened the message in your browser." msgstr "" -#: ui/app.slint:418 +#: ui/app.slint:416 msgid "Could not open message in browser: {}" msgstr "" -#: ui/app.slint:419 +#: ui/app.slint:417 msgid "Connect an account before replying or forwarding." msgstr "" -#: ui/app.slint:420 +#: ui/app.slint:418 msgid "Choose a message first." msgstr "" -#: ui/app.slint:421 +#: ui/app.slint:419 msgid "Only drafts can be reopened for editing." msgstr "" -#: ui/app.slint:422 +#: ui/app.slint:420 msgid "Could not open draft: {}" msgstr "" -#: ui/app.slint:423 +#: ui/app.slint:421 msgid "Could not open draft: invalid account identifier" msgstr "" -#: ui/app.slint:424 +#: ui/app.slint:422 msgid "Connected account" msgstr "" -#: ui/app.slint:425 +#: ui/app.slint:423 msgid "Could not open composer: {}" msgstr "" -#: ui/app.slint:426 +#: ui/app.slint:424 msgid "Could not open composer: invalid account identifier" msgstr "" -#: ui/app.slint:431 +#: ui/app.slint:429 msgid "Attach files to this email" msgstr "" -#: ui/app.slint:432 -msgid "The platform host does not provide a document picker." +#: ui/app.slint:430 +msgid "Could not add attachments: {}" msgstr "" -#: ui/app.slint:433 +#: ui/app.slint:431 msgid "Added 1 attachment." msgstr "" -#: ui/app.slint:434 +#: ui/app.slint:432 msgid "Added {} attachments." msgstr "" -#: ui/app.slint:435 +#: ui/app.slint:433 msgid "" "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB " "total limit)." msgstr "" -#: ui/app.slint:436 +#: ui/app.slint:434 msgid "Attachment removed." msgstr "" -#: ui/app.slint:437 +#: ui/app.slint:435 msgid "Choose the account this message should be sent from." msgstr "" -#: ui/app.slint:438 +#: ui/app.slint:436 msgid "Choose a verified sender identity for this message." msgstr "" -#: ui/app.slint:439 +#: ui/app.slint:437 msgid "Connect an account before saving or sending mail." msgstr "" -#: ui/app.slint:440 +#: ui/app.slint:438 msgid "Message queued for delivery (action {})." msgstr "" -#: ui/app.slint:441 +#: ui/app.slint:439 msgid "Sending message…" msgstr "" -#: ui/app.slint:442 +#: ui/app.slint:440 msgid "Message sent." msgstr "" -#: ui/app.slint:443 +#: ui/app.slint:441 msgid "Message was not sent: {} Your draft is preserved." msgstr "" -#: ui/app.slint:444 +#: ui/app.slint:442 msgid "Draft saved locally (#{})." msgstr "" -#: ui/app.slint:445 +#: ui/app.slint:443 msgid "Compose failed: {}" msgstr "" -#: ui/app.slint:446 +#: ui/app.slint:444 msgid "Connect an account before managing email templates." msgstr "" -#: ui/app.slint:447 +#: ui/app.slint:445 msgid "Could not load email templates: {}" msgstr "" -#: ui/app.slint:448 +#: ui/app.slint:446 msgid "Enter a template name." msgstr "" -#: ui/app.slint:449 +#: ui/app.slint:447 msgid "Add a subject or message to the template." msgstr "" -#: ui/app.slint:450 +#: ui/app.slint:448 msgid "Email template saved." msgstr "" -#: ui/app.slint:451 +#: ui/app.slint:449 msgid "Could not save email template: {}" msgstr "" -#: ui/app.slint:452 +#: ui/app.slint:450 msgid "Email template deleted." msgstr "" -#: ui/app.slint:453 +#: ui/app.slint:451 msgid "Could not delete email template: {}" msgstr "" -#: ui/app.slint:454 +#: ui/app.slint:452 msgid "Email template is no longer available." msgstr "" -#: ui/app.slint:455 +#: ui/app.slint:453 msgid "Email template inserted." msgstr "" -#: ui/app.slint:456 +#: ui/app.slint:454 msgid "Email template inserted, but its usage could not be saved: {}" msgstr "" -#: ui/app.slint:457 +#: ui/app.slint:455 msgid "IMAP port must be between 1 and 65535." msgstr "" -#: ui/app.slint:458 +#: ui/app.slint:456 msgid "SMTP port must be between 1 and 65535." msgstr "" -#: ui/app.slint:459 +#: ui/app.slint:457 msgid "Local mail data is unavailable. Retry startup." msgstr "" -#: ui/app.slint:460 +#: ui/app.slint:458 msgid "Discovering JMAP and checking capabilities…" msgstr "" -#: ui/app.slint:461 +#: ui/app.slint:459 msgid "Certificate imported. Connect to verify the server." msgstr "" -#: ui/app.slint:462 +#: ui/app.slint:460 msgid "Could not load account preferences: {}" msgstr "" -#: ui/app.slint:463 +#: ui/app.slint:461 msgid "" "Your edited signature was preserved. Choose No signature to keep it as " "message text before inserting another." msgstr "" -#: ui/app.slint:464 +#: ui/app.slint:462 msgid "Certificate import failed: {}" msgstr "" -#: ui/app.slint:465 +#: ui/app.slint:463 msgid "Paste the PEM certificate into the certificate field." msgstr "" -#: ui/app.slint:466 +#: ui/app.slint:464 msgid "Testing IMAP and SMTP connections…" msgstr "" -#: ui/app.slint:471 +#: ui/app.slint:469 msgid "" "Another browser authorization is already in progress; finish it or choose " "Cancel." msgstr "" -#: ui/app.slint:472 ui/dialogs/settings-dialog.slint:1643 +#: ui/app.slint:470 ui/dialogs/settings-dialog.slint:1643 #: ui/dialogs/settings-dialog.slint:1686 ui/views/onboarding-view.slint:152 msgid "Add app keys in Sign-in settings to enable this provider." msgstr "" -#: ui/app.slint:473 +#: ui/app.slint:471 msgid "Choose Gmail or Outlook OAuth." msgstr "" -#: ui/app.slint:474 +#: ui/app.slint:472 msgid "Complete sign-in in your browser, or choose Cancel here." msgstr "" -#: ui/app.slint:475 +#: ui/app.slint:473 msgid "Cancelling browser sign-in…" msgstr "" -#: ui/app.slint:476 +#: ui/app.slint:474 msgid "Finishing browser sign-in…" msgstr "" -#: ui/app.slint:477 +#: ui/app.slint:475 msgid "Could not use that browser address: {}" msgstr "" -#: ui/app.slint:478 +#: ui/app.slint:476 msgid "Calendar core is unavailable." msgstr "" -#: ui/app.slint:479 +#: ui/app.slint:477 msgid "Account no longer exists." msgstr "" -#: ui/app.slint:480 +#: ui/app.slint:478 msgid "Authorize calendar access in your browser." msgstr "" -#: ui/app.slint:481 +#: ui/app.slint:479 msgid "OAuth app keys saved." msgstr "" -#: ui/app.slint:482 +#: ui/app.slint:480 msgid "{} OAuth settings saved." msgstr "" -#: ui/app.slint:483 +#: ui/app.slint:481 msgid "OAuth settings failed: {}" msgstr "" -#: ui/app.slint:484 +#: ui/app.slint:482 msgid "Mail refresh failed: {}" msgstr "" -#: ui/app.slint:485 +#: ui/app.slint:483 msgid "Mail filter failed: {}" msgstr "" -#: ui/app.slint:486 +#: ui/app.slint:484 msgid "Mail pagination failed: {}" msgstr "" -#: ui/app.slint:487 +#: ui/app.slint:485 msgid "Could not mark message as read: {}" msgstr "" -#: ui/app.slint:488 ui/views/mail-reading-pane.slint:223 +#: ui/app.slint:486 ui/views/mail-reading-pane.slint:223 #: ui/views/mail-reading-pane.slint:1162 msgid "Loading message…" msgstr "" -#: ui/app.slint:494 ui/components/workspace-navigation.slint:551 +#: ui/app.slint:492 ui/components/workspace-navigation.slint:551 #: ui/tray.slint:9 ui/tray.slint:10 msgid "Flectar Mail" msgstr "" -#: ui/app.slint:1065 ui/app.slint:1078 +#: ui/app.slint:1063 ui/app.slint:1076 msgid "Global Labels (local only)" msgstr "" -#: ui/app.slint:1938 +#: ui/app.slint:1936 msgid "Empty Trash?" msgstr "" -#: ui/app.slint:1938 ui/views/files-view.slint:538 +#: ui/app.slint:1936 ui/views/files-view.slint:538 msgid "Delete permanently?" msgstr "" -#: ui/app.slint:1942 +#: ui/app.slint:1940 msgid "" "Permanently delete all messages in Trash across every connected account?" msgstr "" -#: ui/app.slint:1943 +#: ui/app.slint:1941 msgid "Permanently delete all messages in this account’s Trash?" msgstr "" -#: ui/app.slint:1945 +#: ui/app.slint:1943 msgid "Permanently delete the selected conversations from Trash?" msgstr "" -#: ui/app.slint:1946 +#: ui/app.slint:1944 msgid "Permanently delete this conversation from Trash?" msgstr "" -#: ui/app.slint:1947 ui/components/calendar.slint:926 +#: ui/app.slint:1945 ui/components/calendar.slint:926 msgid "This action cannot be undone." msgstr "" -#: ui/app.slint:1948 +#: ui/app.slint:1946 msgid "Empty Trash" msgstr "" -#: ui/app.slint:1948 ui/views/files-view.slint:620 +#: ui/app.slint:1946 ui/views/files-view.slint:620 msgid "Delete permanently" msgstr "" -#: ui/app.slint:2111 +#: ui/app.slint:2109 msgid "Mailbox startup failed" msgstr "" -#: ui/app.slint:2117 +#: ui/app.slint:2115 msgid "Flectar Mail could not open local storage" msgstr "" -#: ui/app.slint:2130 +#: ui/app.slint:2128 msgid "Diagnostic ID: {}" msgstr "" -#: ui/app.slint:2133 +#: ui/app.slint:2131 msgid "Diagnostic ID {}" msgstr "" -#: ui/app.slint:2138 +#: ui/app.slint:2136 msgid "Retry" msgstr "" -#: ui/app.slint:2146 ui/components/copyable-diagnostic.slint:38 +#: ui/app.slint:2144 ui/components/copyable-diagnostic.slint:38 msgid "Copy diagnostics" msgstr "" diff --git a/lang/es/LC_MESSAGES/flectar-mail.po b/lang/es/LC_MESSAGES/flectar-mail.po index 0878c9b2..95440476 100644 --- a/lang/es/LC_MESSAGES/flectar-mail.po +++ b/lang/es/LC_MESSAGES/flectar-mail.po @@ -186,7 +186,7 @@ msgstr "Historial de conversaciones disponible" msgid "New address-book entry" msgstr "Nueva entrada de contactos" -#: ui/app.slint:121 ui/app.slint:847 ui/components/composer-fields.slint:66 +#: ui/app.slint:121 ui/app.slint:845 ui/components/composer-fields.slint:66 #: ui/dialogs/compose-dialog.slint:37 msgid "No connected account" msgstr "No hay ninguna cuenta conectada" @@ -201,11 +201,11 @@ msgstr[1] "{n} interacciones" msgid "Last contacted {} {}, {}" msgstr "Último contacto: {1} de {0} de {2}" -#: ui/app.slint:128 ui/app.slint:361 +#: ui/app.slint:128 ui/app.slint:359 msgid "Loading local data…" msgstr "Cargando datos locales…" -#: ui/app.slint:129 ui/app.slint:362 +#: ui/app.slint:129 ui/app.slint:360 msgid "Local data ready." msgstr "Datos locales listos." @@ -680,236 +680,228 @@ msgid "Choose where to export the database snapshot" msgstr "Elige dónde exportar la instantánea de la base de datos" #: ui/app.slint:289 -msgid "The platform host does not provide document export." -msgstr "La plataforma no permite exportar documentos." - -#: ui/app.slint:290 -msgid "The platform host does not provide document import." -msgstr "La plataforma no permite importar documentos." - -#: ui/app.slint:291 msgid "Backup exported. Passwords and OAuth tokens were not included." msgstr "" "Copia de seguridad exportada. No se incluyeron contraseñas ni tokens de " "OAuth." -#: ui/app.slint:292 +#: ui/app.slint:290 msgid "Could not export backup: {}" msgstr "No se pudo exportar la copia de seguridad: {}" -#: ui/app.slint:293 +#: ui/app.slint:291 msgid "Local data storage is unavailable. Retry startup." msgstr "" "El almacenamiento de datos locales no está disponible. Vuelve a intentar el " "inicio." -#: ui/app.slint:294 +#: ui/app.slint:292 msgid "Creating verified database snapshot…" msgstr "Creando una instantánea verificada de la base de datos…" -#: ui/app.slint:295 +#: ui/app.slint:293 msgid "Backup imported. All {} account configurations already existed." msgstr "" "Copia de seguridad importada. Las {} configuraciones de cuenta ya existían." -#: ui/app.slint:296 +#: ui/app.slint:294 msgid "" "Imported {} account configurations. Open Accounts and sign in to each one." msgstr "" "Se importaron {} configuraciones de cuenta. Abre Cuentas e inicia sesión en " "cada una." -#: ui/app.slint:297 +#: ui/app.slint:295 msgid "Could not import backup: {}" msgstr "No se pudo importar la copia de seguridad: {}" -#: ui/app.slint:298 +#: ui/app.slint:296 msgid "All local Flectar Mail data was deleted." msgstr "Se eliminaron todos los datos locales de Flectar Mail." -#: ui/app.slint:299 +#: ui/app.slint:297 msgid "Could not delete all local data: {}" msgstr "No se pudieron eliminar todos los datos locales: {}" -#: ui/app.slint:300 +#: ui/app.slint:298 msgid "Deleting local accounts and cached data…" msgstr "Eliminando las cuentas locales y los datos en caché…" -#: ui/app.slint:305 +#: ui/app.slint:303 msgid "Background mail refresh failed: {}" msgstr "Falló la actualización del correo en segundo plano: {}" -#: ui/app.slint:306 +#: ui/app.slint:304 msgid "No conversations in this folder" msgstr "No hay conversaciones en esta carpeta" -#: ui/app.slint:307 +#: ui/app.slint:305 msgid "No conversations match this search" msgstr "Ninguna conversación coincide con esta búsqueda" -#: ui/app.slint:308 +#: ui/app.slint:306 msgid "Mail core is ready for account synchronization and message actions." msgstr "" "El núcleo de correo está listo para sincronizar cuentas y realizar acciones " "con mensajes." -#: ui/app.slint:309 +#: ui/app.slint:307 msgid "Large message rendered in compatibility mode." msgstr "Mensaje grande renderizado en modo de compatibilidad." -#: ui/app.slint:310 +#: ui/app.slint:308 msgid "Software email render failed: {}" msgstr "Falló la renderización por software del correo: {}" -#: ui/app.slint:311 +#: ui/app.slint:309 msgid "Rendered with software fallback after GPU error: {}" msgstr "Renderizado por software tras un error de la GPU: {}" -#: ui/app.slint:312 +#: ui/app.slint:310 msgid "Blitz render failed (GPU: {}; software: {})" msgstr "Falló la renderización de Blitz (GPU: {}; software: {})" -#: ui/app.slint:317 +#: ui/app.slint:315 msgid "Add an email address, then save the contact." msgstr "Añade una dirección de correo y guarda el contacto." -#: ui/app.slint:318 +#: ui/app.slint:316 msgid "Enter a valid email address before saving." msgstr "Introduce una dirección de correo válida antes de guardar." -#: ui/app.slint:319 +#: ui/app.slint:317 msgid "Could not save: contact storage is unavailable." msgstr "No se pudo guardar: el almacenamiento de contactos no está disponible." -#: ui/app.slint:320 +#: ui/app.slint:318 msgid "Could not save contact: {}" msgstr "No se pudo guardar el contacto: {}" -#: ui/app.slint:321 +#: ui/app.slint:319 msgid "Contact saved." msgstr "Contacto guardado." -#: ui/app.slint:322 +#: ui/app.slint:320 msgid "Saved to contacts." msgstr "Guardado en contactos." -#: ui/app.slint:323 +#: ui/app.slint:321 msgid "Could not delete: contact storage is unavailable." msgstr "" "No se pudo eliminar: el almacenamiento de contactos no está disponible." -#: ui/app.slint:324 +#: ui/app.slint:322 msgid "Could not delete contact: {}" msgstr "No se pudo eliminar el contacto: {}" -#: ui/app.slint:325 +#: ui/app.slint:323 msgid "Contact deleted." msgstr "Contacto eliminado." -#: ui/app.slint:326 +#: ui/app.slint:324 msgid "Could not load contacts: {}" msgstr "No se pudieron cargar los contactos: {}" -#: ui/app.slint:327 +#: ui/app.slint:325 msgid "Local contact storage is still starting…" msgstr "El almacenamiento local de contactos aún se está iniciando…" -#: ui/app.slint:328 +#: ui/app.slint:326 msgid "Loading contacts…" msgstr "Cargando contactos…" -#: ui/app.slint:333 ui/components/calendar-agenda.slint:233 +#: ui/app.slint:331 ui/components/calendar-agenda.slint:233 msgid "Local calendar" msgstr "Calendario local" -#: ui/app.slint:334 ui/components/calendar.slint:271 +#: ui/app.slint:332 ui/components/calendar.slint:271 #: ui/components/calendar.slint:319 ui/components/calendar.slint:434 msgid "Calendar" msgstr "Calendario" -#: ui/app.slint:335 +#: ui/app.slint:333 msgid "Could not load calendar: {}" msgstr "No se pudo cargar el calendario: {}" -#: ui/app.slint:336 +#: ui/app.slint:334 msgid "Calendar shown and syncing." msgstr "Calendario visible y sincronizándose." -#: ui/app.slint:337 +#: ui/app.slint:335 msgid "Calendar hidden and sync paused." msgstr "Calendario oculto y sincronización en pausa." -#: ui/app.slint:338 +#: ui/app.slint:336 msgid "Calendar update failed: {}" msgstr "Falló la actualización del calendario: {}" -#: ui/app.slint:339 +#: ui/app.slint:337 msgid "Only events created in Flectar Mail can be edited." msgstr "Solo se pueden editar los eventos creados en Flectar Mail." -#: ui/app.slint:340 +#: ui/app.slint:338 msgid "Only events created in Flectar Mail can be deleted." msgstr "Solo se pueden eliminar los eventos creados en Flectar Mail." -#: ui/app.slint:341 +#: ui/app.slint:339 msgid "Calendar storage is unavailable." msgstr "El almacenamiento del calendario no está disponible." -#: ui/app.slint:342 +#: ui/app.slint:340 msgid "Could not delete event: {}" msgstr "No se pudo eliminar el evento: {}" -#: ui/app.slint:343 +#: ui/app.slint:341 msgid "Event deleted, but refresh failed: {}" msgstr "El evento se eliminó, pero falló la actualización: {}" -#: ui/app.slint:344 +#: ui/app.slint:342 msgid "Event deleted." msgstr "Evento eliminado." -#: ui/app.slint:345 +#: ui/app.slint:343 msgid "Use YYYY-MM-DD and a 24-hour time such as 09:30." msgstr "Usa AAAA-MM-DD y una hora en formato de 24 horas, como 09:30." -#: ui/app.slint:346 +#: ui/app.slint:344 msgid "Event duration must be a number of minutes." msgstr "La duración del evento debe ser un número de minutos." -#: ui/app.slint:347 +#: ui/app.slint:345 msgid "Add a title and choose a duration from 15 to 1440 minutes." msgstr "Añade un título y elige una duración de entre 15 y 1440 minutos." -#: ui/app.slint:348 +#: ui/app.slint:346 msgid "That local time does not exist in the current time zone." msgstr "Esa hora local no existe en la zona horaria actual." -#: ui/app.slint:349 +#: ui/app.slint:347 msgid "Could not save event: {}" msgstr "No se pudo guardar el evento: {}" -#: ui/app.slint:350 +#: ui/app.slint:348 msgid "Event saved, but refresh failed: {}" msgstr "El evento se guardó, pero falló la actualización: {}" -#: ui/app.slint:351 +#: ui/app.slint:349 msgid "Event saved. Provider sync will run when enabled." msgstr "" "Evento guardado. La sincronización del proveedor se ejecutará cuando esté " "activada." -#: ui/app.slint:356 +#: ui/app.slint:354 msgid "Email resource render failed: {}" msgstr "Falló la renderización de un recurso del correo: {}" -#: ui/app.slint:357 +#: ui/app.slint:355 msgid "Message refresh failed: {}" msgstr "Falló la actualización del mensaje: {}" -#: ui/app.slint:358 +#: ui/app.slint:356 msgid "Message load failed: {}" msgstr "Falló la carga del mensaje: {}" -#: ui/app.slint:359 +#: ui/app.slint:357 msgid "" "Your local mailbox could not be opened. No account operation was started. " "Retry, or copy the diagnostics for support." @@ -917,257 +909,257 @@ msgstr "" "No se pudo abrir tu buzón local. No se inició ninguna operación de cuenta. " "Vuelve a intentarlo o copia el diagnóstico para solicitar asistencia." -#: ui/app.slint:360 +#: ui/app.slint:358 msgid "Local storage startup failed." msgstr "Falló el inicio del almacenamiento local." -#: ui/app.slint:367 +#: ui/app.slint:365 msgid "Could not identify the dragged account." msgstr "No se pudo identificar la cuenta arrastrada." -#: ui/app.slint:368 +#: ui/app.slint:366 msgid "Account ordering requires local mail data." msgstr "Para ordenar las cuentas se necesitan los datos de correo locales." -#: ui/app.slint:369 +#: ui/app.slint:367 msgid "Could not find the account to reorder." msgstr "No se encontró la cuenta que se iba a reordenar." -#: ui/app.slint:370 +#: ui/app.slint:368 msgid "Saving account order…" msgstr "Guardando el orden de las cuentas…" -#: ui/app.slint:371 +#: ui/app.slint:369 msgid "Sent copy settings require local mail data." msgstr "La configuración de copias enviadas requiere datos de correo locales." -#: ui/app.slint:372 +#: ui/app.slint:370 msgid "Updating Sent copy settings…" msgstr "Actualizando la configuración de copias enviadas…" -#: ui/app.slint:373 +#: ui/app.slint:371 msgid "Sent copy settings updated." msgstr "Configuración de copias enviadas actualizada." -#: ui/app.slint:374 +#: ui/app.slint:372 msgid "Could not update Sent copy settings: {}" msgstr "No se pudo actualizar la configuración de copias enviadas: {}" -#: ui/app.slint:375 +#: ui/app.slint:373 msgid "Mail history requires local mail data." msgstr "El historial de correo necesita los datos de correo locales." -#: ui/app.slint:376 +#: ui/app.slint:374 msgid "Updating mail history…" msgstr "Actualizando el historial de correo…" -#: ui/app.slint:377 +#: ui/app.slint:375 msgid "Remote images are unavailable in this minimal build." msgstr "Las imágenes remotas no están disponibles en esta compilación mínima." -#: ui/app.slint:378 +#: ui/app.slint:376 msgid "Could not save remote-image setting: {}" msgstr "No se pudo guardar el ajuste de imágenes remotas: {}" -#: ui/app.slint:379 +#: ui/app.slint:377 msgid "Remote sender icons unavailable: {}" msgstr "Los iconos remotos de los remitentes no están disponibles: {}" -#: ui/app.slint:380 +#: ui/app.slint:378 msgid "Remote images unavailable: {}" msgstr "Las imágenes remotas no están disponibles: {}" -#: ui/app.slint:381 +#: ui/app.slint:379 msgid "Remote body images and sender favicons enabled." msgstr "Imágenes remotas del cuerpo e iconos de remitentes activados." -#: ui/app.slint:382 +#: ui/app.slint:380 msgid "Remote images blocked." msgstr "Imágenes remotas bloqueadas." -#: ui/app.slint:383 +#: ui/app.slint:381 msgid "Could not refresh message images: {}" msgstr "No se pudieron actualizar las imágenes del mensaje: {}" -#: ui/app.slint:384 +#: ui/app.slint:382 msgid "Remote-image loading is disabled." msgstr "La carga de imágenes remotas está desactivada." -#: ui/app.slint:385 +#: ui/app.slint:383 msgid "Could not load message images: {}" msgstr "No se pudieron cargar las imágenes del mensaje: {}" -#: ui/app.slint:386 +#: ui/app.slint:384 msgid "Sync finished, refresh failed: {}" msgstr "La sincronización terminó, pero falló la actualización: {}" -#: ui/app.slint:387 +#: ui/app.slint:385 msgid "Sync complete." msgstr "Sincronización completada." -#: ui/app.slint:388 +#: ui/app.slint:386 msgid "Sync failed: {}" msgstr "Falló la sincronización: {}" -#: ui/app.slint:389 +#: ui/app.slint:387 msgid "Local mail data is unavailable; preview data is read-only." msgstr "" "Los datos de correo locales no están disponibles; los datos de vista previa " "son de solo lectura." -#: ui/app.slint:390 +#: ui/app.slint:388 msgid "Synchronizing accounts…" msgstr "Sincronizando cuentas…" -#: ui/app.slint:395 +#: ui/app.slint:393 msgid "Message action completed." msgstr "Acción del mensaje completada." -#: ui/app.slint:396 +#: ui/app.slint:394 msgid "" "Permanent deletion queued. Mail will disappear after the server confirms." msgstr "" "Eliminación permanente en cola. Los mensajes desaparecerán cuando el " "servidor la confirme." -#: ui/app.slint:397 +#: ui/app.slint:395 msgid "Message action failed: {}" msgstr "Falló la acción del mensaje: {}" -#: ui/app.slint:398 +#: ui/app.slint:396 msgid "Message action failed: message is no longer available" msgstr "Falló la acción del mensaje: el mensaje ya no está disponible" -#: ui/app.slint:399 +#: ui/app.slint:397 msgid "Label added." msgstr "Etiqueta agregada." -#: ui/app.slint:400 +#: ui/app.slint:398 msgid "Label removed." msgstr "Etiqueta eliminada." -#: ui/app.slint:401 +#: ui/app.slint:399 msgid "Label created and added." msgstr "Etiqueta creada y agregada." -#: ui/app.slint:402 +#: ui/app.slint:400 msgid "Label created." msgstr "Etiqueta creada." -#: ui/app.slint:403 +#: ui/app.slint:401 msgid "Label updated." msgstr "Etiqueta actualizada." -#: ui/app.slint:404 +#: ui/app.slint:402 msgid "Label deleted." msgstr "Etiqueta eliminada." -#: ui/app.slint:405 +#: ui/app.slint:403 msgid "Could not update label: {}" msgstr "No se pudo actualizar la etiqueta: {}" -#: ui/app.slint:406 +#: ui/app.slint:404 msgid "Could not save label: {}" msgstr "No se pudo guardar la etiqueta: {}" -#: ui/app.slint:407 +#: ui/app.slint:405 msgid "Could not delete label: {}" msgstr "No se pudo eliminar la etiqueta: {}" -#: ui/app.slint:408 +#: ui/app.slint:406 msgid "Folder created." msgstr "Carpeta creada." -#: ui/app.slint:409 +#: ui/app.slint:407 msgid "Folder renamed." msgstr "Carpeta renombrada." -#: ui/app.slint:410 +#: ui/app.slint:408 msgid "Folder deleted." msgstr "Carpeta eliminada." -#: ui/app.slint:411 +#: ui/app.slint:409 msgid "Could not create folder: {}" msgstr "No se pudo crear la carpeta: {}" -#: ui/app.slint:412 +#: ui/app.slint:410 msgid "Could not rename folder: {}" msgstr "No se pudo renombrar la carpeta: {}" -#: ui/app.slint:413 +#: ui/app.slint:411 msgid "Could not delete folder: {}" msgstr "No se pudo eliminar la carpeta: {}" -#: ui/app.slint:414 +#: ui/app.slint:412 msgid "Could not close preview: {}" msgstr "No se pudo cerrar la vista previa: {}" -#: ui/app.slint:415 +#: ui/app.slint:413 msgid "Opened the system print view." msgstr "Se abrió la vista de impresión del sistema." -#: ui/app.slint:416 +#: ui/app.slint:414 msgid "Could not print message: {}" msgstr "No se pudo imprimir el mensaje: {}" -#: ui/app.slint:417 +#: ui/app.slint:415 msgid "Opened the message in your browser." msgstr "Se abrió el mensaje en el navegador." -#: ui/app.slint:418 +#: ui/app.slint:416 msgid "Could not open message in browser: {}" msgstr "No se pudo abrir el mensaje en el navegador: {}" -#: ui/app.slint:419 +#: ui/app.slint:417 msgid "Connect an account before replying or forwarding." msgstr "Conecta una cuenta antes de responder o reenviar." -#: ui/app.slint:420 +#: ui/app.slint:418 msgid "Choose a message first." msgstr "Elige primero un mensaje." -#: ui/app.slint:421 +#: ui/app.slint:419 msgid "Only drafts can be reopened for editing." msgstr "Solo se pueden volver a abrir borradores para editarlos." -#: ui/app.slint:422 +#: ui/app.slint:420 msgid "Could not open draft: {}" msgstr "No se pudo abrir el borrador: {}" -#: ui/app.slint:423 +#: ui/app.slint:421 msgid "Could not open draft: invalid account identifier" msgstr "" "No se pudo abrir el borrador: el identificador de la cuenta no es válido" -#: ui/app.slint:424 +#: ui/app.slint:422 msgid "Connected account" msgstr "Cuenta conectada" -#: ui/app.slint:425 +#: ui/app.slint:423 msgid "Could not open composer: {}" msgstr "No se pudo abrir el editor: {}" -#: ui/app.slint:426 +#: ui/app.slint:424 msgid "Could not open composer: invalid account identifier" msgstr "No se pudo abrir el editor: el identificador de la cuenta no es válido" -#: ui/app.slint:431 +#: ui/app.slint:429 msgid "Attach files to this email" msgstr "Adjuntar archivos a este correo" -#: ui/app.slint:432 -msgid "The platform host does not provide a document picker." -msgstr "La plataforma no proporciona un selector de documentos." +#: ui/app.slint:430 +msgid "Could not add attachments: {}" +msgstr "No se pudieron añadir los archivos adjuntos: {}" -#: ui/app.slint:433 +#: ui/app.slint:431 msgid "Added 1 attachment." msgstr "Se añadió 1 archivo adjunto." -#: ui/app.slint:434 +#: ui/app.slint:432 msgid "Added {} attachments." msgstr "Se añadieron {} archivos adjuntos." -#: ui/app.slint:435 +#: ui/app.slint:433 msgid "" "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB " "total limit)." @@ -1175,117 +1167,117 @@ msgstr "" "Archivos adjuntos añadidos: {}. Omitidos: {} (duplicados, ilegibles o por " "superar el límite total de 25 MB)." -#: ui/app.slint:436 +#: ui/app.slint:434 msgid "Attachment removed." msgstr "Archivo adjunto eliminado." -#: ui/app.slint:437 +#: ui/app.slint:435 msgid "Choose the account this message should be sent from." msgstr "Elige la cuenta desde la que se debe enviar este mensaje." -#: ui/app.slint:438 +#: ui/app.slint:436 msgid "Choose a verified sender identity for this message." msgstr "Elige una identidad de remitente verificada para este mensaje." -#: ui/app.slint:439 +#: ui/app.slint:437 msgid "Connect an account before saving or sending mail." msgstr "Conecta una cuenta antes de guardar o enviar correo." -#: ui/app.slint:440 +#: ui/app.slint:438 msgid "Message queued for delivery (action {})." msgstr "Mensaje en cola para su entrega (acción {})." -#: ui/app.slint:441 +#: ui/app.slint:439 msgid "Sending message…" msgstr "Enviando mensaje…" -#: ui/app.slint:442 +#: ui/app.slint:440 msgid "Message sent." msgstr "Mensaje enviado." -#: ui/app.slint:443 +#: ui/app.slint:441 msgid "Message was not sent: {} Your draft is preserved." msgstr "El mensaje no se envió: {} El borrador se ha conservado." -#: ui/app.slint:444 +#: ui/app.slint:442 msgid "Draft saved locally (#{})." msgstr "Borrador guardado localmente (#{})." -#: ui/app.slint:445 +#: ui/app.slint:443 msgid "Compose failed: {}" msgstr "Falló la redacción: {}" -#: ui/app.slint:446 +#: ui/app.slint:444 msgid "Connect an account before managing email templates." msgstr "Conecta una cuenta antes de gestionar plantillas de correo." -#: ui/app.slint:447 +#: ui/app.slint:445 msgid "Could not load email templates: {}" msgstr "No se pudieron cargar las plantillas de correo: {}" -#: ui/app.slint:448 +#: ui/app.slint:446 msgid "Enter a template name." msgstr "Escribe un nombre para la plantilla." -#: ui/app.slint:449 +#: ui/app.slint:447 msgid "Add a subject or message to the template." msgstr "Añade un asunto o mensaje a la plantilla." -#: ui/app.slint:450 +#: ui/app.slint:448 msgid "Email template saved." msgstr "Plantilla de correo guardada." -#: ui/app.slint:451 +#: ui/app.slint:449 msgid "Could not save email template: {}" msgstr "No se pudo guardar la plantilla de correo: {}" -#: ui/app.slint:452 +#: ui/app.slint:450 msgid "Email template deleted." msgstr "Plantilla de correo eliminada." -#: ui/app.slint:453 +#: ui/app.slint:451 msgid "Could not delete email template: {}" msgstr "No se pudo eliminar la plantilla de correo: {}" -#: ui/app.slint:454 +#: ui/app.slint:452 msgid "Email template is no longer available." msgstr "La plantilla de correo ya no está disponible." -#: ui/app.slint:455 +#: ui/app.slint:453 msgid "Email template inserted." msgstr "Plantilla de correo insertada." -#: ui/app.slint:456 +#: ui/app.slint:454 msgid "Email template inserted, but its usage could not be saved: {}" msgstr "Plantilla de correo insertada, pero no se pudo guardar su uso: {}" -#: ui/app.slint:457 +#: ui/app.slint:455 msgid "IMAP port must be between 1 and 65535." msgstr "El puerto IMAP debe estar entre 1 y 65535." -#: ui/app.slint:458 +#: ui/app.slint:456 msgid "SMTP port must be between 1 and 65535." msgstr "El puerto SMTP debe estar entre 1 y 65535." -#: ui/app.slint:459 +#: ui/app.slint:457 msgid "Local mail data is unavailable. Retry startup." msgstr "" "Los datos de correo locales no están disponibles. Vuelve a intentar el " "inicio." -#: ui/app.slint:460 +#: ui/app.slint:458 msgid "Discovering JMAP and checking capabilities…" msgstr "Detectando JMAP y comprobando sus capacidades…" -#: ui/app.slint:461 +#: ui/app.slint:459 msgid "Certificate imported. Connect to verify the server." msgstr "Certificado importado. Conecta para verificar el servidor." -#: ui/app.slint:462 +#: ui/app.slint:460 msgid "Could not load account preferences: {}" msgstr "No se pudieron cargar las preferencias de la cuenta: {}" -#: ui/app.slint:463 +#: ui/app.slint:461 msgid "" "Your edited signature was preserved. Choose No signature to keep it as " "message text before inserting another." @@ -1293,19 +1285,19 @@ msgstr "" "Se conservó la firma que editaste. Elige Sin firma para mantenerla como " "texto del mensaje antes de insertar otra." -#: ui/app.slint:464 +#: ui/app.slint:462 msgid "Certificate import failed: {}" msgstr "Error al importar el certificado: {}" -#: ui/app.slint:465 +#: ui/app.slint:463 msgid "Paste the PEM certificate into the certificate field." msgstr "Pega el certificado PEM en el campo del certificado." -#: ui/app.slint:466 +#: ui/app.slint:464 msgid "Testing IMAP and SMTP connections…" msgstr "Comprobando las conexiones IMAP y SMTP…" -#: ui/app.slint:471 +#: ui/app.slint:469 msgid "" "Another browser authorization is already in progress; finish it or choose " "Cancel." @@ -1313,150 +1305,150 @@ msgstr "" "Ya hay otra autorización del navegador en curso; termínala o selecciona " "Cancelar." -#: ui/app.slint:472 ui/dialogs/settings-dialog.slint:1643 +#: ui/app.slint:470 ui/dialogs/settings-dialog.slint:1643 #: ui/dialogs/settings-dialog.slint:1686 ui/views/onboarding-view.slint:152 msgid "Add app keys in Sign-in settings to enable this provider." msgstr "" "Añade claves de aplicación en los ajustes de inicio de sesión para habilitar " "este proveedor." -#: ui/app.slint:473 +#: ui/app.slint:471 msgid "Choose Gmail or Outlook OAuth." msgstr "Elige OAuth de Gmail o Outlook." -#: ui/app.slint:474 +#: ui/app.slint:472 msgid "Complete sign-in in your browser, or choose Cancel here." msgstr "" "Completa el inicio de sesión en el navegador o selecciona Cancelar aquí." -#: ui/app.slint:475 +#: ui/app.slint:473 msgid "Cancelling browser sign-in…" msgstr "Cancelando el inicio de sesión en el navegador…" -#: ui/app.slint:476 +#: ui/app.slint:474 msgid "Finishing browser sign-in…" msgstr "Finalizando el inicio de sesión en el navegador…" -#: ui/app.slint:477 +#: ui/app.slint:475 msgid "Could not use that browser address: {}" msgstr "No se pudo usar esa dirección del navegador: {}" -#: ui/app.slint:478 +#: ui/app.slint:476 msgid "Calendar core is unavailable." msgstr "El núcleo del calendario no está disponible." -#: ui/app.slint:479 +#: ui/app.slint:477 msgid "Account no longer exists." msgstr "La cuenta ya no existe." -#: ui/app.slint:480 +#: ui/app.slint:478 msgid "Authorize calendar access in your browser." msgstr "Autoriza el acceso al calendario en el navegador." -#: ui/app.slint:481 +#: ui/app.slint:479 msgid "OAuth app keys saved." msgstr "Claves de la aplicación OAuth guardadas." -#: ui/app.slint:482 +#: ui/app.slint:480 msgid "{} OAuth settings saved." msgstr "Ajustes de OAuth de {} guardados." -#: ui/app.slint:483 +#: ui/app.slint:481 msgid "OAuth settings failed: {}" msgstr "Fallaron los ajustes de OAuth: {}" -#: ui/app.slint:484 +#: ui/app.slint:482 msgid "Mail refresh failed: {}" msgstr "Falló la actualización del correo: {}" -#: ui/app.slint:485 +#: ui/app.slint:483 msgid "Mail filter failed: {}" msgstr "Falló el filtro de correo: {}" -#: ui/app.slint:486 +#: ui/app.slint:484 msgid "Mail pagination failed: {}" msgstr "Falló la paginación del correo: {}" -#: ui/app.slint:487 +#: ui/app.slint:485 msgid "Could not mark message as read: {}" msgstr "No se pudo marcar el mensaje como leído: {}" -#: ui/app.slint:488 ui/views/mail-reading-pane.slint:223 +#: ui/app.slint:486 ui/views/mail-reading-pane.slint:223 #: ui/views/mail-reading-pane.slint:1162 msgid "Loading message…" msgstr "Cargando mensaje…" -#: ui/app.slint:494 ui/components/workspace-navigation.slint:551 +#: ui/app.slint:492 ui/components/workspace-navigation.slint:551 #: ui/tray.slint:9 ui/tray.slint:10 msgid "Flectar Mail" msgstr "Flectar Mail" -#: ui/app.slint:1065 ui/app.slint:1078 +#: ui/app.slint:1063 ui/app.slint:1076 msgid "Global Labels (local only)" msgstr "Etiquetas globales (solo locales)" -#: ui/app.slint:1938 +#: ui/app.slint:1936 msgid "Empty Trash?" msgstr "¿Vaciar la papelera?" -#: ui/app.slint:1938 ui/views/files-view.slint:538 +#: ui/app.slint:1936 ui/views/files-view.slint:538 msgid "Delete permanently?" msgstr "¿Eliminar permanentemente?" -#: ui/app.slint:1942 +#: ui/app.slint:1940 msgid "" "Permanently delete all messages in Trash across every connected account?" msgstr "" "¿Eliminar permanentemente todos los mensajes de la papelera de todas las " "cuentas conectadas?" -#: ui/app.slint:1943 +#: ui/app.slint:1941 msgid "Permanently delete all messages in this account’s Trash?" msgstr "" "¿Eliminar permanentemente todos los mensajes de la papelera de esta cuenta?" -#: ui/app.slint:1945 +#: ui/app.slint:1943 msgid "Permanently delete the selected conversations from Trash?" msgstr "" "¿Eliminar permanentemente las conversaciones seleccionadas de la papelera?" -#: ui/app.slint:1946 +#: ui/app.slint:1944 msgid "Permanently delete this conversation from Trash?" msgstr "¿Eliminar permanentemente esta conversación de la papelera?" -#: ui/app.slint:1947 ui/components/calendar.slint:926 +#: ui/app.slint:1945 ui/components/calendar.slint:926 msgid "This action cannot be undone." msgstr "Esta acción no se puede deshacer." -#: ui/app.slint:1948 +#: ui/app.slint:1946 msgid "Empty Trash" msgstr "Vaciar la papelera" -#: ui/app.slint:1948 ui/views/files-view.slint:620 +#: ui/app.slint:1946 ui/views/files-view.slint:620 msgid "Delete permanently" msgstr "Eliminar permanentemente" -#: ui/app.slint:2111 +#: ui/app.slint:2109 msgid "Mailbox startup failed" msgstr "Error al iniciar el buzón" -#: ui/app.slint:2117 +#: ui/app.slint:2115 msgid "Flectar Mail could not open local storage" msgstr "Flectar Mail no pudo abrir el almacenamiento local" -#: ui/app.slint:2130 +#: ui/app.slint:2128 msgid "Diagnostic ID: {}" msgstr "ID de diagnóstico: {}" -#: ui/app.slint:2133 +#: ui/app.slint:2131 msgid "Diagnostic ID {}" msgstr "ID de diagnóstico {}" -#: ui/app.slint:2138 +#: ui/app.slint:2136 msgid "Retry" msgstr "Reintentar" -#: ui/app.slint:2146 ui/components/copyable-diagnostic.slint:38 +#: ui/app.slint:2144 ui/components/copyable-diagnostic.slint:38 msgid "Copy diagnostics" msgstr "Copiar datos de diagnóstico" diff --git a/lang/flectar-mail.pot b/lang/flectar-mail.pot index 69579671..33264175 100644 --- a/lang/flectar-mail.pot +++ b/lang/flectar-mail.pot @@ -184,8 +184,8 @@ msgstr "" msgid "New address-book entry" msgstr "" -#: ui/app.slint:121 ui/app.slint:847 ui/components/composer-fields.slint:66 -#: ui/app.slint:121 ui/app.slint:847 ui/components/composer-fields.slint:66 ui/dialogs/compose-dialog.slint:37 +#: ui/app.slint:121 ui/app.slint:845 ui/components/composer-fields.slint:66 +#: ui/app.slint:121 ui/app.slint:845 ui/components/composer-fields.slint:66 ui/dialogs/compose-dialog.slint:37 msgid "No connected account" msgstr "" @@ -199,11 +199,11 @@ msgstr[1] "" msgid "Last contacted {} {}, {}" msgstr "" -#: ui/app.slint:128 ui/app.slint:361 +#: ui/app.slint:128 ui/app.slint:359 msgid "Loading local data…" msgstr "" -#: ui/app.slint:129 ui/app.slint:362 +#: ui/app.slint:129 ui/app.slint:360 msgid "Local data ready." msgstr "" @@ -664,759 +664,751 @@ msgid "Choose where to export the database snapshot" msgstr "" #: ui/app.slint:289 -msgid "The platform host does not provide document export." -msgstr "" - -#: ui/app.slint:290 -msgid "The platform host does not provide document import." -msgstr "" - -#: ui/app.slint:291 msgid "Backup exported. Passwords and OAuth tokens were not included." msgstr "" -#: ui/app.slint:292 +#: ui/app.slint:290 msgid "Could not export backup: {}" msgstr "" -#: ui/app.slint:293 +#: ui/app.slint:291 msgid "Local data storage is unavailable. Retry startup." msgstr "" -#: ui/app.slint:294 +#: ui/app.slint:292 msgid "Creating verified database snapshot…" msgstr "" -#: ui/app.slint:295 +#: ui/app.slint:293 msgid "Backup imported. All {} account configurations already existed." msgstr "" -#: ui/app.slint:296 +#: ui/app.slint:294 msgid "" "Imported {} account configurations. Open Accounts and sign in to each one." msgstr "" -#: ui/app.slint:297 +#: ui/app.slint:295 msgid "Could not import backup: {}" msgstr "" -#: ui/app.slint:298 +#: ui/app.slint:296 msgid "All local Flectar Mail data was deleted." msgstr "" -#: ui/app.slint:299 +#: ui/app.slint:297 msgid "Could not delete all local data: {}" msgstr "" -#: ui/app.slint:300 +#: ui/app.slint:298 msgid "Deleting local accounts and cached data…" msgstr "" -#: ui/app.slint:305 +#: ui/app.slint:303 msgid "Background mail refresh failed: {}" msgstr "" -#: ui/app.slint:306 +#: ui/app.slint:304 msgid "No conversations in this folder" msgstr "" -#: ui/app.slint:307 +#: ui/app.slint:305 msgid "No conversations match this search" msgstr "" -#: ui/app.slint:308 +#: ui/app.slint:306 msgid "Mail core is ready for account synchronization and message actions." msgstr "" -#: ui/app.slint:309 +#: ui/app.slint:307 msgid "Large message rendered in compatibility mode." msgstr "" -#: ui/app.slint:310 +#: ui/app.slint:308 msgid "Software email render failed: {}" msgstr "" -#: ui/app.slint:311 +#: ui/app.slint:309 msgid "Rendered with software fallback after GPU error: {}" msgstr "" -#: ui/app.slint:312 +#: ui/app.slint:310 msgid "Blitz render failed (GPU: {}; software: {})" msgstr "" -#: ui/app.slint:317 +#: ui/app.slint:315 msgid "Add an email address, then save the contact." msgstr "" -#: ui/app.slint:318 +#: ui/app.slint:316 msgid "Enter a valid email address before saving." msgstr "" -#: ui/app.slint:319 +#: ui/app.slint:317 msgid "Could not save: contact storage is unavailable." msgstr "" -#: ui/app.slint:320 +#: ui/app.slint:318 msgid "Could not save contact: {}" msgstr "" -#: ui/app.slint:321 +#: ui/app.slint:319 msgid "Contact saved." msgstr "" -#: ui/app.slint:322 +#: ui/app.slint:320 msgid "Saved to contacts." msgstr "" -#: ui/app.slint:323 +#: ui/app.slint:321 msgid "Could not delete: contact storage is unavailable." msgstr "" -#: ui/app.slint:324 +#: ui/app.slint:322 msgid "Could not delete contact: {}" msgstr "" -#: ui/app.slint:325 +#: ui/app.slint:323 msgid "Contact deleted." msgstr "" -#: ui/app.slint:326 +#: ui/app.slint:324 msgid "Could not load contacts: {}" msgstr "" -#: ui/app.slint:327 +#: ui/app.slint:325 msgid "Local contact storage is still starting…" msgstr "" -#: ui/app.slint:328 +#: ui/app.slint:326 msgid "Loading contacts…" msgstr "" -#: ui/app.slint:333 ui/components/calendar-agenda.slint:233 -#: ui/app.slint:333 ui/components/calendar-agenda.slint:233 +#: ui/app.slint:331 ui/components/calendar-agenda.slint:233 +#: ui/app.slint:331 ui/components/calendar-agenda.slint:233 msgid "Local calendar" msgstr "" -#: ui/app.slint:334 ui/components/calendar.slint:271 -#: ui/app.slint:334 ui/components/calendar.slint:271 ui/components/calendar.slint:319 -#: ui/app.slint:334 ui/components/calendar.slint:271 ui/components/calendar.slint:319 ui/components/calendar.slint:434 -#: ui/app.slint:334 ui/components/calendar.slint:271 ui/components/calendar.slint:319 ui/components/calendar.slint:434 -#: ui/app.slint:334 ui/components/calendar.slint:271 ui/components/calendar.slint:319 ui/components/calendar.slint:434 -#: ui/app.slint:334 ui/components/calendar.slint:271 ui/components/calendar.slint:319 ui/components/calendar.slint:434 -#: ui/app.slint:334 ui/components/calendar.slint:271 ui/components/calendar.slint:319 ui/components/calendar.slint:434 +#: ui/app.slint:332 ui/components/calendar.slint:271 +#: ui/app.slint:332 ui/components/calendar.slint:271 ui/components/calendar.slint:319 +#: ui/app.slint:332 ui/components/calendar.slint:271 ui/components/calendar.slint:319 ui/components/calendar.slint:434 +#: ui/app.slint:332 ui/components/calendar.slint:271 ui/components/calendar.slint:319 ui/components/calendar.slint:434 +#: ui/app.slint:332 ui/components/calendar.slint:271 ui/components/calendar.slint:319 ui/components/calendar.slint:434 +#: ui/app.slint:332 ui/components/calendar.slint:271 ui/components/calendar.slint:319 ui/components/calendar.slint:434 +#: ui/app.slint:332 ui/components/calendar.slint:271 ui/components/calendar.slint:319 ui/components/calendar.slint:434 msgid "Calendar" msgstr "" -#: ui/app.slint:335 +#: ui/app.slint:333 msgid "Could not load calendar: {}" msgstr "" -#: ui/app.slint:336 +#: ui/app.slint:334 msgid "Calendar shown and syncing." msgstr "" -#: ui/app.slint:337 +#: ui/app.slint:335 msgid "Calendar hidden and sync paused." msgstr "" -#: ui/app.slint:338 +#: ui/app.slint:336 msgid "Calendar update failed: {}" msgstr "" -#: ui/app.slint:339 +#: ui/app.slint:337 msgid "Only events created in Flectar Mail can be edited." msgstr "" -#: ui/app.slint:340 +#: ui/app.slint:338 msgid "Only events created in Flectar Mail can be deleted." msgstr "" -#: ui/app.slint:341 +#: ui/app.slint:339 msgid "Calendar storage is unavailable." msgstr "" -#: ui/app.slint:342 +#: ui/app.slint:340 msgid "Could not delete event: {}" msgstr "" -#: ui/app.slint:343 +#: ui/app.slint:341 msgid "Event deleted, but refresh failed: {}" msgstr "" -#: ui/app.slint:344 +#: ui/app.slint:342 msgid "Event deleted." msgstr "" -#: ui/app.slint:345 +#: ui/app.slint:343 msgid "Use YYYY-MM-DD and a 24-hour time such as 09:30." msgstr "" -#: ui/app.slint:346 +#: ui/app.slint:344 msgid "Event duration must be a number of minutes." msgstr "" -#: ui/app.slint:347 +#: ui/app.slint:345 msgid "Add a title and choose a duration from 15 to 1440 minutes." msgstr "" -#: ui/app.slint:348 +#: ui/app.slint:346 msgid "That local time does not exist in the current time zone." msgstr "" -#: ui/app.slint:349 +#: ui/app.slint:347 msgid "Could not save event: {}" msgstr "" -#: ui/app.slint:350 +#: ui/app.slint:348 msgid "Event saved, but refresh failed: {}" msgstr "" -#: ui/app.slint:351 +#: ui/app.slint:349 msgid "Event saved. Provider sync will run when enabled." msgstr "" -#: ui/app.slint:356 +#: ui/app.slint:354 msgid "Email resource render failed: {}" msgstr "" -#: ui/app.slint:357 +#: ui/app.slint:355 msgid "Message refresh failed: {}" msgstr "" -#: ui/app.slint:358 +#: ui/app.slint:356 msgid "Message load failed: {}" msgstr "" -#: ui/app.slint:359 +#: ui/app.slint:357 msgid "" "Your local mailbox could not be opened. No account operation was started. " "Retry, or copy the diagnostics for support." msgstr "" -#: ui/app.slint:360 +#: ui/app.slint:358 msgid "Local storage startup failed." msgstr "" -#: ui/app.slint:367 +#: ui/app.slint:365 msgid "Could not identify the dragged account." msgstr "" -#: ui/app.slint:368 +#: ui/app.slint:366 msgid "Account ordering requires local mail data." msgstr "" -#: ui/app.slint:369 +#: ui/app.slint:367 msgid "Could not find the account to reorder." msgstr "" -#: ui/app.slint:370 +#: ui/app.slint:368 msgid "Saving account order…" msgstr "" -#: ui/app.slint:371 +#: ui/app.slint:369 msgid "Sent copy settings require local mail data." msgstr "" -#: ui/app.slint:372 +#: ui/app.slint:370 msgid "Updating Sent copy settings…" msgstr "" -#: ui/app.slint:373 +#: ui/app.slint:371 msgid "Sent copy settings updated." msgstr "" -#: ui/app.slint:374 +#: ui/app.slint:372 msgid "Could not update Sent copy settings: {}" msgstr "" -#: ui/app.slint:375 +#: ui/app.slint:373 msgid "Mail history requires local mail data." msgstr "" -#: ui/app.slint:376 +#: ui/app.slint:374 msgid "Updating mail history…" msgstr "" -#: ui/app.slint:377 +#: ui/app.slint:375 msgid "Remote images are unavailable in this minimal build." msgstr "" -#: ui/app.slint:378 +#: ui/app.slint:376 msgid "Could not save remote-image setting: {}" msgstr "" -#: ui/app.slint:379 +#: ui/app.slint:377 msgid "Remote sender icons unavailable: {}" msgstr "" -#: ui/app.slint:380 +#: ui/app.slint:378 msgid "Remote images unavailable: {}" msgstr "" -#: ui/app.slint:381 +#: ui/app.slint:379 msgid "Remote body images and sender favicons enabled." msgstr "" -#: ui/app.slint:382 +#: ui/app.slint:380 msgid "Remote images blocked." msgstr "" -#: ui/app.slint:383 +#: ui/app.slint:381 msgid "Could not refresh message images: {}" msgstr "" -#: ui/app.slint:384 +#: ui/app.slint:382 msgid "Remote-image loading is disabled." msgstr "" -#: ui/app.slint:385 +#: ui/app.slint:383 msgid "Could not load message images: {}" msgstr "" -#: ui/app.slint:386 +#: ui/app.slint:384 msgid "Sync finished, refresh failed: {}" msgstr "" -#: ui/app.slint:387 +#: ui/app.slint:385 msgid "Sync complete." msgstr "" -#: ui/app.slint:388 +#: ui/app.slint:386 msgid "Sync failed: {}" msgstr "" -#: ui/app.slint:389 +#: ui/app.slint:387 msgid "Local mail data is unavailable; preview data is read-only." msgstr "" -#: ui/app.slint:390 +#: ui/app.slint:388 msgid "Synchronizing accounts…" msgstr "" -#: ui/app.slint:395 +#: ui/app.slint:393 msgid "Message action completed." msgstr "" -#: ui/app.slint:396 +#: ui/app.slint:394 msgid "" "Permanent deletion queued. Mail will disappear after the server confirms." msgstr "" -#: ui/app.slint:397 +#: ui/app.slint:395 msgid "Message action failed: {}" msgstr "" -#: ui/app.slint:398 +#: ui/app.slint:396 msgid "Message action failed: message is no longer available" msgstr "" -#: ui/app.slint:399 +#: ui/app.slint:397 msgid "Label added." msgstr "" -#: ui/app.slint:400 +#: ui/app.slint:398 msgid "Label removed." msgstr "" -#: ui/app.slint:401 +#: ui/app.slint:399 msgid "Label created and added." msgstr "" -#: ui/app.slint:402 +#: ui/app.slint:400 msgid "Label created." msgstr "" -#: ui/app.slint:403 +#: ui/app.slint:401 msgid "Label updated." msgstr "" -#: ui/app.slint:404 +#: ui/app.slint:402 msgid "Label deleted." msgstr "" -#: ui/app.slint:405 +#: ui/app.slint:403 msgid "Could not update label: {}" msgstr "" -#: ui/app.slint:406 +#: ui/app.slint:404 msgid "Could not save label: {}" msgstr "" -#: ui/app.slint:407 +#: ui/app.slint:405 msgid "Could not delete label: {}" msgstr "" -#: ui/app.slint:408 +#: ui/app.slint:406 msgid "Folder created." msgstr "" -#: ui/app.slint:409 +#: ui/app.slint:407 msgid "Folder renamed." msgstr "" -#: ui/app.slint:410 +#: ui/app.slint:408 msgid "Folder deleted." msgstr "" -#: ui/app.slint:411 +#: ui/app.slint:409 msgid "Could not create folder: {}" msgstr "" -#: ui/app.slint:412 +#: ui/app.slint:410 msgid "Could not rename folder: {}" msgstr "" -#: ui/app.slint:413 +#: ui/app.slint:411 msgid "Could not delete folder: {}" msgstr "" -#: ui/app.slint:414 +#: ui/app.slint:412 msgid "Could not close preview: {}" msgstr "" -#: ui/app.slint:415 +#: ui/app.slint:413 msgid "Opened the system print view." msgstr "" -#: ui/app.slint:416 +#: ui/app.slint:414 msgid "Could not print message: {}" msgstr "" -#: ui/app.slint:417 +#: ui/app.slint:415 msgid "Opened the message in your browser." msgstr "" -#: ui/app.slint:418 +#: ui/app.slint:416 msgid "Could not open message in browser: {}" msgstr "" -#: ui/app.slint:419 +#: ui/app.slint:417 msgid "Connect an account before replying or forwarding." msgstr "" -#: ui/app.slint:420 +#: ui/app.slint:418 msgid "Choose a message first." msgstr "" -#: ui/app.slint:421 +#: ui/app.slint:419 msgid "Only drafts can be reopened for editing." msgstr "" -#: ui/app.slint:422 +#: ui/app.slint:420 msgid "Could not open draft: {}" msgstr "" -#: ui/app.slint:423 +#: ui/app.slint:421 msgid "Could not open draft: invalid account identifier" msgstr "" -#: ui/app.slint:424 +#: ui/app.slint:422 msgid "Connected account" msgstr "" -#: ui/app.slint:425 +#: ui/app.slint:423 msgid "Could not open composer: {}" msgstr "" -#: ui/app.slint:426 +#: ui/app.slint:424 msgid "Could not open composer: invalid account identifier" msgstr "" -#: ui/app.slint:431 +#: ui/app.slint:429 msgid "Attach files to this email" msgstr "" -#: ui/app.slint:432 -msgid "The platform host does not provide a document picker." +#: ui/app.slint:430 +msgid "Could not add attachments: {}" msgstr "" -#: ui/app.slint:433 +#: ui/app.slint:431 msgid "Added 1 attachment." msgstr "" -#: ui/app.slint:434 +#: ui/app.slint:432 msgid "Added {} attachments." msgstr "" -#: ui/app.slint:435 +#: ui/app.slint:433 msgid "" "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB " "total limit)." msgstr "" -#: ui/app.slint:436 +#: ui/app.slint:434 msgid "Attachment removed." msgstr "" -#: ui/app.slint:437 +#: ui/app.slint:435 msgid "Choose the account this message should be sent from." msgstr "" -#: ui/app.slint:438 +#: ui/app.slint:436 msgid "Choose a verified sender identity for this message." msgstr "" -#: ui/app.slint:439 +#: ui/app.slint:437 msgid "Connect an account before saving or sending mail." msgstr "" -#: ui/app.slint:440 +#: ui/app.slint:438 msgid "Message queued for delivery (action {})." msgstr "" -#: ui/app.slint:441 +#: ui/app.slint:439 msgid "Sending message…" msgstr "" -#: ui/app.slint:442 +#: ui/app.slint:440 msgid "Message sent." msgstr "" -#: ui/app.slint:443 +#: ui/app.slint:441 msgid "Message was not sent: {} Your draft is preserved." msgstr "" -#: ui/app.slint:444 +#: ui/app.slint:442 msgid "Draft saved locally (#{})." msgstr "" -#: ui/app.slint:445 +#: ui/app.slint:443 msgid "Compose failed: {}" msgstr "" -#: ui/app.slint:446 +#: ui/app.slint:444 msgid "Connect an account before managing email templates." msgstr "" -#: ui/app.slint:447 +#: ui/app.slint:445 msgid "Could not load email templates: {}" msgstr "" -#: ui/app.slint:448 +#: ui/app.slint:446 msgid "Enter a template name." msgstr "" -#: ui/app.slint:449 +#: ui/app.slint:447 msgid "Add a subject or message to the template." msgstr "" -#: ui/app.slint:450 +#: ui/app.slint:448 msgid "Email template saved." msgstr "" -#: ui/app.slint:451 +#: ui/app.slint:449 msgid "Could not save email template: {}" msgstr "" -#: ui/app.slint:452 +#: ui/app.slint:450 msgid "Email template deleted." msgstr "" -#: ui/app.slint:453 +#: ui/app.slint:451 msgid "Could not delete email template: {}" msgstr "" -#: ui/app.slint:454 +#: ui/app.slint:452 msgid "Email template is no longer available." msgstr "" -#: ui/app.slint:455 +#: ui/app.slint:453 msgid "Email template inserted." msgstr "" -#: ui/app.slint:456 +#: ui/app.slint:454 msgid "Email template inserted, but its usage could not be saved: {}" msgstr "" -#: ui/app.slint:457 +#: ui/app.slint:455 msgid "IMAP port must be between 1 and 65535." msgstr "" -#: ui/app.slint:458 +#: ui/app.slint:456 msgid "SMTP port must be between 1 and 65535." msgstr "" -#: ui/app.slint:459 +#: ui/app.slint:457 msgid "Local mail data is unavailable. Retry startup." msgstr "" -#: ui/app.slint:460 +#: ui/app.slint:458 msgid "Discovering JMAP and checking capabilities…" msgstr "" -#: ui/app.slint:461 +#: ui/app.slint:459 msgid "Certificate imported. Connect to verify the server." msgstr "" -#: ui/app.slint:462 +#: ui/app.slint:460 msgid "Could not load account preferences: {}" msgstr "" -#: ui/app.slint:463 +#: ui/app.slint:461 msgid "" "Your edited signature was preserved. Choose No signature to keep it as " "message text before inserting another." msgstr "" -#: ui/app.slint:464 +#: ui/app.slint:462 msgid "Certificate import failed: {}" msgstr "" -#: ui/app.slint:465 +#: ui/app.slint:463 msgid "Paste the PEM certificate into the certificate field." msgstr "" -#: ui/app.slint:466 +#: ui/app.slint:464 msgid "Testing IMAP and SMTP connections…" msgstr "" -#: ui/app.slint:471 +#: ui/app.slint:469 msgid "" "Another browser authorization is already in progress; finish it or choose " "Cancel." msgstr "" -#: ui/app.slint:472 ui/dialogs/settings-dialog.slint:1643 -#: ui/app.slint:472 ui/dialogs/settings-dialog.slint:1643 ui/dialogs/settings-dialog.slint:1686 -#: ui/app.slint:472 ui/dialogs/settings-dialog.slint:1643 ui/dialogs/settings-dialog.slint:1686 ui/views/onboarding-view.slint:152 -#: ui/app.slint:472 ui/dialogs/settings-dialog.slint:1643 ui/dialogs/settings-dialog.slint:1686 ui/views/onboarding-view.slint:152 +#: ui/app.slint:470 ui/dialogs/settings-dialog.slint:1643 +#: ui/app.slint:470 ui/dialogs/settings-dialog.slint:1643 ui/dialogs/settings-dialog.slint:1686 +#: ui/app.slint:470 ui/dialogs/settings-dialog.slint:1643 ui/dialogs/settings-dialog.slint:1686 ui/views/onboarding-view.slint:152 +#: ui/app.slint:470 ui/dialogs/settings-dialog.slint:1643 ui/dialogs/settings-dialog.slint:1686 ui/views/onboarding-view.slint:152 msgid "Add app keys in Sign-in settings to enable this provider." msgstr "" -#: ui/app.slint:473 +#: ui/app.slint:471 msgid "Choose Gmail or Outlook OAuth." msgstr "" -#: ui/app.slint:474 +#: ui/app.slint:472 msgid "Complete sign-in in your browser, or choose Cancel here." msgstr "" -#: ui/app.slint:475 +#: ui/app.slint:473 msgid "Cancelling browser sign-in…" msgstr "" -#: ui/app.slint:476 +#: ui/app.slint:474 msgid "Finishing browser sign-in…" msgstr "" -#: ui/app.slint:477 +#: ui/app.slint:475 msgid "Could not use that browser address: {}" msgstr "" -#: ui/app.slint:478 +#: ui/app.slint:476 msgid "Calendar core is unavailable." msgstr "" -#: ui/app.slint:479 +#: ui/app.slint:477 msgid "Account no longer exists." msgstr "" -#: ui/app.slint:480 +#: ui/app.slint:478 msgid "Authorize calendar access in your browser." msgstr "" -#: ui/app.slint:481 +#: ui/app.slint:479 msgid "OAuth app keys saved." msgstr "" -#: ui/app.slint:482 +#: ui/app.slint:480 msgid "{} OAuth settings saved." msgstr "" -#: ui/app.slint:483 +#: ui/app.slint:481 msgid "OAuth settings failed: {}" msgstr "" -#: ui/app.slint:484 +#: ui/app.slint:482 msgid "Mail refresh failed: {}" msgstr "" -#: ui/app.slint:485 +#: ui/app.slint:483 msgid "Mail filter failed: {}" msgstr "" -#: ui/app.slint:486 +#: ui/app.slint:484 msgid "Mail pagination failed: {}" msgstr "" -#: ui/app.slint:487 +#: ui/app.slint:485 msgid "Could not mark message as read: {}" msgstr "" -#: ui/app.slint:488 ui/views/mail-reading-pane.slint:223 -#: ui/app.slint:488 ui/views/mail-reading-pane.slint:223 ui/views/mail-reading-pane.slint:1162 +#: ui/app.slint:486 ui/views/mail-reading-pane.slint:223 +#: ui/app.slint:486 ui/views/mail-reading-pane.slint:223 ui/views/mail-reading-pane.slint:1162 msgid "Loading message…" msgstr "" -#: ui/app.slint:494 ui/components/workspace-navigation.slint:551 -#: ui/app.slint:494 ui/components/workspace-navigation.slint:551 ui/tray.slint:9 ui/tray.slint:10 -#: ui/app.slint:494 ui/components/workspace-navigation.slint:551 ui/tray.slint:9 ui/tray.slint:10 +#: ui/app.slint:492 ui/components/workspace-navigation.slint:551 +#: ui/app.slint:492 ui/components/workspace-navigation.slint:551 ui/tray.slint:9 ui/tray.slint:10 +#: ui/app.slint:492 ui/components/workspace-navigation.slint:551 ui/tray.slint:9 ui/tray.slint:10 msgid "Flectar Mail" msgstr "" -#: ui/app.slint:1065 ui/app.slint:1078 +#: ui/app.slint:1063 ui/app.slint:1076 msgid "Global Labels (local only)" msgstr "" -#: ui/app.slint:1938 +#: ui/app.slint:1936 msgid "Empty Trash?" msgstr "" -#: ui/app.slint:1938 ui/views/files-view.slint:538 +#: ui/app.slint:1936 ui/views/files-view.slint:538 msgid "Delete permanently?" msgstr "" -#: ui/app.slint:1942 +#: ui/app.slint:1940 msgid "Permanently delete all messages in Trash across every connected account?" msgstr "" -#: ui/app.slint:1943 +#: ui/app.slint:1941 msgid "Permanently delete all messages in this account’s Trash?" msgstr "" -#: ui/app.slint:1945 +#: ui/app.slint:1943 msgid "Permanently delete the selected conversations from Trash?" msgstr "" -#: ui/app.slint:1946 +#: ui/app.slint:1944 msgid "Permanently delete this conversation from Trash?" msgstr "" -#: ui/app.slint:1947 ui/components/calendar.slint:926 -#: ui/app.slint:1947 ui/components/calendar.slint:926 +#: ui/app.slint:1945 ui/components/calendar.slint:926 +#: ui/app.slint:1945 ui/components/calendar.slint:926 msgid "This action cannot be undone." msgstr "" -#: ui/app.slint:1948 +#: ui/app.slint:1946 msgid "Empty Trash" msgstr "" -#: ui/app.slint:1948 ui/views/files-view.slint:620 -#: ui/app.slint:1948 ui/views/files-view.slint:620 +#: ui/app.slint:1946 ui/views/files-view.slint:620 +#: ui/app.slint:1946 ui/views/files-view.slint:620 msgid "Delete permanently" msgstr "" -#: ui/app.slint:2111 +#: ui/app.slint:2109 msgid "Mailbox startup failed" msgstr "" -#: ui/app.slint:2117 +#: ui/app.slint:2115 msgid "Flectar Mail could not open local storage" msgstr "" -#: ui/app.slint:2130 +#: ui/app.slint:2128 msgid "Diagnostic ID: {}" msgstr "" -#: ui/app.slint:2133 +#: ui/app.slint:2131 msgid "Diagnostic ID {}" msgstr "" -#: ui/app.slint:2138 +#: ui/app.slint:2136 msgid "Retry" msgstr "" -#: ui/app.slint:2146 ui/components/copyable-diagnostic.slint:38 +#: ui/app.slint:2144 ui/components/copyable-diagnostic.slint:38 msgid "Copy diagnostics" msgstr "" diff --git a/lang/fr/LC_MESSAGES/flectar-mail.po b/lang/fr/LC_MESSAGES/flectar-mail.po index c2b3a68f..9c4b2dcf 100644 --- a/lang/fr/LC_MESSAGES/flectar-mail.po +++ b/lang/fr/LC_MESSAGES/flectar-mail.po @@ -184,7 +184,7 @@ msgstr "" msgid "New address-book entry" msgstr "" -#: ui/app.slint:121 ui/app.slint:847 ui/components/composer-fields.slint:66 +#: ui/app.slint:121 ui/app.slint:845 ui/components/composer-fields.slint:66 #: ui/dialogs/compose-dialog.slint:37 msgid "No connected account" msgstr "" @@ -199,11 +199,11 @@ msgstr[1] "" msgid "Last contacted {} {}, {}" msgstr "" -#: ui/app.slint:128 ui/app.slint:361 +#: ui/app.slint:128 ui/app.slint:359 msgid "Loading local data…" msgstr "" -#: ui/app.slint:129 ui/app.slint:362 +#: ui/app.slint:129 ui/app.slint:360 msgid "Local data ready." msgstr "" @@ -664,751 +664,743 @@ msgid "Choose where to export the database snapshot" msgstr "" #: ui/app.slint:289 -msgid "The platform host does not provide document export." -msgstr "" - -#: ui/app.slint:290 -msgid "The platform host does not provide document import." -msgstr "" - -#: ui/app.slint:291 msgid "Backup exported. Passwords and OAuth tokens were not included." msgstr "" -#: ui/app.slint:292 +#: ui/app.slint:290 msgid "Could not export backup: {}" msgstr "" -#: ui/app.slint:293 +#: ui/app.slint:291 msgid "Local data storage is unavailable. Retry startup." msgstr "" -#: ui/app.slint:294 +#: ui/app.slint:292 msgid "Creating verified database snapshot…" msgstr "" -#: ui/app.slint:295 +#: ui/app.slint:293 msgid "Backup imported. All {} account configurations already existed." msgstr "" -#: ui/app.slint:296 +#: ui/app.slint:294 msgid "" "Imported {} account configurations. Open Accounts and sign in to each one." msgstr "" -#: ui/app.slint:297 +#: ui/app.slint:295 msgid "Could not import backup: {}" msgstr "" -#: ui/app.slint:298 +#: ui/app.slint:296 msgid "All local Flectar Mail data was deleted." msgstr "" -#: ui/app.slint:299 +#: ui/app.slint:297 msgid "Could not delete all local data: {}" msgstr "" -#: ui/app.slint:300 +#: ui/app.slint:298 msgid "Deleting local accounts and cached data…" msgstr "" -#: ui/app.slint:305 +#: ui/app.slint:303 msgid "Background mail refresh failed: {}" msgstr "" -#: ui/app.slint:306 +#: ui/app.slint:304 msgid "No conversations in this folder" msgstr "" -#: ui/app.slint:307 +#: ui/app.slint:305 msgid "No conversations match this search" msgstr "" -#: ui/app.slint:308 +#: ui/app.slint:306 msgid "Mail core is ready for account synchronization and message actions." msgstr "" -#: ui/app.slint:309 +#: ui/app.slint:307 msgid "Large message rendered in compatibility mode." msgstr "" -#: ui/app.slint:310 +#: ui/app.slint:308 msgid "Software email render failed: {}" msgstr "" -#: ui/app.slint:311 +#: ui/app.slint:309 msgid "Rendered with software fallback after GPU error: {}" msgstr "" -#: ui/app.slint:312 +#: ui/app.slint:310 msgid "Blitz render failed (GPU: {}; software: {})" msgstr "" -#: ui/app.slint:317 +#: ui/app.slint:315 msgid "Add an email address, then save the contact." msgstr "" -#: ui/app.slint:318 +#: ui/app.slint:316 msgid "Enter a valid email address before saving." msgstr "" -#: ui/app.slint:319 +#: ui/app.slint:317 msgid "Could not save: contact storage is unavailable." msgstr "" -#: ui/app.slint:320 +#: ui/app.slint:318 msgid "Could not save contact: {}" msgstr "" -#: ui/app.slint:321 +#: ui/app.slint:319 msgid "Contact saved." msgstr "" -#: ui/app.slint:322 +#: ui/app.slint:320 msgid "Saved to contacts." msgstr "" -#: ui/app.slint:323 +#: ui/app.slint:321 msgid "Could not delete: contact storage is unavailable." msgstr "" -#: ui/app.slint:324 +#: ui/app.slint:322 msgid "Could not delete contact: {}" msgstr "" -#: ui/app.slint:325 +#: ui/app.slint:323 msgid "Contact deleted." msgstr "" -#: ui/app.slint:326 +#: ui/app.slint:324 msgid "Could not load contacts: {}" msgstr "" -#: ui/app.slint:327 +#: ui/app.slint:325 msgid "Local contact storage is still starting…" msgstr "" -#: ui/app.slint:328 +#: ui/app.slint:326 msgid "Loading contacts…" msgstr "" -#: ui/app.slint:333 ui/components/calendar-agenda.slint:233 +#: ui/app.slint:331 ui/components/calendar-agenda.slint:233 msgid "Local calendar" msgstr "" -#: ui/app.slint:334 ui/components/calendar.slint:271 +#: ui/app.slint:332 ui/components/calendar.slint:271 #: ui/components/calendar.slint:319 ui/components/calendar.slint:434 msgid "Calendar" msgstr "" -#: ui/app.slint:335 +#: ui/app.slint:333 msgid "Could not load calendar: {}" msgstr "" -#: ui/app.slint:336 +#: ui/app.slint:334 msgid "Calendar shown and syncing." msgstr "" -#: ui/app.slint:337 +#: ui/app.slint:335 msgid "Calendar hidden and sync paused." msgstr "" -#: ui/app.slint:338 +#: ui/app.slint:336 msgid "Calendar update failed: {}" msgstr "" -#: ui/app.slint:339 +#: ui/app.slint:337 msgid "Only events created in Flectar Mail can be edited." msgstr "" -#: ui/app.slint:340 +#: ui/app.slint:338 msgid "Only events created in Flectar Mail can be deleted." msgstr "" -#: ui/app.slint:341 +#: ui/app.slint:339 msgid "Calendar storage is unavailable." msgstr "" -#: ui/app.slint:342 +#: ui/app.slint:340 msgid "Could not delete event: {}" msgstr "" -#: ui/app.slint:343 +#: ui/app.slint:341 msgid "Event deleted, but refresh failed: {}" msgstr "" -#: ui/app.slint:344 +#: ui/app.slint:342 msgid "Event deleted." msgstr "" -#: ui/app.slint:345 +#: ui/app.slint:343 msgid "Use YYYY-MM-DD and a 24-hour time such as 09:30." msgstr "" -#: ui/app.slint:346 +#: ui/app.slint:344 msgid "Event duration must be a number of minutes." msgstr "" -#: ui/app.slint:347 +#: ui/app.slint:345 msgid "Add a title and choose a duration from 15 to 1440 minutes." msgstr "" -#: ui/app.slint:348 +#: ui/app.slint:346 msgid "That local time does not exist in the current time zone." msgstr "" -#: ui/app.slint:349 +#: ui/app.slint:347 msgid "Could not save event: {}" msgstr "" -#: ui/app.slint:350 +#: ui/app.slint:348 msgid "Event saved, but refresh failed: {}" msgstr "" -#: ui/app.slint:351 +#: ui/app.slint:349 msgid "Event saved. Provider sync will run when enabled." msgstr "" -#: ui/app.slint:356 +#: ui/app.slint:354 msgid "Email resource render failed: {}" msgstr "" -#: ui/app.slint:357 +#: ui/app.slint:355 msgid "Message refresh failed: {}" msgstr "" -#: ui/app.slint:358 +#: ui/app.slint:356 msgid "Message load failed: {}" msgstr "" -#: ui/app.slint:359 +#: ui/app.slint:357 msgid "" "Your local mailbox could not be opened. No account operation was started. " "Retry, or copy the diagnostics for support." msgstr "" -#: ui/app.slint:360 +#: ui/app.slint:358 msgid "Local storage startup failed." msgstr "" -#: ui/app.slint:367 +#: ui/app.slint:365 msgid "Could not identify the dragged account." msgstr "" -#: ui/app.slint:368 +#: ui/app.slint:366 msgid "Account ordering requires local mail data." msgstr "" -#: ui/app.slint:369 +#: ui/app.slint:367 msgid "Could not find the account to reorder." msgstr "" -#: ui/app.slint:370 +#: ui/app.slint:368 msgid "Saving account order…" msgstr "" -#: ui/app.slint:371 +#: ui/app.slint:369 msgid "Sent copy settings require local mail data." msgstr "" "Les paramètres des copies envoyées nécessitent des données de messagerie " "locales." -#: ui/app.slint:372 +#: ui/app.slint:370 msgid "Updating Sent copy settings…" msgstr "Mise à jour des paramètres des copies envoyées…" -#: ui/app.slint:373 +#: ui/app.slint:371 msgid "Sent copy settings updated." msgstr "Paramètres des copies envoyées mis à jour." -#: ui/app.slint:374 +#: ui/app.slint:372 msgid "Could not update Sent copy settings: {}" msgstr "Impossible de mettre à jour les paramètres des copies envoyées : {}" -#: ui/app.slint:375 +#: ui/app.slint:373 msgid "Mail history requires local mail data." msgstr "" -#: ui/app.slint:376 +#: ui/app.slint:374 msgid "Updating mail history…" msgstr "" -#: ui/app.slint:377 +#: ui/app.slint:375 msgid "Remote images are unavailable in this minimal build." msgstr "" -#: ui/app.slint:378 +#: ui/app.slint:376 msgid "Could not save remote-image setting: {}" msgstr "" -#: ui/app.slint:379 +#: ui/app.slint:377 msgid "Remote sender icons unavailable: {}" msgstr "" -#: ui/app.slint:380 +#: ui/app.slint:378 msgid "Remote images unavailable: {}" msgstr "" -#: ui/app.slint:381 +#: ui/app.slint:379 msgid "Remote body images and sender favicons enabled." msgstr "" -#: ui/app.slint:382 +#: ui/app.slint:380 msgid "Remote images blocked." msgstr "" -#: ui/app.slint:383 +#: ui/app.slint:381 msgid "Could not refresh message images: {}" msgstr "" -#: ui/app.slint:384 +#: ui/app.slint:382 msgid "Remote-image loading is disabled." msgstr "" -#: ui/app.slint:385 +#: ui/app.slint:383 msgid "Could not load message images: {}" msgstr "" -#: ui/app.slint:386 +#: ui/app.slint:384 msgid "Sync finished, refresh failed: {}" msgstr "" -#: ui/app.slint:387 +#: ui/app.slint:385 msgid "Sync complete." msgstr "" -#: ui/app.slint:388 +#: ui/app.slint:386 msgid "Sync failed: {}" msgstr "" -#: ui/app.slint:389 +#: ui/app.slint:387 msgid "Local mail data is unavailable; preview data is read-only." msgstr "" -#: ui/app.slint:390 +#: ui/app.slint:388 msgid "Synchronizing accounts…" msgstr "" -#: ui/app.slint:395 +#: ui/app.slint:393 msgid "Message action completed." msgstr "" -#: ui/app.slint:396 +#: ui/app.slint:394 msgid "" "Permanent deletion queued. Mail will disappear after the server confirms." msgstr "" -#: ui/app.slint:397 +#: ui/app.slint:395 msgid "Message action failed: {}" msgstr "" -#: ui/app.slint:398 +#: ui/app.slint:396 msgid "Message action failed: message is no longer available" msgstr "" -#: ui/app.slint:399 +#: ui/app.slint:397 msgid "Label added." msgstr "" -#: ui/app.slint:400 +#: ui/app.slint:398 msgid "Label removed." msgstr "" -#: ui/app.slint:401 +#: ui/app.slint:399 msgid "Label created and added." msgstr "" -#: ui/app.slint:402 +#: ui/app.slint:400 msgid "Label created." msgstr "" -#: ui/app.slint:403 +#: ui/app.slint:401 msgid "Label updated." msgstr "" -#: ui/app.slint:404 +#: ui/app.slint:402 msgid "Label deleted." msgstr "" -#: ui/app.slint:405 +#: ui/app.slint:403 msgid "Could not update label: {}" msgstr "" -#: ui/app.slint:406 +#: ui/app.slint:404 msgid "Could not save label: {}" msgstr "" -#: ui/app.slint:407 +#: ui/app.slint:405 msgid "Could not delete label: {}" msgstr "" -#: ui/app.slint:408 +#: ui/app.slint:406 msgid "Folder created." msgstr "" -#: ui/app.slint:409 +#: ui/app.slint:407 msgid "Folder renamed." msgstr "" -#: ui/app.slint:410 +#: ui/app.slint:408 msgid "Folder deleted." msgstr "" -#: ui/app.slint:411 +#: ui/app.slint:409 msgid "Could not create folder: {}" msgstr "" -#: ui/app.slint:412 +#: ui/app.slint:410 msgid "Could not rename folder: {}" msgstr "" -#: ui/app.slint:413 +#: ui/app.slint:411 msgid "Could not delete folder: {}" msgstr "" -#: ui/app.slint:414 +#: ui/app.slint:412 msgid "Could not close preview: {}" msgstr "" -#: ui/app.slint:415 +#: ui/app.slint:413 msgid "Opened the system print view." msgstr "" -#: ui/app.slint:416 +#: ui/app.slint:414 msgid "Could not print message: {}" msgstr "" -#: ui/app.slint:417 +#: ui/app.slint:415 msgid "Opened the message in your browser." msgstr "" -#: ui/app.slint:418 +#: ui/app.slint:416 msgid "Could not open message in browser: {}" msgstr "" -#: ui/app.slint:419 +#: ui/app.slint:417 msgid "Connect an account before replying or forwarding." msgstr "" -#: ui/app.slint:420 +#: ui/app.slint:418 msgid "Choose a message first." msgstr "" -#: ui/app.slint:421 +#: ui/app.slint:419 msgid "Only drafts can be reopened for editing." msgstr "" -#: ui/app.slint:422 +#: ui/app.slint:420 msgid "Could not open draft: {}" msgstr "" -#: ui/app.slint:423 +#: ui/app.slint:421 msgid "Could not open draft: invalid account identifier" msgstr "" -#: ui/app.slint:424 +#: ui/app.slint:422 msgid "Connected account" msgstr "" -#: ui/app.slint:425 +#: ui/app.slint:423 msgid "Could not open composer: {}" msgstr "" -#: ui/app.slint:426 +#: ui/app.slint:424 msgid "Could not open composer: invalid account identifier" msgstr "" -#: ui/app.slint:431 +#: ui/app.slint:429 msgid "Attach files to this email" msgstr "" -#: ui/app.slint:432 -msgid "The platform host does not provide a document picker." +#: ui/app.slint:430 +msgid "Could not add attachments: {}" msgstr "" -#: ui/app.slint:433 +#: ui/app.slint:431 msgid "Added 1 attachment." msgstr "" -#: ui/app.slint:434 +#: ui/app.slint:432 msgid "Added {} attachments." msgstr "" -#: ui/app.slint:435 +#: ui/app.slint:433 msgid "" "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB " "total limit)." msgstr "" -#: ui/app.slint:436 +#: ui/app.slint:434 msgid "Attachment removed." msgstr "" -#: ui/app.slint:437 +#: ui/app.slint:435 msgid "Choose the account this message should be sent from." msgstr "" -#: ui/app.slint:438 +#: ui/app.slint:436 msgid "Choose a verified sender identity for this message." msgstr "" -#: ui/app.slint:439 +#: ui/app.slint:437 msgid "Connect an account before saving or sending mail." msgstr "" -#: ui/app.slint:440 +#: ui/app.slint:438 msgid "Message queued for delivery (action {})." msgstr "" -#: ui/app.slint:441 +#: ui/app.slint:439 msgid "Sending message…" msgstr "" -#: ui/app.slint:442 +#: ui/app.slint:440 msgid "Message sent." msgstr "" -#: ui/app.slint:443 +#: ui/app.slint:441 msgid "Message was not sent: {} Your draft is preserved." msgstr "" -#: ui/app.slint:444 +#: ui/app.slint:442 msgid "Draft saved locally (#{})." msgstr "" -#: ui/app.slint:445 +#: ui/app.slint:443 msgid "Compose failed: {}" msgstr "" -#: ui/app.slint:446 +#: ui/app.slint:444 msgid "Connect an account before managing email templates." msgstr "" -#: ui/app.slint:447 +#: ui/app.slint:445 msgid "Could not load email templates: {}" msgstr "" -#: ui/app.slint:448 +#: ui/app.slint:446 msgid "Enter a template name." msgstr "" -#: ui/app.slint:449 +#: ui/app.slint:447 msgid "Add a subject or message to the template." msgstr "" -#: ui/app.slint:450 +#: ui/app.slint:448 msgid "Email template saved." msgstr "" -#: ui/app.slint:451 +#: ui/app.slint:449 msgid "Could not save email template: {}" msgstr "" -#: ui/app.slint:452 +#: ui/app.slint:450 msgid "Email template deleted." msgstr "" -#: ui/app.slint:453 +#: ui/app.slint:451 msgid "Could not delete email template: {}" msgstr "" -#: ui/app.slint:454 +#: ui/app.slint:452 msgid "Email template is no longer available." msgstr "" -#: ui/app.slint:455 +#: ui/app.slint:453 msgid "Email template inserted." msgstr "" -#: ui/app.slint:456 +#: ui/app.slint:454 msgid "Email template inserted, but its usage could not be saved: {}" msgstr "" -#: ui/app.slint:457 +#: ui/app.slint:455 msgid "IMAP port must be between 1 and 65535." msgstr "" -#: ui/app.slint:458 +#: ui/app.slint:456 msgid "SMTP port must be between 1 and 65535." msgstr "" -#: ui/app.slint:459 +#: ui/app.slint:457 msgid "Local mail data is unavailable. Retry startup." msgstr "" -#: ui/app.slint:460 +#: ui/app.slint:458 msgid "Discovering JMAP and checking capabilities…" msgstr "" -#: ui/app.slint:461 +#: ui/app.slint:459 msgid "Certificate imported. Connect to verify the server." msgstr "" -#: ui/app.slint:462 +#: ui/app.slint:460 msgid "Could not load account preferences: {}" msgstr "" -#: ui/app.slint:463 +#: ui/app.slint:461 msgid "" "Your edited signature was preserved. Choose No signature to keep it as " "message text before inserting another." msgstr "" -#: ui/app.slint:464 +#: ui/app.slint:462 msgid "Certificate import failed: {}" msgstr "" -#: ui/app.slint:465 +#: ui/app.slint:463 msgid "Paste the PEM certificate into the certificate field." msgstr "" -#: ui/app.slint:466 +#: ui/app.slint:464 msgid "Testing IMAP and SMTP connections…" msgstr "" -#: ui/app.slint:471 +#: ui/app.slint:469 msgid "" "Another browser authorization is already in progress; finish it or choose " "Cancel." msgstr "" -#: ui/app.slint:472 ui/dialogs/settings-dialog.slint:1643 +#: ui/app.slint:470 ui/dialogs/settings-dialog.slint:1643 #: ui/dialogs/settings-dialog.slint:1686 ui/views/onboarding-view.slint:152 msgid "Add app keys in Sign-in settings to enable this provider." msgstr "" -#: ui/app.slint:473 +#: ui/app.slint:471 msgid "Choose Gmail or Outlook OAuth." msgstr "" -#: ui/app.slint:474 +#: ui/app.slint:472 msgid "Complete sign-in in your browser, or choose Cancel here." msgstr "" -#: ui/app.slint:475 +#: ui/app.slint:473 msgid "Cancelling browser sign-in…" msgstr "" -#: ui/app.slint:476 +#: ui/app.slint:474 msgid "Finishing browser sign-in…" msgstr "" -#: ui/app.slint:477 +#: ui/app.slint:475 msgid "Could not use that browser address: {}" msgstr "" -#: ui/app.slint:478 +#: ui/app.slint:476 msgid "Calendar core is unavailable." msgstr "" -#: ui/app.slint:479 +#: ui/app.slint:477 msgid "Account no longer exists." msgstr "" -#: ui/app.slint:480 +#: ui/app.slint:478 msgid "Authorize calendar access in your browser." msgstr "" -#: ui/app.slint:481 +#: ui/app.slint:479 msgid "OAuth app keys saved." msgstr "" -#: ui/app.slint:482 +#: ui/app.slint:480 msgid "{} OAuth settings saved." msgstr "" -#: ui/app.slint:483 +#: ui/app.slint:481 msgid "OAuth settings failed: {}" msgstr "" -#: ui/app.slint:484 +#: ui/app.slint:482 msgid "Mail refresh failed: {}" msgstr "" -#: ui/app.slint:485 +#: ui/app.slint:483 msgid "Mail filter failed: {}" msgstr "" -#: ui/app.slint:486 +#: ui/app.slint:484 msgid "Mail pagination failed: {}" msgstr "" -#: ui/app.slint:487 +#: ui/app.slint:485 msgid "Could not mark message as read: {}" msgstr "" -#: ui/app.slint:488 ui/views/mail-reading-pane.slint:223 +#: ui/app.slint:486 ui/views/mail-reading-pane.slint:223 #: ui/views/mail-reading-pane.slint:1162 msgid "Loading message…" msgstr "" -#: ui/app.slint:494 ui/components/workspace-navigation.slint:551 +#: ui/app.slint:492 ui/components/workspace-navigation.slint:551 #: ui/tray.slint:9 ui/tray.slint:10 msgid "Flectar Mail" msgstr "" -#: ui/app.slint:1065 ui/app.slint:1078 +#: ui/app.slint:1063 ui/app.slint:1076 msgid "Global Labels (local only)" msgstr "" -#: ui/app.slint:1938 +#: ui/app.slint:1936 msgid "Empty Trash?" msgstr "" -#: ui/app.slint:1938 ui/views/files-view.slint:538 +#: ui/app.slint:1936 ui/views/files-view.slint:538 msgid "Delete permanently?" msgstr "" -#: ui/app.slint:1942 +#: ui/app.slint:1940 msgid "" "Permanently delete all messages in Trash across every connected account?" msgstr "" -#: ui/app.slint:1943 +#: ui/app.slint:1941 msgid "Permanently delete all messages in this account’s Trash?" msgstr "" -#: ui/app.slint:1945 +#: ui/app.slint:1943 msgid "Permanently delete the selected conversations from Trash?" msgstr "" -#: ui/app.slint:1946 +#: ui/app.slint:1944 msgid "Permanently delete this conversation from Trash?" msgstr "" -#: ui/app.slint:1947 ui/components/calendar.slint:926 +#: ui/app.slint:1945 ui/components/calendar.slint:926 msgid "This action cannot be undone." msgstr "" -#: ui/app.slint:1948 +#: ui/app.slint:1946 msgid "Empty Trash" msgstr "" -#: ui/app.slint:1948 ui/views/files-view.slint:620 +#: ui/app.slint:1946 ui/views/files-view.slint:620 msgid "Delete permanently" msgstr "" -#: ui/app.slint:2111 +#: ui/app.slint:2109 msgid "Mailbox startup failed" msgstr "" -#: ui/app.slint:2117 +#: ui/app.slint:2115 msgid "Flectar Mail could not open local storage" msgstr "" -#: ui/app.slint:2130 +#: ui/app.slint:2128 msgid "Diagnostic ID: {}" msgstr "" -#: ui/app.slint:2133 +#: ui/app.slint:2131 msgid "Diagnostic ID {}" msgstr "" -#: ui/app.slint:2138 +#: ui/app.slint:2136 msgid "Retry" msgstr "" -#: ui/app.slint:2146 ui/components/copyable-diagnostic.slint:38 +#: ui/app.slint:2144 ui/components/copyable-diagnostic.slint:38 msgid "Copy diagnostics" msgstr "" diff --git a/lang/ru/LC_MESSAGES/flectar-mail.po b/lang/ru/LC_MESSAGES/flectar-mail.po index 70b2917f..d725fed3 100644 --- a/lang/ru/LC_MESSAGES/flectar-mail.po +++ b/lang/ru/LC_MESSAGES/flectar-mail.po @@ -188,7 +188,7 @@ msgstr "История переписки доступна" msgid "New address-book entry" msgstr "Новая запись в адресной книге" -#: ui/app.slint:121 ui/app.slint:847 ui/components/composer-fields.slint:66 +#: ui/app.slint:121 ui/app.slint:845 ui/components/composer-fields.slint:66 #: ui/dialogs/compose-dialog.slint:37 msgid "No connected account" msgstr "Нет подключённой учётной записи" @@ -204,11 +204,11 @@ msgstr[2] "{n} взаимодействий" msgid "Last contacted {} {}, {}" msgstr "Последний контакт: {1} {0} {2}" -#: ui/app.slint:128 ui/app.slint:361 +#: ui/app.slint:128 ui/app.slint:359 msgid "Loading local data…" msgstr "Загрузка локальных данных…" -#: ui/app.slint:129 ui/app.slint:362 +#: ui/app.slint:129 ui/app.slint:360 msgid "Local data ready." msgstr "Локальные данные готовы." @@ -670,231 +670,223 @@ msgid "Choose where to export the database snapshot" msgstr "Выберите место для экспорта снимка базы данных" #: ui/app.slint:289 -msgid "The platform host does not provide document export." -msgstr "Платформа не предоставляет экспорт документов." - -#: ui/app.slint:290 -msgid "The platform host does not provide document import." -msgstr "Платформа не предоставляет импорт документов." - -#: ui/app.slint:291 msgid "Backup exported. Passwords and OAuth tokens were not included." msgstr "Резервная копия экспортирована. Пароли и токены OAuth не включены." -#: ui/app.slint:292 +#: ui/app.slint:290 msgid "Could not export backup: {}" msgstr "Не удалось экспортировать резервную копию: {}" -#: ui/app.slint:293 +#: ui/app.slint:291 msgid "Local data storage is unavailable. Retry startup." msgstr "Локальное хранилище данных недоступно. Повторите запуск." -#: ui/app.slint:294 +#: ui/app.slint:292 msgid "Creating verified database snapshot…" msgstr "Создание проверенного снимка базы данных…" -#: ui/app.slint:295 +#: ui/app.slint:293 msgid "Backup imported. All {} account configurations already existed." msgstr "" "Резервная копия импортирована. Все настройки учётных записей уже имелись " "({})." -#: ui/app.slint:296 +#: ui/app.slint:294 msgid "" "Imported {} account configurations. Open Accounts and sign in to each one." msgstr "" "Импортировано настроек учётных записей: {}. Откройте раздел «Учётные записи» " "и войдите в каждую." -#: ui/app.slint:297 +#: ui/app.slint:295 msgid "Could not import backup: {}" msgstr "Не удалось импортировать резервную копию: {}" -#: ui/app.slint:298 +#: ui/app.slint:296 msgid "All local Flectar Mail data was deleted." msgstr "Все локальные данные Flectar Mail удалены." -#: ui/app.slint:299 +#: ui/app.slint:297 msgid "Could not delete all local data: {}" msgstr "Не удалось удалить все локальные данные: {}" -#: ui/app.slint:300 +#: ui/app.slint:298 msgid "Deleting local accounts and cached data…" msgstr "Удаление локальных учётных записей и кешированных данных…" -#: ui/app.slint:305 +#: ui/app.slint:303 msgid "Background mail refresh failed: {}" msgstr "Фоновое обновление почты не удалось: {}" -#: ui/app.slint:306 +#: ui/app.slint:304 msgid "No conversations in this folder" msgstr "В этой папке нет переписок" -#: ui/app.slint:307 +#: ui/app.slint:305 msgid "No conversations match this search" msgstr "Нет переписок, соответствующих этому поиску" -#: ui/app.slint:308 +#: ui/app.slint:306 msgid "Mail core is ready for account synchronization and message actions." msgstr "" "Почтовое ядро готово к синхронизации учётных записей и действиям с " "сообщениями." -#: ui/app.slint:309 +#: ui/app.slint:307 msgid "Large message rendered in compatibility mode." msgstr "Большое сообщение отображено в режиме совместимости." -#: ui/app.slint:310 +#: ui/app.slint:308 msgid "Software email render failed: {}" msgstr "Ошибка программной отрисовки письма: {}" -#: ui/app.slint:311 +#: ui/app.slint:309 msgid "Rendered with software fallback after GPU error: {}" msgstr "Отображено программно после ошибки GPU: {}" -#: ui/app.slint:312 +#: ui/app.slint:310 msgid "Blitz render failed (GPU: {}; software: {})" msgstr "Ошибка отрисовки Blitz (GPU: {}; программно: {})" -#: ui/app.slint:317 +#: ui/app.slint:315 msgid "Add an email address, then save the contact." msgstr "Добавьте адрес электронной почты, затем сохраните контакт." -#: ui/app.slint:318 +#: ui/app.slint:316 msgid "Enter a valid email address before saving." msgstr "Введите корректный адрес электронной почты перед сохранением." -#: ui/app.slint:319 +#: ui/app.slint:317 msgid "Could not save: contact storage is unavailable." msgstr "Не удалось сохранить: хранилище контактов недоступно." -#: ui/app.slint:320 +#: ui/app.slint:318 msgid "Could not save contact: {}" msgstr "Не удалось сохранить контакт: {}" -#: ui/app.slint:321 +#: ui/app.slint:319 msgid "Contact saved." msgstr "Контакт сохранён." -#: ui/app.slint:322 +#: ui/app.slint:320 msgid "Saved to contacts." msgstr "Сохранено в контактах." -#: ui/app.slint:323 +#: ui/app.slint:321 msgid "Could not delete: contact storage is unavailable." msgstr "Не удалось удалить: хранилище контактов недоступно." -#: ui/app.slint:324 +#: ui/app.slint:322 msgid "Could not delete contact: {}" msgstr "Не удалось удалить контакт: {}" -#: ui/app.slint:325 +#: ui/app.slint:323 msgid "Contact deleted." msgstr "Контакт удалён." -#: ui/app.slint:326 +#: ui/app.slint:324 msgid "Could not load contacts: {}" msgstr "Не удалось загрузить контакты: {}" -#: ui/app.slint:327 +#: ui/app.slint:325 msgid "Local contact storage is still starting…" msgstr "Локальное хранилище контактов ещё запускается…" -#: ui/app.slint:328 +#: ui/app.slint:326 msgid "Loading contacts…" msgstr "Загрузка контактов…" -#: ui/app.slint:333 ui/components/calendar-agenda.slint:233 +#: ui/app.slint:331 ui/components/calendar-agenda.slint:233 msgid "Local calendar" msgstr "Локальный календарь" -#: ui/app.slint:334 ui/components/calendar.slint:271 +#: ui/app.slint:332 ui/components/calendar.slint:271 #: ui/components/calendar.slint:319 ui/components/calendar.slint:434 msgid "Calendar" msgstr "Календарь" -#: ui/app.slint:335 +#: ui/app.slint:333 msgid "Could not load calendar: {}" msgstr "Не удалось загрузить календарь: {}" -#: ui/app.slint:336 +#: ui/app.slint:334 msgid "Calendar shown and syncing." msgstr "Календарь отображён и синхронизируется." -#: ui/app.slint:337 +#: ui/app.slint:335 msgid "Calendar hidden and sync paused." msgstr "Календарь скрыт, синхронизация приостановлена." -#: ui/app.slint:338 +#: ui/app.slint:336 msgid "Calendar update failed: {}" msgstr "Обновление календаря не удалось: {}" -#: ui/app.slint:339 +#: ui/app.slint:337 msgid "Only events created in Flectar Mail can be edited." msgstr "Редактировать можно только события, созданные в Flectar Mail." -#: ui/app.slint:340 +#: ui/app.slint:338 msgid "Only events created in Flectar Mail can be deleted." msgstr "Удалять можно только события, созданные в Flectar Mail." -#: ui/app.slint:341 +#: ui/app.slint:339 msgid "Calendar storage is unavailable." msgstr "Хранилище календаря недоступно." -#: ui/app.slint:342 +#: ui/app.slint:340 msgid "Could not delete event: {}" msgstr "Не удалось удалить событие: {}" -#: ui/app.slint:343 +#: ui/app.slint:341 msgid "Event deleted, but refresh failed: {}" msgstr "Событие удалено, но обновление не удалось: {}" -#: ui/app.slint:344 +#: ui/app.slint:342 msgid "Event deleted." msgstr "Событие удалено." -#: ui/app.slint:345 +#: ui/app.slint:343 msgid "Use YYYY-MM-DD and a 24-hour time such as 09:30." msgstr "Используйте формат ГГГГ-ММ-ДД и 24-часовое время, например 09:30." -#: ui/app.slint:346 +#: ui/app.slint:344 msgid "Event duration must be a number of minutes." msgstr "Продолжительность события должна быть указана в минутах." -#: ui/app.slint:347 +#: ui/app.slint:345 msgid "Add a title and choose a duration from 15 to 1440 minutes." msgstr "Добавьте название и выберите продолжительность от 15 до 1440 минут." -#: ui/app.slint:348 +#: ui/app.slint:346 msgid "That local time does not exist in the current time zone." msgstr "Такое местное время не существует в текущем часовом поясе." -#: ui/app.slint:349 +#: ui/app.slint:347 msgid "Could not save event: {}" msgstr "Не удалось сохранить событие: {}" -#: ui/app.slint:350 +#: ui/app.slint:348 msgid "Event saved, but refresh failed: {}" msgstr "Событие сохранено, но обновление не удалось: {}" -#: ui/app.slint:351 +#: ui/app.slint:349 msgid "Event saved. Provider sync will run when enabled." msgstr "" "Событие сохранено. Синхронизация с провайдером выполнится при включении." -#: ui/app.slint:356 +#: ui/app.slint:354 msgid "Email resource render failed: {}" msgstr "Ошибка отрисовки ресурса письма: {}" -#: ui/app.slint:357 +#: ui/app.slint:355 msgid "Message refresh failed: {}" msgstr "Обновление сообщения не удалось: {}" -#: ui/app.slint:358 +#: ui/app.slint:356 msgid "Message load failed: {}" msgstr "Загрузка сообщения не удалась: {}" -#: ui/app.slint:359 +#: ui/app.slint:357 msgid "" "Your local mailbox could not be opened. No account operation was started. " "Retry, or copy the diagnostics for support." @@ -903,258 +895,258 @@ msgstr "" "запускалась. Повторите попытку или скопируйте диагностику для службы " "поддержки." -#: ui/app.slint:360 +#: ui/app.slint:358 msgid "Local storage startup failed." msgstr "Запуск локального хранилища не удался." -#: ui/app.slint:367 +#: ui/app.slint:365 msgid "Could not identify the dragged account." msgstr "Не удалось определить перетаскиваемую учётную запись." -#: ui/app.slint:368 +#: ui/app.slint:366 msgid "Account ordering requires local mail data." msgstr "Для упорядочивания учётных записей требуются локальные данные почты." -#: ui/app.slint:369 +#: ui/app.slint:367 msgid "Could not find the account to reorder." msgstr "Не удалось найти учётную запись для перестановки." -#: ui/app.slint:370 +#: ui/app.slint:368 msgid "Saving account order…" msgstr "Сохранение порядка учётных записей…" -#: ui/app.slint:371 +#: ui/app.slint:369 msgid "Sent copy settings require local mail data." msgstr "Для настройки копий отправленных писем нужны локальные данные почты." -#: ui/app.slint:372 +#: ui/app.slint:370 msgid "Updating Sent copy settings…" msgstr "Обновление настроек копий отправленных писем…" -#: ui/app.slint:373 +#: ui/app.slint:371 msgid "Sent copy settings updated." msgstr "Настройки копий отправленных писем обновлены." -#: ui/app.slint:374 +#: ui/app.slint:372 msgid "Could not update Sent copy settings: {}" msgstr "Не удалось обновить настройки копий отправленных писем: {}" -#: ui/app.slint:375 +#: ui/app.slint:373 msgid "Mail history requires local mail data." msgstr "Для истории почты требуются локальные данные почты." -#: ui/app.slint:376 +#: ui/app.slint:374 msgid "Updating mail history…" msgstr "Обновление истории почты…" -#: ui/app.slint:377 +#: ui/app.slint:375 msgid "Remote images are unavailable in this minimal build." msgstr "Внешние изображения недоступны в этой минимальной сборке." -#: ui/app.slint:378 +#: ui/app.slint:376 msgid "Could not save remote-image setting: {}" msgstr "Не удалось сохранить настройку внешних изображений: {}" -#: ui/app.slint:379 +#: ui/app.slint:377 msgid "Remote sender icons unavailable: {}" msgstr "Внешние значки отправителей недоступны: {}" -#: ui/app.slint:380 +#: ui/app.slint:378 msgid "Remote images unavailable: {}" msgstr "Внешние изображения недоступны: {}" -#: ui/app.slint:381 +#: ui/app.slint:379 msgid "Remote body images and sender favicons enabled." msgstr "" "Загрузка внешних изображений в письмах и значков отправителей включена." -#: ui/app.slint:382 +#: ui/app.slint:380 msgid "Remote images blocked." msgstr "Внешние изображения заблокированы." -#: ui/app.slint:383 +#: ui/app.slint:381 msgid "Could not refresh message images: {}" msgstr "Не удалось обновить изображения сообщения: {}" -#: ui/app.slint:384 +#: ui/app.slint:382 msgid "Remote-image loading is disabled." msgstr "Загрузка внешних изображений отключена." -#: ui/app.slint:385 +#: ui/app.slint:383 msgid "Could not load message images: {}" msgstr "Не удалось загрузить изображения сообщения: {}" -#: ui/app.slint:386 +#: ui/app.slint:384 msgid "Sync finished, refresh failed: {}" msgstr "Синхронизация завершена, обновление не удалось: {}" -#: ui/app.slint:387 +#: ui/app.slint:385 msgid "Sync complete." msgstr "Синхронизация завершена." -#: ui/app.slint:388 +#: ui/app.slint:386 msgid "Sync failed: {}" msgstr "Синхронизация не удалась: {}" -#: ui/app.slint:389 +#: ui/app.slint:387 msgid "Local mail data is unavailable; preview data is read-only." msgstr "" "Локальные данные почты недоступны; данные предпросмотра доступны только для " "чтения." -#: ui/app.slint:390 +#: ui/app.slint:388 msgid "Synchronizing accounts…" msgstr "Синхронизация учётных записей…" -#: ui/app.slint:395 +#: ui/app.slint:393 msgid "Message action completed." msgstr "Действие с сообщением выполнено." -#: ui/app.slint:396 +#: ui/app.slint:394 msgid "" "Permanent deletion queued. Mail will disappear after the server confirms." msgstr "" "Безвозвратное удаление запланировано. Письма исчезнут после подтверждения " "сервера." -#: ui/app.slint:397 +#: ui/app.slint:395 msgid "Message action failed: {}" msgstr "Действие с сообщением не удалось: {}" -#: ui/app.slint:398 +#: ui/app.slint:396 msgid "Message action failed: message is no longer available" msgstr "Действие с сообщением не удалось: сообщение больше недоступно" -#: ui/app.slint:399 +#: ui/app.slint:397 msgid "Label added." msgstr "Метка добавлена." -#: ui/app.slint:400 +#: ui/app.slint:398 msgid "Label removed." msgstr "Метка удалена." -#: ui/app.slint:401 +#: ui/app.slint:399 msgid "Label created and added." msgstr "Метка создана и добавлена." -#: ui/app.slint:402 +#: ui/app.slint:400 msgid "Label created." msgstr "Метка создана." -#: ui/app.slint:403 +#: ui/app.slint:401 msgid "Label updated." msgstr "Метка обновлена." -#: ui/app.slint:404 +#: ui/app.slint:402 msgid "Label deleted." msgstr "Метка удалена." -#: ui/app.slint:405 +#: ui/app.slint:403 msgid "Could not update label: {}" msgstr "Не удалось обновить метку: {}" -#: ui/app.slint:406 +#: ui/app.slint:404 msgid "Could not save label: {}" msgstr "Не удалось сохранить метку: {}" -#: ui/app.slint:407 +#: ui/app.slint:405 msgid "Could not delete label: {}" msgstr "Не удалось удалить метку: {}" -#: ui/app.slint:408 +#: ui/app.slint:406 msgid "Folder created." msgstr "Папка создана." -#: ui/app.slint:409 +#: ui/app.slint:407 msgid "Folder renamed." msgstr "Папка переименована." -#: ui/app.slint:410 +#: ui/app.slint:408 msgid "Folder deleted." msgstr "Папка удалена." -#: ui/app.slint:411 +#: ui/app.slint:409 msgid "Could not create folder: {}" msgstr "Не удалось создать папку: {}" -#: ui/app.slint:412 +#: ui/app.slint:410 msgid "Could not rename folder: {}" msgstr "Не удалось переименовать папку: {}" -#: ui/app.slint:413 +#: ui/app.slint:411 msgid "Could not delete folder: {}" msgstr "Не удалось удалить папку: {}" -#: ui/app.slint:414 +#: ui/app.slint:412 msgid "Could not close preview: {}" msgstr "Не удалось закрыть предпросмотр: {}" -#: ui/app.slint:415 +#: ui/app.slint:413 msgid "Opened the system print view." msgstr "Открыт системный просмотр печати." -#: ui/app.slint:416 +#: ui/app.slint:414 msgid "Could not print message: {}" msgstr "Не удалось напечатать сообщение: {}" -#: ui/app.slint:417 +#: ui/app.slint:415 msgid "Opened the message in your browser." msgstr "Сообщение открыто в браузере." -#: ui/app.slint:418 +#: ui/app.slint:416 msgid "Could not open message in browser: {}" msgstr "Не удалось открыть сообщение в браузере: {}" -#: ui/app.slint:419 +#: ui/app.slint:417 msgid "Connect an account before replying or forwarding." msgstr "Подключите учётную запись перед ответом или пересылкой." -#: ui/app.slint:420 +#: ui/app.slint:418 msgid "Choose a message first." msgstr "Сначала выберите сообщение." -#: ui/app.slint:421 +#: ui/app.slint:419 msgid "Only drafts can be reopened for editing." msgstr "Только черновики можно снова открыть для редактирования." -#: ui/app.slint:422 +#: ui/app.slint:420 msgid "Could not open draft: {}" msgstr "Не удалось открыть черновик: {}" -#: ui/app.slint:423 +#: ui/app.slint:421 msgid "Could not open draft: invalid account identifier" msgstr "Не удалось открыть черновик: неверный идентификатор учётной записи" -#: ui/app.slint:424 +#: ui/app.slint:422 msgid "Connected account" msgstr "Подключённая учётная запись" -#: ui/app.slint:425 +#: ui/app.slint:423 msgid "Could not open composer: {}" msgstr "Не удалось открыть редактор письма: {}" -#: ui/app.slint:426 +#: ui/app.slint:424 msgid "Could not open composer: invalid account identifier" msgstr "" "Не удалось открыть редактор письма: неверный идентификатор учётной записи" -#: ui/app.slint:431 +#: ui/app.slint:429 msgid "Attach files to this email" msgstr "Прикрепить файлы к этому письму" -#: ui/app.slint:432 -msgid "The platform host does not provide a document picker." -msgstr "Платформа не предоставляет выбор документов." +#: ui/app.slint:430 +msgid "Could not add attachments: {}" +msgstr "Не удалось добавить вложения: {}" -#: ui/app.slint:433 +#: ui/app.slint:431 msgid "Added 1 attachment." msgstr "Добавлено одно вложение." -#: ui/app.slint:434 +#: ui/app.slint:432 msgid "Added {} attachments." msgstr "Добавлено вложений: {}." -#: ui/app.slint:435 +#: ui/app.slint:433 msgid "" "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB " "total limit)." @@ -1162,116 +1154,116 @@ msgstr "" "Добавлено вложений: {}. Пропущено: {} (дубликаты, недоступные для чтения или " "превышающие общий лимит 25 МБ)." -#: ui/app.slint:436 +#: ui/app.slint:434 msgid "Attachment removed." msgstr "Вложение удалено." -#: ui/app.slint:437 +#: ui/app.slint:435 msgid "Choose the account this message should be sent from." msgstr "Выберите учётную запись, с которой нужно отправить это сообщение." -#: ui/app.slint:438 +#: ui/app.slint:436 msgid "Choose a verified sender identity for this message." msgstr "Выберите подтверждённые данные отправителя для этого письма." -#: ui/app.slint:439 +#: ui/app.slint:437 msgid "Connect an account before saving or sending mail." msgstr "Подключите учётную запись перед сохранением или отправкой почты." -#: ui/app.slint:440 +#: ui/app.slint:438 msgid "Message queued for delivery (action {})." msgstr "Сообщение поставлено в очередь на отправку (действие {})." -#: ui/app.slint:441 +#: ui/app.slint:439 msgid "Sending message…" msgstr "Отправка сообщения…" -#: ui/app.slint:442 +#: ui/app.slint:440 msgid "Message sent." msgstr "Сообщение отправлено." -#: ui/app.slint:443 +#: ui/app.slint:441 msgid "Message was not sent: {} Your draft is preserved." msgstr "Сообщение не отправлено: {} Ваш черновик сохранён." -#: ui/app.slint:444 +#: ui/app.slint:442 msgid "Draft saved locally (#{})." msgstr "Черновик сохранён локально (#{})." -#: ui/app.slint:445 +#: ui/app.slint:443 msgid "Compose failed: {}" msgstr "Создание письма не удалось: {}" -#: ui/app.slint:446 +#: ui/app.slint:444 msgid "Connect an account before managing email templates." msgstr "Подключите учётную запись перед управлением шаблонами писем." -#: ui/app.slint:447 +#: ui/app.slint:445 msgid "Could not load email templates: {}" msgstr "Не удалось загрузить шаблоны писем: {}" -#: ui/app.slint:448 +#: ui/app.slint:446 msgid "Enter a template name." msgstr "Введите имя шаблона." -#: ui/app.slint:449 +#: ui/app.slint:447 msgid "Add a subject or message to the template." msgstr "Добавьте тему или сообщение в шаблон." -#: ui/app.slint:450 +#: ui/app.slint:448 msgid "Email template saved." msgstr "Шаблон письма сохранён." -#: ui/app.slint:451 +#: ui/app.slint:449 msgid "Could not save email template: {}" msgstr "Не удалось сохранить шаблон письма: {}" -#: ui/app.slint:452 +#: ui/app.slint:450 msgid "Email template deleted." msgstr "Шаблон письма удалён." -#: ui/app.slint:453 +#: ui/app.slint:451 msgid "Could not delete email template: {}" msgstr "Не удалось удалить шаблон письма: {}" -#: ui/app.slint:454 +#: ui/app.slint:452 msgid "Email template is no longer available." msgstr "Шаблон письма больше недоступен." -#: ui/app.slint:455 +#: ui/app.slint:453 msgid "Email template inserted." msgstr "Шаблон письма вставлен." -#: ui/app.slint:456 +#: ui/app.slint:454 msgid "Email template inserted, but its usage could not be saved: {}" msgstr "" "Шаблон письма вставлен, но сведения о его использовании не сохранены: {}" -#: ui/app.slint:457 +#: ui/app.slint:455 msgid "IMAP port must be between 1 and 65535." msgstr "Порт IMAP должен быть в диапазоне от 1 до 65535." -#: ui/app.slint:458 +#: ui/app.slint:456 msgid "SMTP port must be between 1 and 65535." msgstr "Порт SMTP должен быть в диапазоне от 1 до 65535." -#: ui/app.slint:459 +#: ui/app.slint:457 msgid "Local mail data is unavailable. Retry startup." msgstr "Локальные данные почты недоступны. Повторите запуск." -#: ui/app.slint:460 +#: ui/app.slint:458 msgid "Discovering JMAP and checking capabilities…" msgstr "Обнаружение JMAP и проверка возможностей…" -#: ui/app.slint:461 +#: ui/app.slint:459 msgid "Certificate imported. Connect to verify the server." msgstr "Сертификат импортирован. Подключитесь для проверки сервера." -#: ui/app.slint:462 +#: ui/app.slint:460 msgid "Could not load account preferences: {}" msgstr "Не удалось загрузить настройки учётной записи: {}" -#: ui/app.slint:463 +#: ui/app.slint:461 msgid "" "Your edited signature was preserved. Choose No signature to keep it as " "message text before inserting another." @@ -1279,19 +1271,19 @@ msgstr "" "Ваша изменённая подпись сохранена. Выберите «Без подписи», чтобы оставить её " "как текст сообщения перед вставкой другой." -#: ui/app.slint:464 +#: ui/app.slint:462 msgid "Certificate import failed: {}" msgstr "Импорт сертификата не удался: {}" -#: ui/app.slint:465 +#: ui/app.slint:463 msgid "Paste the PEM certificate into the certificate field." msgstr "Вставьте сертификат PEM в поле сертификата." -#: ui/app.slint:466 +#: ui/app.slint:464 msgid "Testing IMAP and SMTP connections…" msgstr "Проверка подключений IMAP и SMTP…" -#: ui/app.slint:471 +#: ui/app.slint:469 msgid "" "Another browser authorization is already in progress; finish it or choose " "Cancel." @@ -1299,147 +1291,147 @@ msgstr "" "Другая авторизация в браузере уже выполняется; завершите её или выберите " "«Отмена»." -#: ui/app.slint:472 ui/dialogs/settings-dialog.slint:1643 +#: ui/app.slint:470 ui/dialogs/settings-dialog.slint:1643 #: ui/dialogs/settings-dialog.slint:1686 ui/views/onboarding-view.slint:152 msgid "Add app keys in Sign-in settings to enable this provider." msgstr "" "Добавьте ключи приложения в настройках входа, чтобы включить этого " "провайдера." -#: ui/app.slint:473 +#: ui/app.slint:471 msgid "Choose Gmail or Outlook OAuth." msgstr "Выберите OAuth Gmail или Outlook." -#: ui/app.slint:474 +#: ui/app.slint:472 msgid "Complete sign-in in your browser, or choose Cancel here." msgstr "Завершите вход в браузере или выберите «Отмена» здесь." -#: ui/app.slint:475 +#: ui/app.slint:473 msgid "Cancelling browser sign-in…" msgstr "Отмена входа через браузер…" -#: ui/app.slint:476 +#: ui/app.slint:474 msgid "Finishing browser sign-in…" msgstr "Завершение входа через браузер…" -#: ui/app.slint:477 +#: ui/app.slint:475 msgid "Could not use that browser address: {}" msgstr "Не удалось использовать этот адрес браузера: {}" -#: ui/app.slint:478 +#: ui/app.slint:476 msgid "Calendar core is unavailable." msgstr "Ядро календаря недоступно." -#: ui/app.slint:479 +#: ui/app.slint:477 msgid "Account no longer exists." msgstr "Учётная запись больше не существует." -#: ui/app.slint:480 +#: ui/app.slint:478 msgid "Authorize calendar access in your browser." msgstr "Авторизуйте доступ к календарю в браузере." -#: ui/app.slint:481 +#: ui/app.slint:479 msgid "OAuth app keys saved." msgstr "Ключи приложения OAuth сохранены." -#: ui/app.slint:482 +#: ui/app.slint:480 msgid "{} OAuth settings saved." msgstr "Настройки OAuth {} сохранены." -#: ui/app.slint:483 +#: ui/app.slint:481 msgid "OAuth settings failed: {}" msgstr "Настройки OAuth не удались: {}" -#: ui/app.slint:484 +#: ui/app.slint:482 msgid "Mail refresh failed: {}" msgstr "Обновление почты не удалось: {}" -#: ui/app.slint:485 +#: ui/app.slint:483 msgid "Mail filter failed: {}" msgstr "Фильтрация почты не удалась: {}" -#: ui/app.slint:486 +#: ui/app.slint:484 msgid "Mail pagination failed: {}" msgstr "Постраничная навигация по почте не удалась: {}" -#: ui/app.slint:487 +#: ui/app.slint:485 msgid "Could not mark message as read: {}" msgstr "Не удалось отметить сообщение как прочитанное: {}" -#: ui/app.slint:488 ui/views/mail-reading-pane.slint:223 +#: ui/app.slint:486 ui/views/mail-reading-pane.slint:223 #: ui/views/mail-reading-pane.slint:1162 msgid "Loading message…" msgstr "Загрузка сообщения…" -#: ui/app.slint:494 ui/components/workspace-navigation.slint:551 +#: ui/app.slint:492 ui/components/workspace-navigation.slint:551 #: ui/tray.slint:9 ui/tray.slint:10 msgid "Flectar Mail" msgstr "Flectar Mail" -#: ui/app.slint:1065 ui/app.slint:1078 +#: ui/app.slint:1063 ui/app.slint:1076 msgid "Global Labels (local only)" msgstr "Глобальные метки (только локально)" -#: ui/app.slint:1938 +#: ui/app.slint:1936 msgid "Empty Trash?" msgstr "Очистить корзину?" -#: ui/app.slint:1938 ui/views/files-view.slint:538 +#: ui/app.slint:1936 ui/views/files-view.slint:538 msgid "Delete permanently?" msgstr "Удалить безвозвратно?" -#: ui/app.slint:1942 +#: ui/app.slint:1940 msgid "" "Permanently delete all messages in Trash across every connected account?" msgstr "" "Безвозвратно удалить все письма из корзины во всех подключённых учётных " "записях?" -#: ui/app.slint:1943 +#: ui/app.slint:1941 msgid "Permanently delete all messages in this account’s Trash?" msgstr "Безвозвратно удалить все письма из корзины этой учётной записи?" -#: ui/app.slint:1945 +#: ui/app.slint:1943 msgid "Permanently delete the selected conversations from Trash?" msgstr "Безвозвратно удалить выбранные переписки из корзины?" -#: ui/app.slint:1946 +#: ui/app.slint:1944 msgid "Permanently delete this conversation from Trash?" msgstr "Безвозвратно удалить эту переписку из корзины?" -#: ui/app.slint:1947 ui/components/calendar.slint:926 +#: ui/app.slint:1945 ui/components/calendar.slint:926 msgid "This action cannot be undone." msgstr "Это действие нельзя отменить." -#: ui/app.slint:1948 +#: ui/app.slint:1946 msgid "Empty Trash" msgstr "Очистить корзину" -#: ui/app.slint:1948 ui/views/files-view.slint:620 +#: ui/app.slint:1946 ui/views/files-view.slint:620 msgid "Delete permanently" msgstr "Удалить безвозвратно" -#: ui/app.slint:2111 +#: ui/app.slint:2109 msgid "Mailbox startup failed" msgstr "Запуск почтового ящика не удался" -#: ui/app.slint:2117 +#: ui/app.slint:2115 msgid "Flectar Mail could not open local storage" msgstr "Flectar Mail не смог открыть локальное хранилище" -#: ui/app.slint:2130 +#: ui/app.slint:2128 msgid "Diagnostic ID: {}" msgstr "Идентификатор диагностики: {}" -#: ui/app.slint:2133 +#: ui/app.slint:2131 msgid "Diagnostic ID {}" msgstr "Идентификатор диагностики {}" -#: ui/app.slint:2138 +#: ui/app.slint:2136 msgid "Retry" msgstr "Повторить" -#: ui/app.slint:2146 ui/components/copyable-diagnostic.slint:38 +#: ui/app.slint:2144 ui/components/copyable-diagnostic.slint:38 msgid "Copy diagnostics" msgstr "Копировать диагностику" diff --git a/lang/tr/LC_MESSAGES/flectar-mail.po b/lang/tr/LC_MESSAGES/flectar-mail.po index 29f22342..10a547b3 100644 --- a/lang/tr/LC_MESSAGES/flectar-mail.po +++ b/lang/tr/LC_MESSAGES/flectar-mail.po @@ -184,7 +184,7 @@ msgstr "Konuşma geçmişi mevcut" msgid "New address-book entry" msgstr "Yeni adres defteri kaydı" -#: ui/app.slint:121 ui/app.slint:847 ui/components/composer-fields.slint:66 +#: ui/app.slint:121 ui/app.slint:845 ui/components/composer-fields.slint:66 #: ui/dialogs/compose-dialog.slint:37 msgid "No connected account" msgstr "Bağlı hesap yok" @@ -199,11 +199,11 @@ msgstr[1] "{n} etkileşim" msgid "Last contacted {} {}, {}" msgstr "Son iletişim: {1} {0} {2}" -#: ui/app.slint:128 ui/app.slint:361 +#: ui/app.slint:128 ui/app.slint:359 msgid "Loading local data…" msgstr "Yerel veriler yükleniyor…" -#: ui/app.slint:129 ui/app.slint:362 +#: ui/app.slint:129 ui/app.slint:360 msgid "Local data ready." msgstr "Yerel veriler hazır." @@ -666,227 +666,219 @@ msgid "Choose where to export the database snapshot" msgstr "Veritabanı anlık görüntüsünün nereye aktarılacağını seçin" #: ui/app.slint:289 -msgid "The platform host does not provide document export." -msgstr "Platform ana bilgisayarı belge dışa aktarma sağlamıyor." - -#: ui/app.slint:290 -msgid "The platform host does not provide document import." -msgstr "Platform ana bilgisayarı belge içe aktarma sağlamıyor." - -#: ui/app.slint:291 msgid "Backup exported. Passwords and OAuth tokens were not included." msgstr "Yedek dışa aktarıldı. Parolalar ve OAuth belirteçleri dahil edilmedi." -#: ui/app.slint:292 +#: ui/app.slint:290 msgid "Could not export backup: {}" msgstr "Yedek dışa aktarılamadı: {}" -#: ui/app.slint:293 +#: ui/app.slint:291 msgid "Local data storage is unavailable. Retry startup." msgstr "Yerel veri deposu kullanılamıyor. Başlatmayı yeniden deneyin." -#: ui/app.slint:294 +#: ui/app.slint:292 msgid "Creating verified database snapshot…" msgstr "Doğrulanmış veritabanı anlık görüntüsü oluşturuluyor…" -#: ui/app.slint:295 +#: ui/app.slint:293 msgid "Backup imported. All {} account configurations already existed." msgstr "Yedek içe aktarıldı. Tüm {} hesap yapılandırması zaten mevcuttu." -#: ui/app.slint:296 +#: ui/app.slint:294 msgid "" "Imported {} account configurations. Open Accounts and sign in to each one." msgstr "" "{} hesap yapılandırması içe aktarıldı. Hesaplar'ı açın ve her biriyle oturum " "açın." -#: ui/app.slint:297 +#: ui/app.slint:295 msgid "Could not import backup: {}" msgstr "Yedek içe aktarılamadı: {}" -#: ui/app.slint:298 +#: ui/app.slint:296 msgid "All local Flectar Mail data was deleted." msgstr "Tüm yerel Flectar Mail verileri silindi." -#: ui/app.slint:299 +#: ui/app.slint:297 msgid "Could not delete all local data: {}" msgstr "Tüm yerel veriler silinemedi: {}" -#: ui/app.slint:300 +#: ui/app.slint:298 msgid "Deleting local accounts and cached data…" msgstr "Yerel hesaplar ve önbellek verileri siliniyor…" -#: ui/app.slint:305 +#: ui/app.slint:303 msgid "Background mail refresh failed: {}" msgstr "Arka plan e-posta yenilemesi başarısız: {}" -#: ui/app.slint:306 +#: ui/app.slint:304 msgid "No conversations in this folder" msgstr "Bu klasörde konuşma yok" -#: ui/app.slint:307 +#: ui/app.slint:305 msgid "No conversations match this search" msgstr "Bu aramayla eşleşen konuşma yok" -#: ui/app.slint:308 +#: ui/app.slint:306 msgid "Mail core is ready for account synchronization and message actions." msgstr "E-posta çekirdeği hesap eşitlemesi ve ileti işlemleri için hazır." -#: ui/app.slint:309 +#: ui/app.slint:307 msgid "Large message rendered in compatibility mode." msgstr "Büyük ileti uyumluluk modunda görüntülendi." -#: ui/app.slint:310 +#: ui/app.slint:308 msgid "Software email render failed: {}" msgstr "E-postanın yazılımla görüntülenmesi başarısız oldu: {}" -#: ui/app.slint:311 +#: ui/app.slint:309 msgid "Rendered with software fallback after GPU error: {}" msgstr "GPU hatasından sonra yazılım yedeğiyle görüntülendi: {}" -#: ui/app.slint:312 +#: ui/app.slint:310 msgid "Blitz render failed (GPU: {}; software: {})" msgstr "Blitz oluşturması başarısız (GPU: {}; yazılım: {})" -#: ui/app.slint:317 +#: ui/app.slint:315 msgid "Add an email address, then save the contact." msgstr "Bir e-posta adresi ekleyin, ardından kişiyi kaydedin." -#: ui/app.slint:318 +#: ui/app.slint:316 msgid "Enter a valid email address before saving." msgstr "Kaydetmeden önce geçerli bir e-posta adresi girin." -#: ui/app.slint:319 +#: ui/app.slint:317 msgid "Could not save: contact storage is unavailable." msgstr "Kaydedilemedi: kişi deposu kullanılamıyor." -#: ui/app.slint:320 +#: ui/app.slint:318 msgid "Could not save contact: {}" msgstr "Kişi kaydedilemedi: {}" -#: ui/app.slint:321 +#: ui/app.slint:319 msgid "Contact saved." msgstr "Kişi kaydedildi." -#: ui/app.slint:322 +#: ui/app.slint:320 msgid "Saved to contacts." msgstr "Kişilere kaydedildi." -#: ui/app.slint:323 +#: ui/app.slint:321 msgid "Could not delete: contact storage is unavailable." msgstr "Silinemedi: kişi deposu kullanılamıyor." -#: ui/app.slint:324 +#: ui/app.slint:322 msgid "Could not delete contact: {}" msgstr "Kişi silinemedi: {}" -#: ui/app.slint:325 +#: ui/app.slint:323 msgid "Contact deleted." msgstr "Kişi silindi." -#: ui/app.slint:326 +#: ui/app.slint:324 msgid "Could not load contacts: {}" msgstr "Kişiler yüklenemedi: {}" -#: ui/app.slint:327 +#: ui/app.slint:325 msgid "Local contact storage is still starting…" msgstr "Yerel kişi deposu hâlâ başlıyor…" -#: ui/app.slint:328 +#: ui/app.slint:326 msgid "Loading contacts…" msgstr "Kişiler yükleniyor…" -#: ui/app.slint:333 ui/components/calendar-agenda.slint:233 +#: ui/app.slint:331 ui/components/calendar-agenda.slint:233 msgid "Local calendar" msgstr "Yerel takvim" -#: ui/app.slint:334 ui/components/calendar.slint:271 +#: ui/app.slint:332 ui/components/calendar.slint:271 #: ui/components/calendar.slint:319 ui/components/calendar.slint:434 msgid "Calendar" msgstr "Takvim" -#: ui/app.slint:335 +#: ui/app.slint:333 msgid "Could not load calendar: {}" msgstr "Takvim yüklenemedi: {}" -#: ui/app.slint:336 +#: ui/app.slint:334 msgid "Calendar shown and syncing." msgstr "Takvim gösteriliyor ve eşitleniyor." -#: ui/app.slint:337 +#: ui/app.slint:335 msgid "Calendar hidden and sync paused." msgstr "Takvim gizlendi ve eşitleme duraklatıldı." -#: ui/app.slint:338 +#: ui/app.slint:336 msgid "Calendar update failed: {}" msgstr "Takvim güncellemesi başarısız: {}" -#: ui/app.slint:339 +#: ui/app.slint:337 msgid "Only events created in Flectar Mail can be edited." msgstr "Yalnızca Flectar Mail'de oluşturulan etkinlikler düzenlenebilir." -#: ui/app.slint:340 +#: ui/app.slint:338 msgid "Only events created in Flectar Mail can be deleted." msgstr "Yalnızca Flectar Mail'de oluşturulan etkinlikler silinebilir." -#: ui/app.slint:341 +#: ui/app.slint:339 msgid "Calendar storage is unavailable." msgstr "Takvim deposu kullanılamıyor." -#: ui/app.slint:342 +#: ui/app.slint:340 msgid "Could not delete event: {}" msgstr "Etkinlik silinemedi: {}" -#: ui/app.slint:343 +#: ui/app.slint:341 msgid "Event deleted, but refresh failed: {}" msgstr "Etkinlik silindi, ancak yenileme başarısız: {}" -#: ui/app.slint:344 +#: ui/app.slint:342 msgid "Event deleted." msgstr "Etkinlik silindi." -#: ui/app.slint:345 +#: ui/app.slint:343 msgid "Use YYYY-MM-DD and a 24-hour time such as 09:30." msgstr "YYYY-AA-GG biçimini ve 09:30 gibi 24 saatlik bir saat kullanın." -#: ui/app.slint:346 +#: ui/app.slint:344 msgid "Event duration must be a number of minutes." msgstr "Etkinlik süresi dakika cinsinden bir sayı olmalıdır." -#: ui/app.slint:347 +#: ui/app.slint:345 msgid "Add a title and choose a duration from 15 to 1440 minutes." msgstr "Bir başlık ekleyin ve 15 ile 1440 dakika arasında bir süre seçin." -#: ui/app.slint:348 +#: ui/app.slint:346 msgid "That local time does not exist in the current time zone." msgstr "Bu yerel saat geçerli saat diliminde mevcut değil." -#: ui/app.slint:349 +#: ui/app.slint:347 msgid "Could not save event: {}" msgstr "Etkinlik kaydedilemedi: {}" -#: ui/app.slint:350 +#: ui/app.slint:348 msgid "Event saved, but refresh failed: {}" msgstr "Etkinlik kaydedildi, ancak yenileme başarısız: {}" -#: ui/app.slint:351 +#: ui/app.slint:349 msgid "Event saved. Provider sync will run when enabled." msgstr "" "Etkinlik kaydedildi. Sağlayıcı eşitlemesi etkinleştirildiğinde çalışacak." -#: ui/app.slint:356 +#: ui/app.slint:354 msgid "Email resource render failed: {}" msgstr "E-posta kaynağı görüntülenemedi: {}" -#: ui/app.slint:357 +#: ui/app.slint:355 msgid "Message refresh failed: {}" msgstr "İleti yenilemesi başarısız: {}" -#: ui/app.slint:358 +#: ui/app.slint:356 msgid "Message load failed: {}" msgstr "İleti yüklenemedi: {}" -#: ui/app.slint:359 +#: ui/app.slint:357 msgid "" "Your local mailbox could not be opened. No account operation was started. " "Retry, or copy the diagnostics for support." @@ -894,252 +886,252 @@ msgstr "" "Yerel posta kutunuz açılamadı. Hiçbir hesap işlemi başlatılmadı. Yeniden " "deneyin veya destek için tanılama bilgilerini kopyalayın." -#: ui/app.slint:360 +#: ui/app.slint:358 msgid "Local storage startup failed." msgstr "Yerel depolama başlatılamadı." -#: ui/app.slint:367 +#: ui/app.slint:365 msgid "Could not identify the dragged account." msgstr "Sürüklenen hesap tanımlanamadı." -#: ui/app.slint:368 +#: ui/app.slint:366 msgid "Account ordering requires local mail data." msgstr "Hesap sıralaması yerel e-posta verisi gerektirir." -#: ui/app.slint:369 +#: ui/app.slint:367 msgid "Could not find the account to reorder." msgstr "Yeniden sıralanacak hesap bulunamadı." -#: ui/app.slint:370 +#: ui/app.slint:368 msgid "Saving account order…" msgstr "Hesap sırası kaydediliyor…" -#: ui/app.slint:371 +#: ui/app.slint:369 msgid "Sent copy settings require local mail data." msgstr "Gönderilen kopya ayarları yerel posta verileri gerektirir." -#: ui/app.slint:372 +#: ui/app.slint:370 msgid "Updating Sent copy settings…" msgstr "Gönderilen kopya ayarları güncelleniyor…" -#: ui/app.slint:373 +#: ui/app.slint:371 msgid "Sent copy settings updated." msgstr "Gönderilen kopya ayarları güncellendi." -#: ui/app.slint:374 +#: ui/app.slint:372 msgid "Could not update Sent copy settings: {}" msgstr "Gönderilen kopya ayarları güncellenemedi: {}" -#: ui/app.slint:375 +#: ui/app.slint:373 msgid "Mail history requires local mail data." msgstr "E-posta geçmişi yerel e-posta verisi gerektirir." -#: ui/app.slint:376 +#: ui/app.slint:374 msgid "Updating mail history…" msgstr "E-posta geçmişi güncelleniyor…" -#: ui/app.slint:377 +#: ui/app.slint:375 msgid "Remote images are unavailable in this minimal build." msgstr "Uzak görseller bu minimal derlemede kullanılamıyor." -#: ui/app.slint:378 +#: ui/app.slint:376 msgid "Could not save remote-image setting: {}" msgstr "Uzak görsel ayarı kaydedilemedi: {}" -#: ui/app.slint:379 +#: ui/app.slint:377 msgid "Remote sender icons unavailable: {}" msgstr "Uzak gönderen simgeleri kullanılamıyor: {}" -#: ui/app.slint:380 +#: ui/app.slint:378 msgid "Remote images unavailable: {}" msgstr "Uzak görseller kullanılamıyor: {}" -#: ui/app.slint:381 +#: ui/app.slint:379 msgid "Remote body images and sender favicons enabled." msgstr "Gövde içi uzak görseller ve gönderen favikonları etkinleştirildi." -#: ui/app.slint:382 +#: ui/app.slint:380 msgid "Remote images blocked." msgstr "Uzak görseller engellendi." -#: ui/app.slint:383 +#: ui/app.slint:381 msgid "Could not refresh message images: {}" msgstr "İleti görselleri yenilenemedi: {}" -#: ui/app.slint:384 +#: ui/app.slint:382 msgid "Remote-image loading is disabled." msgstr "Uzak görsel yükleme devre dışı." -#: ui/app.slint:385 +#: ui/app.slint:383 msgid "Could not load message images: {}" msgstr "İleti görselleri yüklenemedi: {}" -#: ui/app.slint:386 +#: ui/app.slint:384 msgid "Sync finished, refresh failed: {}" msgstr "Eşitleme tamamlandı, yenileme başarısız: {}" -#: ui/app.slint:387 +#: ui/app.slint:385 msgid "Sync complete." msgstr "Eşitleme tamamlandı." -#: ui/app.slint:388 +#: ui/app.slint:386 msgid "Sync failed: {}" msgstr "Eşitleme başarısız: {}" -#: ui/app.slint:389 +#: ui/app.slint:387 msgid "Local mail data is unavailable; preview data is read-only." msgstr "Yerel e-posta verisi kullanılamıyor; önizleme verisi salt okunur." -#: ui/app.slint:390 +#: ui/app.slint:388 msgid "Synchronizing accounts…" msgstr "Hesaplar eşitleniyor…" -#: ui/app.slint:395 +#: ui/app.slint:393 msgid "Message action completed." msgstr "İleti işlemi tamamlandı." -#: ui/app.slint:396 +#: ui/app.slint:394 msgid "" "Permanent deletion queued. Mail will disappear after the server confirms." msgstr "" -#: ui/app.slint:397 +#: ui/app.slint:395 msgid "Message action failed: {}" msgstr "İleti işlemi başarısız: {}" -#: ui/app.slint:398 +#: ui/app.slint:396 msgid "Message action failed: message is no longer available" msgstr "İleti işlemi başarısız: ileti artık mevcut değil" -#: ui/app.slint:399 +#: ui/app.slint:397 msgid "Label added." msgstr "Etiket eklendi." -#: ui/app.slint:400 +#: ui/app.slint:398 msgid "Label removed." msgstr "Etiket kaldırıldı." -#: ui/app.slint:401 +#: ui/app.slint:399 msgid "Label created and added." msgstr "Etiket oluşturuldu ve eklendi." -#: ui/app.slint:402 +#: ui/app.slint:400 msgid "Label created." msgstr "Etiket oluşturuldu." -#: ui/app.slint:403 +#: ui/app.slint:401 msgid "Label updated." msgstr "Etiket güncellendi." -#: ui/app.slint:404 +#: ui/app.slint:402 msgid "Label deleted." msgstr "Etiket silindi." -#: ui/app.slint:405 +#: ui/app.slint:403 msgid "Could not update label: {}" msgstr "Etiket güncellenemedi: {}" -#: ui/app.slint:406 +#: ui/app.slint:404 msgid "Could not save label: {}" msgstr "Etiket kaydedilemedi: {}" -#: ui/app.slint:407 +#: ui/app.slint:405 msgid "Could not delete label: {}" msgstr "Etiket silinemedi: {}" -#: ui/app.slint:408 +#: ui/app.slint:406 msgid "Folder created." msgstr "Klasör oluşturuldu." -#: ui/app.slint:409 +#: ui/app.slint:407 msgid "Folder renamed." msgstr "Klasör yeniden adlandırıldı." -#: ui/app.slint:410 +#: ui/app.slint:408 msgid "Folder deleted." msgstr "Klasör silindi." -#: ui/app.slint:411 +#: ui/app.slint:409 msgid "Could not create folder: {}" msgstr "Klasör oluşturulamadı: {}" -#: ui/app.slint:412 +#: ui/app.slint:410 msgid "Could not rename folder: {}" msgstr "Klasör yeniden adlandırılamadı: {}" -#: ui/app.slint:413 +#: ui/app.slint:411 msgid "Could not delete folder: {}" msgstr "Klasör silinemedi: {}" -#: ui/app.slint:414 +#: ui/app.slint:412 msgid "Could not close preview: {}" msgstr "Önizleme kapatılamadı: {}" -#: ui/app.slint:415 +#: ui/app.slint:413 msgid "Opened the system print view." msgstr "Sistem yazdırma görünümü açıldı." -#: ui/app.slint:416 +#: ui/app.slint:414 msgid "Could not print message: {}" msgstr "İleti yazdırılamadı: {}" -#: ui/app.slint:417 +#: ui/app.slint:415 msgid "Opened the message in your browser." msgstr "İleti tarayıcınızda açıldı." -#: ui/app.slint:418 +#: ui/app.slint:416 msgid "Could not open message in browser: {}" msgstr "İleti tarayıcıda açılamadı: {}" -#: ui/app.slint:419 +#: ui/app.slint:417 msgid "Connect an account before replying or forwarding." msgstr "Yanıtlamadan veya iletmeden önce bir hesap bağlayın." -#: ui/app.slint:420 +#: ui/app.slint:418 msgid "Choose a message first." msgstr "Önce bir ileti seçin." -#: ui/app.slint:421 +#: ui/app.slint:419 msgid "Only drafts can be reopened for editing." msgstr "Yalnızca taslaklar düzenlenmek üzere yeniden açılabilir." -#: ui/app.slint:422 +#: ui/app.slint:420 msgid "Could not open draft: {}" msgstr "Taslak açılamadı: {}" -#: ui/app.slint:423 +#: ui/app.slint:421 msgid "Could not open draft: invalid account identifier" msgstr "Taslak açılamadı: geçersiz hesap tanımlayıcısı" -#: ui/app.slint:424 +#: ui/app.slint:422 msgid "Connected account" msgstr "Bağlı hesap" -#: ui/app.slint:425 +#: ui/app.slint:423 msgid "Could not open composer: {}" msgstr "Oluşturucu açılamadı: {}" -#: ui/app.slint:426 +#: ui/app.slint:424 msgid "Could not open composer: invalid account identifier" msgstr "Oluşturucu açılamadı: geçersiz hesap tanımlayıcısı" -#: ui/app.slint:431 +#: ui/app.slint:429 msgid "Attach files to this email" msgstr "Bu e-postaya dosya ekleyin" -#: ui/app.slint:432 -msgid "The platform host does not provide a document picker." -msgstr "Platform ana bilgisayarı belge seçici sağlamıyor." +#: ui/app.slint:430 +msgid "Could not add attachments: {}" +msgstr "Ekler eklenemedi: {}" -#: ui/app.slint:433 +#: ui/app.slint:431 msgid "Added 1 attachment." msgstr "1 ek eklendi." -#: ui/app.slint:434 +#: ui/app.slint:432 msgid "Added {} attachments." msgstr "{} ek eklendi." -#: ui/app.slint:435 +#: ui/app.slint:433 msgid "" "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB " "total limit)." @@ -1147,115 +1139,115 @@ msgstr "" "Eklenen ekler: {}. Atlananlar: {} (yinelenen, okunamayan veya toplam 25 MB " "sınırını aşan)." -#: ui/app.slint:436 +#: ui/app.slint:434 msgid "Attachment removed." msgstr "Ek kaldırıldı." -#: ui/app.slint:437 +#: ui/app.slint:435 msgid "Choose the account this message should be sent from." msgstr "Bu iletinin hangi hesaptan gönderileceğini seçin." -#: ui/app.slint:438 +#: ui/app.slint:436 msgid "Choose a verified sender identity for this message." msgstr "Bu ileti için doğrulanmış bir gönderen kimliği seçin." -#: ui/app.slint:439 +#: ui/app.slint:437 msgid "Connect an account before saving or sending mail." msgstr "E-posta kaydetmeden veya göndermeden önce bir hesap bağlayın." -#: ui/app.slint:440 +#: ui/app.slint:438 msgid "Message queued for delivery (action {})." msgstr "İleti gönderilmek üzere kuyruğa alındı (eylem {})." -#: ui/app.slint:441 +#: ui/app.slint:439 msgid "Sending message…" msgstr "İleti gönderiliyor…" -#: ui/app.slint:442 +#: ui/app.slint:440 msgid "Message sent." msgstr "İleti gönderildi." -#: ui/app.slint:443 +#: ui/app.slint:441 msgid "Message was not sent: {} Your draft is preserved." msgstr "İleti gönderilemedi: {} Taslağınız korunuyor." -#: ui/app.slint:444 +#: ui/app.slint:442 msgid "Draft saved locally (#{})." msgstr "Taslak yerel olarak kaydedildi (#{})." -#: ui/app.slint:445 +#: ui/app.slint:443 msgid "Compose failed: {}" msgstr "Oluşturma başarısız: {}" -#: ui/app.slint:446 +#: ui/app.slint:444 msgid "Connect an account before managing email templates." msgstr "E-posta şablonlarını yönetmeden önce bir hesap bağlayın." -#: ui/app.slint:447 +#: ui/app.slint:445 msgid "Could not load email templates: {}" msgstr "E-posta şablonları yüklenemedi: {}" -#: ui/app.slint:448 +#: ui/app.slint:446 msgid "Enter a template name." msgstr "Bir şablon adı girin." -#: ui/app.slint:449 +#: ui/app.slint:447 msgid "Add a subject or message to the template." msgstr "Şablona bir konu veya ileti ekleyin." -#: ui/app.slint:450 +#: ui/app.slint:448 msgid "Email template saved." msgstr "E-posta şablonu kaydedildi." -#: ui/app.slint:451 +#: ui/app.slint:449 msgid "Could not save email template: {}" msgstr "E-posta şablonu kaydedilemedi: {}" -#: ui/app.slint:452 +#: ui/app.slint:450 msgid "Email template deleted." msgstr "E-posta şablonu silindi." -#: ui/app.slint:453 +#: ui/app.slint:451 msgid "Could not delete email template: {}" msgstr "E-posta şablonu silinemedi: {}" -#: ui/app.slint:454 +#: ui/app.slint:452 msgid "Email template is no longer available." msgstr "E-posta şablonu artık mevcut değil." -#: ui/app.slint:455 +#: ui/app.slint:453 msgid "Email template inserted." msgstr "E-posta şablonu eklendi." -#: ui/app.slint:456 +#: ui/app.slint:454 msgid "Email template inserted, but its usage could not be saved: {}" msgstr "E-posta şablonu eklendi, ancak kullanımı kaydedilemedi: {}" -#: ui/app.slint:457 +#: ui/app.slint:455 msgid "IMAP port must be between 1 and 65535." msgstr "IMAP bağlantı noktası 1 ile 65535 arasında olmalıdır." -#: ui/app.slint:458 +#: ui/app.slint:456 msgid "SMTP port must be between 1 and 65535." msgstr "SMTP bağlantı noktası 1 ile 65535 arasında olmalıdır." -#: ui/app.slint:459 +#: ui/app.slint:457 msgid "Local mail data is unavailable. Retry startup." msgstr "Yerel e-posta verisi kullanılamıyor. Başlatmayı yeniden deneyin." -#: ui/app.slint:460 +#: ui/app.slint:458 msgid "Discovering JMAP and checking capabilities…" msgstr "JMAP keşfediliyor ve yetenekler denetleniyor…" -#: ui/app.slint:461 +#: ui/app.slint:459 msgid "Certificate imported. Connect to verify the server." msgstr "Sertifika içe aktarıldı. Sunucuyu doğrulamak için bağlanın." -#: ui/app.slint:462 +#: ui/app.slint:460 msgid "Could not load account preferences: {}" msgstr "Hesap tercihleri yüklenemedi: {}" -#: ui/app.slint:463 +#: ui/app.slint:461 msgid "" "Your edited signature was preserved. Choose No signature to keep it as " "message text before inserting another." @@ -1263,19 +1255,19 @@ msgstr "" "Düzenlediğiniz imza korundu. Başka birini eklemeden önce onu ileti metni " "olarak tutmak için İmza yok'u seçin." -#: ui/app.slint:464 +#: ui/app.slint:462 msgid "Certificate import failed: {}" msgstr "Sertifika içe aktarılamadı: {}" -#: ui/app.slint:465 +#: ui/app.slint:463 msgid "Paste the PEM certificate into the certificate field." msgstr "PEM sertifikasını sertifika alanına yapıştırın." -#: ui/app.slint:466 +#: ui/app.slint:464 msgid "Testing IMAP and SMTP connections…" msgstr "IMAP ve SMTP bağlantıları sınanıyor…" -#: ui/app.slint:471 +#: ui/app.slint:469 msgid "" "Another browser authorization is already in progress; finish it or choose " "Cancel." @@ -1283,145 +1275,145 @@ msgstr "" "Başka bir tarayıcı yetkilendirmesi zaten sürüyor; onu bitirin veya İptal'i " "seçin." -#: ui/app.slint:472 ui/dialogs/settings-dialog.slint:1643 +#: ui/app.slint:470 ui/dialogs/settings-dialog.slint:1643 #: ui/dialogs/settings-dialog.slint:1686 ui/views/onboarding-view.slint:152 msgid "Add app keys in Sign-in settings to enable this provider." msgstr "" "Bu sağlayıcıyı etkinleştirmek için Oturum açma ayarlarında uygulama " "anahtarları ekleyin." -#: ui/app.slint:473 +#: ui/app.slint:471 msgid "Choose Gmail or Outlook OAuth." msgstr "Gmail veya Outlook OAuth seçin." -#: ui/app.slint:474 +#: ui/app.slint:472 msgid "Complete sign-in in your browser, or choose Cancel here." msgstr "Oturum açmayı tarayıcınızda tamamlayın veya burada İptal'i seçin." -#: ui/app.slint:475 +#: ui/app.slint:473 msgid "Cancelling browser sign-in…" msgstr "Tarayıcıyla oturum açma iptal ediliyor…" -#: ui/app.slint:476 +#: ui/app.slint:474 msgid "Finishing browser sign-in…" msgstr "Tarayıcıyla oturum açma tamamlanıyor…" -#: ui/app.slint:477 +#: ui/app.slint:475 msgid "Could not use that browser address: {}" msgstr "O tarayıcı adresi kullanılamadı: {}" -#: ui/app.slint:478 +#: ui/app.slint:476 msgid "Calendar core is unavailable." msgstr "Takvim çekirdeği kullanılamıyor." -#: ui/app.slint:479 +#: ui/app.slint:477 msgid "Account no longer exists." msgstr "Hesap artık yok." -#: ui/app.slint:480 +#: ui/app.slint:478 msgid "Authorize calendar access in your browser." msgstr "Takvim erişimini tarayıcınızda yetkilendirin." -#: ui/app.slint:481 +#: ui/app.slint:479 msgid "OAuth app keys saved." msgstr "OAuth uygulama anahtarları kaydedildi." -#: ui/app.slint:482 +#: ui/app.slint:480 msgid "{} OAuth settings saved." msgstr "{} OAuth ayarları kaydedildi." -#: ui/app.slint:483 +#: ui/app.slint:481 msgid "OAuth settings failed: {}" msgstr "OAuth ayarları başarısız: {}" -#: ui/app.slint:484 +#: ui/app.slint:482 msgid "Mail refresh failed: {}" msgstr "E-posta yenilemesi başarısız: {}" -#: ui/app.slint:485 +#: ui/app.slint:483 msgid "Mail filter failed: {}" msgstr "E-posta filtresi başarısız: {}" -#: ui/app.slint:486 +#: ui/app.slint:484 msgid "Mail pagination failed: {}" msgstr "E-posta sayfalaması başarısız: {}" -#: ui/app.slint:487 +#: ui/app.slint:485 msgid "Could not mark message as read: {}" msgstr "İleti okundu olarak işaretlenemedi: {}" -#: ui/app.slint:488 ui/views/mail-reading-pane.slint:223 +#: ui/app.slint:486 ui/views/mail-reading-pane.slint:223 #: ui/views/mail-reading-pane.slint:1162 msgid "Loading message…" msgstr "İleti yükleniyor…" -#: ui/app.slint:494 ui/components/workspace-navigation.slint:551 +#: ui/app.slint:492 ui/components/workspace-navigation.slint:551 #: ui/tray.slint:9 ui/tray.slint:10 msgid "Flectar Mail" msgstr "Flectar Mail" -#: ui/app.slint:1065 ui/app.slint:1078 +#: ui/app.slint:1063 ui/app.slint:1076 msgid "Global Labels (local only)" msgstr "Genel Etiketler (yalnızca yerel)" -#: ui/app.slint:1938 +#: ui/app.slint:1936 msgid "Empty Trash?" msgstr "" -#: ui/app.slint:1938 ui/views/files-view.slint:538 +#: ui/app.slint:1936 ui/views/files-view.slint:538 msgid "Delete permanently?" msgstr "Kalıcı olarak silinsin mi?" -#: ui/app.slint:1942 +#: ui/app.slint:1940 msgid "" "Permanently delete all messages in Trash across every connected account?" msgstr "" -#: ui/app.slint:1943 +#: ui/app.slint:1941 msgid "Permanently delete all messages in this account’s Trash?" msgstr "" -#: ui/app.slint:1945 +#: ui/app.slint:1943 msgid "Permanently delete the selected conversations from Trash?" msgstr "" -#: ui/app.slint:1946 +#: ui/app.slint:1944 msgid "Permanently delete this conversation from Trash?" msgstr "" -#: ui/app.slint:1947 ui/components/calendar.slint:926 +#: ui/app.slint:1945 ui/components/calendar.slint:926 msgid "This action cannot be undone." msgstr "Bu işlem geri alınamaz." -#: ui/app.slint:1948 +#: ui/app.slint:1946 msgid "Empty Trash" msgstr "" -#: ui/app.slint:1948 ui/views/files-view.slint:620 +#: ui/app.slint:1946 ui/views/files-view.slint:620 msgid "Delete permanently" msgstr "Kalıcı olarak sil" -#: ui/app.slint:2111 +#: ui/app.slint:2109 msgid "Mailbox startup failed" msgstr "Posta kutusu başlatılamadı" -#: ui/app.slint:2117 +#: ui/app.slint:2115 msgid "Flectar Mail could not open local storage" msgstr "Flectar Mail yerel depolamayı açamadı" -#: ui/app.slint:2130 +#: ui/app.slint:2128 msgid "Diagnostic ID: {}" msgstr "Tanılama kimliği: {}" -#: ui/app.slint:2133 +#: ui/app.slint:2131 msgid "Diagnostic ID {}" msgstr "Tanılama kimliği {}" -#: ui/app.slint:2138 +#: ui/app.slint:2136 msgid "Retry" msgstr "Yeniden dene" -#: ui/app.slint:2146 ui/components/copyable-diagnostic.slint:38 +#: ui/app.slint:2144 ui/components/copyable-diagnostic.slint:38 msgid "Copy diagnostics" msgstr "Tanılama bilgilerini kopyala" diff --git a/lang/zh_Hans/LC_MESSAGES/flectar-mail.po b/lang/zh_Hans/LC_MESSAGES/flectar-mail.po index dc1409cd..dee8af0b 100644 --- a/lang/zh_Hans/LC_MESSAGES/flectar-mail.po +++ b/lang/zh_Hans/LC_MESSAGES/flectar-mail.po @@ -186,7 +186,7 @@ msgstr "有往来邮件记录" msgid "New address-book entry" msgstr "新建通讯录条目" -#: ui/app.slint:121 ui/app.slint:847 ui/components/composer-fields.slint:66 +#: ui/app.slint:121 ui/app.slint:845 ui/components/composer-fields.slint:66 #: ui/dialogs/compose-dialog.slint:37 msgid "No connected account" msgstr "未连接账户" @@ -200,11 +200,11 @@ msgstr[0] "{n} 次互动" msgid "Last contacted {} {}, {}" msgstr "上次联系于 {}年{}月{}日" -#: ui/app.slint:128 ui/app.slint:361 +#: ui/app.slint:128 ui/app.slint:359 msgid "Loading local data…" msgstr "正在加载本地数据…" -#: ui/app.slint:129 ui/app.slint:362 +#: ui/app.slint:129 ui/app.slint:360 msgid "Local data ready." msgstr "本地数据已就绪。" @@ -665,590 +665,582 @@ msgid "Choose where to export the database snapshot" msgstr "选择数据库快照的导出位置" #: ui/app.slint:289 -msgid "The platform host does not provide document export." -msgstr "当前平台环境不支持导出文档。" - -#: ui/app.slint:290 -msgid "The platform host does not provide document import." -msgstr "当前平台环境不支持导入文档。" - -#: ui/app.slint:291 msgid "Backup exported. Passwords and OAuth tokens were not included." msgstr "备份已导出,其中不包含密码和 OAuth 令牌。" -#: ui/app.slint:292 +#: ui/app.slint:290 msgid "Could not export backup: {}" msgstr "无法导出备份:{}" -#: ui/app.slint:293 +#: ui/app.slint:291 msgid "Local data storage is unavailable. Retry startup." msgstr "本地数据存储不可用,请重新启动。" -#: ui/app.slint:294 +#: ui/app.slint:292 msgid "Creating verified database snapshot…" msgstr "正在创建已验证的数据库快照…" -#: ui/app.slint:295 +#: ui/app.slint:293 msgid "Backup imported. All {} account configurations already existed." msgstr "备份已导入。{} 个账户配置均已存在。" -#: ui/app.slint:296 +#: ui/app.slint:294 msgid "" "Imported {} account configurations. Open Accounts and sign in to each one." msgstr "已导入 {} 个账户配置。请打开“账户”并逐一登录。" -#: ui/app.slint:297 +#: ui/app.slint:295 msgid "Could not import backup: {}" msgstr "无法导入备份:{}" -#: ui/app.slint:298 +#: ui/app.slint:296 msgid "All local Flectar Mail data was deleted." msgstr "Flectar Mail 的全部本地数据已删除。" -#: ui/app.slint:299 +#: ui/app.slint:297 msgid "Could not delete all local data: {}" msgstr "无法删除全部本地数据:{}" -#: ui/app.slint:300 +#: ui/app.slint:298 msgid "Deleting local accounts and cached data…" msgstr "正在删除本地账户和缓存数据…" -#: ui/app.slint:305 +#: ui/app.slint:303 msgid "Background mail refresh failed: {}" msgstr "后台邮件刷新失败:{}" -#: ui/app.slint:306 +#: ui/app.slint:304 msgid "No conversations in this folder" msgstr "此文件夹中没有会话" -#: ui/app.slint:307 +#: ui/app.slint:305 msgid "No conversations match this search" msgstr "没有符合该搜索条件的会话" -#: ui/app.slint:308 +#: ui/app.slint:306 msgid "Mail core is ready for account synchronization and message actions." msgstr "邮件核心已就绪,可进行账户同步和邮件操作。" -#: ui/app.slint:309 +#: ui/app.slint:307 msgid "Large message rendered in compatibility mode." msgstr "大型邮件已使用兼容模式渲染。" -#: ui/app.slint:310 +#: ui/app.slint:308 msgid "Software email render failed: {}" msgstr "软件方式渲染邮件失败:{}" -#: ui/app.slint:311 +#: ui/app.slint:309 msgid "Rendered with software fallback after GPU error: {}" msgstr "GPU 出错后已改用软件方式渲染:{}" -#: ui/app.slint:312 +#: ui/app.slint:310 msgid "Blitz render failed (GPU: {}; software: {})" msgstr "Blitz 渲染失败(GPU:{};软件:{})" -#: ui/app.slint:317 +#: ui/app.slint:315 msgid "Add an email address, then save the contact." msgstr "请先填写电子邮件地址,再保存联系人。" -#: ui/app.slint:318 +#: ui/app.slint:316 msgid "Enter a valid email address before saving." msgstr "保存前请填写有效的电子邮件地址。" -#: ui/app.slint:319 +#: ui/app.slint:317 msgid "Could not save: contact storage is unavailable." msgstr "无法保存:联系人存储不可用。" -#: ui/app.slint:320 +#: ui/app.slint:318 msgid "Could not save contact: {}" msgstr "无法保存联系人:{}" -#: ui/app.slint:321 +#: ui/app.slint:319 msgid "Contact saved." msgstr "联系人已保存。" -#: ui/app.slint:322 +#: ui/app.slint:320 msgid "Saved to contacts." msgstr "已保存到联系人。" -#: ui/app.slint:323 +#: ui/app.slint:321 msgid "Could not delete: contact storage is unavailable." msgstr "无法删除:联系人存储不可用。" -#: ui/app.slint:324 +#: ui/app.slint:322 msgid "Could not delete contact: {}" msgstr "无法删除联系人:{}" -#: ui/app.slint:325 +#: ui/app.slint:323 msgid "Contact deleted." msgstr "联系人已删除。" -#: ui/app.slint:326 +#: ui/app.slint:324 msgid "Could not load contacts: {}" msgstr "无法加载联系人:{}" -#: ui/app.slint:327 +#: ui/app.slint:325 msgid "Local contact storage is still starting…" msgstr "本地联系人存储仍在启动中…" -#: ui/app.slint:328 +#: ui/app.slint:326 msgid "Loading contacts…" msgstr "正在加载联系人…" -#: ui/app.slint:333 ui/components/calendar-agenda.slint:233 +#: ui/app.slint:331 ui/components/calendar-agenda.slint:233 msgid "Local calendar" msgstr "本地日历" -#: ui/app.slint:334 ui/components/calendar.slint:271 +#: ui/app.slint:332 ui/components/calendar.slint:271 #: ui/components/calendar.slint:319 ui/components/calendar.slint:434 msgid "Calendar" msgstr "日历" -#: ui/app.slint:335 +#: ui/app.slint:333 msgid "Could not load calendar: {}" msgstr "无法加载日历:{}" -#: ui/app.slint:336 +#: ui/app.slint:334 msgid "Calendar shown and syncing." msgstr "日历已显示,正在同步。" -#: ui/app.slint:337 +#: ui/app.slint:335 msgid "Calendar hidden and sync paused." msgstr "日历已隐藏,同步已暂停。" -#: ui/app.slint:338 +#: ui/app.slint:336 msgid "Calendar update failed: {}" msgstr "日历更新失败:{}" -#: ui/app.slint:339 +#: ui/app.slint:337 msgid "Only events created in Flectar Mail can be edited." msgstr "只能编辑在 Flectar Mail 中创建的日程。" -#: ui/app.slint:340 +#: ui/app.slint:338 msgid "Only events created in Flectar Mail can be deleted." msgstr "只能删除在 Flectar Mail 中创建的日程。" -#: ui/app.slint:341 +#: ui/app.slint:339 msgid "Calendar storage is unavailable." msgstr "日历存储不可用。" -#: ui/app.slint:342 +#: ui/app.slint:340 msgid "Could not delete event: {}" msgstr "无法删除日程:{}" -#: ui/app.slint:343 +#: ui/app.slint:341 msgid "Event deleted, but refresh failed: {}" msgstr "日程已删除,但刷新失败:{}" -#: ui/app.slint:344 +#: ui/app.slint:342 msgid "Event deleted." msgstr "日程已删除。" -#: ui/app.slint:345 +#: ui/app.slint:343 msgid "Use YYYY-MM-DD and a 24-hour time such as 09:30." msgstr "请使用 YYYY-MM-DD 格式的日期和 24 小时制时间,例如 09:30。" -#: ui/app.slint:346 +#: ui/app.slint:344 msgid "Event duration must be a number of minutes." msgstr "日程时长必须是以分钟为单位的数字。" -#: ui/app.slint:347 +#: ui/app.slint:345 msgid "Add a title and choose a duration from 15 to 1440 minutes." msgstr "请填写标题,并选择 15 至 1440 分钟之间的时长。" -#: ui/app.slint:348 +#: ui/app.slint:346 msgid "That local time does not exist in the current time zone." msgstr "该本地时间在当前时区不存在。" -#: ui/app.slint:349 +#: ui/app.slint:347 msgid "Could not save event: {}" msgstr "无法保存日程:{}" -#: ui/app.slint:350 +#: ui/app.slint:348 msgid "Event saved, but refresh failed: {}" msgstr "日程已保存,但刷新失败:{}" -#: ui/app.slint:351 +#: ui/app.slint:349 msgid "Event saved. Provider sync will run when enabled." msgstr "日程已保存。启用服务商同步后将自动同步。" -#: ui/app.slint:356 +#: ui/app.slint:354 msgid "Email resource render failed: {}" msgstr "邮件资源渲染失败:{}" -#: ui/app.slint:357 +#: ui/app.slint:355 msgid "Message refresh failed: {}" msgstr "邮件刷新失败:{}" -#: ui/app.slint:358 +#: ui/app.slint:356 msgid "Message load failed: {}" msgstr "邮件加载失败:{}" -#: ui/app.slint:359 +#: ui/app.slint:357 msgid "" "Your local mailbox could not be opened. No account operation was started. " "Retry, or copy the diagnostics for support." msgstr "" "无法打开本地邮箱,未执行任何账户操作。请重试,或复制诊断信息以寻求支持。" -#: ui/app.slint:360 +#: ui/app.slint:358 msgid "Local storage startup failed." msgstr "本地存储启动失败。" -#: ui/app.slint:367 +#: ui/app.slint:365 msgid "Could not identify the dragged account." msgstr "无法识别所拖动的账户。" -#: ui/app.slint:368 +#: ui/app.slint:366 msgid "Account ordering requires local mail data." msgstr "调整账户顺序需要本地邮件数据。" -#: ui/app.slint:369 +#: ui/app.slint:367 msgid "Could not find the account to reorder." msgstr "找不到要调整顺序的账户。" -#: ui/app.slint:370 +#: ui/app.slint:368 msgid "Saving account order…" msgstr "正在保存账户顺序…" -#: ui/app.slint:371 +#: ui/app.slint:369 msgid "Sent copy settings require local mail data." msgstr "已发送邮件副本设置需要本地邮件数据。" -#: ui/app.slint:372 +#: ui/app.slint:370 msgid "Updating Sent copy settings…" msgstr "正在更新已发送邮件副本设置…" -#: ui/app.slint:373 +#: ui/app.slint:371 msgid "Sent copy settings updated." msgstr "已发送邮件副本设置已更新。" -#: ui/app.slint:374 +#: ui/app.slint:372 msgid "Could not update Sent copy settings: {}" msgstr "无法更新已发送邮件副本设置:{}" -#: ui/app.slint:375 +#: ui/app.slint:373 msgid "Mail history requires local mail data." msgstr "设置邮件同步范围需要本地邮件数据。" -#: ui/app.slint:376 +#: ui/app.slint:374 msgid "Updating mail history…" msgstr "正在更新邮件同步范围…" -#: ui/app.slint:377 +#: ui/app.slint:375 msgid "Remote images are unavailable in this minimal build." msgstr "当前精简版本不支持远程图片。" -#: ui/app.slint:378 +#: ui/app.slint:376 msgid "Could not save remote-image setting: {}" msgstr "无法保存远程图片设置:{}" -#: ui/app.slint:379 +#: ui/app.slint:377 msgid "Remote sender icons unavailable: {}" msgstr "远程发件人图标不可用:{}" -#: ui/app.slint:380 +#: ui/app.slint:378 msgid "Remote images unavailable: {}" msgstr "远程图片不可用:{}" -#: ui/app.slint:381 +#: ui/app.slint:379 msgid "Remote body images and sender favicons enabled." msgstr "已启用正文远程图片和发件人网站图标。" -#: ui/app.slint:382 +#: ui/app.slint:380 msgid "Remote images blocked." msgstr "远程图片已阻止。" -#: ui/app.slint:383 +#: ui/app.slint:381 msgid "Could not refresh message images: {}" msgstr "无法刷新邮件图片:{}" -#: ui/app.slint:384 +#: ui/app.slint:382 msgid "Remote-image loading is disabled." msgstr "远程图片加载已禁用。" -#: ui/app.slint:385 +#: ui/app.slint:383 msgid "Could not load message images: {}" msgstr "无法加载邮件图片:{}" -#: ui/app.slint:386 +#: ui/app.slint:384 msgid "Sync finished, refresh failed: {}" msgstr "同步已完成,但刷新失败:{}" -#: ui/app.slint:387 +#: ui/app.slint:385 msgid "Sync complete." msgstr "同步完成。" -#: ui/app.slint:388 +#: ui/app.slint:386 msgid "Sync failed: {}" msgstr "同步失败:{}" -#: ui/app.slint:389 +#: ui/app.slint:387 msgid "Local mail data is unavailable; preview data is read-only." msgstr "本地邮件数据不可用,预览数据为只读。" -#: ui/app.slint:390 +#: ui/app.slint:388 msgid "Synchronizing accounts…" msgstr "正在同步账户…" -#: ui/app.slint:395 +#: ui/app.slint:393 msgid "Message action completed." msgstr "邮件操作已完成。" -#: ui/app.slint:396 +#: ui/app.slint:394 msgid "" "Permanent deletion queued. Mail will disappear after the server confirms." msgstr "" -#: ui/app.slint:397 +#: ui/app.slint:395 msgid "Message action failed: {}" msgstr "邮件操作失败:{}" -#: ui/app.slint:398 +#: ui/app.slint:396 msgid "Message action failed: message is no longer available" msgstr "邮件操作失败:邮件已不存在" -#: ui/app.slint:399 +#: ui/app.slint:397 msgid "Label added." msgstr "标签已添加。" -#: ui/app.slint:400 +#: ui/app.slint:398 msgid "Label removed." msgstr "标签已移除。" -#: ui/app.slint:401 +#: ui/app.slint:399 msgid "Label created and added." msgstr "标签已创建并添加。" -#: ui/app.slint:402 +#: ui/app.slint:400 msgid "Label created." msgstr "标签已创建。" -#: ui/app.slint:403 +#: ui/app.slint:401 msgid "Label updated." msgstr "标签已更新。" -#: ui/app.slint:404 +#: ui/app.slint:402 msgid "Label deleted." msgstr "标签已删除。" -#: ui/app.slint:405 +#: ui/app.slint:403 msgid "Could not update label: {}" msgstr "无法更新标签:{}" -#: ui/app.slint:406 +#: ui/app.slint:404 msgid "Could not save label: {}" msgstr "无法保存标签:{}" -#: ui/app.slint:407 +#: ui/app.slint:405 msgid "Could not delete label: {}" msgstr "无法删除标签:{}" -#: ui/app.slint:408 +#: ui/app.slint:406 msgid "Folder created." msgstr "文件夹已创建。" -#: ui/app.slint:409 +#: ui/app.slint:407 msgid "Folder renamed." msgstr "文件夹已重命名。" -#: ui/app.slint:410 +#: ui/app.slint:408 msgid "Folder deleted." msgstr "文件夹已删除。" -#: ui/app.slint:411 +#: ui/app.slint:409 msgid "Could not create folder: {}" msgstr "无法创建文件夹:{}" -#: ui/app.slint:412 +#: ui/app.slint:410 msgid "Could not rename folder: {}" msgstr "无法重命名文件夹:{}" -#: ui/app.slint:413 +#: ui/app.slint:411 msgid "Could not delete folder: {}" msgstr "无法删除文件夹:{}" -#: ui/app.slint:414 +#: ui/app.slint:412 msgid "Could not close preview: {}" msgstr "无法关闭预览:{}" -#: ui/app.slint:415 +#: ui/app.slint:413 msgid "Opened the system print view." msgstr "已打开系统打印视图。" -#: ui/app.slint:416 +#: ui/app.slint:414 msgid "Could not print message: {}" msgstr "无法打印邮件:{}" -#: ui/app.slint:417 +#: ui/app.slint:415 msgid "Opened the message in your browser." msgstr "已在浏览器中打开该邮件。" -#: ui/app.slint:418 +#: ui/app.slint:416 msgid "Could not open message in browser: {}" msgstr "无法在浏览器中打开邮件:{}" -#: ui/app.slint:419 +#: ui/app.slint:417 msgid "Connect an account before replying or forwarding." msgstr "请先连接账户再进行回复或转发。" -#: ui/app.slint:420 +#: ui/app.slint:418 msgid "Choose a message first." msgstr "请先选择一封邮件。" -#: ui/app.slint:421 +#: ui/app.slint:419 msgid "Only drafts can be reopened for editing." msgstr "只有草稿箱中的邮件可以重新编辑。" -#: ui/app.slint:422 +#: ui/app.slint:420 msgid "Could not open draft: {}" msgstr "无法打开草稿:{}" -#: ui/app.slint:423 +#: ui/app.slint:421 msgid "Could not open draft: invalid account identifier" msgstr "无法打开草稿:账户标识无效" -#: ui/app.slint:424 +#: ui/app.slint:422 msgid "Connected account" msgstr "已连接账户" -#: ui/app.slint:425 +#: ui/app.slint:423 msgid "Could not open composer: {}" msgstr "无法打开邮件编辑器:{}" -#: ui/app.slint:426 +#: ui/app.slint:424 msgid "Could not open composer: invalid account identifier" msgstr "无法打开邮件编辑器:账户标识无效" -#: ui/app.slint:431 +#: ui/app.slint:429 msgid "Attach files to this email" msgstr "为此邮件添加附件" -#: ui/app.slint:432 -msgid "The platform host does not provide a document picker." -msgstr "当前平台环境不支持文件选择器。" +#: ui/app.slint:430 +msgid "Could not add attachments: {}" +msgstr "无法添加附件:{}" -#: ui/app.slint:433 +#: ui/app.slint:431 msgid "Added 1 attachment." msgstr "已添加 1 个附件。" -#: ui/app.slint:434 +#: ui/app.slint:432 msgid "Added {} attachments." msgstr "已添加 {} 个附件。" -#: ui/app.slint:435 +#: ui/app.slint:433 msgid "" "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB " "total limit)." msgstr "已添加 {} 个附件,跳过 {} 个(重复、无法读取或超过 25 MB 总量上限)。" -#: ui/app.slint:436 +#: ui/app.slint:434 msgid "Attachment removed." msgstr "附件已移除。" -#: ui/app.slint:437 +#: ui/app.slint:435 msgid "Choose the account this message should be sent from." msgstr "选择发送此邮件的账户。" -#: ui/app.slint:438 +#: ui/app.slint:436 msgid "Choose a verified sender identity for this message." msgstr "为此邮件选择已验证的发件人身份。" -#: ui/app.slint:439 +#: ui/app.slint:437 msgid "Connect an account before saving or sending mail." msgstr "请先连接账户再保存或发送邮件。" -#: ui/app.slint:440 +#: ui/app.slint:438 msgid "Message queued for delivery (action {})." msgstr "邮件已加入发送队列(操作 {})。" -#: ui/app.slint:441 +#: ui/app.slint:439 msgid "Sending message…" msgstr "正在发送邮件…" -#: ui/app.slint:442 +#: ui/app.slint:440 msgid "Message sent." msgstr "邮件已发送。" -#: ui/app.slint:443 +#: ui/app.slint:441 msgid "Message was not sent: {} Your draft is preserved." msgstr "邮件未发送:{} 草稿已保留。" -#: ui/app.slint:444 +#: ui/app.slint:442 msgid "Draft saved locally (#{})." msgstr "草稿已保存到本地(#{})。" -#: ui/app.slint:445 +#: ui/app.slint:443 msgid "Compose failed: {}" msgstr "撰写邮件失败:{}" -#: ui/app.slint:446 +#: ui/app.slint:444 msgid "Connect an account before managing email templates." msgstr "请先连接账户再管理邮件模板。" -#: ui/app.slint:447 +#: ui/app.slint:445 msgid "Could not load email templates: {}" msgstr "无法加载邮件模板:{}" -#: ui/app.slint:448 +#: ui/app.slint:446 msgid "Enter a template name." msgstr "请输入模板名称。" -#: ui/app.slint:449 +#: ui/app.slint:447 msgid "Add a subject or message to the template." msgstr "请为模板添加主题或正文。" -#: ui/app.slint:450 +#: ui/app.slint:448 msgid "Email template saved." msgstr "邮件模板已保存。" -#: ui/app.slint:451 +#: ui/app.slint:449 msgid "Could not save email template: {}" msgstr "无法保存邮件模板:{}" -#: ui/app.slint:452 +#: ui/app.slint:450 msgid "Email template deleted." msgstr "邮件模板已删除。" -#: ui/app.slint:453 +#: ui/app.slint:451 msgid "Could not delete email template: {}" msgstr "无法删除邮件模板:{}" -#: ui/app.slint:454 +#: ui/app.slint:452 msgid "Email template is no longer available." msgstr "邮件模板已不存在。" -#: ui/app.slint:455 +#: ui/app.slint:453 msgid "Email template inserted." msgstr "邮件模板已插入。" -#: ui/app.slint:456 +#: ui/app.slint:454 msgid "Email template inserted, but its usage could not be saved: {}" msgstr "邮件模板已插入,但无法记录其使用情况:{}" -#: ui/app.slint:457 +#: ui/app.slint:455 msgid "IMAP port must be between 1 and 65535." msgstr "IMAP 端口必须在 1 到 65535 之间。" -#: ui/app.slint:458 +#: ui/app.slint:456 msgid "SMTP port must be between 1 and 65535." msgstr "SMTP 端口必须在 1 到 65535 之间。" -#: ui/app.slint:459 +#: ui/app.slint:457 msgid "Local mail data is unavailable. Retry startup." msgstr "本地邮件数据不可用,请重新启动。" -#: ui/app.slint:460 +#: ui/app.slint:458 msgid "Discovering JMAP and checking capabilities…" msgstr "正在发现 JMAP 并检查服务能力…" -#: ui/app.slint:461 +#: ui/app.slint:459 msgid "Certificate imported. Connect to verify the server." msgstr "证书已导入。请连接以验证服务器。" -#: ui/app.slint:462 +#: ui/app.slint:460 msgid "Could not load account preferences: {}" msgstr "无法加载账户偏好设置:{}" -#: ui/app.slint:463 +#: ui/app.slint:461 msgid "" "Your edited signature was preserved. Choose No signature to keep it as " "message text before inserting another." @@ -1256,161 +1248,161 @@ msgstr "" "您编辑过的签名已保留。插入其他签名前,请选择“不使用签名”,以便将其保留为邮件" "正文。" -#: ui/app.slint:464 +#: ui/app.slint:462 msgid "Certificate import failed: {}" msgstr "证书导入失败:{}" -#: ui/app.slint:465 +#: ui/app.slint:463 msgid "Paste the PEM certificate into the certificate field." msgstr "请将 PEM 证书粘贴到证书字段中。" -#: ui/app.slint:466 +#: ui/app.slint:464 msgid "Testing IMAP and SMTP connections…" msgstr "正在测试 IMAP 和 SMTP 连接…" -#: ui/app.slint:471 +#: ui/app.slint:469 msgid "" "Another browser authorization is already in progress; finish it or choose " "Cancel." msgstr "已有另一项浏览器授权正在进行,请完成它或选择“取消”。" -#: ui/app.slint:472 ui/dialogs/settings-dialog.slint:1643 +#: ui/app.slint:470 ui/dialogs/settings-dialog.slint:1643 #: ui/dialogs/settings-dialog.slint:1686 ui/views/onboarding-view.slint:152 msgid "Add app keys in Sign-in settings to enable this provider." msgstr "请在“登录设置”中添加应用密钥以启用此服务商。" -#: ui/app.slint:473 +#: ui/app.slint:471 msgid "Choose Gmail or Outlook OAuth." msgstr "请选择 Gmail 或 Outlook OAuth。" -#: ui/app.slint:474 +#: ui/app.slint:472 msgid "Complete sign-in in your browser, or choose Cancel here." msgstr "请在浏览器中完成登录,或在此处选择“取消”。" -#: ui/app.slint:475 +#: ui/app.slint:473 msgid "Cancelling browser sign-in…" msgstr "正在取消浏览器登录…" -#: ui/app.slint:476 +#: ui/app.slint:474 msgid "Finishing browser sign-in…" msgstr "正在完成浏览器登录…" -#: ui/app.slint:477 +#: ui/app.slint:475 msgid "Could not use that browser address: {}" msgstr "无法使用该浏览器地址:{}" -#: ui/app.slint:478 +#: ui/app.slint:476 msgid "Calendar core is unavailable." msgstr "日历核心不可用。" -#: ui/app.slint:479 +#: ui/app.slint:477 msgid "Account no longer exists." msgstr "账户已不存在。" -#: ui/app.slint:480 +#: ui/app.slint:478 msgid "Authorize calendar access in your browser." msgstr "请在浏览器中授权日历访问。" -#: ui/app.slint:481 +#: ui/app.slint:479 msgid "OAuth app keys saved." msgstr "OAuth 应用密钥已保存。" -#: ui/app.slint:482 +#: ui/app.slint:480 msgid "{} OAuth settings saved." msgstr "{} OAuth 设置已保存。" -#: ui/app.slint:483 +#: ui/app.slint:481 msgid "OAuth settings failed: {}" msgstr "OAuth 设置失败:{}" -#: ui/app.slint:484 +#: ui/app.slint:482 msgid "Mail refresh failed: {}" msgstr "邮件刷新失败:{}" -#: ui/app.slint:485 +#: ui/app.slint:483 msgid "Mail filter failed: {}" msgstr "邮件筛选失败:{}" -#: ui/app.slint:486 +#: ui/app.slint:484 msgid "Mail pagination failed: {}" msgstr "邮件分页失败:{}" -#: ui/app.slint:487 +#: ui/app.slint:485 msgid "Could not mark message as read: {}" msgstr "无法将邮件标记为已读:{}" -#: ui/app.slint:488 ui/views/mail-reading-pane.slint:223 +#: ui/app.slint:486 ui/views/mail-reading-pane.slint:223 #: ui/views/mail-reading-pane.slint:1162 msgid "Loading message…" msgstr "正在加载邮件…" -#: ui/app.slint:494 ui/components/workspace-navigation.slint:551 +#: ui/app.slint:492 ui/components/workspace-navigation.slint:551 #: ui/tray.slint:9 ui/tray.slint:10 msgid "Flectar Mail" msgstr "Flectar Mail" -#: ui/app.slint:1065 ui/app.slint:1078 +#: ui/app.slint:1063 ui/app.slint:1076 msgid "Global Labels (local only)" msgstr "全局标签(仅本地)" -#: ui/app.slint:1938 +#: ui/app.slint:1936 msgid "Empty Trash?" msgstr "" -#: ui/app.slint:1938 ui/views/files-view.slint:538 +#: ui/app.slint:1936 ui/views/files-view.slint:538 msgid "Delete permanently?" msgstr "永久删除?" -#: ui/app.slint:1942 +#: ui/app.slint:1940 msgid "" "Permanently delete all messages in Trash across every connected account?" msgstr "" -#: ui/app.slint:1943 +#: ui/app.slint:1941 msgid "Permanently delete all messages in this account’s Trash?" msgstr "" -#: ui/app.slint:1945 +#: ui/app.slint:1943 msgid "Permanently delete the selected conversations from Trash?" msgstr "" -#: ui/app.slint:1946 +#: ui/app.slint:1944 msgid "Permanently delete this conversation from Trash?" msgstr "" -#: ui/app.slint:1947 ui/components/calendar.slint:926 +#: ui/app.slint:1945 ui/components/calendar.slint:926 msgid "This action cannot be undone." msgstr "此操作无法撤销。" -#: ui/app.slint:1948 +#: ui/app.slint:1946 msgid "Empty Trash" msgstr "" -#: ui/app.slint:1948 ui/views/files-view.slint:620 +#: ui/app.slint:1946 ui/views/files-view.slint:620 msgid "Delete permanently" msgstr "永久删除" -#: ui/app.slint:2111 +#: ui/app.slint:2109 msgid "Mailbox startup failed" msgstr "邮箱启动失败" -#: ui/app.slint:2117 +#: ui/app.slint:2115 msgid "Flectar Mail could not open local storage" msgstr "Flectar Mail 无法打开本地存储" -#: ui/app.slint:2130 +#: ui/app.slint:2128 msgid "Diagnostic ID: {}" msgstr "诊断 ID:{}" -#: ui/app.slint:2133 +#: ui/app.slint:2131 msgid "Diagnostic ID {}" msgstr "诊断 ID {}" -#: ui/app.slint:2138 +#: ui/app.slint:2136 msgid "Retry" msgstr "重试" -#: ui/app.slint:2146 ui/components/copyable-diagnostic.slint:38 +#: ui/app.slint:2144 ui/components/copyable-diagnostic.slint:38 msgid "Copy diagnostics" msgstr "复制诊断信息" diff --git a/src/data_controller.rs b/src/data_controller.rs index f1e78278..1c0cd956 100644 --- a/src/data_controller.rs +++ b/src/data_controller.rs @@ -1051,12 +1051,10 @@ pub(super) fn register_data_management_callbacks( } } }); - if spawned.is_err() { + if let Err(error) = spawned { abort_export.abort(); exporting_backup.set(false); - app.set_sync_status(UiMessage::plain( - "The platform host does not provide document export.", - )); + app.set_sync_status(UiMessage::detail("Could not export backup: {}", error)); } }); @@ -1203,12 +1201,10 @@ pub(super) fn register_data_management_callbacks( }; import_account_backup_document(&app, document.path(), &core, &state, &runtime); }); - if spawned.is_err() { + if let Err(error) = spawned { abort_request.abort(); importing_backup.set(false); - app.set_sync_status(UiMessage::plain( - "The platform host does not provide document import.", - )); + app.set_sync_status(UiMessage::detail("Could not import backup: {}", error)); } }); diff --git a/src/main.rs b/src/main.rs index d5baa901..8af02c09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5756,8 +5756,9 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> Ok(documents) => documents, Err(error) => { tracing::warn!(%error, "compose attachment picker failed"); - app.set_compose_notice(UiMessage::plain( - "The platform host does not provide a document picker.", + app.set_compose_notice(UiMessage::detail( + "Could not add attachments: {}", + error, )); app.set_compose_notice_is_error(true); return; @@ -5818,11 +5819,12 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> app.set_compose_notice_is_error(added == 0); } }); - if spawned.is_err() { + if let Err(error) = spawned { abort_request.abort(); picking_for_error.set(false); - app.set_compose_notice(UiMessage::plain( - "The platform host does not provide a document picker.", + app.set_compose_notice(UiMessage::detail( + "Could not add attachments: {}", + error, )); app.set_compose_notice_is_error(true); } diff --git a/ui/app.slint b/ui/app.slint index ee9d58db..b4239122 100644 --- a/ui/app.slint +++ b/ui/app.slint @@ -286,8 +286,6 @@ export global I18n { if (key == "Import Flectar Mail account backup") { return @tr("Import Flectar Mail account backup"); } if (key == "Flectar Mail backup") { return @tr("Flectar Mail backup"); } if (key == "Choose where to export the database snapshot") { return @tr("Choose where to export the database snapshot"); } - if (key == "The platform host does not provide document export.") { return @tr("The platform host does not provide document export."); } - if (key == "The platform host does not provide document import.") { return @tr("The platform host does not provide document import."); } if (key == "Backup exported. Passwords and OAuth tokens were not included.") { return @tr("Backup exported. Passwords and OAuth tokens were not included."); } if (key == "Could not export backup: {}") { return @tr("Could not export backup: {}", argument); } if (key == "Local data storage is unavailable. Retry startup.") { return @tr("Local data storage is unavailable. Retry startup."); } @@ -429,7 +427,7 @@ export global I18n { ui-message-compose(key, argument, second-argument, third-argument) => { if (key == "Attach files to this email") { return @tr("Attach files to this email"); } - if (key == "The platform host does not provide a document picker.") { return @tr("The platform host does not provide a document picker."); } + if (key == "Could not add attachments: {}") { return @tr("Could not add attachments: {}", argument); } if (key == "Added 1 attachment.") { return @tr("Added 1 attachment."); } if (key == "Added {} attachments.") { return @tr("Added {} attachments.", argument); } if (key == "Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB total limit).") { return @tr("Attachments added: {}. Skipped: {} (duplicate, unreadable, or over the 25 MB total limit).", argument, second-argument); } From d4aeff813c634026604a1ff03a9ee559b33659a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Sun, 18 Oct 2026 14:31:41 +0200 Subject: [PATCH 10/11] refactor: route document requests through one helper --- platform/README.md | 5 + .../benchmark-startup.cpython-313.pyc | Bin 0 -> 25391 bytes src/data_controller.rs | 241 +++++++++++------- src/documents.rs | 28 ++ src/main.rs | 23 +- 5 files changed, 179 insertions(+), 118 deletions(-) create mode 100644 scripts/__pycache__/benchmark-startup.cpython-313.pyc diff --git a/platform/README.md b/platform/README.md index 5879769e..ff0bda19 100644 --- a/platform/README.md +++ b/platform/README.md @@ -77,6 +77,11 @@ staged as one zip in `snapshot-staging` beside the data directory because Core refuses to write snapshots inside the data and cache roots. A host that restricts export sources must accept both. +Shared code starts every picker and export through `documents::run_request`, +which runs the provider future on the Tokio runtime while the UI event loop +keeps going. A host that needs another threading policy while its picker is +up changes that one function. + ### Known gaps - iOS has no Keychain-backed `CredentialStore` yet and uses diff --git a/scripts/__pycache__/benchmark-startup.cpython-313.pyc b/scripts/__pycache__/benchmark-startup.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02416af2ea243c5772f6bd7c78c5b89e6c457168 GIT binary patch literal 25391 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 Q(wqN9AMB^}L;wH) literal 0 HcmV?d00001 diff --git a/src/data_controller.rs b/src/data_controller.rs index 1c0cd956..3d28693e 100644 --- a/src/data_controller.rs +++ b/src/data_controller.rs @@ -640,23 +640,25 @@ fn import_account_backup_document( /// Hands generated bytes to the document provider through a private staging /// file; the host copies it to the destination its picker offers. -async fn export_document_bytes( - documents: &std::sync::Arc, +fn export_document_bytes( + documents: std::sync::Arc, staging_root: PathBuf, bytes: Vec, name: String, dialog: documents::DocumentDialog, -) -> Result { - tokio::fs::create_dir_all(&staging_root) - .await - .map_err(|error| format!("creating {}: {error}", staging_root.display()))?; - let staging = tempfile::tempdir_in(&staging_root) - .map_err(|error| format!("creating staging in {}: {error}", staging_root.display()))?; - let path = staging.path().join(&name); - tokio::fs::write(&path, bytes) - .await - .map_err(|error| format!("writing {}: {error}", path.display()))?; - documents.export_with(path, name, dialog).await +) -> documents::DocumentFuture { + Box::pin(async move { + tokio::fs::create_dir_all(&staging_root) + .await + .map_err(|error| format!("creating {}: {error}", staging_root.display()))?; + let staging = tempfile::tempdir_in(&staging_root) + .map_err(|error| format!("creating staging in {}: {error}", staging_root.display()))?; + let path = staging.path().join(&name); + tokio::fs::write(&path, bytes) + .await + .map_err(|error| format!("writing {}: {error}", path.display()))?; + documents.export_with(path, name, dialog).await + }) } fn backup_dialog(app: &AppWindow, title: UiMessage) -> documents::DocumentDialog { @@ -694,19 +696,35 @@ fn snapshot_staging_root(paths: &Paths) -> Result { .join("snapshot-staging")) } +/// A zipped snapshot waiting in staging for the export picker. #[cfg(mobile)] -async fn export_database_snapshot( - core: &CoreMailSource, - documents: &std::sync::Arc, - paths: &Paths, - staging: &tokio::sync::Mutex<()>, -) -> Result, String> { - let _staging = staging.lock().await; - let root = snapshot_staging_root(paths)?; +struct StagedSnapshot { + root: PathBuf, + archive: PathBuf, + name: String, + /// Keeps the startup cleanup out of the staging root until it is removed. + _staging: tokio::sync::OwnedMutexGuard<()>, +} + +#[cfg(mobile)] +impl StagedSnapshot { + async fn remove(self) { + let _ = tokio::fs::remove_dir_all(&self.root).await; + } +} + +#[cfg(mobile)] +async fn stage_database_snapshot( + core: CoreMailSource, + paths: Paths, + staging: Arc>, +) -> Result { + let staging = staging.lock_owned().await; + let root = snapshot_staging_root(&paths)?; // The root only ever holds this export, so clearing it also removes what // an interrupted export left behind. let _ = tokio::fs::remove_dir_all(&root).await; - let result = async { + let staged = async { tokio::fs::create_dir_all(&root) .await .map_err(|error| format!("creating {}: {error}", root.display()))?; @@ -730,12 +748,21 @@ async fn export_database_snapshot( }) .await .map_err(|error| error.to_string())??; - let exported = documents.export(archive, name.clone()).await?; - Ok(exported.then_some(name)) + Ok((archive, name)) } .await; - let _ = tokio::fs::remove_dir_all(&root).await; - result + match staged { + Ok((archive, name)) => Ok(StagedSnapshot { + root, + archive, + name, + _staging: staging, + }), + Err(error) => { + let _ = tokio::fs::remove_dir_all(&root).await; + Err(error) + } + } } /// Archives a snapshot directory without compression: the stores are dense @@ -836,7 +863,8 @@ pub(super) fn register_data_management_callbacks( documents: &std::sync::Arc, state: &Rc>, runtime: &Rc, - ui_task_tx: &UiSender, + // Only the desktop snapshot export reports through the task channel. + #[cfg_attr(mobile, allow(unused_variables))] ui_task_tx: &UiSender, contact_state: &Rc>, contacts_loaded: &Rc>, contacts_loading: &Rc>, @@ -1020,23 +1048,18 @@ pub(super) fn register_data_management_callbacks( "flectar-mail-backup-{}.json", Local::now().format("%Y-%m-%d") ); - let documents = documents_for_backup.clone(); - let staging_root = paths_for_backup.temp_dir(); let dialog = backup_dialog(&app, UiMessage::plain("Export Flectar Mail account backup")); - // Providers rely on Tokio timers and file I/O, so the export runs on - // the runtime and the result is reported from the UI thread. - let export = runtime_for_backup.spawn(async move { - export_document_bytes(&documents, staging_root, bytes, name, dialog).await - }); - let abort_export = export.abort_handle(); + let request = export_document_bytes( + documents_for_backup.clone(), + paths_for_backup.temp_dir(), + bytes, + name, + dialog, + ); let app_weak = app.as_weak(); let exporting = Rc::clone(&exporting_backup); exporting.set(true); - let spawned = slint::spawn_local(async move { - let result = match export.await { - Ok(result) => result, - Err(error) => Err(error.to_string()), - }; + documents::run_request(&runtime_for_backup, request, move |result| { exporting.set(false); let Some(app) = app_weak.upgrade() else { return; @@ -1051,16 +1074,12 @@ pub(super) fn register_data_management_callbacks( } } }); - if let Err(error) = spawned { - abort_export.abort(); - exporting_backup.set(false); - app.set_sync_status(UiMessage::detail("Could not export backup: {}", error)); - } }); let app_weak = app.as_weak(); let state_for_database_snapshot = Rc::clone(state); let runtime_for_database_snapshot = Rc::clone(runtime); + #[cfg(desktop)] let updates_for_database_snapshot = ui_task_tx.clone(); #[cfg(mobile)] let documents_for_database_snapshot = documents.clone(); @@ -1106,49 +1125,89 @@ pub(super) fn register_data_management_callbacks( database_snapshot_running.store(true, Ordering::Release); app.set_sync_status(UiMessage::plain("Creating verified database snapshot…")); - let updates = updates_for_database_snapshot.clone(); - #[cfg(mobile)] - let documents = documents_for_database_snapshot.clone(); - #[cfg(mobile)] - let paths = paths_for_database_snapshot.clone(); - #[cfg(mobile)] - let staging = Arc::clone(&snapshot_staging); let running = SnapshotRunning(Arc::clone(&database_snapshot_running)); - runtime_for_database_snapshot.spawn(async move { - #[cfg(desktop)] - let message = { + #[cfg(desktop)] + { + let updates = updates_for_database_snapshot.clone(); + runtime_for_database_snapshot.spawn(async move { let display_path = path.display().to_string(); - match core.create_database_snapshot(path).await { + let message = match core.create_database_snapshot(path).await { Ok(_) => UiMessage::detail("Database snapshot exported to {}.", display_path), Err(error) => { UiMessage::detail("Could not export database snapshot: {}", error) } - } - }; - #[cfg(mobile)] - let message = match export_database_snapshot(&core, &documents, &paths, &staging).await - { - Ok(Some(name)) => UiMessage::detail("Database snapshot exported to {}.", name), - // Cancelled at the picker: return the status line to idle. - Ok(None) => UiMessage::plain("Local data ready."), - Err(error) => UiMessage::detail("Could not export database snapshot: {}", error), - }; - drop(running); - let _ = updates - .send(UiTaskUpdate { - message, - accounts: None, - calendar_connections: None, - carddav_connections: None, - calendar_error: None, - account_removal: None, - clear_account_form: false, - finishes_account_setup: false, - finishes_oauth: false, - close_to_tray: None, - }) - .await; - }); + }; + drop(running); + let _ = updates + .send(UiTaskUpdate { + message, + accounts: None, + calendar_connections: None, + carddav_connections: None, + calendar_error: None, + account_removal: None, + clear_account_form: false, + finishes_account_setup: false, + finishes_oauth: false, + close_to_tray: None, + }) + .await; + }); + } + // Staging runs on the runtime; only the export picker goes through + // the shared document request path. + #[cfg(mobile)] + { + let staged = runtime_for_database_snapshot.spawn(stage_database_snapshot( + core, + paths_for_database_snapshot.clone(), + Arc::clone(&snapshot_staging), + )); + let abort_staging = staged.abort_handle(); + let app_weak = app.as_weak(); + let documents = documents_for_database_snapshot.clone(); + let runtime = Rc::clone(&runtime_for_database_snapshot); + let spawned = slint::spawn_local(async move { + let snapshot = match staged.await.unwrap_or_else(|error| Err(error.to_string())) { + Ok(snapshot) => snapshot, + Err(error) => { + drop(running); + if let Some(app) = app_weak.upgrade() { + app.set_sync_status(UiMessage::detail( + "Could not export database snapshot: {}", + error, + )); + } + return; + } + }; + let request = documents.export(snapshot.archive.clone(), snapshot.name.clone()); + let cleanup = runtime.handle().clone(); + documents::run_request(&runtime, request, move |exported| { + let name = snapshot.name.clone(); + cleanup.spawn(snapshot.remove()); + drop(running); + let Some(app) = app_weak.upgrade() else { + return; + }; + app.set_sync_status(match exported { + Ok(true) => UiMessage::detail("Database snapshot exported to {}.", name), + // Cancelled at the picker: return the status line to idle. + Ok(false) => UiMessage::plain("Local data ready."), + Err(error) => { + UiMessage::detail("Could not export database snapshot: {}", error) + } + }); + }); + }); + if let Err(error) = spawned { + abort_staging.abort(); + app.set_sync_status(UiMessage::detail( + "Could not export database snapshot: {}", + error, + )); + } + } }); let app_weak = app.as_weak(); @@ -1170,23 +1229,16 @@ pub(super) fn register_data_management_callbacks( )); return; }; - // The picker runs on the runtime (providers rely on Tokio timers); - // the import keeps its synchronous shape on the UI thread so the + // The import keeps its synchronous shape on the UI thread so the // existing state updates stay on the main loop. let dialog = backup_dialog(&app, UiMessage::plain("Import Flectar Mail account backup")); - let request = - runtime_for_backup_import.spawn(documents_for_backup_import.import_with(dialog)); - let abort_request = request.abort_handle(); + let request = documents_for_backup_import.import_with(dialog); let app_weak = app.as_weak(); let state = Rc::clone(&state_for_backup_import); let runtime = Rc::clone(&runtime_for_backup_import); let importing = Rc::clone(&importing_backup); importing.set(true); - let spawned = slint::spawn_local(async move { - let imported = match request.await { - Ok(result) => result, - Err(error) => Err(error.to_string()), - }; + documents::run_request(&runtime_for_backup_import, request, move |imported| { importing.set(false); let Some(app) = app_weak.upgrade() else { return; @@ -1201,11 +1253,6 @@ pub(super) fn register_data_management_callbacks( }; import_account_backup_document(&app, document.path(), &core, &state, &runtime); }); - if let Err(error) = spawned { - abort_request.abort(); - importing_backup.set(false); - app.set_sync_status(UiMessage::detail("Could not import backup: {}", error)); - } }); let (data_reset_raw_tx, data_reset_rx) = bounded_ui_channel::>(); diff --git a/src/documents.rs b/src/documents.rs index f9b52400..e5ca3d54 100644 --- a/src/documents.rs +++ b/src/documents.rs @@ -82,6 +82,34 @@ pub trait DocumentProvider: Send + Sync { Box::pin(async move { Ok(import.await?.into_iter().collect()) }) } } +/// Runs one document request and hands its result to `on_ready` on the UI +/// thread. Providers rely on Tokio timers and file I/O, so the request runs on +/// the runtime while the event loop keeps going, which desktop dialogs need. +/// Every picker and export goes through here, so a host that must keep the +/// presentation thread idle while its picker covers the window changes only +/// this function. +pub(crate) fn run_request( + runtime: &tokio::runtime::Runtime, + request: DocumentFuture, + on_ready: impl FnOnce(Result) + 'static, +) { + let request = runtime.spawn(request); + let abort = request.abort_handle(); + let on_ready = std::rc::Rc::new(std::cell::Cell::new(Some(on_ready))); + let pending = std::rc::Rc::clone(&on_ready); + let spawned = slint::spawn_local(async move { + let result = request.await.unwrap_or_else(|error| Err(error.to_string())); + if let Some(on_ready) = pending.take() { + on_ready(result); + } + }); + if let Err(error) = spawned { + abort.abort(); + if let Some(on_ready) = on_ready.take() { + on_ready(Err(error.to_string())); + } + } +} /// Desktop file dialogs. Mobile hosts inject their own provider through /// `PlatformContext::app_private`. #[cfg(desktop)] diff --git a/src/main.rs b/src/main.rs index 8af02c09..49e4e327 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5722,27 +5722,17 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> if picking_for_picker.replace(true) { return; } - // Providers rely on Tokio timers and file I/O, so the request runs on - // the runtime and the composer awaits it on the UI thread. let dialog = documents::DocumentDialog::titled(translated( &app, &UiMessage::plain("Attach files to this email"), )); - let request = runtime_for_picker.spawn( - documents_for_picker.import_many(MAX_COMPOSE_ATTACHMENT_FILES, dialog), - ); - let abort_request = request.abort_handle(); + let request = documents_for_picker.import_many(MAX_COMPOSE_ATTACHMENT_FILES, dialog); let app_weak = app.as_weak(); let files = Rc::clone(&files_for_picker); let session = Rc::clone(&session_for_picker); let picked_in = session.get(); let picking = Rc::clone(&picking_for_picker); - let picking_for_error = Rc::clone(&picking_for_picker); - let spawned = slint::spawn_local(async move { - let imported = match request.await { - Ok(result) => result, - Err(error) => Err(error.to_string()), - }; + documents::run_request(&runtime_for_picker, request, move |imported| { picking.set(false); let Some(app) = app_weak.upgrade() else { return; @@ -5819,15 +5809,6 @@ pub fn run(platform: PlatformContext) -> Result<(), Box> app.set_compose_notice_is_error(added == 0); } }); - if let Err(error) = spawned { - abort_request.abort(); - picking_for_error.set(false); - app.set_compose_notice(UiMessage::detail( - "Could not add attachments: {}", - error, - )); - app.set_compose_notice_is_error(true); - } }); let app_weak = app.as_weak(); From 5a735117ce1a8480328dc8301fa3d74536937e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Romero?= Date: Sun, 18 Oct 2026 14:31:55 +0200 Subject: [PATCH 11/11] chore: drop stray python cache --- .../benchmark-startup.cpython-313.pyc | Bin 25391 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 scripts/__pycache__/benchmark-startup.cpython-313.pyc diff --git a/scripts/__pycache__/benchmark-startup.cpython-313.pyc b/scripts/__pycache__/benchmark-startup.cpython-313.pyc deleted file mode 100644 index 02416af2ea243c5772f6bd7c78c5b89e6c457168..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25391 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 Q(wqN9AMB^}L;wH)