rwadurian/frontend/mining-app/lib/presentation/widgets/main_shell.dart

218 lines
6.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../core/router/routes.dart';
import '../../core/constants/app_colors.dart';
import '../../core/updater/update_service.dart';
import '../../core/updater/channels/self_hosted_updater.dart';
class MainShell extends StatefulWidget {
final Widget child;
const MainShell({super.key, required this.child});
@override
State<MainShell> createState() => _MainShellState();
}
class _MainShellState extends State<MainShell> with WidgetsBindingObserver {
/// 下次允许检查更新的时间static 防止 widget rebuild 重置)
static DateTime? _nextCheckAllowedTime;
@override
void initState() {
super.initState();
debugPrint('[MainShell] initState called, adding WidgetsBindingObserver');
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
debugPrint('[MainShell] postFrameCallback → 触发首次更新检查');
_checkForUpdateIfNeeded();
});
}
@override
void dispose() {
debugPrint('[MainShell] dispose called, removing WidgetsBindingObserver');
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
debugPrint('[MainShell] didChangeAppLifecycleState: $state');
if (state == AppLifecycleState.resumed) {
_checkForUpdateIfNeeded();
}
}
Future<void> _checkForUpdateIfNeeded() async {
final now = DateTime.now();
// 如果从未检查过,或者已过冷却时间,则检查
if (_nextCheckAllowedTime != null && now.isBefore(_nextCheckAllowedTime!)) {
debugPrint('[MainShell] 跳过更新检查,冷却中 (下次: $_nextCheckAllowedTime)');
return;
}
// 设置下次允许检查时间90-300秒随机间隔
final randomSeconds = 90 + Random().nextInt(211);
_nextCheckAllowedTime = now.add(Duration(seconds: randomSeconds));
debugPrint('[MainShell] 准备检查更新延迟3秒...');
// 延迟3秒避免启动时干扰用户
await Future.delayed(const Duration(seconds: 3));
if (!mounted) {
debugPrint('[MainShell] widget 已 unmount取消检查');
return;
}
try {
debugPrint('[MainShell] 开始调用 UpdateService.checkForUpdate(force: true)');
// force: true 跳过 UpdateService 内部的60分钟间隔限制
// 因为我们已有自己的90-300秒冷却节流
final result = await UpdateService.instance.checkForUpdate(force: true);
debugPrint('[MainShell] 检查结果: hasUpdate=${result.hasUpdate}, error=${result.error}');
if (!mounted) return;
if (result.hasUpdate && result.versionInfo != null) {
debugPrint('[MainShell] 发现新版本: ${result.versionInfo!.versionName},弹出更新对话框');
await SelfHostedUpdater.show(
context,
versionInfo: result.versionInfo!,
isForceUpdate: result.versionInfo!.isForceUpdate,
);
}
} catch (e) {
debugPrint('[MainShell] 检查更新失败: $e');
}
}
@override
Widget build(BuildContext context) {
final isDark = AppColors.isDark(context);
return Scaffold(
body: widget.child,
bottomNavigationBar: Container(
decoration: BoxDecoration(
color: AppColors.cardOf(context),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withOpacity(0.3)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: SafeArea(
top: false,
child: Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildNavItem(
context,
icon: Icons.favorite_outline,
activeIcon: Icons.favorite,
label: '贡献值',
index: 0,
),
_buildNavItem(
context,
icon: Icons.swap_horiz_outlined,
activeIcon: Icons.swap_horiz,
label: '兑换',
index: 1,
),
_buildNavItem(
context,
icon: Icons.account_balance_wallet_outlined,
activeIcon: Icons.account_balance_wallet,
label: '资产',
index: 2,
),
_buildNavItem(
context,
icon: Icons.person_outline,
activeIcon: Icons.person,
label: '我的',
index: 3,
),
],
),
),
),
),
);
}
Widget _buildNavItem(
BuildContext context, {
required IconData icon,
required IconData activeIcon,
required String label,
required int index,
}) {
final currentIndex = _calculateSelectedIndex(context);
final isSelected = currentIndex == index;
final inactiveColor = AppColors.textMutedOf(context);
return GestureDetector(
onTap: () => _onItemTapped(index, context),
behavior: HitTestBehavior.opaque,
child: SizedBox(
width: 64,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isSelected ? activeIcon : icon,
size: 24,
color: isSelected ? AppColors.orange : inactiveColor,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected ? AppColors.orange : inactiveColor,
),
),
],
),
),
);
}
int _calculateSelectedIndex(BuildContext context) {
final location = GoRouterState.of(context).uri.path;
if (location.startsWith(Routes.contribution)) return 0;
if (location.startsWith(Routes.trading)) return 1;
if (location.startsWith(Routes.asset)) return 2;
if (location.startsWith(Routes.profile)) return 3;
return 0;
}
void _onItemTapped(int index, BuildContext context) {
switch (index) {
case 0:
context.go(Routes.contribution);
break;
case 1:
context.go(Routes.trading);
break;
case 2:
context.go(Routes.asset);
break;
case 3:
context.go(Routes.profile);
break;
}
}
}