import 'dart:async'; import 'package:flutter/material.dart'; import '../data/api/report_data_source.dart'; import '../data/models/models.dart'; import '../theme/wise_tokens.dart'; import '../widgets/mini_player.dart'; import 'feed/feed_page.dart'; import 'institutions/institutions_page.dart'; import 'listen/listen_page.dart'; import 'profile/profile_page.dart'; import 'reports/reports_page.dart'; class ShellPage extends StatefulWidget { const ShellPage({required this.dataSource, super.key}); final ReportDataSource dataSource; @override State createState() => _ShellPageState(); } class _ShellPageState extends State { int index = 0; PlayerStateModel player = const PlayerStateModel(); Timer? timer; @override void dispose() { timer?.cancel(); super.dispose(); } void startAudio(AudioItem item) { timer?.cancel(); setState(() { player = PlayerStateModel( audioId: item.audioId, reportId: item.reportId, title: item.titleCn, durationSec: item.durationSec, playing: true, speed: player.speed, ); }); timer = Timer.periodic(const Duration(seconds: 1), (_) => tick()); } void startModuleAudio(String audioId, String reportId, String title, int durationSec) { startAudio( AudioItem( audioId: audioId, reportId: reportId, titleCn: title, reportTitleCn: title, durationSec: durationSec, institution: const Institution(id: '', nameCn: ''), ), ); } void tick() { if (!player.playing) return; final next = player.positionSec + player.speed.round().clamp(1, 2); setState(() { player = player.copyWith( positionSec: next >= player.durationSec ? player.durationSec : next, playing: next < player.durationSec, ); }); } void toggleAudio() { if (!player.hasAudio) return; setState(() => player = player.copyWith(playing: !player.playing)); } void seekAudio(int delta) { if (!player.hasAudio) return; setState(() { player = player.copyWith( positionSec: (player.positionSec + delta).clamp(0, player.durationSec), ); }); } void cycleSpeed() { const speeds = [1.0, 1.25, 1.5, 2.0]; final current = speeds.indexOf(player.speed); setState(() => player = player.copyWith(speed: speeds[(current + 1) % speeds.length])); } @override Widget build(BuildContext context) { final pages = [ FeedPage( dataSource: widget.dataSource, onPlay: startAudio, player: player, onStartModuleAudio: startModuleAudio, onToggleAudio: toggleAudio, onSeekAudio: seekAudio, onSpeed: cycleSpeed, ), ReportsPage( dataSource: widget.dataSource, onPlay: startAudio, player: player, onStartModuleAudio: startModuleAudio, onToggleAudio: toggleAudio, onSeekAudio: seekAudio, onSpeed: cycleSpeed, ), InstitutionsPage(dataSource: widget.dataSource), ListenPage(dataSource: widget.dataSource, onPlay: startAudio), ProfilePage(dataSource: widget.dataSource), ]; return Scaffold( appBar: AppBar( title: const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('研听'), Text( '全球机构研报中文解读', style: TextStyle(fontSize: 12, color: WiseColors.textSecondary, fontWeight: FontWeight.w500), ), ], ), ), body: pages[index], bottomNavigationBar: Column( mainAxisSize: MainAxisSize.min, children: [ MiniPlayer(player: player, onToggle: toggleAudio), NavigationBar( selectedIndex: index, onDestinationSelected: (value) => setState(() => index = value), destinations: const [ NavigationDestination(icon: Icon(Icons.auto_awesome_outlined), selectedIcon: Icon(Icons.auto_awesome), label: '推荐'), NavigationDestination(icon: Icon(Icons.article_outlined), selectedIcon: Icon(Icons.article), label: '研报'), NavigationDestination(icon: Icon(Icons.account_balance_outlined), selectedIcon: Icon(Icons.account_balance), label: '机构'), NavigationDestination(icon: Icon(Icons.headphones_outlined), selectedIcon: Icon(Icons.headphones), label: '听单'), NavigationDestination(icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: '我的'), ], ), ], ), ); } }