fix(gui): tidy the Share dialog

Send Invitations and Create Public Link sat alone in boxed-list rows,
which read as broken settings. They now sit under their section at its
right edge. The role picker moves into the address row it applies to,
people get avatars, a member needs no subtitle, and invitations say they
are pending. The People and Public link sections show a loading row
until the share is read, and one line instead of nothing when it can't
be. The page is an AdwPreferencesPage, for the standard spacing.

Translated in every catalog.
This commit is contained in:
nvrl committed 2026-10-09 02:41:07 +02:00
1 parent 085335ca14
commit 19854b2d76
10 files changed
+4382 -4320

No files matched your search

+68 -48
View File
@@ -127,6 +127,26 @@ impl Drop for Busy {
} }
} }
/// A section's action, such as Send Invitations. It goes under the section's
/// rows, at their right edge, rather than in a row of its own: a row in a boxed
/// list reads as a setting, and a button alone in one looks misplaced.
fn group_button(label: &str) -> gtk4::Button {
let button = gtk4::Button::builder()
.label(label)
.halign(gtk4::Align::End)
.margin_top(12)
.build();
button.add_css_class("suggested-action");
button
}
/// A section's only row while the share is being read.
fn loading_row() -> adw::ActionRow {
let row = adw::ActionRow::builder().title(gettext("Loading…")).build();
row.add_prefix(&spinner());
row
}
/// How an open Share dialog addresses its node. /// How an open Share dialog addresses its node.
/// ///
/// A node the browser is showing has a mountpoint-relative path. A node reached /// A node the browser is showing has a mountpoint-relative path. A node reached
@@ -287,26 +307,17 @@ pub(crate) fn open_share_dialog(ui: &Rc<Ui>, entry: &DirEntry) {
.selected(0) .selected(0)
.valign(gtk4::Align::Center) .valign(gtk4::Align::Center)
.build(); .build();
let role_wrap = adw::ActionRow::builder().title(gettext("Role")).build(); // The role goes with the addresses it applies to, at the end of their row.
role_wrap.add_suffix(&role_drop); role_drop.set_tooltip_text(Some(&gettext("Role")));
email_row.add_suffix(&role_drop);
let message_row = adw::EntryRow::builder() let message_row = adw::EntryRow::builder()
.title(gettext("Message (optional)")) .title(gettext("Message (optional)"))
.build(); .build();
let invite_btn = gtk4::Button::builder() let invite_btn = group_button(&gettext("Send Invitations"));
.label(gettext("Send Invitations"))
.halign(gtk4::Align::End)
.margin_top(6)
.build();
invite_btn.add_css_class("suggested-action");
invite_btn.set_sensitive(false); invite_btn.set_sensitive(false);
let invite_wrap = adw::PreferencesRow::builder()
.activatable(false)
.child(&invite_btn)
.build();
invite_group.add(&email_row); invite_group.add(&email_row);
invite_group.add(&role_wrap);
invite_group.add(&message_row); invite_group.add(&message_row);
invite_group.add(&invite_wrap); invite_group.add(&invite_btn);
let people = adw::PreferencesGroup::builder() let people = adw::PreferencesGroup::builder()
.title(gettext("People with access")) .title(gettext("People with access"))
@@ -315,21 +326,18 @@ pub(crate) fn open_share_dialog(ui: &Rc<Ui>, entry: &DirEntry) {
.title(gettext("Public link")) .title(gettext("Public link"))
.build(); .build();
let content = gtk4::Box::new(gtk4::Orientation::Vertical, 18); // Until the share is read, each section says so rather than standing
content.set_margin_top(12); // empty under its title.
content.set_margin_bottom(12); let people_loading = loading_row();
content.set_margin_start(12); people.add(&people_loading);
content.set_margin_end(12); let link_loading = loading_row();
content.append(&invite_group); link_group.add(&link_loading);
content.append(&people);
content.append(&link_group); let content = adw::PreferencesPage::new();
let clamp = adw::Clamp::builder().child(&content).build(); content.add(&invite_group);
let scroll = gtk4::ScrolledWindow::builder() content.add(&people);
.vexpand(true) content.add(&link_group);
.hscrollbar_policy(gtk4::PolicyType::Never) toolbar.set_content(Some(&content));
.child(&clamp)
.build();
toolbar.set_content(Some(&scroll));
let dialog = adw::Dialog::builder() let dialog = adw::Dialog::builder()
.title(pgettext("verb", "Share")) .title(pgettext("verb", "Share"))
@@ -343,8 +351,8 @@ pub(crate) fn open_share_dialog(ui: &Rc<Ui>, entry: &DirEntry) {
target, target,
people, people,
link_group, link_group,
people_rows: RefCell::new(Vec::new()), people_rows: RefCell::new(vec![people_loading.upcast()]),
link_rows: RefCell::new(Vec::new()), link_rows: RefCell::new(vec![link_loading.upcast()]),
}); });
// Check the addresses as they are typed: a malformed one marks the row as // Check the addresses as they are typed: a malformed one marks the row as
@@ -430,17 +438,35 @@ pub(crate) fn share_dialog_reload(state: &Rc<ShareDialog>) {
repaint_share_link(&state, link.as_ref()); repaint_share_link(&state, link.as_ref());
} }
Ok(Ok(Response::Error { message, .. })) => { Ok(Ok(Response::Error { message, .. })) => {
share_dialog_unread(&state);
toast_error(&state.ui, &gettext("Couldn't load sharing"), &message) toast_error(&state.ui, &gettext("Couldn't load sharing"), &message)
} }
_ => toast_error( _ => {
share_dialog_unread(&state);
toast_error(
&state.ui, &state.ui,
&gettext("Couldn't load sharing"), &gettext("Couldn't load sharing"),
&gettext("The Proton Drive service didn't respond."), &gettext("The Proton Drive service didn't respond."),
), )
}
} }
}); });
} }
/// The share couldn't be read: what the sections showed (rows still loading,
/// or ones that may now be stale) gives way to one line that says so.
fn share_dialog_unread(state: &Rc<ShareDialog>) {
for row in state.people_rows.borrow_mut().drain(..) {
state.people.remove(&row);
}
for row in state.link_rows.borrow_mut().drain(..) {
state.link_group.remove(&row);
}
let row = dim_row(&gettext("Couldn't load sharing"));
state.people.add(&row);
state.people_rows.borrow_mut().push(row.upcast());
}
/// Rebuild the "People with access" rows from a fresh share listing. /// Rebuild the "People with access" rows from a fresh share listing.
pub(crate) fn repaint_share_people(state: &Rc<ShareDialog>, entries: &[ShareEntry]) { pub(crate) fn repaint_share_people(state: &Rc<ShareDialog>, entries: &[ShareEntry]) {
for row in state.people_rows.borrow_mut().drain(..) { for row in state.people_rows.borrow_mut().drain(..) {
@@ -455,15 +481,18 @@ pub(crate) fn repaint_share_people(state: &Rc<ShareDialog>, entries: &[ShareEntr
return; return;
} }
for entry in entries { for entry in entries {
// A member is the plain case and needs no word; an invitation says
// it is still waiting on the other side.
let subtitle = match entry.kind { let subtitle = match entry.kind {
ShareEntryKind::Member => gettext("Member"), ShareEntryKind::Member => String::new(),
ShareEntryKind::ProtonInvite => gettext("Invited (pending)"), ShareEntryKind::ProtonInvite => gettext("Invitation pending"),
ShareEntryKind::ExternalInvite => gettext("Invited (external, pending)"), ShareEntryKind::ExternalInvite => gettext("Invitation pending, no Proton account yet"),
}; };
let row = adw::ActionRow::builder() let row = adw::ActionRow::builder()
.title(&entry.email) .title(&entry.email)
.subtitle(&subtitle) .subtitle(&subtitle)
.build(); .build();
row.add_prefix(&adw::Avatar::new(32, Some(&entry.email), true));
// External invites can't have their role changed; show it read-only. // External invites can't have their role changed; show it read-only.
// Members and Proton invites get a role dropdown. // Members and Proton invites get a role dropdown.
@@ -653,16 +682,7 @@ pub(crate) fn repaint_share_link(state: &Rc<ShareDialog>, link: Option<&PublicLi
.build(); .build();
let expiry_row = adw::ActionRow::builder().title(gettext("Expires")).build(); let expiry_row = adw::ActionRow::builder().title(gettext("Expires")).build();
expiry_row.add_suffix(&expiry_drop); expiry_row.add_suffix(&expiry_drop);
let create = gtk4::Button::builder() let create = group_button(&gettext("Create Public Link"));
.label(gettext("Create Public Link"))
.halign(gtk4::Align::End)
.margin_top(6)
.build();
create.add_css_class("suggested-action");
let create_wrap = adw::PreferencesRow::builder()
.activatable(false)
.child(&create)
.build();
// Enter in the password field creates the link. // Enter in the password field creates the link.
let btn = create.clone(); let btn = create.clone();
@@ -689,11 +709,11 @@ pub(crate) fn repaint_share_link(state: &Rc<ShareDialog>, link: Option<&PublicLi
state.link_group.add(&role_row); state.link_group.add(&role_row);
state.link_group.add(&pw_row); state.link_group.add(&pw_row);
state.link_group.add(&expiry_row); state.link_group.add(&expiry_row);
state.link_group.add(&create_wrap); state.link_group.add(&create);
rows.push(role_row.upcast()); rows.push(role_row.upcast());
rows.push(pw_row.upcast()); rows.push(pw_row.upcast());
rows.push(expiry_row.upcast()); rows.push(expiry_row.upcast());
rows.push(create_wrap.upcast()); rows.push(create.upcast());
} }
} }
*state.link_rows.borrow_mut() = rows; *state.link_rows.borrow_mut() = rows;
+481 -475
View File
File diff suppressed because it is too large. Load diff
+481 -475
View File
File diff suppressed because it is too large. Load diff
+481 -475
View File
File diff suppressed because it is too large. Load diff
+481 -475
View File
File diff suppressed because it is too large. Load diff
+481 -475
View File
File diff suppressed because it is too large. Load diff
+481 -475
View File
File diff suppressed because it is too large. Load diff
+466 -472
View File
File diff suppressed because it is too large. Load diff
+481 -475
View File
File diff suppressed because it is too large. Load diff
+481 -475
View File
File diff suppressed because it is too large. Load diff