218 lines
6.7 KiB
Dart
218 lines
6.7 KiB
Dart
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;
|
||
}
|
||
}
|
||
}
|