2025-01-18 03:25:21 +08:00
|
|
|
import { useDebounceFn } from '@vueuse/core';
|
|
|
|
|
import tinycolor from 'tinycolor2';
|
|
|
|
|
|
2024-10-18 18:37:53 +08:00
|
|
|
interface IColor {
|
|
|
|
|
backgroundColor: string;
|
|
|
|
|
primaryColor: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-18 03:25:21 +08:00
|
|
|
interface ITextColors {
|
|
|
|
|
primary: string;
|
|
|
|
|
active: string;
|
|
|
|
|
theme: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-23 22:47:22 +08:00
|
|
|
export interface LyricThemeColor {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
light: string;
|
|
|
|
|
dark: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface LyricSettings {
|
|
|
|
|
isTop: boolean;
|
|
|
|
|
theme: 'light' | 'dark';
|
|
|
|
|
isLock: boolean;
|
|
|
|
|
highlightColor?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-18 18:37:53 +08:00
|
|
|
export const getImageLinearBackground = async (imageSrc: string): Promise<IColor> => {
|
2024-09-18 15:11:20 +08:00
|
|
|
try {
|
|
|
|
|
const primaryColor = await getImagePrimaryColor(imageSrc);
|
2024-10-18 18:37:53 +08:00
|
|
|
return {
|
|
|
|
|
backgroundColor: generateGradientBackground(primaryColor),
|
2025-01-01 02:25:18 +08:00
|
|
|
primaryColor
|
2024-10-18 18:37:53 +08:00
|
|
|
};
|
2024-09-18 15:11:20 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('error', error);
|
2024-10-18 18:37:53 +08:00
|
|
|
return {
|
|
|
|
|
backgroundColor: '',
|
2025-01-01 02:25:18 +08:00
|
|
|
primaryColor: ''
|
2024-10-18 18:37:53 +08:00
|
|
|
};
|
2024-09-18 15:11:20 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-10-18 18:37:53 +08:00
|
|
|
export const getImageBackground = async (img: HTMLImageElement): Promise<IColor> => {
|
2024-09-18 15:11:20 +08:00
|
|
|
try {
|
|
|
|
|
const primaryColor = await getImageColor(img);
|
2024-10-18 18:37:53 +08:00
|
|
|
return {
|
|
|
|
|
backgroundColor: generateGradientBackground(primaryColor),
|
2025-01-01 02:25:18 +08:00
|
|
|
primaryColor
|
2024-10-18 18:37:53 +08:00
|
|
|
};
|
2024-09-18 15:11:20 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('error', error);
|
2024-10-18 18:37:53 +08:00
|
|
|
return {
|
|
|
|
|
backgroundColor: '',
|
2025-01-01 02:25:18 +08:00
|
|
|
primaryColor: ''
|
2024-10-18 18:37:53 +08:00
|
|
|
};
|
2024-09-18 15:11:20 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getImageColor = (img: HTMLImageElement): Promise<string> => {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
if (!ctx) {
|
|
|
|
|
reject(new Error('无法获取canvas上下文'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
canvas.width = img.width;
|
|
|
|
|
canvas.height = img.height;
|
|
|
|
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
|
|
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
|
|
|
const color = getAverageColor(imageData.data);
|
|
|
|
|
resolve(`rgb(${color.join(',')})`);
|
|
|
|
|
});
|
2024-09-14 18:22:56 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getImagePrimaryColor = (imageSrc: string): Promise<string> => {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const img = new Image();
|
|
|
|
|
img.crossOrigin = 'Anonymous';
|
|
|
|
|
img.src = imageSrc;
|
|
|
|
|
|
|
|
|
|
img.onload = () => {
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
if (!ctx) {
|
|
|
|
|
reject(new Error('无法获取canvas上下文'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
canvas.width = img.width;
|
|
|
|
|
canvas.height = img.height;
|
|
|
|
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
|
|
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
|
|
|
const color = getAverageColor(imageData.data);
|
|
|
|
|
resolve(`rgb(${color.join(',')})`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
img.onerror = () => reject(new Error('图片加载失败'));
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getAverageColor = (data: Uint8ClampedArray): number[] => {
|
|
|
|
|
let r = 0;
|
|
|
|
|
let g = 0;
|
|
|
|
|
let b = 0;
|
|
|
|
|
let count = 0;
|
|
|
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
|
|
|
r += data[i];
|
|
|
|
|
g += data[i + 1];
|
|
|
|
|
b += data[i + 2];
|
|
|
|
|
count++;
|
|
|
|
|
}
|
|
|
|
|
return [Math.round(r / count), Math.round(g / count), Math.round(b / count)];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const generateGradientBackground = (color: string): string => {
|
2025-01-18 03:25:21 +08:00
|
|
|
const tc = tinycolor(color);
|
|
|
|
|
const hsl = tc.toHsl();
|
2024-09-14 18:22:56 +08:00
|
|
|
|
|
|
|
|
// 增加亮度和暗度的差异
|
2025-01-18 03:25:21 +08:00
|
|
|
const lightColor = tinycolor({ h: hsl.h, s: hsl.s * 0.8, l: Math.min(hsl.l + 0.2, 0.95) });
|
|
|
|
|
const midColor = tinycolor({ h: hsl.h, s: hsl.s, l: hsl.l });
|
|
|
|
|
const darkColor = tinycolor({
|
|
|
|
|
h: hsl.h,
|
|
|
|
|
s: Math.min(hsl.s * 1.2, 1),
|
|
|
|
|
l: Math.max(hsl.l - 0.3, 0.05)
|
|
|
|
|
});
|
2024-09-14 18:22:56 +08:00
|
|
|
|
2025-01-18 03:25:21 +08:00
|
|
|
return `linear-gradient(to bottom, ${lightColor.toRgbString()} 0%, ${midColor.toRgbString()} 50%, ${darkColor.toRgbString()} 100%)`;
|
2024-09-14 18:22:56 +08:00
|
|
|
};
|
|
|
|
|
|
2025-01-18 03:25:21 +08:00
|
|
|
export const parseGradient = (gradientStr: string) => {
|
|
|
|
|
if (!gradientStr) return [];
|
|
|
|
|
|
|
|
|
|
// 处理非渐变色
|
|
|
|
|
if (!gradientStr.startsWith('linear-gradient')) {
|
|
|
|
|
const color = tinycolor(gradientStr);
|
|
|
|
|
if (color.isValid()) {
|
|
|
|
|
const rgb = color.toRgb();
|
|
|
|
|
return [{ r: rgb.r, g: rgb.g, b: rgb.b }];
|
2024-09-14 18:22:56 +08:00
|
|
|
}
|
2025-01-18 03:25:21 +08:00
|
|
|
return [];
|
2024-09-14 18:22:56 +08:00
|
|
|
}
|
|
|
|
|
|
2025-01-18 03:25:21 +08:00
|
|
|
// 处理渐变色,支持 rgb、rgba 和十六进制颜色
|
|
|
|
|
const colorMatches = gradientStr.match(/(?:(?:rgb|rgba)\([^)]+\)|#[0-9a-fA-F]{3,8})/g) || [];
|
|
|
|
|
return colorMatches.map((color) => {
|
|
|
|
|
const tc = tinycolor(color);
|
|
|
|
|
const rgb = tc.toRgb();
|
|
|
|
|
return { r: rgb.r, g: rgb.g, b: rgb.b };
|
2024-11-23 22:42:23 +08:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getTextColors = (gradient: string = ''): ITextColors => {
|
|
|
|
|
const defaultColors = {
|
|
|
|
|
primary: 'rgba(255, 255, 255, 0.54)',
|
|
|
|
|
active: '#ffffff',
|
2025-01-01 02:25:18 +08:00
|
|
|
theme: 'light'
|
2024-11-23 22:42:23 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!gradient) return defaultColors;
|
|
|
|
|
|
|
|
|
|
const colors = parseGradient(gradient);
|
|
|
|
|
if (!colors.length) return defaultColors;
|
|
|
|
|
|
2025-01-18 03:25:21 +08:00
|
|
|
const mainColor = colors.length === 1 ? colors[0] : colors[1] || colors[0];
|
|
|
|
|
const tc = tinycolor(mainColor);
|
|
|
|
|
const isDark = tc.getBrightness() > 155; // tinycolor 的亮度范围是 0-255
|
2024-11-23 22:42:23 +08:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
primary: isDark ? 'rgba(0, 0, 0, 0.54)' : 'rgba(255, 255, 255, 0.54)',
|
|
|
|
|
active: isDark ? '#000000' : '#ffffff',
|
2025-01-01 02:25:18 +08:00
|
|
|
theme: isDark ? 'dark' : 'light'
|
2024-11-23 22:42:23 +08:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getHoverBackgroundColor = (isDark: boolean): string => {
|
|
|
|
|
return isDark ? 'rgba(0, 0, 0, 0.08)' : 'rgba(255, 255, 255, 0.08)';
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-18 03:25:21 +08:00
|
|
|
export const animateGradient = (() => {
|
|
|
|
|
let currentAnimation: number | null = null;
|
|
|
|
|
let isAnimating = false;
|
|
|
|
|
let lastProgress = 0;
|
|
|
|
|
|
|
|
|
|
const validateColors = (colors: ReturnType<typeof parseGradient>) => {
|
|
|
|
|
return colors.every(
|
|
|
|
|
(color) =>
|
|
|
|
|
typeof color.r === 'number' &&
|
|
|
|
|
typeof color.g === 'number' &&
|
|
|
|
|
typeof color.b === 'number' &&
|
|
|
|
|
!Number.isNaN(color.r) &&
|
|
|
|
|
!Number.isNaN(color.g) &&
|
|
|
|
|
!Number.isNaN(color.b)
|
|
|
|
|
);
|
|
|
|
|
};
|
2024-11-23 22:42:23 +08:00
|
|
|
|
2025-01-18 03:25:21 +08:00
|
|
|
const easeInOutCubic = (x: number): number => {
|
|
|
|
|
return x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;
|
|
|
|
|
};
|
2024-11-23 22:42:23 +08:00
|
|
|
|
2025-01-18 03:25:21 +08:00
|
|
|
const animate = (
|
|
|
|
|
oldGradient: string,
|
|
|
|
|
newGradient: string,
|
|
|
|
|
onUpdate: (gradient: string) => void,
|
|
|
|
|
duration = 300
|
|
|
|
|
) => {
|
|
|
|
|
// 如果新旧渐变色相同,不执行动画
|
|
|
|
|
if (oldGradient === newGradient) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2024-11-23 22:42:23 +08:00
|
|
|
|
2025-01-18 03:25:21 +08:00
|
|
|
// 如果正在动画中,取消当前动画
|
|
|
|
|
if (currentAnimation !== null) {
|
|
|
|
|
cancelAnimationFrame(currentAnimation);
|
|
|
|
|
currentAnimation = null;
|
|
|
|
|
}
|
2024-11-23 22:42:23 +08:00
|
|
|
|
2025-01-18 03:25:21 +08:00
|
|
|
// 解析颜色
|
|
|
|
|
const startColors = parseGradient(oldGradient);
|
|
|
|
|
const endColors = parseGradient(newGradient);
|
|
|
|
|
|
|
|
|
|
// 验证颜色数组
|
|
|
|
|
if (
|
|
|
|
|
!startColors.length ||
|
|
|
|
|
!endColors.length ||
|
|
|
|
|
!validateColors(startColors) ||
|
|
|
|
|
!validateColors(endColors)
|
|
|
|
|
) {
|
|
|
|
|
console.warn('Invalid color values detected');
|
|
|
|
|
onUpdate(newGradient); // 直接更新到目标颜色
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2024-11-23 22:42:23 +08:00
|
|
|
|
2025-01-18 03:25:21 +08:00
|
|
|
// 如果颜色数量不匹配,直接更新到目标颜色
|
|
|
|
|
if (startColors.length !== endColors.length) {
|
|
|
|
|
onUpdate(newGradient);
|
|
|
|
|
return null;
|
2024-11-23 22:42:23 +08:00
|
|
|
}
|
2025-01-18 03:25:21 +08:00
|
|
|
|
|
|
|
|
isAnimating = true;
|
|
|
|
|
const startTime = performance.now();
|
|
|
|
|
|
|
|
|
|
const animateFrame = (currentTime: number) => {
|
|
|
|
|
if (!isAnimating) return null;
|
|
|
|
|
|
|
|
|
|
const elapsed = currentTime - startTime;
|
|
|
|
|
const rawProgress = Math.min(elapsed / duration, 1);
|
|
|
|
|
// 使用缓动函数使动画更平滑
|
|
|
|
|
const progress = easeInOutCubic(rawProgress);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 使用上一帧的进度来平滑过渡
|
|
|
|
|
const effectiveProgress = lastProgress + (progress - lastProgress) * 0.6;
|
|
|
|
|
lastProgress = effectiveProgress;
|
|
|
|
|
|
|
|
|
|
const currentColors = startColors.map((startColor, i) => {
|
|
|
|
|
const start = tinycolor(startColor);
|
|
|
|
|
const end = tinycolor(endColors[i]);
|
|
|
|
|
return tinycolor.mix(start, end, effectiveProgress * 100);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const gradientString = createGradientString(
|
|
|
|
|
currentColors.map((c) => {
|
|
|
|
|
const rgb = c.toRgb();
|
|
|
|
|
return { r: rgb.r, g: rgb.g, b: rgb.b };
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
onUpdate(gradientString);
|
|
|
|
|
|
|
|
|
|
if (rawProgress < 1) {
|
|
|
|
|
currentAnimation = requestAnimationFrame(animateFrame);
|
|
|
|
|
return currentAnimation;
|
|
|
|
|
}
|
|
|
|
|
// 确保最终颜色正确
|
|
|
|
|
onUpdate(newGradient);
|
|
|
|
|
isAnimating = false;
|
|
|
|
|
currentAnimation = null;
|
|
|
|
|
lastProgress = 0;
|
|
|
|
|
return null;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Animation error:', error);
|
|
|
|
|
onUpdate(newGradient);
|
|
|
|
|
isAnimating = false;
|
|
|
|
|
currentAnimation = null;
|
|
|
|
|
lastProgress = 0;
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
currentAnimation = requestAnimationFrame(animateFrame);
|
|
|
|
|
return currentAnimation;
|
2024-11-23 22:42:23 +08:00
|
|
|
};
|
|
|
|
|
|
2025-01-18 03:25:21 +08:00
|
|
|
// 使用更短的防抖时间
|
|
|
|
|
return useDebounceFn(animate, 50);
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
export const createGradientString = (
|
|
|
|
|
colors: { r: number; g: number; b: number }[],
|
|
|
|
|
percentages = [0, 50, 100]
|
|
|
|
|
) => {
|
|
|
|
|
return `linear-gradient(to bottom, ${colors
|
|
|
|
|
.map((color, i) => `rgb(${color.r}, ${color.g}, ${color.b}) ${percentages[i]}%`)
|
|
|
|
|
.join(', ')})`;
|
2024-11-23 22:42:23 +08:00
|
|
|
};
|
2025-07-23 22:47:22 +08:00
|
|
|
|
|
|
|
|
// ===== 歌词主题色相关工具函数 =====
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 预设歌词主题色配置
|
|
|
|
|
* 注意: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);
|
|
|
|
|
};
|