153 lines
4.6 KiB
Dart
153 lines
4.6 KiB
Dart
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<ShellPage> createState() => _ShellPageState();
|
|
}
|
|
|
|
class _ShellPageState extends State<ShellPage> {
|
|
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: '我的'),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|