Files
xianyan/lib/app/layout/app_shell.dart
22 7ea4a068a1 feat(macos+flutter): 新增 Impeller 渲染引擎开关与 Intel Mac 渲染兼容修复
本次提交包含以下核心变更:
1. 修复 RawKeyboard 断言错误,添加 HardwareKeyboard 事件处理器
2. 实现 Intel Mac 自动降级玻璃渲染质量,避免黑屏闪烁
3. 新增 macOS 端 Impeller 渲染引擎开关设置,支持动态切换
4. 修复 macOS 双标题栏问题,隐藏系统原生交通灯按钮
5. 更新多语言国际化支持,新增 Impeller 相关翻译
6. 优化 WebRTC 依赖下载,使用国内镜像避免超时
2026-06-25 08:44:00 +08:00

573 lines
18 KiB
Dart
Raw Permalink 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-04-20
// 更新时间: 2026-06-23
// 作用: ShellRoute 布局壳,宽屏工作台三栏 + 窄屏底部导航
// 上次更新: 任务10扩展-简化floatingNavBar鸿蒙端防御性冗余判断工作台模式已过滤鸿蒙端
// ============================================================
import 'package:badges/badges.dart' as badges;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu;
import 'package:go_router/go_router.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import '../../core/providers/split_view_provider.dart';
import '../../core/theme/app_theme.dart';
import '../../core/utils/platform/glass_quality_resolver.dart';
import '../../core/utils/ui/interaction_animations.dart';
import '../../features/discover/providers/chat_provider.dart';
import '../../features/settings/providers/theme_settings_provider.dart';
import '../../l10n/translations.dart';
import '../../shared/widgets/animation/tab_icon_sprite.dart';
import '../../main.dart' show liquidGlassReady;
import '../../shared/widgets/containers/wallpaper_background.dart';
import '../../shared/widgets/containers/glass_bottom_nav_bar.dart';
import '../../shared/widgets/feedback/app_error_boundary.dart';
import '../../core/layout/adaptive_split_view.dart';
import '../../core/layout/workbench/workbench_layout.dart';
import '../../core/layout/workbench/right_panel_navigator.dart';
import '../../features/desktop/desktop_window_title_bar.dart';
import 'adaptive_nav_bar.dart';
import 'overview_dashboard.dart';
class AppShell extends ConsumerStatefulWidget {
const AppShell({super.key, required this.child});
final StatefulNavigationShell child;
static bool get _isOhos => pu.isOhos;
@override
ConsumerState<AppShell> createState() => _AppShellState();
}
class _AppShellState extends ConsumerState<AppShell> {
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
final int currentIndex = widget.child.currentIndex;
final screenWidth = MediaQuery.sizeOf(context).width;
final splitState = ref.watch(splitViewProvider);
// 工作台模式判断:宽屏 + 工作台开关开启 + 非鸿蒙端
final isWorkbench = isWorkbenchWidth(screenWidth) &&
splitState.workbenchEnabled &&
!pu.isOhos;
// 同步当前Tab索引到SplitViewProvider
if (splitState.currentTab != currentIndex) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
ref.read(splitViewProvider.notifier).setCurrentTab(currentIndex);
}
});
}
// 构建主内容
Widget content;
if (isWorkbench) {
content = _buildWorkbenchLayout(context, currentIndex);
} else {
content = _buildNarrowLayout(context, ext, currentIndex);
}
// 桌面端在顶部添加自定义软件样式标题栏
if (pu.isDesktop) {
return Column(
children: [
const DesktopWindowTitleBar(),
Expanded(child: content),
],
);
}
return content;
}
// ============================================================
// 工作台布局(宽屏三栏/双栏)
// ============================================================
Widget _buildWorkbenchLayout(BuildContext context, int currentIndex) {
final Widget navBar = AdaptiveNavBar(
currentIndex: currentIndex,
onTabSelected: (index) => _onTabTap(context, index),
);
// 任务8顶/底导航栏使用 app 模式的 GlassBottomBar悬浮不占位
// 任务10扩展简化鸿蒙端防御性冗余判断
// 工作台模式已在 build() 中通过 !pu.isOhos 过滤鸿蒙端,此处无需重复判断
final ext = AppTheme.ext(context);
final Widget floatingNavBar = _buildGlassBottomBar(context, currentIndex, ext);
// 右栏默认内容统一显示概览仪表盘合并三个Tab的仪表盘为一个
const Widget defaultRightPanel = OverviewDashboard();
final Widget body = WorkbenchLayout(
navigationShell: RepaintBoundary(
child: AppErrorBoundary(label: '主页面', child: widget.child),
),
navBar: navBar,
floatingNavBar: floatingNavBar,
defaultRightPanel: defaultRightPanel,
);
// 桌面端添加键盘快捷键
if (pu.isDesktop) {
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit1):
const _SwitchTabIntent(0),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit2):
const _SwitchTabIntent(1),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit3):
const _SwitchTabIntent(2),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyW):
const _ClosePanelIntent(),
// 工作台右栏返回快捷键
LogicalKeySet(LogicalKeyboardKey.escape):
const _RightPanelBackIntent(),
LogicalKeySet(
LogicalKeyboardKey.control, LogicalKeyboardKey.arrowLeft):
const _RightPanelBackIntent(),
},
child: Actions(
actions: <Type, Action<Intent>>{
_SwitchTabIntent: _SwitchTabAction(
onSwitch: (index) => _onTabTap(context, index),
),
_ClosePanelIntent: _ClosePanelAction(ref: ref),
_RightPanelBackIntent: _RightPanelBackAction(ref: ref),
},
child: Focus(child: body),
),
);
}
return body;
}
// ============================================================
// 窄屏布局(原有逻辑)
// ============================================================
/// 构建 GlassBottomBarapp 模式 tap 栏)
///
/// 任务8提取为独立方法供窄屏布局和工作台顶/底布局复用
/// 使用液态玻璃 GPU Shader 实现,悬浮不占位
Widget _buildGlassBottomBar(
BuildContext context,
int currentIndex,
AppThemeExtension ext,
) {
final unreadCount = ref.watch(chatProvider).unreadCount;
final settings = ref.watch(themeSettingsProvider);
final expressionStyle = settings.tabExpressionStyle;
final characterId = settings.tabCharacterStyleId;
final animIntensity = settings.animationIntensity.durationMultiplier;
int adjacentFor(int index) {
if (index == currentIndex) return 0;
if (index < currentIndex) return -1;
return 1;
}
Widget buildSpriteIcon(TabSpriteType type, int index, String label) {
return TabIconSprite(
type: type,
label: label,
isSelected: index == currentIndex,
adjacentDirection: adjacentFor(index),
animationIntensity: animIntensity,
characterId: characterId,
eyeScale: expressionStyle.eyeScale,
mouthCurve: expressionStyle.mouthCurve,
bounceMultiplier: expressionStyle.bounceMultiplier,
);
}
return GlassBottomBar(
tabs: [
GlassBottomBarTab(
label: '',
icon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'),
activeIcon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'),
glowColor: const Color(0xFFE8E8ED),
),
GlassBottomBarTab(
label: '',
icon: badges.Badge(
showBadge: unreadCount > 0,
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: buildSpriteIcon(TabSpriteType.discover, 1, '发现'),
),
activeIcon: badges.Badge(
showBadge: unreadCount > 0,
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: buildSpriteIcon(TabSpriteType.discover, 1, '发现'),
),
glowColor: const Color(0xFFE8E8ED),
),
GlassBottomBarTab(
label: '',
icon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'),
activeIcon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'),
glowColor: const Color(0xFFE8E8ED),
),
],
selectedIndex: currentIndex,
onTabSelected: (index) => _onTabTap(context, index),
quality: GlassQualityResolver.quality,
selectedIconColor: ext.accent,
unselectedIconColor: ext.isDark
? ext.accent.withValues(alpha: 0.38)
: const Color(0xFFAEAEB2),
barHeight: 68,
barBorderRadius: 34,
horizontalPadding: 16,
verticalPadding: 16,
indicatorColor: ext.isDark
? ext.textInverse.withValues(alpha: 0.08)
: ext.overlaySubtle,
indicatorSettings: LiquidGlassSettings(
thickness: 24,
blur: 12,
refractiveIndex: 1.45,
chromaticAberration: 0.3,
lightIntensity: 1.2,
ambientStrength: 0.6,
glassColor: ext.isDark
? const Color.from(alpha: 0.18, red: 1, green: 1, blue: 1)
: const Color.from(alpha: 0.12, red: 1, green: 1, blue: 1),
),
settings: LiquidGlassSettings(
blur: 1.0,
refractiveIndex: 1.35,
chromaticAberration: 0.2,
lightIntensity: 0.8,
saturation: 1.0,
ambientStrength: 0.4,
glassColor: ext.isDark
? const Color.from(alpha: 0.08, red: 1, green: 1, blue: 1)
: const Color.from(alpha: 0.05, red: 1, green: 1, blue: 1),
),
magnification: 1.08,
innerBlur: 1.0,
glowOpacity: 0.3,
glowBlurRadius: 16,
glowSpreadRadius: 2,
);
}
Widget _buildNarrowLayout(
BuildContext context,
AppThemeExtension ext,
int currentIndex,
) {
final Widget bottomBar = (AppShell._isOhos && !liquidGlassReady)
? _buildFallbackNavBar(context, currentIndex, ext, ref.watch(themeSettingsProvider))
: _buildGlassBottomBar(context, currentIndex, ext);
// 检测横屏窄屏模式横屏时tab栏移到左侧不显示底部导航栏
final size = MediaQuery.sizeOf(context);
final isLandscapeNarrow =
size.width > 600 &&
size.width < kCompactBreakpoint &&
size.height < size.width;
return CelebrationOverlay(
child: PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) {
if (didPop) return;
},
child: Scaffold(
extendBody: true,
body: _buildNarrowBody(),
bottomNavigationBar: isLandscapeNarrow ? null : bottomBar,
),
),
);
}
void _onTabTap(BuildContext context, int index) {
widget.child.goBranch(
index,
initialLocation: index == widget.child.currentIndex,
);
}
/// 窄屏布局body横屏未达工作台断点时左侧显示tab栏竖屏保持原样
///
/// 修复:横屏窄屏(600<width<768)时左侧空白用来显示tab栏
/// 替代之前的居中限宽方案
Widget _buildNarrowBody() {
final size = MediaQuery.sizeOf(context);
final isLandscapeNarrow =
size.width > 600 &&
size.width < kCompactBreakpoint &&
size.height < size.width;
final content = Stack(
children: [
const WallpaperBackground(),
RepaintBoundary(
child: AppErrorBoundary(label: '主页面', child: widget.child),
),
],
);
if (isLandscapeNarrow) {
// 横屏窄屏左侧显示垂直tab栏右侧显示页面内容
return Row(
children: [
_buildVerticalTabBar(),
Expanded(child: content),
],
);
}
return content;
}
/// 横屏窄屏模式下的垂直tab栏
Widget _buildVerticalTabBar() {
final ext = AppTheme.ext(context);
final settings = ref.watch(themeSettingsProvider);
final expressionStyle = settings.tabExpressionStyle;
final characterId = settings.tabCharacterStyleId;
final animIntensity = settings.animationIntensity.durationMultiplier;
final currentIndex = widget.child.currentIndex;
final unreadCount = ref.watch(chatProvider).unreadCount;
int adjacentFor(int index) {
if (index == currentIndex) return 0;
if (index < currentIndex) return -1;
return 1;
}
Widget buildSpriteIcon(TabSpriteType type, int index, String label) {
return TabIconSprite(
type: type,
label: label,
isSelected: index == currentIndex,
adjacentDirection: adjacentFor(index),
animationIntensity: animIntensity,
characterId: characterId,
eyeScale: expressionStyle.eyeScale,
mouthCurve: expressionStyle.mouthCurve,
bounceMultiplier: expressionStyle.bounceMultiplier,
);
}
final tabs = <Widget>[];
// 闲言
tabs.add(_buildVerticalTabItem(
ext: ext,
icon: buildSpriteIcon(TabSpriteType.home, 0, '闲言'),
isSelected: currentIndex == 0,
onTap: () => _onTabTap(context, 0),
));
// 发现带未读badge
tabs.add(_buildVerticalTabItem(
ext: ext,
icon: badges.Badge(
showBadge: unreadCount > 0,
badgeContent: Text(
'$unreadCount',
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: buildSpriteIcon(TabSpriteType.discover, 1, '发现'),
),
isSelected: currentIndex == 1,
onTap: () => _onTabTap(context, 1),
));
// 我的
tabs.add(_buildVerticalTabItem(
ext: ext,
icon: buildSpriteIcon(TabSpriteType.profile, 2, '我的'),
isSelected: currentIndex == 2,
onTap: () => _onTabTap(context, 2),
));
return Container(
width: 56,
decoration: BoxDecoration(
color: ext.bgPrimary.withValues(alpha: 0.85),
border: Border(
right: BorderSide(
color: ext.textHint.withValues(alpha: 0.1),
width: 0.5,
),
),
),
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: tabs,
),
),
);
}
/// 垂直tab栏单个item
Widget _buildVerticalTabItem({
required AppThemeExtension ext,
required Widget icon,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
width: 44,
height: 44,
decoration: BoxDecoration(
color: isSelected
? ext.accent.withValues(alpha: 0.12)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Center(child: icon),
),
);
}
Widget _buildFallbackNavBar(
BuildContext context,
int currentIndex,
AppThemeExtension ext,
ThemeSettingsState settings,
) {
final expressionStyle = settings.tabExpressionStyle;
final characterId = settings.tabCharacterStyleId;
final animIntensity = settings.animationIntensity.durationMultiplier;
final unreadCount = ref.watch(chatProvider).unreadCount;
final t = ref.watch(translationsProvider);
return GlassBottomNavBar(
items: [
GlassBottomNavBarItem(spriteType: TabSpriteType.home, label: t.navHome),
GlassBottomNavBarItem(
spriteType: TabSpriteType.discover,
label: t.navDiscover,
badgeCount: unreadCount,
),
GlassBottomNavBarItem(
spriteType: TabSpriteType.profile,
label: t.navProfile,
),
],
selectedIndex: currentIndex,
onTabSelected: (index) => _onTabTap(context, index),
ext: ext,
animationIntensity: animIntensity,
expressionStyle: expressionStyle,
characterId: characterId,
);
}
}
// ============================================================
// 桌面端键盘快捷键 — Intent & Action
// ============================================================
/// 切换Tab意图
class _SwitchTabIntent extends Intent {
const _SwitchTabIntent(this.index);
final int index;
}
/// 关闭面板意图
class _ClosePanelIntent extends Intent {
const _ClosePanelIntent();
}
/// 切换Tab动作
class _SwitchTabAction extends Action<_SwitchTabIntent> {
_SwitchTabAction({required this.onSwitch});
final void Function(int) onSwitch;
@override
Object? invoke(_SwitchTabIntent intent) {
onSwitch(intent.index);
return null;
}
}
/// 关闭面板动作
class _ClosePanelAction extends Action<_ClosePanelIntent> {
_ClosePanelAction({required this.ref});
final WidgetRef ref;
@override
Object? invoke(_ClosePanelIntent intent) {
// 关闭右栏当前页面:清空当前 Tab 的右栏栈
final currentTab = ref.read(splitViewProvider).currentTab;
ref.read(rightPanelStackProvider.notifier).clear(currentTab);
return null;
}
}
/// 右栏返回意图
class _RightPanelBackIntent extends Intent {
const _RightPanelBackIntent();
}
/// 右栏返回动作 — pop 栈顶,逐页返回上一页
class _RightPanelBackAction extends Action<_RightPanelBackIntent> {
_RightPanelBackAction({required this.ref});
final WidgetRef ref;
@override
Object? invoke(_RightPanelBackIntent intent) {
// pop 右栏栈顶条目,逐页返回(符合 iOS 标准返回语义)
final currentTab = ref.read(splitViewProvider).currentTab;
ref.read(rightPanelStackProvider.notifier).pop(currentTab);
return null;
}
}