diff --git a/po/es.po b/po/es.po index 6dd6313..570be9f 100644 --- a/po/es.po +++ b/po/es.po @@ -920,10 +920,6 @@ msgstr "Sincronizado" msgid "Automatic Sync Is Off" msgstr "Sincronización automática desactivada" -#: src/nextsync/ui/folder_status.py:21 -msgid "Synchronization Scheduled" -msgstr "Sincronización programada" - #: src/nextsync/ui/folder_status.py:22 msgid "Synchronizing…" msgstr "Sincronizando…" @@ -2574,10 +2570,6 @@ msgstr "En pausa por batería" msgid "Synchronization is paused" msgstr "La sincronización está en pausa" -#: src/core/scheduler.rs -msgid "Synchronization scheduled" -msgstr "Sincronización programada" - #: src/core/scheduler.rs msgid "Synchronized with conflicts — review the log" msgstr "Sincronizado con conflictos: revisa el registro" @@ -2682,7 +2674,7 @@ msgid "Synced in local {folder}" msgstr "Sincronizado en local {folder}" msgid "Waiting to synchronize" -msgstr "Esperando para sincronizar" +msgstr "A la espera" msgid "checking {file}" msgstr "comprobando {file}" diff --git a/src/core/account_runtime.rs b/src/core/account_runtime.rs index 1b8ccbc..bf73ae6 100644 --- a/src/core/account_runtime.rs +++ b/src/core/account_runtime.rs @@ -330,6 +330,13 @@ impl FolderRuntime { // cannot repaint the row. let run_active = std::rc::Rc::new(std::cell::Cell::new(false)); self.scheduler.set_run_active(run_active.clone()); + // Issue #148: startup syncs begin before connect_progress() wires the + // forwarder, so the flag would stay false for the entire first run and + // the UI would show no per-file progress. Seed it from the scheduler + // state when connecting, so an in-flight run keeps emitting events. + if self.scheduler.is_running() { + run_active.set(true); + } glib::spawn_future_local(async move { while let Ok(progress) = progress_rx.recv().await { if !run_active.get() { diff --git a/src/core/scheduler.rs b/src/core/scheduler.rs index e1e679b..817f55c 100644 --- a/src/core/scheduler.rs +++ b/src/core/scheduler.rs @@ -501,7 +501,7 @@ impl SchedulerInner { return; } self.state - .set(AppState::SyncQueued, t("Synchronization scheduled")); + .set(AppState::SyncQueued, t("Waiting to synchronize")); let weak = self.self_ref.clone(); let id = self.source.borrow_mut().add_idle(Box::new(move || { if let Some(inner) = weak.upgrade() { diff --git a/src/ui/folder_status.rs b/src/ui/folder_status.rs index 2bc415d..7451cbd 100644 --- a/src/ui/folder_status.rs +++ b/src/ui/folder_status.rs @@ -29,7 +29,7 @@ pub fn folder_status_presentation(state: AppState) -> (&'static str, &'static st AppState::IdleOk => ("nextsync-row-ok", t("Synchronized")), AppState::IdleManualOnly => ("nextsync-row-paused", t("Automatic Sync Is Off")), AppState::IdleNotSynced => ("nextsync-row-paused", t("Not Synchronized Yet")), - AppState::SyncQueued => ("nextsync-row-paused", t("Synchronization Scheduled")), + AppState::SyncQueued => ("nextsync-row-paused", t("Waiting to synchronize")), AppState::Syncing => ("nextsync-row-syncing", t("Synchronizing…")), AppState::PausedUser => ("nextsync-row-paused", t("Paused")), AppState::PausedBattery => ("nextsync-row-battery", t("Paused on Battery")), @@ -396,6 +396,7 @@ impl FolderStatusRow { let spinner = this.spinner.clone(); let progress_bar = this.progress_bar.clone(); let row = this.row.clone(); + let slot = this.slot.clone(); let remote_path = this.remote_path.clone(); let format_last_sync = this.format_last_sync.clone(); let subscription = controller.subscribe(move |snapshot: &StateSnapshot| { @@ -408,6 +409,11 @@ impl FolderStatusRow { format_last_sync.as_ref().map(|f| f()), snapshot, ); + // Issue #150: GTK may not repaint the row when properties + // are updated from a subscription callback. Force a redraw + // of the whole slot so state, spinner, bar and caption all + // update together. + slot.queue_draw(); }); this._subscription = Some(subscription); // Live per-file progress (issue #86). Only the widgets are @@ -422,8 +428,12 @@ impl FolderStatusRow { .unwrap_or_else(|| progress.path.clone()); progress_label.set_text(&progress_line_text(progress, &file)); progress_label.set_visible(true); + progress_label.queue_draw(); + } + _ => { + progress_label.set_visible(false); + progress_label.queue_draw(); } - _ => progress_label.set_visible(false), }); this._progress_subscription = Some(progress_subscription); } @@ -745,7 +755,7 @@ mod tests { state.set(AppState::SyncQueued, "queued"); assert_eq!( row.row.subtitle().as_deref(), - Some("Synchronization Scheduled") + Some("Waiting to synchronize") ); state.set(AppState::IdleOk, "ok"); assert_eq!(row.row.subtitle().as_deref(), Some("Synced in local docs")); diff --git a/src/ui/main_window.rs b/src/ui/main_window.rs index 3cbf848..810fa4c 100644 --- a/src/ui/main_window.rs +++ b/src/ui/main_window.rs @@ -135,6 +135,11 @@ pub struct AccountView { /// Per-folder state subscriptions (used-space refreshes after each /// completed synchronization, issue #43). _folder_subscriptions: Vec, + /// The folder rows, owned for the whole lifetime of the view (issue + /// #151): dropping a FolderStatusRow unsubscribes its live state and + /// progress callbacks (issue #121), so the rows must live exactly as + /// long as they are on screen or they freeze at their initial state. + _folder_rows: Vec, } impl AccountView { @@ -281,6 +286,9 @@ impl AccountView { let pairs = pair_folder_runtimes(&account.folders, runtime.folders()); let mut folder_subscriptions: Vec = Vec::new(); + // The view owns its rows (issue #151): dropping one unsubscribes its + // live callbacks, so they stay alive until the whole view is rebuilt. + let mut folder_rows: Vec = Vec::new(); for (folder, folder_runtime) in pairs { let local_root = folder.local_root.clone(); let account_id = account.id.clone(); @@ -438,6 +446,7 @@ impl AccountView { folder_subscriptions.push(subscription); } account_list.append(&row.slot); + folder_rows.push(row); } if account.folders.is_empty() { let row = libadwaita::ActionRow::builder() @@ -472,6 +481,7 @@ impl AccountView { _account_runtime: account_runtime, _subscription: Some(subscription), _folder_subscriptions: folder_subscriptions, + _folder_rows: folder_rows, } } } @@ -2542,6 +2552,89 @@ mod tests { }); } + /// Regression test for issue #151: the view must own its FolderStatusRow + /// objects for as long as they are on screen. Dropping a row unsubscribes + /// its live state/progress callbacks (the Drop impl from issue #121), so + /// a view that does not retain its rows freezes every row at the state it + /// had when the view was built: no spinner, no run bar, no live file + /// progress, no queued state until the whole view is rebuilt. + #[test] + fn folder_rows_follow_live_state_changes() { + crate::ui::test_helpers::gtk_smoke(|| { + set_locale(Locale::English); + let dir = tempfile::tempdir().unwrap(); + let mut account = window_account(); + account.folders = vec![crate::storage::config::FolderConfig { + id: "f1".to_string(), + local_root: dir.path().join("docs").to_string_lossy().into_owned(), + remote_path: "/docs".to_string(), + space_id: None, + size_confirmed: false, + }]; + let mut runtime = AccountRuntime::new( + account, + crate::storage::config::NetworkConfig::default(), + std::rc::Rc::new(std::cell::RefCell::new( + crate::core::debounce::FakeTimeoutSource::default(), + )), + None, + false, + ); + runtime.start_without_watchers(); + let callbacks = AccountCallbacks { + on_open_folder: None, + on_remove_folder: None, + on_edit_ignored: None, + on_add_folder: None, + on_pending_changes: None, + on_review_deletions: None, + on_resolve_conflicts: None, + on_avatar_cached: None, + }; + let view = AccountView::new( + runtime.clone(), + callbacks, + crate::core::log::LogBuffer::new(), + ); + + // Locate the folder row: ListBox > ListBoxRow > slot > ActionRow. + let mut sibling = view.root.first_child(); + let mut list = None; + while let Some(widget) = sibling { + if let Ok(candidate) = widget.clone().downcast::() { + list = Some(candidate); + break; + } + sibling = widget.next_sibling(); + } + let list = list.expect("folder list present"); + let list_row = list.row_at_index(0).expect("one folder row"); + let slot = list_row + .child() + .and_then(|widget| widget.downcast::().ok()) + .expect("row slot"); + let action_row = slot + .first_child() + .and_then(|widget| widget.downcast::().ok()) + .expect("folder action row"); + assert_eq!( + action_row.subtitle().as_deref(), + Some("Not Synchronized Yet") + ); + + // A live state change must repaint the row without a view rebuild. + let state = runtime.folders().get("f1").expect("folder runtime").state(); + state.set(crate::state::AppState::Syncing, ""); + assert_eq!(action_row.subtitle().as_deref(), Some("Synchronizing…")); + state.set(crate::state::AppState::IdleOk, ""); + assert_eq!( + action_row.subtitle().as_deref(), + Some("Synced in local docs") + ); + reset_locale(); + }); + } + #[test] fn account_settings_toggle_reveals_the_panel() { // The Account settings row must open the panel when activated diff --git a/src/ui/tray_state.rs b/src/ui/tray_state.rs index 4875e2d..ceac34c 100644 --- a/src/ui/tray_state.rs +++ b/src/ui/tray_state.rs @@ -75,7 +75,7 @@ const STATE_PRESENTATIONS: [(AppState, TrayPresentation); 13] = [ TrayPresentation { icon_key: "syncing", status: "Active", - label: "Synchronization Scheduled", + label: "Waiting to synchronize", user_paused: false, }, ), @@ -216,7 +216,7 @@ mod tests { TrayPresentation { icon_key: "syncing", status: "Active", - label: "Synchronization Scheduled", + label: "Waiting to synchronize", user_paused: false, }, ), diff --git a/src/util/translations/es.rs b/src/util/translations/es.rs index 2b9a02c..dbb7f7f 100644 --- a/src/util/translations/es.rs +++ b/src/util/translations/es.rs @@ -388,7 +388,6 @@ pub static CATALOG: &[(&str, &str)] = &[ ("Synchronization Error", "Error de sincronización"), ("Synchronization Folders", "Carpetas de sincronización"), ("Synchronization Log", "Registro de sincronización"), - ("Synchronization Scheduled", "Sincronización programada"), ("Synchronization blocked", "Sincronización bloqueada"), ("Synchronization blocked: no saved credentials", "Sincronización bloqueada: no hay credenciales guardadas"), ("Synchronization blocked: password keyring is locked", "Sincronización bloqueada: el almacén de contraseñas está bloqueado"), @@ -399,7 +398,6 @@ pub static CATALOG: &[(&str, &str)] = &[ ("Synchronization failed: credentials rejected", "Sincronización fallida: credenciales rechazadas"), ("Synchronization is paused", "La sincronización está en pausa"), ("Synchronization is paused on battery", "La sincronización está en pausa por batería"), - ("Synchronization scheduled", "Sincronización programada"), ("Synchronize this account now", "Sincroniza esta cuenta ahora"), ("Synchronized", "Sincronizado"), ("Synchronized with conflicts — review the log", "Sincronizado con conflictos: revisa el registro"), @@ -475,7 +473,7 @@ pub static CATALOG: &[(&str, &str)] = &[ ("Waiting for another account to finish…", "Esperando a que otra cuenta termine…"), ("Waiting for authorization in your browser…", "Esperando autorización en el navegador…"), ("Waiting for local changes to settle", "Esperando a que los cambios locales se asienten"), - ("Waiting to synchronize", "Esperando para sincronizar"), + ("Waiting to synchronize", "A la espera"), ("What's New", "Novedades"), ("When a remote folder is configured, the --path argument is passed to nextcloudcmd; leaving the field as / keeps the previous root-to-root behaviour.", "Cuando se configura una carpeta remota, se pasa el argumento --path a nextcloudcmd; dejar el campo como / mantiene el comportamiento anterior de raíz a raíz."), ("Wi-Fi", "Wi-Fi"),