waytrogen 0.6.12

A GTK graphical user interface for changing your wallpapers on Wayland based compositors.
use gtk::{glib::SignalHandlerId, Picture};
use image::ImageReader;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{
    cell::RefCell,
    fmt::Display,
    io::Cursor,
    path::{Path, PathBuf},
    process::Command,
    str::FromStr,
    time::UNIX_EPOCH,
};

use crate::wallpaper_changers::WallpaperChangers;
use gettextrs::gettext;

pub const THUMBNAIL_HEIGHT: i32 = 200;
pub const THUMBNAIL_WIDTH: i32 = THUMBNAIL_HEIGHT;
pub const APP_ID: &str = "org.Waytrogen.Waytrogen";
pub const GETTEXT_DOMAIN: &str = "waytrogen";

pub struct GtkPictureFile {
    pub picture: Picture,
    pub chache_image_file: CacheImageFile,
    pub button_signal_handler: RefCell<Option<SignalHandlerId>>,
}

#[derive(Clone, Default, PartialEq)]
pub struct CacheImageFile {
    pub image: Vec<u8>,
    pub name: String,
    pub date: u64,
    pub path: String,
}

impl CacheImageFile {
    pub fn from_file(path: &Path) -> anyhow::Result<CacheImageFile> {
        let image = Self::generate_thumbnail(path)?;
        Self::create_gtk_image(path, image)
    }

    fn get_metadata(path: &Path) -> anyhow::Result<(String, String, u64)> {
        let path = path.to_path_buf();
        let name = path.file_name().unwrap().to_str().unwrap().to_owned();
        let date = std::fs::File::open(path.clone())?.metadata()?.modified()?;
        let date = date.duration_since(UNIX_EPOCH)?.as_secs();
        Ok((path.to_str().unwrap().to_string(), name, date))
    }

    fn create_gtk_image(path: &Path, image: Vec<u8>) -> anyhow::Result<CacheImageFile> {
        let fields = Self::get_metadata(path)?;
        let image_file = CacheImageFile {
            image,
            path: fields.0,
            name: fields.1,
            date: fields.2,
        };
        Ok(image_file)
    }

    fn generate_thumbnail(path: &Path) -> anyhow::Result<Vec<u8>> {
        if let Ok(i) = Self::try_create_thumbnail_with_image(path) {
            return Ok(i);
        }
        if let Ok(i) = Self::try_create_thumbnail_with_ffmpeg(path) {
            return Ok(i);
        }
        Err(anyhow::anyhow!(
            "{}: {}",
            gettext("Failed to create thumbnail for"),
            path.as_os_str().to_str().unwrap_or_default()
        ))
    }
    fn try_create_thumbnail_with_ffmpeg(path: &Path) -> anyhow::Result<Vec<u8>> {
        let temp_dir = String::from_utf8(Command::new("mktemp").arg("-d").output()?.stdout)?;
        let output_path = PathBuf::from(temp_dir).join("temp.png");
        let code = Command::new("ffmpeg")
            .arg("-i")
            .arg(path)
            .arg("-y")
            .arg("-ss")
            .arg("00:00:00")
            .arg("-frames:v")
            .arg("1")
            .arg(output_path.clone())
            .spawn()?
            .wait()?
            .code()
            .unwrap_or(255);
        match code {
            0 => Self::try_create_thumbnail_with_image(&output_path),
            _ => Err(anyhow::anyhow!(gettext(
                "Thumbnail could not be generated using ffmpg."
            ))),
        }
    }

    fn try_create_thumbnail_with_image(path: &Path) -> anyhow::Result<Vec<u8>> {
        let thumbnail = ImageReader::open(path)?
            .with_guessed_format()?
            .decode()?
            .thumbnail(THUMBNAIL_WIDTH as u32, THUMBNAIL_HEIGHT as u32)
            .to_rgb8();
        let mut buff: Vec<u8> = vec![];
        thumbnail.write_to(&mut Cursor::new(&mut buff), image::ImageFormat::Png)?;
        Ok(buff)
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
pub struct RGB {
    pub red: f32,
    pub green: f32,
    pub blue: f32,
}

impl Display for RGB {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{:02x}{:02x}{:02x}",
            (self.red * 255.0) as u8,
            (self.green * 255.0) as u8,
            (self.blue * 255.0) as u8
        )
    }
}

lazy_static! {
    static ref rgb_regex: Regex = Regex::new(r"[0-9A-Fa-f]{6}").unwrap();
}

impl FromStr for RGB {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if rgb_regex.is_match(s) {
            let s = s.to_lowercase().chars().collect::<Vec<_>>();
            let red = hex::decode(s[0..=1].iter().collect::<String>()).unwrap();
            let red = f32::from(red[0]) / 255.0;
            let green = hex::decode(s[2..=3].iter().collect::<String>()).unwrap();
            let green = f32::from(green[0]) / 255.0;
            let blue = hex::decode(s[4..=5].iter().collect::<String>()).unwrap();
            let blue = f32::from(blue[0]) / 255.0;
            Ok(Self { red, green, blue })
        } else {
            Err(gettext("Invalid string"))
        }
    }
}

#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Wallpaper {
    pub monitor: String,
    pub path: String,
    pub changer: WallpaperChangers,
}

pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION");

pub fn sort_by_sort_dropdown_string(files: &mut [PathBuf], sort_by: &str, invert_sort: bool) {
    match sort_by {
        "name" => {
            files.sort_by(|f1, f2| {
                if invert_sort {
                    f1.file_name().partial_cmp(&f2.file_name()).unwrap()
                } else {
                    f2.file_name().partial_cmp(&f1.file_name()).unwrap()
                }
            });
        }
        "date" => {
            files.sort_by(|f1, f2| {
                if invert_sort {
                    f1.metadata()
                        .unwrap()
                        .created()
                        .unwrap()
                        .partial_cmp(&f2.metadata().unwrap().created().unwrap())
                        .unwrap()
                } else {
                    f2.metadata()
                        .unwrap()
                        .created()
                        .unwrap()
                        .partial_cmp(&f1.metadata().unwrap().created().unwrap())
                        .unwrap()
                }
            });
        }
        _ => {}
    }
}