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

202 lines
5.8 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();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkForUpdateIfNeeded();
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_checkForUpdateIfNeeded();
}
}
Future<void> _checkForUpdateIfNeeded() async {
final now = DateTime.now();
// 如果从未检查过,或者已过冷却时间,则检查
if (_nextCheckAllowedTime == null || now.isAfter(_nextCheckAllowedTime!)) {
// 设置下次允许检查时间90-300秒随机间隔
final randomSeconds = 90 + Random().nextInt(211);
_nextCheckAllowedTime = now.add(Duration(seconds: randomSeconds));
// 延迟3秒避免启动时干扰用户
await Future.delayed(const Duration(seconds: 3));
if (!mounted) return;
try {
final result = await UpdateService.instance.checkForUpdate();
if (!mounted) return;
if (result.hasUpdate && result.versionInfo != null) {
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;
}
}
}