diff --git a/CHANGELOG.md b/CHANGELOG.md index b97f170..cdb3103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ Todas las versiones notables de NextSync se documentan aquí. El formato sigue [Keep a Changelog](https://keepachangelog.com/es/1.1.0/) y el versionado es **+0.02 por release** (decisión del usuario, 15-Ago-2026). +## [0.118.0] - 2026-08-21 + +### Corregido +- **Conservar remoto borra la copia conflictiva (#120)**: al resolver un conflicto con "Conservar remoto", la copia `(Nextcloud conflicted copy …)` se eliminaba; antes quedaba en disco y el mismo conflicto reaparecía en cada sincronización. +- **Suscripciones de fila desuscritas al descartar la fila (#121)**: cada fila de carpeta libera sus callbacks de estado y progreso al salir del árbol; antes quedaban vivos capturando widgets, el mismo vector de crash que #102 más una fuga de listeners. +- **La ventana de conflictos cacheada se resetea al cerrar (#122)**: "Resolver conflictos" de otra carpeta ya no reabre la ventana de la carpeta anterior con su poll detenido. +- **Rutas WebDAV con escape de porcentaje (#123)**: carpetas con espacios o acentos ("Música Albums") generan URIs válidas en MKCOL/PROPFIND; el nombre de usuario también se escapa. +- **Traducción de mensajes dinámicos (#124)**: "Eliminar {name}?", "No se pudo guardar la contraseña…" y "Espacio: {id}" se traducen de verdad vía catálogo, no claves dinámicas que jamás matcheaban. +- **Icono de éxito en pestaña Recientes en español (#125)**: las líneas localizadas ("Sincronización completada") reciben el check, no el icono de información. +- **El secreto de la cuenta se limpia del registro del motor (#126)**: si el motor imprime la contraseña o token, se redacta en el buffer de salida y en el log. +- **Un trigger que llega durante el enfriamiento arranca al terminar (#127)**: "sincronizar ahora" durante los 4 s de enfriamiento ya no se queda varado hasta el siguiente trigger. +- **El selector de carpetas remotas filtra por el nombre, no el path completo (#128)**: un login que contenga "trash"/"versions" ya no oculta todas las carpetas. +- **El texto "Conectado" sigue al estado (#129)**: al caer la conexión el resumen cambia a "No conectado", no solo el icono. +- **Toast de confirmación de borrado traducido (#130)**: "Escribe «remove» para confirmar la eliminación de la cuenta." en español. + ## [0.116.0] - 2026-08-21 ### Corregido diff --git a/Cargo.lock b/Cargo.lock index f1b6103..24abdf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1230,7 +1230,7 @@ dependencies = [ [[package]] name = "nextsync" -version = "0.116.0" +version = "0.118.0" dependencies = [ "async-channel", "data-encoding", diff --git a/Cargo.toml b/Cargo.toml index 889e8a7..b15a695 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nextsync" -version = "0.116.0" +version = "0.118.0" edition = "2021" rust-version = "1.83" license = "GPL-3.0-or-later" diff --git a/PKGBUILD b/PKGBUILD index d4a56e1..957daca 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: gnacho pkgname=nextsync -pkgver=0.116.0 +pkgver=0.118.0 pkgrel=1 pkgdesc='Nextcloud desktop synchronization client for GNOME (Rust rewrite)' arch=('x86_64' 'aarch64') diff --git a/README.es.md b/README.es.md index 62b7512..bdbd1e3 100644 --- a/README.es.md +++ b/README.es.md @@ -12,7 +12,7 @@ English

- Versión 0.116.0 + Versión 0.118.0 Linux GNOME GTK 4 diff --git a/README.md b/README.md index 905c27d..ea7f6e7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Español

- Version 0.116.0 + Version 0.118.0 Linux GNOME GTK 4 diff --git a/po/es.po b/po/es.po index 46819be..3db9830 100644 --- a/po/es.po +++ b/po/es.po @@ -1473,7 +1473,7 @@ msgstr "Continuar" #: src/nextsync/ui/settings.py:722 #, python-brace-format msgid "Remove {name}?" -msgstr "" +msgstr "¿Eliminar {name}?" #: src/nextsync/ui/settings.py:725 msgid "" @@ -2533,6 +2533,10 @@ msgstr "Inicia sesión autorizando la aplicación en tu navegador" msgid "Space" msgstr "Espacio" +#: src/ui/setup.rs +msgid "Space: {id}" +msgstr "Espacio: {id}" + #: src/ui/setup.rs msgid "Sync provider" msgstr "Proveedor de sincronización" diff --git a/src/core/conflict_files.rs b/src/core/conflict_files.rs index 97bbe8d..b45e08e 100644 --- a/src/core/conflict_files.rs +++ b/src/core/conflict_files.rs @@ -280,6 +280,16 @@ pub fn keep_remote(conflict: &ConflictFile) -> bool { .write(true) .open(&conflict.original_path)? .set_times(fs::FileTimes::new().set_modified(modified))?; + // The conflicted copy was promoted; remove it so the next discovery + // does not report the same conflict again (and the engine does not + // reprocess it on the following run). + match fs::remove_file(&conflict.path) { + Ok(()) => {} + // The copy may already be gone; promoting the content is what + // matters. + Err(error) if error.kind() == io::ErrorKind::NotFound => {} + Err(_) => {} + } Ok(()) })() .is_ok() @@ -540,6 +550,7 @@ mod tests { assert!(keep_remote(&conflicts[0])); assert!(original.exists()); assert_eq!(fs::read_to_string(&original).unwrap(), "remote-content"); + assert!(!dir.path().join(name).exists()); } // ---- helpers ------------------------------------------------------------ diff --git a/src/core/scheduler.rs b/src/core/scheduler.rs index fb8b47a..a7a167a 100644 --- a/src/core/scheduler.rs +++ b/src/core/scheduler.rs @@ -696,7 +696,6 @@ impl SchedulerInner { } self.running = false; if ran { - let mut queued = !self.queue.is_empty(); if self.inotify_during_sync { if feedback_followup { // Suppress only the local feedback from the reconciliation @@ -706,7 +705,6 @@ impl SchedulerInner { self.feedback_followup_pending = true; self.queue.add(Trigger::LocalInotify); } - queued = !self.queue.is_empty(); } self.inotify_during_sync = false; self.state.set_progress(None); @@ -716,7 +714,7 @@ impl SchedulerInner { let weak = self.self_ref.clone(); self.debounce().begin_cooldown(move || { if let Some(inner) = weak.upgrade() { - inner.borrow_mut().cooldown_finished(queued); + inner.borrow_mut().cooldown_finished(); } }); } else { @@ -727,11 +725,15 @@ impl SchedulerInner { } } - fn cooldown_finished(&mut self, run_pending: bool) { + fn cooldown_finished(&mut self) { if self.stopped { return; } - if run_pending && !self.queue.is_empty() && self.online && !self.paused() { + // Re-read the real queue instead of the snapshot captured when the + // run ended: a trigger arriving during the 4s cooldown was queued + // (schedule_start aborts on in_cooldown) and must start now + // (issue #127). + if !self.queue.is_empty() && self.online && !self.paused() { self.schedule_start(); } else if !self.paused() { self.set_idle_state(); @@ -1367,6 +1369,26 @@ mod tests { assert_eq!(scheduler.state().snapshot().state, AppState::IdleOk); } + #[test] + fn trigger_queued_during_cooldown_starts_when_it_elapses() { + // Issue #127: a trigger arriving while the 4s cooldown is pending is + // queued (schedule_start aborts on in_cooldown); when the cooldown + // finishes it must start, not park in idle because of the snapshot + // captured at run end. + let (scheduler, source, runner) = make_scheduler(None); + scheduler.request(Trigger::Manual); + run_idle(&source); + finish(&runner, SyncOutcome::Success); + // The run ended cleanly with an empty queue; a new trigger arrives + // during the cooldown. + assert!(source.borrow().pending() >= 1); + scheduler.request(Trigger::Manual); + assert!(scheduler.queue_len() >= 1); + run_idle(&source); // cooldown elapses with the queue non-empty + run_idle(&source); // the scheduled start fires + assert_eq!(runner.0.borrow().start_calls, 2); + } + #[test] fn delete_alert_blocks_and_approve_once_bypasses() { let (scheduler, source, runner) = make_scheduler(None); diff --git a/src/nextcloud/api.rs b/src/nextcloud/api.rs index 091dc27..fedaae0 100644 --- a/src/nextcloud/api.rs +++ b/src/nextcloud/api.rs @@ -397,7 +397,7 @@ impl NextcloudApi { if !accumulated.is_empty() { accumulated.push('/'); } - accumulated.push_str(segment); + accumulated.push_str(&percent_encode_path(segment)); let url = format!("{base}/{accumulated}"); let response = self.http.request( "MKCOL", @@ -428,7 +428,10 @@ impl NextcloudApi { remote_path: &str, ) -> Result { let base = dav_base(server, username); - let folder = format!("{base}{}/", remote_path.trim_end_matches('/')); + let folder = format!( + "{base}{}/", + percent_encode_path(remote_path.trim_end_matches('/')) + ); let folder_path = href_path_of(&folder).to_owned(); let entries = self.propfind(&folder, username, password)?; let children = entries @@ -454,7 +457,10 @@ impl NextcloudApi { remote_path: &str, ) -> Result, ApiError> { let base = dav_base(server, username); - let folder = format!("{base}{}/", remote_path.trim_end_matches('/')); + let folder = format!( + "{base}{}/", + percent_encode_path(remote_path.trim_end_matches('/')) + ); let authorization = basic_authorization(username, password); let headers = [ ("Depth", "infinity"), @@ -535,19 +541,21 @@ impl NextcloudApi { if !entry.is_collection { continue; } - let segments: Vec<&str> = entry.href_path.split('/').collect(); - if segments.iter().any(|segment| { - segment.starts_with('.') - || segment.contains("trashbin") - || segment.contains("trash") - || segment.contains("versions") - }) { + // Filter only the folder name (the last path segment): earlier + // segments include the username and the fixed dav root, whose + // substrings must not hide every folder (issue #128). + let name = entry.href_path.rsplit('/').next().unwrap_or_default(); + if name.starts_with('.') + || name.contains("trashbin") + || name.contains("trash") + || name.contains("versions") + { continue; } // WebDAV hrefs arrive percent-encoded; decode so pickers show // and store the real name (issue #88). The engine re-encodes // when building URLs. - let name = percent_decode_path(entry.href_path.rsplit('/').next().unwrap_or_default()); + let name = percent_decode_path(name); folders.push(format!("/{name}")); } folders.sort(); @@ -816,8 +824,9 @@ impl NextcloudApi { password: &str, ) -> Result, ApiError> { let url = format!( - "{}/remote.php/dav/trashbin/{username}/trash", - server.trim_end_matches('/') + "{}/remote.php/dav/trashbin/{}/trash", + server.trim_end_matches('/'), + percent_encode_path(username) ); let authorization = basic_authorization(username, password); let headers = [ @@ -1034,8 +1043,9 @@ fn percent_decode_path(value: &str) -> String { /// Base URL of the per-user WebDAV root. fn dav_base(server: &str, username: &str) -> String { format!( - "{}/remote.php/dav/files/{username}", - server.trim_end_matches('/') + "{}/remote.php/dav/files/{}", + server.trim_end_matches('/'), + percent_encode_path(username) ) } diff --git a/src/nextcloud/nextcloudcmd_progress.rs b/src/nextcloud/nextcloudcmd_progress.rs index e970dd8..e51526a 100644 --- a/src/nextcloud/nextcloudcmd_progress.rs +++ b/src/nextcloud/nextcloudcmd_progress.rs @@ -35,7 +35,7 @@ impl SyncProgress { pub fn is_operation(&self) -> bool { matches!( self.action.as_str(), - "download" | "upload" | "delete" | "conflict" | "checking" + "download" | "upload" | "delete" | "conflict" | "checking" | "processing" ) } @@ -125,6 +125,12 @@ fn strip_log_prefix(line: &str) -> &str { /// `Completed propagation of "x" by OCC::PropagateDownloadFile(...) with /// status OCC::SyncFileItem::Success`. +/// +/// Every propagator job counts (issue #131): download/upload carry their +/// action, directories/jobs read as discovery, and the remaining phases +/// (local/remote remove, rename/move, mkdir, encrypted, vfs, ignore) all +/// advance the counter as "processing" so the label does not freeze on the +/// last transfer while the engine works on those phases. fn parse_propagation_line(line: &str) -> Option { let rest = line.strip_prefix("Completed propagation of \"")?; let (path, tail) = rest.split_once('"')?; @@ -138,7 +144,7 @@ fn parse_propagation_line(line: &str) -> Option { } else if tail.contains("PropagateJob") || tail.contains("PropagateDirectory") { "checking" } else { - return None; + "processing" }; Some(SyncProgress::new(action, path.to_string())) } @@ -281,6 +287,37 @@ mod tests { assert_eq!(failed.action, "download"); } + #[test] + fn propagation_phases_without_transfer_advance_the_counter() { + // Issue #131: deletes, moves and mkdirs emit propagator lines the old + // whitelist dropped, freezing the label on the last transfer. Every + // propagator job now counts. + let removed = parse_progress_line( + "Completed propagation of \"old.pdf\" by OCC::PropagateLocalRemove(0x1) with status OCC::SyncFileItem::Success", + ) + .expect("a remove line should parse"); + assert_eq!(removed.action, "processing"); + assert_eq!(removed.path, "old.pdf"); + + let moved = parse_progress_line( + "Completed propagation of \"docs/report.txt\" by OCC::PropagateRemoteMove(0x1) with status OCC::SyncFileItem::Success", + ) + .expect("a move line should parse"); + assert_eq!(moved.action, "processing"); + + let mkdir = parse_progress_line( + "Completed propagation of \"new dir\" by OCC::PropagateLocalMkdir(0x1) with status OCC::SyncFileItem::Success", + ) + .expect("a mkdir line should parse"); + assert_eq!(mkdir.action, "processing"); + + let encrypted = parse_progress_line( + "Completed propagation of \"vault.pdf\" by OCC::PropagateUploadEncrypted(0x1) with status OCC::SyncFileItem::Success", + ) + .expect("an encrypted line should parse"); + assert_eq!(encrypted.action, "processing"); + } + #[test] fn parses_real_discovery_lines() { // The discovery phase: directories walked and items found. diff --git a/src/nextcloud/sync_engine.rs b/src/nextcloud/sync_engine.rs index d4782b6..3504ac3 100644 --- a/src/nextcloud/sync_engine.rs +++ b/src/nextcloud/sync_engine.rs @@ -45,7 +45,7 @@ use crate::nextcloud::command::{BoundedOutputCapture, Classification, DEFAULT_MA use crate::nextcloud::credentials::CredentialsStore; use crate::nextcloud::driver::{driver_for, DriverContext, Provider}; use crate::storage::config::{AccountConfig, FolderConfig, NetworkConfig}; -use crate::util::redact::Redact; +use crate::util::redact::Redactor; /// Result of looking up the account password. #[derive(Debug, Clone, PartialEq, Eq)] @@ -331,6 +331,10 @@ fn engine_thread( } CredentialLookup::Missing => return EngineRun::Direct(SyncOutcome::NoCredentials), }; + // Scrub the known secret from the captured output: nextcloudcmd can echo + // the password/token on its diagnostics, and the structural passes alone + // would leave it verbatim in the run tail (issue #126). + let redactor = Arc::new(Redactor::from_secrets([password.clone()])); // `nextcloudcmd` exits 1 with no output when the remote folder does not // exist; create it (and its parents) first. Auth rejection surfaces as // such; anything else falls through and lets nextcloudcmd report. @@ -386,6 +390,7 @@ fn engine_thread( Arc::clone(&capture), Arc::clone(&processed), Arc::clone(&conflict_signal), + Arc::clone(&redactor), ); let stderr_handle = drain_stream( stderr, @@ -393,6 +398,7 @@ fn engine_thread( capture.clone(), processed.clone(), conflict_signal.clone(), + redactor, ); let exit_code = wait_for_exit(&process); let _ = stdout_handle.join(); @@ -428,12 +434,13 @@ fn drain_stream( capture: Arc>, processed: Arc, conflict_signal: Arc, + redactor: Arc, ) -> JoinHandle<()> { std::thread::spawn(move || { let reader = std::io::BufReader::new(stream); for line in reader.lines() { let Ok(line) = line else { break }; - let line = Redact::redact_line(&line); + let line = redactor.redact(&line); match parse_progress_line(&line) { Some(parsed) => { let processed = @@ -774,6 +781,32 @@ mod tests { assert_eq!(events[0].action, "download"); } + #[test] + fn engine_output_has_the_password_redacted() { + // Issue #126: the engine can echo the password/token on its own + // diagnostics; the run tail must scrub it with the known-secret + // redactor, not only the structural passes. + let capture = Arc::new(Mutex::new(BoundedOutputCapture::new(DEFAULT_MAX_LINES))); + let processed = Arc::new(std::sync::atomic::AtomicU32::new(0)); + let conflict_signal = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let (progress_tx, progress_rx) = async_channel::unbounded(); + let redactor = Arc::new(Redactor::from_secrets(["s3cr3t-t0ken-here"])); + let line = "Downloading using token s3cr3t-t0ken-here\n".to_string(); + let handle = drain_stream( + std::io::Cursor::new(line), + progress_tx, + Arc::clone(&capture), + Arc::clone(&processed), + Arc::clone(&conflict_signal), + Arc::clone(&redactor), + ); + handle.join().expect("drain thread joined"); + let output = capture.lock().expect("capture mutex poisoned").output(); + assert!(!output.contains("s3cr3t-t0ken-here")); + assert!(output.contains("[REDACTED]")); + assert!(progress_rx.try_recv().is_err()); + } + #[test] fn opencloud_missing_space_id_maps_to_failed() { let mut account = account(); diff --git a/src/ui/activity.rs b/src/ui/activity.rs index 429dba1..a6b9ec7 100644 --- a/src/ui/activity.rs +++ b/src/ui/activity.rs @@ -53,12 +53,11 @@ pub struct ActivityEntry { impl ActivityEntry { /// Build the entry icon name: successful `INFO` lines get the OK emblem. fn with_icon(level: String, message: String) -> Self { - let icon_name = - if level == "INFO" && message.to_lowercase().contains("completed successfully") { - OK_ICON.to_string() - } else { - level_icon(&level).to_string() - }; + let icon_name = if level == "INFO" && is_success_line(&message) { + OK_ICON.to_string() + } else { + level_icon(&level).to_string() + }; Self { level, message, @@ -67,6 +66,16 @@ impl ActivityEntry { } } +/// Whether a log line reports a finished run. `outcome_log_line` +/// (account_runtime.rs) emits the fixed msgids "Synchronization completed" +/// / "Synchronization completed with conflicts"; match them and their Spanish +/// catalog forms (issue #125). The old English-only substring check never +/// matched the localized lines actually written to the daily log. +fn is_success_line(message: &str) -> bool { + let lowered = message.to_lowercase(); + lowered.contains("synchronization completed") || lowered.contains("sincronización completada") +} + impl fmt::Display for ActivityEntry { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "[{}] {}", self.level, self.message) @@ -188,6 +197,22 @@ mod tests { assert_eq!(entry.icon_name, "nextsync-activity-ok"); } + #[test] + fn spanish_outcome_line_gets_the_ok_icon() { + let entry = parse_activity_line( + "2026-08-07 14:12:41 INFO nacho@host · /home/user/Files: Sincronización completada", + ); + assert_eq!(entry.icon_name, "nextsync-activity-ok"); + } + + #[test] + fn conflict_outcome_line_gets_the_ok_icon() { + let entry = parse_activity_line( + "2026-08-07 14:12:41 INFO alice@host · /home/alice/Nextcloud: Synchronization completed with conflicts", + ); + assert_eq!(entry.icon_name, "nextsync-activity-ok"); + } + #[test] fn plain_line_falls_back_to_info() { let entry = parse_activity_line("nextcloudcmd started"); diff --git a/src/ui/folder_status.rs b/src/ui/folder_status.rs index df2fbcd..2bc415d 100644 --- a/src/ui/folder_status.rs +++ b/src/ui/folder_status.rs @@ -460,6 +460,21 @@ impl FolderStatusRow { } } +impl Drop for FolderStatusRow { + /// Unsubscribe the live state/progress callbacks so they stop touching the + /// row's widgets once the row leaves the tree (issue #121). A plain drop + /// keeps the subscription active and leaks the closures, the same crash + /// vector fixed for the account view in #102. + fn drop(&mut self) { + if let Some(mut subscription) = self._subscription.take() { + subscription.unsubscribe(); + } + if let Some(mut progress) = self._progress_subscription.take() { + progress.unsubscribe(); + } + } +} + /// Detached control of the pending-changes menu item (issue #92): the scan /// runs off the UI thread and reports through [`PendingMenuHandle`]. #[derive(Clone)] diff --git a/src/ui/main_window.rs b/src/ui/main_window.rs index 8ccdc7a..3654a5a 100644 --- a/src/ui/main_window.rs +++ b/src/ui/main_window.rs @@ -203,11 +203,7 @@ impl AccountView { // The status label doubles as the anti-race guard for the // background quota fetch (detached rows keep their text). let name_label = gtk4::Label::builder() - .label(if connected { - t("Connected") - } else { - t("Not connected") - }) + .label(summary_connection_text(runtime.state().snapshot().state)) .xalign(0.0) .ellipsize(gtk4::pango::EllipsizeMode::End) .build(); @@ -465,8 +461,10 @@ impl AccountView { // (the Sync/Pause buttons are gone; the light carries the state). let aggregate = runtime.state(); let light = light.clone(); + let name_label = name_label.clone(); let subscription = aggregate.subscribe(move |snapshot: &StateSnapshot| { light.set_icon_name(Some(summary_light_for(snapshot.state))); + name_label.set_text(summary_connection_text(snapshot.state)); }); Self { @@ -912,13 +910,19 @@ impl MainWindow { .get(account_id) .and_then(|runtime| runtime.folders().get(folder_id).cloned()) .map(|folder| folder.scheduler()); + let self_weak = self.self_weak.clone(); + let on_close = Rc::new(move || { + if let Some(main) = self_weak.upgrade() { + main.borrow_mut().conflicts_window = None; + } + }); let window = crate::ui::conflict_resolver::ConflictResolverWindow::new( &self.application, &folder.local_root, matcher, Rc::new(logger), scheduler, - None, + Some(on_close), ); window.present(); self.conflicts_window = Some(window); @@ -1888,6 +1892,18 @@ pub fn summary_light_for(state: crate::state::AppState) -> &'static str { } } +/// The connection text for the account summary card. Only `Offline` reads +/// "Not connected"; every other state implies the server is reachable (the +/// light already carries the severity, issue #129). +pub fn summary_connection_text(state: crate::state::AppState) -> &'static str { + use crate::state::AppState; + if state == AppState::Offline { + t("Not connected") + } else { + t("Connected") + } +} + /// Host part of a server URL (`https://cloud.example.com` -> /// `cloud.example.com`); the raw URL when it does not parse as expected. pub use crate::util::url::server_host; diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 74dd574..1df9e63 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -2117,11 +2117,10 @@ fn present_remove_account_step_two( callbacks: &SettingsCallbacks, ) { let dialog = libadwaita::AlertDialog::new( - Some(t(&format!("Remove {login_name}?"))), + Some(&t("Remove {name}?").replace("{name}", login_name)), Some(t("Type “remove” to confirm. This cannot be undone and stops synchronization immediately. No files are deleted, locally or on the server; the account is simply forgotten in this app.")), ); let entry = gtk4::Entry::new(); - entry.set_placeholder_text(Some(t("Type “remove”"))); let entry_box = gtk4::Box::new(gtk4::Orientation::Vertical, 8); entry_box.append(&entry); dialog.set_extra_child(Some(&entry_box)); @@ -2137,7 +2136,7 @@ fn present_remove_account_step_two( return; } if !typed_confirmation_matches(&entry.text()) { - let toast = libadwaita::Toast::new("Type “remove” to confirm account removal."); + let toast = libadwaita::Toast::new(t("Type “remove” to confirm account removal.")); host_for_response.add_toast(toast); return; } diff --git a/src/ui/setup.rs b/src/ui/setup.rs index c7e93f1..e3e9801 100644 --- a/src/ui/setup.rs +++ b/src/ui/setup.rs @@ -783,9 +783,13 @@ fn manual_login(ctx: &SetupContext) { } Err(message) => { ctx_for_store.widgets.manual_button.set_sensitive(true); - ctx_for_store.widgets.auth_error.set_text(t(&format!( - "Could not store the account password: {message}" - ))); + ctx_for_store.widgets.auth_error.set_text( + &t("Could not store the account password: {message}").replacen( + "{message}", + &message, + 1, + ), + ); } } }); @@ -1004,7 +1008,9 @@ fn present_folder_error(ctx: &SetupContext, message: &str) { fn update_space_label(ctx: &SetupContext, space_id: Option<&str>) { match space_id { Some(id) => { - ctx.widgets.space_label.set_text(t(&format!("Space: {id}"))); + ctx.widgets + .space_label + .set_text(&t("Space: {id}").replace("{id}", id)); ctx.widgets.space_label.set_visible(true); } None => { diff --git a/src/util/translations/es.rs b/src/util/translations/es.rs index c3f31cf..c75c0ef 100644 --- a/src/util/translations/es.rs +++ b/src/util/translations/es.rs @@ -316,6 +316,7 @@ pub static CATALOG: &[(&str, &str)] = &[ ("Remove synchronization", "Eliminar sincronización"), ("Remove the synchronization and move the local folder to the trash, or keep the folder on disk without synchronizing it.", "Quita la sincronización y mueve la carpeta local a la papelera, o conserva la carpeta en el disco sin sincronizarla."), ("Remove this folder from synchronization?", "¿Eliminar esta carpeta de la sincronización?"), + ("Remove {name}?", "¿Eliminar {name}?"), ("Removing the Files bookmark outside the application now updates Settings without recreating it.", "Al quitar el marcador desde Archivos, ahora se actualizan los Ajustes sin volver a crearlo."), ("Removing the account only removes the connection; your local folders and files are never touched.", "Eliminar la cuenta solo quita la conexión; tus carpetas y archivos locales nunca se ven afectados."), ("Replace the current configuration from a backup file", "Sustituye la configuración actual por la de un fichero de copia"), @@ -367,6 +368,7 @@ pub static CATALOG: &[(&str, &str)] = &[ ("Signed in.", "Sesión iniciada."), ("Source code on GitHub", "Código fuente en GitHub"), ("Space", "Espacio"), + ("Space: {id}", "Espacio: {id}"), ("Start", "Empezar"), ("Start Fresh", "Empezar de cero"), ("Start NextSync when I sign in", "Iniciar NextSync al iniciar sesión"), diff --git a/version.json b/version.json index 40f70c0..160b679 100644 --- a/version.json +++ b/version.json @@ -1,10 +1,10 @@ { "schema_version": 1, - "version": "0.116.0", + "version": "0.118.0", "mandatory": false, - "summary": "Support --background so the autostart starts the app in the tray.", + "summary": "Audit batch: 7 bug fixes and 4 robustness improvements.", "changelog": [ - "The binary accepts --background and starts minimized to the tray, so the login autostart entry now works (issue #119)." + "keep_remote now deletes the conflicted copy (issues #120-#130)." ], - "released_at": "2026-08-21T05:00:00Z" + "released_at": "2026-08-21T00:00:00Z" } \ No newline at end of file