本次提交包含以下核心变更: 1. 修复 RawKeyboard 断言错误,添加 HardwareKeyboard 事件处理器 2. 实现 Intel Mac 自动降级玻璃渲染质量,避免黑屏闪烁 3. 新增 macOS 端 Impeller 渲染引擎开关设置,支持动态切换 4. 修复 macOS 双标题栏问题,隐藏系统原生交通灯按钮 5. 更新多语言国际化支持,新增 Impeller 相关翻译 6. 优化 WebRTC 依赖下载,使用国内镜像避免超时
573 lines
18 KiB
Dart
573 lines
18 KiB
Dart
// ============================================================
|
||
// 闲言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;
|
||
}
|
||
|
||
// ============================================================
|
||
// 窄屏布局(原有逻辑)
|
||
// ============================================================
|
||
|
||
/// 构建 GlassBottomBar(app 模式 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;
|
||
}
|
||
}
|