fileModel: Make file info queries async

We are not entirely sure if looking up the display name might do IO for
google drives.
This commit is contained in:
Sophie Herold 2024-11-13 01:07:08 +01:00
parent 400c7ac2c4
commit 9156533fea
3 changed files with 93 additions and 92 deletions

View file

@ -0,0 +1 @@
The sort order is now also following the one found in Files (Nautilus) if the file name contains non-ascii characters.

View file

@ -1,8 +1,6 @@
// Copyright (c) 2022-2024 Sophie Herold // Copyright (c) 2022-2024 Sophie Herold
// Copyright (c) 2022 Christopher Davis // Copyright (c) 2022 Christopher Davis
// Copyright (c) 2023 Julian Hofer
// Copyright (c) 2023 Gage Berz // Copyright (c) 2023 Gage Berz
// Copyright (c) 2024 Not Committed Yet
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by // it under the terms of the GNU General Public License as published by
@ -36,27 +34,25 @@ use crate::util::gettext::*;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Entry { struct Entry {
/// Determines the sort order
sort: VecDeque<glib::FilenameCollationKey>, sort: VecDeque<glib::FilenameCollationKey>,
file: gio::File, file: gio::File,
} }
impl Entry { impl Entry {
//async async fn new(file: gio::File) -> Self {
fn new(file: gio::File) -> Self {
let mut sort = VecDeque::new(); let mut sort = VecDeque::new();
let mut frac_file = file.clone(); let mut frac_file = file.clone();
// Determine sort order for all path/uri segments one by one
loop { loop {
match frac_file match frac_file
.query_info .query_info_future(
//_future
(
gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
gio::FileQueryInfoFlags::NONE, gio::FileQueryInfoFlags::NONE,
//glib::Priority::LOW, glib::Priority::LOW,
gio::Cancellable::NONE
) )
//.await .await
{ {
Ok(parent) => { Ok(parent) => {
let key = glib::FilenameCollationKey::from(parent.display_name()); let key = glib::FilenameCollationKey::from(parent.display_name());
@ -123,20 +119,20 @@ impl Default for LpFileModel {
impl LpFileModel { impl LpFileModel {
/// Create with a single element /// Create with a single element
pub fn from_file(file: gio::File) -> Self { pub async fn from_file(file: gio::File) -> Self {
Self::from_files(vec![file]) Self::from_files(vec![file]).await
} }
pub fn from_files(files: Vec<gio::File>) -> Self { pub async fn from_files(files: Vec<gio::File>) -> Self {
let model = Self::default(); let model = Self::default();
{ let mut stored_files: IndexMap<GString, Entry> = IndexMap::new();
let mut vec = model.imp().files.borrow_mut(); for file in files {
for file in files { stored_files.insert(file.uri(), Entry::new(file).await);
vec.insert(file.uri(), Entry::new(file));
}
} }
*model.imp().files.borrow_mut() = stored_files;
model model
} }
@ -164,25 +160,29 @@ impl LpFileModel {
} }
} }
let new_files_result = gio::spawn_blocking(move || { let mut new_files = IndexMap::new();
let mut files = IndexMap::new();
let enumerator = directory let enumerator = directory
.enumerate_children( .enumerate_children_future(
&format!( &format!(
"{},{},{},{}", "{},{},{},{}",
gio::FILE_ATTRIBUTE_STANDARD_NAME, gio::FILE_ATTRIBUTE_STANDARD_NAME,
gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
gio::FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE, gio::FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE,
gio::FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, gio::FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
), ),
gio::FileQueryInfoFlags::NONE, gio::FileQueryInfoFlags::NONE,
gio::Cancellable::NONE, glib::Priority::LOW,
) )
.context(gettext("Could not list other files in directory."))?; .await
.context(gettext("Could not list other files in directory."))?;
enumerator.for_each(|info| { for info in enumerator {
if let Ok(info) = info { match info {
Err(err) => {
log::warn!("Unreadable entry in directory: {err}");
}
Ok(info) => {
// GVfs smb does not provide a CONTENT_TYPE if the content type is ambiguous. // GVfs smb does not provide a CONTENT_TYPE if the content type is ambiguous.
// This happens for png/apng. Since we only need to know if something is // This happens for png/apng. Since we only need to know if something is
// probably an image, we can use the FAST_CONTENT_TYPE in these cases. // probably an image, we can use the FAST_CONTENT_TYPE in these cases.
@ -201,22 +201,12 @@ impl LpFileModel {
// images. Usually by inspecting the magic bytes. // images. Usually by inspecting the magic bytes.
if content_type.starts_with("image/") && !info.is_hidden() { if content_type.starts_with("image/") && !info.is_hidden() {
let file = directory.child(info.name()); let file = directory.child(info.name());
files.insert(file.uri(), Entry::new(file)); new_files.insert(file.uri(), Entry::new(file).await);
} }
} }
} }
}); }
}
Ok::<_, anyhow::Error>(files)
})
.await;
let Ok(new_files) = new_files_result else {
log::debug!("Thread listing directory canceled.");
return Ok(());
};
let mut new_files = new_files?;
{ {
// Here we use a nested scope so that the mutable borrow only lasts as long as // Here we use a nested scope so that the mutable borrow only lasts as long as
@ -346,13 +336,32 @@ impl LpFileModel {
}); });
} }
/// Insert a file and signal changes
///
/// Setting `changed` to true will always trigger a `notify::changed``
/// notification. Otherwise, a change signal is only sent if no file with
/// the same URI existed before.
pub fn insert(&self, file: gio::File, mut changed: bool) {
let obj = self.clone();
glib::spawn_future_local(async move {
let entry = Entry::new(file.clone()).await;
let mut files = obj.imp().files.borrow_mut();
changed |= files.insert(file.uri(), entry).is_none();
if changed {
Self::sort(&mut files);
drop(files);
obj.emit_by_name::<()>("changed", &[]);
}
});
}
fn file_monitor_cb( fn file_monitor_cb(
&self, &self,
event: gio::FileMonitorEvent, event: gio::FileMonitorEvent,
file_a: &gio::File, file_a: &gio::File,
file_b: Option<&gio::File>, file_b: Option<&gio::File>,
) { ) {
let changed = match event { match event {
gio::FileMonitorEvent::Created gio::FileMonitorEvent::Created
| gio::FileMonitorEvent::MovedIn | gio::FileMonitorEvent::MovedIn
// Changing file content could theoretically make it an image // Changing file content could theoretically make it an image
@ -360,40 +369,25 @@ impl LpFileModel {
| gio::FileMonitorEvent::ChangesDoneHint | gio::FileMonitorEvent::ChangesDoneHint
if Self::is_image_file(file_a) => if Self::is_image_file(file_a) =>
{ {
let mut files = self.imp().files.borrow_mut(); self.insert(file_a.clone(), false);
let changed = files.insert(file_a.uri(),Entry::new(file_a.clone())).is_none();
if changed {
Self::sort(&mut files);
}
changed
} }
gio::FileMonitorEvent::Deleted | gio::FileMonitorEvent::MovedOut | gio::FileMonitorEvent::Unmounted => { gio::FileMonitorEvent::Deleted | gio::FileMonitorEvent::MovedOut | gio::FileMonitorEvent::Unmounted => {
let mut files = self.imp().files.borrow_mut(); let removed = self.imp().files.borrow_mut().shift_remove(&file_a.uri()).is_some();
files.shift_remove(&file_a.uri()).is_some() if removed {
self.emit_by_name::<()>("changed", &[]);
}
} }
gio::FileMonitorEvent::Renamed => { gio::FileMonitorEvent::Renamed => {
if let Some(file_b) = file_b { if let Some(file_b) = file_b {
let mut changed = false;
{ {
let mut files = self.imp().files.borrow_mut(); let changed = self.imp().files.borrow_mut().shift_remove(&file_a.uri()).is_some();
changed |= files.shift_remove(&file_a.uri()).is_some();
if Self::is_image_file(file_b) { if Self::is_image_file(file_b) {
changed |= files.insert(file_b.uri(), Entry::new(file_b.clone())).is_none(); self.insert(file_b.clone(), changed);
if changed {
Self::sort(&mut files);
}
} }
} }
changed
} else {
false
} }
} }
_ => false, _ => {},
};
if changed {
self.emit_by_name::<()>("changed", &[]);
} }
} }
} }

View file

@ -439,16 +439,22 @@ impl LpImageView {
} }
fn load_files(&self, files: Vec<gio::File>) { fn load_files(&self, files: Vec<gio::File>) {
let sliding_view = self.sliding_view().editor();
if let Some(first) = files.first().cloned() { if let Some(first) = files.first().cloned() {
let model = LpFileModel::from_files(files); glib::spawn_future_local(glib::clone!(
self.set_model(model); #[strong(rename_to=obj)]
let page = self.new_image_page(&first); self,
async move {
let sliding_view = obj.sliding_view().editor();
sliding_view.clear_lazy(); let model = LpFileModel::from_files(files).await;
sliding_view.append(&page); obj.set_model(model);
self.update_sliding_view(&first); let page = obj.new_image_page(&first);
sliding_view.clear_lazy();
sliding_view.append(&page);
obj.update_sliding_view(&first);
}
));
} else { } else {
log::error!("File list was empty"); log::error!("File list was empty");
} }
@ -474,18 +480,18 @@ impl LpImageView {
return; return;
} }
let model = LpFileModel::from_file(file.clone()); glib::spawn_future_local(glib::clone!(
self.set_model(model); #[strong(rename_to = obj)]
log::debug!("New model created"); self,
async move {
let model = LpFileModel::from_file(file.clone()).await;
obj.set_model(model);
log::debug!("New model created");
self.update_sliding_view(&file); obj.update_sliding_view(&file);
// List other files in directory // List other files in directory
if let Some(directory) = directory { if let Some(directory) = directory {
glib::spawn_future_local(glib::clone!(
#[weak(rename_to = obj)]
self,
async move {
if let Err(err) = obj.model().load_directory(directory.clone()).await { if let Err(err) = obj.model().load_directory(directory.clone()).await {
log::warn!("Failed to load directory: {}", err.root_cause()); log::warn!("Failed to load directory: {}", err.root_cause());
obj.activate_action( obj.activate_action(
@ -504,8 +510,8 @@ impl LpImageView {
obj.update_sliding_view(&file); obj.update_sliding_view(&file);
} }
)); }
} ));
} }
/// Move forward or backwards /// Move forward or backwards