本次提交包含以下核心变更: 1. 修复 RawKeyboard 断言错误,添加 HardwareKeyboard 事件处理器 2. 实现 Intel Mac 自动降级玻璃渲染质量,避免黑屏闪烁 3. 新增 macOS 端 Impeller 渲染引擎开关设置,支持动态切换 4. 修复 macOS 双标题栏问题,隐藏系统原生交通灯按钮 5. 更新多语言国际化支持,新增 Impeller 相关翻译 6. 优化 WebRTC 依赖下载,使用国内镜像避免超时
308 lines
9.9 KiB
Dart
308 lines
9.9 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 鸿蒙端专用布局壳
|
||
/// 创建时间: 2026-05-18
|
||
/// 更新时间: 2026-06-12
|
||
/// 作用: 鸿蒙端使用 Scaffold+GlassBottomBar 替代 GoRouter+StatefulShellRoute
|
||
/// 上次更新: 从 core/layout/ 迁移至 app/layout/,修复架构层级违规
|
||
/// ============================================================
|
||
///
|
||
/// 根因: 鸿蒙端 Flutter 引擎中 MaterialApp.router + 额外包导入 = 白屏
|
||
/// 方案: 鸿蒙端仅阉割路由(GoRouter),保留液态玻璃效果
|
||
|
||
import 'package:badges/badges.dart' as badges;
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||
|
||
import '../../core/storage/kv_storage.dart';
|
||
import '../../core/theme/app_theme.dart';
|
||
import '../../core/utils/platform/glass_quality_resolver.dart';
|
||
import '../../core/utils/ui/interaction_animations.dart';
|
||
import '../../core/utils/logger.dart';
|
||
import '../../core/utils/platform/platform_utils.dart'
|
||
show OhosDeviceCapabilities;
|
||
import '../../features/onboarding/presentation/onboarding_page.dart';
|
||
import '../../features/discover/providers/chat_provider.dart';
|
||
import '../../features/discover/presentation/pages/home/discover_page.dart';
|
||
import '../../features/home/presentation/home_page.dart';
|
||
import '../../features/profile/presentation/profile_page.dart';
|
||
import '../../features/settings/providers/theme_settings_provider.dart';
|
||
import '../../l10n/translations.dart';
|
||
import '../../main.dart' show liquidGlassReady;
|
||
import '../../shared/widgets/containers/glass_bottom_nav_bar.dart';
|
||
import '../../shared/widgets/animation/tab_icon_sprite.dart';
|
||
|
||
class OhosAppShell extends ConsumerStatefulWidget {
|
||
const OhosAppShell({super.key});
|
||
|
||
static _OhosAppShellState? _instance;
|
||
|
||
/// 切换到指定 Tab 索引(供 OhosNavBridge.go() 调用)
|
||
static void switchTab(int index) {
|
||
_instance?.switchTab(index);
|
||
}
|
||
|
||
@override
|
||
ConsumerState<OhosAppShell> createState() => _OhosAppShellState();
|
||
}
|
||
|
||
class _OhosAppShellState extends ConsumerState<OhosAppShell> {
|
||
int _currentIndex = 0;
|
||
|
||
static const List<Widget> _tabPages = [
|
||
HomePage(),
|
||
DiscoverPage(),
|
||
ProfilePage(),
|
||
];
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
OhosAppShell._instance = this;
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
_checkOnboarding();
|
||
});
|
||
}
|
||
|
||
void _checkOnboarding() {
|
||
if (!mounted) return;
|
||
|
||
// 等待KvStorage初始化完成再检查引导页状态
|
||
// 避免Hive未初始化时默认值导致引导页重复弹出
|
||
if (!KvStorage.isReady) {
|
||
Log.i('🟢 [OHOS] 引导页检查: KvStorage未就绪,延迟检查');
|
||
Future.delayed(const Duration(milliseconds: 300), () {
|
||
if (mounted) _checkOnboarding();
|
||
});
|
||
return;
|
||
}
|
||
|
||
final shouldShow =
|
||
KvStorage.isFirstLaunch || KvStorage.shouldShowOnboarding;
|
||
if (shouldShow) {
|
||
Log.i('🟢 [OHOS] 引导页检查: 首次启动=$isFirstLaunch, 应显示引导=$shouldShow → 推送引导页');
|
||
Navigator.of(context).push<void>(
|
||
CupertinoPageRoute<void>(
|
||
fullscreenDialog: true,
|
||
builder: (_) => const OnboardingPage(),
|
||
),
|
||
);
|
||
} else {
|
||
Log.i('🟢 [OHOS] 引导页检查: 已完成引导,直接进入主页');
|
||
}
|
||
}
|
||
|
||
bool get isFirstLaunch => KvStorage.isFirstLaunch;
|
||
|
||
@override
|
||
void dispose() {
|
||
OhosAppShell._instance = null;
|
||
super.dispose();
|
||
}
|
||
|
||
/// 切换到指定 Tab 索引
|
||
void switchTab(int index) {
|
||
if (index >= 0 && index < _tabPages.length) {
|
||
setState(() => _currentIndex = index);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
OhosDeviceCapabilities.init(context);
|
||
|
||
final ext = AppTheme.ext(context);
|
||
final unreadCount = ref.watch(chatProvider).unreadCount;
|
||
final settings = ref.watch(themeSettingsProvider);
|
||
final expressionStyle = settings.tabExpressionStyle;
|
||
final characterId = settings.tabCharacterStyleId;
|
||
final animIntensity = settings.animationIntensity.durationMultiplier;
|
||
|
||
Log.i(
|
||
'🟢 [OHOS] OhosAppShell.build() — currentIndex=$_currentIndex liquidGlass=$liquidGlassReady',
|
||
);
|
||
|
||
final Widget bottomBar = liquidGlassReady
|
||
? _buildGlassBottomBar(
|
||
ext: ext,
|
||
unreadCount: unreadCount,
|
||
expressionStyle: expressionStyle,
|
||
characterId: characterId,
|
||
animIntensity: animIntensity,
|
||
)
|
||
: _buildFallbackNavBar(
|
||
ext: ext,
|
||
unreadCount: unreadCount,
|
||
expressionStyle: expressionStyle,
|
||
characterId: characterId,
|
||
animIntensity: animIntensity,
|
||
);
|
||
|
||
return CelebrationOverlay(
|
||
child: PopScope(
|
||
canPop: false,
|
||
onPopInvokedWithResult: (didPop, _) {
|
||
if (didPop) return;
|
||
},
|
||
child: Scaffold(
|
||
extendBody: true,
|
||
body: IndexedStack(index: _currentIndex, children: _tabPages),
|
||
bottomNavigationBar: bottomBar,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// GPU Shader 液态玻璃底部导航栏 (LiquidGlassWidgets 可用时)
|
||
Widget _buildGlassBottomBar({
|
||
required AppThemeExtension ext,
|
||
required int unreadCount,
|
||
required TabExpressionStyleOption expressionStyle,
|
||
required String characterId,
|
||
required double animIntensity,
|
||
}) {
|
||
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: _buildBadgeIcon(
|
||
unreadCount: unreadCount,
|
||
child: buildSpriteIcon(TabSpriteType.discover, 1, '发现'),
|
||
),
|
||
activeIcon: _buildBadgeIcon(
|
||
unreadCount: unreadCount,
|
||
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) => setState(() => _currentIndex = 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
|
||
? Colors.white.withValues(alpha: 0.08)
|
||
: Colors.black.withValues(alpha: 0.04),
|
||
indicatorSettings: LiquidGlassSettings(
|
||
thickness: 40,
|
||
blur: 25,
|
||
refractiveIndex: 1.8,
|
||
chromaticAberration: 1.2,
|
||
lightIntensity: 3.5,
|
||
ambientStrength: 1.2,
|
||
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(
|
||
thickness: 30,
|
||
blur: 1.5,
|
||
refractiveIndex: 1.5,
|
||
chromaticAberration: 0.8,
|
||
lightIntensity: 1.2,
|
||
saturation: 1.0,
|
||
ambientStrength: 0.6,
|
||
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.12,
|
||
innerBlur: 1.5,
|
||
glowOpacity: 0.4,
|
||
glowBlurRadius: 24,
|
||
glowSpreadRadius: 4,
|
||
);
|
||
}
|
||
|
||
/// 纯Dart BackdropFilter 液态玻璃底部导航栏 (降级方案)
|
||
Widget _buildFallbackNavBar({
|
||
required AppThemeExtension ext,
|
||
required int unreadCount,
|
||
required TabExpressionStyleOption expressionStyle,
|
||
required String characterId,
|
||
required double animIntensity,
|
||
}) {
|
||
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) => setState(() => _currentIndex = index),
|
||
ext: ext,
|
||
animationIntensity: animIntensity,
|
||
expressionStyle: expressionStyle,
|
||
characterId: characterId,
|
||
);
|
||
}
|
||
|
||
Widget _buildBadgeIcon({required int unreadCount, required Widget child}) {
|
||
if (unreadCount <= 0) return child;
|
||
return badges.Badge(
|
||
badgeContent: Text(
|
||
'$unreadCount',
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
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: child,
|
||
);
|
||
}
|
||
}
|