mirror of
https://github.com/atdunbg/Nekosonic-Music.git
synced 2026-06-22 00:58:51 +08:00
feat: 皮肤系统重构、seek暂停修复、本地音乐优化、外观一体化
- 重构皮肤系统:提取 skins.ts 管理预设皮肤,CSS 变量由 JS 动态设置 - 提取公共 color.ts 工具函数(hexToRgba/toHex),消除重复定义 - 修复 seek 时暂停状态丢失的 bug(后端 audio_paused 状态保留) - 本地音乐页面:循环排序切换、三点菜单、打开所在文件夹 - 本地音乐文件夹管理:支持启用/禁用切换,兼容旧数据迁移 - 新增 show_item_in_folder 命令(Windows/macOS/Linux 跨平台) - 外观一体化:有壁纸时 TitleBar/Sidebar 透明,PlayerBar 统一透明度+backdrop-blur - 进度条外层直角、内层填充圆角 - 滚动条默认透明,悬停时显示 - 移除 PageHeader 粘性栏 - 内存优化:keep-alive TTL 5min、pageCache TTL 30min/上限30条、colorCache 上限200 - recentLocal 防抖写入、播放器 tick interval 500ms
This commit is contained in:
@ -392,12 +392,51 @@ pub struct ScrobbleQuery {
|
||||
pub id: u64,
|
||||
pub sourceid: Option<String>,
|
||||
pub time: u64,
|
||||
pub alg: Option<String>,
|
||||
pub source: Option<String>,
|
||||
pub bitrate: Option<u64>,
|
||||
}
|
||||
|
||||
/// 听歌打卡
|
||||
#[tauri::command]
|
||||
pub async fn scrobble(query: ScrobbleQuery, state: State<'_, ApiController>) -> Result<String, String> {
|
||||
api_call!(state, scrobble, params: [("id", &query.id.to_string()), ("sourceid", query.sourceid.as_deref().unwrap_or("")), ("time", &query.time.to_string())])
|
||||
let client = state.client.lock().await.clone();
|
||||
let cookie = state.cookie.lock().ok().and_then(|g| g.clone()).unwrap_or_default();
|
||||
let option = ncm_api_rs::request::RequestOption {
|
||||
crypto: ncm_api_rs::request::CryptoType::Weapi,
|
||||
cookie: Some(cookie),
|
||||
ua: None,
|
||||
proxy: None,
|
||||
real_ip: None,
|
||||
random_cn_ip: false,
|
||||
e_r: None,
|
||||
domain: None,
|
||||
check_token: false,
|
||||
};
|
||||
let data = json!({
|
||||
"logs": serde_json::to_string(&json!([{
|
||||
"action": "play",
|
||||
"json": {
|
||||
"download": 0,
|
||||
"end": "playend",
|
||||
"id": query.id.to_string(),
|
||||
"sourceId": query.sourceid.as_deref().unwrap_or(""),
|
||||
"time": query.time as i64,
|
||||
"type": "song",
|
||||
"wifi": 0,
|
||||
"source": query.source.as_deref().unwrap_or("list"),
|
||||
"alg": query.alg.as_deref().unwrap_or(""),
|
||||
"bitrate": query.bitrate.unwrap_or(0),
|
||||
"mainsite": 1,
|
||||
"content": ""
|
||||
}
|
||||
}])).unwrap_or_default()
|
||||
});
|
||||
let result = client.request("/api/feedback/weblog", data.clone(), option)
|
||||
.await
|
||||
.map(|r| r.body.to_string())
|
||||
.map_err(|e| e.to_string());
|
||||
result
|
||||
}
|
||||
|
||||
/// 获取歌曲详情
|
||||
@ -854,6 +893,79 @@ fn sanitize_filename(name: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// 读取本地图片文件并转为 base64 data URL,供前端壁纸等场景使用
|
||||
#[tauri::command]
|
||||
pub async fn read_image_as_data_url(path: String) -> Result<String, String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let file_path = PathBuf::from(&path);
|
||||
if !file_path.exists() {
|
||||
return Err(format!("文件不存在: {}", path));
|
||||
}
|
||||
let bytes = fs::read(&file_path).map_err(|e| format!("读取文件失败: {}", e))?;
|
||||
let mime = match file_path.extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase().as_str() {
|
||||
"png" => "image/png",
|
||||
"jpg" | "jpeg" => "image/jpeg",
|
||||
"webp" => "image/webp",
|
||||
"gif" => "image/gif",
|
||||
"bmp" => "image/bmp",
|
||||
"svg" => "image/svg+xml",
|
||||
_ => "image/jpeg", // 默认 jpeg
|
||||
};
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
|
||||
Ok(format!("data:{};base64,{}", mime, b64))
|
||||
}).await.map_err(|e| format!("任务失败: {}", e))?
|
||||
}
|
||||
|
||||
/// 在系统文件管理器中显示指定文件(选中)
|
||||
#[tauri::command]
|
||||
pub fn show_item_in_folder(path: String) -> Result<(), String> {
|
||||
let p = PathBuf::from(&path);
|
||||
if !p.exists() {
|
||||
return Err(format!("文件不存在: {}", path));
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
std::process::Command::new("explorer")
|
||||
.args(["/select,", &p.to_string_lossy()])
|
||||
.spawn()
|
||||
.map_err(|e| format!("打开文件夹失败: {}", e))?;
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
std::process::Command::new("open")
|
||||
.args(["-R", &p.to_string_lossy()])
|
||||
.spawn()
|
||||
.map_err(|e| format!("打开文件夹失败: {}", e))?;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let uri = format!("file://{}", p.to_string_lossy());
|
||||
// 优先使用 freedesktop DBus FileManager1 接口(支持选中文件,Nautilus/Dolphin 等均实现)
|
||||
let dbus_ok = std::process::Command::new("dbus-send")
|
||||
.args([
|
||||
"--session",
|
||||
"--print-reply",
|
||||
"--dest=org.freedesktop.FileManager1",
|
||||
"/org/freedesktop/FileManager1",
|
||||
"org.freedesktop.FileManager1.ShowItems",
|
||||
&format!("array:string:{}", uri),
|
||||
"string:",
|
||||
])
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
// fallback:仅打开父目录(无法选中文件)
|
||||
if !dbus_ok {
|
||||
let parent = p.parent().unwrap_or(&p).to_string_lossy().to_string();
|
||||
std::process::Command::new("xdg-open")
|
||||
.arg(&parent)
|
||||
.spawn()
|
||||
.map_err(|e| format!("打开文件夹失败: {}", e))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取歌手详情
|
||||
#[tauri::command]
|
||||
pub async fn artist_detail(id: u64, state: State<'_, ApiController>) -> Result<String, String> {
|
||||
|
||||
@ -1066,10 +1066,14 @@ fn audio_thread(rx: Receiver<AudioCmd>, _current_url: Arc<Mutex<Option<String>>>
|
||||
let device = get_output_device(&selected_device);
|
||||
match start_playback(mss, &device, current_volume, Some(time)) {
|
||||
Ok(ctx) => {
|
||||
if audio_paused {
|
||||
is_playing.store(false, Ordering::Relaxed);
|
||||
ctx.playback.playing.store(false, Ordering::Relaxed);
|
||||
} else {
|
||||
is_playing.store(true, Ordering::Relaxed);
|
||||
}
|
||||
output_ctx = Some(ctx);
|
||||
audio_active = true;
|
||||
audio_paused = false;
|
||||
is_playing.store(true, Ordering::Relaxed);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[audio] seek 播放失败: {}", e);
|
||||
|
||||
@ -197,6 +197,8 @@ pub fn run() {
|
||||
api::user_cloud_detail,
|
||||
api::user_cloud_del,
|
||||
api::cloud_upload,
|
||||
api::read_image_as_data_url,
|
||||
api::show_item_in_folder,
|
||||
])
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
|
||||
Reference in New Issue
Block a user