diff --git a/src/composables/UserLyric.ts b/src/composables/UserLyric.ts
index ca15e15..e9ae4bf 100644
--- a/src/composables/UserLyric.ts
+++ b/src/composables/UserLyric.ts
@@ -1,6 +1,6 @@
import { ref, watch } from 'vue';
import { invoke } from '@tauri-apps/api/core';
-import { parseLrc, getCurrentLyricIndex, LyricLine } from '../utils/lyric';
+import { parseLrc, mergeTranslation, getCurrentLyricIndex, LyricLine } from '../utils/lyric';
import { usePlayerStore } from '../stores/player';
export function useLyric() {
@@ -8,21 +8,33 @@ export function useLyric() {
const lyrics = ref
([]);
const currentLyricIdx = ref(-1);
+ const showTranslation = ref(true);
+ const hasTranslation = ref(false);
watch(() => player.currentSong, async (song) => {
if (!song) {
lyrics.value = [];
currentLyricIdx.value = -1;
+ hasTranslation.value = false;
return;
}
try {
const jsonStr: string = await invoke('get_lyric', { id: song.id });
const data = JSON.parse(jsonStr);
const lrc = data?.lrc?.lyric || '';
- lyrics.value = lrc ? parseLrc(lrc) : [];
+ const tLrc = data?.tlyric?.lyric || '';
+ let parsed = lrc ? parseLrc(lrc) : [];
+ if (tLrc && parsed.length > 0) {
+ parsed = mergeTranslation(parsed, tLrc);
+ hasTranslation.value = parsed.some(l => l.translation);
+ } else {
+ hasTranslation.value = false;
+ }
+ lyrics.value = parsed;
currentLyricIdx.value = -1;
} catch {
lyrics.value = [];
+ hasTranslation.value = false;
}
}, { immediate: true });
@@ -34,8 +46,15 @@ export function useLyric() {
}
});
+ function toggleTranslation() {
+ showTranslation.value = !showTranslation.value;
+ }
+
return {
lyrics,
currentLyricIdx,
+ hasTranslation,
+ showTranslation,
+ toggleTranslation,
};
-}
\ No newline at end of file
+}
diff --git a/src/stores/player.ts b/src/stores/player.ts
index e727edb..691249b 100644
--- a/src/stores/player.ts
+++ b/src/stores/player.ts
@@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
-import { ref , watch } from 'vue';
+import { ref, watch, nextTick } from 'vue';
import { invoke } from '@tauri-apps/api/core';
import { normalizeSong } from '../utils/song';
import { useSettingsStore } from './settings';
@@ -422,6 +422,7 @@ export const usePlayerStore = defineStore('player', () => {
function openRoamDrawer(tab: 'lyric' | 'comment' = 'lyric') {
roamInitialTab.value = tab;
showRoamDrawer.value = true;
+ nextTick(() => { roamInitialTab.value = 'lyric'; });
}
function openCommentForSong(songId: number) {
diff --git a/src/stores/settings.ts b/src/stores/settings.ts
index ab5b0f9..a2fabbb 100644
--- a/src/stores/settings.ts
+++ b/src/stores/settings.ts
@@ -2,9 +2,29 @@ import { defineStore } from 'pinia';
import { ref, watch } from 'vue';
export type AudioQuality = 'standard' | 'higher' | 'exhigh' | 'lossless' | 'hires';
-export type ThemeMode = 'dark' | 'light';
+export type ThemeName = 'green' | 'rose' | 'blue' | 'violet' | 'orange' | 'cyan' | 'pink';
export type CloseAction = 'ask' | 'minimize' | 'exit';
+export const themeLabels: Record = {
+ green: '翠绿',
+ rose: '玫红',
+ blue: '天蓝',
+ violet: '紫罗兰',
+ orange: '橙色',
+ cyan: '青色',
+ pink: '粉色',
+};
+
+export const themeColors: Record = {
+ green: '#22c55e',
+ rose: '#f43f5e',
+ blue: '#3b82f6',
+ violet: '#8b5cf6',
+ orange: '#f97316',
+ cyan: '#06b6d4',
+ pink: '#ec4899',
+};
+
export const qualityLabels: Record = {
standard: '标准',
higher: '较高',
@@ -25,10 +45,12 @@ export interface ShortcutBinding {
}
export const defaultShortcuts: Record = {
+ playPause: { key: 'Control+KeyP', label: '播放/暂停' },
prev: { key: 'Control+ArrowLeft', label: '上一首' },
next: { key: 'Control+ArrowRight', label: '下一首' },
volUp: { key: 'Control+ArrowUp', label: '音量增加' },
volDown: { key: 'Control+ArrowDown', label: '音量减小' },
+ globalPlayPause: { key: 'Alt+Control+KeyP', label: '播放/暂停(全局)' },
globalPrev: { key: 'Alt+Control+ArrowLeft', label: '上一首(全局)' },
globalNext: { key: 'Alt+Control+ArrowRight', label: '下一首(全局)' },
globalVolUp: { key: 'Alt+Control+ArrowUp', label: '音量增加(全局)' },
@@ -38,7 +60,7 @@ export const defaultShortcuts: Record = {
interface SettingsData {
audioQuality: AudioQuality;
downloadPath: string;
- theme: ThemeMode;
+ theme: ThemeName;
closeAction: CloseAction;
shortcuts: Record;
outputDevice: string | null;
@@ -49,10 +71,12 @@ function loadSettings(): SettingsData {
const raw = localStorage.getItem('app_settings');
if (raw) {
const parsed = JSON.parse(raw);
+ const theme = parsed.theme || parsed.accentColor || 'green';
+ const validThemes: ThemeName[] = ['green', 'rose', 'blue', 'violet', 'orange', 'cyan', 'pink'];
return {
audioQuality: parsed.audioQuality || 'standard',
downloadPath: parsed.downloadPath || '',
- theme: parsed.theme || 'dark',
+ theme: validThemes.includes(theme) ? theme : 'green',
closeAction: parsed.closeAction || 'ask',
shortcuts: { ...defaultShortcuts, ...(parsed.shortcuts || {}) },
outputDevice: parsed.outputDevice || null,
@@ -62,7 +86,7 @@ function loadSettings(): SettingsData {
return {
audioQuality: 'standard',
downloadPath: '',
- theme: 'dark',
+ theme: 'green',
closeAction: 'ask',
shortcuts: { ...defaultShortcuts },
outputDevice: null,
@@ -74,7 +98,7 @@ export const useSettingsStore = defineStore('settings', () => {
const audioQuality = ref(saved.audioQuality);
const downloadPath = ref(saved.downloadPath);
- const theme = ref(saved.theme);
+ const theme = ref(saved.theme);
const closeAction = ref(saved.closeAction || 'ask');
const shortcuts = ref>(saved.shortcuts);
const outputDevice = ref(saved.outputDevice);
@@ -87,7 +111,7 @@ export const useSettingsStore = defineStore('settings', () => {
downloadPath.value = p;
}
- function setTheme(t: ThemeMode) {
+ function setTheme(t: ThemeName) {
theme.value = t;
}
@@ -110,7 +134,7 @@ export const useSettingsStore = defineStore('settings', () => {
function resetAll() {
audioQuality.value = 'standard';
downloadPath.value = '';
- theme.value = 'dark';
+ theme.value = 'green';
closeAction.value = 'ask';
shortcuts.value = { ...defaultShortcuts };
outputDevice.value = null;
diff --git a/src/style.css b/src/style.css
index 193f667..a4a8496 100644
--- a/src/style.css
+++ b/src/style.css
@@ -23,18 +23,19 @@
}
@layer base {
- :root {
- --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);
+ :root,
+ [data-theme="green"] {
+ --c-bg: #020c06;
+ --c-surface: #0a1a10;
+ --c-subtle: rgba(34, 197, 94, 0.06);
+ --c-muted: rgba(34, 197, 94, 0.10);
+ --c-emphasis: rgba(34, 197, 94, 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-line: rgba(255, 255, 255, 0.08);
+ --c-line-2: rgba(255, 255, 255, 0.04);
--c-accent: #22c55e;
--c-accent-hover: #16a34a;
--c-accent-text: #4ade80;
@@ -45,26 +46,136 @@
--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;
+ [data-theme="rose"] {
+ --c-bg: #0c0206;
+ --c-surface: #1a0a10;
+ --c-subtle: rgba(244, 63, 94, 0.06);
+ --c-muted: rgba(244, 63, 94, 0.10);
+ --c-emphasis: rgba(244, 63, 94, 0.18);
+ --c-content: #ffffff;
+ --c-content-2: #9ca3af;
--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;
+ --c-content-4: #4b5563;
+ --c-line: rgba(255, 255, 255, 0.08);
+ --c-line-2: rgba(255, 255, 255, 0.04);
+ --c-accent: #f43f5e;
+ --c-accent-hover: #e11d48;
+ --c-accent-text: #fb7185;
+ --c-accent-dim: rgba(244, 63, 94, 0.20);
+ --c-danger: #ef4444;
+ --c-danger-dim: rgba(239, 68, 68, 0.20);
+ --c-warning: #eab308;
+ --c-info: #3b82f6;
+ }
+
+ [data-theme="blue"] {
+ --c-bg: #02060c;
+ --c-surface: #0a101a;
+ --c-subtle: rgba(59, 130, 246, 0.06);
+ --c-muted: rgba(59, 130, 246, 0.10);
+ --c-emphasis: rgba(59, 130, 246, 0.18);
+ --c-content: #ffffff;
+ --c-content-2: #9ca3af;
+ --c-content-3: #6b7280;
+ --c-content-4: #4b5563;
+ --c-line: rgba(255, 255, 255, 0.08);
+ --c-line-2: rgba(255, 255, 255, 0.04);
+ --c-accent: #3b82f6;
+ --c-accent-hover: #2563eb;
+ --c-accent-text: #60a5fa;
+ --c-accent-dim: rgba(59, 130, 246, 0.20);
+ --c-danger: #ef4444;
+ --c-danger-dim: rgba(239, 68, 68, 0.20);
+ --c-warning: #eab308;
+ --c-info: #8b5cf6;
+ }
+
+ [data-theme="violet"] {
+ --c-bg: #06020c;
+ --c-surface: #120a1a;
+ --c-subtle: rgba(139, 92, 246, 0.06);
+ --c-muted: rgba(139, 92, 246, 0.10);
+ --c-emphasis: rgba(139, 92, 246, 0.18);
+ --c-content: #ffffff;
+ --c-content-2: #9ca3af;
+ --c-content-3: #6b7280;
+ --c-content-4: #4b5563;
+ --c-line: rgba(255, 255, 255, 0.08);
+ --c-line-2: rgba(255, 255, 255, 0.04);
+ --c-accent: #8b5cf6;
+ --c-accent-hover: #7c3aed;
+ --c-accent-text: #a78bfa;
+ --c-accent-dim: rgba(139, 92, 246, 0.20);
+ --c-danger: #ef4444;
+ --c-danger-dim: rgba(239, 68, 68, 0.20);
+ --c-warning: #eab308;
+ --c-info: #3b82f6;
+ }
+
+ [data-theme="orange"] {
+ --c-bg: #0c0602;
+ --c-surface: #1a120a;
+ --c-subtle: rgba(249, 115, 22, 0.06);
+ --c-muted: rgba(249, 115, 22, 0.10);
+ --c-emphasis: rgba(249, 115, 22, 0.18);
+ --c-content: #ffffff;
+ --c-content-2: #9ca3af;
+ --c-content-3: #6b7280;
+ --c-content-4: #4b5563;
+ --c-line: rgba(255, 255, 255, 0.08);
+ --c-line-2: rgba(255, 255, 255, 0.04);
+ --c-accent: #f97316;
+ --c-accent-hover: #ea580c;
+ --c-accent-text: #fb923c;
+ --c-accent-dim: rgba(249, 115, 22, 0.20);
+ --c-danger: #ef4444;
+ --c-danger-dim: rgba(239, 68, 68, 0.20);
+ --c-warning: #eab308;
+ --c-info: #3b82f6;
+ }
+
+ [data-theme="cyan"] {
+ --c-bg: #020c0c;
+ --c-surface: #0a1a1a;
+ --c-subtle: rgba(6, 182, 212, 0.06);
+ --c-muted: rgba(6, 182, 212, 0.10);
+ --c-emphasis: rgba(6, 182, 212, 0.18);
+ --c-content: #ffffff;
+ --c-content-2: #9ca3af;
+ --c-content-3: #6b7280;
+ --c-content-4: #4b5563;
+ --c-line: rgba(255, 255, 255, 0.08);
+ --c-line-2: rgba(255, 255, 255, 0.04);
+ --c-accent: #06b6d4;
+ --c-accent-hover: #0891b2;
+ --c-accent-text: #22d3ee;
+ --c-accent-dim: rgba(6, 182, 212, 0.20);
+ --c-danger: #ef4444;
+ --c-danger-dim: rgba(239, 68, 68, 0.20);
+ --c-warning: #eab308;
+ --c-info: #3b82f6;
+ }
+
+ [data-theme="pink"] {
+ --c-bg: #0c020a;
+ --c-surface: #1a0a16;
+ --c-subtle: rgba(236, 72, 153, 0.06);
+ --c-muted: rgba(236, 72, 153, 0.10);
+ --c-emphasis: rgba(236, 72, 153, 0.18);
+ --c-content: #ffffff;
+ --c-content-2: #9ca3af;
+ --c-content-3: #6b7280;
+ --c-content-4: #4b5563;
+ --c-line: rgba(255, 255, 255, 0.08);
+ --c-line-2: rgba(255, 255, 255, 0.04);
+ --c-accent: #ec4899;
+ --c-accent-hover: #db2777;
+ --c-accent-text: #f472b6;
+ --c-accent-dim: rgba(236, 72, 153, 0.20);
+ --c-danger: #ef4444;
+ --c-danger-dim: rgba(239, 68, 68, 0.20);
+ --c-warning: #eab308;
+ --c-info: #3b82f6;
}
html {
@@ -78,6 +189,7 @@
@apply antialiased;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: var(--c-bg);
+ color: var(--c-content);
position: fixed;
inset: 0;
overflow: hidden;
diff --git a/src/utils/lyric.ts b/src/utils/lyric.ts
index 8367d5c..11cabea 100644
--- a/src/utils/lyric.ts
+++ b/src/utils/lyric.ts
@@ -1,6 +1,7 @@
export interface LyricLine {
- time: number; // 秒
+ time: number;
text: string;
+ translation?: string;
}
export function parseLrc(lrcStr: string): LyricLine[] {
@@ -20,11 +21,37 @@ export function parseLrc(lrcStr: string): LyricLine[] {
}
}
}
- // 按时长排序
result.sort((a, b) => a.time - b.time);
return result;
}
+export function mergeTranslation(lyrics: LyricLine[], tLrcStr: string): LyricLine[] {
+ if (!tLrcStr) return lyrics;
+ const tLines = parseLrc(tLrcStr);
+ if (tLines.length === 0) return lyrics;
+
+ const tMap = new Map();
+ for (const t of tLines) {
+ const key = Math.round(t.time * 100);
+ tMap.set(key, t.text);
+ }
+
+ return lyrics.map(line => {
+ const key = Math.round(line.time * 100);
+ const translation = tMap.get(key);
+ if (translation) {
+ return { ...line, translation };
+ }
+ for (let offset = -3; offset <= 3; offset++) {
+ const t = tMap.get(key + offset);
+ if (t) {
+ return { ...line, translation: t };
+ }
+ }
+ return line;
+ });
+}
+
export function getCurrentLyricIndex(lyrics: LyricLine[], currentTime: number): number {
let index = -1;
for (let i = 0; i < lyrics.length; i++) {
@@ -35,4 +62,4 @@ export function getCurrentLyricIndex(lyrics: LyricLine[], currentTime: number):
}
}
return index;
-}
\ No newline at end of file
+}
diff --git a/src/views/Settings.vue b/src/views/Settings.vue
index 8c11eea..3629cab 100644
--- a/src/views/Settings.vue
+++ b/src/views/Settings.vue
@@ -28,23 +28,19 @@
外观
-
-
-
-
-
-
+
+
主题色
+
+
@@ -241,7 +237,7 @@