From 7847a9f6b2b728177bc9d8c30839dce382338bad Mon Sep 17 00:00:00 2001 From: Atdunbg Date: Tue, 12 May 2026 09:58:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B7=A8=E5=B9=B3=E5=8F=B0=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=E4=B8=8E=E7=89=88=E6=9C=AC=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cookie 存储从 temp_dir 迁移至 Tauri app_data_dir,兼容 Linux - 简单统一风格,UI优化 - recentLocal 播放历史持久化到 localStorage - 添加设置界面可以修改简单的设置 --- README.md | 82 +++++- src-tauri/capabilities/default.json | 1 + src-tauri/netease_cookies.json | 1 - src-tauri/src/api.rs | 165 +++++++++--- src-tauri/src/lib.rs | 32 ++- src-tauri/tauri.conf.json | 10 +- src/App.vue | 376 +++++++++++++++++++++------- src/assets/app-icon.png | Bin 0 -> 28066 bytes src/components/CustomSelect.vue | 79 ++++++ src/components/PlayerBar.vue | 162 +++++------- src/components/ToastContainer.vue | 60 +++++ src/composables/UserLyric.ts | 9 +- src/composables/useToast.ts | 22 ++ src/router/index.ts | 8 +- src/stores/player.ts | 92 ++++++- src/stores/settings.ts | 86 +++++++ src/style.css | 133 +++++++++- src/utils/format.ts | 20 ++ src/views/DailySongs.vue | 60 +++-- src/views/Discover.vue | 20 +- src/views/FavoriteSongs.vue | 84 ++++++- src/views/Home.vue | 143 +++++------ src/views/Login.vue | 58 ++--- src/views/PlaylistDetail.vue | 137 +++++++--- src/views/RecentPlays.vue | 40 ++- src/views/Roam.vue | 53 +--- src/views/Search.vue | 30 +-- src/views/Settings.vue | 164 ++++++++++++ 28 files changed, 1592 insertions(+), 535 deletions(-) delete mode 100644 src-tauri/netease_cookies.json create mode 100644 src/assets/app-icon.png create mode 100644 src/components/CustomSelect.vue create mode 100644 src/components/ToastContainer.vue create mode 100644 src/composables/useToast.ts create mode 100644 src/stores/settings.ts create mode 100644 src/utils/format.ts create mode 100644 src/views/Settings.vue diff --git a/README.md b/README.md index 12920b6..316e3fc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,81 @@ -# Tauri + Vue + TypeScript +# Nekosonic -This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` \ No newline at end of file +.roam-lyric-active:hover { + background: var(--c-subtle) !important; + color: var(--c-content) !important; +} + diff --git a/src/assets/app-icon.png b/src/assets/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0a65c08eacb496df7a82d5dba5cfac4b9d02bb62 GIT binary patch literal 28066 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 literal 0 HcmV?d00001 diff --git a/src/components/CustomSelect.vue b/src/components/CustomSelect.vue new file mode 100644 index 0000000..5f19352 --- /dev/null +++ b/src/components/CustomSelect.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/src/components/PlayerBar.vue b/src/components/PlayerBar.vue index 5e213ec..f4d9d63 100644 --- a/src/components/PlayerBar.vue +++ b/src/components/PlayerBar.vue @@ -1,52 +1,44 @@ \ No newline at end of file + diff --git a/src/components/ToastContainer.vue b/src/components/ToastContainer.vue new file mode 100644 index 0000000..956021b --- /dev/null +++ b/src/components/ToastContainer.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/src/composables/UserLyric.ts b/src/composables/UserLyric.ts index bb4ff0e..ca15e15 100644 --- a/src/composables/UserLyric.ts +++ b/src/composables/UserLyric.ts @@ -1,4 +1,4 @@ -import { ref, computed, watch } from 'vue'; +import { ref, watch } from 'vue'; import { invoke } from '@tauri-apps/api/core'; import { parseLrc, getCurrentLyricIndex, LyricLine } from '../utils/lyric'; import { usePlayerStore } from '../stores/player'; @@ -9,12 +9,6 @@ export function useLyric() { const lyrics = ref([]); const currentLyricIdx = ref(-1); - const currentLyricText = computed(() => { - if (lyrics.value.length === 0) return ''; - const idx = currentLyricIdx.value; - return idx >= 0 && idx < lyrics.value.length ? lyrics.value[idx].text : ''; - }); - watch(() => player.currentSong, async (song) => { if (!song) { lyrics.value = []; @@ -43,6 +37,5 @@ export function useLyric() { return { lyrics, currentLyricIdx, - currentLyricText, }; } \ No newline at end of file diff --git a/src/composables/useToast.ts b/src/composables/useToast.ts new file mode 100644 index 0000000..0dba215 --- /dev/null +++ b/src/composables/useToast.ts @@ -0,0 +1,22 @@ +import { ref } from 'vue'; + +export interface Toast { + id: number; + message: string; + type: 'success' | 'error' | 'info'; +} + +const toasts = ref([]); +let nextId = 0; + +export function showToast(message: string, type: 'success' | 'error' | 'info' = 'info', duration = 3000) { + const id = nextId++; + toasts.value.push({ id, message, type }); + setTimeout(() => { + toasts.value = toasts.value.filter(t => t.id !== id); + }, duration); +} + +export function useToast() { + return { toasts, showToast }; +} diff --git a/src/router/index.ts b/src/router/index.ts index 32d647b..67134d6 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -6,18 +6,20 @@ import Login from '@/views/Login.vue'; import FavoriteSongs from '@/views/FavoriteSongs.vue'; import RecentPlays from '@/views/RecentPlays.vue'; import DailySongs from '@/views/DailySongs.vue'; +import Settings from '@/views/Settings.vue'; const routes = [ { path: '/', name: 'home', component: Home }, { path: '/discover', name: 'discover', component: Discover }, - { path: '/search', name: 'search', component: Discover }, // 同样指向Discover,保留兼容 - { path: '/roam', name: 'roam', component: () => import('@/views/Roam.vue') }, // 漫游页面 + { path: '/search', name: 'search', component: Discover }, + { path: '/roam', name: 'roam', component: () => import('@/views/Roam.vue') }, { path: '/favorites', name: 'favorites', component: FavoriteSongs }, { path: '/recent', name: 'recent', component: RecentPlays }, - { path: '/daily', name: 'daily', component: DailySongs }, // 每日推荐 + { path: '/daily', name: 'daily', component: DailySongs }, { path: '/login', name: 'login', component: Login }, { path: '/playlist/:id', name: 'playlist', component: PlaylistDetail }, + { path: '/settings', name: 'settings', component: Settings }, ]; export default createRouter({ diff --git a/src/stores/player.ts b/src/stores/player.ts index 2d27359..2bcab6b 100644 --- a/src/stores/player.ts +++ b/src/stores/player.ts @@ -2,8 +2,9 @@ import { defineStore } from 'pinia'; import { ref , watch } from 'vue'; import { invoke } from '@tauri-apps/api/core'; import { normalizeSong } from '../utils/song'; +import { useSettingsStore } from './settings'; +import { useUserStore } from './user'; -// 设置播放模式,目前只有顺序循环,后续可扩展 export type PlayMode = 'loop' | 'shuffle' | 'repeat-one'; export interface Song { @@ -33,6 +34,22 @@ export function setupCacheProgressListener() { // 在 store 定义外调用 setupCacheProgressListener(),或者在应用入口调用 +function loadRecentLocal(): Song[] { + try { + const raw = localStorage.getItem('recent_local'); + if (raw) return JSON.parse(raw); + } catch {} + return []; +} + +function loadLikedIdsFromStorage(): Set { + 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(null); const playing = ref(false); @@ -44,6 +61,56 @@ export const usePlayerStore = defineStore('player', () => { let tickInterval: ReturnType | null = null; + const recentLocal = ref(loadRecentLocal()); + const MAX_RECENT = 200; + + const likedIds = ref>(loadLikedIdsFromStorage()); + + 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; @@ -62,7 +129,7 @@ export const usePlayerStore = defineStore('player', () => { // 如果缺少时长,尝试从详情接口获取 if (!song.dt || song.dt === 0) { try { - const jsonStr: string = await invoke('get_song_detail', { id: Number(song.id) }); + const jsonStr: string = await invoke('get_song_detail', { id: String(song.id) }); const data = JSON.parse(jsonStr); const full = data.songs?.[0]; if (full) { @@ -80,13 +147,15 @@ export const usePlayerStore = defineStore('player', () => { currentSong.value = song; try { - const url: string = await invoke('get_song_url', { id: Number(song.id) }); + const settings = useSettingsStore(); + const url: string = await invoke('get_song_url', { query: { id: Number(song.id), level: settings.audioQuality } }); if (!url) throw new Error('无播放源'); await invoke('play_audio', { url }); playing.value = true; duration.value = (song.dt || 0) / 1000; currentTime.value = 0; startTick(); + addRecent(song); } catch (e) { console.error('FM播放失败', e); playing.value = false; @@ -122,8 +191,8 @@ export const usePlayerStore = defineStore('player', () => { currentTime.value = 0; duration.value = (song.dt || 0) / 1000; - // 获取 URL 并播放 - const url: string = await invoke('get_song_url', { id: Number(song.id) }); + const settings = useSettingsStore(); + const url: string = await invoke('get_song_url', { query: { id: Number(song.id), level: settings.audioQuality } }); if (!url) { console.error('未获取到有效播放地址', song); return; @@ -132,6 +201,7 @@ export const usePlayerStore = defineStore('player', () => { await invoke('play_audio', { url }); playing.value = true; startTick(); + addRecent(song); } catch (e) { console.error('播放失败', e); playing.value = false; @@ -279,6 +349,10 @@ export const usePlayerStore = defineStore('player', () => { showRoamDrawer.value = false; } + function toggleRoamDrawer() { + showRoamDrawer.value = !showRoamDrawer.value; + } + async function loadFirstFmSong() { try { const jsonStr: string = await invoke('personal_fm'); @@ -388,9 +462,17 @@ watch(playing, (val) => { removeFromQueue, clearQueue, + recentLocal, + + likedIds, + isLiked, + loadLikedIds, + toggleLike, + showRoamDrawer, openRoamDrawer, closeRoamDrawer, + toggleRoamDrawer, loadFirstFmSong, fmSong, diff --git a/src/stores/settings.ts b/src/stores/settings.ts new file mode 100644 index 0000000..12719ec --- /dev/null +++ b/src/stores/settings.ts @@ -0,0 +1,86 @@ +import { defineStore } from 'pinia'; +import { ref, watch } from 'vue'; + +export type AudioQuality = 'standard' | 'higher' | 'exhigh' | 'lossless' | 'hires'; +export type ThemeMode = 'dark' | 'light'; +export type CloseAction = 'ask' | 'minimize' | 'exit'; + +export const qualityLabels: Record = { + standard: '标准', + higher: '较高', + exhigh: '极高 (HQ)', + lossless: '无损 (SQ)', + hires: 'Hi-Res', +}; + +export const closeActionLabels: Record = { + ask: '每次询问', + minimize: '最小化到托盘', + exit: '直接退出', +}; + +interface SettingsData { + audioQuality: AudioQuality; + downloadPath: string; + theme: ThemeMode; + closeAction: CloseAction; +} + +function loadSettings(): SettingsData { + try { + const raw = localStorage.getItem('app_settings'); + if (raw) return JSON.parse(raw); + } catch {} + return { + audioQuality: 'standard', + downloadPath: '', + theme: 'dark', + closeAction: 'ask', + }; +} + +export const useSettingsStore = defineStore('settings', () => { + const saved = loadSettings(); + + const audioQuality = ref(saved.audioQuality); + const downloadPath = ref(saved.downloadPath); + const theme = ref(saved.theme); + const closeAction = ref(saved.closeAction || 'ask'); + + function setAudioQuality(q: AudioQuality) { + audioQuality.value = q; + } + + function setDownloadPath(p: string) { + downloadPath.value = p; + } + + function setTheme(t: ThemeMode) { + theme.value = t; + } + + function setCloseAction(a: CloseAction) { + closeAction.value = a; + } + + watch([audioQuality, downloadPath, theme, closeAction], () => { + const data: SettingsData = { + audioQuality: audioQuality.value, + downloadPath: downloadPath.value, + theme: theme.value, + closeAction: closeAction.value, + }; + localStorage.setItem('app_settings', JSON.stringify(data)); + }, { deep: true }); + + return { + audioQuality, + downloadPath, + theme, + closeAction, + setAudioQuality, + setDownloadPath, + setTheme, + setCloseAction, + }; +}); diff --git a/src/style.css b/src/style.css index 77c08af..193f667 100644 --- a/src/style.css +++ b/src/style.css @@ -1,14 +1,74 @@ @import "tailwindcss"; +@theme { + --color-base: var(--c-bg); + --color-surface: var(--c-surface); + --color-subtle: var(--c-subtle); + --color-muted: var(--c-muted); + --color-emphasis: var(--c-emphasis); + --color-content: var(--c-content); + --color-content-2: var(--c-content-2); + --color-content-3: var(--c-content-3); + --color-content-4: var(--c-content-4); + --color-line: var(--c-line); + --color-line-2: var(--c-line-2); + --color-accent: var(--c-accent); + --color-accent-hover: var(--c-accent-hover); + --color-accent-text: var(--c-accent-text); + --color-accent-dim: var(--c-accent-dim); + --color-danger: var(--c-danger); + --color-danger-dim: var(--c-danger-dim); + --color-warning: var(--c-warning); + --color-info: var(--c-info); +} + @layer base { :root { - --color-surface: 255 255 255; - --color-primary: 34 197 94; + --c-bg: #030712; + --c-surface: #111827; + --c-subtle: rgba(255, 255, 255, 0.05); + --c-muted: rgba(255, 255, 255, 0.10); + --c-emphasis: rgba(255, 255, 255, 0.18); + --c-content: #ffffff; + --c-content-2: #9ca3af; + --c-content-3: #6b7280; + --c-content-4: #4b5563; + --c-line: rgba(255, 255, 255, 0.10); + --c-line-2: rgba(255, 255, 255, 0.05); + --c-accent: #22c55e; + --c-accent-hover: #16a34a; + --c-accent-text: #4ade80; + --c-accent-dim: rgba(34, 197, 94, 0.20); + --c-danger: #ef4444; + --c-danger-dim: rgba(239, 68, 68, 0.20); + --c-warning: #eab308; + --c-info: #3b82f6; + } + + [data-theme="light"] { + --c-bg: #f3f4f6; + --c-surface: #ffffff; + --c-subtle: rgba(0, 0, 0, 0.04); + --c-muted: rgba(0, 0, 0, 0.08); + --c-emphasis: rgba(0, 0, 0, 0.12); + --c-content: #111827; + --c-content-2: #4b5563; + --c-content-3: #6b7280; + --c-content-4: #9ca3af; + --c-line: rgba(0, 0, 0, 0.10); + --c-line-2: rgba(0, 0, 0, 0.05); + --c-accent: #16a34a; + --c-accent-hover: #15803d; + --c-accent-text: #16a34a; + --c-accent-dim: rgba(22, 163, 74, 0.15); + --c-danger: #dc2626; + --c-danger-dim: rgba(220, 38, 38, 0.15); + --c-warning: #ca8a04; + --c-info: #2563eb; } - /* 确保 html 也应用暗色背景,防止空白区域 */ html { - background: #0f172a; + background: var(--c-bg); overflow: hidden; height: 100%; overscroll-behavior: none; @@ -17,17 +77,14 @@ body { @apply antialiased; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; - background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%); - /* 关键:锁住 body,彻底消除整体拖动 */ + background: var(--c-bg); position: fixed; inset: 0; overflow: hidden; overscroll-behavior: none; - /* 阻止触控板手势触发页面导航 */ touch-action: none; } - /* 自定义滚动条保持不变 */ ::-webkit-scrollbar { width: 5px; height: 5px; @@ -36,10 +93,64 @@ background: transparent; } ::-webkit-scrollbar-thumb { - background-color: rgba(255, 255, 255, 0.2); + background-color: var(--c-muted); border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { - background-color: rgba(255, 255, 255, 0.4); + background-color: var(--c-emphasis); } -} \ No newline at end of file + + select { + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 10px center; + padding-right: 30px; + cursor: pointer; + } + + select:focus { + border-color: var(--c-accent); + box-shadow: 0 0 0 2px var(--c-accent-dim); + } + + select option { + background: var(--c-surface); + color: var(--c-content); + padding: 8px; + } + + input[type="checkbox"] { + appearance: none; + width: 16px; + height: 16px; + border: 2px solid var(--c-emphasis); + border-radius: 4px; + background: transparent; + cursor: pointer; + position: relative; + transition: all 0.15s ease; + flex-shrink: 0; + } + + input[type="checkbox"]:hover { + border-color: var(--c-accent); + } + + input[type="checkbox"]:checked { + background: var(--c-accent); + border-color: var(--c-accent); + } + + input[type="checkbox"]:checked::after { + content: ''; + position: absolute; + left: 4px; + top: 1px; + width: 4px; + height: 8px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } +} diff --git a/src/utils/format.ts b/src/utils/format.ts new file mode 100644 index 0000000..1a4725d --- /dev/null +++ b/src/utils/format.ts @@ -0,0 +1,20 @@ +export function formatDuration(ms: number): string { + const sec = Math.floor(ms / 1000); + const m = Math.floor(sec / 60); + const s = sec % 60; + return `${m}:${s.toString().padStart(2, '0')}`; +} + +export function formatTime(sec: number): string { + if (!sec || isNaN(sec)) return '0:00'; + const m = Math.floor(sec / 60); + const s = Math.floor(sec % 60); + return `${m}:${s.toString().padStart(2, '0')}`; +} + +export function formatPlayCount(count: number): string { + if (!count) return '0'; + if (count >= 100000000) return (count / 100000000).toFixed(1) + '亿'; + if (count >= 10000) return (count / 10000).toFixed(1) + '万'; + return count.toString(); +} diff --git a/src/views/DailySongs.vue b/src/views/DailySongs.vue index 341a64c..4341a53 100644 --- a/src/views/DailySongs.vue +++ b/src/views/DailySongs.vue @@ -1,26 +1,52 @@ + + diff --git a/src/views/Home.vue b/src/views/Home.vue index fc0aa99..b887a9e 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,7 +1,7 @@ + + diff --git a/src/views/Roam.vue b/src/views/Roam.vue index 8546c92..0371f7c 100644 --- a/src/views/Roam.vue +++ b/src/views/Roam.vue @@ -1,51 +1,44 @@