进度条图标自定义

This commit is contained in:
mtvpls
2026-02-21 02:13:50 +08:00
parent 5d2f944346
commit f02dce869c
7 changed files with 273 additions and 1 deletions

BIN
public/icons/q/renako.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -6268,6 +6268,9 @@ const ThemeConfigComponent = ({
customCSS: '',
enableCache: true,
cacheMinutes: 1440, // 默认1天1440分钟
progressThumbType: 'default' as 'default' | 'preset' | 'custom',
progressThumbPresetId: '',
progressThumbCustomUrl: '',
});
const [loginBackgroundImages, setLoginBackgroundImages] = useState<string[]>(['']);
const [registerBackgroundImages, setRegisterBackgroundImages] = useState<string[]>(['']);
@@ -6280,6 +6283,9 @@ const ThemeConfigComponent = ({
customCSS: config.ThemeConfig.customCSS || '',
enableCache: config.ThemeConfig.enableCache !== false,
cacheMinutes: config.ThemeConfig.cacheMinutes || 1440,
progressThumbType: config.ThemeConfig.progressThumbType || 'default',
progressThumbPresetId: config.ThemeConfig.progressThumbPresetId || '',
progressThumbCustomUrl: config.ThemeConfig.progressThumbCustomUrl || '',
});
// 解析背景图配置
@@ -6711,6 +6717,170 @@ const ThemeConfigComponent = ({
</p>
</div>
{/* 进度条图标配置 */}
<div className='bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700'>
<h3 className='text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center gap-2'>
<Palette className='w-5 h-5' />
</h3>
<p className='text-sm text-gray-600 dark:text-gray-400 mb-4'>
</p>
{/* 图标类型选择 */}
<div className='space-y-4 mb-6'>
<label className='flex items-center space-x-3 cursor-pointer'>
<input
type='radio'
checked={themeSettings.progressThumbType === 'default'}
onChange={() =>
setThemeSettings((prev) => ({
...prev,
progressThumbType: 'default',
}))
}
className='w-4 h-4 text-blue-600'
/>
<span className='text-gray-900 dark:text-gray-100'>
</span>
</label>
<label className='flex items-center space-x-3 cursor-pointer'>
<input
type='radio'
checked={themeSettings.progressThumbType === 'preset'}
onChange={() =>
setThemeSettings((prev) => ({
...prev,
progressThumbType: 'preset',
}))
}
className='w-4 h-4 text-blue-600'
/>
<span className='text-gray-900 dark:text-gray-100'>
</span>
</label>
<label className='flex items-center space-x-3 cursor-pointer'>
<input
type='radio'
checked={themeSettings.progressThumbType === 'custom'}
onChange={() =>
setThemeSettings((prev) => ({
...prev,
progressThumbType: 'custom',
}))
}
className='w-4 h-4 text-blue-600'
/>
<span className='text-gray-900 dark:text-gray-100'>
</span>
</label>
</div>
{/* 预制图标选择 */}
{themeSettings.progressThumbType === 'preset' && (
<div className='space-y-3 mb-4'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300'>
</label>
<div className='grid grid-cols-2 md:grid-cols-3 gap-3'>
{[
{ id: 'renako', name: '玲奈子', url: '/icons/q/renako.png', color: '#ec4899' },
].map((thumb) => (
<button
key={thumb.id}
type='button'
onClick={() =>
setThemeSettings((prev) => ({
...prev,
progressThumbPresetId: thumb.id,
}))
}
className={`relative p-4 border-2 rounded-lg transition-all ${
themeSettings.progressThumbPresetId === thumb.id
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
}`}
>
<div className='flex flex-col items-center gap-2'>
<img
src={thumb.url}
alt={thumb.name}
className='w-12 h-12 object-contain'
onError={(e) => {
(e.target as HTMLImageElement).src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="48" height="48"%3E%3Crect width="48" height="48" fill="%23ddd"/%3E%3Ctext x="50%25" y="50%25" text-anchor="middle" dy=".3em" fill="%23999"%3E?%3C/text%3E%3C/svg%3E';
}}
/>
<span className='text-sm font-medium text-gray-700 dark:text-gray-300 text-center'>
{thumb.name}
</span>
<div
className='w-8 h-2 rounded-full'
style={{ backgroundColor: thumb.color }}
title='进度条颜色'
/>
</div>
{themeSettings.progressThumbPresetId === thumb.id && (
<div className='absolute top-2 right-2'>
<Check className='w-5 h-5 text-blue-600 dark:text-blue-400' />
</div>
)}
</button>
))}
</div>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-2'>
</p>
</div>
)}
{/* 自定义图标URL输入 */}
{themeSettings.progressThumbType === 'custom' && (
<div className='space-y-3'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300'>
URL
</label>
<input
type='text'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400'
placeholder='例如: https://example.com/icon.png'
value={themeSettings.progressThumbCustomUrl}
onChange={(e) =>
setThemeSettings((prev) => ({
...prev,
progressThumbCustomUrl: e.target.value,
}))
}
/>
<p className='text-xs text-gray-500 dark:text-gray-400'>
PNGJPGGIFWebP 32x32pxURL必须可公开访问
</p>
{themeSettings.progressThumbCustomUrl && (
<div className='mt-2 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg'>
<p className='text-xs text-gray-600 dark:text-gray-400 mb-2'></p>
<img
src={themeSettings.progressThumbCustomUrl}
alt='自定义图标预览'
className='w-12 h-12 object-contain border border-gray-300 dark:border-gray-600 rounded'
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
const parent = (e.target as HTMLImageElement).parentElement;
if (parent && !parent.querySelector('.error-msg')) {
const errorMsg = document.createElement('p');
errorMsg.className = 'text-xs text-red-500 error-msg';
errorMsg.textContent = '图片加载失败请检查URL是否正确';
parent.appendChild(errorMsg);
}
}}
/>
</div>
)}
</div>
)}
</div>
{/* 保存按钮 */}
<div className='flex justify-end'>
<button

