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

+5 -2
View File
@@ -89,7 +89,7 @@ pub(crate) fn build_details_pane() -> (gtk4::Widget, DetailsWidgets) {
let offline = adw::PreferencesGroup::new();
let pin_row = adw::SwitchRow::builder()
.title("Available offline")
.subtitle("Keep a local copy on this device.")
.subtitle("Keep a copy on this computer, even without a connection.")
.build();
offline.add(&pin_row);
@@ -208,7 +208,7 @@ pub(crate) fn wire_details(ui: &Rc<Ui>) {
ui.details.details.trash_button.connect_clicked(move |_| {
let entry = ui_trash.details.details_entry.borrow().clone();
if let Some(entry) = entry {
prompt_delete(&ui_trash, &entry);
trash_entry(&ui_trash, &entry);
}
});
let ui_pin = ui.clone();
@@ -256,7 +256,10 @@ pub(crate) fn show_details(ui: &Rc<Ui>, entry: &DirEntry) {
} else {
parent
});
// Pinning is per file: the context menu and the selection bar offer it for
// files only, so the pane must not offer it for folders either.
d.pin_row.set_active(entry.pinned);
d.pin_row.set_visible(!entry.is_dir);
d.pin_row.set_sensitive(*ui.mounted.borrow());
d.open_button
.set_label(if entry.is_dir { "Open folder" } else { "Open" });
+41 -16
View File
@@ -361,13 +361,24 @@ pub(crate) fn repaint_share_people(state: &Rc<ShareDialog>, entries: &[ShareEntr
let state_rm = state.clone();
let id = entry.id.clone();
let kind = entry.kind;
remove.connect_clicked(move |_| {
share_dialog_op(
&state_rm,
state_rm.target.remove_entry(id.clone(), kind),
"Access removed",
"Couldn't remove access",
None,
let who = entry.email.clone();
remove.connect_clicked(move |btn| {
let state = state_rm.clone();
let id = id.clone();
confirm_destructive(
btn,
"Remove Access?",
&format!("{who} will no longer be able to open this item."),
"Remove",
move || {
share_dialog_op(
&state,
state.target.remove_entry(id.clone(), kind),
"Access removed",
"Couldn't remove access",
None,
);
},
);
});
row.add_suffix(&remove);
@@ -390,9 +401,12 @@ pub(crate) fn repaint_share_link(state: &Rc<ShareDialog>, link: Option<&PublicLi
Some(link) => {
let url = link.url.clone().unwrap_or_default();
let subtitle = if link.has_password {
format!("Anyone with the link ({}) · password-protected", link.role)
format!(
"Anyone with the link ({}) · password-protected",
capitalize(&link.role)
)
} else {
format!("Anyone with the link ({})", link.role)
format!("Anyone with the link ({})", capitalize(&link.role))
};
let row = adw::ActionRow::builder()
.title(if url.is_empty() { "Public link" } else { &url })
@@ -424,13 +438,24 @@ pub(crate) fn repaint_share_link(state: &Rc<ShareDialog>, link: Option<&PublicLi
remove.add_css_class("flat");
let state_rm = state.clone();
let id = link.id.clone();
remove.connect_clicked(move |_| {
share_dialog_op(
&state_rm,
state_rm.target.remove_link(id.clone()),
"Public link removed",
"Couldn't remove the link",
None,
remove.connect_clicked(move |btn| {
let state = state_rm.clone();
let id = id.clone();
confirm_destructive(
btn,
"Remove Public Link?",
"The link stops working for everyone who has it. A new link will have \
a different address.",
"Remove Link",
move || {
share_dialog_op(
&state,
state.target.remove_link(id.clone()),
"Public link removed",
"Couldn't remove the link",
None,
);
},
);
});
row.add_suffix(&remove);
@@ -182,16 +182,20 @@ fn repaint_versions(state: &Rc<VersionsDialog>, items: &[RevisionInfo]) {
.filter(|size| *size >= 0)
.unwrap_or(item.size_on_storage)
.max(0) as u64;
let mut subtitle = format!("{} · {}", activity_time(item.created), human_bytes(size));
// A revision id means nothing to a person; name earlier versions by
// when they were saved instead.
let (title, mut subtitle) = if item.is_active {
(
"Current version".to_string(),
format!("{} · {}", activity_time(item.created), human_bytes(size)),
)
} else {
(activity_time(item.created), human_bytes(size))
};
if let Some(email) = &item.signed_by {
subtitle.push_str(" · ");
subtitle.push_str(email);
}
let title = if item.is_active {
"Current version".to_string()
} else {
format!("Version {}", item.id)
};
let row = adw::ActionRow::builder()
.title(title)
.subtitle(subtitle)