diff --git a/justfile b/justfile index 2abfbee..59414c7 100644 --- a/justfile +++ b/justfile @@ -67,7 +67,7 @@ dev *args: # Run with debug logs run *args: cargo build --release - env RUST_LOG=cosmic_files=info RUST_BACKTRACE=full {{bin-src}} {{args}} + env RUST_LOG=cosmic_files=debug RUST_BACKTRACE=full {{bin-src}} {{args}} # Run tests test *args: diff --git a/src/app.rs b/src/app.rs index e6e0242..ec9a11a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1906,16 +1906,25 @@ impl App { PreviewKind::Selected => { if let Some(tab) = self.tab_model.data::(entity) { if let Some(items) = tab.items_opt() { - for item in items { - if item.selected { - children.push( + let preview_opt = { + let mut selected = items.iter().filter(|item| item.selected); + + match (selected.next(), selected.next()) { + // At least two selected items + (Some(_), Some(_)) => Some(tab.multi_preview_view()), + // Exactly one selected item + (Some(item), None) => Some( item.preview_view(Some(&self.mime_app_cache), military_time), - ); - // Only show one property view to avoid issues like hangs when generating - // preview images on thousands of files - break; + ), + // No selected items + _ => None, } + }; + + if let Some(preview) = preview_opt { + children.push(preview); } + if children.is_empty() { if let Some(item) = &tab.parent_item_opt { children.push( @@ -3627,9 +3636,18 @@ impl Application for App { return cosmic::task::message(Message::SetShowDetails(show_details)); } Mode::Desktop => { - let selected_paths: Box<[_]> = self.selected_paths(entity_opt).collect(); - let mut commands = Vec::with_capacity(selected_paths.len()); - for path in selected_paths { + let preview_kind = { + let mut selected_paths = self.selected_paths(entity_opt); + match (selected_paths.next(), selected_paths.next()) { + (Some(_), Some(_)) => Some(PreviewKind::Selected), + (Some(path), None) => { + Some(PreviewKind::Location(Location::Path(path))) + } + _ => None, + } + }; + + if let Some(preview_kind) = preview_kind { let mut settings = window::Settings { decorations: true, min_size: Some(Size::new(360.0, 180.0)), @@ -3649,14 +3667,10 @@ impl Application for App { let (id, command) = window::open(settings); self.windows.insert( id, - Window::new(WindowKind::Preview( - entity_opt, - PreviewKind::Location(Location::Path(path)), - )), + Window::new(WindowKind::Preview(entity_opt, preview_kind)), ); - commands.push(command.map(|_id| cosmic::action::none())); + return command.map(|_id| cosmic::action::none()); } - return Task::batch(commands); } } } @@ -4786,13 +4800,17 @@ impl Application for App { .tab_model .data::(entity) .and_then(|tab| { - tab.items_opt()? - .iter() - .find(|item| item.selected) - .map(|item| { + let mut selected = tab.items_opt()?.iter().filter(|item| item.selected); + + match (selected.next(), selected.next()) { + // Exactly one item + (Some(item), None) => Some( item.preview_actions() - .map(move |x| Message::TabMessage(Some(entity), x)) - }) + .map(move |x| Message::TabMessage(Some(entity), x)), + ), + // Zero or more than one item + _ => None, + } }) .unwrap_or_else(|| widget::horizontal_space().into()); context_drawer::context_drawer( diff --git a/src/dialog.rs b/src/dialog.rs index 270f026..b4dc047 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -697,14 +697,23 @@ impl App { } PreviewKind::Selected => { if let Some(items) = self.tab.items_opt() { - for item in items { - if item.selected { - children.push(item.preview_view(None, military_time)); - // Only show one property view to avoid issues like hangs when generating - // preview images on thousands of files - break; + let preview_opt = { + let mut selected = items.iter().filter(|item| item.selected); + + match (selected.next(), selected.next()) { + // At least two selected items + (Some(_), Some(_)) => Some(self.tab.multi_preview_view()), + // Exactly one selected item + (Some(item), None) => Some(item.preview_view(None, military_time)), + // No selected items + _ => None, } + }; + + if let Some(preview) = preview_opt { + children.push(preview); } + if children.is_empty() { if let Some(item) = &self.tab.parent_item_opt { children.push(item.preview_view(None, military_time)); diff --git a/src/tab.rs b/src/tab.rs index 4f1c67f..9c48d76 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -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 = BTreeMap::new(); + let mut calculating_dir_size = false; + let mut dir_size_error: Option = 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 = 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, @@ -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