feat: add multi-file preview

This commit is contained in:
Jonatan Pettersson 2025-12-21 19:53:09 +01:00 committed by Jacob Kauffmann
parent 456d4b003e
commit 361465e337
4 changed files with 197 additions and 35 deletions

View file

@ -20,11 +20,13 @@ use cosmic::{
event,
futures::{self, SinkExt},
keyboard::Modifiers,
padding,
stream,
//TODO: export in cosmic::widget
widget::{
horizontal_rule, rule,
scrollable::{self, AbsoluteOffset, Viewport},
stack,
},
window,
},
@ -55,7 +57,7 @@ use std::{
borrow::Cow,
cell::{Cell, RefCell},
cmp::{Ordering, Reverse},
collections::HashMap,
collections::{BTreeMap, HashMap},
error::Error,
fmt::{self, Display},
fs::{self, File, Metadata},
@ -5851,7 +5853,135 @@ impl Tab {
dnd_dest.into()
}
pub fn multi_preview_view<'a>(&'a self) -> Element<'a, Message> {
let cosmic_theme::Spacing {
space_xxxs,
space_m,
..
} = theme::active().cosmic().spacing;
let mut column = widget::column().spacing(space_m);
let handle = widget::icon::from_name("text-x-generic")
.size(IconSizes::default().grid())
.handle();
let icon = widget::icon::icon(handle.clone())
.content_fit(ContentFit::Contain)
.size(IconSizes::default().grid());
let icon_container1 = widget::container(icon.clone()).padding(padding::bottom(10).left(10));
let icon_container2 =
widget::container(icon.clone()).padding(padding::top(5).bottom(5).left(5).right(5));
let icon_container3 = widget::container(icon).padding(padding::top(10).right(10));
let stack = stack![icon_container1, icon_container2, icon_container3];
column = column.push(
widget::container(stack)
.center_x(Length::Fill)
.max_height(THUMBNAIL_SIZE as f32),
);
let selected_items: Vec<&Item> = self.items_opt().map_or(Vec::new(), |items| {
items.iter().filter(|item| item.selected).collect()
});
let mut details = widget::column().spacing(space_xxxs);
details = details.push(widget::text::body(fl!(
"items",
items = selected_items.len()
)));
let mut total_size: u64 = 0;
let mut mime_type_counts: BTreeMap<String, u64> = BTreeMap::new();
let mut calculating_dir_size = false;
let mut dir_size_error: Option<String> = None;
for item in selected_items.iter() {
*mime_type_counts.entry(item.mime.to_string()).or_insert(0) += 1;
let mut file_metadata = None;
match &item.metadata {
ItemMetadata::Path { metadata, .. } => {
file_metadata = Some(metadata.clone());
}
#[cfg(feature = "gvfs")]
ItemMetadata::GvfsPath { .. } => {
// grab the fs::metadata object for gvfs paths since this is run on-demand
if let Some(path) = item.path_opt() {
file_metadata = fs::metadata(path).ok();
}
}
_ => {
//TODO: other metadata types
}
}
if let Some(metadata) = file_metadata {
if metadata.is_dir() {
match &item.dir_size {
DirSize::Calculating(_) => {
calculating_dir_size = true;
}
DirSize::Directory(size) => {
total_size = total_size.saturating_add(*size);
}
DirSize::NotDirectory => (),
DirSize::Error(err) => {
dir_size_error = Some(err.clone());
}
};
} else {
total_size = total_size.saturating_add(metadata.len());
}
}
}
let mut mime_types: Vec<(String, u64)> = mime_type_counts.into_iter().collect();
mime_types.sort_by(|(_, v1), (_, v2)| v2.cmp(v1));
// Limit the number of displayed mime types
mime_types.truncate(10);
let mut mime_types_total: u64 = 0;
let mut mime_types: Vec<String> = mime_types
.into_iter()
.map(|(mime, count)| {
mime_types_total += count;
format!("{} ({})", mime, count)
})
.collect();
if selected_items
.len()
.saturating_sub(mime_types_total as usize)
> 0
{
mime_types.push(format!("..."));
}
details = details.push(widget::text::body(fl!(
"type",
mime = mime_types.join(", ")
)));
let size = {
if calculating_dir_size {
fl!("calculating")
} else if let Some(error) = dir_size_error {
error
} else {
format_size(total_size)
}
};
details = details.push(widget::text::body(fl!("item-size", size = size)));
column = column.push(details);
column.into()
}
pub fn view<'a>(
&'a self,
key_binds: &'a HashMap<KeyBind, Action>,
@ -5980,11 +6110,16 @@ impl Tab {
if preview {
// Load directory size for selected items
if let Some(item) = items
.iter()
.find(|&item| item.selected)
.or(self.parent_item_opt.as_ref())
{
let mut selected_items: Vec<&Item> =
items.iter().filter(|item| item.selected).collect();
if selected_items.is_empty() {
if let Some(p) = self.parent_item_opt.as_ref() {
selected_items.push(p)
}
}
for item in selected_items {
// Item must have a path
if let Some(path) = item.path_opt().cloned() {
// Item must be calculating directory size