diff --git a/PKGBUILD b/PKGBUILD
index 451f239..11c3e77 100644
--- a/PKGBUILD
+++ b/PKGBUILD
@@ -83,6 +83,7 @@ package() {
nextsync-status-syncing-symbolic
nextsync-info-symbolic
nextsync-settings-2-symbolic
+ nextsync-undo-2-symbolic
)
for icon in "${symbolic[@]}"; do
install -Dm644 "data/icons/$icon.svg" \
diff --git a/data/icons/nextsync-undo-2-symbolic.svg b/data/icons/nextsync-undo-2-symbolic.svg
new file mode 100644
index 0000000..3654609
--- /dev/null
+++ b/data/icons/nextsync-undo-2-symbolic.svg
@@ -0,0 +1,14 @@
+
diff --git a/po/es.po b/po/es.po
index fff8aaa..39a244f 100644
--- a/po/es.po
+++ b/po/es.po
@@ -2138,3 +2138,19 @@ msgstr "Error de sincronización — consulta el registro"
#~ msgid "Use This Folder"
#~ msgstr "Usar esta carpeta"
+
+#: src/ui/main_window.rs
+msgid "System"
+msgstr "Sistema"
+
+#: src/ui/main_window.rs
+msgid "Light"
+msgstr "Claro"
+
+#: src/ui/main_window.rs
+msgid "Dark"
+msgstr "Oscuro"
+
+#: src/ui/settings.rs
+msgid "Remove account"
+msgstr "Quitar cuenta"
diff --git a/src/main.rs b/src/main.rs
index fb97f76..3fe4d80 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -57,12 +57,17 @@ fn main() {
};
let config = match config_store.load() {
Ok(config) => config,
+
Err(error) => {
eprintln!("Could not load configuration: {error}");
std::process::exit(1);
}
};
+ // Apply the persisted color scheme before any window is shown.
+ libadwaita::StyleManager::default()
+ .set_color_scheme(nextsync::ui::color_scheme_for(&config.general.color_scheme));
+
let source: Rc> =
Rc::new(RefCell::new(GlibTimeoutSource::new()));
let logger = nextsync::core::log::LogBuffer::new();
diff --git a/src/storage/config.rs b/src/storage/config.rs
index 36398a9..95f61c8 100644
--- a/src/storage/config.rs
+++ b/src/storage/config.rs
@@ -205,6 +205,14 @@ pub struct RuntimeConfig {
pub struct GeneralConfig {
pub autostart: bool,
pub pause_on_battery: bool,
+ /// Color scheme preference: `"system"`, `"light"` or `"dark"`.
+ #[serde(default = "default_color_scheme")]
+ pub color_scheme: String,
+}
+
+/// Default color scheme (follow the desktop).
+fn default_color_scheme() -> String {
+ "system".to_string()
}
impl Default for GeneralConfig {
@@ -212,6 +220,7 @@ impl Default for GeneralConfig {
Self {
autostart: true,
pause_on_battery: false,
+ color_scheme: default_color_scheme(),
}
}
}
@@ -988,6 +997,7 @@ fn validate_general(raw: Option<&Value>) -> GeneralConfig {
GeneralConfig {
autostart: get_bool(obj, "autostart", true),
pause_on_battery: get_bool(obj, "pause_on_battery", false),
+ color_scheme: get_string(obj, "color_scheme", &default_color_scheme()),
}
}
@@ -1159,6 +1169,13 @@ fn get_bool(obj: &Map, key: &str, default: bool) -> bool {
}
}
+fn get_string(obj: &Map, key: &str, default: &str) -> String {
+ match obj.get(key) {
+ Some(Value::String(text)) => text.clone(),
+ _ => default.to_string(),
+ }
+}
+
fn sha256_hex(input: &[u8]) -> String {
let digest = Sha256::digest(input);
digest.iter().map(|byte| format!("{byte:02x}")).collect()
diff --git a/src/ui/main_window.rs b/src/ui/main_window.rs
index 4af9c7b..f3ac386 100644
--- a/src/ui/main_window.rs
+++ b/src/ui/main_window.rs
@@ -374,9 +374,22 @@ impl MainWindow {
let title = libadwaita::WindowTitle::new(window_title(), window_subtitle());
header.set_title_widget(Some(&title));
- // Hamburger menu (official-client style): Preferences, Advanced and
- // About, rendered in-app by sliding the settings view over the sync
- // view.
+ // gnome-text-editor layout: the back-to-sync button sits at the left
+ // of the header; the hamburger menu sits at the far right, next to
+ // the window close button.
+ let back_button = gtk4::Button::builder()
+ .icon_name("nextsync-undo-2-symbolic")
+ .tooltip_text(t("Synchronization"))
+ .css_classes(["flat"])
+ .build();
+ let back_weak = self_weak.clone();
+ back_button.connect_clicked(move |_button| {
+ if let Some(main) = back_weak.upgrade() {
+ main.borrow_mut().show_sync_view();
+ }
+ });
+ header.pack_start(&back_button);
+
let hamburger = gtk4::MenuButton::builder()
.icon_name("open-menu-symbolic")
.tooltip_text(t("Settings"))
@@ -384,16 +397,6 @@ impl MainWindow {
.build();
hamburger.set_menu_model(Some(&hamburger_menu_model()));
let actions = gio::SimpleActionGroup::new();
- actions.add_action(&{
- let weak = self_weak.clone();
- let action = gio::SimpleAction::new("sync", None);
- action.connect_activate(move |_action, _param| {
- if let Some(main) = weak.upgrade() {
- main.borrow_mut().show_sync_view();
- }
- });
- action
- });
actions.add_action(&{
let weak = self_weak.clone();
let action = gio::SimpleAction::new("preferences", None);
@@ -405,27 +408,43 @@ impl MainWindow {
action
});
actions.add_action(&{
- let weak = self_weak.clone();
- let action = gio::SimpleAction::new("advanced", None);
+ let on_about = on_show_about.clone();
+ let action = gio::SimpleAction::new("about", None);
action.connect_activate(move |_action, _param| {
- if let Some(main) = weak.upgrade() {
- main.borrow_mut().show_advanced();
+ if let Some(cb) = &on_about {
+ cb();
}
});
action
});
actions.add_action(&{
- let on_about = on_show_about.clone();
- let action = gio::SimpleAction::new("about", None);
- action.connect_activate(move |_action, _param| {
- if let Some(cb) = &on_about {
- cb();
+ let weak = self_weak.clone();
+ let store_for_theme = config_store.clone();
+ let current = config.general.color_scheme.clone();
+ let action = gio::SimpleAction::new_stateful(
+ "theme",
+ Some(glib::VariantTy::STRING),
+ ¤t.to_variant(),
+ );
+ action.connect_activate(move |action, parameter| {
+ let Some(value) = parameter.and_then(|value| value.str()) else {
+ return;
+ };
+ let scheme = value.to_string();
+ libadwaita::StyleManager::default().set_color_scheme(color_scheme_for(&scheme));
+ action.set_state(&scheme.to_variant());
+ if let Some(main) = weak.upgrade() {
+ let mut main = main.borrow_mut();
+ main.config.general.color_scheme = scheme.clone();
+ let mut persisted = store_for_theme.load().unwrap_or_default();
+ persisted.general.color_scheme = scheme;
+ let _ = store_for_theme.save(&persisted);
}
});
action
});
hamburger.insert_action_group("app", Some(&actions));
- header.pack_start(&hamburger);
+ header.pack_end(&hamburger);
toolbar.add_top_bar(&header);
@@ -740,11 +759,6 @@ impl MainWindow {
self.show_settings_page(settings_page::GENERAL);
}
- /// Open the in-app Advanced page (slides over the sync view).
- pub fn show_advanced(&mut self) {
- self.show_settings_page(settings_page::ADVANCED);
- }
-
/// Ensure the settings view exists for the active account and slide to the
/// given page.
fn show_settings_page(&mut self, page: &str) {
@@ -1036,23 +1050,42 @@ impl MainWindow {
///
/// Extracted from [`MainWindow::new`] so the menu contract (sections, actions
/// and icons) is testable without a display.
+/// The header menu (gnome-text-editor style): Preferences and About, plus a
+/// color-scheme radio section (System / Light / Dark).
fn hamburger_menu_model() -> gio::Menu {
let menu = gio::Menu::new();
- let sync_item = gio::MenuItem::new(Some(t("Synchronization")), Some("app.sync"));
- sync_item.set_icon(&gio::ThemedIcon::new("emblem-synchronizing-symbolic"));
- menu.append_item(&sync_item);
let preferences_item = gio::MenuItem::new(Some(t("Preferences")), Some("app.preferences"));
preferences_item.set_icon(&gio::ThemedIcon::new("preferences-system-symbolic"));
menu.append_item(&preferences_item);
- let advanced_item = gio::MenuItem::new(Some(t("Advanced")), Some("app.advanced"));
- advanced_item.set_icon(&gio::ThemedIcon::new("applications-system-symbolic"));
- menu.append_item(&advanced_item);
let about_item = gio::MenuItem::new(Some(t("About")), Some("app.about"));
about_item.set_icon(&gio::ThemedIcon::new("nextsync-info-symbolic"));
menu.append_item(&about_item);
+
+ let theme_section = gio::Menu::new();
+ for (label, value) in [
+ (t("System"), "system"),
+ (t("Light"), "light"),
+ (t("Dark"), "dark"),
+ ] {
+ let item = gio::MenuItem::new(Some(label), None);
+ item.set_action_and_target_value(Some("app.theme"), Some(&value.to_variant()));
+ theme_section.append_item(&item);
+ }
+ menu.append_section(None, &theme_section);
menu
}
+/// Map the persisted color-scheme preference to a libadwaita color scheme.
+///
+/// Unknown values fall back to following the desktop (`system`).
+pub fn color_scheme_for(preference: &str) -> libadwaita::ColorScheme {
+ match preference {
+ "light" => libadwaita::ColorScheme::ForceLight,
+ "dark" => libadwaita::ColorScheme::ForceDark,
+ _ => libadwaita::ColorScheme::Default,
+ }
+}
+
/// Build the sidebar: the container, the accounts list and the Add Account
/// button.
fn build_sidebar() -> (gtk4::Box, gtk4::ListBox, gtk4::Button) {
@@ -1183,13 +1216,10 @@ mod tests {
set_locale(Locale::English);
let menu = hamburger_menu_model();
- assert_eq!(menu.n_items(), 4);
- let expected: [(&str, &str); 4] = [
- ("Synchronization", "app.sync"),
- ("Preferences", "app.preferences"),
- ("Advanced", "app.advanced"),
- ("About", "app.about"),
- ];
+ // Preferences + About, then the color-scheme section.
+ assert_eq!(menu.n_items(), 3);
+ let expected: [(&str, &str); 2] =
+ [("Preferences", "app.preferences"), ("About", "app.about")];
for (index, (label, action)) in expected.iter().enumerate() {
let attrs = item_attrs(&menu, index as i32);
assert_eq!(attrs.label.as_deref(), Some(*label), "item {index}");
@@ -1197,25 +1227,61 @@ mod tests {
assert!(attrs.has_icon, "item {index} must carry an icon");
}
- // The Spanish catalog covers every menu section (issue #10 renders
- // the menu in-app, so the labels are user-visible on every launch).
+ // The color-scheme section carries radio items with the target value.
+ let theme_section = menu.item_link(2, gio::MENU_LINK_SECTION).expect("section");
+ let expected_theme: [(&str, &str); 3] =
+ [("System", "system"), ("Light", "light"), ("Dark", "dark")];
+ for (index, (label, target)) in expected_theme.iter().enumerate() {
+ let mut found_label = None;
+ let mut found_action = None;
+ let mut found_target = None;
+ let iter = theme_section.iterate_item_attributes(index as i32);
+ while let Some((key, value)) = iter.next() {
+ match key.as_str() {
+ "label" => found_label = value.str().map(str::to_string),
+ "action" => found_action = value.str().map(str::to_string),
+ "target" => found_target = value.str().map(str::to_string),
+ _ => {}
+ }
+ }
+ assert_eq!(found_label.as_deref(), Some(*label), "theme item {index}");
+ assert_eq!(
+ found_action.as_deref(),
+ Some("app.theme"),
+ "theme items must be radio-style with an action target"
+ );
+ assert_eq!(found_target.as_deref(), Some(*target), "theme item {index}");
+ }
+
+ // The Spanish catalog covers every menu entry (the menu is
+ // user-visible on every launch).
set_locale(Locale::Spanish);
let menu = hamburger_menu_model();
- let labels: Vec = (0..menu.n_items())
+ let labels: Vec = (0..2)
.map(|index| item_attrs(&menu, index).label.expect("label"))
.collect();
assert_eq!(
labels,
- vec![
- "Sincronización".to_string(),
- "Preferencias".to_string(),
- "Avanzado".to_string(),
- "Acerca de".to_string(),
- ]
+ vec!["Preferencias".to_string(), "Acerca de".to_string(),]
);
reset_locale();
}
+ #[test]
+ fn color_scheme_preference_maps_to_libadwaita() {
+ assert_eq!(color_scheme_for("system"), libadwaita::ColorScheme::Default);
+ assert_eq!(
+ color_scheme_for("light"),
+ libadwaita::ColorScheme::ForceLight
+ );
+ assert_eq!(color_scheme_for("dark"), libadwaita::ColorScheme::ForceDark);
+ // Unknown values follow the desktop.
+ assert_eq!(
+ color_scheme_for("nonsense"),
+ libadwaita::ColorScheme::Default
+ );
+ }
+
#[test]
fn outer_stack_slides_the_settings_view_over_sync() {
// Must run through the shared GTK test worker (see
@@ -1272,11 +1338,6 @@ mod tests {
window.root_stack.visible_child_name().as_deref(),
Some("settings")
);
- window.show_advanced();
- assert_eq!(
- window.root_stack.visible_child_name().as_deref(),
- Some("settings")
- );
// Synchronization slides back without dropping the view.
window.show_sync_view();
@@ -1365,7 +1426,6 @@ mod tests {
Weak::new(),
);
window.show_preferences();
- window.show_advanced();
assert!(window.settings_view.is_none());
assert!(window.root_stack.child_by_name("settings").is_none());
assert_eq!(
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 00a0ad9..6287c57 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -5,6 +5,7 @@ pub mod activity;
pub mod conflict_resolver;
pub mod folder_status;
pub mod main_window;
+pub use main_window::color_scheme_for;
pub mod settings;
pub mod setup;
pub mod tray;
diff --git a/src/ui/settings.rs b/src/ui/settings.rs
index 2ecdc0e..e26b133 100644
--- a/src/ui/settings.rs
+++ b/src/ui/settings.rs
@@ -1,10 +1,10 @@
//! Settings window (Task 5.2).
//!
-//! Port of `ui/settings.py` (v0.4.0) to gtk-rs/libadwaita: a
-//! `Adw.PreferencesWindow` with General, Synchronization, Network and
-//! Advanced pages, the per-folder groups with the Add Folder flow (including
-//! the remote folder picker, issue #25) and the typed Remove Account
-//! confirmation (issue #35, moved to Advanced by the account-view redesign).
+//! Port of `ui/settings.py` (v0.4.0) to gtk-rs/libadwaita: a single in-app
+//! `SettingsView` (ToolbarView + ViewStack + ViewSwitcherBar) with General,
+//! Synchronization, Network and Advanced pages, and the typed Remove account
+//! confirmation. Folder management lives in the sync view (issue #18); the
+//! standalone Add Folder dialog here is shared with it.
//!
//! # Deviations from `settings.py` (motivated)
//!
@@ -34,12 +34,10 @@
//! private validator in `storage::config`.
use std::cell::RefCell;
-use std::path::PathBuf;
use std::rc::Rc;
use libadwaita::prelude::*;
-use crate::core::desktop_integration::DesktopIntegration;
use crate::core::triggers::TriggerSettings;
use crate::nextcloud::api::{ApiError, NextcloudApi};
use crate::nextcloud::credentials::CredentialsStore;
@@ -56,7 +54,7 @@ pub type SettingsCallback = Rc;
/// Callbacks the Settings window invokes after mutating the configuration.
#[derive(Clone, Default)]
pub struct SettingsCallbacks {
- /// Invoked after the typed Remove Account confirmation succeeds.
+ /// Invoked after the typed Remove account confirmation succeeds.
pub on_remove_account: Option,
/// Invoked after a folder is added or removed (refreshes the account view).
pub on_folder_changed: Option,
@@ -122,16 +120,10 @@ impl SettingsView {
// configuration; account-owned settings come from the snapshot.
let config = config_store.load().unwrap_or_default();
- let folder_ui = FolderUi {
- store: config_store.clone(),
- account_id: account_id.clone(),
- callbacks: callbacks.clone(),
- group: libadwaita::PreferencesGroup::new(),
- host: host.clone(),
- rows: Rc::new(RefCell::new(Vec::new())),
- };
-
- let general = build_general_page(&config_store, &config.general, &folder_ui.group);
+ // Folder management (list, Add Folder, desktop integration) was
+ // removed from Settings by user decision (issue #18): the sync view
+ // owns it, so the settings pages never duplicate it.
+ let general = build_general_page(&config_store, &config.general);
let synchronization = build_sync_page(&config_store, &account_id, &account, &callbacks);
let network = build_network_page(&config_store, &account, &config.network);
let advanced = build_advanced_page(
@@ -143,16 +135,6 @@ impl SettingsView {
host,
);
- folder_ui.refresh();
-
- // Desktop integrations sit in the folders group, after the Add Folder
- // row (same order as `_build_desktop_integrations` in the Python).
- // They are added once and are NOT tracked in `FolderUi::rows`, so a
- // folder add/remove refresh never destroys them.
- for row in desktop_integration_rows(&account, host) {
- folder_ui.group.add(&row);
- }
-
let stack = libadwaita::ViewStack::new();
let toolbar = libadwaita::ToolbarView::new();
let switcher = libadwaita::ViewSwitcherBar::new();
@@ -211,13 +193,8 @@ pub mod page {
// Page builders
// ---------------------------------------------------------------------------
-/// General page: Startup switch, Power switch and the Synchronization Folders
-/// group (managed by [`FolderUi`]).
-fn build_general_page(
- store: &ConfigStore,
- general: &GeneralConfig,
- folders_group: &libadwaita::PreferencesGroup,
-) -> libadwaita::PreferencesPage {
+/// General page: Startup and Power switches.
+fn build_general_page(store: &ConfigStore, general: &GeneralConfig) -> libadwaita::PreferencesPage {
let page = libadwaita::PreferencesPage::builder()
.title(t("General"))
.icon_name("preferences-system-symbolic")
@@ -262,12 +239,6 @@ fn build_general_page(
power.add(&pause_battery);
page.add(&power);
- let folders = libadwaita::PreferencesGroup::builder()
- .title(t("Synchronization Folders"))
- .build();
- folders.add(folders_group);
- page.add(&folders);
-
page
}
@@ -728,22 +699,8 @@ fn build_advanced_page(
guard.add(&guard_percent);
page.add(&guard);
- // Diagnostics.
- let diagnostics = libadwaita::PreferencesGroup::builder()
- .title(t("Diagnostics"))
- .build();
- let last_code = account
- .runtime
- .last_exit_code
- .map(|code| code.to_string())
- .unwrap_or_else(|| t("None").to_string());
- diagnostics.add(
- &libadwaita::ActionRow::builder()
- .title(t("Last exit code"))
- .subtitle(last_code)
- .build(),
- );
- page.add(&diagnostics);
+ // Diagnostics removed by user decision (issue #18): the log files under
+ // $XDG_STATE_HOME carry the same information.
// Authentication: re-enter credentials without removing the account.
let auth_group = libadwaita::PreferencesGroup::builder()
@@ -791,7 +748,7 @@ fn build_advanced_page(
)
.build();
let remove = libadwaita::ActionRow::builder()
- .title(t("Remove Account"))
+ .title(t("Remove account"))
.subtitle(t("Rarely needed. Keeps all local files."))
.tooltip_text(t("Disconnect this account; local files are kept"))
.activatable(true)
@@ -956,122 +913,9 @@ fn save_network(store: &ConfigStore, proxy: &libadwaita::EntryRow, trust: &libad
}
// ---------------------------------------------------------------------------
-// Synchronization Folders group
+// Add Folder dialog
// ---------------------------------------------------------------------------
-/// Shared state for the Synchronization Folders group: the store, the account
-/// id and the widgets the Add/Remove flows rebuild.
-#[derive(Clone)]
-struct FolderUi {
- store: ConfigStore,
- account_id: String,
- callbacks: SettingsCallbacks,
- group: libadwaita::PreferencesGroup,
- host: SettingsHost,
- /// Rows added by [`refresh`](Self::refresh). `PreferencesGroup` keeps an
- /// internal box as its direct child, so only these rows may be removed.
- rows: Rc>>,
-}
-
-impl FolderUi {
- /// Rebuild the folder rows from the current configuration.
- fn refresh(&self) {
- for row in self.rows.borrow_mut().drain(..) {
- self.group.remove(&row);
- }
- let Ok(Some(account)) = self.store.account(&self.account_id) else {
- return;
- };
- for folder in &account.folders {
- let row = libadwaita::ActionRow::builder()
- .title(folder.local_root.as_str())
- .subtitle(t("Remote: {remote}").replacen(
- "{remote}",
- folder_subtitle(&folder.remote_path),
- 1,
- ))
- .build();
- let icon = gtk4::Image::builder()
- .icon_name("folder-symbolic")
- .pixel_size(16)
- .build();
- row.add_prefix(&icon);
- let remove = gtk4::Button::builder()
- .icon_name("user-trash-symbolic")
- .valign(gtk4::Align::Center)
- .tooltip_text(t("Remove folder"))
- .css_classes(["flat"])
- .build();
- let folder_id = folder.id.clone();
- let ui = self.clone();
- remove.connect_clicked(move |_| {
- let _ = ui.store.remove_folder(&ui.account_id, &folder_id);
- ui.refresh();
- invoke(&ui.callbacks.on_folder_changed);
- });
- row.add_suffix(&remove);
- self.group.add(&row);
- self.rows.borrow_mut().push(row.upcast::());
- }
-
- let add_row = libadwaita::ActionRow::builder()
- .title(t("Add Folder"))
- .subtitle(t("Mirror another local folder from this account"))
- .tooltip_text(t("Add a local folder to synchronize with this account"))
- .activatable(true)
- .build();
- let add_icon = gtk4::Image::builder()
- .icon_name("folder-new-symbolic")
- .pixel_size(16)
- .build();
- add_row.add_prefix(&add_icon);
- let next = gtk4::Image::builder()
- .icon_name("go-next-symbolic")
- .pixel_size(16)
- .build();
- add_row.add_suffix(&next);
- let ui = self.clone();
- add_row.connect_activated(move |_| {
- ui.present_add_folder_dialog(None, None);
- });
- self.group.add(&add_row);
- self.rows
- .borrow_mut()
- .push(add_row.upcast::());
- }
-
- /// Present the Add Folder dialog. `previous` and `error` let a failed
- /// attempt re-open with the typed values and an inline message.
- ///
- /// Thin wrapper around the freestanding [`present_add_folder_dialog`]:
- /// supplies this group's refresh + the host toast from the folder UI's
- /// own state so the Settings call site stays a one-liner.
- fn present_add_folder_dialog(&self, previous: Option<(String, String)>, error: Option) {
- let on_folder_added = {
- let ui = self.clone();
- Rc::new(move || {
- ui.refresh();
- invoke(&ui.callbacks.on_folder_changed);
- })
- };
- let on_error = {
- let host = self.host.clone();
- Rc::new(move |message: String| {
- host.add_toast(libadwaita::Toast::new(&message));
- })
- };
- present_add_folder_dialog(
- self.store.clone(),
- self.account_id.clone(),
- self.host.parent(),
- on_folder_added,
- on_error,
- previous,
- error,
- );
- }
-}
-
/// Present the Add Folder dialog for an account against a config store.
///
/// This is the single construction site for the dialog, shared by the
@@ -1354,10 +1198,6 @@ fn populate_remote_picker(
/// The folder the desktop integration switches target: the account's first
/// folder (the Python used the "active" folder; the rewrite has none).
-fn integration_target(account: &AccountConfig) -> Option<&FolderConfig> {
- account.folders.first()
-}
-
/// Build the three desktop integration switches for the first folder of the
/// account, replicating `_build_desktop_integrations`: "Show in Files
/// sidebar" (Nautilus bookmark), "Show on Desktop" (shortcut) and "Use
@@ -1367,103 +1207,6 @@ fn integration_target(account: &AccountConfig) -> Option<&FolderConfig> {
/// Each switch applies its [`DesktopIntegration`] setter on toggle; a `false`
/// result (e.g. a missing icon asset) reverts the switch to the real state
/// and surfaces a toast.
-fn desktop_integration_rows(
- account: &AccountConfig,
- host: &SettingsHost,
-) -> Vec {
- let Some(folder) = integration_target(account) else {
- return Vec::new();
- };
- let local_root = folder.local_root.clone();
- // One instance per closure: `DesktopIntegration` is not `Clone`, and each
- // instance is a cheap paths-only struct over the same real XDG dirs.
- let make_integration =
- || DesktopIntegration::new(PathBuf::from(local_root.clone()), None, None);
- let state = make_integration().state();
-
- let bookmark = libadwaita::SwitchRow::builder()
- .title(t("Show in Files sidebar"))
- .subtitle(t(
- "Adds the synchronized folder to the file manager sidebar.",
- ))
- .active(state.nautilus_bookmark)
- .build();
- let shortcut = libadwaita::SwitchRow::builder()
- .title(t("Show on Desktop"))
- .subtitle(t(
- "Creates a link to the synchronized folder on the desktop.",
- ))
- .active(state.desktop_shortcut)
- .build();
- let icon = libadwaita::SwitchRow::builder()
- .title(t("Use special folder icon"))
- .subtitle(t(
- "Identifies the synchronized folder and its shortcuts in Files.",
- ))
- .active(state.special_icon)
- .build();
-
- connect_integration_switch(
- &bookmark,
- host,
- {
- let integration = make_integration();
- move |enabled| integration.set_nautilus_bookmark(enabled)
- },
- {
- let integration = make_integration();
- move || integration.state().nautilus_bookmark
- },
- );
- connect_integration_switch(
- &shortcut,
- host,
- {
- let integration = make_integration();
- move |enabled| integration.set_desktop_shortcut(enabled)
- },
- {
- let integration = make_integration();
- move || integration.state().desktop_shortcut
- },
- );
- connect_integration_switch(
- &icon,
- host,
- {
- let integration = make_integration();
- move |enabled| integration.set_special_icon(enabled)
- },
- {
- let integration = make_integration();
- move || integration.state().special_icon
- },
- );
-
- vec![bookmark, shortcut, icon]
-}
-
-/// Wire one integration switch: apply the setter on toggle and, when it
-/// reports `false`, revert to the real state (a no-op notification when the
-/// switch already matches, so the re-entry terminates) and toast.
-fn connect_integration_switch(
- row: &libadwaita::SwitchRow,
- host: &SettingsHost,
- apply: impl Fn(bool) -> bool + 'static,
- read_state: impl Fn() -> bool + 'static,
-) {
- let host = host.clone();
- row.connect_active_notify(move |row| {
- let desired = row.is_active();
- if !apply(desired) {
- row.set_active(read_state());
- host.add_toast(libadwaita::Toast::new(t(
- "The change could not be applied.",
- )));
- }
- });
-}
-
/// Open the log folder in the file manager, creating it when missing.
fn open_log_folder() {
let directory = state_dir();
@@ -1718,15 +1461,6 @@ fn spin_row(title: &str, lower: f64, upper: f64, value: f64) -> libadwaita::Spin
row
}
-/// Remote subtitle of a folder row: the account root displays as `/`.
-fn folder_subtitle(remote_path: &str) -> &str {
- if remote_path.is_empty() {
- "/"
- } else {
- remote_path
- }
-}
-
/// Light proxy validation (scheme + non-empty authority, no userinfo).
fn valid_proxy_url(value: &str) -> bool {
let value = value.trim();
@@ -1860,7 +1594,7 @@ fn present_sign_in_again_dialog(
dialog.present(Some(host.parent()));
}
-/// The two-step Remove Account flow (issue #35).
+/// The two-step Remove account flow (issue #35).
fn present_remove_account(login_name: &str, host: &SettingsHost, callbacks: &SettingsCallbacks) {
let dialog = libadwaita::AlertDialog::new(
Some(t("Remove Nextcloud Account?")),
@@ -1901,7 +1635,7 @@ fn present_remove_account_step_two(
entry_box.append(&entry);
dialog.set_extra_child(Some(&entry_box));
dialog.add_response("cancel", t("Cancel"));
- dialog.add_response("remove", t("Remove Account"));
+ dialog.add_response("remove", t("Remove account"));
dialog.set_response_appearance("remove", libadwaita::ResponseAppearance::Destructive);
dialog.set_default_response(Some("cancel"));
@@ -1967,12 +1701,6 @@ mod tests {
assert!(!valid_proxy_url("http://user@proxy.example.com"));
}
- #[test]
- fn folder_subtitle_uses_root_for_empty_remote() {
- assert_eq!(folder_subtitle(""), "/");
- assert_eq!(folder_subtitle("/Documents"), "/Documents");
- }
-
#[test]
fn classify_lookup_success_keeps_the_folder_list() {
assert_eq!(
@@ -2057,22 +1785,7 @@ mod tests {
assert_eq!(validate_pattern("*.swp").unwrap(), "*.swp");
}
- #[test]
- fn integration_target_is_the_first_folder_or_none() {
- let account = sample_account();
- assert_eq!(
- integration_target(&account).map(|folder| folder.id.as_str()),
- Some("folder-1")
- );
- let empty = AccountConfig {
- folders: Vec::new(),
- ..account
- };
- assert!(integration_target(&empty).is_none());
- }
-
// ---- persistence without GTK ------------------------------------------
-
#[test]
fn add_and_remove_folder_persist_on_disk() {
let dir = tempdir().unwrap();
@@ -2233,40 +1946,4 @@ mod tests {
reset_locale();
});
}
-
- /// The three integration switches carry the Python titles/subtitles and
- /// disappear without folders. Building them only READS the real user
- /// state (gtk-3.0 bookmarks, GIO metadata); nothing is written here.
- #[test]
- fn desktop_integration_switches_replicate_the_python_rows() {
- crate::ui::test_helpers::gtk_smoke(|| {
- let host = test_host();
-
- set_locale(Locale::English);
- let rows = desktop_integration_rows(&sample_account(), &host);
- assert_eq!(rows.len(), 3);
- assert_eq!(rows[0].title().as_str(), "Show in Files sidebar");
- assert_eq!(rows[1].title().as_str(), "Show on Desktop");
- assert_eq!(rows[2].title().as_str(), "Use special folder icon");
-
- set_locale(Locale::Spanish);
- let rows = desktop_integration_rows(&sample_account(), &host);
- assert_eq!(
- rows[0].title().as_str(),
- "Mostrar en la barra lateral de Archivos"
- );
- assert_eq!(rows[1].title().as_str(), "Mostrar en el escritorio");
- assert_eq!(
- rows[2].title().as_str(),
- "Usar un icono especial para la carpeta"
- );
- reset_locale();
-
- let empty = AccountConfig {
- folders: Vec::new(),
- ..sample_account()
- };
- assert!(desktop_integration_rows(&empty, &host).is_empty());
- });
- }
}
diff --git a/src/util/translations/es.rs b/src/util/translations/es.rs
index 0f89512..c0f8ed1 100644
--- a/src/util/translations/es.rs
+++ b/src/util/translations/es.rs
@@ -86,6 +86,7 @@ pub static CATALOG: &[(&str, &str)] = &[
("Creates a link to the synchronized folder on the desktop.", "Crea un acceso a la carpeta sincronizada en el escritorio."),
("Custom HTTP proxy", "Proxy HTTP personalizado"),
("Daily file naming", "Nombre de archivos diarios"),
+ ("Dark", "Oscuro"),
("Desktop Technologies", "Tecnologías de escritorio"),
("Detailed synchronization output", "Salida detallada de sincronización"),
("Diagnostics", "Diagnóstico"),
@@ -129,6 +130,7 @@ pub static CATALOG: &[(&str, &str)] = &[
("Kept local version of {name}", "Se conservó la versión local de {name}"),
("Kept remote version of {name}", "Se conservó la versión remota de {name}"),
("Languages", "Idiomas"),
+ ("Light", "Claro"),
("Live activity remains available when file logging is off.", "La actividad en vivo sigue disponible cuando el registro en archivos está desactivado."),
("Local Changes", "Cambios locales"),
("Local Detection", "Detección local"),
@@ -206,6 +208,7 @@ pub static CATALOG: &[(&str, &str)] = &[
("Remote: {remote}", "Remoto: {remote}"),
("Remove Account", "Eliminar cuenta"),
("Remove Nextcloud Account?", "¿Eliminar la cuenta de Nextcloud?"),
+ ("Remove account", "Quitar cuenta"),
("Remove folder", "Quitar carpeta"),
("Remove pattern", "Quitar patrón"),
("Remove synchronization", "Eliminar sincronización"),
@@ -266,6 +269,7 @@ pub static CATALOG: &[(&str, &str)] = &[
("Synchronizes shortly after a local file changes.", "Sincroniza poco después de que un archivo local cambie."),
("Synchronizing…", "Sincronizando…"),
("Syncing", "Sincronizando"),
+ ("System", "Sistema"),
("Terms of Use", "Términos de uso"),
("The account view lists every sync folder of the active account.", "La vista de la cuenta muestra todas las carpetas de sincronización de la cuenta activa."),
("The account will be connected without synchronizing any folder. You can add folders later from Settings.", "La cuenta se conectará sin sincronizar ninguna carpeta. Puedes añadir carpetas más tarde desde Ajustes."),