Files
KatelyaTV/src/components/SkipController.tsx
T
katelya d5726c4f07 feat: Implement intelligent skip feature with enhanced settings
- Added support for time input in minutes:seconds format for skipping segments.
- Introduced automatic skipping functionality for both opening and ending segments.
- Enhanced UI for skip settings with a floating configuration card.
- Implemented countdown timer for automatic next episode playback.
- Added batch settings for configuring multiple skip segments at once.
- Updated SkipController component to handle new skip logic and UI changes.
- Created comprehensive usage guide for the new skip feature.
2025-09-02 14:49:56 +08:00

778 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import {
deleteSkipConfig,
EpisodeSkipConfig,
getSkipConfig,
saveSkipConfig,
SkipSegment,
} from '@/lib/db.client';
interface SkipControllerProps {
source: string;
id: string;
title: string;
artPlayerRef: React.MutableRefObject<any>;
currentTime?: number;
duration?: number;
isSettingMode?: boolean;
onSettingModeChange?: (isOpen: boolean) => void;
onNextEpisode?: () => void; // 新增:跳转下一集的回调
}
export default function SkipController({
source,
id,
title,
artPlayerRef,
currentTime = 0,
duration = 0,
isSettingMode = false,
onSettingModeChange,
onNextEpisode,
}: SkipControllerProps) {
const [skipConfig, setSkipConfig] = useState<EpisodeSkipConfig | null>(null);
const [showSkipButton, setShowSkipButton] = useState(false);
const [currentSkipSegment, setCurrentSkipSegment] = useState<SkipSegment | null>(null);
const [newSegment, setNewSegment] = useState<Partial<SkipSegment>>({});
// 新增状态:批量设置模式 - 支持分:秒格式
const [batchSettings, setBatchSettings] = useState({
openingStart: '0:00', // 片头开始时间(分:秒格式)
openingEnd: '1:30', // 片头结束时间(分:秒格式,90秒=1分30秒)
endingStart: '20:00', // 片尾开始时间(分:秒格式)
endingEnd: '', // 片尾结束时间(可选,空表示直接跳转下一集)
autoSkip: true, // 自动跳过开关
autoNextEpisode: true, // 自动下一集开关
});
const [showCountdown, setShowCountdown] = useState(false);
const [countdownSeconds, setCountdownSeconds] = useState(0);
const lastSkipTimeRef = useRef<number>(0);
const skipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const autoSkipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const countdownIntervalRef = useRef<NodeJS.Timeout | null>(null);
// 时间格式转换函数
const timeToSeconds = useCallback((timeStr: string): number => {
if (!timeStr || timeStr.trim() === '') return 0;
// 支持多种格式: "2:10", "2:10.5", "130", "130.5"
if (timeStr.includes(':')) {
const parts = timeStr.split(':');
const minutes = parseInt(parts[0]) || 0;
const seconds = parseFloat(parts[1]) || 0;
return minutes * 60 + seconds;
} else {
return parseFloat(timeStr) || 0;
}
}, []);
const secondsToTime = useCallback((seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
const decimal = seconds % 1;
if (decimal > 0) {
return `${mins}:${secs.toString().padStart(2, '0')}.${Math.floor(decimal * 10)}`;
}
return `${mins}:${secs.toString().padStart(2, '0')}`;
}, []);
// 加载跳过配置
const loadSkipConfig = useCallback(async () => {
try {
const config = await getSkipConfig(source, id);
setSkipConfig(config);
} catch (err) {
console.error('加载跳过配置失败:', err);
}
}, [source, id]);
// 自动跳过逻辑
const handleAutoSkip = useCallback((segment: SkipSegment) => {
if (!artPlayerRef.current) return;
const targetTime = segment.end + 1;
artPlayerRef.current.currentTime = targetTime;
lastSkipTimeRef.current = Date.now();
// 显示跳过提示
if (artPlayerRef.current.notice) {
const segmentName = segment.type === 'opening' ? '片头' : '片尾';
artPlayerRef.current.notice.show = `自动跳过${segmentName}`;
}
setCurrentSkipSegment(null);
}, [artPlayerRef]);
// 开始片尾倒计时
const startEndingCountdown = useCallback((seconds: number) => {
setShowCountdown(true);
setCountdownSeconds(seconds);
if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current);
}
countdownIntervalRef.current = setInterval(() => {
setCountdownSeconds(prev => {
if (prev <= 1) {
// 倒计时结束,跳转下一集
if (onNextEpisode) {
onNextEpisode();
}
setShowCountdown(false);
if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current);
}
return 0;
}
return prev - 1;
});
}, 1000);
}, [onNextEpisode]);
// 检查片尾倒计时
const checkEndingCountdown = useCallback((time: number) => {
if (!skipConfig?.segments?.length || !duration || !onNextEpisode) return;
const endingSegments = skipConfig.segments.filter(s => s.type === 'ending' && s.autoNextEpisode !== false);
if (!endingSegments.length) return;
for (const segment of endingSegments) {
const timeToEnd = duration - time;
const timeToSegmentStart = duration - segment.start;
// 当距离视频结束的时间等于设定的片尾开始时间时,开始倒计时
if (timeToEnd <= timeToSegmentStart && timeToEnd > 0 && !showCountdown) {
startEndingCountdown(Math.ceil(timeToEnd));
break;
}
}
}, [skipConfig, duration, onNextEpisode, showCountdown, startEndingCountdown]);
// 检查当前播放时间是否在跳过区间内
const checkSkipSegment = useCallback(
(time: number) => {
if (!skipConfig?.segments?.length) return;
const currentSegment = skipConfig.segments.find(
(segment) => time >= segment.start && time <= segment.end
);
if (currentSegment && currentSegment !== currentSkipSegment) {
setCurrentSkipSegment(currentSegment);
// 检查是否开启自动跳过
const hasAutoSkipSetting = skipConfig.segments.some(s => s.autoSkip !== false);
if (hasAutoSkipSetting) {
// 自动跳过:延迟1秒执行跳过
if (autoSkipTimeoutRef.current) {
clearTimeout(autoSkipTimeoutRef.current);
}
autoSkipTimeoutRef.current = setTimeout(() => {
handleAutoSkip(currentSegment);
}, 1000);
setShowSkipButton(false); // 自动跳过时不显示按钮
} else {
// 手动模式:显示跳过按钮
setShowSkipButton(true);
// 自动隐藏跳过按钮
if (skipTimeoutRef.current) {
clearTimeout(skipTimeoutRef.current);
}
skipTimeoutRef.current = setTimeout(() => {
setShowSkipButton(false);
setCurrentSkipSegment(null);
}, 8000);
}
} else if (!currentSegment && currentSkipSegment) {
setCurrentSkipSegment(null);
setShowSkipButton(false);
if (skipTimeoutRef.current) {
clearTimeout(skipTimeoutRef.current);
}
if (autoSkipTimeoutRef.current) {
clearTimeout(autoSkipTimeoutRef.current);
}
}
// 检查片尾倒计时
checkEndingCountdown(time);
},
[skipConfig, currentSkipSegment, handleAutoSkip, checkEndingCountdown]
);
// 执行跳过
const handleSkip = useCallback(() => {
if (!currentSkipSegment || !artPlayerRef.current) return;
const targetTime = currentSkipSegment.end + 1; // 跳到片段结束后1秒
artPlayerRef.current.currentTime = targetTime;
lastSkipTimeRef.current = Date.now();
setShowSkipButton(false);
setCurrentSkipSegment(null);
if (skipTimeoutRef.current) {
clearTimeout(skipTimeoutRef.current);
}
// 显示跳过提示
if (artPlayerRef.current.notice) {
const segmentName = currentSkipSegment.type === 'opening' ? '片头' : '片尾';
artPlayerRef.current.notice.show = `已跳过${segmentName}`;
}
}, [currentSkipSegment, artPlayerRef]);
// 保存新的跳过片段(单个片段模式)
const handleSaveSegment = useCallback(async () => {
if (!newSegment.start || !newSegment.end || !newSegment.type) {
alert('请填写完整的跳过片段信息');
return;
}
if (newSegment.start >= newSegment.end) {
alert('开始时间必须小于结束时间');
return;
}
try {
const segment: SkipSegment = {
start: newSegment.start,
end: newSegment.end,
type: newSegment.type as 'opening' | 'ending',
title: newSegment.title || (newSegment.type === 'opening' ? '片头' : '片尾'),
autoSkip: true, // 默认开启自动跳过
autoNextEpisode: newSegment.type === 'ending', // 片尾默认开启自动下一集
};
const updatedConfig: EpisodeSkipConfig = {
source,
id,
title,
segments: skipConfig?.segments ? [...skipConfig.segments, segment] : [segment],
updated_time: Date.now(),
};
await saveSkipConfig(source, id, updatedConfig);
setSkipConfig(updatedConfig);
onSettingModeChange?.(false);
setNewSegment({});
alert('跳过片段已保存');
} catch (err) {
console.error('保存跳过片段失败:', err);
alert('保存失败,请重试');
}
}, [newSegment, skipConfig, source, id, title, onSettingModeChange]);
// 保存批量设置的跳过配置
const handleSaveBatchSettings = useCallback(async () => {
const segments: SkipSegment[] = [];
// 添加片头设置
if (batchSettings.openingStart && batchSettings.openingEnd) {
const start = timeToSeconds(batchSettings.openingStart);
const end = timeToSeconds(batchSettings.openingEnd);
if (start >= end) {
alert('片头开始时间必须小于结束时间');
return;
}
segments.push({
start,
end,
type: 'opening',
title: '片头',
autoSkip: batchSettings.autoSkip,
});
}
// 添加片尾设置
if (batchSettings.endingStart) {
const endingStartSeconds = timeToSeconds(batchSettings.endingStart);
// 如果没有设置结束时间,则直接跳转到下一集
if (!batchSettings.endingEnd || batchSettings.endingEnd.trim() === '') {
// 直接从指定时间跳转下一集
segments.push({
start: endingStartSeconds,
end: duration, // 设置为视频总长度
type: 'ending',
title: '片尾跳转下一集',
autoSkip: batchSettings.autoSkip,
autoNextEpisode: batchSettings.autoNextEpisode,
});
} else {
const endingEndSeconds = timeToSeconds(batchSettings.endingEnd);
if (endingStartSeconds >= endingEndSeconds) {
alert('片尾开始时间必须小于结束时间');
return;
}
segments.push({
start: endingStartSeconds,
end: endingEndSeconds,
type: 'ending',
title: '片尾',
autoSkip: batchSettings.autoSkip,
autoNextEpisode: batchSettings.autoNextEpisode,
});
}
}
if (segments.length === 0) {
alert('请至少设置片头或片尾时间');
return;
}
try {
const updatedConfig: EpisodeSkipConfig = {
source,
id,
title,
segments,
updated_time: Date.now(),
};
await saveSkipConfig(source, id, updatedConfig);
setSkipConfig(updatedConfig);
onSettingModeChange?.(false);
// 重置批量设置
setBatchSettings({
openingStart: '0:00',
openingEnd: '1:30',
endingStart: '20:00',
endingEnd: '',
autoSkip: true,
autoNextEpisode: true,
});
alert('跳过配置已保存');
} catch (err) {
console.error('保存跳过配置失败:', err);
alert('保存失败,请重试');
}
}, [batchSettings, duration, source, id, title, onSettingModeChange, timeToSeconds]);
// 删除跳过片段
const handleDeleteSegment = useCallback(
async (index: number) => {
if (!skipConfig?.segments) return;
try {
const updatedSegments = skipConfig.segments.filter((_, i) => i !== index);
if (updatedSegments.length === 0) {
// 如果没有片段了,删除整个配置
await deleteSkipConfig(source, id);
setSkipConfig(null);
} else {
// 更新配置
const updatedConfig: EpisodeSkipConfig = {
...skipConfig,
segments: updatedSegments,
updated_time: Date.now(),
};
await saveSkipConfig(source, id, updatedConfig);
setSkipConfig(updatedConfig);
}
alert('跳过片段已删除');
} catch (err) {
console.error('删除跳过片段失败:', err);
alert('删除失败,请重试');
}
},
[skipConfig, source, id]
);
// 格式化时间显示
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
// 初始化加载配置
useEffect(() => {
loadSkipConfig();
}, [loadSkipConfig]);
// 监听播放时间变化
useEffect(() => {
if (currentTime > 0) {
checkSkipSegment(currentTime);
}
}, [currentTime, checkSkipSegment]);
// 清理定时器
useEffect(() => {
return () => {
if (skipTimeoutRef.current) {
clearTimeout(skipTimeoutRef.current);
}
if (autoSkipTimeoutRef.current) {
clearTimeout(autoSkipTimeoutRef.current);
}
if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current);
}
};
}, []);
return (
<div className="skip-controller">
{/* 倒计时显示 - 片尾自动跳转下一集 */}
{showCountdown && (
<div className="fixed top-20 left-1/2 transform -translate-x-1/2 z-50 bg-blue-600/90 text-white px-6 py-3 rounded-lg backdrop-blur-sm border border-white/20 shadow-lg animate-fade-in">
<div className="flex items-center space-x-3">
<svg className="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-sm font-medium">
{countdownSeconds}
</span>
<button
onClick={() => {
setShowCountdown(false);
if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current);
}
}}
className="px-2 py-1 bg-white/20 hover:bg-white/30 rounded text-xs transition-colors"
>
</button>
</div>
</div>
)}
{/* 跳过按钮 */}
{showSkipButton && currentSkipSegment && (
<div className="fixed top-20 right-4 z-50 bg-black/80 text-white px-4 py-2 rounded-lg backdrop-blur-sm border border-white/20 shadow-lg animate-fade-in">
<div className="flex items-center space-x-3">
<span className="text-sm">
{currentSkipSegment.type === 'opening' ? '检测到片头' : '检测到片尾'}
</span>
<button
onClick={handleSkip}
className="px-3 py-1 bg-green-600 hover:bg-green-700 rounded text-sm font-medium transition-colors"
>
</button>
</div>
</div>
)}
{/* 设置模式面板 - 增强版批量设置 */}
{isSettingMode && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100">
</h3>
{/* 全局开关 */}
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg mb-6">
<div className="flex items-center justify-between mb-2">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={batchSettings.autoSkip}
onChange={(e) => setBatchSettings({...batchSettings, autoSkip: e.target.checked})}
className="rounded"
/>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
</span>
</label>
</div>
<div className="flex items-center justify-between">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={batchSettings.autoNextEpisode}
onChange={(e) => setBatchSettings({...batchSettings, autoNextEpisode: e.target.checked})}
className="rounded"
/>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
</span>
</label>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 片头设置 */}
<div className="space-y-4">
<h4 className="font-medium text-gray-900 dark:text-gray-100 border-b pb-2">
🎬
</h4>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
(:)
</label>
<input
type="text"
value={batchSettings.openingStart}
onChange={(e) => setBatchSettings({...batchSettings, openingStart: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="0:00"
/>
<p className="text-xs text-gray-500 mt-1">格式: : ( 0:00)</p>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
(:)
</label>
<input
type="text"
value={batchSettings.openingEnd}
onChange={(e) => setBatchSettings({...batchSettings, openingEnd: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="1:30"
/>
<p className="text-xs text-gray-500 mt-1">格式: : ( 1:30)</p>
</div>
</div>
{/* 片尾设置 */}
<div className="space-y-4">
<h4 className="font-medium text-gray-900 dark:text-gray-100 border-b pb-2">
🎭
</h4>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
(:)
</label>
<input
type="text"
value={batchSettings.endingStart}
onChange={(e) => setBatchSettings({...batchSettings, endingStart: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="20:00"
/>
<p className="text-xs text-gray-500 mt-1"></p>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
(:) -
</label>
<input
type="text"
value={batchSettings.endingEnd}
onChange={(e) => setBatchSettings({...batchSettings, endingEnd: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="留空直接跳下一集"
/>
<p className="text-xs text-gray-500 mt-1">=</p>
</div>
</div>
</div>
<div className="mt-6 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<p><strong>:</strong> {secondsToTime(currentTime)}</p>
{duration > 0 && (
<p><strong>:</strong> {secondsToTime(duration)}</p>
)}
<div className="text-xs mt-2 text-gray-500 space-y-1">
<p>💡 <strong>:</strong> 0:00 1:30</p>
<p>💡 <strong>:</strong> 20:00 </p>
<p>💡 支持格式: 1:30 (130) 90 (90)</p>
</div>
</div>
</div>
<div className="flex space-x-3 mt-6">
<button
onClick={handleSaveBatchSettings}
className="flex-1 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded font-medium transition-colors"
>
</button>
<button
onClick={() => {
onSettingModeChange?.(false);
setBatchSettings({
openingStart: '0:00',
openingEnd: '1:30',
endingStart: '20:00',
endingEnd: '',
autoSkip: true,
autoNextEpisode: true,
});
}}
className="flex-1 px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded font-medium transition-colors"
>
</button>
</div>
{/* 分割线 */}
<div className="my-6 border-t border-gray-200 dark:border-gray-600"></div>
{/* 传统单个设置模式 */}
<details className="mb-4">
<summary className="cursor-pointer text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
</summary>
<div className="mt-4 space-y-4 pl-4 border-l-2 border-gray-200 dark:border-gray-600">
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
</label>
<select
value={newSegment.type || ''}
onChange={(e) => setNewSegment({ ...newSegment, type: e.target.value as 'opening' | 'ending' })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<option value=""></option>
<option value="opening"></option>
<option value="ending"></option>
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
()
</label>
<input
type="number"
value={newSegment.start || ''}
onChange={(e) => setNewSegment({ ...newSegment, start: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
()
</label>
<input
type="number"
value={newSegment.end || ''}
onChange={(e) => setNewSegment({ ...newSegment, end: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
</div>
<button
onClick={handleSaveSegment}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm font-medium transition-colors"
>
</button>
</div>
</details>
</div>
</div>
)}
{/* 管理已有片段 - 优化布局避免重叠 */}
{skipConfig && skipConfig.segments && skipConfig.segments.length > 0 && !isSettingMode && (
<div className="fixed bottom-4 right-4 z-40 max-w-sm bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 animate-fade-in">
<div className="p-3">
<h4 className="font-medium mb-2 text-gray-900 dark:text-gray-100 text-sm flex items-center">
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
</h4>
<div className="space-y-1">
{skipConfig.segments.map((segment, index) => (
<div
key={index}
className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700 rounded text-xs"
>
<span className="text-gray-800 dark:text-gray-200 flex-1 mr-2">
<span className="font-medium">
{segment.type === 'opening' ? '🎬片头' : '🎭片尾'}
</span>
<br />
<span className="text-gray-600 dark:text-gray-400">
{formatTime(segment.start)} - {formatTime(segment.end)}
</span>
{segment.autoSkip && (
<span className="ml-1 px-1 bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-400 rounded text-xs">
</span>
)}
</span>
<button
onClick={() => handleDeleteSegment(index)}
className="px-1.5 py-0.5 bg-red-500 hover:bg-red-600 text-white rounded text-xs transition-colors flex-shrink-0"
title="删除"
>
×
</button>
</div>
))}
</div>
<div className="mt-2 pt-2 border-t border-gray-200 dark:border-gray-600">
<button
onClick={() => onSettingModeChange?.(true)}
className="w-full px-2 py-1 bg-blue-100 hover:bg-blue-200 dark:bg-blue-900 dark:hover:bg-blue-800 text-blue-700 dark:text-blue-300 rounded text-xs transition-colors"
>
</button>
</div>
</div>
</div>
)}
<style jsx>{`
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.3s ease-out;
}
`}</style>
</div>
);
}
// 导出跳过控制器的设置按钮组件
export function SkipSettingsButton({ onClick }: { onClick: () => void }) {
return (
<button
onClick={onClick}
className="flex items-center space-x-1 px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded text-sm text-gray-700 dark:text-gray-300 transition-colors"
title="设置跳过片头片尾"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
<span></span>
</button>
);
}