mirror of
https://github.com/atdunbg/Nekosonic-Music.git
synced 2026-06-22 00:58:51 +08:00
## 后端 - 替换 rodio 为 symphonia + ringbuf,重构 audio.rs 播放引擎 - 重构 api.rs,使用 api_call! 宏统一 API 调用模式 - 新增 media_controls.rs,使用 souvlaki 实现跨平台系统媒体控制 (Linux MPRIS / Windows SMTC / macOS Now Playing) - 版本号升至 v0.5.0 ## 前端 - 新增 - 新增 SongListItem 通用组件 - 新增 useOnlineStatus composable,检测网络状态 - 新增 usePageCache composable,页面数据缓存与失效 - 新增 getCoverUrl()、formatDate() 工具函数 - 新增 emitPlaybackState() 同步播放状态到系统媒体控制 - 新增 mpris-command 事件监听,响应系统媒体控制命令 - 新增 Toast 离线/恢复在线提示 - 各页面新增断网恢复后自动重试加载 - 新增路由守卫:已登录用户访问 /login 重定向至首页 - 新增音量持久化(settings store + localStorage) - 新增禁用右键菜单与用户选择限制(输入框除外) ## 前端 - 变更 - Song 接口从 player.ts 迁移至 song.ts 并导出 - AlbumDetail/ArtistDetail/PlaylistDetail/RecentPlays/LocalMusic 迁移至 SongListItem - PlayerBar 队列列表迁移至 SongListItem,封面使用 getCoverUrl() - downloadSong 参数类型从内联对象改为 Song,使用 getCoverUrl() - 默认主题从 green 改为 blue,ThemeName 及相关列表中 blue 移至首位 - 全局快捷键从 Alt+Control 改为 Control+Alt 顺序 - formatShortcut 新增 KeyP → P 显示 - keep-alive 从 max=3 固定 include 改为 max=5 动态列表,窗口隐藏时释放 - App.vue 封面使用 getCoverUrl() 替代手动 al/album 回退 - formatPlayCount 提取常量 - Login.vue text-warning 改为 text-yellow-400 ## 前端 - 删除 - 删除 Search.vue(与 Discover.vue 重复) - 删除 SongItemMenu.vue(被 SongListItem 替代) ## 修复 - 更新器跳过版本逻辑:仅静默检查时跳过已忽略版本,手动检查不再跳过 - 重复播放同一首歌时无法恢复播放 - settings.ts 重复的 ThemeName 定义 - PlayerBar.vue modeTexts 缺少类型注解 - Home.vue map 回调参数缺少类型 - Settings.vue v-for key 类型不匹配
676 lines
18 KiB
TypeScript
676 lines
18 KiB
TypeScript
import { defineStore } from 'pinia';
|
|
import { ref, watch, nextTick } from 'vue';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { normalizeSong, type Song } from '../utils/song';
|
|
import { useSettingsStore } from './settings';
|
|
import { useUserStore } from './user';
|
|
import { showToast } from '../composables/useToast';
|
|
|
|
export type PlayMode = 'loop' | 'shuffle' | 'repeat-one';
|
|
export type { Song };
|
|
|
|
import { listen, emit } from '@tauri-apps/api/event';
|
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
|
|
|
function loadRecentLocal(): Song[] {
|
|
try {
|
|
const raw = localStorage.getItem('recent_local');
|
|
if (raw) return JSON.parse(raw);
|
|
} catch {}
|
|
return [];
|
|
}
|
|
|
|
function loadLikedIdsFromStorage(): Set<number> {
|
|
try {
|
|
const raw = localStorage.getItem('liked_ids');
|
|
if (raw) return new Set(JSON.parse(raw));
|
|
} catch {}
|
|
return new Set();
|
|
}
|
|
|
|
export const usePlayerStore = defineStore('player', () => {
|
|
const currentSong = ref<Song | null>(null);
|
|
const playing = ref(false);
|
|
const currentTime = ref(0);
|
|
const duration = ref(0);
|
|
|
|
const queue = ref<Song[]>([]);
|
|
const currentIndex = ref(-1);
|
|
|
|
const settings = useSettingsStore();
|
|
const volume = ref(settings.volume);
|
|
|
|
watch(volume, (val) => { settings.volume = val; });
|
|
|
|
let tickInterval: ReturnType<typeof setInterval> | null = null;
|
|
function setTickInterval(v: ReturnType<typeof setInterval> | null) { _tickInterval = v; tickInterval = v; }
|
|
|
|
const recentLocal = ref<Song[]>(loadRecentLocal());
|
|
const MAX_RECENT = 200;
|
|
|
|
const likedIds = ref<Set<number>>(loadLikedIdsFromStorage());
|
|
|
|
function emitPlaybackState() {
|
|
const song = currentSong.value;
|
|
const status = playing.value ? 'playing' : (song ? 'paused' : 'stopped');
|
|
emit('playback-state', {
|
|
status,
|
|
title: song?.name || '',
|
|
album: song?.al?.name || '',
|
|
artists: song?.ar?.map(a => a.name) || [],
|
|
coverUrl: song?.al?.picUrl || '',
|
|
durationUs: (song?.dt || 0) * 1000,
|
|
positionUs: Math.round(currentTime.value * 1_000_000),
|
|
volume: volume.value / 100,
|
|
});
|
|
}
|
|
|
|
function isLiked(songId: number): boolean {
|
|
return likedIds.value.has(songId);
|
|
}
|
|
|
|
async function loadLikedIds() {
|
|
const userStore = useUserStore();
|
|
if (!userStore.isLoggedIn) return;
|
|
try {
|
|
const json: string = await invoke('likelist', { uid: userStore.user!.userId });
|
|
const data = JSON.parse(json);
|
|
const ids: number[] = data.ids || data.data?.ids || [];
|
|
likedIds.value = new Set(ids);
|
|
} catch { /* 忽略 */ }
|
|
}
|
|
|
|
async function toggleLike(songId: number) {
|
|
const wasLiked = likedIds.value.has(songId);
|
|
const newLike = !wasLiked;
|
|
try {
|
|
await invoke('like_song', { query: { id: songId, like: newLike ? 'true' : 'false' } });
|
|
if (newLike) {
|
|
likedIds.value.add(songId);
|
|
} else {
|
|
likedIds.value.delete(songId);
|
|
}
|
|
likedIds.value = new Set(likedIds.value);
|
|
} catch { /* 忽略 */ }
|
|
}
|
|
|
|
function addRecent(song: Song) {
|
|
recentLocal.value = recentLocal.value.filter(s => s.id !== song.id);
|
|
recentLocal.value.unshift(song);
|
|
if (recentLocal.value.length > MAX_RECENT) {
|
|
recentLocal.value = recentLocal.value.slice(0, MAX_RECENT);
|
|
}
|
|
}
|
|
|
|
watch(recentLocal, (val) => {
|
|
localStorage.setItem('recent_local', JSON.stringify(val));
|
|
}, { deep: true });
|
|
|
|
watch(likedIds, (val) => {
|
|
localStorage.setItem('liked_ids', JSON.stringify([...val]));
|
|
}, { deep: true });
|
|
|
|
const isFmMode = ref(false);
|
|
let fmNextCallback: (() => void) | null = null;
|
|
|
|
function enableFmMode(onNext: () => void) {
|
|
isFmMode.value = true;
|
|
fmNextCallback = onNext;
|
|
}
|
|
|
|
function disableFmMode() {
|
|
isFmMode.value = false;
|
|
fmNextCallback = null;
|
|
}
|
|
|
|
let fmVipSkipCount = 0;
|
|
const MAX_FM_VIP_SKIP = 10;
|
|
|
|
async function playFmSong(song: Song) {
|
|
if (tickInterval) { clearInterval(tickInterval); setTickInterval(null); }
|
|
if (!song.dt || song.dt === 0) {
|
|
try {
|
|
const jsonStr: string = await invoke('get_song_detail', { id: String(song.id) });
|
|
const data = JSON.parse(jsonStr);
|
|
const full = data.songs?.[0];
|
|
if (full) {
|
|
song.dt = full.dt || 0;
|
|
song.al = full.al || song.al;
|
|
song.ar = full.ar || song.ar;
|
|
}
|
|
} catch (e) { /* 忽略 */ }
|
|
}
|
|
|
|
await invoke('stop_audio');
|
|
queue.value = [];
|
|
currentIndex.value = -1;
|
|
playing.value = false;
|
|
|
|
fmSong.value = song;
|
|
currentSong.value = song;
|
|
try {
|
|
const jsonStr: string = await invoke('get_song_url', { query: { id: Number(song.id), level: settings.audioQuality, fm_mode: true } });
|
|
const data = JSON.parse(jsonStr);
|
|
const url: string | undefined = data.url;
|
|
if (!url) throw new Error('无播放源');
|
|
|
|
if (data.freeTrialInfo) {
|
|
console.warn('FM VIP 试听歌曲,自动跳过', song.name);
|
|
showToast(`${song.name} 为 VIP 试听,已跳过`, 'info');
|
|
fmVipSkipCount++;
|
|
if (fmVipSkipCount >= MAX_FM_VIP_SKIP) {
|
|
console.warn('FM 连续跳过 VIP 歌曲过多,停止');
|
|
fmVipSkipCount = 0;
|
|
disableFmMode();
|
|
return;
|
|
}
|
|
if (fmNextCallback) {
|
|
fmNextCallback();
|
|
} else {
|
|
disableFmMode();
|
|
}
|
|
return;
|
|
}
|
|
|
|
fmVipSkipCount = 0;
|
|
await invoke('play_audio', { url });
|
|
await waitForAudioStart();
|
|
playing.value = true;
|
|
duration.value = (song.dt || 0) / 1000;
|
|
currentTime.value = 0;
|
|
startTick();
|
|
addRecent(song);
|
|
emitPlaybackState();
|
|
} catch (e) {
|
|
console.error('FM播放失败', e);
|
|
playing.value = false;
|
|
if (fmNextCallback) {
|
|
fmNextCallback();
|
|
} else {
|
|
disableFmMode();
|
|
}
|
|
}
|
|
}
|
|
|
|
async function play(song: Song) {
|
|
disableFmMode();
|
|
|
|
const idx = queue.value.findIndex(s => s.id === song.id);
|
|
if (idx !== -1 && idx === currentIndex.value && currentSong.value?.id === song.id) {
|
|
if (!playing.value) {
|
|
await invoke('resume_audio');
|
|
playing.value = true;
|
|
startTick();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (idx === -1) {
|
|
queue.value.push(song);
|
|
currentIndex.value = queue.value.length - 1;
|
|
} else {
|
|
currentIndex.value = idx;
|
|
}
|
|
await playCurrent();
|
|
}
|
|
|
|
async function playFromList(songs: Song[], startIndex: number) {
|
|
disableFmMode();
|
|
if (songs.length === 0) return;
|
|
|
|
const targetSong = songs[startIndex];
|
|
if (targetSong && currentSong.value?.id === targetSong.id && currentIndex.value >= 0) {
|
|
const sameQueue = queue.value.length === songs.length
|
|
&& queue.value.every((s, i) => s.id === songs[i].id);
|
|
if (sameQueue) {
|
|
if (!playing.value) {
|
|
await invoke('resume_audio');
|
|
playing.value = true;
|
|
startTick();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
queue.value = [...songs];
|
|
currentIndex.value = Math.max(0, Math.min(startIndex, songs.length - 1));
|
|
await playCurrent();
|
|
}
|
|
|
|
let vipSkipCount = 0;
|
|
const MAX_VIP_SKIP = 10;
|
|
|
|
function waitForAudioStart(): Promise<void> {
|
|
return new Promise<void>((resolve) => {
|
|
_audioStartedResolve = resolve;
|
|
});
|
|
}
|
|
|
|
async function playCurrent() {
|
|
if (tickInterval) { clearInterval(tickInterval); setTickInterval(null); }
|
|
const song = queue.value[currentIndex.value];
|
|
if (!song?.id) {
|
|
console.error('无效的歌曲数据', song);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
currentSong.value = song;
|
|
playing.value = false;
|
|
currentTime.value = 0;
|
|
duration.value = (song.dt || 0) / 1000;
|
|
|
|
if (song.localPath) {
|
|
await invoke('play_local_audio', { path: song.localPath });
|
|
await waitForAudioStart();
|
|
playing.value = true;
|
|
startTick();
|
|
addRecent(song);
|
|
emitPlaybackState();
|
|
return;
|
|
}
|
|
|
|
const jsonStr: string = await invoke('get_song_url', { query: { id: Number(song.id), level: settings.audioQuality } });
|
|
const data = JSON.parse(jsonStr);
|
|
const url: string | undefined = data.url;
|
|
|
|
if (!url) {
|
|
console.error('未获取到有效播放地址', song);
|
|
return;
|
|
}
|
|
|
|
if (data.freeTrialInfo) {
|
|
console.warn('VIP 试听歌曲,自动跳过', song.name);
|
|
showToast(`${song.name} 为 VIP 试听,已跳过`, 'info');
|
|
vipSkipCount++;
|
|
if (vipSkipCount >= MAX_VIP_SKIP) {
|
|
console.warn('连续跳过 VIP 歌曲过多,停止跳过');
|
|
vipSkipCount = 0;
|
|
return;
|
|
}
|
|
next();
|
|
return;
|
|
}
|
|
|
|
await invoke('play_audio', { url });
|
|
await waitForAudioStart();
|
|
playing.value = true;
|
|
startTick();
|
|
addRecent(song);
|
|
vipSkipCount = 0;
|
|
emitPlaybackState();
|
|
} catch (e) {
|
|
console.error('播放失败', e);
|
|
playing.value = false;
|
|
}
|
|
}
|
|
|
|
let onSeekStart: (() => void) | null = null;
|
|
|
|
function startTick() {
|
|
if (tickInterval) clearInterval(tickInterval);
|
|
let seekGuard = false;
|
|
onSeekStart = () => { seekGuard = true; };
|
|
let syncCounter = 1;
|
|
let lastSyncPos = -1;
|
|
let backendFrozen = false;
|
|
setTickInterval(setInterval(async () => {
|
|
if (playing.value && duration.value > 0) {
|
|
if (seekGuard) return;
|
|
syncCounter++;
|
|
if (syncCounter >= 2) {
|
|
syncCounter = 0;
|
|
try {
|
|
const pos = await invoke<number>('get_audio_position');
|
|
if (pos >= currentTime.value - 0.5) {
|
|
currentTime.value = pos;
|
|
}
|
|
if (lastSyncPos < 0) {
|
|
lastSyncPos = pos;
|
|
} else if (pos <= lastSyncPos + 0.05) {
|
|
backendFrozen = true;
|
|
lastSyncPos = pos;
|
|
} else {
|
|
backendFrozen = false;
|
|
lastSyncPos = pos;
|
|
}
|
|
} catch {}
|
|
} else {
|
|
if (!backendFrozen) {
|
|
const next = currentTime.value + 0.25;
|
|
if (next <= duration.value) {
|
|
currentTime.value = next;
|
|
}
|
|
}
|
|
}
|
|
if (currentTime.value > duration.value) {
|
|
currentTime.value = duration.value;
|
|
}
|
|
}
|
|
}, 250));
|
|
}
|
|
|
|
async function toggle() {
|
|
if (playing.value) {
|
|
await invoke('pause_audio');
|
|
playing.value = false;
|
|
} else {
|
|
await invoke('resume_audio');
|
|
playing.value = true;
|
|
}
|
|
emitPlaybackState();
|
|
}
|
|
|
|
async function stop() {
|
|
await invoke('stop_audio');
|
|
playing.value = false;
|
|
currentSong.value = null;
|
|
currentTime.value = 0;
|
|
if (tickInterval) { clearInterval(tickInterval); setTickInterval(null); }
|
|
disableFmMode();
|
|
emitPlaybackState();
|
|
}
|
|
|
|
|
|
function prev() {
|
|
if (isFmMode.value) return;
|
|
if (queue.value.length === 0) return;
|
|
currentIndex.value = (currentIndex.value - 1 + queue.value.length) % queue.value.length;
|
|
playCurrent();
|
|
}
|
|
|
|
async function playAll(songs: Song[]) {
|
|
if (songs.length === 0) return;
|
|
queue.value = [...songs];
|
|
currentIndex.value = 0;
|
|
await playCurrent();
|
|
}
|
|
|
|
function removeFromQueue(index: number) {
|
|
if (index < 0 || index >= queue.value.length) return;
|
|
const isCurrent = index === currentIndex.value;
|
|
if (isCurrent) {
|
|
stop();
|
|
queue.value.splice(index, 1);
|
|
if (queue.value.length === 0) {
|
|
currentIndex.value = -1;
|
|
return;
|
|
}
|
|
if (currentIndex.value >= queue.value.length) {
|
|
currentIndex.value = queue.value.length - 1;
|
|
}
|
|
} else {
|
|
queue.value.splice(index, 1);
|
|
if (index < currentIndex.value) {
|
|
currentIndex.value -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
function clearQueue() {
|
|
stop();
|
|
queue.value = [];
|
|
currentIndex.value = -1;
|
|
}
|
|
|
|
async function seek(time: number) {
|
|
try {
|
|
currentTime.value = time;
|
|
if (onSeekStart) onSeekStart();
|
|
await invoke('seek_audio', { time });
|
|
startTick();
|
|
emitPlaybackState();
|
|
} catch (e) {
|
|
console.error('seek 失败', e);
|
|
}
|
|
}
|
|
|
|
async function adjustVolume(delta: number) {
|
|
const newVol = Math.max(0, Math.min(100, volume.value + delta));
|
|
volume.value = newVol;
|
|
await invoke('set_volume', { vol: newVol / 100 });
|
|
emitPlaybackState();
|
|
}
|
|
|
|
|
|
const playMode = ref<PlayMode>('loop');
|
|
|
|
function setPlayMode(mode: PlayMode) {
|
|
playMode.value = mode;
|
|
}
|
|
|
|
function next() {
|
|
if (isFmMode.value && fmNextCallback) {
|
|
fmNextCallback();
|
|
return;
|
|
}
|
|
|
|
if (queue.value.length === 0) return;
|
|
let nextIndex: number;
|
|
switch (playMode.value) {
|
|
case 'repeat-one':
|
|
playCurrent();
|
|
return;
|
|
case 'shuffle':
|
|
if (queue.value.length === 1) {
|
|
nextIndex = 0;
|
|
} else {
|
|
do {
|
|
nextIndex = Math.floor(Math.random() * queue.value.length);
|
|
} while (nextIndex === currentIndex.value);
|
|
}
|
|
break;
|
|
case 'loop':
|
|
default:
|
|
nextIndex = (currentIndex.value + 1) % queue.value.length;
|
|
break;
|
|
}
|
|
currentIndex.value = nextIndex;
|
|
playCurrent();
|
|
}
|
|
|
|
const showRoamDrawer = ref(false);
|
|
const roamInitialTab = ref<'lyric' | 'comment'>('lyric');
|
|
const commentSongId = ref<number | null>(null);
|
|
|
|
function openRoamDrawer(tab: 'lyric' | 'comment' = 'lyric') {
|
|
roamInitialTab.value = tab;
|
|
showRoamDrawer.value = true;
|
|
nextTick(() => { roamInitialTab.value = 'lyric'; });
|
|
}
|
|
|
|
function openCommentForSong(songId: number) {
|
|
commentSongId.value = songId;
|
|
openRoamDrawer('comment');
|
|
}
|
|
|
|
function closeRoamDrawer() {
|
|
showRoamDrawer.value = false;
|
|
}
|
|
|
|
function toggleRoamDrawer() {
|
|
showRoamDrawer.value = !showRoamDrawer.value;
|
|
}
|
|
|
|
async function loadFirstFmSong() {
|
|
try {
|
|
const jsonStr: string = await invoke('personal_fm');
|
|
const data = JSON.parse(jsonStr);
|
|
const songs = data.data || data;
|
|
if (songs && songs.length > 0) {
|
|
const song = normalizeSong(songs[0]);
|
|
enableFmMode(() => loadFirstFmSong());
|
|
await playFmSong(song);
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// -------- FM 专属状态 --------
|
|
const fmSong = ref<Song | null>(null);
|
|
const fmPlaying = ref(false);
|
|
|
|
async function loadFm() {
|
|
try {
|
|
const jsonStr: string = await invoke('personal_fm');
|
|
const data = JSON.parse(jsonStr);
|
|
const songs = data.data || data;
|
|
if (songs && songs.length > 0) {
|
|
const song = normalizeSong(songs[0]);
|
|
fmSong.value = song;
|
|
enableFmMode(nextFm);
|
|
await playFmSong(song);
|
|
fmPlaying.value = true;
|
|
}
|
|
} catch (e) {
|
|
console.error('FM加载失败', e);
|
|
}
|
|
}
|
|
|
|
async function toggleFm() {
|
|
if (!fmSong.value) return;
|
|
if (fmPlaying.value) {
|
|
await toggle();
|
|
fmPlaying.value = playing.value;
|
|
} else {
|
|
if (currentSong.value?.id === fmSong.value.id) {
|
|
await toggle();
|
|
fmPlaying.value = playing.value;
|
|
} else {
|
|
enableFmMode(nextFm);
|
|
await playFmSong(fmSong.value);
|
|
fmPlaying.value = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function nextFm() {
|
|
await loadFm();
|
|
}
|
|
|
|
let _audioStartedResolve: (() => void) | null = null;
|
|
let _tickInterval: ReturnType<typeof setInterval> | null = null;
|
|
|
|
listen('audio-started', () => {
|
|
if (_audioStartedResolve) {
|
|
_audioStartedResolve();
|
|
_audioStartedResolve = null;
|
|
}
|
|
});
|
|
|
|
listen('audio-ended', () => {
|
|
if (_tickInterval) { clearInterval(_tickInterval); _tickInterval = null; }
|
|
const player = usePlayerStore();
|
|
player.next();
|
|
});
|
|
|
|
listen<string>('mpris-command', (event) => {
|
|
const cmd = event.payload;
|
|
const player = usePlayerStore();
|
|
if (cmd === 'Next') {
|
|
player.next();
|
|
} else if (cmd === 'Previous') {
|
|
player.prev();
|
|
} else if (cmd === 'PlayPause') {
|
|
player.toggle();
|
|
} else if (cmd === 'Play') {
|
|
if (!player.playing) player.toggle();
|
|
} else if (cmd === 'Pause') {
|
|
if (player.playing) player.toggle();
|
|
} else if (cmd === 'Stop') {
|
|
player.stop();
|
|
} else if (cmd.startsWith('SetVolume:')) {
|
|
const vol = parseFloat(cmd.slice(10));
|
|
if (!isNaN(vol)) {
|
|
player.volume = Math.round(vol * 100);
|
|
invoke('set_volume', { vol }).catch(() => {});
|
|
}
|
|
} else if (cmd.startsWith('Seek:')) {
|
|
const offsetUs = parseInt(cmd.slice(5), 10);
|
|
const offsetSec = offsetUs / 1_000_000;
|
|
const newPos = Math.max(0, Math.min(player.currentTime + offsetSec, player.duration));
|
|
player.seek(newPos);
|
|
} else if (cmd.startsWith('SetPosition:')) {
|
|
const posUs = parseInt(cmd.slice(13), 10);
|
|
const posSec = posUs / 1_000_000;
|
|
player.seek(posSec);
|
|
} else if (cmd === 'Raise') {
|
|
getCurrentWindow().show().catch(() => {});
|
|
getCurrentWindow().setFocus().catch(() => {});
|
|
} else if (cmd === 'Quit') {
|
|
getCurrentWindow().close().catch(() => {});
|
|
}
|
|
});
|
|
|
|
watch(currentSong, (newSong) => {
|
|
if (isFmMode.value && newSong?.id !== fmSong.value?.id) {
|
|
fmPlaying.value = false;
|
|
disableFmMode();
|
|
}
|
|
});
|
|
|
|
watch(playing, (val) => {
|
|
if (currentSong.value?.id === fmSong.value?.id) {
|
|
fmPlaying.value = val;
|
|
} else {
|
|
fmPlaying.value = false;
|
|
}
|
|
});
|
|
|
|
|
|
return {
|
|
currentSong,
|
|
playing,
|
|
currentTime,
|
|
duration,
|
|
queue,
|
|
currentIndex,
|
|
playMode,
|
|
isFmMode,
|
|
enableFmMode,
|
|
disableFmMode,
|
|
playFmSong,
|
|
setPlayMode,
|
|
play,
|
|
playFromList,
|
|
playAll,
|
|
toggle,
|
|
stop,
|
|
prev,
|
|
next,
|
|
seek,
|
|
playCurrent,
|
|
volume,
|
|
adjustVolume,
|
|
|
|
removeFromQueue,
|
|
clearQueue,
|
|
|
|
recentLocal,
|
|
|
|
likedIds,
|
|
isLiked,
|
|
loadLikedIds,
|
|
toggleLike,
|
|
|
|
showRoamDrawer,
|
|
roamInitialTab,
|
|
commentSongId,
|
|
openCommentForSong,
|
|
openRoamDrawer,
|
|
closeRoamDrawer,
|
|
toggleRoamDrawer,
|
|
loadFirstFmSong,
|
|
|
|
fmSong,
|
|
fmPlaying,
|
|
loadFm,
|
|
toggleFm,
|
|
nextFm,
|
|
};
|
|
});
|