fix(core,fuse,gui): UI/UX quick wins from the audit (phase 0)

- Move Google Takeout import out of Settings; Photos owns it, and the
  Takeout page's back button returns there.
- Trash from the file browser is Undo-only (no confirm); F2 on a
  multi-selection explains why it does nothing.
- Ask before sign-out, rejecting an invitation, removing someone's access,
  removing a public link, and switching a folder to on-demand.
- Clearer offline wording ("Make available offline" / "Make online only")
  with dedicated offline, online-only and upload symbolic icons; the pin row
  is hidden for folders.
- Search hits report whether a file is cached, so search results show the
  offline badge.
- Activity log: readable Empty Trash / link entries, distinct icons, and
  error/warning styling for failures and conflicts.
- Proper singular/plural counts instead of "item(s)" / "archive(s)".
- Name earlier versions by save time instead of revision id; capitalise
  the public link role; stop-syncing uses a stop icon, not a trash can.
- Label the Photos Upload and "Show on map" buttons; toast when a
  thumbnail build finishes; the versions button follows the mount state.
- Mode switches and device mutations reload both sync pages.
This commit is contained in:
nvrl committed 2026-09-23 01:34:35 +02:00
1 parent 690b91deab
commit cc308ac25a
27 files changed
+341 -190

No files matched your search

+36 -5
View File
@@ -603,7 +603,6 @@ fn build_window(app: &adw::Application) {
&ui,
&main_widgets.purge_button,
&main_widgets.mountpoint_button,
&main_widgets.import_row,
);
wire_sidebar(&ui);
wire_browser(&ui, &browser_widgets.grid, &browser_widgets.column_view);
@@ -773,12 +772,12 @@ fn build_sidebar() -> (adw::NavigationPage, gtk4::ListBox) {
}
/// The sidebar destination a page belongs under. Most pages are their own
/// destination; a sub-page reached from one (Import, off Settings) has no row of
/// destination; a sub-page reached from one (Import, off Photos) has no row of
/// its own and answers with its parent, so the sidebar highlights where the user
/// came from rather than clearing — which would read as "nowhere".
fn destination_of(page: &str) -> &str {
match page {
"takeout" => "main",
"takeout" => "gallery",
other => other,
}
}
@@ -956,6 +955,12 @@ fn sync_sidebar(ui: &Rc<Ui>) {
}
}
/// "1 item" / "5 items": a count with the noun form that agrees with it, so
/// no user-facing string has to fall back to "item(s)".
fn count_noun(n: usize, one: &str, many: &str) -> String {
format!("{n} {}", if n == 1 { one } else { many })
}
/// Show a transient toast. Non-blocking by design: an action's outcome is
/// reported without stealing focus or forcing a click, so the user can keep
/// working while a slow upload lands.
@@ -1134,7 +1139,11 @@ fn install_shortcuts(ui: &Rc<Ui>, window: &adw::ApplicationWindow) {
gtk4::gdk::Key::n | gtk4::gdk::Key::N if ctrl && on_browser => prompt_new_folder(&ui),
gtk4::gdk::Key::u | gtk4::gdk::Key::U if ctrl && on_browser => prompt_upload(&ui),
gtk4::gdk::Key::F2 if on_browser => {
if let Some(entry) = selected_entry(&ui) {
// Renaming is one name at a time; saying so beats a key that
// silently does nothing with several items highlighted.
if selected_entries(&ui).len() > 1 {
toast(&ui, "Select a single item to rename it");
} else if let Some(entry) = selected_entry(&ui) {
prompt_rename(&ui, &entry);
}
}
@@ -1144,7 +1153,7 @@ fn install_shortcuts(ui: &Rc<Ui>, window: &adw::ApplicationWindow) {
gtk4::gdk::Key::Delete if on_browser => {
let entries = selected_entries(&ui);
if !entries.is_empty() {
prompt_delete_many(&ui, entries);
trash_entries(&ui, entries);
}
}
gtk4::gdk::Key::Escape
@@ -1196,6 +1205,28 @@ fn ui_window(ui: &Rc<Ui>) -> Option<gtk4::Window> {
ui.stack.root().and_downcast::<gtk4::Window>()
}
/// Ask before an action that has no Undo. `on_confirm` runs only when the user
/// picks `action`; Cancel is the default, so Enter never destroys anything.
fn confirm_destructive(
parent: &impl IsA<gtk4::Widget>,
heading: &str,
body: &str,
action: &str,
on_confirm: impl Fn() + 'static,
) {
let dialog = adw::AlertDialog::builder()
.heading(heading)
.body(body)
.build();
dialog.add_response("cancel", "Cancel");
dialog.add_response("confirm", action);
dialog.set_response_appearance("confirm", adw::ResponseAppearance::Destructive);
dialog.set_default_response(Some("cancel"));
dialog.set_close_response("cancel");
dialog.connect_response(Some("confirm"), move |_, _| on_confirm());
dialog.present(Some(parent));
}
/// A dim, non-interactive placeholder row for an empty section.
fn dim_row(text: &str) -> adw::ActionRow {
let row = adw::ActionRow::builder().title(text).build();