Loupe/src/util/mod.rs

231 lines
6.5 KiB
Rust
Raw Normal View History

pub mod gettext;
use crate::deps::*;
use crate::util::gettext::*;
use anyhow::{anyhow, bail, Context};
use gio::prelude::*;
use once_cell::sync::Lazy;
use std::fmt::{Debug, Write};
use std::path::{Path, PathBuf};
/// Returns localized date + time format
pub fn datetime_fmt(datetime: &glib::DateTime) -> Option<String> {
// Translators: This is the date and time format we use in metadata output etc.
// The format has to follow <https://docs.gtk.org/glib/method.DateTime.format.html>
// The default is already translated. Don't change if you are not sure what to use.
let datetime_format = gettext("%x %X");
let local_datetime = datetime.to_local();
let fmt = local_datetime
.as_ref()
.unwrap_or(datetime)
.format(&datetime_format);
if let Err(err) = &fmt {
log::error!("Could not format DateTime with '{datetime_format}': {err}");
}
fmt.ok().map(|x| x.to_string())
}
pub fn get_file_display_name(file: &gio::File) -> Option<String> {
2023-02-12 02:07:42 +01:00
let info = query_attributes(file, vec![gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME]).ok()?;
Some(info.display_name().to_string())
}
pub fn query_attributes(file: &gio::File, attributes: Vec<&str>) -> anyhow::Result<gio::FileInfo> {
let mut attributes = attributes;
let mut attr_str = String::from(attributes.remove(0));
for attr in attributes {
2023-02-12 01:19:47 +01:00
write!(attr_str, ",{attr}")?;
}
file.query_info(
&attr_str,
gio::FileQueryInfoFlags::empty(),
gio::Cancellable::NONE,
)
.context("Failed to query string")
}
pub async fn query_attributes_future(
file: &gio::File,
attributes: Vec<&str>,
) -> anyhow::Result<gio::FileInfo> {
2022-11-04 19:27:43 +01:00
let mut attr_str = String::from(*attributes.first().context("No attributes")?);
for attr in &attributes[1..] {
2023-02-12 01:19:47 +01:00
write!(attr_str, ",{attr}")?;
}
file.query_info_future(
&attr_str,
gio::FileQueryInfoFlags::empty(),
glib::Priority::default(),
)
.await
.context("Failed to query attributes")
}
pub fn compare_by_name(name_a: &glib::GString, name_b: &glib::GString) -> std::cmp::Ordering {
let key_a = glib::FilenameCollationKey::from(name_a);
let key_b = glib::FilenameCollationKey::from(name_b);
key_a.cmp(&key_b)
}
static FILE_ATTRIBUTE_TRASH: Lazy<String> = Lazy::new(|| {
[
2023-02-12 02:07:42 +01:00
gio::FILE_ATTRIBUTE_STANDARD_NAME.as_str(),
gio::FILE_ATTRIBUTE_TRASH_ORIG_PATH.as_str(),
]
.join(",")
});
/// Recover file from trash
///
/// This is based on Nautilus' implementation
/// <https://gitlab.gnome.org/GNOME/glib/-/issues/845>
pub async fn untrash(path: &Path) -> anyhow::Result<()> {
let trash = gio::File::for_uri("trash:///");
let enumerator = trash
.enumerate_children_future(
&FILE_ATTRIBUTE_TRASH,
gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS,
glib::Priority::default(),
)
.await?;
let mut error = Err(anyhow!("Image not found in trash"));
while let Ok(info) = enumerator
.next_files_future(1, glib::Priority::default())
.await
{
let Some(file_info) = info.first()
else { break; };
2023-02-12 02:07:42 +01:00
let Some(original_path) = file_info.attribute_byte_string(gio::FILE_ATTRIBUTE_TRASH_ORIG_PATH).as_ref().map(PathBuf::from)
else { break; };
if original_path == path {
let trash_file = trash.child(file_info.name());
let original_file = gio::File::for_path(original_path);
let mut target_file = original_file.clone();
// Find available filename if original is used
for i in 1.. {
if !target_file.query_exists(gio::Cancellable::NONE) {
break;
}
// Construct new name of the form "<filename> (i).<ext>"
let Some(path) = original_file.path() else { bail!("File without path") };
let mut name = path
.file_stem()
.map(|x| x.to_os_string())
.unwrap_or_default();
name.push(format!(" ({i})"));
// Construct new path
let mut new_path = path.clone();
new_path.set_file_name(name);
if let Some(ext) = path.extension() {
new_path.set_extension(ext);
}
target_file = gio::File::for_path(new_path);
}
error = trash_file
.move_future(
&target_file,
gio::FileCopyFlags::NOFOLLOW_SYMLINKS,
glib::Priority::default(),
)
.0
.await
.context(gettext("Failed to restore image from trash"));
break;
}
}
error
}
pub trait BufReadSeek: std::io::BufRead + std::io::Seek {}
impl BufReadSeek for std::io::BufReader<gio::InputStreamRead<gio::FileInputStream>> {}
pub trait ToBufRead {
fn to_buf_read(&self) -> anyhow::Result<Box<dyn BufReadSeek>>;
}
impl ToBufRead for gio::File {
fn to_buf_read(&self) -> anyhow::Result<Box<dyn BufReadSeek>> {
let read = self
.read(gio::Cancellable::NONE)
.context(gettext("Failed to open image"))?
.into_read();
Ok(Box::new(std::io::BufReader::new(read)))
}
}
#[derive(Debug, Clone, Copy)]
pub enum Position {
First,
Last,
}
#[derive(Debug, Clone, Copy)]
pub enum Direction {
Back,
Forward,
}
#[derive(Copy, Clone, Debug)]
pub enum Gesture {
/// Rotate with threshold offset
Rotate(f64),
Scale,
}
fn srgb_linear(v: f32) -> f32 {
if v <= 0.04045 {
v / 12.92
} else {
((v + 0.055) / 1.055).powf(2.4)
}
}
/// Relative luminance <https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html#dfn-relative-luminance>
fn relative_luminance(color: &gdk::RGBA) -> f32 {
0.2126 * srgb_linear(color.red())
+ 0.7152 * srgb_linear(color.green())
+ 0.0722 * srgb_linear(color.blue())
}
/// Contrast ratio <https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html#dfn-contrast-ratio>
pub fn contrast_ratio(color1: &gdk::RGBA, color2: &gdk::RGBA) -> f32 {
let la = relative_luminance(color1);
let lb = relative_luminance(color2);
let l1 = f32::max(la, lb);
let l2 = f32::min(la, lb);
(l1 + 0.05) / (l2 + 0.05)
}
pub fn spawn<R: 'static, F: std::future::Future<Output = R> + 'static>(
f: F,
) -> glib::JoinHandle<R> {
let ctx = glib::MainContext::default();
ctx.spawn_local(f)
}