Dynamically load thumbnails based on what files are visible
This commit is contained in:
parent
5c8fcd4f2e
commit
27b29e9fd8
3 changed files with 120 additions and 94 deletions
12
src/app.rs
12
src/app.rs
|
|
@ -11,7 +11,7 @@ use cosmic::{
|
||||||
subscription::{self, Subscription},
|
subscription::{self, Subscription},
|
||||||
window, Alignment, Event, Length,
|
window, Alignment, Event, Length,
|
||||||
},
|
},
|
||||||
style,
|
style, theme,
|
||||||
widget::{self, segmented_button},
|
widget::{self, segmented_button},
|
||||||
Application, ApplicationExt, Element,
|
Application, ApplicationExt, Element,
|
||||||
};
|
};
|
||||||
|
|
@ -352,7 +352,7 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn about(&self) -> Element<Message> {
|
fn about(&self) -> Element<Message> {
|
||||||
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing;
|
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||||
let repository = "https://github.com/pop-os/cosmic-files";
|
let repository = "https://github.com/pop-os/cosmic-files";
|
||||||
let hash = env!("VERGEN_GIT_SHA");
|
let hash = env!("VERGEN_GIT_SHA");
|
||||||
let short_hash: String = hash.chars().take(7).collect();
|
let short_hash: String = hash.chars().take(7).collect();
|
||||||
|
|
@ -431,7 +431,7 @@ impl App {
|
||||||
if let Some(ref items) = tab.items_opt {
|
if let Some(ref items) = tab.items_opt {
|
||||||
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(tab.config.icon_sizes));
|
||||||
// Only show one property view to avoid issues like hangs when generating
|
// Only show one property view to avoid issues like hangs when generating
|
||||||
// preview images on thousands of files
|
// preview images on thousands of files
|
||||||
break;
|
break;
|
||||||
|
|
@ -1146,7 +1146,7 @@ impl Application for App {
|
||||||
None => return None,
|
None => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing;
|
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
let dialog = match dialog_page {
|
let dialog = match dialog_page {
|
||||||
DialogPage::FailedOperation(id) => {
|
DialogPage::FailedOperation(id) => {
|
||||||
|
|
@ -1320,7 +1320,7 @@ impl Application for App {
|
||||||
|
|
||||||
/// Creates a view after each update.
|
/// Creates a view after each update.
|
||||||
fn view(&self) -> Element<Self::Message> {
|
fn view(&self) -> Element<Self::Message> {
|
||||||
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing;
|
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
let mut tab_column = widget::column::with_capacity(1);
|
let mut tab_column = widget::column::with_capacity(1);
|
||||||
|
|
||||||
|
|
@ -1342,7 +1342,7 @@ impl Application for App {
|
||||||
match self.tab_model.data::<Tab>(entity) {
|
match self.tab_model.data::<Tab>(entity) {
|
||||||
Some(tab) => {
|
Some(tab) => {
|
||||||
let tab_view = tab
|
let tab_view = tab
|
||||||
.view(self.core(), &self.key_binds)
|
.view(&self.key_binds)
|
||||||
.map(move |message| Message::TabMessage(Some(entity), message));
|
.map(move |message| Message::TabMessage(Some(entity), message));
|
||||||
tab_column = tab_column.push(tab_view);
|
tab_column = tab_column.push(tab_view);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use cosmic::{
|
||||||
subscription::{self, Subscription},
|
subscription::{self, Subscription},
|
||||||
window, Event, Length, Size,
|
window, Event, Length, Size,
|
||||||
},
|
},
|
||||||
|
theme,
|
||||||
widget::{self, segmented_button},
|
widget::{self, segmented_button},
|
||||||
Application, ApplicationExt, Element,
|
Application, ApplicationExt, Element,
|
||||||
};
|
};
|
||||||
|
|
@ -616,13 +617,13 @@ impl Application for App {
|
||||||
|
|
||||||
/// Creates a view after each update.
|
/// Creates a view after each update.
|
||||||
fn view(&self) -> Element<Message> {
|
fn view(&self) -> Element<Message> {
|
||||||
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing;
|
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
let mut tab_column = widget::column::with_capacity(2);
|
let mut tab_column = widget::column::with_capacity(2);
|
||||||
tab_column = tab_column.push(
|
tab_column = tab_column.push(
|
||||||
//TODO: key binds for dialog
|
//TODO: key binds for dialog
|
||||||
self.tab
|
self.tab
|
||||||
.view(self.core(), &HashMap::new())
|
.view(&HashMap::new())
|
||||||
.map(move |message| Message::TabMessage(message)),
|
.map(move |message| Message::TabMessage(message)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
197
src/tab.rs
197
src/tab.rs
|
|
@ -1,5 +1,4 @@
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
app::Core,
|
|
||||||
cosmic_theme,
|
cosmic_theme,
|
||||||
iced::{
|
iced::{
|
||||||
alignment::{Horizontal, Vertical},
|
alignment::{Horizontal, Vertical},
|
||||||
|
|
@ -444,8 +443,8 @@ pub struct Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item {
|
impl Item {
|
||||||
pub fn property_view(&self, core: &Core, sizes: IconSizes) -> Element<crate::app::Message> {
|
pub fn property_view(&self, sizes: IconSizes) -> Element<crate::app::Message> {
|
||||||
let cosmic_theme::Spacing { space_xxxs, .. } = core.system_theme().cosmic().spacing;
|
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
let mut column = widget::column().spacing(space_xxxs);
|
let mut column = widget::column().spacing(space_xxxs);
|
||||||
|
|
||||||
|
|
@ -874,7 +873,7 @@ impl Tab {
|
||||||
commands
|
commands
|
||||||
}
|
}
|
||||||
|
|
||||||
fn column_sort(&self) -> Option<Vec<Item>> {
|
fn column_sort(&self) -> Option<Vec<&Item>> {
|
||||||
let check_reverse = |ord: Ordering, sort: bool| {
|
let check_reverse = |ord: Ordering, sort: bool| {
|
||||||
if sort {
|
if sort {
|
||||||
ord
|
ord
|
||||||
|
|
@ -882,11 +881,11 @@ impl Tab {
|
||||||
ord.reverse()
|
ord.reverse()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut item = self.items_opt.clone()?;
|
let mut items: Vec<&Item> = self.items_opt.as_ref()?.iter().collect();
|
||||||
let heading_sort = self.sort_direction;
|
let heading_sort = self.sort_direction;
|
||||||
match self.sort_name {
|
match self.sort_name {
|
||||||
HeadingOptions::Size => {
|
HeadingOptions::Size => {
|
||||||
item.sort_by(|a, b| {
|
items.sort_by(|a, b| {
|
||||||
// entries take precedence over size
|
// entries take precedence over size
|
||||||
let get_size = |x: &Item| match &x.metadata {
|
let get_size = |x: &Item| match &x.metadata {
|
||||||
ItemMetadata::Path { metadata, children } => {
|
ItemMetadata::Path { metadata, children } => {
|
||||||
|
|
@ -912,7 +911,7 @@ impl Tab {
|
||||||
check_reverse(ord, heading_sort)
|
check_reverse(ord, heading_sort)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
HeadingOptions::Name => item.sort_by(|a, b| {
|
HeadingOptions::Name => items.sort_by(|a, b| {
|
||||||
let ord = match (a.path.is_dir(), b.path.is_dir()) {
|
let ord = match (a.path.is_dir(), b.path.is_dir()) {
|
||||||
(true, false) => Ordering::Less,
|
(true, false) => Ordering::Less,
|
||||||
(false, true) => Ordering::Greater,
|
(false, true) => Ordering::Greater,
|
||||||
|
|
@ -921,7 +920,7 @@ impl Tab {
|
||||||
check_reverse(ord, heading_sort)
|
check_reverse(ord, heading_sort)
|
||||||
}),
|
}),
|
||||||
HeadingOptions::Modified => {
|
HeadingOptions::Modified => {
|
||||||
item.sort_by(|a, b| {
|
items.sort_by(|a, b| {
|
||||||
let get_modified = |x: &Item| match &x.metadata {
|
let get_modified = |x: &Item| match &x.metadata {
|
||||||
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
|
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
|
||||||
ItemMetadata::Trash { .. } => None,
|
ItemMetadata::Trash { .. } => None,
|
||||||
|
|
@ -933,16 +932,16 @@ impl Tab {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(item)
|
Some(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn location_view(&self, core: &Core) -> Element<Message> {
|
pub fn location_view(&self) -> Element<Message> {
|
||||||
let cosmic_theme::Spacing {
|
let cosmic_theme::Spacing {
|
||||||
space_xxxs,
|
space_xxxs,
|
||||||
space_xxs,
|
space_xxs,
|
||||||
space_s,
|
space_s,
|
||||||
..
|
..
|
||||||
} = core.system_theme().cosmic().spacing;
|
} = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
let mut row = widget::row::with_capacity(5).align_items(Alignment::Center);
|
let mut row = widget::row::with_capacity(5).align_items(Alignment::Center);
|
||||||
|
|
||||||
|
|
@ -1080,8 +1079,8 @@ impl Tab {
|
||||||
row.into()
|
row.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn empty_view(&self, has_hidden: bool, core: &Core) -> Element<Message> {
|
pub fn empty_view(&self, has_hidden: bool) -> Element<Message> {
|
||||||
let cosmic_theme::Spacing { space_xxs, .. } = core.system_theme().cosmic().spacing;
|
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
widget::column::with_children(vec![widget::container(
|
widget::column::with_children(vec![widget::container(
|
||||||
widget::column::with_children(vec![
|
widget::column::with_children(vec![
|
||||||
|
|
@ -1107,13 +1106,13 @@ impl Tab {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn grid_view(&self, core: &Core) -> Element<Message> {
|
pub fn grid_view(&self) -> Element<Message> {
|
||||||
let cosmic_theme::Spacing {
|
let cosmic_theme::Spacing {
|
||||||
space_xs,
|
space_xs,
|
||||||
space_xxs,
|
space_xxs,
|
||||||
space_xxxs,
|
space_xxxs,
|
||||||
..
|
..
|
||||||
} = core.system_theme().cosmic().spacing;
|
} = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
let TabConfig {
|
let TabConfig {
|
||||||
show_hidden,
|
show_hidden,
|
||||||
|
|
@ -1214,7 +1213,7 @@ impl Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return self.empty_view(hidden > 0, core);
|
return self.empty_view(hidden > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that
|
//TODO: HACK If we don't reach the bottom of the view, go ahead and add a spacer to do that
|
||||||
|
|
@ -1250,10 +1249,12 @@ impl Tab {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_view(&self, core: &Core) -> Element<Message> {
|
pub fn list_view(&self) -> Element<Message> {
|
||||||
let cosmic_theme::Spacing { space_xxs, .. } = core.system_theme().cosmic().spacing;
|
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
//TODO: make adaptive?
|
//TODO: make adaptive?
|
||||||
|
let size = self.size_opt.unwrap_or_else(|| Size::new(0.0, 0.0));
|
||||||
|
let row_height = 40;
|
||||||
let modified_width = Length::Fixed(200.0);
|
let modified_width = Length::Fixed(200.0);
|
||||||
let size_width = Length::Fixed(100.0);
|
let size_width = Length::Fixed(100.0);
|
||||||
|
|
||||||
|
|
@ -1288,12 +1289,15 @@ impl Tab {
|
||||||
heading_item!("size", size_width, HeadingOptions::Size),
|
heading_item!("size", size_width, HeadingOptions::Size),
|
||||||
])
|
])
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
|
.height(Length::Fixed(row_height as f32))
|
||||||
.padding(space_xxs)
|
.padding(space_xxs)
|
||||||
.spacing(space_xxs)
|
.spacing(space_xxs)
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
let mut y = row_height;
|
||||||
|
|
||||||
children.push(horizontal_rule(1).into());
|
children.push(horizontal_rule(1).into());
|
||||||
|
y += 1;
|
||||||
|
|
||||||
if let Some(ref items) = self.column_sort() {
|
if let Some(ref items) = self.column_sort() {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
@ -1308,11 +1312,14 @@ impl Tab {
|
||||||
hidden += 1;
|
hidden += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
//TODO: correct rectangle
|
item.rect_opt.set(Some(Rectangle::new(
|
||||||
item.rect_opt.set(None);
|
Point::new(0.0, y as f32),
|
||||||
|
Size::new(size.width, row_height as f32),
|
||||||
|
)));
|
||||||
|
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
children.push(horizontal_rule(1).into());
|
children.push(horizontal_rule(1).into());
|
||||||
|
y += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let modified_text = match &item.metadata {
|
let modified_text = match &item.metadata {
|
||||||
|
|
@ -1360,6 +1367,7 @@ impl Tab {
|
||||||
.align_items(Alignment::Center)
|
.align_items(Alignment::Center)
|
||||||
.spacing(space_xxs),
|
.spacing(space_xxs),
|
||||||
)
|
)
|
||||||
|
.height(Length::Fixed(row_height as f32))
|
||||||
.padding(space_xxs)
|
.padding(space_xxs)
|
||||||
.style(button_style(item.selected, false))
|
.style(button_style(item.selected, false))
|
||||||
.on_press(Message::Click(Some(i)));
|
.on_press(Message::Click(Some(i)));
|
||||||
|
|
@ -1373,10 +1381,11 @@ impl Tab {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
count += 1;
|
count += 1;
|
||||||
|
y += row_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return self.empty_view(hidden > 0, core);
|
return self.empty_view(hidden > 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1390,11 +1399,11 @@ impl Tab {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&self, core: &Core, key_binds: &HashMap<KeyBind, Action>) -> Element<Message> {
|
pub fn view(&self, key_binds: &HashMap<KeyBind, Action>) -> Element<Message> {
|
||||||
let location_view = self.location_view(core);
|
let location_view = self.location_view();
|
||||||
let item_view = match self.view {
|
let item_view = match self.view {
|
||||||
View::Grid => self.grid_view(core),
|
View::Grid => self.grid_view(),
|
||||||
View::List => self.list_view(core),
|
View::List => self.list_view(),
|
||||||
};
|
};
|
||||||
let mut mouse_area =
|
let mut mouse_area =
|
||||||
mouse_area::MouseArea::new(widget::container(item_view).height(Length::Fill))
|
mouse_area::MouseArea::new(widget::container(item_view).height(Length::Fill))
|
||||||
|
|
@ -1428,72 +1437,88 @@ impl Tab {
|
||||||
//TODO: how many thumbnail loads should be in flight at once?
|
//TODO: how many thumbnail loads should be in flight at once?
|
||||||
let jobs = 8;
|
let jobs = 8;
|
||||||
let mut subscriptions = Vec::with_capacity(jobs);
|
let mut subscriptions = Vec::with_capacity(jobs);
|
||||||
|
|
||||||
|
let visible_rect = {
|
||||||
|
let point = match self.scroll_opt {
|
||||||
|
Some(viewport) => Point::new(0.0, viewport.absolute_offset().y),
|
||||||
|
None => Point::new(0.0, 0.0),
|
||||||
|
};
|
||||||
|
let size = self.size_opt.unwrap_or_else(|| Size::new(0.0, 0.0));
|
||||||
|
Rectangle::new(point, size)
|
||||||
|
};
|
||||||
|
|
||||||
|
//TODO: HACK to ensure positions are up to date since subscription runs before view
|
||||||
|
let _ = match self.view {
|
||||||
|
View::Grid => self.grid_view(),
|
||||||
|
View::List => self.list_view(),
|
||||||
|
};
|
||||||
|
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
match item.thumbnail_res_opt {
|
if item.thumbnail_res_opt.is_some() {
|
||||||
Some(_) => continue,
|
// Skip items that already have a thumbnail
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match item.rect_opt.get() {
|
||||||
|
Some(rect) => {
|
||||||
|
if !rect.intersects(&visible_rect) {
|
||||||
|
// Skip items that are not visible
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
let path = item.path.clone();
|
// Skip items with no determined rect (this should include hidden items)
|
||||||
subscriptions.push(subscription::channel(
|
continue;
|
||||||
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(64, 64);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(64, 64);
|
||||||
|
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 {
|
if subscriptions.len() >= jobs {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue