diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6857ab0..4323287 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -224,71 +224,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "axum" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" -dependencies = [ - "axum-core", - "axum-macros", - "bytes", - "form_urlencoded", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "multer", - "percent-encoding", - "pin-project-lite", - "serde_core", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-macros" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "base64" version = "0.21.7" @@ -536,10 +471,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", - "js-sys", "num-traits", "serde", - "wasm-bindgen", "windows-link 0.2.1", ] @@ -953,15 +886,6 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "endi" version = "1.1.1" @@ -1132,21 +1056,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.32" @@ -1154,7 +1063,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", - "futures-sink", ] [[package]] @@ -1222,7 +1130,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1614,24 +1521,12 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" - [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "hyper" version = "1.9.0" @@ -1645,7 +1540,6 @@ dependencies = [ "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -2102,12 +1996,6 @@ dependencies = [ "web_atoms", ] -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - [[package]] name = "memchr" version = "2.8.0" @@ -2129,16 +2017,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2181,23 +2059,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "multer" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "memchr", - "mime", - "spin", - "version_check", -] - [[package]] name = "ndk" version = "0.9.0" @@ -2932,40 +2793,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "rust-embed" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn 2.0.117", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" -dependencies = [ - "sha2", - "walkdir", -] - [[package]] name = "rustc-hash" version = "2.1.2" @@ -3000,12 +2827,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - [[package]] name = "same-file" version = "1.0.6" @@ -3167,17 +2988,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - [[package]] name = "serde_repr" version = "0.1.20" @@ -3207,18 +3017,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_with" version = "3.20.0" @@ -3391,12 +3189,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3948,37 +3740,11 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", - "tokio-macros", "windows-sys 0.61.2", ] -[[package]] -name = "tokio-macros" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -4125,7 +3891,6 @@ dependencies = [ "tokio", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -4136,19 +3901,10 @@ checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" dependencies = [ "bitflags 2.11.1", "bytes", - "futures-core", "futures-util", "http", "http-body", - "http-body-util", - "http-range-header", - "httpdate", - "mime", - "mime_guess", - "percent-encoding", "pin-project-lite", - "tokio", - "tokio-util", "tower", "tower-layer", "tower-service", @@ -4173,7 +3929,6 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -4291,12 +4046,6 @@ dependencies = [ "unic-common", ] -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -5390,21 +5139,9 @@ dependencies = [ name = "昆明大学生创业园展播系统" version = "0.1.0" dependencies = [ - "axum", - "chrono", - "dirs 6.0.0", - "futures", - "once_cell", - "rust-embed", "serde", - "serde_json", "tauri", "tauri-build", "tauri-plugin-autostart", "tauri-plugin-opener", - "tokio", - "tokio-stream", - "tower", - "tower-http", - "uuid", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c58353a..33aa812 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,15 +17,3 @@ tauri = { version = "2", features = ["macos-private-api"] } tauri-plugin-opener = "2" tauri-plugin-autostart = "2" serde = { version = "1", features = ["derive"] } -serde_json = "1" -tokio = { version = "1", features = ["full"] } -axum = { version = "0.8", features = ["macros", "multipart"] } -tower-http = { version = "0.6", features = ["cors", "fs"] } -uuid = { version = "1", features = ["v4"] } -chrono = { version = "0.4", features = ["serde"] } -tower = "0.5" -once_cell = "1" -dirs = "6" -futures = "0.3" -tokio-stream = { version = "0.1", features = ["sync"] } -rust-embed = "8" diff --git a/src-tauri/src/events.rs b/src-tauri/src/events.rs deleted file mode 100644 index 15a95d3..0000000 --- a/src-tauri/src/events.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::models::ServerEvent; -use once_cell::sync::Lazy; -use tokio::sync::broadcast; - -/// SSE 事件管理器 -pub struct EventManager { - tx: broadcast::Sender, -} - -impl EventManager { - pub fn new() -> Self { - let (tx, _) = broadcast::channel(64); - Self { tx } - } - - /// 广播事件给所有 SSE 客户端 - pub fn broadcast(&self, event: &ServerEvent) { - if let Ok(json) = serde_json::to_string(event) { - let _ = self.tx.send(json); - } - } - - /// 获取广播接收器 - pub fn subscribe(&self) -> broadcast::Receiver { - self.tx.subscribe() - } -} - -pub static EVENTS: Lazy = Lazy::new(EventManager::new); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3a836a2..bc46ba1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,7 +1,5 @@ -mod events; -mod models; -mod server; -mod storage; +// 说明:早期 Rust 内嵌后端(server/storage/models/events,axum :10801)已随架构迁移到 +// Python 后端(FastAPI :10085)而移除;当前 Rust 侧仅负责窗口与生命周期。 #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs deleted file mode 100644 index 912f672..0000000 --- a/src-tauri/src/models.rs +++ /dev/null @@ -1,171 +0,0 @@ -use serde::{Deserialize, Serialize}; - -// ============ Settings ============ -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Settings { - pub username: String, - pub password: String, - pub volume: u32, - pub auto_play: bool, - pub play_mode: String, - pub image_duration: u32, - pub fullscreen: bool, - pub autostart: bool, -} - -impl Default for Settings { - fn default() -> Self { - Self { - username: "admin".into(), - password: "123456".into(), - volume: 80, - auto_play: true, - play_mode: "sequential".into(), - image_duration: 5, - fullscreen: true, - autostart: false, - } - } -} - -// ============ Playlist Item ============ -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PlaylistItem { - pub path: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub source: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, -} - -// ============ URL Media Item ============ -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UrlMediaItem { - pub url: String, - pub name: String, - #[serde(rename = "type")] - pub media_type: String, - pub added_at: String, -} - -// ============ Media File (response) ============ -#[derive(Debug, Clone, Serialize)] -pub struct MediaFile { - pub name: String, - pub relative_path: String, - #[serde(rename = "type")] - pub file_type: String, - pub url: String, - pub source: String, -} - -// ============ Playback State ============ -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PlaybackState { - pub status: String, - pub index: usize, - pub name: String, - #[serde(rename = "type")] - pub media_type: String, -} - -impl Default for PlaybackState { - fn default() -> Self { - Self { - status: "idle".into(), - index: 0, - name: String::new(), - media_type: String::new(), - } - } -} - -// ============ Events ============ -#[derive(Debug, Clone, Serialize)] -pub struct ServerEvent { - pub action: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub state: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub volume: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub play_mode: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub image_duration: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub fullscreen: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub autostart: Option, -} - -// ============ Playlist Response ============ -#[derive(Debug, Clone, Serialize)] -pub struct PlaylistResponse { - pub files: Vec, - pub volume: u32, - pub play_mode: String, - pub image_duration: u32, -} - -// ============ API Request Bodies ============ -#[derive(Debug, Deserialize)] -pub struct LoginForm { - pub username: String, - pub password: String, -} - -#[derive(Debug, Serialize)] -pub struct LoginResponse { - pub success: bool, -} - -#[derive(Debug, Deserialize)] -pub struct SettingsUpdate { - pub volume: Option, - pub play_mode: Option, - pub image_duration: Option, - pub fullscreen: Option, - pub autostart: Option, -} - -#[derive(Debug, Deserialize)] -pub struct PathBody { - pub path: String, -} - -#[derive(Debug, Deserialize)] -pub struct UrlBody { - pub url: String, -} - -#[derive(Debug, Deserialize)] -pub struct ActionBody { - pub action: String, -} - -#[derive(Debug, Deserialize)] -pub struct StateBody { - pub state: PlaybackState, -} - -#[derive(Debug, Serialize)] -pub struct OkResponse { - pub ok: bool, -} - -#[derive(Debug, Serialize)] -pub struct OkDuplicateResponse { - pub ok: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub duplicate: Option, -} - -#[derive(Debug, Serialize)] -pub struct FilesResponse { - pub files: Vec, -} - -#[derive(Debug, Serialize)] -pub struct StateResponse { - pub state: PlaybackState, -} diff --git a/src-tauri/src/server.rs b/src-tauri/src/server.rs deleted file mode 100644 index 757be1e..0000000 --- a/src-tauri/src/server.rs +++ /dev/null @@ -1,586 +0,0 @@ -use crate::events::EVENTS; -use crate::models::*; -use crate::storage::{get_media_type, is_allowed_extension, STORAGE}; -use axum::{ - body::Body, - extract::{DefaultBodyLimit, Form, Multipart, State}, - http::{Response, StatusCode, Uri}, - response::sse::{Event, KeepAlive, Sse}, - response::Json, - routing::{get, post}, - Router, -}; -use chrono::Local; -use futures::stream::Stream; -use rust_embed::RustEmbed; -use std::convert::Infallible; -use std::path::PathBuf; -use std::sync::Arc; -use std::fs; -use tokio::io::AsyncWriteExt; -use tokio_stream::wrappers::BroadcastStream; -use tokio_stream::StreamExt; -use tower_http::cors::CorsLayer; -use tower_http::services::ServeDir; - -// ===================== 嵌入式前端文件 ===================== -// dist/ 编译进二进制,无需文件系统依赖 -#[derive(RustEmbed)] -#[folder = "../dist"] -struct Asset; - -fn mime_type(path: &str) -> &'static str { - let ext = path.rsplit('.').next().unwrap_or(""); - match ext { - "html" => "text/html; charset=utf-8", - "css" => "text/css; charset=utf-8", - "js" => "application/javascript; charset=utf-8", - "png" => "image/png", - "jpg" | "jpeg" => "image/jpeg", - "svg" => "image/svg+xml", - "ico" => "image/x-icon", - "json" => "application/json", - "woff2" => "font/woff2", - "woff" => "font/woff", - "ttf" => "font/ttf", - _ => "application/octet-stream", - } -} - -async fn spa_fallback(uri: Uri) -> Response { - let path = uri.path().trim_start_matches('/'); - - // 1. 尝试从嵌入式文件提供 - if !path.is_empty() && !path.starts_with("api/") { - if let Some(file) = Asset::get(path) { - return Response::builder() - .header("Content-Type", mime_type(path)) - .body(Body::from(file.data.into_owned())) - .unwrap(); - } - } - - // 2. SPA 回退:所有非 API 路径都返回 index.html - if !path.starts_with("api/") { - if let Some(file) = Asset::get("index.html") { - return Response::builder() - .header("Content-Type", "text/html; charset=utf-8") - .body(Body::from(file.data.into_owned())) - .unwrap(); - } - } - - // 3. 文件系统回退(开发模式可用) - let dist = dist_dir(); - let file_path = dist.join(path); - if let Ok(data) = fs::read(&file_path) { - return Response::builder() - .header("Content-Type", mime_type(path)) - .body(Body::from(data)) - .unwrap(); - } - let index = dist.join("index.html"); - if let Ok(data) = fs::read(&index) { - return Response::builder() - .header("Content-Type", "text/html; charset=utf-8") - .body(Body::from(data)) - .unwrap(); - } - - Response::builder() - .status(StatusCode::NOT_FOUND) - .header("Content-Type", "text/plain; charset=utf-8") - .body(Body::from("Not Found")) - .unwrap() -} - -// ===================== 媒体目录 ===================== -fn media_dir() -> PathBuf { - let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); - path.push("Downloads"); - path.push("Media"); - fs::create_dir_all(&path).ok(); - path -} - -fn media_dir_str() -> String { - media_dir().to_string_lossy().to_string() -} - -#[derive(Clone)] -pub struct AppState { - pub playback: Arc>, -} - -impl AppState { - pub fn new() -> Self { - Self { - playback: Arc::new(tokio::sync::Mutex::new(PlaybackState::default())), - } - } -} - -/// 智能查找 dist 目录(前端静态文件) -/// 支持多种运行场景: -/// - tauri dev 时 CWD = src-tauri/ → ../dist -/// - 二进制在 target/release/dpm → ../../dist -/// - 从项目根目录运行时 → dist -fn dist_dir() -> PathBuf { - let candidates: Vec = { - let mut c = Vec::new(); - // 1. 相对于当前工作目录 - c.push(PathBuf::from("dist")); - c.push(PathBuf::from("../dist")); - // 2. 相对于可执行文件路径 - if let Ok(exe) = std::env::current_exe() { - if let Some(dir) = exe.parent() { - // 开发模式: 二进制在 src-tauri/target/debug/dpm → dist 在项目根目录 - c.push(dir.join("../../../dist")); - c.push(dir.join("../../dist")); - // macOS .app bundle 资源目录: dpm.app/Contents/Resources/dist/ - c.push(dir.join("../Resources/dist")); - // Windows/Linux: 资源在二进制同目录下的 dist/ - c.push(dir.join("dist")); - // Windows NSIS 安装: 资源在 resources/dist/ - c.push(dir.join("resources/dist")); - } - } - c - }; - - for p in &candidates { - let canonical = p.canonicalize(); - if let Ok(path) = canonical { - if path.join("index.html").exists() { - return path; - } - } - } - - // Fallback - let fallback = PathBuf::from("../dist"); - eprintln!("[dpm] 警告: 找不到 dist 目录 (已尝试 {:?}),使用: {:?}", candidates, fallback); - fallback -} - -// ===================== 路由构建 ===================== -pub fn create_router(state: AppState) -> Router { - Router::new() - .route("/api/login", post(login_handler)) - .route("/api/settings", get(get_settings).post(update_settings)) - .route("/api/display-command", post(display_command_handler)) - .route("/media", get(list_media)) - .route("/upload", post(upload_handler)) - .route("/api/delete", post(delete_handler)) - .route("/api/media/add-url", post(add_url_handler)) - .route("/api/playlist", get(get_playlist)) - .route("/api/playlist/add", post(add_playlist)) - .route("/api/playlist/remove", post(remove_playlist)) - .route("/api/control", post(control_handler)) - .route("/api/state", get(get_state).post(update_state)) - .route("/api/events", get(sse_handler)) - // 媒体文件:用户目录 ~/Downloads/Media/ - .nest_service("/file", ServeDir::new(media_dir_str())) - // SPA 回退 → 嵌入式文件(跨平台,编译到二进制中) - .fallback_service(get(spa_fallback)) - .layer(DefaultBodyLimit::max(2048 * 1024 * 1024)) // 2GB 上传限制 - .layer(CorsLayer::permissive()) - .with_state(state) -} - -pub async fn start() { - let state = AppState::new(); - let app = create_router(state); - - println!("🚀 大屏媒体轮播系统启动于 http://0.0.0.0:10801"); - println!(" - 管理后台: http://localhost:10801/admin"); - println!(" - 大屏展示: http://localhost:10801/screen"); - - let listener = tokio::net::TcpListener::bind("0.0.0.0:10801") - .await - .expect("无法绑定 0.0.0.0:10801,请检查端口是否被占用"); - axum::serve(listener, app) - .await - .expect("服务器启动失败"); -} - -// ===================== Handler: 登录 ===================== -async fn login_handler( - Form(form): Form, -) -> Json { - let s = STORAGE.get_settings(); - Json(LoginResponse { - success: form.username == s.username && form.password == s.password, - }) -} - -// ===================== Handler: 设置 ===================== -async fn get_settings() -> Json { - let s = STORAGE.get_settings(); - Json(serde_json::json!({ - "volume": s.volume, - "play_mode": s.play_mode, - "image_duration": s.image_duration, - "fullscreen": s.fullscreen, - "autostart": s.autostart, - })) -} - -async fn update_settings( - Json(body): Json, -) -> Json { - STORAGE.update_settings(SettingsUpdate { - volume: body.volume, - play_mode: body.play_mode.clone(), - image_duration: body.image_duration, - fullscreen: body.fullscreen, - autostart: body.autostart, - }); - - EVENTS.broadcast(&ServerEvent { - action: "settings_changed".into(), - state: None, - volume: body.volume, - play_mode: body.play_mode, - image_duration: body.image_duration, - fullscreen: body.fullscreen, - autostart: body.autostart, - }); - - Json(serde_json::json!({"ok": true})) -} - -// ===================== Handler: 媒体列表 ===================== -async fn list_media() -> Json { - let mut files = Vec::new(); - let dir = media_dir(); - - // 本地文件 - if let Ok(entries) = fs::read_dir(&dir) { - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_file() { - continue; - } - let name = path.file_name() - .and_then(|n| n.to_str()) - .unwrap_or("") - .to_string(); - if !is_allowed_extension(&name) { - continue; - } - let rel = name.clone(); - let t = get_media_type(&name).to_string(); - files.push(MediaFile { - name, - relative_path: rel.clone(), - file_type: t, - url: format!("/file/{}", rel), - source: "local".into(), - }); - } - } - - // URL 媒体 - for item in STORAGE.get_url_media() { - files.push(MediaFile { - name: item.name, - relative_path: item.url.clone(), - file_type: item.media_type, - url: item.url, - source: "url".into(), - }); - } - - Json(FilesResponse { files }) -} - -// ===================== Handler: 上传 ===================== -async fn upload_handler( - mut multipart: Multipart, -) -> Result, (StatusCode, String)> { - while let Ok(Some(mut field)) = multipart.next_field().await { - let file_name = field.file_name().unwrap_or("file").to_string(); - let ext = std::path::Path::new(&file_name) - .extension() - .and_then(|e| e.to_str()) - .map(|e| format!(".{}", e.to_lowercase())) - .unwrap_or_default(); - - if !is_allowed_extension(&ext) { - return Err((StatusCode::BAD_REQUEST, "不支持的文件类型".into())); - } - - let ts = Local::now().format("%Y%m%d%H%M%S"); - let stem = std::path::Path::new(&file_name) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("file"); - let new_name = format!("{}_{}{}", stem, ts, ext); - let save_path = media_dir().join(&new_name); - - // 流式写入文件,避免大视频文件占用过多内存 - let mut file = tokio::fs::File::create(&save_path).await.map_err(|e| { - (StatusCode::INTERNAL_SERVER_ERROR, format!("文件创建失败: {}", e)) - })?; - loop { - match field.chunk().await { - Ok(Some(chunk)) => { - file.write_all(&chunk).await.map_err(|e| { - (StatusCode::INTERNAL_SERVER_ERROR, format!("写入文件失败: {}", e)) - })?; - } - Ok(None) => break, - Err(e) => { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("读取上传数据失败: {}", e), - )) - } - } - } - } - Ok(Json(serde_json::json!({"ok": true}))) -} - -// ===================== Handler: 删除 ===================== -async fn delete_handler( - Json(body): Json, -) -> Json { - let path = body.path; - if !path.starts_with("http://") && !path.starts_with("https://") { - let file_path = media_dir().join(&path); - if file_path.exists() && file_path.is_file() { - fs::remove_file(&file_path).ok(); - } - } - STORAGE.delete_media(&path); - - EVENTS.broadcast(&ServerEvent { - action: "playlist_changed".into(), - state: None, - volume: None, - play_mode: None, - image_duration: None, - fullscreen: None, - autostart: None, - }); - - Json(OkResponse { ok: true }) -} - -// ===================== Handler: 添加 URL ===================== -async fn add_url_handler( - Json(body): Json, -) -> Json { - let url = body.url.trim().to_string(); - if url.is_empty() { - return Json(OkDuplicateResponse { ok: false, duplicate: None }); - } - - let name = url.rsplit('/').next().unwrap_or(&url) - .split('?').next().unwrap_or(&url) - .to_string(); - let t = get_media_type(&url).to_string(); - let is_new = STORAGE.add_url_media(&url, name, t); - - Json(OkDuplicateResponse { ok: true, duplicate: Some(!is_new) }) -} - -// ===================== Handler: 播放列表 ===================== -async fn get_playlist() -> Json { - let items = STORAGE.get_playlist(); - let settings = STORAGE.get_settings(); - let dir = media_dir(); - let mut files = Vec::new(); - - for item in &items { - if item.source.as_deref() == Some("url") { - let ext = std::path::Path::new(&item.path) - .extension() - .and_then(|e| e.to_str()) - .unwrap_or(""); - let t = if ["mp4", "mkv", "avi"].contains(&ext) { "video" } else { "image" }; - files.push(MediaFile { - name: item.name.clone().unwrap_or_else(|| item.path.clone()), - relative_path: item.path.clone(), - file_type: t.to_string(), - url: item.path.clone(), - source: "url".into(), - }); - } else { - let file_path = dir.join(&item.path); - if file_path.exists() { - let ext = file_path.extension() - .and_then(|e| e.to_str()) - .unwrap_or(""); - let t = if ["mp4", "mkv", "avi"].contains(&ext) { "video" } else { "image" }; - files.push(MediaFile { - name: file_path.file_name() - .and_then(|n| n.to_str()) - .unwrap_or(&item.path) - .to_string(), - relative_path: item.path.clone(), - file_type: t.to_string(), - url: format!("/file/{}", item.path), - source: "local".into(), - }); - } - } - } - - Json(PlaylistResponse { - files, - volume: settings.volume, - play_mode: settings.play_mode, - image_duration: settings.image_duration, - }) -} - -async fn add_playlist( - Json(body): Json, -) -> Json { - STORAGE.add_to_playlist(&body.path); - EVENTS.broadcast(&ServerEvent { - action: "playlist_changed".into(), - state: None, - volume: None, - play_mode: None, - image_duration: None, - fullscreen: None, - autostart: None, - }); - Json(OkResponse { ok: true }) -} - -async fn remove_playlist( - Json(body): Json, -) -> Json { - STORAGE.remove_from_playlist(&body.path); - EVENTS.broadcast(&ServerEvent { - action: "playlist_changed".into(), - state: None, - volume: None, - play_mode: None, - image_duration: None, - fullscreen: None, - autostart: None, - }); - Json(OkResponse { ok: true }) -} - -// ===================== Handler: 播放控制 ===================== -async fn control_handler( - State(state): State, - Json(body): Json, -) -> Result, (StatusCode, String)> { - if !["play", "pause", "next", "prev"].contains(&body.action.as_str()) { - return Err((StatusCode::BAD_REQUEST, "action 必须是 play/pause/next/prev".into())); - } - - EVENTS.broadcast(&ServerEvent { - action: body.action.clone(), - state: None, - volume: None, - play_mode: None, - image_duration: None, - fullscreen: None, - autostart: None, - }); - - let mut playback = state.playback.lock().await; - playback.status = match body.action.as_str() { - "play" => "playing".into(), - "pause" => "paused".into(), - _ => playback.status.clone(), - }; - let state_snapshot = playback.clone(); - drop(playback); - - EVENTS.broadcast(&ServerEvent { - action: "state_update".into(), - state: Some(state_snapshot), - volume: None, - play_mode: None, - image_duration: None, - fullscreen: None, - autostart: None, - }); - - Ok(Json(serde_json::json!({"ok": true, "action": body.action}))) -} - -// ===================== Handler: 显示命令(最小化、全屏等) ===================== -async fn display_command_handler( - Json(body): Json, -) -> Result, (StatusCode, String)> { - match body.action.as_str() { - "minimize" => { - EVENTS.broadcast(&ServerEvent { - action: "minimize_window".into(), - state: None, - volume: None, - play_mode: None, - image_duration: None, - fullscreen: None, - autostart: None, - }); - Ok(Json(serde_json::json!({"ok": true, "action": "minimize"}))) - } - _ => Err((StatusCode::BAD_REQUEST, "不支持的显示命令".into())), - } -} - -// ===================== Handler: 状态 ===================== -async fn get_state( - State(state): State, -) -> Json { - let s = state.playback.lock().await; - Json(StateResponse { state: s.clone() }) -} - -async fn update_state( - State(state): State, - Json(body): Json, -) -> Json { - { - let mut s = state.playback.lock().await; - if !body.state.status.is_empty() { - s.status = body.state.status.clone(); - } - s.index = body.state.index; - if !body.state.name.is_empty() { - s.name = body.state.name.clone(); - } - if !body.state.media_type.is_empty() { - s.media_type = body.state.media_type.clone(); - } - } - - let s2 = state.playback.lock().await; - EVENTS.broadcast(&ServerEvent { - action: "state_update".into(), - state: Some(s2.clone()), - volume: None, - play_mode: None, - image_duration: None, - fullscreen: None, - autostart: None, - }); - - Json(OkResponse { ok: true }) -} - -// ===================== Handler: SSE ===================== -async fn sse_handler() -> Sse>> { - let rx = EVENTS.subscribe(); - let stream = BroadcastStream::new(rx).map(|msg| match msg { - Ok(data) => Ok(Event::default().data(data)), - Err(_) => Ok(Event::default().data("")), - }); - Sse::new(stream).keep_alive( - KeepAlive::new() - .interval(std::time::Duration::from_secs(15)), - ) -} - diff --git a/src-tauri/src/storage.rs b/src-tauri/src/storage.rs deleted file mode 100644 index 1e634ea..0000000 --- a/src-tauri/src/storage.rs +++ /dev/null @@ -1,203 +0,0 @@ -use crate::models::*; -use once_cell::sync::Lazy; -use std::path::PathBuf; -use std::sync::Mutex; - -/// 数据文件路径:系统数据目录 /dpm/data.json -fn db_path() -> PathBuf { - let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(".")); - path.push("dpm"); - std::fs::create_dir_all(&path).ok(); - path.push("data.json"); - path -} - -/// 持久化数据结构 -#[derive(Debug, serde::Serialize, serde::Deserialize)] -struct PersistentData { - settings: std::collections::HashMap, - playlist: Vec, - url_media: Vec, -} - -impl Default for PersistentData { - fn default() -> Self { - let mut s = std::collections::HashMap::new(); - s.insert("admin".into(), Settings::default()); - Self { - settings: s, - playlist: Vec::new(), - url_media: Vec::new(), - } - } -} - -/// 数据存储 -pub struct Storage { - inner: Mutex, - path: PathBuf, -} - -impl Storage { - pub fn new() -> Self { - let path = db_path(); - let data = if path.exists() { - std::fs::read_to_string(&path) - .ok() - .and_then(|c| serde_json::from_str(&c).ok()) - .unwrap_or_default() - } else { - PersistentData::default() - }; - let storage = Self { - inner: Mutex::new(data), - path, - }; - storage.save(); - storage - } - - fn save(&self) { - if let Ok(data) = self.inner.lock() { - if let Ok(json) = serde_json::to_string_pretty(&*data) { - std::fs::write(&self.path, json).ok(); - } - } - } - - // === Settings === - pub fn get_settings(&self) -> Settings { - self.inner - .lock() - .ok() - .and_then(|d| d.settings.get("admin").cloned()) - .unwrap_or_default() - } - - pub fn update_settings(&self, update: SettingsUpdate) { - if let Ok(mut data) = self.inner.lock() { - if let Some(s) = data.settings.get_mut("admin") { - if let Some(v) = update.volume { - s.volume = v; - } - if let Some(m) = update.play_mode { - s.play_mode = m; - } - if let Some(d) = update.image_duration { - s.image_duration = d; - } - if let Some(f) = update.fullscreen { - s.fullscreen = f; - } - if let Some(a) = update.autostart { - s.autostart = a; - } - } - } - self.save(); - } - - // === Playlist === - pub fn get_playlist(&self) -> Vec { - self.inner - .lock() - .ok() - .map(|d| d.playlist.clone()) - .unwrap_or_default() - } - - pub fn add_to_playlist(&self, path: &str) { - if let Ok(mut data) = self.inner.lock() { - if !data.playlist.iter().any(|p| p.path == path) { - let mut item = PlaylistItem { - path: path.into(), - source: None, - name: None, - }; - if path.starts_with("http://") || path.starts_with("https://") { - item.source = Some("url".into()); - if let Some(um) = data.url_media.iter().find(|u| u.url == path) { - item.name = Some(um.name.clone()); - } - } - data.playlist.push(item); - } - } - self.save(); - } - - pub fn remove_from_playlist(&self, path: &str) { - if let Ok(mut data) = self.inner.lock() { - data.playlist.retain(|p| p.path != path); - } - self.save(); - } - - // === URL Media === - pub fn add_url_media(&self, url: &str, name: String, media_type: String) -> bool { - let is_new = if let Ok(mut data) = self.inner.lock() { - if !data.url_media.iter().any(|u| u.url == url) { - data.url_media.push(UrlMediaItem { - url: url.into(), - name, - media_type, - added_at: chrono::Local::now() - .format("%Y-%m-%dT%H:%M:%S%.3f") - .to_string(), - }); - true - } else { - false - } - } else { - false - }; - if is_new { - self.save(); - } - is_new - } - - pub fn get_url_media(&self) -> Vec { - self.inner - .lock() - .ok() - .map(|d| d.url_media.clone()) - .unwrap_or_default() - } - - pub fn remove_url_media(&self, url: &str) { - if let Ok(mut data) = self.inner.lock() { - data.url_media.retain(|u| u.url != url); - } - self.save(); - } - - // === Delete === - pub fn delete_media(&self, path: &str) { - if path.starts_with("http://") || path.starts_with("https://") { - self.remove_url_media(path); - } - self.remove_from_playlist(path); - } -} - -// 全局共享实例 -pub static STORAGE: Lazy = Lazy::new(Storage::new); - -/// 判断文件扩展名是否允许 -pub fn is_allowed_extension(path: &str) -> bool { - let allowed = [".mp4", ".mkv", ".avi", ".jpg", ".jpeg", ".png"]; - let lower = path.to_lowercase(); - allowed.iter().any(|ext| lower.ends_with(ext)) -} - -/// 获取媒体类型 -pub fn get_media_type(path: &str) -> &'static str { - let lower = path.to_lowercase(); - if lower.ends_with(".mp4") || lower.ends_with(".mkv") || lower.ends_with(".avi") { - "video" - } else { - "image" - } -}