Add thumbnailing

This commit is contained in:
Jeremy Soller 2024-02-22 16:17:39 -07:00
parent f38ce15bf9
commit 9905af8f6f
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
5 changed files with 166 additions and 43 deletions

1
Cargo.lock generated
View file

@ -1156,6 +1156,7 @@ dependencies = [
"fork", "fork",
"i18n-embed", "i18n-embed",
"i18n-embed-fl", "i18n-embed-fl",
"image 0.24.8",
"lexical-sort", "lexical-sort",
"libcosmic", "libcosmic",
"log", "log",

View file

@ -8,6 +8,7 @@ rust-version = "1.71"
chrono = { version = "0.4", features = ["unstable-locales"] } chrono = { version = "0.4", features = ["unstable-locales"] }
dirs = "5.0.1" dirs = "5.0.1"
env_logger = "0.11" env_logger = "0.11"
image = "0.24"
once_cell = "1.19" once_cell = "1.19"
lexical-sort = "0.3.1" lexical-sort = "0.3.1"
log = "0.4" log = "0.4"

View file

@ -359,6 +359,9 @@ impl App {
for item in items.iter() { for item in items.iter() {
if item.selected { if item.selected {
children.push(item.property_view(&self.core, tab.config.icon_sizes)); children.push(item.property_view(&self.core, tab.config.icon_sizes));
// Only show one property view to avoid issues like hangs when generating
// preview images on thousands of files
break;
} }
} }
} }
@ -1158,6 +1161,16 @@ impl Application for App {
)); ));
} }
for entity in self.tab_model.iter() {
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
subscriptions.push(
tab.subscription()
.with(entity)
.map(|(entity, tab_msg)| Message::TabMessage(Some(entity), tab_msg)),
);
}
}
Subscription::batch(subscriptions) Subscription::batch(subscriptions)
} }
} }

View file

@ -632,6 +632,7 @@ impl Application for App {
} }
}, },
), ),
self.tab.subscription().map(Message::TabMessage),
]) ])
} }
} }

View file

