主要变更: 1. 新增桌面端托盘图标支持深色/浅色主题切换 2. 重构应用锁、动画配置、小组件导航服务职责 3. 修复Riverpod初始化断言、防重复点击、工作台模式残留选中态问题 4. 优化诗词服务、阅读进度、搜索结果空状态体验 5. 完善macOS打包配置与错误静默处理逻辑 6. 新增快速卡片多语言适配与动画退出队列管理
328 lines
13 KiB
Dart
328 lines
13 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 自适应导航栏
|
||
/// 创建时间: 2026-05-29
|
||
/// 更新时间: 2026-06-23
|
||
/// 作用: 宽屏时垂直导航栏,窄屏时底部导航栏,支持4种停靠位置
|
||
/// 上次更新: 任务10扩展-水平模式移除窗口控制按钮(DesktopWindowTitleBar已提供),修复floatingNavBar回退时重复显示
|
||
/// ============================================================
|
||
|
||
import 'dart:ui';
|
||
import 'package:badges/badges.dart' as badges;
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import '../../core/providers/split_view_provider.dart';
|
||
import '../../core/theme/app_theme.dart';
|
||
import '../../core/theme/app_spacing.dart';
|
||
import '../../core/theme/glass_tokens.dart';
|
||
import '../../features/discover/providers/chat_provider.dart';
|
||
import '../../features/settings/providers/theme_settings_provider.dart';
|
||
import '../../shared/widgets/animation/tab_icon_sprite.dart';
|
||
|
||
class AdaptiveNavBar extends ConsumerWidget {
|
||
const AdaptiveNavBar({
|
||
required this.currentIndex,
|
||
required this.onTabSelected,
|
||
super.key,
|
||
});
|
||
|
||
final int currentIndex;
|
||
final ValueChanged<int> onTabSelected;
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final splitState = ref.watch(splitViewProvider);
|
||
final position = splitState.navBarPosition;
|
||
return switch (position) {
|
||
NavBarPosition.left ||
|
||
NavBarPosition.right => _buildVertical(context, ref),
|
||
NavBarPosition.top ||
|
||
NavBarPosition.bottom => _buildHorizontal(context, ref),
|
||
};
|
||
}
|
||
|
||
Widget _buildVertical(BuildContext context, WidgetRef ref) {
|
||
final ext = AppTheme.ext(context);
|
||
final settings = ref.watch(themeSettingsProvider);
|
||
final unreadCount = ref.watch(chatProvider).unreadCount;
|
||
final animIntensity = settings.animationIntensity.durationMultiplier;
|
||
final expressionStyle = settings.tabExpressionStyle;
|
||
final characterId = settings.tabCharacterStyleId;
|
||
final splitState = ref.watch(splitViewProvider);
|
||
final isCollapsed = splitState.navBarCollapsed;
|
||
|
||
Widget buildTab(TabSpriteType type, int index, String label) {
|
||
final isSelected = index == currentIndex;
|
||
final showBadge = index == 1 && unreadCount > 0;
|
||
|
||
return GestureDetector(
|
||
onTap: () => onTabSelected(index),
|
||
behavior: HitTestBehavior.opaque,
|
||
child: AnimatedContainer(
|
||
duration: const Duration(milliseconds: 200),
|
||
curve: Curves.easeOut,
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
AnimatedScale(
|
||
scale: isSelected ? 1.08 : 1.0,
|
||
duration: const Duration(milliseconds: 200),
|
||
curve: Curves.easeOut,
|
||
child: showBadge
|
||
? badges.Badge(
|
||
badgeContent: Text(
|
||
'',
|
||
style: TextStyle(
|
||
color: ext.textOnAccent,
|
||
fontSize: 9,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
badgeStyle: const badges.BadgeStyle(
|
||
badgeColor: CupertinoColors.systemRed,
|
||
padding: EdgeInsets.all(3),
|
||
),
|
||
position: badges.BadgePosition.topEnd(top: -4, end: -6),
|
||
child: TabIconSprite(
|
||
type: type,
|
||
label: '',
|
||
isSelected: isSelected,
|
||
adjacentDirection: 0,
|
||
animationIntensity: animIntensity,
|
||
characterId: characterId,
|
||
eyeScale: expressionStyle.eyeScale,
|
||
mouthCurve: expressionStyle.mouthCurve,
|
||
bounceMultiplier: expressionStyle.bounceMultiplier,
|
||
),
|
||
)
|
||
: TabIconSprite(
|
||
type: type,
|
||
label: '',
|
||
isSelected: isSelected,
|
||
adjacentDirection: 0,
|
||
animationIntensity: animIntensity,
|
||
characterId: characterId,
|
||
eyeScale: expressionStyle.eyeScale,
|
||
mouthCurve: expressionStyle.mouthCurve,
|
||
bounceMultiplier: expressionStyle.bounceMultiplier,
|
||
),
|
||
),
|
||
// 折叠态隐藏文字标签
|
||
AnimatedSize(
|
||
duration: const Duration(milliseconds: 200),
|
||
curve: Curves.easeOut,
|
||
child: isCollapsed
|
||
? const SizedBox.shrink()
|
||
: Column(
|
||
children: [
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
fontWeight: isSelected
|
||
? FontWeight.w600
|
||
: FontWeight.w400,
|
||
color: isSelected
|
||
? (ext.isDark ? ext.textInverse : ext.accent)
|
||
: (ext.isDark
|
||
? ext.textInverse.withValues(
|
||
alpha: 0.38,
|
||
)
|
||
: const Color(0xFFAEAEB2)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
return AnimatedContainer(
|
||
duration: const Duration(milliseconds: 200),
|
||
curve: Curves.easeOut,
|
||
width: isCollapsed ? 48 : 72,
|
||
decoration: BoxDecoration(
|
||
color: ext.glassColor.withValues(
|
||
alpha: ext.isDark
|
||
? GlassTokens.elevatedOpacityDark
|
||
: GlassTokens.elevatedOpacityLight,
|
||
),
|
||
),
|
||
child: ClipRect(
|
||
child: BackdropFilter(
|
||
filter: ImageFilter.blur(
|
||
sigmaX: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
|
||
sigmaY: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
|
||
),
|
||
child: SafeArea(
|
||
// 垂直导航栏:仅处理顶部和侧边安全区域
|
||
// 底部不处理(Column 内有 Spacer 自适应,避免底部 SafeArea 挤压内容)
|
||
bottom: false,
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
buildTab(TabSpriteType.home, 0, '闲言'),
|
||
const SizedBox(height: AppSpacing.lg),
|
||
buildTab(TabSpriteType.discover, 1, '发现'),
|
||
const SizedBox(height: AppSpacing.lg),
|
||
buildTab(TabSpriteType.profile, 2, '我的'),
|
||
const Spacer(),
|
||
// 折叠/展开按钮
|
||
_buildCollapseButton(ref, isCollapsed, ext),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 折叠/展开导航栏按钮
|
||
Widget _buildCollapseButton(
|
||
WidgetRef ref,
|
||
bool isCollapsed,
|
||
AppThemeExtension ext,
|
||
) {
|
||
return GestureDetector(
|
||
onTap: () => ref.read(splitViewProvider.notifier).toggleNavBarCollapsed(),
|
||
behavior: HitTestBehavior.opaque,
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
child: Icon(
|
||
isCollapsed
|
||
? CupertinoIcons.chevron_right
|
||
: CupertinoIcons.chevron_left,
|
||
size: 16,
|
||
color: ext.textSecondary,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildHorizontal(BuildContext context, WidgetRef ref) {
|
||
final ext = AppTheme.ext(context);
|
||
final settings = ref.watch(themeSettingsProvider);
|
||
final unreadCount = ref.watch(chatProvider).unreadCount;
|
||
final animIntensity = settings.animationIntensity.durationMultiplier;
|
||
final expressionStyle = settings.tabExpressionStyle;
|
||
final characterId = settings.tabCharacterStyleId;
|
||
|
||
Widget buildTab(TabSpriteType type, int index, String label) {
|
||
final isSelected = index == currentIndex;
|
||
final showBadge = index == 1 && unreadCount > 0;
|
||
|
||
return Expanded(
|
||
child: GestureDetector(
|
||
onTap: () => onTabSelected(index),
|
||
behavior: HitTestBehavior.opaque,
|
||
child: AnimatedContainer(
|
||
duration: const Duration(milliseconds: 200),
|
||
curve: Curves.easeOut,
|
||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
AnimatedScale(
|
||
scale: isSelected ? 1.08 : 1.0,
|
||
duration: const Duration(milliseconds: 200),
|
||
curve: Curves.easeOut,
|
||
child: showBadge
|
||
? badges.Badge(
|
||
badgeContent: Text(
|
||
'',
|
||
style: TextStyle(
|
||
color: ext.textOnAccent,
|
||
fontSize: 9,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
badgeStyle: const badges.BadgeStyle(
|
||
badgeColor: CupertinoColors.systemRed,
|
||
padding: EdgeInsets.all(3),
|
||
),
|
||
position: badges.BadgePosition.topEnd(
|
||
top: -4,
|
||
end: -6,
|
||
),
|
||
child: TabIconSprite(
|
||
type: type,
|
||
label: '',
|
||
isSelected: isSelected,
|
||
adjacentDirection: 0,
|
||
animationIntensity: animIntensity,
|
||
characterId: characterId,
|
||
eyeScale: expressionStyle.eyeScale,
|
||
mouthCurve: expressionStyle.mouthCurve,
|
||
bounceMultiplier: expressionStyle.bounceMultiplier,
|
||
),
|
||
)
|
||
: TabIconSprite(
|
||
type: type,
|
||
label: '',
|
||
isSelected: isSelected,
|
||
adjacentDirection: 0,
|
||
animationIntensity: animIntensity,
|
||
characterId: characterId,
|
||
eyeScale: expressionStyle.eyeScale,
|
||
mouthCurve: expressionStyle.mouthCurve,
|
||
bounceMultiplier: expressionStyle.bounceMultiplier,
|
||
),
|
||
),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||
color: isSelected
|
||
? (ext.isDark ? ext.textInverse : ext.accent)
|
||
: (ext.isDark
|
||
? ext.textInverse.withValues(alpha: 0.38)
|
||
: const Color(0xFFAEAEB2)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
return Container(
|
||
height: 52,
|
||
decoration: BoxDecoration(
|
||
color: ext.glassColor.withValues(
|
||
alpha: ext.isDark
|
||
? GlassTokens.elevatedOpacityDark
|
||
: GlassTokens.elevatedOpacityLight,
|
||
),
|
||
),
|
||
child: ClipRect(
|
||
child: BackdropFilter(
|
||
filter: ImageFilter.blur(
|
||
sigmaX: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
|
||
sigmaY: GlassTokens.elevatedBlur * ext.glassBlurMultiplier,
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||
children: [
|
||
buildTab(TabSpriteType.home, 0, '闲言'),
|
||
buildTab(TabSpriteType.discover, 1, '发现'),
|
||
buildTab(TabSpriteType.profile, 2, '我的'),
|
||
// 任务10扩展:移除水平模式的窗口控制按钮
|
||
// DesktopWindowTitleBar 已在顶部提供完整窗口控制(macOS红黄绿灯/Windows方按钮)
|
||
// 此处重复显示会导致用户困惑,且占用导航栏空间
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|