初步完成框架
This commit is contained in:
220
src-tauri/Cargo.lock
generated
220
src-tauri/Cargo.lock
generated
@@ -56,6 +56,56 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
@@ -488,6 +538,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@@ -678,6 +734,12 @@ dependencies = [
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
@@ -874,6 +936,29 @@ dependencies = [
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -1764,6 +1849,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
@@ -1793,6 +1884,30 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
@@ -2172,21 +2287,6 @@ dependencies = [
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nuxtor"
|
||||
version = "1.4.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-store",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
@@ -2416,6 +2516,12 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.3.2"
|
||||
@@ -2736,6 +2842,21 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
@@ -3349,6 +3470,17 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
@@ -4121,7 +4253,9 @@ dependencies = [
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
@@ -4139,6 +4273,18 @@ dependencies = [
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.15"
|
||||
@@ -4358,6 +4504,24 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"sha1",
|
||||
"thiserror 1.0.69",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
@@ -4470,6 +4634,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.17.0"
|
||||
@@ -4494,6 +4664,26 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "video-controller"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-store",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vswhom"
|
||||
version = "0.1.0"
|
||||
|
@@ -1,14 +1,14 @@
|
||||
[package]
|
||||
name = "nuxtor"
|
||||
version = "1.4.0"
|
||||
description = "Starter template for Nuxt 3 and Tauri 2"
|
||||
authors = [ "NicolaSpadari" ]
|
||||
name = "video-controller"
|
||||
version = "1.0.0"
|
||||
description = "Video Player Controller Application"
|
||||
authors = [ "estel" ]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/NicolaSpadari/nuxtor"
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "nuxtor_lib"
|
||||
name = "video_controller_lib"
|
||||
crate-type = [
|
||||
"staticlib",
|
||||
"cdylib",
|
||||
@@ -26,6 +26,11 @@ tauri-plugin-os = "2.3.0"
|
||||
tauri-plugin-fs = "2.4.0"
|
||||
tauri-plugin-store = "2.3.0"
|
||||
serde_json = "1"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tokio-tungstenite = "0.23"
|
||||
futures-util = "0.3"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
|
||||
[dependencies.tauri]
|
||||
version = "2.6.2"
|
||||
|
103
src-tauri/src/api/connection.rs
Normal file
103
src-tauri/src/api/connection.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use crate::models::{ApiResponse, ConnectionConfig, PlaybackCommand, PlayerState};
|
||||
use crate::services::ConnectionService;
|
||||
use std::sync::Arc;
|
||||
use tauri::{Emitter, State, Window};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
type ConnectionState = Arc<Mutex<Option<ConnectionService>>>;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn connect_to_player(
|
||||
connection_state: State<'_, ConnectionState>,
|
||||
config: ConnectionConfig,
|
||||
) -> Result<ApiResponse<PlayerState>, String> {
|
||||
let mut service = ConnectionService::new(config.clone());
|
||||
|
||||
match service.connect().await {
|
||||
Ok(_) => {
|
||||
let state = service.get_state().await;
|
||||
*connection_state.lock().await = Some(service);
|
||||
Ok(ApiResponse::success_with_message(
|
||||
state,
|
||||
"成功连接到视频播放器".to_string(),
|
||||
))
|
||||
}
|
||||
Err(e) => Ok(ApiResponse::error(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn disconnect_from_player(
|
||||
connection_state: State<'_, ConnectionState>,
|
||||
) -> Result<ApiResponse<()>, String> {
|
||||
let mut guard = connection_state.lock().await;
|
||||
|
||||
if let Some(service) = guard.as_mut() {
|
||||
service.disconnect().await;
|
||||
*guard = None;
|
||||
Ok(ApiResponse::success_with_message(
|
||||
(),
|
||||
"已断开连接".to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok(ApiResponse::error("未建立连接".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_connection_status(
|
||||
connection_state: State<'_, ConnectionState>,
|
||||
) -> Result<ApiResponse<PlayerState>, String> {
|
||||
let guard = connection_state.lock().await;
|
||||
|
||||
if let Some(service) = guard.as_ref() {
|
||||
let state = service.get_state().await;
|
||||
Ok(ApiResponse::success(state))
|
||||
} else {
|
||||
Ok(ApiResponse::error("未建立连接".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_connection_config(
|
||||
connection_state: State<'_, ConnectionState>,
|
||||
config: ConnectionConfig,
|
||||
) -> Result<ApiResponse<()>, String> {
|
||||
let guard = connection_state.lock().await;
|
||||
|
||||
if let Some(service) = guard.as_ref() {
|
||||
service.update_config(config).await;
|
||||
Ok(ApiResponse::success_with_message(
|
||||
(),
|
||||
"连接配置已更新".to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok(ApiResponse::error("未建立连接".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn send_playback_command(
|
||||
connection_state: State<'_, ConnectionState>,
|
||||
command: PlaybackCommand,
|
||||
window: Window,
|
||||
) -> Result<ApiResponse<()>, String> {
|
||||
let guard = connection_state.lock().await;
|
||||
|
||||
if let Some(service) = guard.as_ref() {
|
||||
match service.send_playback_command(command.clone()).await {
|
||||
Ok(_) => {
|
||||
// 发送事件到前端,通知命令已发送
|
||||
let _ = window.emit("playback-command-sent", &command);
|
||||
|
||||
Ok(ApiResponse::success_with_message(
|
||||
(),
|
||||
"命令发送成功".to_string(),
|
||||
))
|
||||
}
|
||||
Err(e) => Ok(ApiResponse::error(e)),
|
||||
}
|
||||
} else {
|
||||
Ok(ApiResponse::error("未建立连接".to_string()))
|
||||
}
|
||||
}
|
160
src-tauri/src/api/files.rs
Normal file
160
src-tauri/src/api/files.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use crate::models::{ApiResponse, VideoInfo};
|
||||
use std::path::PathBuf;
|
||||
use tauri::AppHandle;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn select_video_files(
|
||||
_app_handle: AppHandle,
|
||||
) -> Result<ApiResponse<Vec<String>>, String> {
|
||||
// TODO: 在 Tauri 2 中实现文件对话框
|
||||
// 暂时返回示例数据
|
||||
let example_files = vec![
|
||||
"/path/to/example1.mp4".to_string(),
|
||||
"/path/to/example2.mp4".to_string(),
|
||||
];
|
||||
|
||||
Ok(ApiResponse::success_with_message(
|
||||
example_files,
|
||||
"请使用前端界面选择文件".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn select_single_video_file(
|
||||
_app_handle: AppHandle,
|
||||
) -> Result<ApiResponse<String>, String> {
|
||||
// TODO: 在 Tauri 2 中实现文件对话框
|
||||
// 暂时返回示例数据
|
||||
Ok(ApiResponse::success_with_message(
|
||||
"/path/to/example.mp4".to_string(),
|
||||
"请使用前端界面选择文件".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_video_info(file_path: String) -> Result<ApiResponse<VideoInfo>, String> {
|
||||
let path = PathBuf::from(&file_path);
|
||||
|
||||
// 检查文件是否存在
|
||||
if !path.exists() {
|
||||
return Ok(ApiResponse::error("文件不存在".to_string()));
|
||||
}
|
||||
|
||||
// 获取文件基本信息
|
||||
let metadata = std::fs::metadata(&path)
|
||||
.map_err(|e| format!("无法读取文件信息: {}", e))?;
|
||||
|
||||
let file_name = path
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let file_extension = path
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let video_info = VideoInfo {
|
||||
path: file_path,
|
||||
title: file_name,
|
||||
duration: None, // TODO: 使用 ffprobe 或类似工具获取视频时长
|
||||
size: Some(metadata.len()),
|
||||
format: Some(file_extension),
|
||||
};
|
||||
|
||||
Ok(ApiResponse::success(video_info))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn validate_video_files(
|
||||
file_paths: Vec<String>,
|
||||
) -> Result<ApiResponse<Vec<VideoInfo>>, String> {
|
||||
let mut valid_videos = Vec::new();
|
||||
let mut invalid_count = 0;
|
||||
|
||||
for file_path in file_paths {
|
||||
match get_video_info(file_path.clone()).await {
|
||||
Ok(response) => {
|
||||
if response.success {
|
||||
if let Some(video_info) = response.data {
|
||||
valid_videos.push(video_info);
|
||||
}
|
||||
} else {
|
||||
invalid_count += 1;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
invalid_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let message = if invalid_count > 0 {
|
||||
format!("验证完成,{} 个有效文件,{} 个无效文件", valid_videos.len(), invalid_count)
|
||||
} else {
|
||||
format!("所有 {} 个文件均有效", valid_videos.len())
|
||||
};
|
||||
|
||||
Ok(ApiResponse::success_with_message(valid_videos, message))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_supported_video_formats() -> Result<ApiResponse<Vec<String>>, String> {
|
||||
let formats = vec![
|
||||
"mp4".to_string(),
|
||||
"avi".to_string(),
|
||||
"mkv".to_string(),
|
||||
"mov".to_string(),
|
||||
"wmv".to_string(),
|
||||
"flv".to_string(),
|
||||
"webm".to_string(),
|
||||
"m4v".to_string(),
|
||||
"3gp".to_string(),
|
||||
"ogv".to_string(),
|
||||
];
|
||||
|
||||
Ok(ApiResponse::success(formats))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_file_location(file_path: String) -> Result<ApiResponse<()>, String> {
|
||||
let path = PathBuf::from(&file_path);
|
||||
|
||||
if !path.exists() {
|
||||
return Ok(ApiResponse::error("文件不存在".to_string()));
|
||||
}
|
||||
|
||||
// 在文件资源管理器中显示文件
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
std::process::Command::new("explorer")
|
||||
.args(["/select,", &file_path])
|
||||
.spawn()
|
||||
.map_err(|e| format!("无法打开文件位置: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
std::process::Command::new("open")
|
||||
.args(["-R", &file_path])
|
||||
.spawn()
|
||||
.map_err(|e| format!("无法打开文件位置: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Some(parent) = path.parent() {
|
||||
std::process::Command::new("xdg-open")
|
||||
.arg(parent)
|
||||
.spawn()
|
||||
.map_err(|e| format!("无法打开文件位置: {}", e))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ApiResponse::success_with_message(
|
||||
(),
|
||||
"文件位置已打开".to_string(),
|
||||
))
|
||||
}
|
7
src-tauri/src/api/mod.rs
Normal file
7
src-tauri/src/api/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod connection;
|
||||
pub mod settings;
|
||||
pub mod files;
|
||||
|
||||
pub use connection::*;
|
||||
pub use settings::*;
|
||||
pub use files::*;
|
88
src-tauri/src/api/settings.rs
Normal file
88
src-tauri/src/api/settings.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use crate::models::{ApiResponse, AppSettings};
|
||||
use crate::services::SettingsService;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_app_settings(
|
||||
settings_service: State<'_, SettingsService>,
|
||||
) -> Result<ApiResponse<AppSettings>, String> {
|
||||
match settings_service.load_settings().await {
|
||||
Ok(settings) => Ok(ApiResponse::success(settings)),
|
||||
Err(e) => Ok(ApiResponse::error(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_app_settings(
|
||||
settings_service: State<'_, SettingsService>,
|
||||
settings: AppSettings,
|
||||
) -> Result<ApiResponse<()>, String> {
|
||||
match settings_service.save_settings(&settings).await {
|
||||
Ok(_) => Ok(ApiResponse::success_with_message(
|
||||
(),
|
||||
"设置保存成功".to_string(),
|
||||
)),
|
||||
Err(e) => Ok(ApiResponse::error(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reset_app_settings(
|
||||
settings_service: State<'_, SettingsService>,
|
||||
) -> Result<ApiResponse<AppSettings>, String> {
|
||||
match settings_service.reset_settings().await {
|
||||
Ok(settings) => Ok(ApiResponse::success_with_message(
|
||||
settings,
|
||||
"设置已重置为默认值".to_string(),
|
||||
)),
|
||||
Err(e) => Ok(ApiResponse::error(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn export_settings_to_file(
|
||||
settings_service: State<'_, SettingsService>,
|
||||
_app_handle: AppHandle,
|
||||
) -> Result<ApiResponse<String>, String> {
|
||||
// TODO: 在 Tauri 2 中实现文件对话框
|
||||
// 暂时使用固定路径导出
|
||||
let export_path = std::env::temp_dir().join("video_controller_settings.json");
|
||||
|
||||
match settings_service.export_settings(export_path.clone()).await {
|
||||
Ok(_) => Ok(ApiResponse::success_with_message(
|
||||
export_path.to_string_lossy().to_string(),
|
||||
"设置导出成功".to_string(),
|
||||
)),
|
||||
Err(e) => Ok(ApiResponse::error(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn import_settings_from_file(
|
||||
settings_service: State<'_, SettingsService>,
|
||||
_app_handle: AppHandle,
|
||||
) -> Result<ApiResponse<AppSettings>, String> {
|
||||
// TODO: 在 Tauri 2 中实现文件对话框
|
||||
// 暂时从固定路径导入
|
||||
let import_path = std::env::temp_dir().join("video_controller_settings.json");
|
||||
|
||||
if !import_path.exists() {
|
||||
return Ok(ApiResponse::error("没有找到设置文件".to_string()));
|
||||
}
|
||||
|
||||
match settings_service.import_settings(import_path.clone()).await {
|
||||
Ok(settings) => Ok(ApiResponse::success_with_message(
|
||||
settings,
|
||||
format!("从 {} 导入设置成功", import_path.to_string_lossy()),
|
||||
)),
|
||||
Err(e) => Ok(ApiResponse::error(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_settings_file_path(
|
||||
settings_service: State<'_, SettingsService>,
|
||||
) -> Result<ApiResponse<String>, String> {
|
||||
let path = settings_service.get_settings_path().await;
|
||||
Ok(ApiResponse::success(path.to_string_lossy().to_string()))
|
||||
}
|
@@ -1,15 +1,29 @@
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
|
||||
mod api;
|
||||
mod models;
|
||||
mod services;
|
||||
|
||||
use api::*;
|
||||
use services::{ConnectionService, SettingsService};
|
||||
use std::sync::Arc;
|
||||
use tauri::{
|
||||
menu::{Menu, MenuItem},
|
||||
tray::TrayIconBuilder
|
||||
tray::TrayIconBuilder,
|
||||
Manager,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub fn run() {
|
||||
// 初始化日志
|
||||
env_logger::init();
|
||||
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
|
||||
let menu = Menu::with_items(app, &[&quit_i])?;
|
||||
// 创建系统托盘
|
||||
let quit_i = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
|
||||
let show_i = MenuItem::with_id(app, "show", "显示窗口", true, None::<&str>)?;
|
||||
let menu = Menu::with_items(app, &[&show_i, &quit_i])?;
|
||||
|
||||
let _tray = TrayIconBuilder::new()
|
||||
.menu(&menu)
|
||||
@@ -19,19 +33,57 @@ pub fn run() {
|
||||
"quit" => {
|
||||
app.exit(0);
|
||||
}
|
||||
"show" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
other => {
|
||||
println!("menu item {} not handled", other);
|
||||
log::warn!("未处理的菜单事件: {}", other);
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
// 初始化服务
|
||||
let settings_service = SettingsService::new(app.handle().clone());
|
||||
app.manage(settings_service);
|
||||
|
||||
// 初始化连接状态
|
||||
let connection_state: Arc<Mutex<Option<ConnectionService>>> = Arc::new(Mutex::new(None));
|
||||
app.manage(connection_state);
|
||||
|
||||
log::info!("应用初始化完成");
|
||||
Ok(())
|
||||
})
|
||||
// 注册 API 命令
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// 连接相关
|
||||
connect_to_player,
|
||||
disconnect_from_player,
|
||||
get_connection_status,
|
||||
update_connection_config,
|
||||
send_playback_command,
|
||||
// 设置相关
|
||||
load_app_settings,
|
||||
save_app_settings,
|
||||
reset_app_settings,
|
||||
export_settings_to_file,
|
||||
import_settings_from_file,
|
||||
get_settings_file_path,
|
||||
// 文件操作相关
|
||||
select_video_files,
|
||||
select_single_video_file,
|
||||
get_video_info,
|
||||
validate_video_files,
|
||||
get_supported_video_formats,
|
||||
open_file_location,
|
||||
])
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
.expect("启动 Tauri 应用程序时出错");
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
nuxtor_lib::run();
|
||||
}
|
||||
video_controller_lib::run();
|
||||
}
|
||||
|
186
src-tauri/src/models/mod.rs
Normal file
186
src-tauri/src/models/mod.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// 视频播放器连接状态
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ConnectionStatus {
|
||||
Connected,
|
||||
Connecting,
|
||||
Disconnected,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
// 播放状态
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PlaybackStatus {
|
||||
Playing,
|
||||
Paused,
|
||||
Stopped,
|
||||
Loading,
|
||||
}
|
||||
|
||||
// 视频信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VideoInfo {
|
||||
pub path: String,
|
||||
pub title: String,
|
||||
pub duration: Option<f64>,
|
||||
pub size: Option<u64>,
|
||||
pub format: Option<String>,
|
||||
}
|
||||
|
||||
// 播放器状态
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlayerState {
|
||||
pub connection_status: ConnectionStatus,
|
||||
pub playback_status: PlaybackStatus,
|
||||
pub current_video: Option<VideoInfo>,
|
||||
pub position: f64,
|
||||
pub duration: f64,
|
||||
pub volume: f64,
|
||||
pub is_looping: bool,
|
||||
pub is_fullscreen: bool,
|
||||
pub playlist: Vec<VideoInfo>,
|
||||
pub current_playlist_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for PlayerState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
connection_status: ConnectionStatus::Disconnected,
|
||||
playback_status: PlaybackStatus::Stopped,
|
||||
current_video: None,
|
||||
position: 0.0,
|
||||
duration: 0.0,
|
||||
volume: 50.0,
|
||||
is_looping: false,
|
||||
is_fullscreen: false,
|
||||
playlist: Vec::new(),
|
||||
current_playlist_index: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 连接配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConnectionConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub timeout: u64,
|
||||
pub auto_reconnect: bool,
|
||||
pub reconnect_interval: u64,
|
||||
}
|
||||
|
||||
impl Default for ConnectionConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host: "192.168.1.100".to_string(),
|
||||
port: 8080,
|
||||
timeout: 10,
|
||||
auto_reconnect: true,
|
||||
reconnect_interval: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应用设置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppSettings {
|
||||
pub connection: ConnectionConfig,
|
||||
pub default_volume: f64,
|
||||
pub default_loop: bool,
|
||||
pub auto_fullscreen: bool,
|
||||
pub playback_end_behavior: PlaybackEndBehavior,
|
||||
pub theme: String,
|
||||
pub language: String,
|
||||
pub show_notifications: bool,
|
||||
pub debug_mode: bool,
|
||||
pub cache_size: u64,
|
||||
pub proxy: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PlaybackEndBehavior {
|
||||
Stop,
|
||||
Next,
|
||||
Repeat,
|
||||
}
|
||||
|
||||
impl Default for AppSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
connection: ConnectionConfig::default(),
|
||||
default_volume: 50.0,
|
||||
default_loop: false,
|
||||
auto_fullscreen: false,
|
||||
playback_end_behavior: PlaybackEndBehavior::Stop,
|
||||
theme: "system".to_string(),
|
||||
language: "zh-CN".to_string(),
|
||||
show_notifications: true,
|
||||
debug_mode: false,
|
||||
cache_size: 100,
|
||||
proxy: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 播放控制命令
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PlaybackCommand {
|
||||
Play,
|
||||
Pause,
|
||||
Stop,
|
||||
Seek { position: f64 },
|
||||
SetVolume { volume: f64 },
|
||||
SetLoop { enabled: bool },
|
||||
ToggleFullscreen,
|
||||
LoadVideo { path: String },
|
||||
SetPlaylist { videos: Vec<String> },
|
||||
PlayFromPlaylist { index: usize },
|
||||
}
|
||||
|
||||
// API 响应
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApiResponse<T> {
|
||||
pub success: bool,
|
||||
pub data: Option<T>,
|
||||
pub message: Option<String>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> ApiResponse<T> {
|
||||
pub fn success(data: T) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
data: Some(data),
|
||||
message: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn success_with_message(data: T, message: String) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
data: Some(data),
|
||||
message: Some(message),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(error: String) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
data: None,
|
||||
message: None,
|
||||
error: Some(error),
|
||||
}
|
||||
}
|
||||
}
|
310
src-tauri/src/services/connection.rs
Normal file
310
src-tauri/src/services/connection.rs
Normal file
@@ -0,0 +1,310 @@
|
||||
use crate::models::{ConnectionConfig, ConnectionStatus, PlaybackCommand, PlayerState};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use log::{debug, error, info, warn};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex, RwLock};
|
||||
use tokio::time::{interval, sleep, Duration};
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message, WebSocketStream, MaybeTlsStream};
|
||||
|
||||
pub type WebSocket = WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConnectionService {
|
||||
config: Arc<RwLock<ConnectionConfig>>,
|
||||
state: Arc<RwLock<PlayerState>>,
|
||||
tx: Option<mpsc::UnboundedSender<PlaybackCommand>>,
|
||||
websocket: Arc<Mutex<Option<WebSocket>>>,
|
||||
}
|
||||
|
||||
impl ConnectionService {
|
||||
pub fn new(config: ConnectionConfig) -> Self {
|
||||
Self {
|
||||
config: Arc::new(RwLock::new(config)),
|
||||
state: Arc::new(RwLock::new(PlayerState::default())),
|
||||
tx: None,
|
||||
websocket: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self) -> Result<(), String> {
|
||||
let config = self.config.read().await.clone();
|
||||
let url = format!("ws://{}:{}/ws", config.host, config.port);
|
||||
|
||||
info!("尝试连接到视频播放器: {}", url);
|
||||
|
||||
// 更新连接状态为连接中
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
state.connection_status = ConnectionStatus::Connecting;
|
||||
}
|
||||
|
||||
match self.establish_connection(&url, config.timeout).await {
|
||||
Ok(ws_stream) => {
|
||||
info!("成功连接到视频播放器");
|
||||
|
||||
// 存储 WebSocket 连接
|
||||
*self.websocket.lock().await = Some(ws_stream);
|
||||
|
||||
// 更新连接状态
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
state.connection_status = ConnectionStatus::Connected;
|
||||
}
|
||||
|
||||
// 启动消息处理
|
||||
self.start_message_handling().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("连接失败: {}", e);
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
state.connection_status = ConnectionStatus::Error(e.clone());
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn establish_connection(&self, url: &str, timeout_secs: u64) -> Result<WebSocket, String> {
|
||||
// 直接使用字符串 URL,不转换为 Url 类型
|
||||
let connection_future = connect_async(url);
|
||||
let timeout_future = sleep(Duration::from_secs(timeout_secs));
|
||||
|
||||
tokio::select! {
|
||||
result = connection_future => {
|
||||
match result {
|
||||
Ok((ws_stream, _)) => Ok(ws_stream),
|
||||
Err(e) => Err(format!("WebSocket连接错误: {}", e)),
|
||||
}
|
||||
}
|
||||
_ = timeout_future => {
|
||||
Err(format!("连接超时 ({} 秒)", timeout_secs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_message_handling(&mut self) {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<PlaybackCommand>();
|
||||
self.tx = Some(tx);
|
||||
|
||||
let websocket = self.websocket.clone();
|
||||
let state = self.state.clone();
|
||||
let config = self.config.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
// 心跳检测
|
||||
let mut heartbeat_interval = interval(Duration::from_secs(30));
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// 处理发送的命令
|
||||
command = rx.recv() => {
|
||||
if let Some(cmd) = command {
|
||||
if let Err(e) = Self::send_command(&websocket, cmd).await {
|
||||
error!("发送命令失败: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 心跳检测
|
||||
_ = heartbeat_interval.tick() => {
|
||||
if let Err(e) = Self::send_heartbeat(&websocket).await {
|
||||
warn!("心跳失败: {}", e);
|
||||
// 可能需要重连
|
||||
if config.read().await.auto_reconnect {
|
||||
// 触发重连逻辑
|
||||
Self::handle_reconnection(&websocket, &state, &config).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 接收播放器状态更新
|
||||
message = Self::receive_message(&websocket) => {
|
||||
match message {
|
||||
Ok(msg) => {
|
||||
if let Err(e) = Self::handle_player_message(&state, msg).await {
|
||||
error!("处理播放器消息失败: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("接收消息失败: {}", e);
|
||||
if config.read().await.auto_reconnect {
|
||||
Self::handle_reconnection(&websocket, &state, &config).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn send_command(
|
||||
websocket: &Arc<Mutex<Option<WebSocket>>>,
|
||||
command: PlaybackCommand,
|
||||
) -> Result<(), String> {
|
||||
let mut ws_guard = websocket.lock().await;
|
||||
|
||||
if let Some(ws) = ws_guard.as_mut() {
|
||||
let command_json = serde_json::to_string(&command)
|
||||
.map_err(|e| format!("序列化命令失败: {}", e))?;
|
||||
|
||||
ws.send(Message::Text(command_json))
|
||||
.await
|
||||
.map_err(|e| format!("发送消息失败: {}", e))?;
|
||||
|
||||
debug!("发送命令: {:?}", command);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("WebSocket 连接不可用".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_heartbeat(websocket: &Arc<Mutex<Option<WebSocket>>>) -> Result<(), String> {
|
||||
let mut ws_guard = websocket.lock().await;
|
||||
|
||||
if let Some(ws) = ws_guard.as_mut() {
|
||||
ws.send(Message::Ping(vec![]))
|
||||
.await
|
||||
.map_err(|e| format!("发送心跳失败: {}", e))?;
|
||||
|
||||
debug!("发送心跳");
|
||||
Ok(())
|
||||
} else {
|
||||
Err("WebSocket 连接不可用".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn receive_message(
|
||||
websocket: &Arc<Mutex<Option<WebSocket>>>,
|
||||
) -> Result<String, String> {
|
||||
let mut ws_guard = websocket.lock().await;
|
||||
|
||||
if let Some(ws) = ws_guard.as_mut() {
|
||||
match ws.next().await {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
debug!("收到消息: {}", text);
|
||||
Ok(text)
|
||||
}
|
||||
Some(Ok(Message::Pong(_))) => {
|
||||
debug!("收到心跳响应");
|
||||
Ok("pong".to_string())
|
||||
}
|
||||
Some(Ok(_)) => {
|
||||
Err("收到非文本消息".to_string())
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
Err(format!("WebSocket 错误: {}", e))
|
||||
}
|
||||
None => {
|
||||
Err("WebSocket 连接关闭".to_string())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err("WebSocket 连接不可用".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_player_message(
|
||||
state: &Arc<RwLock<PlayerState>>,
|
||||
message: String,
|
||||
) -> Result<(), String> {
|
||||
if message == "pong" {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 尝试解析播放器状态更新
|
||||
match serde_json::from_str::<PlayerState>(&message) {
|
||||
Ok(new_state) => {
|
||||
let mut current_state = state.write().await;
|
||||
current_state.playback_status = new_state.playback_status;
|
||||
current_state.position = new_state.position;
|
||||
current_state.duration = new_state.duration;
|
||||
current_state.volume = new_state.volume;
|
||||
current_state.is_looping = new_state.is_looping;
|
||||
current_state.is_fullscreen = new_state.is_fullscreen;
|
||||
|
||||
if let Some(video) = new_state.current_video {
|
||||
current_state.current_video = Some(video);
|
||||
}
|
||||
|
||||
debug!("更新播放器状态");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("无法解析播放器状态: {} - 消息: {}", e, message);
|
||||
Ok(()) // 不是致命错误,继续运行
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_reconnection(
|
||||
websocket: &Arc<Mutex<Option<WebSocket>>>,
|
||||
state: &Arc<RwLock<PlayerState>>,
|
||||
config: &Arc<RwLock<ConnectionConfig>>,
|
||||
) {
|
||||
info!("开始重连流程");
|
||||
|
||||
// 清空当前连接
|
||||
{
|
||||
let mut ws_guard = websocket.lock().await;
|
||||
*ws_guard = None;
|
||||
}
|
||||
|
||||
// 更新状态为断开连接
|
||||
{
|
||||
let mut current_state = state.write().await;
|
||||
current_state.connection_status = ConnectionStatus::Disconnected;
|
||||
}
|
||||
|
||||
// 等待重连间隔
|
||||
let reconnect_interval = {
|
||||
let config_guard = config.read().await;
|
||||
config_guard.reconnect_interval
|
||||
};
|
||||
|
||||
sleep(Duration::from_secs(reconnect_interval)).await;
|
||||
|
||||
// 尝试重连 (这里需要重新创建 ConnectionService 实例)
|
||||
info!("尝试重新连接...");
|
||||
}
|
||||
|
||||
pub async fn disconnect(&mut self) {
|
||||
info!("断开连接");
|
||||
|
||||
// 关闭 WebSocket 连接
|
||||
{
|
||||
let mut ws_guard = self.websocket.lock().await;
|
||||
if let Some(ws) = ws_guard.as_mut() {
|
||||
let _ = ws.close(None).await;
|
||||
}
|
||||
*ws_guard = None;
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
state.connection_status = ConnectionStatus::Disconnected;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_playback_command(&self, command: PlaybackCommand) -> Result<(), String> {
|
||||
if let Some(tx) = &self.tx {
|
||||
tx.send(command)
|
||||
.map_err(|e| format!("发送命令到队列失败: {}", e))?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("连接服务未初始化".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_state(&self) -> PlayerState {
|
||||
self.state.read().await.clone()
|
||||
}
|
||||
|
||||
pub async fn update_config(&self, new_config: ConnectionConfig) {
|
||||
*self.config.write().await = new_config;
|
||||
}
|
||||
}
|
5
src-tauri/src/services/mod.rs
Normal file
5
src-tauri/src/services/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod connection;
|
||||
pub mod settings;
|
||||
|
||||
pub use connection::ConnectionService;
|
||||
pub use settings::SettingsService;
|
124
src-tauri/src/services/settings.rs
Normal file
124
src-tauri/src/services/settings.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use crate::models::AppSettings;
|
||||
use log::{error, info};
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
pub struct SettingsService {
|
||||
app_handle: AppHandle,
|
||||
store_path: PathBuf,
|
||||
}
|
||||
|
||||
impl SettingsService {
|
||||
pub fn new(app_handle: AppHandle) -> Self {
|
||||
let mut store_path = app_handle
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("."));
|
||||
store_path.push("settings.json");
|
||||
|
||||
Self {
|
||||
app_handle,
|
||||
store_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_settings(&self) -> Result<AppSettings, String> {
|
||||
match self.app_handle.store_builder(self.store_path.clone()).build() {
|
||||
Ok(store) => {
|
||||
info!("加载应用设置");
|
||||
|
||||
// 尝试从存储中读取设置
|
||||
let settings = match store.get("app_settings") {
|
||||
Some(value) => {
|
||||
match serde_json::from_value::<AppSettings>(value.clone()) {
|
||||
Ok(settings) => {
|
||||
info!("成功加载保存的设置");
|
||||
settings
|
||||
}
|
||||
Err(e) => {
|
||||
error!("解析设置失败: {}, 使用默认设置", e);
|
||||
AppSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
info!("未找到保存的设置,使用默认设置");
|
||||
AppSettings::default()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("无法创建设置存储: {}", e);
|
||||
// 即使存储创建失败,也返回默认设置
|
||||
Ok(AppSettings::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_settings(&self, settings: &AppSettings) -> Result<(), String> {
|
||||
match self.app_handle.store_builder(self.store_path.clone()).build() {
|
||||
Ok(store) => {
|
||||
info!("保存应用设置");
|
||||
|
||||
let settings_value = serde_json::to_value(settings)
|
||||
.map_err(|e| format!("序列化设置失败: {}", e))?;
|
||||
|
||||
store.set("app_settings".to_string(), settings_value);
|
||||
|
||||
if let Err(e) = store.save() {
|
||||
error!("保存设置到文件失败: {}", e);
|
||||
return Err(format!("保存设置失败: {}", e));
|
||||
}
|
||||
|
||||
info!("设置保存成功");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("无法创建设置存储: {}", e);
|
||||
Err(format!("创建设置存储失败: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn export_settings(&self, export_path: PathBuf) -> Result<(), String> {
|
||||
let settings = self.load_settings().await?;
|
||||
|
||||
let settings_json = serde_json::to_string_pretty(&settings)
|
||||
.map_err(|e| format!("序列化设置失败: {}", e))?;
|
||||
|
||||
std::fs::write(&export_path, settings_json)
|
||||
.map_err(|e| format!("写入文件失败: {}", e))?;
|
||||
|
||||
info!("设置导出到: {:?}", export_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn import_settings(&self, import_path: PathBuf) -> Result<AppSettings, String> {
|
||||
let content = std::fs::read_to_string(&import_path)
|
||||
.map_err(|e| format!("读取文件失败: {}", e))?;
|
||||
|
||||
let settings: AppSettings = serde_json::from_str(&content)
|
||||
.map_err(|e| format!("解析设置文件失败: {}", e))?;
|
||||
|
||||
// 保存导入的设置
|
||||
self.save_settings(&settings).await?;
|
||||
|
||||
info!("从文件导入设置: {:?}", import_path);
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
pub async fn reset_settings(&self) -> Result<AppSettings, String> {
|
||||
let default_settings = AppSettings::default();
|
||||
self.save_settings(&default_settings).await?;
|
||||
|
||||
info!("设置已重置为默认值");
|
||||
Ok(default_settings)
|
||||
}
|
||||
|
||||
pub async fn get_settings_path(&self) -> PathBuf {
|
||||
self.store_path.clone()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user