- file_picker升级到12.x兼容win32 6.x - 新增scripts/patch_pub_cache.sh补丁脚本(quill_native_bridge_windows+flutter_vibrate) - bitsdojo_window→window_manager代码迁移 - 更新iOS_macOS_Developer_Guide.md v6(新增§2.6 pub cache补丁文档) - 更新CHANGELOG.md v6.9.51
381 lines
14 KiB
Dart
381 lines
14 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 自适应导航栏
|
||
/// 创建时间: 2026-05-29
|
||
/// 更新时间: 2026-05-29
|
||
/// 作用: 宽屏时垂直导航栏,窄屏时底部导航栏,支持4种停靠位置
|
||
/// 上次更新: 替换bitsdojo_window为window_manager桌面端窗口控制
|
||
/// ============================================================
|
||
|
||
import 'dart:ui';
|
||
import 'package:badges/badges.dart' as badges;
|
||
import 'package:window_manager/window_manager.dart';
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import '../providers/split_view_provider.dart';
|
||
import '../theme/app_theme.dart';
|
||
import '../theme/app_spacing.dart';
|
||
import '../theme/glass_tokens.dart';
|
||
import '../utils/platform/platform_utils.dart' as pu;
|
||
import '../../features/discover/providers/chat_provider.dart';
|
||
import '../../features/mine/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;
|
||
|
||
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,
|
||
),
|
||
),
|
||
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)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
final splitState = ref.watch(splitViewProvider);
|
||
final isRight = splitState.navBarPosition == NavBarPosition.right;
|
||
|
||
return Container(
|
||
width: 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(
|
||
right: isRight,
|
||
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, '我的'),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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;
|
||
final splitState = ref.watch(splitViewProvider);
|
||
final isTop = splitState.navBarPosition == NavBarPosition.top;
|
||
|
||
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: SafeArea(
|
||
top: isTop,
|
||
bottom: !isTop,
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||
children: [
|
||
buildTab(TabSpriteType.home, 0, '闲言'),
|
||
buildTab(TabSpriteType.discover, 1, '发现'),
|
||
buildTab(TabSpriteType.profile, 2, '我的'),
|
||
if (isTop && pu.isDesktop) _buildDesktopWindowControls(ext),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 桌面端窗口控制按钮(最小化/最大化/关闭)+ 可拖拽标题栏区域
|
||
Widget _buildDesktopWindowControls(AppThemeExtension ext) {
|
||
return Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
GestureDetector(
|
||
onPanStart: (_) => windowManager.startDragging(),
|
||
child: const SizedBox(height: 52, width: 40),
|
||
),
|
||
_WindowControlBtn(
|
||
icon: CupertinoIcons.minus,
|
||
ext: ext,
|
||
onPressed: () => windowManager.minimize(),
|
||
),
|
||
_WindowControlBtn(
|
||
icon: CupertinoIcons.square,
|
||
ext: ext,
|
||
iconSize: 12,
|
||
onPressed: () async {
|
||
if (await windowManager.isMaximized()) {
|
||
await windowManager.unmaximize();
|
||
} else {
|
||
await windowManager.maximize();
|
||
}
|
||
},
|
||
),
|
||
_WindowControlBtn(
|
||
icon: CupertinoIcons.xmark,
|
||
ext: ext,
|
||
onPressed: () => windowManager.close(),
|
||
isClose: true,
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 窗口控制按钮(最小化/最大化/关闭)
|
||
class _WindowControlBtn extends StatefulWidget {
|
||
const _WindowControlBtn({
|
||
required this.icon,
|
||
required this.ext,
|
||
required this.onPressed,
|
||
this.iconSize = 14,
|
||
this.isClose = false,
|
||
});
|
||
|
||
final IconData icon;
|
||
final AppThemeExtension ext;
|
||
final VoidCallback onPressed;
|
||
final double iconSize;
|
||
final bool isClose;
|
||
|
||
@override
|
||
State<_WindowControlBtn> createState() => _WindowControlBtnState();
|
||
}
|
||
|
||
class _WindowControlBtnState extends State<_WindowControlBtn> {
|
||
bool _isHovering = false;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final ext = widget.ext;
|
||
final bgColor = _isHovering
|
||
? (widget.isClose
|
||
? CupertinoColors.systemRed.withValues(alpha: 0.9)
|
||
: ext.textHint.withValues(alpha: 0.15))
|
||
: const Color(0x00000000);
|
||
|
||
return MouseRegion(
|
||
onEnter: (_) => setState(() => _isHovering = true),
|
||
onExit: (_) => setState(() => _isHovering = false),
|
||
child: GestureDetector(
|
||
onTap: widget.onPressed,
|
||
child: Container(
|
||
width: 46,
|
||
height: 52,
|
||
color: bgColor,
|
||
child: Icon(
|
||
widget.icon,
|
||
size: widget.iconSize,
|
||
color: _isHovering && widget.isClose
|
||
? CupertinoColors.white
|
||
: ext.textSecondary,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|