feat: add multi-file preview
This commit is contained in:
parent
456d4b003e
commit
361465e337
4 changed files with 197 additions and 35 deletions
2
justfile
2
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:
|
||||
|
|
|
|||
62
src/app.rs
62
src/app.rs
|
|
@ -1906,16 +1906,25 @@ impl App {
|
|||
PreviewKind::Selected => {
|
||||
if let Some(tab) = self.tab_model.data::<Tab>(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::<Tab>(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(
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
147
src/tab.rs
147
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<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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue