feat: 新增自定义窗口背景功能
This commit is contained in:
Generated
+7
@@ -1843,6 +1843,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
@@ -4595,6 +4601,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"http-range",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
|
||||
@@ -22,7 +22,7 @@ tauri-build = { version = "2", features = [] }
|
||||
tauri-helper = "0.2.1"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.12.3", features = ["tray-icon"] }
|
||||
tauri = { version = "2.12.3", features = ["protocol-asset", "tray-icon"] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use tauri::{AppHandle, State};
|
||||
use tauri_helper::auto_collect_command;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
use crate::models::ApiResponse;
|
||||
use crate::services::background::{find_custom_background, import_custom_background};
|
||||
use crate::services::storage::StoragePaths;
|
||||
|
||||
/// 打开原生文件选择器并将图片复制到应用数据目录。
|
||||
#[tauri::command]
|
||||
#[auto_collect_command]
|
||||
pub async fn choose_custom_background(
|
||||
app: AppHandle,
|
||||
paths: State<'_, StoragePaths>,
|
||||
) -> Result<ApiResponse<Option<String>>, String> {
|
||||
let picked = app
|
||||
.dialog()
|
||||
.file()
|
||||
.add_filter("图片", &["png", "jpg", "jpeg", "webp"])
|
||||
.set_title("选择背景图片")
|
||||
.blocking_pick_file();
|
||||
let Some(file) = picked else {
|
||||
return Ok(ApiResponse::success(None));
|
||||
};
|
||||
let source = file.into_path().map_err(|e| e.to_string())?;
|
||||
let saved = import_custom_background(&paths, &source)?;
|
||||
Ok(ApiResponse::success(Some(
|
||||
saved.to_string_lossy().into_owned(),
|
||||
)))
|
||||
}
|
||||
|
||||
/// 获取已保存的自定义背景路径,图片内容通过受限的 asset 协议读取。
|
||||
#[tauri::command]
|
||||
#[auto_collect_command]
|
||||
pub async fn get_custom_background(
|
||||
paths: State<'_, StoragePaths>,
|
||||
) -> Result<ApiResponse<Option<String>>, String> {
|
||||
Ok(ApiResponse::success(
|
||||
find_custom_background(&paths).map(|path| path.to_string_lossy().into_owned()),
|
||||
))
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod background;
|
||||
pub mod frp;
|
||||
pub mod runner;
|
||||
pub mod server;
|
||||
|
||||
@@ -5,6 +5,7 @@ mod services;
|
||||
use tauri::{Manager, RunEvent};
|
||||
use tauri_plugin_autostart::ManagerExt as _;
|
||||
|
||||
use crate::commands::background::{choose_custom_background, get_custom_background};
|
||||
use crate::commands::frp::{
|
||||
delete_frp, download_frp, get_frp_downloads, get_frp_releases, import_frp,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::services::storage::StoragePaths;
|
||||
|
||||
const MAX_BACKGROUND_SIZE: u64 = 15 * 1024 * 1024;
|
||||
|
||||
pub fn import_custom_background(paths: &StoragePaths, source: &Path) -> Result<PathBuf, String> {
|
||||
let size = fs::metadata(source)
|
||||
.map_err(|e| format!("读取图片信息失败:{e}"))?
|
||||
.len();
|
||||
if size == 0 || size > MAX_BACKGROUND_SIZE {
|
||||
return Err("图片大小必须在 1 字节到 15 MB 之间".into());
|
||||
}
|
||||
|
||||
let bytes = fs::read(source).map_err(|e| format!("读取背景图片失败:{e}"))?;
|
||||
if bytes.is_empty() || bytes.len() as u64 > MAX_BACKGROUND_SIZE {
|
||||
return Err("图片大小必须在 1 字节到 15 MB 之间".into());
|
||||
}
|
||||
let extension = detect_extension(&bytes).ok_or("仅支持 PNG、JPG 和 WebP 图片")?;
|
||||
let directory = paths.data().join("backgrounds");
|
||||
fs::create_dir_all(&directory).map_err(|e| format!("创建背景图片目录失败:{e}"))?;
|
||||
|
||||
let destination = directory.join(format!("custom.{extension}"));
|
||||
let temporary = directory.join(format!("custom.{extension}.tmp"));
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&temporary)
|
||||
.map_err(|e| format!("创建背景图片文件失败:{e}"))?;
|
||||
if let Err(error) = file.write_all(&bytes).and_then(|()| file.sync_all()) {
|
||||
drop(file);
|
||||
let _ = fs::remove_file(&temporary);
|
||||
return Err(format!("保存背景图片失败:{error}"));
|
||||
}
|
||||
drop(file);
|
||||
fs::rename(&temporary, &destination).map_err(|e| {
|
||||
let _ = fs::remove_file(&temporary);
|
||||
format!("更新背景图片失败:{e}")
|
||||
})?;
|
||||
|
||||
for stale_extension in ["png", "jpg", "webp"] {
|
||||
if stale_extension != extension {
|
||||
let stale = directory.join(format!("custom.{stale_extension}"));
|
||||
let _ = fs::remove_file(stale);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(destination)
|
||||
}
|
||||
|
||||
pub fn find_custom_background(paths: &StoragePaths) -> Option<PathBuf> {
|
||||
["png", "jpg", "webp"]
|
||||
.into_iter()
|
||||
.map(|extension| {
|
||||
paths
|
||||
.data()
|
||||
.join("backgrounds")
|
||||
.join(format!("custom.{extension}"))
|
||||
})
|
||||
.filter(|path| path.is_file())
|
||||
.max_by_key(|path| {
|
||||
fs::metadata(path)
|
||||
.and_then(|metadata| metadata.modified())
|
||||
.unwrap_or(std::time::UNIX_EPOCH)
|
||||
})
|
||||
}
|
||||
|
||||
fn detect_extension(bytes: &[u8]) -> Option<&'static str> {
|
||||
if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
|
||||
Some("png")
|
||||
} else if bytes.starts_with(b"\xff\xd8\xff") {
|
||||
Some("jpg")
|
||||
} else if bytes.len() >= 12 && &bytes[..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
|
||||
Some("webp")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn imports_supported_image_and_replaces_previous_format() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"frpcpilot-background-test-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
fs::create_dir_all(&root).unwrap();
|
||||
let paths = StoragePaths { root: root.clone() };
|
||||
let source = root.join("source-image.bin");
|
||||
let png = b"\x89PNG\r\n\x1a\nimage-data";
|
||||
fs::write(&source, png).unwrap();
|
||||
|
||||
let saved = import_custom_background(&paths, &source).unwrap();
|
||||
|
||||
assert_eq!(saved, paths.data().join("backgrounds/custom.png"));
|
||||
assert_eq!(fs::read(&saved).unwrap(), png);
|
||||
assert_eq!(find_custom_background(&paths), Some(saved.clone()));
|
||||
|
||||
let jpeg = b"\xff\xd8\xffimage-data";
|
||||
fs::write(&source, jpeg).unwrap();
|
||||
let replaced = import_custom_background(&paths, &source).unwrap();
|
||||
assert_eq!(replaced, paths.data().join("backgrounds/custom.jpg"));
|
||||
assert!(!saved.exists());
|
||||
assert_eq!(fs::read(&replaced).unwrap(), jpeg);
|
||||
assert_eq!(find_custom_background(&paths), Some(replaced));
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_image_files() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"frpcpilot-background-invalid-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
fs::create_dir_all(&root).unwrap();
|
||||
let paths = StoragePaths { root: root.clone() };
|
||||
let source = root.join("not-an-image");
|
||||
fs::write(&source, b"not an image").unwrap();
|
||||
|
||||
assert!(import_custom_background(&paths, &source).is_err());
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod background;
|
||||
pub mod config;
|
||||
pub mod db;
|
||||
pub mod runner;
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
"csp": null,
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": ["$APPDATA/backgrounds/**"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
||||
Reference in New Issue
Block a user