72 lines
2.1 KiB
Rust
72 lines
2.1 KiB
Rust
use std::{
|
|
env, fs,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
|
use serde_json::Value;
|
|
|
|
pub fn local_app_data_dir() -> Option<PathBuf> {
|
|
env::var_os("LOCALAPPDATA").map(PathBuf::from).or_else(|| {
|
|
env::var_os("USERPROFILE")
|
|
.map(PathBuf::from)
|
|
.map(|path| path.join("AppData").join("Local"))
|
|
})
|
|
}
|
|
|
|
pub fn pick_latest_subdirectory(root: &Path) -> Option<PathBuf> {
|
|
let entries = fs::read_dir(root).ok()?;
|
|
let mut candidates = entries
|
|
.flatten()
|
|
.filter_map(|entry| {
|
|
let file_type = entry.file_type().ok()?;
|
|
if !file_type.is_dir() {
|
|
return None;
|
|
}
|
|
let metadata = entry.metadata().ok()?;
|
|
let modified = metadata.modified().ok();
|
|
Some((
|
|
modified,
|
|
entry.file_name().to_string_lossy().to_string(),
|
|
entry.path(),
|
|
))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
candidates.sort_by(|left, right| right.0.cmp(&left.0).then_with(|| right.1.cmp(&left.1)));
|
|
candidates.into_iter().next().map(|(_, _, path)| path)
|
|
}
|
|
|
|
pub fn load_image_as_data_url(path: &Path) -> Option<String> {
|
|
let bytes = fs::read(path).ok()?;
|
|
let extension = path
|
|
.extension()
|
|
.and_then(|value| value.to_str())
|
|
.map(|value| value.to_ascii_lowercase())?;
|
|
let mime_type = match extension.as_str() {
|
|
"png" => "image/png",
|
|
"jpg" | "jpeg" => "image/jpeg",
|
|
"webp" => "image/webp",
|
|
"gif" => "image/gif",
|
|
"svg" => "image/svg+xml",
|
|
_ => return None,
|
|
};
|
|
|
|
Some(format!(
|
|
"data:{mime_type};base64,{}",
|
|
STANDARD.encode(bytes)
|
|
))
|
|
}
|
|
|
|
pub fn read_json_file(path: &Path) -> Option<Value> {
|
|
let content = fs::read_to_string(path).ok()?;
|
|
serde_json::from_str(&content).ok()
|
|
}
|
|
|
|
pub fn first_non_empty<'a>(values: impl IntoIterator<Item = Option<&'a str>>) -> Option<&'a str> {
|
|
values
|
|
.into_iter()
|
|
.flatten()
|
|
.find(|value| !value.trim().is_empty())
|
|
}
|