View File

@@ -36,6 +36,9 @@ export async function POST(request: NextRequest) {
cacheMinutes,
loginBackgroundImage,
registerBackgroundImage,
progressThumbType,
progressThumbPresetId,
progressThumbCustomUrl,
} = body as {
enableBuiltInTheme: boolean;
builtInTheme: string;
@@ -44,6 +47,9 @@ export async function POST(request: NextRequest) {
cacheMinutes: number;
loginBackgroundImage?: string;
registerBackgroundImage?: string;
progressThumbType?: 'default' | 'preset' | 'custom';
progressThumbPresetId?: string;
progressThumbCustomUrl?: string;
};
// 参数校验
@@ -118,6 +124,9 @@ export async function POST(request: NextRequest) {
cacheVersion: cssChanged ? currentVersion + 1 : currentVersion,
loginBackgroundImage: loginBackgroundImage?.trim() || undefined,
registerBackgroundImage: registerBackgroundImage?.trim() || undefined,
progressThumbType: progressThumbType || 'default',
progressThumbPresetId: progressThumbPresetId?.trim() || undefined,
progressThumbCustomUrl: progressThumbCustomUrl?.trim() || undefined,
};
// 写入数据库

View File

@@ -50,6 +50,9 @@ export async function GET(request: NextRequest) {
OIDCButtonText: config.SiteConfig.OIDCButtonText || '',
loginBackgroundImage: config.ThemeConfig?.loginBackgroundImage || '',
registerBackgroundImage: config.ThemeConfig?.registerBackgroundImage || '',
progressThumbType: config.ThemeConfig?.progressThumbType || 'default',
progressThumbPresetId: config.ThemeConfig?.progressThumbPresetId || '',
progressThumbCustomUrl: config.ThemeConfig?.progressThumbCustomUrl || '',
// AI配置只暴露功能开关不暴露API密钥等敏感信息
AIEnabled: config.AIConfig?.Enabled || false,
AIEnableHomepageEntry: config.AIConfig?.EnableHomepageEntry || false,

View File

@@ -70,6 +70,9 @@ export default async function RootLayout({
let xiaoyaEnabled = false;
let loginBackgroundImage = '';
let registerBackgroundImage = '';
let progressThumbType = 'default';
let progressThumbPresetId = '';
let progressThumbCustomUrl = '';
let enableRegistration = false;
let loginRequireTurnstile = false;
let registrationRequireTurnstile = false;
@@ -115,6 +118,9 @@ export default async function RootLayout({
tmdbApiKey = config.SiteConfig.TMDBApiKey || '';
loginBackgroundImage = config.ThemeConfig?.loginBackgroundImage || '';
registerBackgroundImage = config.ThemeConfig?.registerBackgroundImage || '';
progressThumbType = config.ThemeConfig?.progressThumbType || 'default';
progressThumbPresetId = config.ThemeConfig?.progressThumbPresetId || '';
progressThumbCustomUrl = config.ThemeConfig?.progressThumbCustomUrl || '';
enableRegistration = config.SiteConfig.EnableRegistration || false;
loginRequireTurnstile = config.SiteConfig.LoginRequireTurnstile || false;
registrationRequireTurnstile = config.SiteConfig.RegistrationRequireTurnstile || false;
@@ -178,6 +184,9 @@ export default async function RootLayout({
PRIVATE_LIBRARY_ENABLED: openListEnabled || embyEnabled || xiaoyaEnabled,
LOGIN_BACKGROUND_IMAGE: loginBackgroundImage,
REGISTER_BACKGROUND_IMAGE: registerBackgroundImage,
PROGRESS_THUMB_TYPE: progressThumbType,
PROGRESS_THUMB_PRESET_ID: progressThumbPresetId,
PROGRESS_THUMB_CUSTOM_URL: progressThumbCustomUrl,
ENABLE_REGISTRATION: enableRegistration,
LOGIN_REQUIRE_TURNSTILE: loginRequireTurnstile,
REGISTRATION_REQUIRE_TURNSTILE: registrationRequireTurnstile,

View File

@@ -5812,6 +5812,83 @@ function PlayPageClient() {
setPlayerReady(true);
console.log('[PlayPage] Player ready, triggering sync setup');
// 应用进度条图标配置 - 尽早执行
const applyProgressThumbConfig = () => {
try {
const config = (window as any).RUNTIME_CONFIG;
if (!config || config.PROGRESS_THUMB_TYPE === 'default') {
// 使用默认样式,移除自定义样式
const oldStyle = document.getElementById('custom-progress-thumb-style');
if (oldStyle) oldStyle.remove();
return;
}
let thumbUrl = '';
let thumbColor = '#22c55e'; // 默认绿色
if (config.PROGRESS_THUMB_TYPE === 'preset' && config.PROGRESS_THUMB_PRESET_ID) {
const presetConfig: Record<string, { url: string; color: string }> = {
renako: { url: '/icons/q/renako.png', color: '#ec4899' }, // 粉色
};
const preset = presetConfig[config.PROGRESS_THUMB_PRESET_ID];
if (preset) {
thumbUrl = preset.url;
thumbColor = preset.color;
}
} else if (config.PROGRESS_THUMB_TYPE === 'custom' && config.PROGRESS_THUMB_CUSTOM_URL) {
thumbUrl = config.PROGRESS_THUMB_CUSTOM_URL;
}
// 修改 ArtPlayer 的主题色
if (artPlayerRef.current) {
artPlayerRef.current.theme = thumbColor;
}
if (thumbUrl) {
// 根据预设ID确定尺寸
let width = '30px';
let height = '30px';
let marginLeft = '-15px';
// renako 图标特殊处理288x404比例放大1.25倍)
if (config.PROGRESS_THUMB_TYPE === 'preset' && config.PROGRESS_THUMB_PRESET_ID === 'renako') {
width = '26.875px'; // 21.5 * 1.25
height = '37.5px'; // 30 * 1.25
marginLeft = '-13.4375px'; // 10.75 * 1.25
}
// 动态设置背景图片
const style = document.createElement('style');
style.id = 'custom-progress-thumb-style';
style.textContent = `
/* 替换默认的进度条圆点为自定义图标 */
.art-video-player .art-progress-indicator {
width: ${width} !important;
height: ${height} !important;
background-image: url('${thumbUrl}') !important;
background-size: contain !important;
background-repeat: no-repeat !important;
background-position: center !important;
background-color: transparent !important;
border-radius: 0 !important;
margin-left: ${marginLeft} !important;
}
`;
// 移除旧样式
const oldStyle = document.getElementById('custom-progress-thumb-style');
if (oldStyle) oldStyle.remove();
document.head.appendChild(style);
}
} catch (error) {
console.error('[进度条图标] 应用配置失败:', error);
}
};
applyProgressThumbConfig();
// 添加字幕切换功能
const currentSubtitles = detailRef.current?.subtitles?.[currentEpisodeIndex] || [];
if (currentSubtitles.length > 0 && artPlayerRef.current) {
@@ -6031,7 +6108,7 @@ function PlayPageClient() {
saveDanmakuDisplayState(false);
});
}
}
// 播放器就绪后,如果正在播放则请求 Wake Lock

View File

@@ -115,6 +115,10 @@ export interface AdminConfig {
cacheVersion: number; // CSS版本号用于缓存控制
loginBackgroundImage?: string; // 登录界面背景图
registerBackgroundImage?: string; // 注册界面背景图
// 进度条图标配置
progressThumbType?: 'default' | 'preset' | 'custom'; // 图标类型
progressThumbPresetId?: string; // 预制图标ID
progressThumbCustomUrl?: string; // 自定义图标URL
};
OpenListConfig?: {
Enabled: boolean; // 是否启用私人影库功能