mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 00:47:00 +01:00
The share dialog gets Link settings under an existing link: role, password and expiry apply at once and keep the URL. The daemon fills in the options a change keeps from the link as it stands, and refuses to keep a password it cannot read rather than dropping it. pdfs public-link update does the same from a terminal, and pdfs members shows the expiry. Details shows a folder's size and item count as Proton Drive counts it, trash included, and pdfs du prints it. Both are refused offline and for a folder that is not on Drive yet. Needs proton-drive-rs with update_public_link, get_folder_size and PublicLink::custom_password.
620 lines
22 KiB
Rust
620 lines
22 KiB
Rust
use crate::*;
|
|
|
|
pub(crate) struct DetailsState {
|
|
/// Widgets in the details pane, repainted from the selected entry.
|
|
pub(crate) details: DetailsWidgets,
|
|
/// The entry the details pane is currently showing, so its buttons act on the
|
|
/// same one the user is looking at even after the model is repopulated.
|
|
pub(crate) details_entry: RefCell<Option<DirEntry>>,
|
|
/// Set while the details pane is being populated, so setting the offline
|
|
/// switch programmatically doesn't fire a pin/unpin round-trip.
|
|
pub(crate) details_suppress: Cell<bool>,
|
|
/// The one selection model behind both the grid and the list, so switching
|
|
/// views keeps what is selected; it drives the details pane.
|
|
pub(crate) selection: gtk4::MultiSelection,
|
|
}
|
|
|
|
/// The widgets in the browser's details pane that a selection repaints.
|
|
pub(crate) struct DetailsWidgets {
|
|
pub(crate) icon: gtk4::Image,
|
|
pub(crate) name: gtk4::Label,
|
|
pub(crate) kind: gtk4::Label,
|
|
pub(crate) size_row: adw::ActionRow,
|
|
pub(crate) modified_row: adw::ActionRow,
|
|
pub(crate) path_row: adw::ActionRow,
|
|
pub(crate) pin_row: adw::SwitchRow,
|
|
pub(crate) open_button: gtk4::Button,
|
|
pub(crate) rename_button: gtk4::Button,
|
|
/// Who has access and whether there is a public link; filled on selection.
|
|
pub(crate) sharing_row: adw::ActionRow,
|
|
pub(crate) share_button: gtk4::Button,
|
|
pub(crate) copy_link_button: gtk4::Button,
|
|
/// Files only — folders have no revision history.
|
|
pub(crate) versions_group: adw::PreferencesGroup,
|
|
pub(crate) versions_row: adw::ActionRow,
|
|
pub(crate) versions_button: gtk4::Button,
|
|
pub(crate) trash_button: gtk4::Button,
|
|
pub(crate) close_button: gtk4::Button,
|
|
/// The header toggle that asks for the pane. Selecting an entry only
|
|
/// fills the pane; it is shown while this is on, so a click never moves
|
|
/// the files under the pointer halfway through a double-click.
|
|
pub(crate) toggle: gtk4::ToggleButton,
|
|
/// "details" while an entry is shown, "empty" with nothing selected.
|
|
pub(crate) pages: gtk4::Stack,
|
|
}
|
|
|
|
/// The details pane shown beside the file views: a big type icon over the entry's
|
|
/// name, then Info, Offline, Sharing and Versions sections and the primary
|
|
/// actions. Built empty; [`show_details`] fills it from the selected
|
|
/// [`DirEntry`] and [`wire_details`] connects the buttons.
|
|
pub(crate) fn build_details_pane() -> (gtk4::Widget, DetailsWidgets) {
|
|
let toggle = gtk4::ToggleButton::builder()
|
|
.icon_name("pdfs-details-pane-symbolic")
|
|
.tooltip_text(gettext("Details (Alt+Enter)"))
|
|
.build();
|
|
|
|
let close_button = gtk4::Button::builder()
|
|
.icon_name("window-close-symbolic")
|
|
.tooltip_text(gettext("Close details"))
|
|
.halign(gtk4::Align::End)
|
|
.build();
|
|
close_button.add_css_class("flat");
|
|
close_button.add_css_class("circular");
|
|
|
|
let icon = gtk4::Image::builder()
|
|
.icon_name("text-x-generic-symbolic")
|
|
.pixel_size(64)
|
|
.build();
|
|
let name = gtk4::Label::builder()
|
|
.wrap(true)
|
|
.wrap_mode(gtk4::pango::WrapMode::WordChar)
|
|
.justify(gtk4::Justification::Center)
|
|
.build();
|
|
name.add_css_class("title-4");
|
|
let kind = gtk4::Label::new(None);
|
|
kind.add_css_class("dim-label");
|
|
kind.add_css_class("caption");
|
|
|
|
let head = gtk4::Box::new(gtk4::Orientation::Vertical, 6);
|
|
head.set_margin_bottom(6);
|
|
head.append(&icon);
|
|
head.append(&name);
|
|
head.append(&kind);
|
|
|
|
let props = adw::PreferencesGroup::builder()
|
|
.title(gettext("Info"))
|
|
.build();
|
|
let size_row = adw::ActionRow::builder()
|
|
.title(pgettext("column", "Size"))
|
|
.subtitle("—")
|
|
.build();
|
|
size_row.add_css_class("property");
|
|
let modified_row = adw::ActionRow::builder()
|
|
.title(pgettext("column", "Modified"))
|
|
.subtitle("—")
|
|
.build();
|
|
modified_row.add_css_class("property");
|
|
let path_row = adw::ActionRow::builder()
|
|
.title(gettext("Location"))
|
|
.subtitle("—")
|
|
.subtitle_lines(3)
|
|
// A folder path, which may hold `&` or `<`.
|
|
.use_markup(false)
|
|
.build();
|
|
path_row.add_css_class("property");
|
|
props.add(&size_row);
|
|
props.add(&modified_row);
|
|
props.add(&path_row);
|
|
|
|
let offline = adw::PreferencesGroup::builder()
|
|
.title(pgettext("state", "Offline"))
|
|
.build();
|
|
let pin_row = adw::SwitchRow::builder()
|
|
.title(gettext("Available offline"))
|
|
.subtitle(gettext(
|
|
"Keep a copy on this computer, even without a connection.",
|
|
))
|
|
.build();
|
|
offline.add(&pin_row);
|
|
|
|
let sharing = adw::PreferencesGroup::builder()
|
|
.title(gettext("Sharing"))
|
|
.build();
|
|
let sharing_row = adw::ActionRow::builder()
|
|
.title(gettext("Access"))
|
|
.subtitle("—")
|
|
.build();
|
|
sharing_row.add_css_class("property");
|
|
sharing.add(&sharing_row);
|
|
let share_button = gtk4::Button::builder().label(gettext("Share…")).build();
|
|
let copy_link_button = gtk4::Button::builder().label(gettext("Copy Link")).build();
|
|
let share_actions = gtk4::Box::builder()
|
|
.orientation(gtk4::Orientation::Horizontal)
|
|
.spacing(6)
|
|
.homogeneous(true)
|
|
.margin_top(6)
|
|
.build();
|
|
share_actions.append(&share_button);
|
|
share_actions.append(©_link_button);
|
|
sharing.add(&share_actions);
|
|
|
|
let versions_group = adw::PreferencesGroup::builder()
|
|
.title(gettext("Versions"))
|
|
.build();
|
|
let versions_row = adw::ActionRow::builder()
|
|
.title(gettext("History"))
|
|
.subtitle("—")
|
|
.build();
|
|
versions_row.add_css_class("property");
|
|
versions_group.add(&versions_row);
|
|
let versions_button = gtk4::Button::builder()
|
|
.label(gettext("Versions…"))
|
|
.margin_top(6)
|
|
.build();
|
|
versions_group.add(&versions_button);
|
|
|
|
let open_button = gtk4::Button::builder()
|
|
.label(pgettext("verb", "Open"))
|
|
.build();
|
|
open_button.add_css_class("suggested-action");
|
|
open_button.add_css_class("pill");
|
|
let rename_button = gtk4::Button::builder().label(gettext("Rename")).build();
|
|
rename_button.add_css_class("pill");
|
|
let trash_button = gtk4::Button::builder()
|
|
.label(gettext("Move to Trash"))
|
|
.build();
|
|
trash_button.add_css_class("destructive-action");
|
|
trash_button.add_css_class("pill");
|
|
|
|
let actions = gtk4::Box::new(gtk4::Orientation::Vertical, 6);
|
|
actions.set_margin_top(6);
|
|
actions.append(&open_button);
|
|
actions.append(&rename_button);
|
|
actions.append(&trash_button);
|
|
|
|
let inner = gtk4::Box::new(gtk4::Orientation::Vertical, 12);
|
|
inner.set_margin_top(12);
|
|
inner.set_margin_bottom(12);
|
|
inner.set_margin_start(12);
|
|
inner.set_margin_end(12);
|
|
inner.append(&close_button);
|
|
inner.append(&head);
|
|
inner.append(&props);
|
|
inner.append(&offline);
|
|
inner.append(&sharing);
|
|
inner.append(&versions_group);
|
|
inner.append(&actions);
|
|
|
|
let scroll = gtk4::ScrolledWindow::builder()
|
|
.hscrollbar_policy(gtk4::PolicyType::Never)
|
|
.child(&inner)
|
|
.build();
|
|
// With the pane open and nothing selected (a fresh folder, a cleared
|
|
// selection) it says so rather than closing: opening and closing it by
|
|
// itself would move the files under the pointer.
|
|
let empty_close = gtk4::Button::builder()
|
|
.icon_name("window-close-symbolic")
|
|
.tooltip_text(gettext("Close details"))
|
|
.halign(gtk4::Align::End)
|
|
.margin_top(12)
|
|
.margin_end(12)
|
|
.build();
|
|
empty_close.add_css_class("flat");
|
|
empty_close.add_css_class("circular");
|
|
let toggle_off = toggle.clone();
|
|
empty_close.connect_clicked(move |_| toggle_off.set_active(false));
|
|
let empty_status = adw::StatusPage::builder()
|
|
.icon_name("pdfs-details-pane-symbolic")
|
|
.title(gettext("No item selected"))
|
|
.description(gettext("Select a file or folder to see its details."))
|
|
.vexpand(true)
|
|
.build();
|
|
empty_status.add_css_class("compact");
|
|
let empty = gtk4::Box::new(gtk4::Orientation::Vertical, 0);
|
|
empty.append(&empty_close);
|
|
empty.append(&empty_status);
|
|
|
|
let pages = gtk4::Stack::new();
|
|
pages.add_named(&scroll, Some("details"));
|
|
pages.add_named(&empty, Some("empty"));
|
|
pages.set_visible_child_name("empty");
|
|
let pane = adw::ToolbarView::new();
|
|
pane.set_content(Some(&pages));
|
|
pane.add_css_class("background");
|
|
|
|
(
|
|
pane.upcast(),
|
|
DetailsWidgets {
|
|
icon,
|
|
name,
|
|
kind,
|
|
size_row,
|
|
modified_row,
|
|
path_row,
|
|
pin_row,
|
|
sharing_row,
|
|
share_button,
|
|
copy_link_button,
|
|
versions_group,
|
|
versions_row,
|
|
open_button,
|
|
rename_button,
|
|
versions_button,
|
|
trash_button,
|
|
close_button,
|
|
toggle,
|
|
pages,
|
|
},
|
|
)
|
|
}
|
|
|
|
/// Connect the details pane: mirror the grid's and list's selection into it, and
|
|
/// wire its buttons back onto the entry it's showing.
|
|
pub(crate) fn wire_details(ui: &Rc<Ui>) {
|
|
{
|
|
let ui_sel = ui.clone();
|
|
ui.details
|
|
.selection
|
|
.connect_selection_changed(move |_, _, _| {
|
|
// The details pane describes *one* entry. A batch is the bulk bar's
|
|
// business, so the pane steps aside rather than picking a member of
|
|
// the selection to speak for the rest.
|
|
let entries = selected_entries(&ui_sel);
|
|
match entries.as_slice() {
|
|
[entry] => show_details(&ui_sel, entry),
|
|
_ => clear_details(&ui_sel),
|
|
}
|
|
sync_bulk_bar(&ui_sel);
|
|
});
|
|
}
|
|
|
|
let ui_close = ui.clone();
|
|
ui.details.details.close_button.connect_clicked(move |_| {
|
|
ui_close.details.details.toggle.set_active(false);
|
|
});
|
|
|
|
let ui_toggle = ui.clone();
|
|
ui.details.details.toggle.connect_toggled(move |toggle| {
|
|
ui_toggle.browser.split.set_show_sidebar(toggle.is_active());
|
|
});
|
|
// On a narrow window the pane overlays the files, and a click outside it
|
|
// closes it; the toggle follows so the next click on it opens it again.
|
|
let ui_split = ui.clone();
|
|
ui.browser.split.connect_show_sidebar_notify(move |split| {
|
|
let toggle = &ui_split.details.details.toggle;
|
|
if toggle.is_active() != split.shows_sidebar() {
|
|
toggle.set_active(split.shows_sidebar());
|
|
}
|
|
});
|
|
|
|
let ui_open = ui.clone();
|
|
ui.details.details.open_button.connect_clicked(move |_| {
|
|
// Bind the clone to a local so the `Ref` from `borrow()` is dropped before
|
|
// `activate_entry` runs; it navigates and repaints the details pane, which
|
|
// takes `details_entry.borrow_mut()` and would panic against a live borrow.
|
|
let entry = ui_open.details.details_entry.borrow().clone();
|
|
if let Some(entry) = entry {
|
|
activate_entry(&ui_open, &entry);
|
|
}
|
|
});
|
|
let ui_rename = ui.clone();
|
|
ui.details.details.rename_button.connect_clicked(move |_| {
|
|
let entry = ui_rename.details.details_entry.borrow().clone();
|
|
if let Some(entry) = entry {
|
|
prompt_rename(&ui_rename, &entry);
|
|
}
|
|
});
|
|
let ui_versions = ui.clone();
|
|
ui.details
|
|
.details
|
|
.versions_button
|
|
.connect_clicked(move |_| {
|
|
let entry = ui_versions.details.details_entry.borrow().clone();
|
|
if let Some(entry) = entry {
|
|
open_versions_dialog(&ui_versions, &entry);
|
|
}
|
|
});
|
|
let ui_share = ui.clone();
|
|
ui.details.details.share_button.connect_clicked(move |_| {
|
|
let entry = ui_share.details.details_entry.borrow().clone();
|
|
if let Some(entry) = entry {
|
|
open_share_dialog(&ui_share, &entry);
|
|
}
|
|
});
|
|
let ui_link = ui.clone();
|
|
ui.details
|
|
.details
|
|
.copy_link_button
|
|
.connect_clicked(move |_| {
|
|
let entry = ui_link.details.details_entry.borrow().clone();
|
|
if let Some(entry) = entry {
|
|
copy_entry_link(&ui_link, &entry);
|
|
}
|
|
});
|
|
let ui_trash = ui.clone();
|
|
ui.details.details.trash_button.connect_clicked(move |_| {
|
|
let entry = ui_trash.details.details_entry.borrow().clone();
|
|
if let Some(entry) = entry {
|
|
trash_entry(&ui_trash, &entry);
|
|
}
|
|
});
|
|
let ui_pin = ui.clone();
|
|
ui.details
|
|
.details
|
|
.pin_row
|
|
.connect_active_notify(move |row| {
|
|
if ui_pin.details.details_suppress.get() {
|
|
return;
|
|
}
|
|
let Some(entry) = ui_pin.details.details_entry.borrow().clone() else {
|
|
return;
|
|
};
|
|
// The switch reads the *desired* state; `toggle_pin` derives the request
|
|
// from the entry's current one, so only act when they actually differ.
|
|
if row.is_active() != entry.pinned {
|
|
toggle_pin(&ui_pin, &entry);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Paint the details pane from `entry`. It shows while the header toggle is on.
|
|
pub(crate) fn show_details(ui: &Rc<Ui>, entry: &DirEntry) {
|
|
ui.details.details_suppress.set(true);
|
|
let d = &ui.details.details;
|
|
d.icon.set_icon_name(Some(icon_for(entry)));
|
|
d.name.set_label(&entry.name);
|
|
d.kind.set_label(&if entry.is_dir {
|
|
gettext("Folder")
|
|
} else {
|
|
gettext("File")
|
|
});
|
|
// A folder's size is Proton Drive's count, which takes a request; it lands
|
|
// in `load_details_extras`.
|
|
if entry.is_dir {
|
|
d.size_row.set_title(&gettext("Size on Proton Drive"));
|
|
d.size_row.set_subtitle("—");
|
|
d.size_row.set_tooltip_text(Some(&gettext(
|
|
"Counted by Proton Drive, including items in the trash.",
|
|
)));
|
|
} else {
|
|
d.size_row.set_title(&pgettext("column", "Size"));
|
|
d.size_row.set_subtitle(&human_bytes(entry.size));
|
|
d.size_row.set_tooltip_text(None);
|
|
}
|
|
d.modified_row
|
|
.set_subtitle(&dates::short_date(entry.modified));
|
|
let rel = entry_rel(ui, entry);
|
|
let parent = match rel.rfind('/') {
|
|
Some(i) => &rel[..i],
|
|
None => "",
|
|
};
|
|
d.path_row.set_subtitle(&if parent.is_empty() {
|
|
gettext("Proton Drive")
|
|
} else {
|
|
parent.to_string()
|
|
});
|
|
// Kept through a pinned folder above it, the entry has no pin of its own
|
|
// to remove, so the switch shows that state without offering to change it.
|
|
d.pin_row.set_active(entry.pinned);
|
|
d.pin_row.set_subtitle(&if entry.pinned_by_folder {
|
|
gettext("Kept on this computer because a folder it is in is available offline.")
|
|
} else if entry.is_dir {
|
|
gettext(
|
|
"Keep a copy of everything in this folder on this computer, even without a connection.",
|
|
)
|
|
} else {
|
|
gettext("Keep a copy on this computer, even without a connection.")
|
|
});
|
|
d.pin_row
|
|
.set_sensitive(*ui.mounted.borrow() && !entry.pinned_by_folder);
|
|
d.open_button.set_label(&if entry.is_dir {
|
|
gettext("Open folder")
|
|
} else {
|
|
pgettext("verb", "Open")
|
|
});
|
|
// Only files have revisions, and only a connected daemon can fetch them.
|
|
let mounted = *ui.mounted.borrow();
|
|
d.versions_group.set_visible(!entry.is_dir);
|
|
d.versions_button.set_sensitive(mounted);
|
|
d.share_button.set_sensitive(mounted);
|
|
d.copy_link_button.set_sensitive(mounted);
|
|
d.sharing_row.set_subtitle("—");
|
|
d.versions_row.set_subtitle("—");
|
|
ui.details.details_suppress.set(false);
|
|
|
|
*ui.details.details_entry.borrow_mut() = Some(entry.clone());
|
|
if mounted {
|
|
load_details_extras(ui, entry);
|
|
}
|
|
// Only the content changes. Whether the pane shows is the toggle's call:
|
|
// this runs from `selection_changed`, on the *first* press of a
|
|
// double-click, and sliding the pane in there would move the files so the
|
|
// second press lands elsewhere and the folder never opens.
|
|
d.pages.set_visible_child_name("details");
|
|
}
|
|
|
|
/// Fill the Sharing and Versions rows, and a folder's size, which need the
|
|
/// network. The answers land only if the pane still shows the same entry.
|
|
fn load_details_extras(ui: &Rc<Ui>, entry: &DirEntry) {
|
|
let by_uid = entry.path.is_empty() && !entry.uid.is_empty();
|
|
let rel = entry_rel(ui, entry);
|
|
let share_req = if by_uid {
|
|
Request::ListShareByUid {
|
|
uid: entry.uid.clone(),
|
|
}
|
|
} else {
|
|
Request::ListShare { path: rel.clone() }
|
|
};
|
|
let rx = spawn_request(ui.dirs.control_socket(), share_req);
|
|
let (ui_c, uid) = (ui.clone(), entry.uid.clone());
|
|
glib::spawn_future_local(async move {
|
|
let text = match rx.recv().await {
|
|
Ok(Ok(Response::Share { entries, link })) => {
|
|
sharing_summary(entries.len(), link.is_some())
|
|
}
|
|
_ => gettext("Couldn't load sharing"),
|
|
};
|
|
if details_showing(&ui_c, &uid) {
|
|
ui_c.details.details.sharing_row.set_subtitle(&text);
|
|
}
|
|
});
|
|
|
|
if entry.is_dir {
|
|
let req = if by_uid {
|
|
Request::FolderSizeByUid {
|
|
uid: entry.uid.clone(),
|
|
}
|
|
} else {
|
|
Request::FolderSize { path: rel }
|
|
};
|
|
let rx = spawn_request(ui.dirs.control_socket(), req);
|
|
let (ui_c, uid) = (ui.clone(), entry.uid.clone());
|
|
glib::spawn_future_local(async move {
|
|
let text = match rx.recv().await {
|
|
Ok(Ok(Response::FolderSize { size, items })) => folder_size_summary(size, items),
|
|
_ => gettext("Couldn't load the size"),
|
|
};
|
|
if details_showing(&ui_c, &uid) {
|
|
ui_c.details.details.size_row.set_subtitle(&text);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
let req = if by_uid {
|
|
Request::ListRevisionsByUid {
|
|
uid: entry.uid.clone(),
|
|
}
|
|
} else {
|
|
Request::ListRevisions { path: rel }
|
|
};
|
|
let rx = spawn_request(ui.dirs.control_socket(), req);
|
|
let (ui_c, uid) = (ui.clone(), entry.uid.clone());
|
|
glib::spawn_future_local(async move {
|
|
let text = match rx.recv().await {
|
|
Ok(Ok(Response::Revisions { items })) => versions_summary(&items),
|
|
_ => gettext("Couldn't load versions"),
|
|
};
|
|
if details_showing(&ui_c, &uid) {
|
|
ui_c.details.details.versions_row.set_subtitle(&text);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn details_showing(ui: &Rc<Ui>, uid: &str) -> bool {
|
|
ui.details
|
|
.details_entry
|
|
.borrow()
|
|
.as_ref()
|
|
.is_some_and(|e| e.uid == uid)
|
|
}
|
|
|
|
/// "Only you", "3 people", "Public link" or both.
|
|
pub(crate) fn sharing_summary(people: usize, link: bool) -> String {
|
|
let n = people as u64;
|
|
match (people, link) {
|
|
(0, false) => gettext("Only you"),
|
|
(0, true) => gettext("Anyone with the link"),
|
|
(_, false) => ngettext_f("Shared with {n} person", "Shared with {n} people", n, &[]),
|
|
(_, true) => ngettext_f(
|
|
"Shared with {n} person · Public link",
|
|
"Shared with {n} people · Public link",
|
|
n,
|
|
&[],
|
|
),
|
|
}
|
|
}
|
|
|
|
/// "1.2 GiB · 340 items": what a folder holds, as Proton Drive counts it.
|
|
pub(crate) fn folder_size_summary(size: u64, items: u64) -> String {
|
|
// Translators: {size} is a size such as "1.2 GiB"; {n} counts the files
|
|
// and folders inside a folder.
|
|
ngettext_f(
|
|
"{size} · {n} item",
|
|
"{size} · {n} items",
|
|
items,
|
|
&[("size", &human_bytes(size))],
|
|
)
|
|
}
|
|
|
|
/// "3 versions · latest 2 Sep 2026".
|
|
pub(crate) fn versions_summary(items: &[RevisionInfo]) -> String {
|
|
let latest = items.iter().map(|r| r.created).max();
|
|
match (items.len(), latest) {
|
|
(0, _) | (_, None) => gettext("No earlier versions"),
|
|
// Translators: {date} is the date of the only version.
|
|
(1, Some(t)) => gettext_f("1 version · {date}", &[("date", &dates::short_date(t))]),
|
|
// Translators: {date} is the date of the newest version.
|
|
(n, Some(t)) => ngettext_f(
|
|
"{n} version · latest {date}",
|
|
"{n} versions · latest {date}",
|
|
n as u64,
|
|
&[("date", &dates::short_date(t))],
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Show the pane for `entry`, turning the header toggle on.
|
|
pub(crate) fn open_details(ui: &Rc<Ui>, entry: &DirEntry) {
|
|
ui.details.details.toggle.set_active(true);
|
|
show_details(ui, entry);
|
|
}
|
|
|
|
/// Alt+Enter: show or hide the pane for the selected entry.
|
|
pub(crate) fn toggle_details(ui: &Rc<Ui>) {
|
|
let toggle = &ui.details.details.toggle;
|
|
toggle.set_active(!toggle.is_active());
|
|
}
|
|
|
|
/// Forget the entry the pane was showing, so a stale entry can't be acted on
|
|
/// after the listing moves on. An open pane stays open and says nothing is
|
|
/// selected.
|
|
pub(crate) fn clear_details(ui: &Rc<Ui>) {
|
|
ui.details.details.pages.set_visible_child_name("empty");
|
|
*ui.details.details_entry.borrow_mut() = None;
|
|
}
|
|
|
|
/// The single entry the details pane is showing, if any. Backs the actions that
|
|
/// only make sense on one entry (rename, versions); the batch-capable ones ask
|
|
/// [`selected_entries`] instead.
|
|
pub(crate) fn selected_entry(ui: &Rc<Ui>) -> Option<DirEntry> {
|
|
ui.details.details_entry.borrow().clone()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn folder_size_summary_reads_size_and_item_count() {
|
|
assert_eq!(folder_size_summary(0, 0), "0 bytes · 0 items");
|
|
assert_eq!(folder_size_summary(2048, 1), "2.0 KiB · 1 item");
|
|
}
|
|
|
|
#[test]
|
|
fn sharing_summary_names_people_and_link() {
|
|
assert_eq!(sharing_summary(0, false), "Only you");
|
|
assert_eq!(sharing_summary(0, true), "Anyone with the link");
|
|
assert_eq!(sharing_summary(1, false), "Shared with 1 person");
|
|
assert_eq!(
|
|
sharing_summary(3, true),
|
|
"Shared with 3 people · Public link"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn versions_summary_counts_revisions() {
|
|
let rev = |created| RevisionInfo {
|
|
id: String::new(),
|
|
is_active: false,
|
|
created,
|
|
size_on_storage: 0,
|
|
claimed_size: None,
|
|
claimed_modified: None,
|
|
signed_by: None,
|
|
has_thumbnails: false,
|
|
};
|
|
assert_eq!(versions_summary(&[]), "No earlier versions");
|
|
assert!(versions_summary(&[rev(10), rev(20)]).starts_with("2 versions · latest "));
|
|
}
|
|
}
|