PortraitAnimeSelector

This commit is contained in:
北枳 2025-10-17 21:00:14 +08:00
parent 1c2421e2b6
commit 5c33805284
6 changed files with 169 additions and 10 deletions

View File

@ -3,14 +3,13 @@
import { import {
GlobalOutlined, GlobalOutlined,
ClockCircleOutlined, ClockCircleOutlined,
DownOutlined, DownOutlined
MobileOutlined,
DesktopOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { WandSparkles, RectangleHorizontal, RectangleVertical } from 'lucide-react'; import { WandSparkles, RectangleHorizontal, RectangleVertical } from 'lucide-react';
import { Dropdown, Menu, Tooltip } from 'antd'; import { Dropdown, Menu, Tooltip } from 'antd';
import { LanguageOptions, VideoDurationOptions } from './config-options'; import { LanguageOptions, VideoDurationOptions } from './config-options';
import type { ConfigOptions, LanguageValue, VideoDurationValue } from './config-options'; import type { ConfigOptions, LanguageValue, VideoDurationValue } from './config-options';
import { PortraitAnimeSelector } from './PortraitAnimeSelector';
interface ConfigPanelProps { interface ConfigPanelProps {
/** Current configuration options */ /** Current configuration options */
@ -115,7 +114,7 @@ export const ConfigPanel = ({
</Dropdown> </Dropdown>
{/* Aspect ratio toggles */} {/* Aspect ratio toggles */}
<div data-alt="aspect-ratio-controls" className="h-8 px-2 py-1 flex items-center gap-1 rounded-full border border-white/20 hover:border-cyan-400/60"> <div data-alt="aspect-ratio-controls" className="h-8 px-2 py-1 flex items-center rounded-full border border-white/20 hover:border-cyan-400/60">
<button <button
data-alt="portrait-button" data-alt="portrait-button"
className={`w-8 h-6 rounded-full transition-all duration-200 flex items-center justify-center ${ className={`w-8 h-6 rounded-full transition-all duration-200 flex items-center justify-center ${
@ -139,6 +138,12 @@ export const ConfigPanel = ({
<RectangleHorizontal className="w-4 h-4" /> <RectangleHorizontal className="w-4 h-4" />
</button> </button>
</div> </div>
{/* Portrait/Anime selector */}
<PortraitAnimeSelector
value={configOptions.pcode}
onChange={(v) => onConfigChange('pcode', v)}
/>
</div> </div>
); );
}; };

View File

@ -0,0 +1,130 @@
"use client";
import React, { useEffect, useMemo, useState } from 'react';
import { Dropdown, Menu } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import { fetchSettingByCode } from '@/api/serversetting';
import type { PortraitAnimeValue } from './config-options';
interface PortraitAnimeSelectorProps {
/** Current value: 'portrait' or anime pcode */
value: PortraitAnimeValue;
/** Change handler */
onChange: (value: PortraitAnimeValue) => void;
/** Optional class name */
className?: string;
/** Disabled state */
disabled?: boolean;
}
/**
* Portrait/Anime selector with dropdown for Anime subtypes.
* Styled to match VideoCreationForm design.
* @param {PortraitAnimeValue} value - Current value: 'portrait' or anime pcode
* @param {(value: PortraitAnimeValue) => void} onChange - Change handler
* @param {string} [className] - Optional wrapper class
* @param {boolean} [disabled] - Disable interaction when true
* @returns {JSX.Element}
*/
export function PortraitAnimeSelector({
value,
onChange,
className,
disabled = false,
}: PortraitAnimeSelectorProps) {
const [lastAnimeChoice, setLastAnimeChoice] = useState<string>('STANDARD_V1_734684_116483');
const [animeOptions, setAnimeOptions] = useState<Array<{ name: string; pcode: string }>>([]);
useEffect(() => {
if (value && value !== 'portrait') {
setLastAnimeChoice(value);
}
}, [value]);
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);
setLastAnimeChoice((prev) => (prev === 'STANDARD_V1_734684_116483' ? list[0].pcode : prev));
} else {
setAnimeOptions([
{ name: 'Korean Comics Long', pcode: 'STANDARD_V1_734684_116483' },
]);
}
})();
return () => { mounted = false; };
}, []);
const isPortrait = value === 'portrait';
const currentAnime = useMemo(() => (value && value !== 'portrait' ? value : lastAnimeChoice), [value, lastAnimeChoice]);
const pcodeToName = useMemo(() => {
const map: Record<string, string> = {};
animeOptions.forEach((o) => { map[o.pcode] = o.name; });
return map;
}, [animeOptions]);
/** Anime dropdown menu */
const animeMenu = (
<Menu
className="bg-[#1a1a1a] border border-white/10"
onClick={({ key }) => onChange(key as PortraitAnimeValue)}
items={(animeOptions.length > 0 ? animeOptions : []).map((opt) => ({
key: opt.pcode,
label: <span className="text-gray-300 text-sm">{opt.name}</span>,
}))}
/>
);
return (
<div
data-alt="portrait-anime-selector"
className={`h-8 px-2 py-1 flex items-center rounded-full border border-white/20 hover:border-cyan-400/60 ${className || ''}`}
>
{/* Portrait button */}
<button
data-alt="portrait-button"
type="button"
disabled={disabled}
onClick={() => onChange('portrait')}
className={`h-6 px-2 rounded-full transition-all duration-200 flex items-center text-sm ${
disabled
? 'opacity-40 cursor-not-allowed bg-transparent text-gray-400'
: isPortrait
? 'bg-white/10 text-cyan-400 shadow-sm'
: 'bg-transparent text-gray-400 hover:text-gray-300'
}`}
>
Portrait
</button>
{/* Anime dropdown button */}
<Dropdown overlay={animeMenu} trigger={['click']} disabled={disabled}>
<button
data-alt="anime-button"
type="button"
disabled={disabled}
className={`h-6 px-2 rounded-full transition-all duration-200 flex items-center gap-1.5 text-sm ${
disabled
? 'opacity-40 cursor-not-allowed bg-transparent text-gray-400'
: !isPortrait
? 'bg-white/10 text-cyan-400 shadow-sm'
: 'bg-transparent text-gray-400 hover:text-gray-300'
}`}
>
<span className="max-w-[100px] truncate">
{!isPortrait && pcodeToName[currentAnime]
? pcodeToName[currentAnime]
: 'Anime'}
</span>
<DownOutlined className="text-xs" />
</button>
</Dropdown>
</div>
);
}
export default PortraitAnimeSelector;

