2023-04-19 20:02:01 +02:00
|
|
|
// Copyright (c) 2022-2023 Sophie Herold
|
|
|
|
|
// Copyright (c) 2022 Christopher Davis
|
2023-06-07 23:44:20 +02:00
|
|
|
// Copyright (c) 2023 Gage Berz
|
2022-01-16 19:03:13 -08:00
|
|
|
//
|
|
|
|
|
// 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
|
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
//
|
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
|
//
|
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
//
|
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
|
|
|
|
use crate::deps::*;
|
|
|
|
|
use crate::util;
|
2023-04-11 00:02:28 +02:00
|
|
|
use crate::util::gettext::*;
|
2022-01-16 19:03:13 -08:00
|
|
|
|
2022-11-04 19:28:48 +01:00
|
|
|
use anyhow::Context;
|
2023-01-23 01:10:15 +01:00
|
|
|
use gio::prelude::*;
|
|
|
|
|
use gio::subclass::prelude::*;
|
2023-04-12 01:11:59 +02:00
|
|
|
use glib::GString;
|
2023-04-12 04:13:30 +02:00
|
|
|
use indexmap::IndexMap;
|
2023-01-23 01:10:15 +01:00
|
|
|
use once_cell::sync::{Lazy, OnceCell};
|
2022-01-16 19:03:13 -08:00
|
|
|
use std::cell::RefCell;
|
|
|
|
|
|
|
|
|
|
mod imp {
|
|
|
|
|
use super::*;
|
2023-01-23 01:10:15 +01:00
|
|
|
use gtk::glib::subclass::Signal;
|
2022-01-16 19:03:13 -08:00
|
|
|
|
|
|
|
|
#[derive(Debug, Default)]
|
|
|
|
|
pub struct LpFileModel {
|
2023-04-12 04:13:30 +02:00
|
|
|
/// Use and IndexMap such that we can put the elements into an arbitrary order
|
|
|
|
|
/// while still having fast hash access via file URI.
|
2023-04-12 01:11:59 +02:00
|
|
|
pub(super) files: RefCell<IndexMap<GString, gio::File>>,
|
|
|
|
|
pub(super) directory: OnceCell<gio::File>,
|
2023-01-23 01:10:15 +01:00
|
|
|
/// Track file changes.
|
|
|
|
|
pub(super) monitor: OnceCell<gio::FileMonitor>,
|
2022-01-16 19:03:13 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[glib::object_subclass]
|
|
|
|
|
impl ObjectSubclass for LpFileModel {
|
|
|
|
|
const NAME: &'static str = "LpFileModel";
|
|
|
|
|
type Type = super::LpFileModel;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
impl ObjectImpl for LpFileModel {
|
|
|
|
|
fn signals() -> &'static [Signal] {
|
|
|
|
|
static SIGNALS: Lazy<Vec<Signal>> =
|
|
|
|
|
Lazy::new(|| vec![Signal::builder("changed").build()]);
|
|
|
|
|
SIGNALS.as_ref()
|
2022-01-16 19:03:13 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glib::wrapper! {
|
2023-01-23 01:10:15 +01:00
|
|
|
pub struct LpFileModel(ObjectSubclass<imp::LpFileModel>);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for LpFileModel {
|
|
|
|
|
fn default() -> Self {
|
2023-02-12 02:07:42 +01:00
|
|
|
glib::Object::new()
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
2022-01-16 19:03:13 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl LpFileModel {
|
2023-01-23 01:10:15 +01:00
|
|
|
/// Create with a single element
|
2023-04-18 01:52:06 +02:00
|
|
|
pub fn from_file(file: gio::File) -> Self {
|
|
|
|
|
Self::from_files(vec![file])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn from_files(files: Vec<gio::File>) -> Self {
|
2023-01-23 01:10:15 +01:00
|
|
|
let model = Self::default();
|
2022-01-16 19:03:13 -08:00
|
|
|
|
|
|
|
|
{
|
2023-01-23 01:10:15 +01:00
|
|
|
let mut vec = model.imp().files.borrow_mut();
|
2023-04-18 01:52:06 +02:00
|
|
|
for file in files {
|
|
|
|
|
vec.insert(file.uri(), file);
|
|
|
|
|
}
|
2022-11-26 00:02:56 +01:00
|
|
|
}
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
model
|
2022-11-26 00:02:56 +01:00
|
|
|
}
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
/// Load all file from the given directory
|
|
|
|
|
///
|
|
|
|
|
/// This can be used if there are already files present
|
2023-04-12 01:11:59 +02:00
|
|
|
pub async fn load_directory(&self, directory: gio::File) -> anyhow::Result<()> {
|
2023-01-23 01:10:15 +01:00
|
|
|
self.imp().directory.set(directory.clone()).unwrap();
|
2022-11-26 00:02:56 +01:00
|
|
|
|
2023-04-12 01:11:59 +02:00
|
|
|
let monitor = directory
|
2023-01-23 01:10:15 +01:00
|
|
|
.monitor_directory(gio::FileMonitorFlags::WATCH_MOVES, gio::Cancellable::NONE)?;
|
|
|
|
|
|
|
|
|
|
monitor.connect_changed(
|
|
|
|
|
glib::clone!(@weak self as obj => move |_monitor, file_a, file_b, event| {
|
|
|
|
|
obj.file_monitor_cb(event, file_a, file_b);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
self.imp().monitor.set(monitor).unwrap();
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-03-20 17:15:52 +01:00
|
|
|
let new_files_result = gio::spawn_blocking(move || {
|
2023-04-12 01:11:59 +02:00
|
|
|
let mut files = IndexMap::new();
|
2022-01-16 19:03:13 -08:00
|
|
|
|
2023-04-12 01:11:59 +02:00
|
|
|
let enumerator = directory
|
2022-01-16 19:03:13 -08:00
|
|
|
.enumerate_children(
|
|
|
|
|
&format!(
|
2023-06-07 23:47:58 +02:00
|
|
|
"{},{},{}",
|
2023-02-12 02:07:42 +01:00
|
|
|
gio::FILE_ATTRIBUTE_STANDARD_NAME,
|
2023-06-07 23:47:58 +02:00
|
|
|
gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
|
|
|
|
|
gio::FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
|
2022-01-16 19:03:13 -08:00
|
|
|
),
|
|
|
|
|
gio::FileQueryInfoFlags::NONE,
|
|
|
|
|
gio::Cancellable::NONE,
|
|
|
|
|
)
|
2023-04-11 00:02:28 +02:00
|
|
|
.context(gettext("Could not list other files in directory."))?;
|
2022-01-16 19:03:13 -08:00
|
|
|
|
|
|
|
|
enumerator.for_each(|info| {
|
|
|
|
|
if let Ok(info) = info {
|
|
|
|
|
if let Some(content_type) = info.content_type().map(|t| t.to_string()) {
|
2023-01-23 01:10:15 +01:00
|
|
|
// Filter out non-images; For now we support "all" image types.
|
2023-05-17 15:49:13 +00:00
|
|
|
if content_type.starts_with("image/") && !info.is_hidden() {
|
2023-04-12 01:11:59 +02:00
|
|
|
let file = directory.child(info.name());
|
|
|
|
|
files.insert(file.uri(), file);
|
2022-01-16 19:03:13 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
Ok::<_, anyhow::Error>(files)
|
2022-11-16 20:14:39 +01:00
|
|
|
})
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let Ok(new_files) = new_files_result else {
|
|
|
|
|
log::debug!("Thread listing directory canceled.");
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
let mut new_files = new_files?;
|
|
|
|
|
|
2022-11-16 20:14:39 +01:00
|
|
|
{
|
|
|
|
|
// Here we use a nested scope so that the mutable borrow only lasts as long as we need it
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
let mut files = self.imp().files.borrow_mut();
|
2023-04-12 01:11:59 +02:00
|
|
|
for (uri, file) in files.iter() {
|
|
|
|
|
new_files.insert(uri.clone(), file.clone());
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
|
|
|
|
*files = new_files;
|
2022-01-16 19:03:13 -08:00
|
|
|
// Then sort by name.
|
2023-01-23 01:10:15 +01:00
|
|
|
Self::sort(&mut files);
|
2022-01-16 19:03:13 -08:00
|
|
|
}
|
|
|
|
|
|
2022-11-26 00:02:56 +01:00
|
|
|
Ok(())
|
2022-01-16 19:03:13 -08:00
|
|
|
}
|
|
|
|
|
|
2023-04-12 01:11:59 +02:00
|
|
|
pub fn directory(&self) -> Option<gio::File> {
|
2022-01-16 19:03:13 -08:00
|
|
|
self.imp().directory.get().cloned()
|
|
|
|
|
}
|
2022-01-19 23:11:38 -08:00
|
|
|
|
2023-04-12 01:11:59 +02:00
|
|
|
pub fn index_of(&self, file: &gio::File) -> Option<usize> {
|
|
|
|
|
self.imp().files.borrow().get_index_of(&file.uri())
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn n_files(&self) -> usize {
|
|
|
|
|
self.imp().files.borrow().len()
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-12 01:11:59 +02:00
|
|
|
pub fn contains(&self, file: &gio::File) -> bool {
|
|
|
|
|
self.imp().files.borrow().contains_key(&file.uri())
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
|
|
|
|
|
2023-04-12 01:11:59 +02:00
|
|
|
pub fn before(&self, file: &gio::File) -> Option<gio::File> {
|
2023-04-12 04:13:30 +02:00
|
|
|
let index = self.index_of(file)?;
|
2023-01-23 01:10:15 +01:00
|
|
|
|
|
|
|
|
self.imp()
|
|
|
|
|
.files
|
|
|
|
|
.borrow()
|
|
|
|
|
.get_index(index.checked_sub(1)?)
|
2023-04-12 01:11:59 +02:00
|
|
|
.map(|x| x.1)
|
2023-01-23 01:10:15 +01:00
|
|
|
.cloned()
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-12 01:11:59 +02:00
|
|
|
pub fn after(&self, file: &gio::File) -> Option<gio::File> {
|
|
|
|
|
let index = self.index_of(file)?;
|
2023-01-23 01:10:15 +01:00
|
|
|
|
|
|
|
|
self.imp()
|
|
|
|
|
.files
|
|
|
|
|
.borrow()
|
|
|
|
|
.get_index(index.checked_add(1)?)
|
2023-04-12 01:11:59 +02:00
|
|
|
.map(|x| x.1)
|
2023-01-23 01:10:15 +01:00
|
|
|
.cloned()
|
2022-06-29 16:51:16 -04:00
|
|
|
}
|
|
|
|
|
|
2023-04-12 04:13:30 +02:00
|
|
|
/// Returns `n` elements before and after given file
|
2023-04-12 01:11:59 +02:00
|
|
|
pub fn files_around(&self, file: &gio::File, n: usize) -> IndexMap<GString, gio::File> {
|
|
|
|
|
let Some(index) = self.index_of(file) else {
|
|
|
|
|
log::error!("URI not in model: {}", file.uri());
|
|
|
|
|
return IndexMap::new();
|
2023-01-23 01:10:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let reduce = if index < n { n - index } else { 0 };
|
|
|
|
|
|
|
|
|
|
self.imp()
|
|
|
|
|
.files
|
|
|
|
|
.borrow()
|
|
|
|
|
.iter()
|
|
|
|
|
.skip(index.saturating_sub(n))
|
|
|
|
|
.take(2 * n + 1 - reduce)
|
2023-04-12 01:11:59 +02:00
|
|
|
.map(|(k, v)| (k.clone(), v.clone()))
|
2023-01-23 01:10:15 +01:00
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-12 04:13:30 +02:00
|
|
|
/// Return first file
|
2023-04-12 01:11:59 +02:00
|
|
|
pub fn first(&self) -> Option<gio::File> {
|
|
|
|
|
self.imp().files.borrow().first().map(|x| x.1).cloned()
|
2023-02-06 19:14:01 +01:00
|
|
|
}
|
|
|
|
|
|
2023-04-12 04:13:30 +02:00
|
|
|
/// Returns last file
|
2023-04-12 01:11:59 +02:00
|
|
|
pub fn last(&self) -> Option<gio::File> {
|
|
|
|
|
self.imp().files.borrow().last().map(|x| x.1).cloned()
|
2023-02-06 19:14:01 +01:00
|
|
|
}
|
|
|
|
|
|
2023-04-28 00:44:04 +02:00
|
|
|
pub fn remove(&self, file: &gio::File) -> Option<gio::File> {
|
|
|
|
|
self.imp().files.borrow_mut().remove(&file.uri())
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
/// Currently sorts by name
|
2023-04-12 01:11:59 +02:00
|
|
|
fn sort(files: &mut IndexMap<GString, gio::File>) {
|
|
|
|
|
files.sort_by(|_, x, _, y| util::compare_by_name(&x.uri(), &y.uri()));
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Determines if a file is added
|
|
|
|
|
fn is_image(info: &gio::FileInfo) -> bool {
|
|
|
|
|
info.content_type()
|
|
|
|
|
.map(|t| t.to_string())
|
|
|
|
|
.filter(|t| t.starts_with("image/"))
|
|
|
|
|
.is_some()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_image_file(file: &gio::File) -> bool {
|
2023-02-12 02:07:42 +01:00
|
|
|
let Ok(info) = util::query_attributes(file, vec![gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE]) else {
|
2023-04-12 04:13:30 +02:00
|
|
|
log::warn!("Could not query file info: {}", file.uri());
|
2023-01-23 01:10:15 +01:00
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Self::is_image(&info)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Signal that notifies about added/removed files
|
|
|
|
|
pub fn connect_changed(&self, f: impl Fn() + 'static) {
|
|
|
|
|
self.connect_local("changed", false, move |_| {
|
|
|
|
|
f();
|
|
|
|
|
None
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn file_monitor_cb(
|
|
|
|
|
&self,
|
|
|
|
|
event: gio::FileMonitorEvent,
|
|
|
|
|
file_a: &gio::File,
|
|
|
|
|
file_b: Option<&gio::File>,
|
|
|
|
|
) {
|
|
|
|
|
let changed = match event {
|
|
|
|
|
gio::FileMonitorEvent::Created
|
|
|
|
|
| gio::FileMonitorEvent::MovedIn
|
|
|
|
|
// Changing file content could theoretically make it an image
|
|
|
|
|
// by adding a magic byte
|
|
|
|
|
| gio::FileMonitorEvent::ChangesDoneHint
|
|
|
|
|
if Self::is_image_file(file_a) =>
|
|
|
|
|
{
|
|
|
|
|
let mut files = self.imp().files.borrow_mut();
|
2023-04-12 01:11:59 +02:00
|
|
|
let changed = files.insert(file_a.uri(),file_a.clone()).is_some();
|
2023-01-23 01:10:15 +01:00
|
|
|
if changed {
|
|
|
|
|
Self::sort(&mut files);
|
|
|
|
|
}
|
|
|
|
|
changed
|
|
|
|
|
}
|
|
|
|
|
gio::FileMonitorEvent::Deleted | gio::FileMonitorEvent::MovedOut | gio::FileMonitorEvent::Unmounted => {
|
|
|
|
|
let mut files = self.imp().files.borrow_mut();
|
2023-04-12 01:11:59 +02:00
|
|
|
let changed = files.remove(&file_a.uri()).is_some();
|
2023-01-23 01:10:15 +01:00
|
|
|
if changed {
|
|
|
|
|
Self::sort(&mut files);
|
|
|
|
|
}
|
|
|
|
|
changed
|
|
|
|
|
}
|
|
|
|
|
gio::FileMonitorEvent::Renamed => {
|
2023-04-12 01:11:59 +02:00
|
|
|
if let Some(file_b) = file_b {
|
2023-01-23 01:10:15 +01:00
|
|
|
let mut changed = false;
|
|
|
|
|
{
|
|
|
|
|
let mut files = self.imp().files.borrow_mut();
|
2023-04-12 01:11:59 +02:00
|
|
|
changed |= files.remove(&file_a.uri()).is_some();
|
2023-01-23 01:10:15 +01:00
|
|
|
if Self::is_image_file(file_b) {
|
2023-04-12 01:11:59 +02:00
|
|
|
changed |= files.insert(file_b.uri(), file_b.clone()).is_some();
|
2023-01-23 01:10:15 +01:00
|
|
|
if changed {
|
|
|
|
|
Self::sort(&mut files);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
changed
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if changed {
|
|
|
|
|
self.emit_by_name::<()>("changed", &[]);
|
|
|
|
|
}
|
2022-01-19 23:11:38 -08:00
|
|
|
}
|
2022-01-16 19:03:13 -08:00
|
|
|
}
|