@ -3,7 +3,9 @@ use cosmic::{
cosmic_theme, cosmic_theme,
iced::{ iced::{
alignment::{Horizontal, Vertical}, alignment::{Horizontal, Vertical},
futures::SinkExt,
keyboard::Modifiers, keyboard::Modifiers,
subscription::{self, Subscription},
//TODO: export in cosmic::widget //TODO: export in cosmic::widget
widget::horizontal_rule, widget::horizontal_rule,
Alignment, Alignment,
@ -234,8 +236,6 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
let mime_guess = MimeGuess::from_path(&path); let mime_guess = MimeGuess::from_path(&path);
//TODO: previews of images
let (icon_handle_dialog, icon_handle_grid, icon_handle_list) = if metadata.is_dir() let (icon_handle_dialog, icon_handle_grid, icon_handle_list) = if metadata.is_dir()
{ {
( (
@ -273,6 +273,10 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
icon_handle_dialog, icon_handle_dialog,
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
thumbnail_res_opt: match mime_guess.first() {
Some(mime) if mime.type_() == "image" => None,
_ => Some(Err(())),
},
selected: false, selected: false,
click_time: None, click_time: None,
}); });
@ -355,6 +359,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
icon_handle_dialog, icon_handle_dialog,
icon_handle_grid, icon_handle_grid,
icon_handle_list, icon_handle_list,
thumbnail_res_opt: Some(Err(())),
selected: false, selected: false,
click_time: None, click_time: None,
}); });
@ -397,6 +402,7 @@ pub enum Message {
Location(Location), Location(Location),
LocationUp, LocationUp,
RightClick(usize), RightClick(usize),
Thumbnail(PathBuf, Result<image::RgbaImage, ()>),
ToggleShowHidden, ToggleShowHidden,
View(View), View(View),
} }
@ -435,25 +441,39 @@ pub struct Item {
pub icon_handle_dialog: widget::icon::Handle, pub icon_handle_dialog: widget::icon::Handle,
pub icon_handle_grid: widget::icon::Handle, pub icon_handle_grid: widget::icon::Handle,
pub icon_handle_list: widget::icon::Handle, pub icon_handle_list: widget::icon::Handle,
pub thumbnail_res_opt: Option<Result<image::RgbaImage, ()>>,
pub selected: bool, pub selected: bool,
pub click_time: Option<Instant>, pub click_time: Option<Instant>,
} }
impl Item { impl Item {
pub fn property_view(&self, core: &Core, sizes: IconSizes) -> Element<crate::app::Message> { pub fn property_view(&self, core: &Core, sizes: IconSizes) -> Element<crate::app::Message> {
let mut section = widget::settings::view_section(""); let cosmic_theme::Spacing { space_xxxs, .. } = core.system_theme().cosmic().spacing;
section = section.add(widget::icon::icon(self.icon_handle_grid.clone()).size(sizes.grid())); let mut column = widget::column().spacing(space_xxxs);
section = section.add(widget::settings::item::item_row(vec![ let is_image = if let Some(mime) = self.mime_guess.first() {
widget::text::heading(self.name.clone()).into(), mime.type_() == "image" && self.path.is_file()
} else {
false
};
column = column.push(widget::row::with_children(vec![
widget::horizontal_space(Length::Fill).into(),
if is_image {
widget::image::Image::new(&self.path).into()
} else {
widget::icon::icon(self.icon_handle_grid.clone())
.size(sizes.grid())
.into()
},
widget::horizontal_space(Length::Fill).into(),
])); ]));
column = column.push(widget::text::heading(self.name.clone()));
if let Some(mime) = self.mime_guess.first() { if let Some(mime) = self.mime_guess.first() {
section = section.add(widget::settings::item( column = column.push(widget::text(format!("Type: {}", mime)));
"Type",
widget::text(format!("{}", mime)),
));
} }
//TODO: translate! //TODO: translate!
@ -461,48 +481,33 @@ impl Item {
match &self.metadata { match &self.metadata {
ItemMetadata::Path { metadata, children } => { ItemMetadata::Path { metadata, children } => {
if metadata.is_dir() { if metadata.is_dir() {
section = section.add(widget::settings::item::item( column = column.push(widget::text(format!("Items: {}", children)));
"Items",
widget::text(format!("{}", children)),
));
} else { } else {
section = section.add(widget::settings::item::item( column = column.push(widget::text(format!(
"Size", "Size: {}",
widget::text(format_size(metadata.len())), format_size(metadata.len())
)); )));
} }
if let Ok(time) = metadata.created() { if let Ok(time) = metadata.created() {
section = section.add(widget::settings::item( column = column.push(widget::text(format!(
"Created", "Created: {}",
widget::text( chrono::DateTime::<chrono::Local>::from(time).format(TIME_FORMAT)
chrono::DateTime::<chrono::Local>::from(time) )));
.format(TIME_FORMAT)
.to_string(),
),
));
} }
if let Ok(time) = metadata.modified() { if let Ok(time) = metadata.modified() {
section = section.add(widget::settings::item( column = column.push(widget::text(format!(
"Modified", "Modified: {}",
widget::text( chrono::DateTime::<chrono::Local>::from(time).format(TIME_FORMAT)
chrono::DateTime::<chrono::Local>::from(time) )));
.format(TIME_FORMAT)
.to_string(),
),
));
} }
if let Ok(time) = metadata.accessed() { if let Ok(time) = metadata.accessed() {
section = section.add(widget::settings::item( column = column.push(widget::text(format!(
"Accessed", "Accessed: {}",
widget::text( chrono::DateTime::<chrono::Local>::from(time).format(TIME_FORMAT)
chrono::DateTime::<chrono::Local>::from(time) )));
.format(TIME_FORMAT)
.to_string(),
),
));
} }
} }
ItemMetadata::Trash { .. } => { ItemMetadata::Trash { .. } => {
@ -510,7 +515,7 @@ impl Item {
} }
} }
section.into() column.into()
} }
} }
@ -678,6 +683,27 @@ impl Tab {
} }
} }
} }
Message::Thumbnail(path, thumbnail_res) => {
if let Some(ref mut items) = self.items_opt {
for item in items.iter_mut() {
if item.path == path {
if let Ok(thumbnail) = &thumbnail_res {
//TODO: pass handles already generated to avoid blocking main thread
let handle = widget::icon::from_raster_pixels(
thumbnail.width(),
thumbnail.height(),
thumbnail.as_raw().clone(),
);
item.icon_handle_dialog = handle.clone();
item.icon_handle_grid = handle.clone();
item.icon_handle_list = handle;
}
item.thumbnail_res_opt = Some(thumbnail_res);
break;
}
}
}
}
Message::ToggleShowHidden => self.config.show_hidden = !self.config.show_hidden, Message::ToggleShowHidden => self.config.show_hidden = !self.config.show_hidden,
Message::View(view) => { Message::View(view) => {
self.view = view; self.view = view;
@ -1077,6 +1103,87 @@ impl Tab {
.width(Length::Fill) .width(Length::Fill)
.into() .into()
} }
pub fn subscription(&self) -> Subscription<Message> {
if let Some(items) = &self.items_opt {
//TODO: how many thumbnail loads should be in flight at once?
let jobs = 8;
let mut subscriptions = Vec::with_capacity(jobs);
for item in items.iter() {
match item.thumbnail_res_opt {
Some(_) => continue,
None => {
let path = item.path.clone();
subscriptions.push(subscription::channel(
path.clone(),
1,
|mut output| async move {
let (path, thumbnail_res) =
tokio::task::spawn_blocking(move || {
let start = std::time::Instant::now();
let thumbnail_res = match image::io::Reader::open(&path) {
Ok(reader) => match reader.decode() {
Ok(image) => {
//TODO: configurable thumbnail size
let thumbnail = image.thumbnail(256, 256);
Ok(thumbnail.to_rgba8())
}
Err(err) => {
log::warn!(
"failed to decode {:?}: {}",
path,
err
);
Err(())
}
},
Err(err) => {
log::warn!("failed to read {:?}: {}", path, err);
Err(())
}
};
log::info!(
"thumbnailed {:?} in {:?}",
path,
start.elapsed()
);
(path, thumbnail_res)
})
.await
.unwrap();
match output
.send(Message::Thumbnail(path.clone(), thumbnail_res))
.await
{
Ok(()) => {}
Err(err) => {
log::warn!(
"failed to send thumbnail for {:?}: {}",
path,
err
);
}
}
//TODO: how to properly kill this task?
loop {
tokio::time::sleep(std::time::Duration::new(1, 0)).await;
}
},
));
}
}
if subscriptions.len() >= jobs {
break;
}
}
Subscription::batch(subscriptions)
} else {
Subscription::none()
}
}
} }
#[cfg(test)] #[cfg(test)]