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},
|
||||
window, Alignment, Event, Length,
|
||||
},
|
||||
style,
|
||||
style, theme,
|
||||
widget::{self, segmented_button},
|
||||
Application, ApplicationExt, Element,
|
||||
};
|
||||
|
|
@ -352,7 +352,7 @@ impl App {
|
|||
}
|
||||
|
||||
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 hash = env!("VERGEN_GIT_SHA");
|
||||
let short_hash: String = hash.chars().take(7).collect();
|
||||
|
|
@ -431,7 +431,7 @@ impl App {
|
|||
if let Some(ref items) = tab.items_opt {
|
||||
for item in items.iter() {
|
||||
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
|
||||
// preview images on thousands of files
|
||||
break;
|
||||
|
|
@ -1146,7 +1146,7 @@ impl Application for App {
|
|||
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 {
|
||||
DialogPage::FailedOperation(id) => {
|
||||
|
|
@ -1320,7 +1320,7 @@ impl Application for App {
|
|||
|
||||
/// Creates a view after each update.
|
||||
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);
|
||||
|
||||
|
|
@ -1342,7 +1342,7 @@ impl Application for App {
|
|||
match self.tab_model.data::<Tab>(entity) {
|
||||
Some(tab) => {
|
||||
let tab_view = tab
|
||||
.view(self.core(), &self.key_binds)
|
||||
.view(&self.key_binds)
|
||||
.map(move |message| Message::TabMessage(Some(entity), message));
|
||||
tab_column = tab_column.push(tab_view);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use cosmic::{
|
|||
subscription::{self, Subscription},
|
||||
window, Event, Length, Size,
|
||||
},
|
||||
theme,
|
||||
widget::{self, segmented_button},
|
||||
Application, ApplicationExt, Element,
|
||||
};
|
||||
|
|
@ -616,13 +617,13 @@ impl Application for App {
|
|||
|
||||
/// Creates a view after each update.
|
||||
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);
|
||||
tab_column = tab_column.push(
|
||||
//TODO: key binds for dialog
|
||||
self.tab
|
||||
.view(self.core(), &HashMap::new())
|
||||
.view(&HashMap::new())
|
||||
.map(move |message| Message::TabMessage(message)),
|
||||
);
|
||||
|
||||
|
|
|
|||
197
src/tab.rs
197
src/tab.rs
|
|
@ -1,5 +1,4 @@
|
|||
use cosmic::{
|
||||
app::Core,
|
||||
cosmic_theme,
|
||||
iced::{
|
||||
alignment::{Horizontal, Vertical},
|
||||
|
|
@ -444,8 +443,8 @@ pub struct Item {
|
|||
}
|
||||
|
||||
impl Item {
|
||||
pub fn property_view(&self, core: &Core, sizes: IconSizes) -> Element<crate::app::Message> {
|
||||
let cosmic_theme::Spacing { space_xxxs, .. } = core.system_theme().cosmic().spacing;
|
||||
pub fn property_view(&self, sizes: IconSizes) -> Element<crate::app::Message> {
|
||||
let cosmic_theme::Spacing { space_xxxs, .. } = theme::active().cosmic().spacing;
|
||||
|
||||
let mut column = widget::column().spacing(space_xxxs);
|
||||
|
||||
|
|
@ -874,7 +873,7 @@ impl Tab {
|
|||
commands
|
||||
}
|
||||
|
||||
fn column_sort(&self) -> Option<Vec<Item>> {
|
||||
fn column_sort(&self) -> Option<Vec<&Item>> {
|
||||
let check_reverse = |ord: Ordering, sort: bool| {
|
||||
if sort {
|
||||
ord
|
||||
|
|
@ -882,11 +881,11 @@ impl Tab {
|
|||
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;
|
||||
match self.sort_name {
|
||||
HeadingOptions::Size => {
|
||||
item.sort_by(|a, b| {
|
||||
items.sort_by(|a, b| {
|
||||
// entries take precedence over size
|
||||
let get_size = |x: &Item| match &x.metadata {
|
||||
ItemMetadata::Path { metadata, children } => {
|
||||
|
|
@ -912,7 +911,7 @@ impl Tab {
|
|||
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()) {
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
|
|
@ -921,7 +920,7 @@ impl Tab {
|
|||
check_reverse(ord, heading_sort)
|
||||
}),
|
||||
HeadingOptions::Modified => {
|
||||
item.sort_by(|a, b| {
|
||||
items.sort_by(|a, b| {
|
||||
let get_modified = |x: &Item| match &x.metadata {
|
||||
ItemMetadata::Path { metadata, .. } => metadata.modified().ok(),
|
||||
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 {
|
||||
space_xxxs,
|
||||
space_xxs,
|
||||
space_s,
|
||||
..
|
||||
} = core.system_theme().cosmic().spacing;
|
||||
} = theme::active().cosmic().spacing;
|
||||
|
||||
let mut row = widget::row::with_capacity(5).align_items(Alignment::Center);
|
||||
|
||||
|
|
@ -1080,8 +1079,8 @@ impl Tab {
|
|||
row.into()
|
||||
}
|
||||
|
||||
pub fn empty_view(&self, has_hidden: bool, core: &Core) -> Element<Message> {
|
||||
let cosmic_theme::Spacing { space_xxs, .. } = core.system_theme().cosmic().spacing;
|
||||
pub fn empty_view(&self, has_hidden: bool) -> Element<Message> {
|
||||
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||
|
||||
widget::column::with_children(vec![widget::container(
|
||||
widget::column::with_children(vec![
|
||||
|
|
@ -1107,13 +1106,13 @@ impl Tab {
|
|||
.into()
|
||||
}
|
||||
|
||||
pub fn grid_view(&self, core: &Core) -> Element<Message> {
|
||||
pub fn grid_view(&self) -> Element<Message> {
|
||||
let cosmic_theme::Spacing {
|
||||
space_xs,
|
||||
space_xxs,
|
||||
space_xxxs,
|
||||
..
|
||||
} = core.system_theme().cosmic().spacing;
|
||||
} = theme::active().cosmic().spacing;
|
||||
|
||||
let TabConfig {
|
||||
show_hidden,
|
||||
|
|
@ -1214,7 +1213,7 @@ impl Tab {
|
|||
}
|
||||
|
||||
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
|
||||
|
|
@ -1250,10 +1249,12 @@ impl Tab {
|
|||
.into()
|
||||
}
|
||||
|
||||
pub fn list_view(&self, core: &Core) -> Element<Message> {
|
||||
let cosmic_theme::Spacing { space_xxs, .. } = core.system_theme().cosmic().spacing;
|
||||
pub fn list_view(&self) -> Element<Message> {
|
||||
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
|
||||
|
||||
//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 size_width = Length::Fixed(100.0);
|
||||
|
||||
|
|
@ -1288,12 +1289,15 @@ impl Tab {
|
|||
heading_item!("size", size_width, HeadingOptions::Size),
|
||||
])
|
||||
.align_items(Alignment::Center)
|
||||
.height(Length::Fixed(row_height as f32))
|
||||
.padding(space_xxs)
|
||||
.spacing(space_xxs)
|
||||
.into(),
|
||||
);
|
||||
let mut y = row_height;
|
||||
|
||||
children.push(horizontal_rule(1).into());
|
||||
y += 1;
|
||||
|
||||
if let Some(ref items) = self.column_sort() {
|
||||
let mut count = 0;
|
||||
|
|
@ -1308,11 +1312,14 @@ impl Tab {
|
|||
hidden += 1;
|
||||
continue;
|
||||
}
|
||||
//TODO: correct rectangle
|
||||
item.rect_opt.set(None);
|
||||
item.rect_opt.set(Some(Rectangle::new(
|
||||
Point::new(0.0, y as f32),
|
||||
Size::new(size.width, row_height as f32),
|
||||
)));
|
||||
|
||||
if count > 0 {
|
||||
children.push(horizontal_rule(1).into());
|
||||
y += 1;
|
||||
}
|
||||
|
||||
let modified_text = match &item.metadata {
|
||||
|
|
@ -1360,6 +1367,7 @@ impl Tab {
|
|||
.align_items(Alignment::Center)
|
||||
.spacing(space_xxs),
|
||||
)
|
||||
.height(Length::Fixed(row_height as f32))
|
||||
.padding(space_xxs)
|
||||
.style(button_style(item.selected, false))
|
||||
.on_press(Message::Click(Some(i)));
|
||||
|
|
@ -1373,10 +1381,11 @@ impl Tab {
|
|||
);
|
||||
}
|
||||
count += 1;
|
||||
y += row_height;
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return self.empty_view(hidden > 0, core);
|
||||
return self.empty_view(hidden > 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1390,11 +1399,11 @@ impl Tab {
|
|||
.into()
|
||||
}
|
||||
|
||||
pub fn view(&self, core: &Core, key_binds: &HashMap<KeyBind, Action>) -> Element<Message> {
|
||||
let location_view = self.location_view(core);
|
||||
pub fn view(&self, key_binds: &HashMap<KeyBind, Action>) -> Element<Message> {
|
||||
let location_view = self.location_view();
|
||||
let item_view = match self.view {
|
||||
View::Grid => self.grid_view(core),
|
||||
View::List => self.list_view(core),
|
||||
View::Grid => self.grid_view(),
|
||||
View::List => self.list_view(),
|
||||
};
|
||||
let mut mouse_area =
|
||||
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?
|
||||
let jobs = 8;
|
||||
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() {
|
||||
match item.thumbnail_res_opt {
|
||||
Some(_) => continue,
|
||||
if item.thumbnail_res_opt.is_some() {
|
||||
// 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 => {
|
||||
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;
|
||||
}
|
||||
},
|
||||
));
|
||||
// Skip items with no determined rect (this should include hidden items)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue