Add thumbnailing
This commit is contained in:
parent
f38ce15bf9
commit
9905af8f6f
5 changed files with 166 additions and 43 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1156,6 +1156,7 @@ dependencies = [
|
|||
"fork",
|
||||
"i18n-embed",
|
||||
"i18n-embed-fl",
|
||||
"image 0.24.8",
|
||||
"lexical-sort",
|
||||
"libcosmic",
|
||||
"log",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ rust-version = "1.71"
|
|||
chrono = { version = "0.4", features = ["unstable-locales"] }
|
||||
dirs = "5.0.1"
|
||||
env_logger = "0.11"
|
||||
image = "0.24"
|
||||
once_cell = "1.19"
|
||||
lexical-sort = "0.3.1"
|
||||
log = "0.4"
|
||||
|
|
|
|||
13
src/app.rs
13
src/app.rs
|
|
@ -359,6 +359,9 @@ impl App {
|
|||
for item in items.iter() {
|
||||
if item.selected {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -632,6 +632,7 @@ impl Application for App {
|
|||
}
|
||||
},
|
||||
),
|
||||
self.tab.subscription().map(Message::TabMessage),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
193
src/tab.rs
193
src/tab.rs
|
|
@ -3,7 +3,9 @@ use cosmic::{
|
|||
cosmic_theme,
|
||||
iced::{
|
||||
alignment::{Horizontal, Vertical},
|
||||
futures::SinkExt,
|
||||
keyboard::Modifiers,
|
||||
subscription::{self, Subscription},
|
||||
//TODO: export in cosmic::widget
|
||||
widget::horizontal_rule,
|
||||
Alignment,
|
||||
|
|
@ -234,8 +236,6 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
|||
|
||||
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()
|
||||
{
|
||||
(
|
||||
|
|
@ -273,6 +273,10 @@ pub fn scan_path(tab_path: &PathBuf, sizes: IconSizes) -> Vec<Item> {
|
|||
icon_handle_dialog,
|
||||
icon_handle_grid,
|
||||
icon_handle_list,
|
||||
thumbnail_res_opt: match mime_guess.first() {
|
||||
Some(mime) if mime.type_() == "image" => None,
|
||||
_ => Some(Err(())),
|
||||
},
|
||||
selected: false,
|
||||
click_time: None,
|
||||
});
|
||||
|
|
@ -355,6 +359,7 @@ pub fn scan_trash(sizes: IconSizes) -> Vec<Item> {
|
|||
icon_handle_dialog,
|
||||
icon_handle_grid,
|
||||
icon_handle_list,
|
||||
thumbnail_res_opt: Some(Err(())),
|
||||
selected: false,
|
||||
click_time: None,
|
||||
});
|
||||
|
|
@ -397,6 +402,7 @@ pub enum Message {
|
|||
Location(Location),
|
||||
LocationUp,
|
||||
RightClick(usize),
|
||||
Thumbnail(PathBuf, Result<image::RgbaImage, ()>),
|
||||
ToggleShowHidden,
|
||||
View(View),
|
||||
}
|
||||
|
|
@ -435,25 +441,39 @@ pub struct Item {
|
|||
pub icon_handle_dialog: widget::icon::Handle,
|
||||
pub icon_handle_grid: widget::icon::Handle,
|
||||
pub icon_handle_list: widget::icon::Handle,
|
||||
pub thumbnail_res_opt: Option<Result<image::RgbaImage, ()>>,
|
||||
pub selected: bool,
|
||||
pub click_time: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
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![
|
||||
widget::text::heading(self.name.clone()).into(),
|
||||
let is_image = if let Some(mime) = self.mime_guess.first() {
|
||||
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() {
|
||||
section = section.add(widget::settings::item(
|
||||
"Type",
|
||||
widget::text(format!("{}", mime)),
|
||||
));
|
||||
column = column.push(widget::text(format!("Type: {}", mime)));
|
||||
}
|
||||
|
||||
//TODO: translate!
|
||||
|
|
@ -461,48 +481,33 @@ impl Item {
|
|||
match &self.metadata {
|
||||
ItemMetadata::Path { metadata, children } => {
|
||||
if metadata.is_dir() {
|
||||
section = section.add(widget::settings::item::item(
|
||||
"Items",
|
||||
widget::text(format!("{}", children)),
|
||||
));
|
||||
column = column.push(widget::text(format!("Items: {}", children)));
|
||||
} else {
|
||||
section = section.add(widget::settings::item::item(
|
||||
"Size",
|
||||
widget::text(format_size(metadata.len())),
|
||||
));
|
||||
column = column.push(widget::text(format!(
|
||||
"Size: {}",
|
||||
format_size(metadata.len())
|
||||
)));
|
||||
}
|
||||
|
||||
if let Ok(time) = metadata.created() {
|
||||
section = section.add(widget::settings::item(
|
||||
"Created",
|
||||
widget::text(
|
||||
chrono::DateTime::<chrono::Local>::from(time)
|
||||
.format(TIME_FORMAT)
|
||||
.to_string(),
|
||||
),
|
||||
));
|
||||
column = column.push(widget::text(format!(
|
||||
"Created: {}",
|
||||
chrono::DateTime::<chrono::Local>::from(time).format(TIME_FORMAT)
|
||||
)));
|
||||
}
|
||||
|
||||
if let Ok(time) = metadata.modified() {
|
||||
section = section.add(widget::settings::item(
|
||||
"Modified",
|
||||
widget::text(
|
||||
chrono::DateTime::<chrono::Local>::from(time)
|
||||
.format(TIME_FORMAT)
|
||||
.to_string(),
|
||||
),
|
||||
));
|
||||
column = column.push(widget::text(format!(
|
||||
"Modified: {}",
|
||||
chrono::DateTime::<chrono::Local>::from(time).format(TIME_FORMAT)
|
||||
)));
|
||||
}
|
||||
|
||||
if let Ok(time) = metadata.accessed() {
|
||||
section = section.add(widget::settings::item(
|
||||
"Accessed",
|
||||
widget::text(
|
||||
chrono::DateTime::<chrono::Local>::from(time)
|
||||
.format(TIME_FORMAT)
|
||||
.to_string(),
|
||||
),
|
||||
));
|
||||
column = column.push(widget::text(format!(
|
||||
"Accessed: {}",
|
||||
chrono::DateTime::<chrono::Local>::from(time).format(TIME_FORMAT)
|
||||
)));
|
||||
}
|
||||
}
|
||||
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::View(view) => {
|
||||
self.view = view;
|
||||
|
|
@ -1077,6 +1103,87 @@ impl Tab {
|
|||
.width(Length::Fill)
|
||||
.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)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue