fix:优化使用常用技术框架
This commit is contained in:
+96
-127
@@ -1,121 +1,26 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../data/api/report_data_source.dart';
|
||||
import '../data/models/models.dart';
|
||||
import '../routing/app_routes.dart';
|
||||
import '../data/providers.dart';
|
||||
import '../theme/app_icons.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});
|
||||
class ShellPage extends ConsumerWidget {
|
||||
const ShellPage({required this.child, required this.currentPath, super.key});
|
||||
|
||||
final ReportDataSource dataSource;
|
||||
final Widget child;
|
||||
final String currentPath;
|
||||
|
||||
@override
|
||||
State<ShellPage> createState() => _ShellPageState();
|
||||
}
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final player = ref.watch(audioPlayerControllerProvider);
|
||||
final controller = ref.read(audioPlayerControllerProvider.notifier);
|
||||
final selectedIndex = _tabs.indexWhere((tab) => tab.path == currentPath);
|
||||
final safeIndex = selectedIndex < 0 ? 0 : selectedIndex;
|
||||
|
||||
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(
|
||||
@@ -124,29 +29,93 @@ class _ShellPageState extends State<ShellPage> {
|
||||
Text('研听'),
|
||||
Text(
|
||||
'全球机构研报中文解读',
|
||||
style: TextStyle(fontSize: 12, color: WiseColors.textSecondary, fontWeight: FontWeight.w500),
|
||||
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: '我的'),
|
||||
],
|
||||
),
|
||||
],
|
||||
body: ColoredBox(
|
||||
color: WiseColors.canvas,
|
||||
child: Stack(children: [Positioned.fill(child: child)]),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MiniPlayer(player: player, onToggle: controller.toggleAudio),
|
||||
Container(
|
||||
height: 64,
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
|
||||
decoration: const BoxDecoration(
|
||||
color: WiseColors.canvas,
|
||||
border: Border(
|
||||
top: BorderSide(color: Color(0x11000000), width: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: List.generate(_tabs.length, (index) {
|
||||
final tab = _tabs[index];
|
||||
final active = index == safeIndex;
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => context.go(tab.path),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
tab.icon,
|
||||
size: 20,
|
||||
color: active
|
||||
? WiseColors.ink
|
||||
: WiseColors.textTertiary,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
tab.label,
|
||||
style:
|
||||
(Theme.of(context).textTheme.labelLarge ??
|
||||
const TextStyle())
|
||||
.copyWith(
|
||||
color: active
|
||||
? WiseColors.ink
|
||||
: WiseColors.textTertiary,
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
letterSpacing: 0.72,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabItem {
|
||||
const _TabItem({required this.label, required this.path, required this.icon});
|
||||
|
||||
final String label;
|
||||
final String path;
|
||||
final IconData icon;
|
||||
}
|
||||
|
||||
const List<_TabItem> _tabs = [
|
||||
_TabItem(label: '推荐', path: AppRoutes.home, icon: AppIcons.sparkle),
|
||||
_TabItem(label: '研报', path: AppRoutes.reports, icon: AppIcons.article),
|
||||
_TabItem(label: '机构', path: AppRoutes.institutions, icon: AppIcons.bank),
|
||||
_TabItem(label: '听单', path: AppRoutes.listen, icon: AppIcons.headphones),
|
||||
_TabItem(label: '我的', path: AppRoutes.profile, icon: AppIcons.user),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user