forked from 77media/video-flow
279 lines
14 KiB
TypeScript
279 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;
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
}: 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">
|
|
{VideoDurationOptions.map((option) => (
|
|
<button
|
|
key={option.value}
|
|
data-alt={`duration-${option.value}`}
|
|
onClick={() => onConfigChange('videoDuration', option.value as VideoDurationValue)}
|
|
className={`h-7 px-3 rounded-md transition-all duration-200 text-xs font-medium ${
|
|
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>
|
|
);
|
|
}
|
|
|