forked from 77media/video-flow
PortraitAnimeSelector
This commit is contained in:
parent
1c2421e2b6
commit
5c33805284
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user