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",
|
"fork",
|
||||||
"i18n-embed",
|
"i18n-embed",
|
||||||
"i18n-embed-fl",
|
"i18n-embed-fl",
|
||||||
|
"image 0.24.8",
|
||||||
"lexical-sort",
|
"lexical-sort",
|
||||||
"libcosmic",
|
"libcosmic",
|
||||||
"log",
|
"log",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
13
src/app.rs
13
src/app.rs
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
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)]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue