diff --git a/src-tauri/src/api.rs b/src-tauri/src/api.rs index 15dff53..20ca024 100644 --- a/src-tauri/src/api.rs +++ b/src-tauri/src/api.rs @@ -392,12 +392,51 @@ pub struct ScrobbleQuery { pub id: u64, pub sourceid: Option, pub time: u64, + pub alg: Option, + pub source: Option, + pub bitrate: Option, } /// 听歌打卡 #[tauri::command] pub async fn scrobble(query: ScrobbleQuery, state: State<'_, ApiController>) -> Result { - 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 { + 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 { diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index bf27eac..2585223 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -1066,10 +1066,14 @@ fn audio_thread(rx: Receiver, _current_url: Arc>> 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); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1c5b936..dc5ca39 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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()) diff --git a/src/App.vue b/src/App.vue index 50f4051..b669b1a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,24 @@ diff --git a/src/components/PlayerBar.vue b/src/components/PlayerBar.vue index d45e33f..19290bc 100644 --- a/src/components/PlayerBar.vue +++ b/src/components/PlayerBar.vue @@ -1,6 +1,7 @@