Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions po/es.po
Original file line number Diff line number Diff line change
Expand Up @@ -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…"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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}"
Expand Down
7 changes: 7 additions & 0 deletions src/core/account_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion src/core/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
16 changes: 13 additions & 3 deletions src/ui/folder_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down Expand Up @@ -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| {
Expand All @@ -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
Expand All @@ -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);
}
Expand Down Expand Up @@ -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"));
Expand Down
93 changes: 93 additions & 0 deletions src/ui/main_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ pub struct AccountView {
/// Per-folder state subscriptions (used-space refreshes after each
/// completed synchronization, issue #43).
_folder_subscriptions: Vec<crate::state::Subscription>,
/// 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<FolderStatusRow>,
}

impl AccountView {
Expand Down Expand Up @@ -281,6 +286,9 @@ impl AccountView {

let pairs = pair_folder_runtimes(&account.folders, runtime.folders());
let mut folder_subscriptions: Vec<crate::state::Subscription> = 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<FolderStatusRow> = Vec::new();
for (folder, folder_runtime) in pairs {
let local_root = folder.local_root.clone();
let account_id = account.id.clone();
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -472,6 +481,7 @@ impl AccountView {
_account_runtime: account_runtime,
_subscription: Some(subscription),
_folder_subscriptions: folder_subscriptions,
_folder_rows: folder_rows,
}
}
}
Expand Down Expand Up @@ -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::<gtk4::ListBox>() {
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::<gtk4::Box>().ok())
.expect("row slot");
let action_row = slot
.first_child()
.and_then(|widget| widget.downcast::<libadwaita::ActionRow>().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
Expand Down
4 changes: 2 additions & 2 deletions src/ui/tray_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
),
Expand Down Expand Up @@ -216,7 +216,7 @@ mod tests {
TrayPresentation {
icon_key: "syncing",
status: "Active",
label: "Synchronization Scheduled",
label: "Waiting to synchronize",
user_paused: false,
},
),
Expand Down
4 changes: 1 addition & 3 deletions src/util/translations/es.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down
Loading