优化视频下载

This commit is contained in:
mtvpls
2026-06-04 14:09:12 +08:00
parent caa9cd82c0
commit 63fb51ce99
5 changed files with 520 additions and 97 deletions
+225 -41
View File
@@ -1,13 +1,28 @@
'use client';
import React from 'react';
import React, { useMemo, useState } from 'react';
import { M3U8DownloadTask } from '@/lib/m3u8-downloader';
import { M3U8DownloadTask, M3U8SegmentLogStatus } from '@/lib/m3u8-downloader';
import { useDownload } from '@/contexts/DownloadContext';
export function DownloadPanel() {
const { tasks, showDownloadPanel, setShowDownloadPanel, startTask, pauseTask, cancelTask, retryFailedSegments, getProgress } = useDownload();
const [logTaskId, setLogTaskId] = useState<string | null>(null);
const [logFilter, setLogFilter] = useState<'all' | M3U8SegmentLogStatus>('all');
const logTask = useMemo(
() => tasks.find((task) => task.id === logTaskId) || null,
[logTaskId, tasks]
);
const filteredLogs = useMemo(() => {
if (!logTask) return [];
const logs = logFilter === 'all'
? logTask.segmentLogs
: logTask.segmentLogs.filter((log) => log.status === logFilter);
return [...logs].reverse();
}, [logFilter, logTask]);
if (!showDownloadPanel) {
return null;
@@ -33,31 +48,83 @@ export function DownloadPanel() {
const getStatusColor = (status: M3U8DownloadTask['status']) => {
switch (status) {
case 'ready':
return 'text-gray-500';
return 'text-gray-500 dark:text-slate-400';
case 'downloading':
return 'text-blue-500';
return 'text-blue-500 dark:text-sky-400';
case 'pause':
return 'text-yellow-500';
return 'text-yellow-600 dark:text-amber-400';
case 'done':
return 'text-green-500';
return 'text-green-600 dark:text-emerald-400';
case 'error':
return 'text-red-500';
return 'text-red-600 dark:text-rose-400';
default:
return 'text-gray-500';
return 'text-gray-500 dark:text-slate-400';
}
};
const getLogBadgeClass = (status: M3U8SegmentLogStatus) => {
switch (status) {
case 'downloading':
return 'border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-400/30 dark:bg-sky-400/10 dark:text-sky-300';
case 'success':
return 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-400/30 dark:bg-emerald-400/10 dark:text-emerald-300';
case 'retry':
return 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-400/30 dark:bg-amber-400/10 dark:text-amber-300';
case 'timeout':
case 'error':
return 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-400/30 dark:bg-rose-400/10 dark:text-rose-300';
case 'aborted':
return 'border-slate-200 bg-slate-50 text-slate-700 dark:border-slate-500/30 dark:bg-slate-500/10 dark:text-slate-300';
default:
return 'border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-500/30 dark:bg-gray-500/10 dark:text-gray-300';
}
};
const getLogStatusText = (status: M3U8SegmentLogStatus) => {
switch (status) {
case 'queued':
return '排队';
case 'downloading':
return '下载中';
case 'success':
return '成功';
case 'retry':
return '重试';
case 'error':
return '失败';
case 'timeout':
return '超时';
case 'aborted':
return '中止';
default:
return status;
}
};
const formatTime = (timestamp: number) => new Date(timestamp).toLocaleTimeString();
const logStats = logTask ? {
total: logTask.segmentLogs.length,
success: logTask.finishList.filter((item) => item.status === 'is-success').length,
downloading: logTask.finishList.filter((item) => item.status === 'is-downloading').length,
error: logTask.finishList.filter((item) => item.status === 'is-error').length,
} : null;
return (
<div className='fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm'>
<div className='bg-white dark:bg-gray-800 rounded-lg shadow-2xl w-full max-w-4xl max-h-[80vh] flex flex-col'>
<div className='fixed inset-0 z-[9999] flex items-center justify-center bg-black/60 p-3 backdrop-blur-sm sm:p-6'>
<div className='flex max-h-[86vh] w-full max-w-6xl flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl dark:border-slate-700/80 dark:bg-slate-950'>
{/* 标题栏 */}
<div className='flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700'>
<h2 className='text-xl font-bold text-gray-900 dark:text-white'></h2>
<div className='flex items-center justify-between border-b border-gray-200 bg-white/90 p-4 dark:border-slate-800 dark:bg-slate-950/90'>
<div>
<h2 className='text-xl font-bold text-gray-900 dark:text-slate-50'></h2>
<p className='mt-1 text-xs text-gray-500 dark:text-slate-400'></p>
</div>
<button
onClick={() => setShowDownloadPanel(false)}
className='text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors'
className='cursor-pointer rounded-lg p-2 text-gray-500 transition-colors duration-200 hover:bg-gray-100 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-sky-500 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200'
aria-label='关闭下载任务列表'
>
<svg className='w-6 h-6' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<svg className='h-6 w-6' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M6 18L18 6M6 6l12 12' />
</svg>
</button>
@@ -66,8 +133,8 @@ export function DownloadPanel() {
{/* 任务列表 */}
<div className='flex-1 overflow-y-auto p-4 space-y-3'>
{tasks.length === 0 ? (
<div className='flex flex-col items-center justify-center h-full text-gray-500 dark:text-gray-400'>
<svg className='w-16 h-16 mb-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<div className='flex min-h-[320px] flex-col items-center justify-center text-gray-500 dark:text-slate-400'>
<svg className='mb-4 h-16 w-16' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path
strokeLinecap='round'
strokeLinejoin='round'
@@ -83,21 +150,21 @@ export function DownloadPanel() {
return (
<div
key={task.id}
className='bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 border border-gray-200 dark:border-gray-600'
className='rounded-xl border border-gray-200 bg-gray-50 p-4 transition-colors duration-200 dark:border-slate-700 dark:bg-slate-900/70'
>
{/* 任务信息 */}
<div className='flex items-start justify-between mb-3'>
<div className='flex-1 min-w-0'>
<h3 className='text-sm font-medium text-gray-900 dark:text-white truncate mb-1'>
<div className='mb-3 flex items-start justify-between gap-4'>
<div className='min-w-0 flex-1'>
<h3 className='mb-1 truncate text-sm font-semibold text-gray-900 dark:text-slate-50'>
{task.title}
</h3>
<p className='text-xs text-gray-500 dark:text-gray-400 truncate'>{task.url}</p>
<p className='truncate text-xs text-gray-500 dark:text-slate-400'>{task.url}</p>
</div>
<div className='flex items-center gap-2 ml-4'>
<div className='flex shrink-0 items-center gap-2'>
<span className={`text-xs font-medium ${getStatusColor(task.status)}`}>
{getStatusText(task.status)}
</span>
<span className='text-xs text-gray-500 dark:text-gray-400'>
<span className='rounded-md border border-gray-200 px-2 py-0.5 text-xs text-gray-500 dark:border-slate-700 dark:text-slate-400'>
{task.type}
</span>
</div>
@@ -105,22 +172,22 @@ export function DownloadPanel() {
{/* 进度条 */}
<div className='mb-3'>
<div className='flex items-center justify-between text-xs text-gray-600 dark:text-gray-300 mb-1'>
<div className='mb-1 flex items-center justify-between text-xs text-gray-600 dark:text-slate-300'>
<span>
{task.finishNum} / {task.rangeDownload.targetSegment}
</span>
<span>{progress.toFixed(1)}%</span>
</div>
<div className='w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2 overflow-hidden'>
<div className='h-2 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-slate-800'>
<div
className={`h-full rounded-full transition-all duration-300 ${
task.status === 'downloading'
? 'bg-gradient-to-r from-blue-500 to-purple-600 animate-pulse'
? 'bg-gradient-to-r from-sky-500 to-emerald-500 motion-safe:animate-pulse'
: task.status === 'done'
? 'bg-green-500'
? 'bg-emerald-500'
: task.status === 'error'
? 'bg-red-500'
: 'bg-gray-400'
? 'bg-rose-500'
: 'bg-slate-400'
}`}
style={{ width: `${progress}%` }}
></div>
@@ -129,13 +196,13 @@ export function DownloadPanel() {
{/* 错误信息 */}
{task.errorNum > 0 && (
<div className='mb-3 flex items-center justify-between'>
<div className='text-xs text-red-500 dark:text-red-400'>
<div className='mb-3 flex items-center justify-between rounded-lg border border-rose-200 bg-rose-50 px-3 py-2 dark:border-rose-400/30 dark:bg-rose-400/10'>
<div className='text-xs text-rose-600 dark:text-rose-300' role='alert'>
{task.errorNum}
</div>
<button
onClick={() => retryFailedSegments(task.id)}
className='text-xs text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 underline'
className='cursor-pointer text-xs text-sky-600 underline transition-colors hover:text-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-500 dark:text-sky-300 dark:hover:text-sky-200'
>
</button>
@@ -143,13 +210,31 @@ export function DownloadPanel() {
)}
{/* 操作按钮 */}
<div className='flex items-center gap-2'>
<div className='flex flex-wrap items-center gap-2'>
<button
onClick={() => {
setLogTaskId(task.id);
setLogFilter('all');
}}
className='flex cursor-pointer items-center gap-1 rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition-colors duration-200 hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-sky-500 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700'
>
<svg className='h-4 w-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M9 17v-6m4 6V7m4 10v-3M5 19h14M5 5h14' />
</svg>
{task.segmentLogs.length > 0 && (
<span className='rounded-full bg-slate-200 px-1.5 py-0.5 text-[10px] text-slate-700 dark:bg-slate-700 dark:text-slate-200'>
{task.segmentLogs.length}
</span>
)}
</button>
{task.status === 'downloading' && (
<button
onClick={() => pauseTask(task.id)}
className='flex items-center gap-1 px-3 py-1.5 bg-yellow-500 hover:bg-yellow-600 text-white text-xs font-medium rounded transition-colors'
className='flex cursor-pointer items-center gap-1 rounded-lg bg-amber-500 px-3 py-1.5 text-xs font-medium text-white transition-colors duration-200 hover:bg-amber-600 focus:outline-none focus:ring-2 focus:ring-amber-400'
>
<svg className='w-4 h-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<svg className='h-4 w-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M10 9v6m4-6v6' />
</svg>
@@ -159,9 +244,9 @@ export function DownloadPanel() {
{(task.status === 'pause' || task.status === 'ready' || task.status === 'error') && (
<button
onClick={() => startTask(task.id)}
className='flex items-center gap-1 px-3 py-1.5 bg-blue-500 hover:bg-blue-600 text-white text-xs font-medium rounded transition-colors'
className='flex cursor-pointer items-center gap-1 rounded-lg bg-sky-500 px-3 py-1.5 text-xs font-medium text-white transition-colors duration-200 hover:bg-sky-600 focus:outline-none focus:ring-2 focus:ring-sky-400'
>
<svg className='w-4 h-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<svg className='h-4 w-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z' />
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M21 12a9 9 0 11-18 0 9 9 0 0118 0z' />
</svg>
@@ -171,9 +256,9 @@ export function DownloadPanel() {
<button
onClick={() => cancelTask(task.id)}
className='flex items-center gap-1 px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white text-xs font-medium rounded transition-colors'
className='flex cursor-pointer items-center gap-1 rounded-lg bg-rose-500 px-3 py-1.5 text-xs font-medium text-white transition-colors duration-200 hover:bg-rose-600 focus:outline-none focus:ring-2 focus:ring-rose-400'
>
<svg className='w-4 h-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<svg className='h-4 w-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16' />
</svg>
@@ -187,8 +272,8 @@ export function DownloadPanel() {
{/* 底部统计 */}
{tasks.length > 0 && (
<div className='p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/30'>
<div className='flex items-center justify-between text-sm text-gray-600 dark:text-gray-300'>
<div className='border-t border-gray-200 bg-gray-50 p-4 dark:border-slate-800 dark:bg-slate-900/80'>
<div className='grid grid-cols-2 gap-2 text-sm text-gray-600 dark:text-slate-300 sm:grid-cols-4'>
<span>: {tasks.length}</span>
<span>: {tasks.filter(t => t.status === 'downloading').length}</span>
<span>: {tasks.filter(t => t.status === 'done').length}</span>
@@ -197,6 +282,105 @@ export function DownloadPanel() {
</div>
)}
</div>
{logTask && (
<div className='fixed inset-0 z-[10000] flex items-center justify-center bg-black/50 p-3 backdrop-blur-sm sm:p-6'>
<div className='flex max-h-[82vh] w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-950'>
<div className='border-b border-gray-200 bg-white/95 p-4 dark:border-slate-800 dark:bg-slate-950/95'>
<div className='flex items-start justify-between gap-4'>
<div className='min-w-0'>
<h3 className='truncate text-lg font-bold text-gray-900 dark:text-slate-50'></h3>
<p className='mt-1 truncate text-xs text-gray-500 dark:text-slate-400'>{logTask.title}</p>
</div>
<button
onClick={() => setLogTaskId(null)}
className='cursor-pointer rounded-lg p-2 text-gray-500 transition-colors duration-200 hover:bg-gray-100 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-sky-500 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200'
aria-label='关闭分片下载日志'
>
<svg className='h-5 w-5' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M6 18L18 6M6 6l12 12' />
</svg>
</button>
</div>
{logStats && (
<div className='mt-4 grid grid-cols-2 gap-2 sm:grid-cols-4'>
<div className='rounded-xl border border-slate-200 bg-slate-50 p-3 dark:border-slate-700 dark:bg-slate-900'>
<div className='text-xs text-slate-500 dark:text-slate-400'></div>
<div className='mt-1 text-lg font-semibold text-slate-900 dark:text-slate-50'>{logStats.total}</div>
</div>
<div className='rounded-xl border border-emerald-200 bg-emerald-50 p-3 dark:border-emerald-400/30 dark:bg-emerald-400/10'>
<div className='text-xs text-emerald-700 dark:text-emerald-300'></div>
<div className='mt-1 text-lg font-semibold text-emerald-700 dark:text-emerald-200'>{logStats.success}</div>
</div>
<div className='rounded-xl border border-sky-200 bg-sky-50 p-3 dark:border-sky-400/30 dark:bg-sky-400/10'>
<div className='text-xs text-sky-700 dark:text-sky-300'></div>
<div className='mt-1 text-lg font-semibold text-sky-700 dark:text-sky-200'>{logStats.downloading}</div>
</div>
<div className='rounded-xl border border-rose-200 bg-rose-50 p-3 dark:border-rose-400/30 dark:bg-rose-400/10'>
<div className='text-xs text-rose-700 dark:text-rose-300'></div>
<div className='mt-1 text-lg font-semibold text-rose-700 dark:text-rose-200'>{logStats.error}</div>
</div>
</div>
)}
<div className='mt-4 flex flex-wrap gap-2'>
{(['all', 'downloading', 'success', 'retry', 'timeout', 'error', 'aborted'] as Array<'all' | M3U8SegmentLogStatus>).map((filter) => (
<button
key={filter}
onClick={() => setLogFilter(filter)}
className={`cursor-pointer rounded-full border px-3 py-1 text-xs font-medium transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-sky-500 ${
logFilter === filter
? 'border-sky-500 bg-sky-500 text-white'
: 'border-gray-200 bg-white text-gray-600 hover:bg-gray-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800'
}`}
>
{filter === 'all' ? '全部' : getLogStatusText(filter)}
</button>
))}
</div>
</div>
<div className='flex-1 overflow-y-auto p-4'>
{filteredLogs.length === 0 ? (
<div className='flex min-h-[220px] flex-col items-center justify-center rounded-xl border border-dashed border-gray-300 text-gray-500 dark:border-slate-700 dark:text-slate-400'>
<svg className='mb-3 h-10 w-10' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M9 12h6m-6 4h6M7 4h10a2 2 0 012 2v12a2 2 0 01-2 2H7a2 2 0 01-2-2V6a2 2 0 012-2z' />
</svg>
<p className='text-sm'></p>
</div>
) : (
<div className='space-y-2'>
{filteredLogs.map((log) => (
<div
key={log.id}
className='grid gap-3 rounded-xl border border-gray-200 bg-white p-3 text-sm dark:border-slate-800 dark:bg-slate-900 md:grid-cols-[120px_110px_1fr_160px]'
>
<div className='font-mono text-xs text-slate-500 dark:text-slate-400'>
{formatTime(log.timestamp)}
</div>
<div>
<span className={`inline-flex rounded-full border px-2 py-0.5 text-xs font-medium ${getLogBadgeClass(log.status)}`}>
{getLogStatusText(log.status)}
</span>
</div>
<div className='min-w-0'>
<div className='truncate text-slate-900 dark:text-slate-100'>{log.message}</div>
<div className='mt-1 truncate font-mono text-xs text-slate-500 dark:text-slate-500'>segment #{log.index + 1}</div>
</div>
<div className='flex flex-wrap items-center gap-2 text-xs text-slate-500 dark:text-slate-400 md:justify-end'>
{typeof log.retryCount === 'number' && <span> {log.retryCount}</span>}
{typeof log.durationMs === 'number' && <span>{log.durationMs}ms</span>}
{typeof log.httpStatus === 'number' && <span>HTTP {log.httpStatus}</span>}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}
+104
View File
@@ -208,6 +208,7 @@ export const UserMenu: React.FC = () => {
const [exactSearch, setExactSearch] = useState(true);
const [maxConcurrentDownloads, setMaxConcurrentDownloads] = useState(6);
const [downloadThreadsPerTask, setDownloadThreadsPerTask] = useState(6);
const [downloadSegmentTimeout, setDownloadSegmentTimeout] = useState(30000);
const [downloadMode, setDownloadMode] = useState<'browser' | 'filesystem'>(
'browser'
);
@@ -824,6 +825,17 @@ export const UserMenu: React.FC = () => {
setDownloadThreadsPerTask(Number(savedDownloadThreadsPerTask));
}
// 加载分片下载超时设置
const savedDownloadSegmentTimeout = localStorage.getItem(
'downloadSegmentTimeout'
);
if (savedDownloadSegmentTimeout !== null) {
const timeout = Number(savedDownloadSegmentTimeout);
if (Number.isFinite(timeout)) {
setDownloadSegmentTimeout(Math.min(Math.max(timeout, 30000), 300000));
}
}
// 加载下载模式设置
const savedDownloadMode = localStorage.getItem('downloadMode');
if (
@@ -1591,6 +1603,24 @@ export const UserMenu: React.FC = () => {
}
};
const handleDownloadSegmentTimeoutChange = (value: number) => {
const normalizedValue = Math.min(Math.max(value, 30000), 300000);
setDownloadSegmentTimeout(normalizedValue);
if (typeof window !== 'undefined') {
localStorage.setItem('downloadSegmentTimeout', String(normalizedValue));
}
};
const formatDownloadSegmentTimeout = (value: number) => {
if (value < 60000) {
return `${Math.round(value / 1000)}`;
}
const minutes = Math.floor(value / 60000);
const seconds = Math.round((value % 60000) / 1000);
return seconds > 0 ? `${minutes}${seconds}` : `${minutes}分钟`;
};
const handleDownloadModeChange = (mode: 'browser' | 'filesystem') => {
// 如果选择 filesystem 模式,先检测浏览器是否支持
if (
@@ -3470,6 +3500,80 @@ export const UserMenu: React.FC = () => {
</div>
</div>
{/* 分片下载超时 */}
<div className='space-y-2'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<div className='flex items-center justify-between'>
<span className='text-xs text-gray-600 dark:text-gray-400'>
</span>
<span className='text-xs font-medium text-gray-700 dark:text-gray-300'>
{formatDownloadSegmentTimeout(downloadSegmentTimeout)}
</span>
</div>
<div className='flex items-center gap-2'>
<input
type='range'
min='30000'
max='300000'
step='10000'
value={downloadSegmentTimeout}
onChange={(e) =>
handleDownloadSegmentTimeoutChange(
Number(e.target.value)
)
}
className='flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700'
style={{
background: `linear-gradient(to right, #10b981 0%, #10b981 ${
((downloadSegmentTimeout - 30000) / (300000 - 30000)) * 100
}%, #e5e7eb ${
((downloadSegmentTimeout - 30000) / (300000 - 30000)) * 100
}%, #e5e7eb 100%)`,
}}
/>
</div>
<div className='flex justify-between text-xs text-gray-500 dark:text-gray-400'>
<button
onClick={() => handleDownloadSegmentTimeoutChange(30000)}
className={`px-2 py-0.5 rounded cursor-pointer ${
downloadSegmentTimeout === 30000
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
30
</button>
<button
onClick={() => handleDownloadSegmentTimeoutChange(120000)}
className={`px-2 py-0.5 rounded cursor-pointer ${
downloadSegmentTimeout === 120000
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
2
</button>
<button
onClick={() => handleDownloadSegmentTimeoutChange(300000)}
className={`px-2 py-0.5 rounded cursor-pointer ${
downloadSegmentTimeout === 300000
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
5
</button>
</div>
</div>
{/* 下载模式 */}
<div className='space-y-2'>
<div>