Files
xianyan/lib/core/layout/adaptive_nav_bar.dart
Developer 22987e4ad1 fix: MacBook Pro端iOS/macOS构建修复 - win32 6.x兼容+Swift 6修复
- 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
2026-06-01 11:15:59 +08:00

381 lines
14 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.
/// ============================================================
/// 闲言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,
),
),
),
);
}
}