2025-10-21 22:17:54 +08:00

285 lines
14 KiB
TypeScript

"use client";
import {
GlobalOutlined,
ClockCircleOutlined,
CloseOutlined,
DownOutlined
} from '@ant-design/icons';
import { WandSparkles, RectangleHorizontal, RectangleVertical, Palette } from 'lucide-react';
import { Modal, Dropdown } from 'antd';
import { LanguageOptions, VideoDurationOptions } from './config-options';
import type { ConfigOptions, LanguageValue, VideoDurationValue } from './config-options';
import { useEffect, useMemo, useState } from 'react';
import { fetchSettingByCode } from '@/api/serversetting';
interface MobileConfigModalProps {
/** Modal visibility state */
visible: boolean;
/** Close handler */
onClose: () => void;
/** Current configuration options */
configOptions: ConfigOptions;
/** Handler for configuration changes */
onConfigChange: <K extends keyof ConfigOptions>(key: K, value: ConfigOptions[K], exclude?: boolean) => void;
/** Whether to disable duration selection */
disableDuration?: boolean;
}
/**
* Mobile configuration modal with card-style selections.
* Provides a modern, non-traditional form interface for video settings.
* @param {boolean} visible - Modal visibility
* @param {Function} onClose - Close handler
* @param {ConfigOptions} configOptions - Current configuration
* @param {Function} onConfigChange - Handler for config changes
* @returns {JSX.Element}
*/
export function MobileConfigModal({
visible,
onClose,
configOptions,
onConfigChange,
disableDuration = false,
}: MobileConfigModalProps) {
const [animeOptions, setAnimeOptions] = useState<Array<{ name: string; pcode: string }>>([]);
useEffect(() => {
let mounted = true;
(async () => {
const list = await fetchSettingByCode<Array<{ name: string; pcode: string }>>('comic_config', []);
if (!mounted) return;
if (Array.isArray(list) && list.length > 0) {
setAnimeOptions(list);
} else {
setAnimeOptions([
{ name: 'Korean Comics Long', pcode: 'STANDARD_V1_734684_116483' },
]);
}
})();
return () => { mounted = false; };
}, []);
/** Check if currentAnime is valid in animeOptions */
const isValidAnimeChoice = useMemo(() => {
return animeOptions.some(opt => opt.pcode === configOptions.pcode);
}, [animeOptions, configOptions.pcode]);
const isPortrait = configOptions.pcode === 'portrait';
const currentLanguage = LanguageOptions.find(opt => opt.value === configOptions.language);
/** Language dropdown menu items */
const languageMenuItems = LanguageOptions.map((option) => ({
key: option.value,
label: (
<span className={`text-sm ${configOptions.language === option.value ? 'text-cyan-400' : 'text-gray-300'}`}>
{option.label}
</span>
),
}));
/** Anime dropdown menu items */
const animeMenuItems = animeOptions.map((option) => ({
key: option.pcode,
label: (
<span className={`text-sm ${configOptions.pcode === option.pcode ? 'text-cyan-400' : 'text-gray-300'}`}>
{option.name}
</span>
),
}));
return (
<Modal
open={visible}
onCancel={onClose}
footer={null}
closeIcon={<CloseOutlined className="text-gray-400 hover:text-cyan-400" />}
width="90%"
style={{ maxWidth: '420px' }}
styles={{
mask: { backdropFilter: 'blur(2px)', backgroundColor: 'transparent' },
content: {
padding: 0,
backdropFilter: 'blur(4px)',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '20px',
},
}}
>
<div data-alt="mobile-config-modal" className="p-4 space-y-3">
{/* Header */}
<div data-alt="modal-header" className="pb-2.5 border-b border-white/10">
<h2 className="text-base font-semibold text-white">Video Settings</h2>
</div>
{/* Language Row */}
<div data-alt="language-row" className="flex items-center justify-between">
<div className="flex items-center gap-2">
<GlobalOutlined className="text-base text-cyan-400" />
<span className="text-sm text-gray-300">Language</span>
</div>
<Dropdown
menu={{
items: languageMenuItems,
onClick: ({ key }) => onConfigChange('language', key as LanguageValue),
className: 'bg-[#1a1a1a] border border-white/10 max-h-[300px] overflow-y-auto'
}}
trigger={['click']}
>
<button
data-alt="language-selector"
className="h-8 px-3 rounded-lg border border-white/20 bg-transparent hover:border-cyan-400/60 transition-all duration-200 flex items-center gap-2 text-gray-400 hover:text-cyan-400"
>
<span className="text-xs">{currentLanguage?.label}</span>
<DownOutlined className="text-xs" />
</button>
</Dropdown>
</div>
{/* AutoScript Row */}
<div data-alt="autoscript-row" className="flex items-center justify-between">
<div className="flex items-center gap-2">
<WandSparkles className="w-4 h-4 text-cyan-400" />
<span className="text-sm text-gray-300">AI Copilot</span>
</div>
<button
data-alt="autoscript-toggle"
disabled={configOptions.videoDuration === '8s'}
onClick={() => {
if (configOptions.videoDuration !== '8s') {
onConfigChange('expansion_mode', !configOptions.expansion_mode);
}
}}
className="flex items-center"
>
<div className={`w-11 h-6 rounded-full transition-all duration-200 relative ${
configOptions.videoDuration === '8s'
? 'opacity-40 cursor-not-allowed bg-gray-600'
: configOptions.expansion_mode
? 'bg-cyan-400'
: 'bg-gray-600'
}`}>
<div className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-all duration-200 ${
configOptions.expansion_mode ? 'left-5' : 'left-0.5'
}`} />
</div>
</button>
</div>
{/* Video Duration Row */}
<div data-alt="duration-row" className="flex items-center justify-between">
<div className="flex items-center gap-2">
<ClockCircleOutlined className="text-base text-cyan-400" />
<span className="text-sm text-gray-300">Duration</span>
</div>
<div className={`flex items-center p-0.5 rounded-lg border border-white/20 bg-black/20 ${disableDuration ? 'opacity-40' : ''}`}>
{VideoDurationOptions.map((option) => (
<button
key={option.value}
data-alt={`duration-${option.value}`}
onClick={() => !disableDuration && onConfigChange('videoDuration', option.value as VideoDurationValue)}
disabled={disableDuration}
className={`h-7 px-3 rounded-md transition-all duration-200 text-xs font-medium ${
disableDuration
? 'cursor-not-allowed'
: configOptions.videoDuration === option.value
? 'bg-cyan-400/20 text-cyan-400'
: 'bg-transparent text-gray-400 hover:text-gray-300'
}`}
>
{option.label}
</button>
))}
</div>
</div>
{/* Aspect Ratio Row */}
<div data-alt="aspect-ratio-row" className="flex items-center justify-between">
<div className="flex items-center gap-2">
<RectangleHorizontal className="w-4 h-4 text-cyan-400" />
<span className="text-sm text-gray-300">Aspect Ratio</span>
</div>
<div className="flex items-center p-0.5 rounded-lg border border-white/20 bg-black/20">
<button
data-alt="aspect-portrait"
onClick={() => onConfigChange('aspect_ratio', 'VIDEO_ASPECT_RATIO_PORTRAIT')}
className={`h-7 w-9 rounded-md transition-all duration-200 flex items-center justify-center ${
configOptions.aspect_ratio === 'VIDEO_ASPECT_RATIO_PORTRAIT'
? 'bg-cyan-400/20 text-cyan-400'
: 'bg-transparent text-gray-400 hover:text-gray-300'
}`}
>
<RectangleVertical className="w-3.5 h-3.5" />
</button>
<button
data-alt="aspect-landscape"
onClick={() => onConfigChange('aspect_ratio', 'VIDEO_ASPECT_RATIO_LANDSCAPE')}
className={`h-7 w-9 rounded-md transition-all duration-200 flex items-center justify-center ${
configOptions.aspect_ratio === 'VIDEO_ASPECT_RATIO_LANDSCAPE'
? 'bg-cyan-400/20 text-cyan-400'
: 'bg-transparent text-gray-400 hover:text-gray-300'
}`}
>
<RectangleHorizontal className="w-3.5 h-3.5" />
</button>
</div>
</div>
{/* Video Style Row */}
<div data-alt="style-row" className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Palette className="w-4 h-4 text-cyan-400" />
<span className="text-sm text-gray-300">Video Style</span>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center p-0.5 rounded-lg border border-white/20 bg-black/20">
<button
data-alt="style-portrait"
onClick={() => onConfigChange('pcode', 'portrait', true)}
className={`h-7 px-3 rounded-md transition-all duration-200 text-xs font-medium whitespace-nowrap ${
isPortrait
? 'bg-cyan-400/20 text-cyan-400'
: 'bg-transparent text-gray-400 hover:text-gray-300'
}`}
>
Portrait
</button>
<Dropdown
menu={{
items: animeMenuItems,
onClick: ({ key }) => onConfigChange('pcode', key, true),
className: 'bg-[#1a1a1a] border border-white/10'
}}
trigger={['click']}
>
<button
data-alt="style-anime"
onClick={() => {
if (isPortrait && animeOptions.length > 0) {
onConfigChange('pcode', animeOptions[0].pcode, true);
}
}}
className={`h-7 px-3 rounded-md transition-all duration-200 text-xs font-medium max-w-[120px] flex items-center gap-1 ${
!isPortrait
? isValidAnimeChoice
? 'bg-cyan-400/20 text-cyan-400'
: 'bg-transparent text-gray-400 cursor-not-allowed'
: 'bg-transparent text-gray-400 hover:text-gray-300'
}`}
>
<span className="truncate">
{animeOptions.find(opt => opt.pcode === configOptions.pcode)?.name || 'Anime'}
</span>
<DownOutlined className="text-xs flex-shrink-0" />
</button>
</Dropdown>
</div>
</div>
</div>
</div>
</Modal>
);
}