import 'package:flutter/material.dart'; import '../data/models/models.dart'; import '../theme/wise_tokens.dart'; import 'app_card.dart'; class PlayerStateModel { const PlayerStateModel({ this.audioId = '', this.reportId = '', this.title = '', this.durationSec = 0, this.positionSec = 0, this.playing = false, this.speed = 1.0, }); final String audioId; final String reportId; final String title; final int durationSec; final int positionSec; final bool playing; final double speed; bool get hasAudio => audioId.isNotEmpty; PlayerStateModel copyWith({ String? audioId, String? reportId, String? title, int? durationSec, int? positionSec, bool? playing, double? speed, }) { return PlayerStateModel( audioId: audioId ?? this.audioId, reportId: reportId ?? this.reportId, title: title ?? this.title, durationSec: durationSec ?? this.durationSec, positionSec: positionSec ?? this.positionSec, playing: playing ?? this.playing, speed: speed ?? this.speed, ); } } class MiniPlayer extends StatelessWidget { const MiniPlayer({ required this.player, required this.onToggle, super.key, }); final PlayerStateModel player; final VoidCallback onToggle; @override Widget build(BuildContext context) { if (!player.hasAudio) return const SizedBox.shrink(); final ratio = player.durationSec == 0 ? 0.0 : player.positionSec / player.durationSec; return Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 8), child: AppCard( padding: const EdgeInsets.all(12), color: WiseColors.primary, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ IconButton.filled( onPressed: onToggle, icon: Icon(player.playing ? Icons.pause : Icons.play_arrow), style: IconButton.styleFrom( backgroundColor: WiseColors.secondary, foregroundColor: WiseColors.primary, ), ), const SizedBox(width: WiseSpacing.x2), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( player.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800), ), Text( '${formatDuration(player.positionSec)} / ${formatDuration(player.durationSec)} · ${player.speed.toStringAsFixed(1)}x', style: const TextStyle(color: Color(0xCCFFFFFF), fontSize: 12), ), ], ), ), ], ), const SizedBox(height: WiseSpacing.x2), LinearProgressIndicator( value: ratio.clamp(0, 1), minHeight: 4, backgroundColor: const Color(0x33FFFFFF), color: WiseColors.secondary, ), ], ), ), ); } } class PlayerCard extends StatelessWidget { const PlayerCard({ required this.title, required this.durationSec, required this.player, required this.onStart, required this.onToggle, required this.onSeek, required this.onSpeed, super.key, }); final String title; final int durationSec; final PlayerStateModel player; final VoidCallback onStart; final VoidCallback onToggle; final void Function(int delta) onSeek; final VoidCallback onSpeed; @override Widget build(BuildContext context) { final active = player.hasAudio && player.title == title; final position = active ? player.positionSec : 0; final ratio = durationSec == 0 ? 0.0 : position / durationSec; return AppCard( color: WiseColors.secondary200, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('音频解读', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: WiseSpacing.x2), Text(title, style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: WiseSpacing.x3), LinearProgressIndicator( value: ratio.clamp(0, 1), minHeight: 6, backgroundColor: Colors.white, color: WiseColors.accent, ), const SizedBox(height: WiseSpacing.x2), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(formatDuration(position), style: Theme.of(context).textTheme.bodySmall), Text(formatDuration(durationSec), style: Theme.of(context).textTheme.bodySmall), ], ), const SizedBox(height: WiseSpacing.x3), Row( children: [ IconButton.outlined(onPressed: () => onSeek(-15), icon: const Icon(Icons.replay_10)), IconButton.filled( onPressed: active ? onToggle : onStart, icon: Icon(active && player.playing ? Icons.pause : Icons.play_arrow), style: IconButton.styleFrom( backgroundColor: WiseColors.primary, foregroundColor: Colors.white, ), ), IconButton.outlined(onPressed: () => onSeek(15), icon: const Icon(Icons.forward_10)), const Spacer(), TextButton(onPressed: onSpeed, child: Text('${player.speed.toStringAsFixed(1)}x')), ], ), Text( '真实音频流待 /audio/{id}/stream 接入,当前为本地进度占位。', style: Theme.of(context).textTheme.bodySmall, ), ], ), ); } }