import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../theme/design_tokens.dart'; import '../widgets/gradient_button.dart'; enum StoryMode { generated, read } class StoryDetailPage extends StatefulWidget { final Map? story; // Pass story object final StoryMode mode; const StoryDetailPage({ super.key, this.story, this.mode = StoryMode.read, // Default: Read mode (from HTML logic) }); @override State createState() => _StoryDetailPageState(); } class _StoryDetailPageState extends State { // Tab State String _activeTab = 'text'; // 'text' or 'video' bool _isPlaying = false; bool _hasGeneratedVideo = false; bool _isLoadingVideo = false; // Mock Content from HTML final Map _defaultStory = { 'title': "星际忍者的茶话会", 'content': """ 在遥远的银河系边缘,有一个被星云包裹的神秘茶馆。今天,这里迎来了两位特殊的客人:刚执行完火星探测任务的宇航员波波,和正在追捕暗影怪兽的忍者小次郎。 “这儿的重力好像有点不对劲?”波波飘在半空中,试图抓住飞来飞去的茶杯。小次郎则冷静地倒挂在天花板上,手里紧握着一枚手里剑——其实那是用来切月饼的。 突然,桌上的魔法茶壶“噗”地一声喷出了七彩烟雾,一只会说话的卡皮巴拉钻了出来:“别打架,别打架,喝了这杯‘银河气泡茶’,我们都是好朋友!” 于是,宇宙中最奇怪的组合诞生了。他们决定,下一站,去黑洞边缘钓星星。 """, }; Map get _currentStory => widget.story ?? _defaultStory; @override void initState() { super.initState(); // Logic from HTML: if mode is read, we might start with text. // HTML defaults to text tab. } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.storyBackground, // #FDF9F3 body: SafeArea( child: Column( children: [ // Header _buildHeader(), // Tab Switcher (Visible if video generated or implies interactability) // HTML hides it initially (`style="display:none;"`), shows when generating. if (_hasGeneratedVideo || _isLoadingVideo) _buildTabSwitcher(), // Content Card (Scrollable) Expanded(child: _buildContentCard()), // Footer _buildFooter(), ], ), ), ); } Widget _buildHeader() { return Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), child: Stack( alignment: Alignment.center, children: [ Align( alignment: Alignment.centerLeft, child: IconButton( icon: const Icon(Icons.arrow_back_ios_new, size: 20), color: const Color(0xFF4B5563), onPressed: () => Navigator.of(context).pop(), ), ), Text( _currentStory['title'], style: const TextStyle( fontFamily: 'Inter', fontSize: 18, fontWeight: FontWeight.w700, // HTML: 700 color: AppColors.storyTitle, // #4B2404 ), ), ], ), ); } Widget _buildTabSwitcher() { return Container( margin: const EdgeInsets.only(bottom: 16), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildTabBtn('📄 故事', 'text'), const SizedBox(width: 8), _buildTabBtn('🎬 绘本', 'video'), ], ), ); } Widget _buildTabBtn(String label, String key) { bool isActive = _activeTab == key; return GestureDetector( onTap: () { setState(() { _activeTab = key; }); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: isActive ? Colors.white : Colors.transparent, borderRadius: BorderRadius.circular(20), boxShadow: isActive ? [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2), ), ] : null, ), child: Text( label, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: isActive ? AppColors.storyTitle : AppColors.textSecondary, ), ), ), ); } Widget _buildContentCard() { // HTML: .story-paper bool isVideoMode = _activeTab == 'video'; return Container( margin: const EdgeInsets.fromLTRB( 24, 0, 24, 10, ), // HTML: 0 24px 110px padding on parent, paper fills flex decoration: isVideoMode ? null : BoxDecoration( color: Colors.white.withOpacity( 0.6, ), // HTML says transparent for video, what about text? It implies simple text flow in HTML... wait. // HTML: .story-paper has margin-bottom 10px. Scrollbar none. // Actually HTML doesn't explicitly set white background on .story-paper unless implies by default? // Ah, looking closely at styles.css or html structure: // .story-paper { flex: 1; overflow-y: auto ... } // .story-content { font-size: 16px ... } // It seems the background is just the page background #FDF9F3. // But in `story_detail_page.dart` (original), it had white card. // HTML PRD: `body { background: #FDF9F3; }`. .story-paper doesn't have background color set, so it's transparent? // Let's assume transparent to match "Paper" feel being part of background or if existing Flutter impl used white card, user might prefer that. // BUT strict 1:1 implies following HTML. HTML has NO white card background on .story-paper. // So I will remove the white background container. ), child: isVideoMode ? _buildVideoView() : _buildTextView(), ); } Widget _buildTextView() { return SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Text( _currentStory['content'] .toString() .replaceAll(RegExp(r'\n+'), '\n\n') .trim(), // Simple paragraph spacing style: const TextStyle( fontFamily: 'Inter', fontSize: 16, // HTML: 16px height: 2.0, // HTML: line-height 2.0 color: AppColors.storyText, // #374151 ), textAlign: TextAlign.justify, ), ); } Widget _buildVideoView() { if (_isLoadingVideo) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox( width: 40, height: 40, child: CircularProgressIndicator( color: Color(0xFFF43F5E), // HTML: #F43F5E strokeWidth: 3, ), ), const SizedBox(height: 16), const Text( 'AI 正在绘制动态绘本...', style: TextStyle( fontWeight: FontWeight.w600, color: Color(0xFF4B5563), ), ), const SizedBox(height: 8), const Text( '消耗 10 SP', style: TextStyle(fontSize: 12, color: Color(0xFF9CA3AF)), ), ], ), ); } return Stack( alignment: Alignment.center, children: [ AspectRatio( aspectRatio: 16 / 9, // Assume landscape video child: Container( color: Colors.black, child: const Center( child: Icon(Icons.videocam, color: Colors.white54, size: 48), ), // Placeholder for Video Player ), ), // Play Button Overlay Container( width: 48, height: 48, decoration: BoxDecoration( color: Colors.white.withOpacity(0.8), shape: BoxShape.circle, ), child: const Icon(Icons.play_arrow, color: Colors.black), ), ], ); } Widget _buildFooter() { // HTML: .generator-footer { padding: 0 24px 30px; ... } (Inferred from container padding bottom 110px? No, fixed to bottom?) // Actually HTML has .generator-footer inside body? No, .result-container has padding-bottom 110px? // Let's stick to a fixed bottom container. return Container( padding: EdgeInsets.fromLTRB( 24, 0, 24, MediaQuery.of(context).padding.bottom + 20, ), // HTML footer is customized per mode. child: _activeTab == 'text' ? _buildTextFooter() : _buildVideoFooter(), ); } Widget _buildTextFooter() { if (widget.mode == StoryMode.generated) { // Generator Mode: Rewrite + Save return Row( children: [ // Rewrite (Secondary) Expanded( child: Container( height: 50, decoration: BoxDecoration( border: Border.all(color: const Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(25), color: Colors.white.withOpacity(0.8), ), alignment: Alignment.center, child: const Text( '↻ 重写', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF4B5563), ), ), ), ), const SizedBox(width: 16), // Save (Primary) - Returns 'saved' to trigger add book animation Expanded( child: GradientButton( text: '保存故事', onPressed: () { Navigator.of(context).pop('saved'); }, gradient: const LinearGradient( colors: AppColors.btnCapybaraGradient, ), height: 50, ), ), ], ); } else { // Read Mode: TTS + Make Picture Book return Row( children: [ // TTS Expanded( child: GestureDetector( onTap: () => setState(() => _isPlaying = !_isPlaying), child: Container( height: 50, decoration: BoxDecoration( border: Border.all(color: const Color(0xFFE5E7EB)), borderRadius: BorderRadius.circular(25), color: Colors.white.withOpacity(0.8), ), alignment: Alignment.center, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( _isPlaying ? Icons.pause : Icons.headphones, size: 20, color: const Color(0xFF4B5563), ), const SizedBox(width: 6), Text( _isPlaying ? '暂停' : '朗读', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF4B5563), ), ), ], ), ), ), ), const SizedBox(width: 16), // Make Picture Book Expanded( child: GradientButton( text: '变绘本', onPressed: _startVideoGeneration, // Simulate logic gradient: const LinearGradient( colors: AppColors.btnCapybaraGradient, ), height: 50, ), ), ], ); } } Widget _buildVideoFooter() { return Row( children: [ Expanded( child: GradientButton( text: '↻ 重新生成', onPressed: _startVideoGeneration, gradient: const LinearGradient( colors: AppColors.btnCapybaraGradient, ), height: 50, ), ), ], ); } void _startVideoGeneration() { setState(() { _isLoadingVideo = true; _activeTab = 'video'; }); // Mock delay Future.delayed(const Duration(seconds: 2), () { if (mounted) { setState(() { _isLoadingVideo = false; _hasGeneratedVideo = true; }); } }); } }