2023-04-19 20:02:01 +02:00
|
|
|
// Copyright (c) 2021-2023 Christopher Davis
|
2025-12-19 00:06:29 +01:00
|
|
|
// Copyright (c) 2022-2025 Sophie Herold
|
2023-04-19 20:02:01 +02: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
|
|
|
|
|
|
2023-04-11 00:02:28 +02:00
|
|
|
pub mod gettext;
|
2024-12-18 17:12:17 +01:00
|
|
|
pub mod root;
|
2023-04-11 00:02:28 +02:00
|
|
|
|
2024-09-09 22:55:39 +02:00
|
|
|
use std::ffi::CStr;
|
2023-12-06 23:45:02 +01:00
|
|
|
use std::fmt::{Debug, Write};
|
|
|
|
|
use std::path::{Path, PathBuf};
|
2024-09-27 02:20:32 +02:00
|
|
|
use std::sync::LazyLock;
|
2021-12-21 16:09:48 -08:00
|
|
|
|
2025-12-04 06:14:49 +01:00
|
|
|
use anyhow::{Context, anyhow, bail};
|
2023-01-23 01:10:15 +01:00
|
|
|
use gio::prelude::*;
|
2022-07-14 15:25:03 -04:00
|
|
|
|
2023-12-06 23:45:02 +01:00
|
|
|
use crate::deps::*;
|
|
|
|
|
use crate::util::gettext::*;
|
2022-07-14 15:25:03 -04:00
|
|
|
|
2023-03-14 02:27:31 +01:00
|
|
|
/// 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>
|
2023-11-29 21:15:36 +01:00
|
|
|
// The default is already translated. Don't change if you are not sure what to
|
|
|
|
|
// use.
|
2023-04-11 00:02:28 +02:00
|
|
|
let datetime_format = gettext("%x %X");
|
2023-03-14 02:27:31 +01:00
|
|
|
|
2023-04-10 02:47:57 +02:00
|
|
|
let local_datetime = datetime.to_local();
|
|
|
|
|
|
|
|
|
|
let fmt = local_datetime
|
|
|
|
|
.as_ref()
|
|
|
|
|
.unwrap_or(datetime)
|
|
|
|
|
.format(&datetime_format);
|
2023-03-14 02:27:31 +01:00
|
|
|
|
|
|
|
|
if let Err(err) = &fmt {
|
2025-12-18 23:15:46 +01:00
|
|
|
tracing::error!("Could not format DateTime with '{datetime_format}': {err}");
|
2023-03-14 02:27:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.ok().map(|x| x.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-05 05:51:34 -07:00
|
|
|
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()?;
|
2021-09-05 05:51:34 -07:00
|
|
|
|
|
|
|
|
Some(info.display_name().to_string())
|
|
|
|
|
}
|
2021-12-27 15:03:31 -08:00
|
|
|
|
2022-07-14 15:25:03 -04:00
|
|
|
pub fn query_attributes(file: &gio::File, attributes: Vec<&str>) -> anyhow::Result<gio::FileInfo> {
|
2021-12-27 15:03:31 -08:00
|
|
|
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}")?;
|
2021-12-27 15:03:31 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file.query_info(
|
|
|
|
|
&attr_str,
|
|
|
|
|
gio::FileQueryInfoFlags::empty(),
|
|
|
|
|
gio::Cancellable::NONE,
|
|
|
|
|
)
|
2022-07-14 15:25:03 -04:00
|
|
|
.context("Failed to query string")
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-26 11:31:11 -04:00
|
|
|
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")?);
|
2022-07-26 11:31:11 -04:00
|
|
|
|
|
|
|
|
for attr in &attributes[1..] {
|
2023-02-12 01:19:47 +01:00
|
|
|
write!(attr_str, ",{attr}")?;
|
2022-07-26 11:31:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file.query_info_future(
|
|
|
|
|
&attr_str,
|
|
|
|
|
gio::FileQueryInfoFlags::empty(),
|
|
|
|
|
glib::Priority::default(),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.context("Failed to query attributes")
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-12 01:11:59 +02:00
|
|
|
pub fn compare_by_name(name_a: &glib::GString, name_b: &glib::GString) -> std::cmp::Ordering {
|
2023-01-22 14:21:38 -05:00
|
|
|
let key_a = glib::FilenameCollationKey::from(name_a);
|
|
|
|
|
let key_b = glib::FilenameCollationKey::from(name_b);
|
|
|
|
|
|
|
|
|
|
key_a.cmp(&key_b)
|
2021-12-27 15:03:31 -08:00
|
|
|
}
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2024-09-27 02:20:32 +02:00
|
|
|
static FILE_ATTRIBUTE_TRASH: LazyLock<String> = LazyLock::new(|| {
|
2023-01-31 21:56:44 +01:00
|
|
|
[
|
2023-02-12 02:07:42 +01:00
|
|
|
gio::FILE_ATTRIBUTE_STANDARD_NAME.as_str(),
|
|
|
|
|
gio::FILE_ATTRIBUTE_TRASH_ORIG_PATH.as_str(),
|
2023-01-31 21:56:44 +01:00
|
|
|
]
|
|
|
|
|
.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
|
|
|
|
|
{
|
2023-07-06 23:48:56 +02:00
|
|
|
let Some(file_info) = info.first() else {
|
|
|
|
|
break;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let Some(original_path) = file_info
|
|
|
|
|
.attribute_byte_string(gio::FILE_ATTRIBUTE_TRASH_ORIG_PATH)
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(PathBuf::from)
|
|
|
|
|
else {
|
|
|
|
|
break;
|
|
|
|
|
};
|
2023-01-31 21:56:44 +01:00
|
|
|
|
|
|
|
|
if original_path == path {
|
|
|
|
|
let trash_file = trash.child(file_info.name());
|
|
|
|
|
let original_file = gio::File::for_path(original_path);
|
2023-03-09 04:13:48 +01:00
|
|
|
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>"
|
2023-07-06 23:48:56 +02:00
|
|
|
let Some(path) = original_file.path() else {
|
|
|
|
|
bail!("File without path")
|
|
|
|
|
};
|
2023-03-09 04:13:48 +01:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-31 21:56:44 +01:00
|
|
|
error = trash_file
|
|
|
|
|
.move_future(
|
2023-03-09 04:13:48 +01:00
|
|
|
&target_file,
|
2023-01-31 21:56:44 +01:00
|
|
|
gio::FileCopyFlags::NOFOLLOW_SYMLINKS,
|
|
|
|
|
glib::Priority::default(),
|
|
|
|
|
)
|
|
|
|
|
.0
|
|
|
|
|
.await
|
2023-04-11 00:02:28 +02:00
|
|
|
.context(gettext("Failed to restore image from trash"));
|
2023-03-09 04:13:48 +01:00
|
|
|
|
2023-01-31 21:56:44 +01:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
error
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-06 19:14:01 +01:00
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
pub enum Position {
|
|
|
|
|
First,
|
|
|
|
|
Last,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
pub enum Direction {
|
|
|
|
|
Back,
|
|
|
|
|
Forward,
|
|
|
|
|
}
|
2023-03-17 01:21:45 +01:00
|
|
|
|
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
|
|
|
pub enum Gesture {
|
|
|
|
|
/// Rotate with threshold offset
|
|
|
|
|
Rotate(f64),
|
|
|
|
|
Scale,
|
|
|
|
|
}
|
2023-04-02 22:22:00 +02:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2024-09-09 22:55:39 +02:00
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
pub struct LocaleSettings {
|
|
|
|
|
pub decimal_point: Option<String>,
|
|
|
|
|
pub thousands_sep: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn locale_settings() -> LocaleSettings {
|
|
|
|
|
unsafe {
|
|
|
|
|
let lconv = libc::localeconv();
|
|
|
|
|
let mut locale_settings = LocaleSettings::default();
|
|
|
|
|
|
|
|
|
|
if lconv.is_null() {
|
|
|
|
|
return locale_settings;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let lconv = *lconv;
|
|
|
|
|
|
|
|
|
|
if !(lconv).decimal_point.is_null() {
|
|
|
|
|
let s = CStr::from_ptr(lconv.decimal_point);
|
|
|
|
|
locale_settings.decimal_point = s.to_str().map(|x| x.to_string()).ok();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !lconv.thousands_sep.is_null() {
|
|
|
|
|
let s = CStr::from_ptr(lconv.thousands_sep);
|
|
|
|
|
locale_settings.thousands_sep = s.to_str().map(|x| x.to_string()).ok();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
locale_settings
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-12-18 17:12:17 +01:00
|
|
|
|
|
|
|
|
pub enum ErrorType {
|
|
|
|
|
Loader,
|
|
|
|
|
General,
|
|
|
|
|
}
|
2026-07-09 22:40:39 +02:00
|
|
|
|
|
|
|
|
pub fn locale_uses_inch() -> bool {
|
|
|
|
|
// Use inch for USA and Liberia
|
|
|
|
|
if let Some(unit_locale) = getlocale(gettextrs::LocaleCategory::LcMeasurement)
|
|
|
|
|
&& let Some(locale) = unit_locale
|
|
|
|
|
.split(|x| *x == b'_')
|
|
|
|
|
.nth(1)
|
|
|
|
|
.and_then(|x| x.get(0..2))
|
|
|
|
|
&& (locale == b"US" || locale == b"LR")
|
|
|
|
|
{
|
2026-07-07 13:52:08 +02:00
|
|
|
true
|
2026-07-09 22:40:39 +02:00
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Upstream to gettext if possible
|
|
|
|
|
fn getlocale(category: gettextrs::LocaleCategory) -> Option<Vec<u8>> {
|
|
|
|
|
unsafe {
|
|
|
|
|
let ret = gettext_sys::setlocale(category as i32, std::ptr::null());
|
|
|
|
|
if ret.is_null() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(std::ffi::CStr::from_ptr(ret).to_bytes().to_owned())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|