View File

@ -6,11 +6,12 @@ Video creation input form with configuration options.
``` ```
CreateInput/ CreateInput/
├── VideoCreationForm.tsx # Main form component ├── VideoCreationForm.tsx # Main form component
├── ConfigPanel.tsx # Configuration panel with all options ├── ConfigPanel.tsx # Configuration panel with all options
├── config-options.ts # Configuration options and types ├── PortraitAnimeSelector.tsx # Portrait/Anime style selector
├── index.ts # Module exports ├── config-options.ts # Configuration options and types
└── README.md # This file ├── index.ts # Module exports
└── README.md # This file
``` ```
## Components ## Components
@ -28,6 +29,7 @@ Configuration panel with unified circular button style:
- Auto Script toggle (AI Story Copilot) - Auto Script toggle (AI Story Copilot)
- Duration selector (8s, 1min, 2min, auto) - Duration selector (8s, 1min, 2min, auto)
- Aspect ratio selector (landscape/portrait) - Aspect ratio selector (landscape/portrait)
- Portrait/Anime style selector
All buttons follow the same design pattern: All buttons follow the same design pattern:
- Circular buttons with `rounded-full` - Circular buttons with `rounded-full`
@ -36,6 +38,13 @@ All buttons follow the same design pattern:
- Active state: `border-cyan-400 bg-cyan-400/10` - Active state: `border-cyan-400 bg-cyan-400/10`
- Cyan color theme for interactions - Cyan color theme for interactions
### PortraitAnimeSelector
Selector for choosing between Portrait and Anime (comic) styles:
- Portrait mode: realistic portrait style
- Anime mode: comic/anime styles with multiple options (Korean Comics Long, etc.)
- Fetches anime options from server dynamically
- Styled to match other configuration buttons
## Configuration Options ## Configuration Options
Defined in `config-options.ts`: Defined in `config-options.ts`:
@ -46,9 +55,18 @@ interface ConfigOptions {
expansion_mode: boolean; // Default: false expansion_mode: boolean; // Default: false
videoDuration: VideoDurationValue; // Default: 'unlimited' videoDuration: VideoDurationValue; // Default: 'unlimited'
aspect_ratio: AspectRatioValue; // Default: 'VIDEO_ASPECT_RATIO_LANDSCAPE' aspect_ratio: AspectRatioValue; // Default: 'VIDEO_ASPECT_RATIO_LANDSCAPE'
pcode: PortraitAnimeValue; // Default: 'portrait'
} }
``` ```
### Configuration Options Details
- **language**: Video output language (14 options: English, Chinese, Japanese, Spanish, etc.)
- **expansion_mode**: AI Story Copilot toggle (auto-generates extended script)
- **videoDuration**: Video length (8s, 1min, 2min, auto)
- **aspect_ratio**: Video orientation (landscape/portrait)
- **pcode**: Visual style (portrait or anime/comic style codes)
## Usage ## Usage
```tsx ```tsx

