Files
Nekosonic-Music/src-tauri/src/lib.rs
T
atdunbg 1fddbeab77 feat: v0.9.0 多音源系统、歌曲标签、搜索建议、播放器改进
音源系统:
- 新增可配置多音源(波点/酷狗/酷我),支持启用/禁用/排序/自定义源
- 酷我 kwDES 加密 + mobi.s 接口绕过 VIP 限制
- VIP/版权歌曲跨源 fallback,版本一致性匹配(原版/DJ/Live/混音)
- 多源聚合搜索,歌名+歌手组合去重

歌曲标签:
- 付费/音质/版本/音源标签,网易云 fee 字段精确判断
- privilege 数组合并(歌手/歌单/专辑/每日推荐/收藏页)

搜索建议:
- 120ms 防抖 + compositionend 支持,中文输入实时建议
- Teleport + fixed 定位避免父容器裁剪

播放器:
- 网络流 seek 支持(SharedBuffer 下载完成后 byte_len 可用)
- 切歌停止旧播放再加载新歌,进度条起步归零
- 切歌加载状态可视化,按钮禁用反馈
- tick generation 防竞态,旧 tick 不覆盖新歌位置
- 系统媒体控制(跨平台,Windows 增加 hwnd 就绪检查避免 panic)

其他:
- 设置迁移 v7(恢复酷我音源)
- useUpdater 模块级共享状态
2026-07-02 09:54:45 +08:00

233 lines
8.8 KiB
Rust

use tauri::{
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
menu::{MenuBuilder, MenuItemBuilder, PredefinedMenuItem},
Manager, Emitter,
};
use std::sync::atomic::{AtomicBool, Ordering};
mod api;
mod audio;
mod media_controls;
mod music_sources;
use api::ApiController;
use audio::AppAudio;
static ALLOW_EXIT: AtomicBool = AtomicBool::new(false);
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let app_data_dir = app.path().app_data_dir().expect("无法获取应用数据目录");
let api_controller = ApiController::new(app_data_dir);
app.manage(api_controller);
let audio_controller = audio::AudioController::new(app.handle().clone());
let app_audio = AppAudio(std::sync::Mutex::new(audio_controller));
app.manage(app_audio);
#[cfg(target_os = "windows")]
{
use raw_window_handle::HasWindowHandle;
use raw_window_handle::RawWindowHandle;
let hwnd = if let Some(win) = app.get_webview_window("main") {
win.window_handle().ok().and_then(|h| {
if let RawWindowHandle::Win32(h) = h.as_raw() {
Some(h.hwnd.get() as *mut std::ffi::c_void)
} else {
None
}
})
} else {
None
};
// Windows 上 souvlaki 要求 hwnd 必须存在,否则 panic
// 窗口尚未创建时跳过媒体控制初始化(不阻塞 app 启动)
if hwnd.is_some() {
media_controls::start_media_controls(app.handle().clone(), hwnd);
} else {
eprintln!("[media_controls] 窗口未就绪,跳过媒体控制初始化");
}
}
#[cfg(not(target_os = "windows"))]
media_controls::start_media_controls(app.handle().clone(), None);
let show = MenuItemBuilder::with_id("show", "显示窗口").build(app)?;
let _sep1 = PredefinedMenuItem::separator(app)?;
let prev = MenuItemBuilder::with_id("prev", "上一首").build(app)?;
let play_pause = MenuItemBuilder::with_id("play_pause", "播放/暂停").build(app)?;
let next = MenuItemBuilder::with_id("next", "下一首").build(app)?;
let _sep2 = PredefinedMenuItem::separator(app)?;
let quit = MenuItemBuilder::with_id("quit", "退出").build(app)?;
let menu = MenuBuilder::new(app)
.item(&show)
.separator()
.items(&[&prev, &play_pause, &next])
.separator()
.item(&quit)
.build()?;
let icon = app.default_window_icon().cloned().unwrap();
let _tray = TrayIconBuilder::with_id("main-tray")
.tooltip("Nekosonic Music")
.icon(icon)
.show_menu_on_left_click(false)
.menu(&menu)
.on_menu_event(|app, event| {
match event.id().as_ref() {
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
let _ = app.emit("window-shown", ());
}
}
"play_pause" => {
let _ = app.emit("tray-play-pause", ());
}
"next" => {
let _ = app.emit("tray-next", ());
}
"prev" => {
let _ = app.emit("tray-prev", ());
}
"quit" => {
ALLOW_EXIT.store(true, Ordering::SeqCst);
if let Some(w) = app.get_webview_window("main") {
let _ = w.close();
}
}
_ => {}
}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
let _ = app.emit("window-shown", ());
}
}
})
.build(app)?;
let window = app.get_webview_window("main").unwrap();
let window_clone = window.clone();
let app_handle = app.handle().clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::CloseRequested { api: close_api, .. } = event {
if ALLOW_EXIT.load(Ordering::SeqCst) {
return;
}
close_api.prevent_close();
let _ = window_clone.hide();
let _ = app_handle.emit("window-hidden", ());
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
api::login,
api::logout,
api::search_songs,
api::cloudsearch,
api::search_suggest,
api::get_song_url,
api::search_songs_multi,
api::get_external_song_url,
api::get_hot_search,
api::get_playlist_detail,
api::get_lyric,
api::user_playlist,
api::recommend_resource,
api::recommend_songs,
api::personalized,
api::personalized_newsong,
api::top_artists,
api::top_song,
api::album_newest,
api::personal_fm,
api::personal_fm_mode,
api::fm_trash,
api::scrobble,
api::get_song_detail,
api::get_qr_key,
api::create_qr,
api::check_qr_status,
api::get_login_status,
api::likelist,
api::user_record,
api::like_song,
api::record_recent_song,
api::playlist_subscribe,
api::playlist_track_all,
api::exit_app,
audio::commands::play_audio,
audio::commands::play_local_audio,
audio::commands::pause_audio,
audio::commands::resume_audio,
audio::commands::stop_audio,
audio::commands::get_output_devices,
audio::commands::set_output_device,
audio::commands::seek_audio,
audio::commands::get_audio_position,
audio::commands::set_volume,
audio::commands::is_audio_playing,
api::download_song,
api::list_local_songs,
api::scan_local_folders,
api::delete_local_song,
api::check_local_song,
api::get_default_download_path,
api::artist_detail,
api::artist_songs,
api::artist_album,
api::artist_desc,
api::artist_sub,
api::artist_sublist,
api::album_detail,
api::comment_new,
api::comment_hot,
api::comment_floor,
api::comment_like,
api::user_cloud,
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())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
let _ = window.unminimize();
let _ = app.emit("window-shown", ());
}
}))
.run(tauri::generate_context!())
.expect("error while running Nekosonic");
}