mirror of
https://github.com/atdunbg/Nekosonic-Music.git
synced 2026-08-06 03:44:18 +08:00
音源系统: - 新增可配置多音源(波点/酷狗/酷我),支持启用/禁用/排序/自定义源 - 酷我 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 模块级共享状态
233 lines
8.8 KiB
Rust
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");
|
|
}
|