import 'dart:async'; import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:fl_chart/fl_chart.dart'; import '../../data/mock_data.dart'; import '../../models/scenario.dart'; class ScriptPlayerScreen extends StatefulWidget { final String scenarioId; const ScriptPlayerScreen({super.key, required this.scenarioId}); @override State createState() => _ScriptPlayerScreenState(); } class _ScriptPlayerScreenState extends State with TickerProviderStateMixin { bool _isPlaying = true; double _progress = 0; bool _isDragging = false; bool _showAlert = false; Timer? _timer; late Scenario _scenario; late List _waveData; @override void initState() { super.initState(); _scenario = mockScenarios.firstWhere( (s) => s.id == widget.scenarioId, orElse: () => mockScenarios.first, ); _generateWaveData(); _startPlayback(); } void _generateWaveData() { // Generate 101 data points (0 to 100) for realistic wave final random = Random(42); // Fixed seed for consistency _waveData = List.generate(101, (i) { double hz; if (i < 20) { // Warmup hz = 10 + (i * 1.5) + (random.nextDouble() * 5); } else if (i < 50) { // Plateau/Tease hz = 40 + sin(i * 0.5) * 10 + (random.nextDouble() * 10); } else if (i < 85) { // Build up to climax hz = 70 + (i - 50) * 0.8 + (random.nextDouble() * 15); } else { // Cooldown hz = 90 - ((i - 85) * 5) + (random.nextDouble() * 5); } return FlSpot(i.toDouble(), hz.clamp(0, 100)); }); } void _startPlayback() { _timer?.cancel(); _timer = Timer.periodic(const Duration(milliseconds: 50), (timer) { if (!_isPlaying || _isDragging) return; setState(() { if (_progress >= 100) { _isPlaying = false; timer.cancel(); } else { _progress += 0.1; } }); }); } void _handleEmergencyStop() { setState(() { _isPlaying = false; _showAlert = true; }); Future.delayed(const Duration(milliseconds: 1500), () { if (mounted) { Navigator.of(context).pop(); } }); } int get _activeLineIndex { for (int i = 0; i < dialogueScript.length; i++) { final nextLine = i + 1 < dialogueScript.length ? dialogueScript[i + 1] : null; if (_progress >= dialogueScript[i].time && (nextLine == null || _progress < nextLine.time)) { return i; } } return 0; } double get _currentHz { final index = _progress.floor().clamp(0, 100); return _waveData[index].y; } @override void dispose() { _timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( onDoubleTap: _handleEmergencyStop, child: Scaffold( backgroundColor: const Color(0xFF2E1065), body: Stack( children: [ // Background Visual Positioned.fill( child: Opacity( opacity: 0.4, child: Stack( fit: StackFit.expand, children: [ Image.network( _scenario.cover, fit: BoxFit.cover, ), BackdropFilter( filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), child: Container(color: Colors.transparent), ), Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color(0x992E1065), Color(0xCC2E1065), Color(0xFF2E1065), ], stops: [0.0, 0.5, 1.0], ), ), ), ], ), ), ), // Emergency Overlay if (_showAlert) Positioned.fill( child: Container( color: const Color(0xF02E1065), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( LucideIcons.alertTriangle, size: 64, color: const Color(0xFFF43F5E), ), const SizedBox(height: 16), const Text( 'EMERGENCY STOP', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Color(0xFFF43F5E), letterSpacing: 4, ), ), const SizedBox(height: 8), const Text( '系统已强制中断', style: TextStyle(fontSize: 14, color: Colors.white70), ), ], ), ), ), // Main Content SafeArea( child: Column( children: [ // Header _buildHeader(), // Central Lyrics Expanded(child: _buildLyrics()), // Bottom Controls _buildControls(), ], ), ), ], ), ), ); } Widget _buildHeader() { return Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _scenario.title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 4), Text( '${_scenario.category} SCENARIO', style: const TextStyle( fontSize: 10, letterSpacing: 1, color: Color(0xFFC084FC), ), ), ], ), GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( width: 40, height: 40, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withOpacity(0.1), border: Border.all(color: Colors.white.withOpacity(0.1)), ), child: const Icon(LucideIcons.x, size: 20, color: Colors.white70), ), ), ], ), ); } Widget _buildLyrics() { // 简单展示:只显示当前行附近的几行 return Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 上面 2 行 for (int i = _activeLineIndex - 2; i < _activeLineIndex; i++) if (i >= 0) _buildLyricLine(i, isActive: false, distance: _activeLineIndex - i), // 当前行 _buildLyricLine(_activeLineIndex, isActive: true, distance: 0), // 下面 2 行 for (int i = _activeLineIndex + 1; i <= _activeLineIndex + 2; i++) if (i < dialogueScript.length) _buildLyricLine(i, isActive: false, distance: i - _activeLineIndex), ], ), ), ); } Widget _buildLyricLine(int index, {required bool isActive, required int distance}) { final opacity = isActive ? 1.0 : (distance > 1 ? 0.2 : 0.5); final scale = isActive ? 1.0 : 0.9; return AnimatedContainer( duration: const Duration(milliseconds: 400), height: isActive ? 80 : 56, child: AnimatedOpacity( duration: const Duration(milliseconds: 400), opacity: opacity, child: Transform.scale( scale: scale, child: Center( child: Text( dialogueScript[index].text, textAlign: TextAlign.center, style: TextStyle( fontSize: isActive ? 20 : 16, fontWeight: isActive ? FontWeight.bold : FontWeight.w400, color: isActive ? Colors.white : const Color(0xFFCBD5E1), shadows: isActive ? [ const Shadow( color: Color(0x99C084FC), blurRadius: 20, ), ] : null, ), ), ), ), ), ); } Widget _buildControls() { return Container( padding: const EdgeInsets.fromLTRB(0, 0, 0, 24), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [ const Color(0xFF2E1065), const Color(0xFF2E1065), const Color(0xFF2E1065).withOpacity(0), ], stops: const [0.0, 0.7, 1.0], ), ), child: Column( children: [ // Wave Chart SizedBox( height: 100, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: LineChart( LineChartData( gridData: const FlGridData(show: false), titlesData: const FlTitlesData(show: false), borderData: FlBorderData(show: false), minX: 0, maxX: 100, minY: 0, maxY: 100, lineBarsData: [ LineChartBarData( spots: _waveData, isCurved: true, curveSmoothness: 0.3, gradient: const LinearGradient( colors: [Color(0xFFC084FC), Color(0xFFF472B6)], ), barWidth: 3, isStrokeCapRound: true, dotData: const FlDotData(show: false), belowBarData: BarAreaData( show: true, gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ const Color(0xFFC084FC).withOpacity(0.5), const Color(0xFFC084FC).withOpacity(0), ], ), ), ), ], lineTouchData: const LineTouchData(enabled: false), extraLinesData: ExtraLinesData( verticalLines: [ VerticalLine( x: _progress, color: Colors.white.withOpacity(0.3), strokeWidth: 1, dashArray: [4, 4], ), ], ), ), ), ), ), const SizedBox(height: 16), // Control Bar Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Row( children: [ // Play/Pause Button GestureDetector( onTap: () { setState(() { _isPlaying = !_isPlaying; if (_isPlaying && _progress >= 100) { _progress = 0; } }); }, child: Container( width: 48, height: 48, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, boxShadow: [ BoxShadow( color: Colors.white.withOpacity(0.3), blurRadius: 20, ), ], ), child: Icon( _isPlaying ? LucideIcons.pause : LucideIcons.play, size: 20, color: const Color(0xFF2E1065), ), ), ), const SizedBox(width: 20), // Progress Info & Slider Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'DEVICE INTENSITY', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, letterSpacing: 1, color: Color(0xFFCBD5E1), ), ), const SizedBox(height: 2), Text( '伸缩频率: ${max(1, (_progress / 10).ceil())}档', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, fontFamily: 'monospace', color: Color(0xFFF472B6), ), ), ], ), Text( '${(_progress * 1.5).toStringAsFixed(0)}s / ${_scenario.duration}', style: const TextStyle( fontSize: 10, fontFamily: 'monospace', color: Color(0xFFCBD5E1), ), ), ], ), const SizedBox(height: 8), // Progress Slider SliderTheme( data: SliderThemeData( trackHeight: 4, thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8), overlayShape: const RoundSliderOverlayShape(overlayRadius: 16), activeTrackColor: const Color(0xFFF472B6), inactiveTrackColor: Colors.white.withOpacity(0.2), thumbColor: Colors.white, ), child: Slider( value: _progress.clamp(0, 100), min: 0, max: 100, onChangeStart: (_) => _isDragging = true, onChangeEnd: (_) => _isDragging = false, onChanged: (value) { setState(() { _progress = value; }); }, ), ), ], ), ), ], ), ), const SizedBox(height: 16), // Emergency Stop Hint Text( '双击屏幕紧急停止', style: TextStyle( fontSize: 10, letterSpacing: 2, color: Colors.white.withOpacity(0.4), ), ), ], ), ); } }