| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- 'use client';
- import {
- Settings,
- Sun,
- Moon,
- Monitor,
- ArrowLeft,
- Loader2,
- Download,
- FileDown,
- Package,
- } from 'lucide-react';
- import { useI18n } from '@/lib/hooks/use-i18n';
- import { useTheme } from '@/lib/hooks/use-theme';
- import { LanguageSwitcher } from './language-switcher';
- import { useState, useEffect, useRef, useCallback } from 'react';
- import { useRouter } from 'next/navigation';
- import { SettingsDialog } from './settings';
- import { cn } from '@/lib/utils';
- import { useStageStore } from '@/lib/store/stage';
- import { useMediaGenerationStore } from '@/lib/store/media-generation';
- import { useExportPPTX } from '@/lib/export/use-export-pptx';
- interface HeaderProps {
- readonly currentSceneTitle: string;
- }
- export function Header({ currentSceneTitle }: HeaderProps) {
- const { t } = useI18n();
- const { theme, setTheme } = useTheme();
- const router = useRouter();
- const [settingsOpen, setSettingsOpen] = useState(false);
- const [themeOpen, setThemeOpen] = useState(false);
- // Export
- const { exporting: isExporting, exportPPTX, exportResourcePack } = useExportPPTX();
- const [exportMenuOpen, setExportMenuOpen] = useState(false);
- const exportRef = useRef<HTMLDivElement>(null);
- const scenes = useStageStore((s) => s.scenes);
- const generatingOutlines = useStageStore((s) => s.generatingOutlines);
- const failedOutlines = useStageStore((s) => s.failedOutlines);
- const mediaTasks = useMediaGenerationStore((s) => s.tasks);
- const canExport =
- scenes.length > 0 &&
- generatingOutlines.length === 0 &&
- failedOutlines.length === 0 &&
- Object.values(mediaTasks).every((task) => task.status === 'done' || task.status === 'failed');
- const themeRef = useRef<HTMLDivElement>(null);
- // Close dropdown when clicking outside
- const handleClickOutside = useCallback(
- (e: MouseEvent) => {
- if (themeOpen && themeRef.current && !themeRef.current.contains(e.target as Node)) {
- setThemeOpen(false);
- }
- if (exportMenuOpen && exportRef.current && !exportRef.current.contains(e.target as Node)) {
- setExportMenuOpen(false);
- }
- },
- [themeOpen, exportMenuOpen],
- );
- useEffect(() => {
- if (themeOpen || exportMenuOpen) {
- document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }
- }, [themeOpen, exportMenuOpen, handleClickOutside]);
- return (
- <>
- <header className="h-20 px-8 flex items-center justify-between z-10 bg-transparent gap-4">
- <div className="flex items-center gap-3 min-w-0 flex-1">
- <button
- onClick={() => router.push('/')}
- className="shrink-0 p-2 rounded-lg text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
- title={t('generation.backToHome')}
- >
- <ArrowLeft className="w-5 h-5" />
- </button>
- <div className="flex flex-col min-w-0">
- <span className="text-[10px] uppercase tracking-widest font-bold text-gray-400 dark:text-gray-500 mb-0.5">
- {t('stage.currentScene')}
- </span>
- <h1
- className="text-xl font-bold text-gray-800 dark:text-gray-200 tracking-tight truncate"
- suppressHydrationWarning
- >
- {currentSceneTitle || t('common.loading')}
- </h1>
- </div>
- </div>
- <div className="flex items-center gap-4 bg-white/60 dark:bg-gray-800/60 backdrop-blur-md px-2 py-1.5 rounded-full border border-gray-100/50 dark:border-gray-700/50 shadow-sm shrink-0">
- {/* Language Selector */}
- <LanguageSwitcher onOpen={() => setThemeOpen(false)} />
- <div className="w-[1px] h-4 bg-gray-200 dark:bg-gray-700" />
- {/* Theme Selector */}
- <div className="relative" ref={themeRef}>
- <button
- onClick={() => {
- setThemeOpen(!themeOpen);
- }}
- className="p-2 rounded-full text-gray-400 dark:text-gray-500 hover:bg-white dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200 hover:shadow-sm transition-all group"
- >
- {theme === 'light' && <Sun className="w-4 h-4" />}
- {theme === 'dark' && <Moon className="w-4 h-4" />}
- {theme === 'system' && <Monitor className="w-4 h-4" />}
- </button>
- {themeOpen && (
- <div className="absolute top-full mt-2 right-0 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden z-50 min-w-[140px]">
- <button
- onClick={() => {
- setTheme('light');
- setThemeOpen(false);
- }}
- className={cn(
- 'w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2',
- theme === 'light' &&
- 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
- )}
- >
- <Sun className="w-4 h-4" />
- {t('settings.themeOptions.light')}
- </button>
- <button
- onClick={() => {
- setTheme('dark');
- setThemeOpen(false);
- }}
- className={cn(
- 'w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2',
- theme === 'dark' &&
- 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
- )}
- >
- <Moon className="w-4 h-4" />
- {t('settings.themeOptions.dark')}
- </button>
- <button
- onClick={() => {
- setTheme('system');
- setThemeOpen(false);
- }}
- className={cn(
- 'w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2',
- theme === 'system' &&
- 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
- )}
- >
- <Monitor className="w-4 h-4" />
- {t('settings.themeOptions.system')}
- </button>
- </div>
- )}
- </div>
- <div className="w-[1px] h-4 bg-gray-200 dark:bg-gray-700" />
- {/* Settings Button */}
- <div className="relative">
- <button
- onClick={() => setSettingsOpen(true)}
- className="p-2 rounded-full text-gray-400 dark:text-gray-500 hover:bg-white dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200 hover:shadow-sm transition-all group"
- >
- <Settings className="w-4 h-4 group-hover:rotate-90 transition-transform duration-500" />
- </button>
- </div>
- </div>
- {/* Export Dropdown */}
- <div className="relative" ref={exportRef}>
- <button
- onClick={() => {
- if (canExport && !isExporting) setExportMenuOpen(!exportMenuOpen);
- }}
- disabled={!canExport || isExporting}
- title={
- canExport
- ? isExporting
- ? t('export.exporting')
- : t('export.pptx')
- : t('share.notReady')
- }
- className={cn(
- 'shrink-0 p-2 rounded-full transition-all',
- canExport && !isExporting
- ? 'text-gray-400 dark:text-gray-500 hover:bg-white dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200 hover:shadow-sm'
- : 'text-gray-300 dark:text-gray-600 cursor-not-allowed opacity-50',
- )}
- >
- {isExporting ? (
- <Loader2 className="w-4 h-4 animate-spin" />
- ) : (
- <Download className="w-4 h-4" />
- )}
- </button>
- {exportMenuOpen && (
- <div className="absolute top-full mt-2 right-0 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden z-50 min-w-[200px]">
- <button
- onClick={() => {
- setExportMenuOpen(false);
- exportPPTX();
- }}
- className="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2.5"
- >
- <FileDown className="w-4 h-4 text-gray-400 shrink-0" />
- <span>{t('export.pptx')}</span>
- </button>
- <button
- onClick={() => {
- setExportMenuOpen(false);
- exportResourcePack();
- }}
- className="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2.5"
- >
- <Package className="w-4 h-4 text-gray-400 shrink-0" />
- <div>
- <div>{t('export.resourcePack')}</div>
- <div className="text-[11px] text-gray-400 dark:text-gray-500">
- {t('export.resourcePackDesc')}
- </div>
- </div>
- </button>
- </div>
- )}
- </div>
- </header>
- <SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
- </>
- );
- }
|