From d1f5c8af8480a3ea47020fb7fa343ef20152f728 Mon Sep 17 00:00:00 2001 From: alger Date: Wed, 23 Jul 2025 22:47:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A1=8C=E9=9D=A2=E6=AD=8C=E8=AF=8D?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=BB=E9=A2=98=E9=A2=9C=E8=89=B2=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/i18n/lang/en-US/settings.ts | 20 + src/i18n/lang/zh-CN/settings.ts | 20 + src/i18n/lang/zh-Hant/settings.ts | 20 + .../components/lyric/ThemeColorPanel.vue | 480 ++++++++++++++++++ src/renderer/utils/linearColor.ts | 246 +++++++++ src/renderer/views/lyric/index.vue | 326 +++++++++++- 6 files changed, 1092 insertions(+), 20 deletions(-) create mode 100644 src/renderer/components/lyric/ThemeColorPanel.vue diff --git a/src/i18n/lang/en-US/settings.ts b/src/i18n/lang/en-US/settings.ts index ecd3e9e..04f2e62 100644 --- a/src/i18n/lang/en-US/settings.ts +++ b/src/i18n/lang/en-US/settings.ts @@ -222,6 +222,26 @@ export default { lyricLines: 'Lyric Lines', mobileUnavailable: 'This setting is only available on mobile devices' }, + themeColor: { + title: 'Lyric Theme Color', + presetColors: 'Preset Colors', + customColor: 'Custom Color', + preview: 'Preview', + previewText: 'Lyric Effect', + colorNames: { + 'spotify-green': 'Spotify Green', + 'apple-blue': 'Apple Blue', + 'youtube-red': 'YouTube Red', + 'orange': 'Vibrant Orange', + 'purple': 'Mystic Purple', + 'pink': 'Cherry Pink' + }, + tooltips: { + openColorPicker: 'Open Color Picker', + closeColorPicker: 'Close Color Picker' + }, + placeholder: '#1db954' + }, shortcutSettings: { title: 'Shortcut Settings', shortcut: 'Shortcut', diff --git a/src/i18n/lang/zh-CN/settings.ts b/src/i18n/lang/zh-CN/settings.ts index c4f14d3..a932398 100644 --- a/src/i18n/lang/zh-CN/settings.ts +++ b/src/i18n/lang/zh-CN/settings.ts @@ -222,6 +222,26 @@ export default { lyricLines: '歌词行数', mobileUnavailable: '此设置仅在移动端可用' }, + themeColor: { + title: '歌词主题色', + presetColors: '预设颜色', + customColor: '自定义颜色', + preview: '预览效果', + previewText: '歌词效果', + colorNames: { + 'spotify-green': 'Spotify 绿', + 'apple-blue': '苹果蓝', + 'youtube-red': 'YouTube 红', + 'orange': '活力橙', + 'purple': '神秘紫', + 'pink': '樱花粉' + }, + tooltips: { + openColorPicker: '打开色板', + closeColorPicker: '关闭色板' + }, + placeholder: '#1db954' + }, shortcutSettings: { title: '快捷键设置', shortcut: '快捷键', diff --git a/src/i18n/lang/zh-Hant/settings.ts b/src/i18n/lang/zh-Hant/settings.ts index 1248f8d..15b9710 100644 --- a/src/i18n/lang/zh-Hant/settings.ts +++ b/src/i18n/lang/zh-Hant/settings.ts @@ -201,5 +201,25 @@ export default { default: '預設', loose: '寬鬆' } + }, + themeColor: { + title: '歌詞主題色', + presetColors: '預設顏色', + customColor: '自訂顏色', + preview: '預覽效果', + previewText: '歌詞效果', + colorNames: { + 'spotify-green': 'Spotify 綠', + 'apple-blue': '蘋果藍', + 'youtube-red': 'YouTube 紅', + 'orange': '活力橙', + 'purple': '神秘紫', + 'pink': '櫻花粉' + }, + tooltips: { + openColorPicker: '開啟色板', + closeColorPicker: '關閉色板' + }, + placeholder: '#1db954' } }; \ No newline at end of file diff --git a/src/renderer/components/lyric/ThemeColorPanel.vue b/src/renderer/components/lyric/ThemeColorPanel.vue new file mode 100644 index 0000000..3e22a56 --- /dev/null +++ b/src/renderer/components/lyric/ThemeColorPanel.vue @@ -0,0 +1,480 @@ + + + + + \ No newline at end of file diff --git a/src/renderer/utils/linearColor.ts b/src/renderer/utils/linearColor.ts index 08334ec..56e8a20 100644 --- a/src/renderer/utils/linearColor.ts +++ b/src/renderer/utils/linearColor.ts @@ -12,6 +12,20 @@ interface ITextColors { theme: string; } +export interface LyricThemeColor { + id: string; + name: string; + light: string; + dark: string; +} + +interface LyricSettings { + isTop: boolean; + theme: 'light' | 'dark'; + isLock: boolean; + highlightColor?: string; +} + export const getImageLinearBackground = async (imageSrc: string): Promise => { try { const primaryColor = await getImagePrimaryColor(imageSrc); @@ -296,3 +310,235 @@ export const createGradientString = ( .map((color, i) => `rgb(${color.r}, ${color.g}, ${color.b}) ${percentages[i]}%`) .join(', ')})`; }; + +// ===== 歌词主题色相关工具函数 ===== + +/** + * 预设歌词主题色配置 + * 注意:name 字段将通过国际化系统动态获取,这里的值仅作为后备 + */ +const PRESET_LYRIC_COLORS: LyricThemeColor[] = [ + { + id: 'spotify-green', + name: 'Spotify Green', // 后备名称,实际使用时会被国际化替换 + light: '#1db954', + dark: '#1ed760' + }, + { + id: 'apple-blue', + name: 'Apple Blue', + light: '#007aff', + dark: '#0a84ff' + }, + { + id: 'youtube-red', + name: 'YouTube Red', + light: '#ff0000', + dark: '#ff4444' + }, + { + id: 'orange', + name: 'Vibrant Orange', + light: '#ff6b35', + dark: '#ff8c42' + }, + { + id: 'purple', + name: 'Mystic Purple', + light: '#8b5cf6', + dark: '#a78bfa' + }, + { + id: 'pink', + name: 'Cherry Pink', + light: '#ec4899', + dark: '#f472b6' + } +]; + +/** + * 验证颜色是否有效 + */ +export const validateColor = (color: string): boolean => { + if (!color || typeof color !== 'string') return false; + const tc = tinycolor(color); + return tc.isValid() && tc.getAlpha() > 0; +}; + +/** + * 检查颜色对比度是否符合可读性标准 + */ +export const validateColorContrast = (color: string, theme: 'light' | 'dark'): boolean => { + if (!validateColor(color)) return false; + + const backgroundColor = theme === 'dark' ? '#000000' : '#ffffff'; + const contrast = tinycolor.readability(color, backgroundColor); + return contrast >= 4.5; // WCAG AA 标准 +}; + +/** + * 为特定主题优化颜色 + */ +export const optimizeColorForTheme = (color: string, theme: 'light' | 'dark'): string => { + if (!validateColor(color)) { + return getDefaultHighlightColor(theme); + } + + const tc = tinycolor(color); + const hsl = tc.toHsl(); + + if (theme === 'dark') { + // 暗色主题:增加亮度和饱和度 + const optimized = tinycolor({ + h: hsl.h, + s: Math.min(hsl.s * 1.1, 1), + l: Math.max(hsl.l, 0.4) // 确保最小亮度 + }); + + // 检查对比度,如果不够则进一步调整 + if (!validateColorContrast(optimized.toHexString(), theme)) { + return tinycolor({ + h: hsl.h, + s: Math.min(hsl.s * 1.2, 1), + l: Math.max(hsl.l * 1.3, 0.5) + }).toHexString(); + } + + return optimized.toHexString(); + } else { + // 亮色主题:适当降低亮度 + const optimized = tinycolor({ + h: hsl.h, + s: Math.min(hsl.s * 1.05, 1), + l: Math.min(hsl.l, 0.6) // 确保最大亮度 + }); + + // 检查对比度 + if (!validateColorContrast(optimized.toHexString(), theme)) { + return tinycolor({ + h: hsl.h, + s: Math.min(hsl.s * 1.1, 1), + l: Math.min(hsl.l * 0.8, 0.5) + }).toHexString(); + } + + return optimized.toHexString(); + } +}; + +/** + * 获取默认高亮颜色 + */ +export const getDefaultHighlightColor = (theme?: 'light' | 'dark'): string => { + const defaultColor = PRESET_LYRIC_COLORS[0]; // Spotify 绿 + if (!theme) return defaultColor.light; + return theme === 'dark' ? defaultColor.dark : defaultColor.light; +}; + +/** + * 获取预设主题色列表 + */ +export const getLyricThemeColors = (): LyricThemeColor[] => { + return [...PRESET_LYRIC_COLORS]; +}; + +/** + * 根据主题获取预设颜色的实际值 + */ +export const getPresetColorValue = (colorId: string, theme: 'light' | 'dark'): string => { + const color = PRESET_LYRIC_COLORS.find(c => c.id === colorId); + if (!color) return getDefaultHighlightColor(theme); + return theme === 'dark' ? color.dark : color.light; +}; + + + +/** + * 安全加载歌词设置 + */ +const safeLoadLyricSettings = (): LyricSettings => { + try { + const stored = localStorage.getItem('lyricData'); + if (stored) { + const parsed = JSON.parse(stored) as LyricSettings; + + // 验证 highlightColor 字段 + if (parsed.highlightColor && !validateColor(parsed.highlightColor)) { + console.warn('Invalid stored highlight color, removing it'); + delete parsed.highlightColor; + } + + return parsed; + } + } catch (error) { + console.error('Failed to load lyric settings:', error); + } + + // 返回默认设置 + return { + isTop: false, + theme: 'dark', + isLock: false + }; +}; + +/** + * 安全保存歌词设置 + */ +const safeSaveLyricSettings = (settings: LyricSettings): void => { + try { + localStorage.setItem('lyricData', JSON.stringify(settings)); + } catch (error) { + console.error('Failed to save lyric settings:', error); + } +}; + +/** + * 保存歌词主题色 + */ +export const saveLyricThemeColor = (color: string): void => { + if (!validateColor(color)) { + console.warn('Attempted to save invalid color:', color); + return; + } + + const settings = safeLoadLyricSettings(); + settings.highlightColor = color; + safeSaveLyricSettings(settings); +}; + +/** + * 加载歌词主题色 + */ +export const loadLyricThemeColor = (): string => { + const settings = safeLoadLyricSettings(); + + if (settings.highlightColor && validateColor(settings.highlightColor)) { + return settings.highlightColor; + } + + // 如果没有保存的颜色或颜色无效,返回默认颜色 + return getDefaultHighlightColor(settings.theme); +}; + +/** + * 重置歌词主题色到默认值 + */ +export const resetLyricThemeColor = (): void => { + const settings = safeLoadLyricSettings(); + delete settings.highlightColor; + safeSaveLyricSettings(settings); +}; + +/** + * 获取当前有效的歌词主题色 + */ +export const getCurrentLyricThemeColor = (theme: 'light' | 'dark'): string => { + const savedColor = loadLyricThemeColor(); + + if (savedColor && validateColor(savedColor)) { + return optimizeColorForTheme(savedColor, theme); + } + + return getDefaultHighlightColor(theme); +}; diff --git a/src/renderer/views/lyric/index.vue b/src/renderer/views/lyric/index.vue index 52705b4..c650986 100644 --- a/src/renderer/views/lyric/index.vue +++ b/src/renderer/views/lyric/index.vue @@ -37,6 +37,13 @@ +
+ +
@@ -50,6 +57,15 @@ + + +
@@ -91,6 +107,13 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; import { SongResult } from '@/type/music'; +import ThemeColorPanel from '@/components/lyric/ThemeColorPanel.vue'; +import { + getCurrentLyricThemeColor, + loadLyricThemeColor, + saveLyricThemeColor, + validateColor +} from '@/utils/linearColor'; defineOptions({ name: 'Lyric' @@ -127,20 +150,51 @@ const dynamicData = ref({ isPlay: true }); -const lyricSetting = ref({ - ...(localStorage.getItem('lyricData') - ? JSON.parse(localStorage.getItem('lyricData') || '') - : { - isTop: false, - theme: 'dark', - isLock: false - }) -}); +// 安全加载歌词设置 +const loadLyricSettings = () => { + try { + const stored = localStorage.getItem('lyricData'); + if (stored) { + const parsed = JSON.parse(stored); + + // 验证 highlightColor 字段 + let validatedHighlightColor = parsed.highlightColor; + if (validatedHighlightColor && !validateColor(validatedHighlightColor)) { + console.warn('Invalid stored highlight color, removing it'); + validatedHighlightColor = undefined; + } + + // 确保所有必需字段存在并有效 + return { + isTop: parsed.isTop ?? false, + theme: (parsed.theme === 'light' || parsed.theme === 'dark') ? parsed.theme : 'dark', + isLock: parsed.isLock ?? false, + highlightColor: validatedHighlightColor + }; + } + } catch (error) { + console.error('Failed to load lyric settings:', error); + } + + // 返回默认设置 + return { + isTop: false, + theme: 'dark' as 'light' | 'dark', + isLock: false, + highlightColor: undefined as string | undefined + }; +}; + +const lyricSetting = ref(loadLyricSettings()); let hideControlsTimer: number | null = null; const isHovering = ref(false); +// 主题色相关状态 +const showThemeColorPanel = ref(false); +const currentHighlightColor = ref('#1db954'); + // 计算是否栏 const showControls = computed(() => { if (lyricSetting.value.isLock) { @@ -364,11 +418,20 @@ const getLyricStyle = (index: number) => { if (index !== currentIndex.value) return {}; const progress = currentProgress.value * 100; + + // 使用更清晰的渐变实现 return { background: `linear-gradient(to right, var(--highlight-color) ${progress}%, var(--text-color) ${progress}%)`, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', - transition: 'all 0.1s linear' + // 优化字体渲染,减少发虚 + textRendering: 'optimizeLegibility' as const, + WebkitFontSmoothing: 'antialiased' as const, + MozOsxFontSmoothing: 'grayscale' as const, + // 使用 transform 而不是直接的 transition 来提高性能 + transform: 'translateZ(0)', // 启用硬件加速 + backfaceVisibility: 'hidden' as const, // 减少渲染问题 + transition: 'background 0.1s linear' }; }; @@ -520,6 +583,140 @@ const checkTheme = () => { } }; +// 主题色相关函数 +const toggleThemeColorPanel = () => { + showThemeColorPanel.value = !showThemeColorPanel.value; +}; + +const handleColorChange = (color: string) => { + // 验证颜色有效性 + if (!validateColor(color)) { + console.error('Invalid color received:', color); + return; + } + + try { + currentHighlightColor.value = color; + updateThemeColorWithTransition(color); + + // 更新 lyricSetting 中的 highlightColor + lyricSetting.value.highlightColor = color; + + // 同时保存到专用的主题色存储 + saveLyricThemeColor(color); + } catch (error) { + console.error('Failed to handle color change:', error); + // 恢复到默认颜色 + const defaultColor = getCurrentLyricThemeColor(lyricSetting.value.theme); + currentHighlightColor.value = defaultColor; + updateThemeColorWithTransition(defaultColor); + } +}; + +const handleThemeColorPanelClose = () => { + showThemeColorPanel.value = false; +}; + +// 导出重置函数以供将来使用 +const resetThemeColor = () => { + // 重置到默认颜色 + const defaultColor = getCurrentLyricThemeColor(lyricSetting.value.theme); + + // 更新所有相关状态 + currentHighlightColor.value = defaultColor; + lyricSetting.value.highlightColor = undefined; + updateThemeColorWithTransition(defaultColor); + + // 清除专用存储 + try { + const settings = loadLyricSettings(); + delete settings.highlightColor; + saveLyricSettings(settings); + } catch (error) { + console.error('Failed to reset theme color:', error); + } +}; + +// 验证和修复颜色设置 +const validateAndFixColorSettings = () => { + try { + // 检查当前高亮颜色是否有效 + if (currentHighlightColor.value && !validateColor(currentHighlightColor.value)) { + console.warn('Current highlight color is invalid, resetting to default'); + const defaultColor = getCurrentLyricThemeColor(lyricSetting.value.theme); + currentHighlightColor.value = defaultColor; + lyricSetting.value.highlightColor = undefined; + updateCSSVariable('--lyric-highlight-color', defaultColor); + } + + // 检查 lyricSetting 中的颜色是否有效 + if (lyricSetting.value.highlightColor && !validateColor(lyricSetting.value.highlightColor)) { + console.warn('Stored highlight color is invalid, removing it'); + lyricSetting.value.highlightColor = undefined; + } + } catch (error) { + console.error('Failed to validate color settings:', error); + // 完全重置到默认状态 + const defaultColor = getCurrentLyricThemeColor(lyricSetting.value.theme); + currentHighlightColor.value = defaultColor; + lyricSetting.value.highlightColor = undefined; + updateCSSVariable('--lyric-highlight-color', defaultColor); + } +}; + +// 暴露函数 +defineExpose({ + resetThemeColor, + validateAndFixColorSettings +}); + +const updateCSSVariable = (name: string, value: string) => { + document.documentElement.style.setProperty(name, value); +}; + +const updateThemeColorWithTransition = (newColor: string) => { + // 添加过渡类 + const lyricWindow = document.querySelector('.lyric-window'); + if (lyricWindow) { + lyricWindow.classList.add('color-transitioning'); + } + + // 更新CSS变量 + updateCSSVariable('--lyric-highlight-color', newColor); + + // 移除过渡类 + setTimeout(() => { + if (lyricWindow) { + lyricWindow.classList.remove('color-transitioning'); + } + }, 300); +}; + +const initializeThemeColor = () => { + // 优先从 lyricSetting 中读取颜色 + let savedColor = lyricSetting.value.highlightColor; + + // 如果 lyricSetting 中没有,则从专用存储中读取 + if (!savedColor) { + savedColor = loadLyricThemeColor(); + // 如果从专用存储中读取到了颜色,同步到 lyricSetting + if (savedColor) { + lyricSetting.value.highlightColor = savedColor; + } + } + + if (savedColor) { + const optimizedColor = getCurrentLyricThemeColor(lyricSetting.value.theme); + currentHighlightColor.value = optimizedColor; + updateCSSVariable('--lyric-highlight-color', optimizedColor); + } else { + // 如果没有保存的颜色,使用默认颜色 + const defaultColor = getCurrentLyricThemeColor(lyricSetting.value.theme); + currentHighlightColor.value = defaultColor; + updateCSSVariable('--lyric-highlight-color', defaultColor); + } +}; + // const handleTop = () => { // lyricSetting.value.isTop = !lyricSetting.value.isTop; // windowData.electron.ipcRenderer.send('top-lyric', lyricSetting.value.isTop); @@ -534,14 +731,35 @@ const handleClose = () => { windowData.electron.ipcRenderer.send('close-lyric'); }; +// 安全保存歌词设置 +const saveLyricSettings = (settings: typeof lyricSetting.value) => { + try { + localStorage.setItem('lyricData', JSON.stringify(settings)); + } catch (error) { + console.error('Failed to save lyric settings:', error); + } +}; + watch( () => lyricSetting.value, - (newValue: any) => { - localStorage.setItem('lyricData', JSON.stringify(newValue)); + (newValue) => { + saveLyricSettings(newValue); }, { deep: true } ); +// 监听主题切换,自动调整颜色 +watch( + () => lyricSetting.value.theme, + (newTheme) => { + if (currentHighlightColor.value) { + const optimizedColor = getCurrentLyricThemeColor(newTheme); + currentHighlightColor.value = optimizedColor; + updateThemeColorWithTransition(optimizedColor); + } + } +); + // 添加拖动相关变量 const isDragging = ref(false); const startPosition = ref({ x: 0, y: 0 }); @@ -626,6 +844,12 @@ onMounted(() => { } }; } + + // 初始化主题色 + initializeThemeColor(); + + // 验证和修复颜色设置 + validateAndFixColorSettings(); }); // 添加播放控制相关的函数 @@ -663,6 +887,18 @@ body, transition: background-color 0.3s ease; cursor: default; border-radius: 14px; + + &.color-transitioning { + .lyric-text-inner { + transition: background 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + } + + .control-button { + i { + transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + } + } + } &:hover { .control-bar { @@ -680,7 +916,7 @@ body, &.dark { --text-color: #ffffff; --text-secondary: #ffffffea; - --highlight-color: #1db954; + --highlight-color: var(--lyric-highlight-color, #1ed760); --control-bg: rgba(124, 124, 124, 0.3); &:hover:not(.lyric_lock) { background: rgba(44, 44, 44, 0.466) !important; @@ -688,10 +924,10 @@ body, } &.light { - --text-color: #333333; - --text-secondary: #39393989; - --highlight-color: #1db954; - --control-bg: rgba(255, 255, 255, 0.3); + --text-color: #383838; + --text-secondary: #282828ae; + --highlight-color: var(--lyric-highlight-color, #1db954); + --control-bg: rgba(38, 38, 38, 0.532); &:hover:not(.lyric_lock) { background: rgba(0, 0, 0, 0.434) !important; } @@ -775,6 +1011,16 @@ body, color: var(--highlight-color); } } + + &.theme-color-button { + &.active { + background: var(--control-bg); + + i { + color: var(--highlight-color); + } + } + } } .lyric-container { @@ -816,6 +1062,19 @@ body, &.lyric-line-current { transform: scale(1.05); opacity: 1; + + // 当前播放歌词的特殊样式 + .lyric-text { + // 移除阴影,避免干扰渐变效果 + text-shadow: none; + + .lyric-text-inner { + // 为渐变文字添加轻微的外发光 + filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5)); + // 确保渐变效果清晰 + -webkit-font-smoothing: antialiased; + } + } } &.lyric-line-passed { @@ -829,9 +1088,22 @@ body, color: var(--text-color); white-space: pre-wrap; word-break: break-all; - transition: all 0.2s ease; + transition: transform 0.2s ease; line-height: 1.4; - -webkit-text-stroke: 0.5px #0000008a; + // 优化字体渲染 + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + // 为非当前播放的歌词添加阴影效果 + text-shadow: + 0 0 2px rgba(0, 0, 0, 0.8), + 0 1px 1px rgba(0, 0, 0, 0.6), + 0 0 4px rgba(255, 255, 255, 0.2); + + .lyric-text-inner { + transition: background 0.3s ease; + } } .lyric-translation { @@ -839,7 +1111,15 @@ body, white-space: pre-wrap; word-break: break-all; transition: font-size 0.2s ease; - line-height: 1.4; // 添加行高比例 + line-height: 1.4; + + // 为翻译文本也添加阴影效果,但稍微轻一些 + text-shadow: + 0 0 2px rgba(0, 0, 0, 0.7), + 0 1px 1px rgba(0, 0, 0, 0.5), + 0 0 4px rgba(255, 255, 255, 0.2), + 1px 1px 1px rgba(0, 0, 0, 0.4), + -1px -1px 1px rgba(0, 0, 0, 0.4); } .lyric-empty { @@ -847,6 +1127,12 @@ body, color: var(--text-secondary); font-size: 16px; padding: 20px; + + // 为空歌词提示也添加阴影效果 + text-shadow: + 0 0 2px rgba(0, 0, 0, 0.7), + 0 1px 1px rgba(0, 0, 0, 0.5), + 0 0 4px rgba(255, 255, 255, 0.2); } body {