View File

@ -74,7 +74,7 @@ export default function VideoCreationForm() {
> >
{/* Photo Preview Section - Top */} {/* Photo Preview Section - Top */}
{photos.length > 0 && ( {photos.length > 0 && (
<div data-alt="photo-preview-wrapper" className="px-6 pt-6"> <div data-alt="photo-preview-wrapper" className="p-4 pb-0">
<PhotoPreviewSection <PhotoPreviewSection
photos={photos} photos={photos}
onDelete={handleDeletePhoto} onDelete={handleDeletePhoto}

View File

@ -33,12 +33,16 @@ export type VideoDurationValue = typeof VideoDurationOptions[number]['value'];
/** Aspect ratio type */ /** Aspect ratio type */
export type AspectRatioValue = 'VIDEO_ASPECT_RATIO_LANDSCAPE' | 'VIDEO_ASPECT_RATIO_PORTRAIT'; export type AspectRatioValue = 'VIDEO_ASPECT_RATIO_LANDSCAPE' | 'VIDEO_ASPECT_RATIO_PORTRAIT';
/** Portrait/Anime type */
export type PortraitAnimeValue = 'portrait' | string;
/** Configuration options interface */ /** Configuration options interface */
export interface ConfigOptions { export interface ConfigOptions {
language: LanguageValue; language: LanguageValue;
expansion_mode: boolean; expansion_mode: boolean;
videoDuration: VideoDurationValue; videoDuration: VideoDurationValue;
aspect_ratio: AspectRatioValue; aspect_ratio: AspectRatioValue;
pcode: PortraitAnimeValue;
} }
/** Default configuration */ /** Default configuration */
@ -47,5 +51,6 @@ export const defaultConfig: ConfigOptions = {
expansion_mode: false, expansion_mode: false,
videoDuration: 'unlimited', videoDuration: 'unlimited',
aspect_ratio: 'VIDEO_ASPECT_RATIO_LANDSCAPE', aspect_ratio: 'VIDEO_ASPECT_RATIO_LANDSCAPE',
pcode: 'portrait',
}; };

View File

@ -1,4 +1,5 @@
export { default as VideoCreationForm } from './VideoCreationForm'; export { default as VideoCreationForm } from './VideoCreationForm';
export { ConfigPanel } from './ConfigPanel'; export { ConfigPanel } from './ConfigPanel';
export { PortraitAnimeSelector } from './PortraitAnimeSelector';
export * from './config-options'; export * from './config-options';