chore: 完成项目品牌域名批量替换与功能迭代
本次提交包含多项核心更新: 1. 全量替换项目内所有xianyan.app域名变更为s2ss.com,包含配置文件、路由、隐私政策等 2. 重构图表库从fl_chart迁移至syncfusion_flutter_charts,优化图表渲染效果 3. 新增宽屏分屏布局支持,包含右侧面板注册表与可拖拽分割线 4. 完善触觉反馈服务与认证感知Mixin,修复多处内存泄漏问题 5. 合并勋章墙与金币记录入口至成就中心,简化个人中心导航 6. 新增收藏与时间线数据合并导入功能 7. 修复多处UI样式问题,统一主题颜色使用规范 8. 新增日历同步与跨平台触觉反馈依赖库 9. 修复BotToast初始化流程,避免路由切换时的弹窗崩溃
This commit is contained in:
166
lib/core/layout/adaptive_nav_bar.dart
Normal file
166
lib/core/layout/adaptive_nav_bar.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 自适应导航栏
|
||||
/// 创建时间: 2026-05-29
|
||||
/// 更新时间: 2026-05-29
|
||||
/// 作用: 宽屏时垂直导航栏,窄屏时底部导航栏,支持4种停靠位置
|
||||
/// 上次更新: 适配Riverpod 3.0统一Provider
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:ui';
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
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 '../../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(
|
||||
showBadge: true,
|
||||
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) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
137
lib/core/layout/adaptive_split_view.dart
Normal file
137
lib/core/layout/adaptive_split_view.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 自适应分屏组件
|
||||
/// 创建时间: 2026-05-29
|
||||
/// 更新时间: 2026-05-29
|
||||
/// 作用: 宽屏时左右分屏布局,支持可拖拽分割线、手势隔离、动画过渡
|
||||
/// 上次更新: 适配Riverpod 3.0统一Provider
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../providers/split_view_provider.dart';
|
||||
import 'split_divider.dart';
|
||||
|
||||
/// 分屏断点:宽度 >= 900px 进入分屏模式
|
||||
const double kSplitViewBreakpoint = 900.0;
|
||||
|
||||
class AdaptiveSplitView extends ConsumerStatefulWidget {
|
||||
const AdaptiveSplitView({
|
||||
required this.leftPanel,
|
||||
required this.rightPanel,
|
||||
this.minLeftWidth = 320,
|
||||
this.minRightWidth = 400,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget leftPanel;
|
||||
final Widget rightPanel;
|
||||
final double minLeftWidth;
|
||||
final double minRightWidth;
|
||||
|
||||
@override
|
||||
ConsumerState<AdaptiveSplitView> createState() => _AdaptiveSplitViewState();
|
||||
}
|
||||
|
||||
class _AdaptiveSplitViewState extends ConsumerState<AdaptiveSplitView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _panelAnimationController;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
bool _wasSplitView = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_panelAnimationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
);
|
||||
_slideAnimation =
|
||||
Tween<Offset>(begin: const Offset(1.0, 0.0), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _panelAnimationController,
|
||||
curve: Curves.easeInOutCubic,
|
||||
),
|
||||
);
|
||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _panelAnimationController, curve: Curves.easeIn),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_panelAnimationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
final splitState = ref.watch(splitViewProvider);
|
||||
final isSplitView =
|
||||
screenWidth >= kSplitViewBreakpoint && splitState.splitViewEnabled;
|
||||
final splitRatio = splitState.splitRatio;
|
||||
final hasRightContent = splitState.activeRightPanel != null;
|
||||
|
||||
if (isSplitView && hasRightContent) {
|
||||
_panelAnimationController.forward();
|
||||
} else {
|
||||
_panelAnimationController.reverse();
|
||||
}
|
||||
|
||||
if (!isSplitView) {
|
||||
if (_wasSplitView) {
|
||||
_wasSplitView = false;
|
||||
}
|
||||
return widget.leftPanel;
|
||||
}
|
||||
|
||||
_wasSplitView = true;
|
||||
|
||||
final leftWidth = screenWidth * splitRatio;
|
||||
const double dividerWidth = 17.0;
|
||||
final double clampedLeftWidth = leftWidth.clamp(
|
||||
widget.minLeftWidth,
|
||||
screenWidth - widget.minRightWidth - dividerWidth,
|
||||
);
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: clampedLeftWidth,
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (_) => true,
|
||||
child: widget.leftPanel,
|
||||
),
|
||||
),
|
||||
SplitDivider(
|
||||
currentPosition: splitRatio,
|
||||
onPositionChanged: (newRatio) {
|
||||
ref.read(splitViewProvider.notifier).setSplitRatio(newRatio);
|
||||
},
|
||||
minPosition: widget.minLeftWidth / screenWidth,
|
||||
maxPosition:
|
||||
(screenWidth - widget.minRightWidth - dividerWidth) / screenWidth,
|
||||
),
|
||||
SizedBox(
|
||||
width: (screenWidth - clampedLeftWidth - dividerWidth).clamp(
|
||||
widget.minRightWidth,
|
||||
screenWidth - widget.minLeftWidth - dividerWidth,
|
||||
),
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (_) => true,
|
||||
child: widget.rightPanel,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 应用布局壳
|
||||
// 创建时间: 2026-04-20
|
||||
// 更新时间: 2026-05-18
|
||||
// 作用: ShellRoute 布局壳,包含底部 GlassBottomBar 导航 + 发现小红点
|
||||
// 上次更新: 恢复鸿蒙端液态玻璃效果,统一使用GlassBottomBar
|
||||
// 更新时间: 2026-05-29
|
||||
// 作用: ShellRoute 布局壳,宽屏分屏 + 窄屏底部导航
|
||||
// 上次更新: 集成宽屏分屏布局AdaptiveSplitView+AdaptiveNavBar
|
||||
// ============================================================
|
||||
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
@@ -14,6 +14,7 @@ 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 '../providers/split_view_provider.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../utils/ui/interaction_animations.dart';
|
||||
import '../utils/logger.dart';
|
||||
@@ -23,8 +24,12 @@ import '../../l10n/translations.dart';
|
||||
import '../../shared/widgets/animation/tab_icon_sprite.dart';
|
||||
import '../../main.dart' show liquidGlassReady;
|
||||
import '../../shared/widgets/containers/glass_bottom_nav_bar.dart';
|
||||
import 'adaptive_split_view.dart';
|
||||
import 'adaptive_nav_bar.dart';
|
||||
import 'overview_dashboard.dart';
|
||||
import 'right_panel_registry.dart';
|
||||
|
||||
class AppShell extends ConsumerWidget {
|
||||
class AppShell extends ConsumerStatefulWidget {
|
||||
const AppShell({super.key, required this.child});
|
||||
|
||||
final StatefulNavigationShell child;
|
||||
@@ -32,14 +37,123 @@ class AppShell extends ConsumerWidget {
|
||||
static bool get _isOhos => pu.isOhos;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<AppShell> createState() => _AppShellState();
|
||||
}
|
||||
|
||||
class _AppShellState extends ConsumerState<AppShell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final int currentIndex = child.currentIndex;
|
||||
if (_isOhos) {
|
||||
final int currentIndex = widget.child.currentIndex;
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
final splitState = ref.watch(splitViewProvider);
|
||||
final isWidescreen = screenWidth >= kSplitViewBreakpoint &&
|
||||
splitState.splitViewEnabled;
|
||||
|
||||
if (AppShell._isOhos) {
|
||||
Log.i(
|
||||
'🟢 [OHOS] AppShell.build() — currentIndex=$currentIndex isDark=${ext.isDark}',
|
||||
);
|
||||
}
|
||||
|
||||
// 同步当前Tab索引到SplitViewProvider
|
||||
if (splitState.currentTab != currentIndex) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
ref.read(splitViewProvider.notifier).setCurrentTab(currentIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isWidescreen) {
|
||||
return _buildWidescreenLayout(context, currentIndex);
|
||||
}
|
||||
|
||||
return _buildNarrowLayout(context, ext, currentIndex);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 宽屏分屏布局
|
||||
// ============================================================
|
||||
|
||||
Widget _buildWidescreenLayout(BuildContext context, int currentIndex) {
|
||||
final splitState = ref.watch(splitViewProvider);
|
||||
final navBarPosition = splitState.navBarPosition;
|
||||
final isNavBarVertical =
|
||||
navBarPosition == NavBarPosition.left || navBarPosition == NavBarPosition.right;
|
||||
|
||||
Widget navBar = AdaptiveNavBar(
|
||||
currentIndex: currentIndex,
|
||||
onTabSelected: (index) => _onTabTap(context, index),
|
||||
);
|
||||
|
||||
Widget splitView = AdaptiveSplitView(
|
||||
leftPanel: RepaintBoundary(child: widget.child),
|
||||
rightPanel: _buildRightPanel(context),
|
||||
);
|
||||
|
||||
if (isNavBarVertical) {
|
||||
final isLeft = navBarPosition == NavBarPosition.left;
|
||||
return CelebrationOverlay(
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (isLeft) navBar,
|
||||
Expanded(child: splitView),
|
||||
if (!isLeft) navBar,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 水平导航栏(顶部/底部)— 暂时使用底部导航
|
||||
return CelebrationOverlay(
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: splitView,
|
||||
bottomNavigationBar: navBar,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建右侧面板内容
|
||||
Widget _buildRightPanel(BuildContext context) {
|
||||
final splitState = ref.watch(splitViewProvider);
|
||||
final activePanel = splitState.activeRightPanel;
|
||||
|
||||
if (activePanel == null) {
|
||||
return const OverviewDashboard();
|
||||
}
|
||||
|
||||
return RightPanelRegistry.build(
|
||||
activePanel,
|
||||
context,
|
||||
args: splitState.rightPanelArgs,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 窄屏布局(原有逻辑)
|
||||
// ============================================================
|
||||
|
||||
Widget _buildNarrowLayout(
|
||||
BuildContext context,
|
||||
AppThemeExtension ext,
|
||||
int currentIndex,
|
||||
) {
|
||||
final unreadCount = ref.watch(chatProvider).unreadCount;
|
||||
final settings = ref.watch(themeSettingsProvider);
|
||||
final expressionStyle = settings.tabExpressionStyle;
|
||||
@@ -66,8 +180,8 @@ class AppShell extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
final Widget bottomBar = (_isOhos && !liquidGlassReady)
|
||||
? _buildFallbackNavBar(context, currentIndex, ext, settings, ref)
|
||||
final Widget bottomBar = (AppShell._isOhos && !liquidGlassReady)
|
||||
? _buildFallbackNavBar(context, currentIndex, ext, settings)
|
||||
: GlassBottomBar(
|
||||
tabs: [
|
||||
GlassBottomBarTab(
|
||||
@@ -172,7 +286,7 @@ class AppShell extends ConsumerWidget {
|
||||
},
|
||||
child: Scaffold(
|
||||
extendBody: true,
|
||||
body: Stack(children: [RepaintBoundary(child: child)]),
|
||||
body: Stack(children: [RepaintBoundary(child: widget.child)]),
|
||||
bottomNavigationBar: bottomBar,
|
||||
),
|
||||
),
|
||||
@@ -180,7 +294,7 @@ class AppShell extends ConsumerWidget {
|
||||
}
|
||||
|
||||
void _onTabTap(BuildContext context, int index) {
|
||||
child.goBranch(index, initialLocation: index == child.currentIndex);
|
||||
widget.child.goBranch(index, initialLocation: index == widget.child.currentIndex);
|
||||
}
|
||||
|
||||
Widget _buildFallbackNavBar(
|
||||
@@ -188,7 +302,6 @@ class AppShell extends ConsumerWidget {
|
||||
int currentIndex,
|
||||
AppThemeExtension ext,
|
||||
ThemeSettingsState settings,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
final expressionStyle = settings.tabExpressionStyle;
|
||||
final characterId = settings.tabCharacterStyleId;
|
||||
|
||||
278
lib/core/layout/overview_dashboard.dart
Normal file
278
lib/core/layout/overview_dashboard.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 概览仪表盘
|
||||
/// 创建时间: 2026-05-29
|
||||
/// 更新时间: 2026-05-29
|
||||
/// 作用: 宽屏分屏右侧面板的空状态页面,显示概览信息
|
||||
/// 上次更新: 修复未使用变量警告
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../theme/app_theme.dart';
|
||||
import '../theme/app_spacing.dart';
|
||||
import '../../shared/widgets/containers/glass_container.dart';
|
||||
|
||||
class OverviewDashboard extends ConsumerWidget {
|
||||
const OverviewDashboard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildGreeting(context),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildTodayRecommend(context),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildQuickActions(context),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildRecentHistory(context),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildStats(context),
|
||||
const SizedBox(height: AppSpacing.xxl),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGreeting(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final hour = DateTime.now().hour;
|
||||
final greeting = switch (hour) {
|
||||
>= 6 && < 12 => '早上好 ☀️',
|
||||
>= 12 && < 14 => '中午好 🌤️',
|
||||
>= 14 && < 18 => '下午好 🌅',
|
||||
>= 18 && < 22 => '晚上好 🌙',
|
||||
_ => '夜深了 🌛',
|
||||
};
|
||||
|
||||
return GlassContainer(
|
||||
depth: GlassDepth.elevated,
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
greeting,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'选择左侧内容查看详情',
|
||||
style: TextStyle(fontSize: 14, color: ext.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTodayRecommend(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'✨ 今日推荐',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: 3,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: AppSpacing.sm),
|
||||
itemBuilder: (context, index) {
|
||||
return GlassContainer(
|
||||
depth: GlassDepth.base,
|
||||
width: 180,
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'📝 示例句子 ${index + 1}',
|
||||
style: TextStyle(fontSize: 13, color: ext.textPrimary),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'—— 作者',
|
||||
style: TextStyle(fontSize: 11, color: ext.textHint),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActions(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final actions = [
|
||||
('🔍', '搜索'),
|
||||
('⭐', '收藏'),
|
||||
('📖', '稍后读'),
|
||||
('📝', '笔记'),
|
||||
('📊', '统计'),
|
||||
('⚙️', '设置'),
|
||||
('🎨', '主题'),
|
||||
('🔔', '提醒'),
|
||||
];
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'🚀 快捷操作',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Wrap(
|
||||
spacing: AppSpacing.sm,
|
||||
runSpacing: AppSpacing.sm,
|
||||
children: actions.map((action) {
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
child: GlassContainer(
|
||||
depth: GlassDepth.base,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(action.$1, style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
action.$2,
|
||||
style: TextStyle(fontSize: 13, color: ext.textPrimary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecentHistory(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'🕐 最近浏览',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
GlassContainer(
|
||||
depth: GlassDepth.base,
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Text('📭', style: TextStyle(fontSize: 32)),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
'暂无浏览记录',
|
||||
style: TextStyle(fontSize: 13, color: ext.textHint),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStats(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final stats = [
|
||||
('📖', '阅读', '0'),
|
||||
('⭐', '收藏', '0'),
|
||||
('👍', '点赞', '0'),
|
||||
('🔥', '连续', '0天'),
|
||||
];
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'📊 数据统计',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Row(
|
||||
children: stats.map((stat) {
|
||||
return Expanded(
|
||||
child: GlassContainer(
|
||||
depth: GlassDepth.base,
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xs / 2,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(stat.$1, style: const TextStyle(fontSize: 20)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
stat.$3,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ext.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
stat.$2,
|
||||
style: TextStyle(fontSize: 11, color: ext.textHint),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
62
lib/core/layout/right_panel_registry.dart
Normal file
62
lib/core/layout/right_panel_registry.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 右侧面板注册表
|
||||
/// 创建时间: 2026-05-29
|
||||
/// 更新时间: 2026-05-29
|
||||
/// 作用: 管理右侧面板的注册与构建,各Tab页面通过注册表提供面板内容
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
typedef RightPanelBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
Map<String, dynamic>? args,
|
||||
);
|
||||
|
||||
class RightPanelRegistry {
|
||||
RightPanelRegistry._();
|
||||
|
||||
static final Map<String, RightPanelBuilder> _builders = {};
|
||||
|
||||
/// 注册面板构建器
|
||||
static void register(String panelId, RightPanelBuilder builder) {
|
||||
_builders[panelId] = builder;
|
||||
}
|
||||
|
||||
/// 批量注册
|
||||
static void registerAll(Map<String, RightPanelBuilder> entries) {
|
||||
_builders.addAll(entries);
|
||||
}
|
||||
|
||||
/// 构建面板Widget
|
||||
static Widget build(String panelId, BuildContext context, {Map<String, dynamic>? args}) {
|
||||
final builder = _builders[panelId];
|
||||
if (builder == null) {
|
||||
return _buildPlaceholder(context, panelId);
|
||||
}
|
||||
return builder(context, args);
|
||||
}
|
||||
|
||||
/// 是否已注册
|
||||
static bool hasPanel(String panelId) => _builders.containsKey(panelId);
|
||||
|
||||
/// 获取所有已注册的panelId
|
||||
static List<String> get registeredIds => _builders.keys.toList();
|
||||
|
||||
/// 未注册面板的占位Widget
|
||||
static Widget _buildPlaceholder(BuildContext context, String panelId) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('🚧', style: TextStyle(fontSize: 48)),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'面板 "$panelId" 尚未注册',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
101
lib/core/layout/split_divider.dart
Normal file
101
lib/core/layout/split_divider.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 可拖拽分割线
|
||||
/// 创建时间: 2026-05-29
|
||||
/// 更新时间: 2026-05-29
|
||||
/// 作用: 宽屏分屏的分割线组件,支持拖拽调整比例、hover高亮、触觉反馈
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class SplitDivider extends StatefulWidget {
|
||||
const SplitDivider({
|
||||
required this.onPositionChanged,
|
||||
this.currentPosition = 0.4,
|
||||
this.minPosition = 0.2,
|
||||
this.maxPosition = 0.7,
|
||||
this.isVertical = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ValueChanged<double> onPositionChanged;
|
||||
final double currentPosition;
|
||||
final double minPosition;
|
||||
final double maxPosition;
|
||||
final bool isVertical;
|
||||
|
||||
@override
|
||||
State<SplitDivider> createState() => _SplitDividerState();
|
||||
}
|
||||
|
||||
class _SplitDividerState extends State<SplitDivider> {
|
||||
bool _isHovering = false;
|
||||
bool _isDragging = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
final dividerColor = ext.textHint.withValues(alpha: 0.15);
|
||||
final handleColor = _isDragging
|
||||
? ext.accent.withValues(alpha: 0.8)
|
||||
: _isHovering
|
||||
? ext.accent.withValues(alpha: 0.5)
|
||||
: ext.textHint.withValues(alpha: 0.3);
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovering = true),
|
||||
onExit: (_) => setState(() => _isHovering = false),
|
||||
cursor: SystemMouseCursors.resizeColumn,
|
||||
child: GestureDetector(
|
||||
onHorizontalDragStart: _onDragStart,
|
||||
onHorizontalDragUpdate: _onDragUpdate,
|
||||
onHorizontalDragEnd: _onDragEnd,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Container(
|
||||
width: 17,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
width: 1,
|
||||
color: dividerColor,
|
||||
child: Center(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
curve: Curves.easeOut,
|
||||
width: _isDragging ? 6 : _isHovering ? 4 : 4,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: handleColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDragStart(DragStartDetails details) {
|
||||
setState(() => _isDragging = true);
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
|
||||
void _onDragUpdate(DragUpdateDetails details) {
|
||||
final box = context.findRenderObject() as RenderBox;
|
||||
final parent = box.parent as RenderBox;
|
||||
final totalWidth = parent.size.width;
|
||||
if (totalWidth <= 0) return;
|
||||
|
||||
final localX = details.globalPosition.dx - parent.localToGlobal(Offset.zero).dx;
|
||||
final newPosition = (localX / totalWidth).clamp(widget.minPosition, widget.maxPosition);
|
||||
|
||||
widget.onPositionChanged(newPosition);
|
||||
}
|
||||
|
||||
void _onDragEnd(DragEndDetails details) {
|
||||
setState(() => _isDragging = false);
|
||||
}
|
||||
}
|
||||
193
lib/core/providers/split_view_provider.dart
Normal file
193
lib/core/providers/split_view_provider.dart
Normal file
@@ -0,0 +1,193 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 宽屏分屏状态管理
|
||||
/// 创建时间: 2026-05-29
|
||||
/// 更新时间: 2026-05-29
|
||||
/// 作用: 管理分屏比例、右侧面板内容、导航栏位置、分屏开关
|
||||
/// 上次更新: 适配Riverpod 3.0 Notifier模式
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:xianyan/core/storage/kv_storage.dart';
|
||||
|
||||
/// 导航栏停靠位置
|
||||
enum NavBarPosition {
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom;
|
||||
|
||||
String get label => switch (this) {
|
||||
left => '左侧',
|
||||
right => '右侧',
|
||||
top => '顶部',
|
||||
bottom => '底部',
|
||||
};
|
||||
|
||||
String get emoji => switch (this) {
|
||||
left => '⬅️',
|
||||
right => '➡️',
|
||||
top => '⬆️',
|
||||
bottom => '⬇️',
|
||||
};
|
||||
}
|
||||
|
||||
/// 分屏比例选项
|
||||
class SplitRatioOption {
|
||||
const SplitRatioOption(this.value, this.label);
|
||||
final double value;
|
||||
final String label;
|
||||
|
||||
static const List<SplitRatioOption> options = [
|
||||
SplitRatioOption(0.30, '30:70'),
|
||||
SplitRatioOption(0.35, '35:65'),
|
||||
SplitRatioOption(0.40, '40:60'),
|
||||
SplitRatioOption(0.45, '45:55'),
|
||||
SplitRatioOption(0.50, '50:50'),
|
||||
SplitRatioOption(0.55, '55:45'),
|
||||
SplitRatioOption(0.60, '60:40'),
|
||||
];
|
||||
}
|
||||
|
||||
const String _kSplitRatio = 'split_view_ratio';
|
||||
const String _kNavBarPosition = 'nav_bar_position';
|
||||
const String _kSplitViewEnabled = 'split_view_enabled';
|
||||
|
||||
/// 分屏视图状态
|
||||
class SplitViewState {
|
||||
const SplitViewState({
|
||||
this.splitRatio = 0.4,
|
||||
this.rightPanelContent,
|
||||
this.rightPanelArgs,
|
||||
this.navBarPosition = NavBarPosition.left,
|
||||
this.splitViewEnabled = true,
|
||||
this.homeRightPanel,
|
||||
this.discoverRightPanel,
|
||||
this.profileRightPanel,
|
||||
this.currentTab = 0,
|
||||
});
|
||||
|
||||
final double splitRatio;
|
||||
final String? rightPanelContent;
|
||||
final Map<String, dynamic>? rightPanelArgs;
|
||||
final NavBarPosition navBarPosition;
|
||||
final bool splitViewEnabled;
|
||||
final String? homeRightPanel;
|
||||
final String? discoverRightPanel;
|
||||
final String? profileRightPanel;
|
||||
final int currentTab;
|
||||
|
||||
/// 当前活跃的右侧面板(根据当前Tab自动选择)
|
||||
String? get activeRightPanel => switch (currentTab) {
|
||||
0 => homeRightPanel,
|
||||
1 => discoverRightPanel,
|
||||
2 => profileRightPanel,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
SplitViewState copyWith({
|
||||
double? splitRatio,
|
||||
String? rightPanelContent,
|
||||
Map<String, dynamic>? rightPanelArgs,
|
||||
NavBarPosition? navBarPosition,
|
||||
bool? splitViewEnabled,
|
||||
String? homeRightPanel,
|
||||
String? discoverRightPanel,
|
||||
String? profileRightPanel,
|
||||
int? currentTab,
|
||||
}) {
|
||||
return SplitViewState(
|
||||
splitRatio: splitRatio ?? this.splitRatio,
|
||||
rightPanelContent: rightPanelContent ?? this.rightPanelContent,
|
||||
rightPanelArgs: rightPanelArgs ?? this.rightPanelArgs,
|
||||
navBarPosition: navBarPosition ?? this.navBarPosition,
|
||||
splitViewEnabled: splitViewEnabled ?? this.splitViewEnabled,
|
||||
homeRightPanel: homeRightPanel ?? this.homeRightPanel,
|
||||
discoverRightPanel: discoverRightPanel ?? this.discoverRightPanel,
|
||||
profileRightPanel: profileRightPanel ?? this.profileRightPanel,
|
||||
currentTab: currentTab ?? this.currentTab,
|
||||
);
|
||||
}
|
||||
|
||||
SplitViewState copyWithClearPanel({
|
||||
String? homeRightPanel,
|
||||
String? discoverRightPanel,
|
||||
String? profileRightPanel,
|
||||
}) {
|
||||
return SplitViewState(
|
||||
splitRatio: splitRatio,
|
||||
rightPanelContent: rightPanelContent,
|
||||
rightPanelArgs: rightPanelArgs,
|
||||
navBarPosition: navBarPosition,
|
||||
splitViewEnabled: splitViewEnabled,
|
||||
homeRightPanel: homeRightPanel,
|
||||
discoverRightPanel: discoverRightPanel,
|
||||
profileRightPanel: profileRightPanel,
|
||||
currentTab: currentTab,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 分屏视图状态管理Notifier
|
||||
class SplitViewNotifier extends Notifier<SplitViewState> {
|
||||
@override
|
||||
SplitViewState build() {
|
||||
return SplitViewState(
|
||||
splitRatio: KvStorage.getDouble(_kSplitRatio) ?? 0.4,
|
||||
navBarPosition:
|
||||
NavBarPosition.values[KvStorage.getInt(_kNavBarPosition) ?? 0],
|
||||
splitViewEnabled: KvStorage.getBool(_kSplitViewEnabled) ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
void setSplitRatio(double value) {
|
||||
KvStorage.setDouble(_kSplitRatio, value);
|
||||
state = state.copyWith(splitRatio: value);
|
||||
}
|
||||
|
||||
void setRightPanelContent(String? content, {Map<String, dynamic>? args}) {
|
||||
state = state.copyWith(rightPanelContent: content, rightPanelArgs: args);
|
||||
}
|
||||
|
||||
void setNavBarPosition(NavBarPosition position) {
|
||||
KvStorage.setInt(_kNavBarPosition, position.index);
|
||||
state = state.copyWith(navBarPosition: position);
|
||||
}
|
||||
|
||||
void setSplitViewEnabled(bool enabled) {
|
||||
KvStorage.setBool(_kSplitViewEnabled, enabled);
|
||||
state = state.copyWith(splitViewEnabled: enabled);
|
||||
}
|
||||
|
||||
void setHomeRightPanel(String? panelId, {Map<String, dynamic>? args}) {
|
||||
state = state.copyWith(homeRightPanel: panelId, rightPanelArgs: args);
|
||||
}
|
||||
|
||||
void setDiscoverRightPanel(String? panelId, {Map<String, dynamic>? args}) {
|
||||
state = state.copyWith(discoverRightPanel: panelId, rightPanelArgs: args);
|
||||
}
|
||||
|
||||
void setProfileRightPanel(String? panelId, {Map<String, dynamic>? args}) {
|
||||
state = state.copyWith(profileRightPanel: panelId, rightPanelArgs: args);
|
||||
}
|
||||
|
||||
void setCurrentTab(int index) {
|
||||
state = state.copyWith(currentTab: index);
|
||||
}
|
||||
|
||||
void clearActivePanel() {
|
||||
final tab = state.currentTab;
|
||||
switch (tab) {
|
||||
case 0:
|
||||
state = state.copyWithClearPanel(homeRightPanel: null);
|
||||
case 1:
|
||||
state = state.copyWithClearPanel(discoverRightPanel: null);
|
||||
case 2:
|
||||
state = state.copyWithClearPanel(profileRightPanel: null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 分屏视图Provider
|
||||
final splitViewProvider = NotifierProvider<SplitViewNotifier, SplitViewState>(
|
||||
SplitViewNotifier.new,
|
||||
);
|
||||
@@ -6,6 +6,7 @@
|
||||
// 上次更新: 对调inspiration/discover路由,discover为Tab主页面
|
||||
// ============================================================
|
||||
|
||||
import 'package:bot_toast/bot_toast.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -42,7 +43,9 @@ final GoRouter appRouter = GoRouter(
|
||||
navigatorKey: rootNavigatorKey,
|
||||
initialLocation: _resolveInitialLocation(),
|
||||
debugLogDiagnostics: true,
|
||||
observers: pu.isOhos ? [_OhosRouteObserver()] : null,
|
||||
observers: pu.isOhos
|
||||
? [_OhosRouteObserver(), BotToastNavigatorObserver()]
|
||||
: [BotToastNavigatorObserver()],
|
||||
redirect: _handleDeepLinkRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
@@ -119,7 +122,7 @@ String? _handleDeepLinkRedirect(BuildContext context, GoRouterState state) {
|
||||
}
|
||||
|
||||
/// 统一深度链接URI解析入口
|
||||
/// 支持 xianyan:// scheme 和 https://xianyan.app 通用链接
|
||||
/// 支持 xianyan:// scheme 和 https://s2ss.com 通用链接
|
||||
/// 供 GoRouter redirect 和 DeepLinkService 共用
|
||||
class AppRouter {
|
||||
AppRouter._();
|
||||
@@ -135,7 +138,7 @@ class AppRouter {
|
||||
|
||||
if (scheme == 'https' || scheme == 'http') {
|
||||
final host = uri.host.toLowerCase();
|
||||
if (host == 'xianyan.app' || host == 'www.xianyan.app') {
|
||||
if (host == 's2ss.com' || host == 'www.s2ss.com') {
|
||||
return _resolveHttps(uri);
|
||||
}
|
||||
}
|
||||
@@ -183,8 +186,8 @@ class AppRouter {
|
||||
};
|
||||
}
|
||||
|
||||
/// 解析 https://xianyan.app 通用链接
|
||||
/// 格式: https://xianyan.app/<segment>[/<sub>]
|
||||
/// 解析 https://s2ss.com 通用链接
|
||||
/// 格式: https://s2ss.com/<segment>[/<sub>]
|
||||
static String? _resolveHttps(Uri uri) {
|
||||
final segments = uri.pathSegments;
|
||||
if (segments.isEmpty) return AppRoutes.home;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 用户/认证/个人中心路由
|
||||
// 创建时间: 2026-05-22
|
||||
// 更新时间: 2026-05-22
|
||||
// 更新时间: 2026-05-29
|
||||
// 作用: 登录、签到、设备、收藏、历史、会员、个人中心等 GoRoute 定义
|
||||
// 上次更新: 从 app_router.dart 拆分
|
||||
// 上次更新: badge-wall/coin-log路由指向AchievementCenterPage
|
||||
// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -28,13 +28,12 @@ import '../../features/mine/user_center/presentation/public_profile_page.dart';
|
||||
import '../../features/mine/user_center/presentation/tag_cloud_page.dart';
|
||||
import '../../features/tool_center/statistics/presentation/user_stats_page.dart';
|
||||
import '../../features/mine/user_center/presentation/user_debug_page.dart';
|
||||
import '../../features/mine/user_center/presentation/coin_log_page.dart';
|
||||
import '../../features/search/presentation/user_preference_page.dart';
|
||||
import '../../features/share/presentation/share_history_page.dart';
|
||||
import '../../features/share/presentation/share_target_edit_page.dart';
|
||||
import '../../features/mine/user_center/presentation/learning_center_page.dart';
|
||||
import '../../features/mine/achievement/presentation/achievement_page.dart';
|
||||
import '../../features/mine/achievement/presentation/badge_wall_page.dart';
|
||||
import '../../features/mine/achievement/presentation/achievement_center_page.dart';
|
||||
import '../../features/task/presentation/daily_task_page.dart';
|
||||
import '../../features/rank/presentation/rank_page.dart';
|
||||
import '../../features/source/presentation/source_page.dart';
|
||||
@@ -185,7 +184,7 @@ List<GoRoute> buildUserRoutes(GlobalKey<NavigatorState> rootNavigatorKey) => [
|
||||
name: 'coin-log',
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) =>
|
||||
iosSlideTransition(state: state, child: const CoinLogPage()),
|
||||
iosSlideTransition(state: state, child: const AchievementCenterPage(initialTab: 1)),
|
||||
),
|
||||
GoRoute(
|
||||
path: AppRoutes.userPreference,
|
||||
@@ -239,7 +238,7 @@ List<GoRoute> buildUserRoutes(GlobalKey<NavigatorState> rootNavigatorKey) => [
|
||||
name: 'badge-wall',
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) =>
|
||||
iosSlideTransition(state: state, child: const BadgeWallPage()),
|
||||
iosSlideTransition(state: state, child: const AchievementCenterPage()),
|
||||
),
|
||||
GoRoute(
|
||||
path: AppRoutes.dailyTask,
|
||||
|
||||
223
lib/core/services/device/calendar_service.dart
Normal file
223
lib/core/services/device/calendar_service.dart
Normal file
@@ -0,0 +1,223 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 日历同步服务
|
||||
/// 创建时间: 2026-05-29
|
||||
/// 更新时间: 2026-05-29
|
||||
/// 作用: 跨平台日历事件同步(Android/iOS/HarmonyOS/macOS/Windows)
|
||||
/// 上次更新: 修复analyze错误(unnecessary_import/prefer_final_locals/errorMessages→errors)
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_calendar/device_calendar.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
|
||||
import 'package:xianyan/core/utils/logger.dart';
|
||||
|
||||
/// 日历事件数据模型
|
||||
class CalendarEvent {
|
||||
final String title;
|
||||
final String? description;
|
||||
final DateTime start;
|
||||
final DateTime end;
|
||||
final int? reminderMinutesBefore;
|
||||
final String? location;
|
||||
|
||||
const CalendarEvent({
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.start,
|
||||
required this.end,
|
||||
this.reminderMinutesBefore,
|
||||
this.location,
|
||||
});
|
||||
}
|
||||
|
||||
/// 日历同步服务 — 单例
|
||||
/// 支持Android/iOS/macOS/Windows原生日历 + HarmonyOS MethodChannel桥接
|
||||
class CalendarService {
|
||||
CalendarService._();
|
||||
static final CalendarService instance = CalendarService._();
|
||||
|
||||
final DeviceCalendarPlugin _plugin = DeviceCalendarPlugin();
|
||||
String? _calendarId;
|
||||
bool _isAvailable = false;
|
||||
|
||||
/// 日历是否可用
|
||||
bool get isAvailable => _isAvailable;
|
||||
|
||||
/// HarmonyOS日历桥接通道
|
||||
static const _ohosChannel = MethodChannel('com.xianyan/calendar');
|
||||
|
||||
/// 当前平台是否支持日历
|
||||
bool get isPlatformSupported {
|
||||
if (kIsWeb) return false;
|
||||
try {
|
||||
if (Platform.isAndroid || Platform.isIOS || Platform.isMacOS) return true;
|
||||
if (Platform.isWindows) return true;
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 权限请求
|
||||
// ============================================================
|
||||
|
||||
/// 请求日历权限,成功后自动创建/获取日历
|
||||
Future<bool> requestPermission() async {
|
||||
try {
|
||||
if (_isOhos()) {
|
||||
return await _requestPermissionOhos();
|
||||
}
|
||||
if (!isPlatformSupported) {
|
||||
Log.w('CalendarService: 当前平台不支持日历');
|
||||
return false;
|
||||
}
|
||||
final hasPermissions = await _plugin.hasPermissions();
|
||||
if (hasPermissions.isSuccess && !hasPermissions.data!) {
|
||||
final result = await _plugin.requestPermissions();
|
||||
if (!result.isSuccess || !result.data!) {
|
||||
Log.w('CalendarService: 日历权限被拒绝');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_isAvailable = true;
|
||||
await _ensureCalendar();
|
||||
return true;
|
||||
} catch (e) {
|
||||
Log.e('CalendarService: 请求日历权限失败', e);
|
||||
_isAvailable = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// HarmonyOS权限请求
|
||||
Future<bool> _requestPermissionOhos() async {
|
||||
try {
|
||||
final result = await _ohosChannel.invokeMethod<bool>('requestPermission');
|
||||
_isAvailable = result ?? false;
|
||||
return _isAvailable;
|
||||
} catch (e) {
|
||||
Log.e('CalendarService: OHOS日历权限请求失败', e);
|
||||
_isAvailable = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 日历管理
|
||||
// ============================================================
|
||||
|
||||
/// 确保闲言专属日历存在,不存在则创建
|
||||
Future<void> _ensureCalendar() async {
|
||||
final calendars = await _plugin.retrieveCalendars();
|
||||
if (calendars.isSuccess && calendars.data != null) {
|
||||
final existing = calendars.data!.where(
|
||||
(c) => c.name == '闲言' || c.name == 'Xianyan',
|
||||
);
|
||||
if (existing.isNotEmpty) {
|
||||
_calendarId = existing.first.id;
|
||||
} else {
|
||||
final result = await _plugin.createCalendar(
|
||||
'闲言',
|
||||
calendarColor: const Color(0xFF007AFF),
|
||||
localAccountName: '闲言APP',
|
||||
);
|
||||
if (result.isSuccess) {
|
||||
_calendarId = result.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 事件操作
|
||||
// ============================================================
|
||||
|
||||
/// 添加日历事件
|
||||
Future<bool> addEvent(CalendarEvent event) async {
|
||||
if (_isOhos()) {
|
||||
return _addEventOhos(event);
|
||||
}
|
||||
|
||||
if (!_isAvailable || _calendarId == null) {
|
||||
final granted = await requestPermission();
|
||||
if (!granted) return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final local = tz.local;
|
||||
final calendarEvent = Event(
|
||||
_calendarId,
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
start: tz.TZDateTime.from(event.start, local),
|
||||
end: tz.TZDateTime.from(event.end, local),
|
||||
location: event.location,
|
||||
);
|
||||
|
||||
if (event.reminderMinutesBefore != null) {
|
||||
calendarEvent.reminders = [
|
||||
Reminder(minutes: event.reminderMinutesBefore!),
|
||||
];
|
||||
}
|
||||
|
||||
final result = await _plugin.createOrUpdateEvent(calendarEvent);
|
||||
if (result?.isSuccess == true) {
|
||||
Log.i('CalendarService: 事件已添加 - ${event.title}');
|
||||
return true;
|
||||
}
|
||||
Log.e(
|
||||
'CalendarService: 添加事件失败 - ${result?.errors.map((e) => e.errorMessage)}',
|
||||
);
|
||||
return false;
|
||||
} catch (e) {
|
||||
Log.e('CalendarService: 添加事件异常', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// HarmonyOS添加事件
|
||||
Future<bool> _addEventOhos(CalendarEvent event) async {
|
||||
try {
|
||||
final result = await _ohosChannel.invokeMethod<bool>('addEvent', {
|
||||
'title': event.title,
|
||||
'description': event.description,
|
||||
'startTime': event.start.millisecondsSinceEpoch,
|
||||
'endTime': event.end.millisecondsSinceEpoch,
|
||||
'reminderMinutes': event.reminderMinutesBefore,
|
||||
'location': event.location,
|
||||
});
|
||||
return result ?? false;
|
||||
} catch (e) {
|
||||
Log.e('CalendarService: OHOS日历桥接失败', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除日历事件
|
||||
Future<bool> deleteEvent(String eventId) async {
|
||||
if (_calendarId == null) return false;
|
||||
try {
|
||||
final result = await _plugin.deleteEvent(_calendarId!, eventId);
|
||||
return result.isSuccess;
|
||||
} catch (e) {
|
||||
Log.e('CalendarService: 删除事件异常', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 平台检测
|
||||
// ============================================================
|
||||
|
||||
/// 是否为HarmonyOS平台
|
||||
bool _isOhos() {
|
||||
try {
|
||||
return Platform.operatingSystem == 'ohos';
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 触觉反馈服务
|
||||
/// 创建时间: 2026-05-07
|
||||
/// 更新时间: 2026-05-19
|
||||
/// 更新时间: 2026-05-29
|
||||
/// 作用: 统一管理触觉反馈,4档位控制(关闭/轻柔/标准/强烈)
|
||||
/// 上次更新: 新增error()方法用于传输失败等错误场景反馈
|
||||
/// 优先使用flutter_vibrate,不可用时降级HapticFeedback
|
||||
/// 上次更新: 集成flutter_vibrate库,增加init()初始化+降级策略
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_vibrate/flutter_vibrate.dart';
|
||||
import 'package:xianyan/core/utils/logger.dart';
|
||||
|
||||
/// 震动档位枚举
|
||||
enum VibrationLevel {
|
||||
@@ -31,11 +34,14 @@ enum VibrationLevel {
|
||||
/// 触觉反馈服务 — 全局单例
|
||||
///
|
||||
/// 根据用户设置的震动档位,自动决定是否执行触觉反馈及强度。
|
||||
/// 优先使用flutter_vibrate(支持更多反馈类型),不可用时降级HapticFeedback。
|
||||
/// 所有需要触觉反馈的地方统一调用此服务。
|
||||
class HapticService {
|
||||
HapticService._();
|
||||
|
||||
static VibrationLevel _level = VibrationLevel.medium;
|
||||
static bool _canVibrate = false;
|
||||
static bool _initialized = false;
|
||||
|
||||
static VibrationLevel get level => _level;
|
||||
|
||||
@@ -43,42 +49,57 @@ class HapticService {
|
||||
_level = level;
|
||||
}
|
||||
|
||||
/// 初始化 — 检测设备是否支持flutter_vibrate
|
||||
static Future<void> init() async {
|
||||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
try {
|
||||
_canVibrate = await Vibrate.canVibrate;
|
||||
Log.i(
|
||||
'HapticService: 初始化完成 (canVibrate=$_canVibrate, level=${_level.label})',
|
||||
);
|
||||
} catch (e) {
|
||||
Log.w('HapticService: flutter_vibrate不可用,使用HapticFeedback降级');
|
||||
_canVibrate = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 通用冲击反馈 — 根据档位自动选择强度
|
||||
static void impact() {
|
||||
switch (_level) {
|
||||
case VibrationLevel.off:
|
||||
return;
|
||||
case VibrationLevel.light:
|
||||
HapticFeedback.lightImpact();
|
||||
_doLight();
|
||||
case VibrationLevel.medium:
|
||||
HapticFeedback.mediumImpact();
|
||||
_doMedium();
|
||||
case VibrationLevel.heavy:
|
||||
HapticFeedback.heavyImpact();
|
||||
_doHeavy();
|
||||
}
|
||||
}
|
||||
|
||||
/// 选择反馈 — 切换Tab/滑动选择等
|
||||
static void selection() {
|
||||
if (_level == VibrationLevel.off) return;
|
||||
HapticFeedback.selectionClick();
|
||||
_doSelection();
|
||||
}
|
||||
|
||||
/// 轻柔反馈 — 仅轻柔/标准/强烈时触发
|
||||
static void light() {
|
||||
if (_level == VibrationLevel.off) return;
|
||||
HapticFeedback.lightImpact();
|
||||
_doLight();
|
||||
}
|
||||
|
||||
/// 中等反馈 — 仅标准/强烈时触发
|
||||
static void medium() {
|
||||
if (_level.index < VibrationLevel.medium.index) return;
|
||||
HapticFeedback.mediumImpact();
|
||||
_doMedium();
|
||||
}
|
||||
|
||||
/// 强烈反馈 — 仅强烈时触发
|
||||
static void heavy() {
|
||||
if (_level.index < VibrationLevel.heavy.index) return;
|
||||
HapticFeedback.heavyImpact();
|
||||
_doHeavy();
|
||||
}
|
||||
|
||||
/// 按钮点击反馈
|
||||
@@ -93,7 +114,8 @@ class HapticService {
|
||||
|
||||
/// 成功操作反馈
|
||||
static void success() {
|
||||
medium();
|
||||
if (_level == VibrationLevel.off) return;
|
||||
_doSuccess();
|
||||
}
|
||||
|
||||
/// 危险操作反馈
|
||||
@@ -103,6 +125,81 @@ class HapticService {
|
||||
|
||||
/// 错误反馈 — 传输失败等场景
|
||||
static void error() {
|
||||
medium();
|
||||
if (_level == VibrationLevel.off) return;
|
||||
_doError();
|
||||
}
|
||||
|
||||
// ---- 内部实现: flutter_vibrate优先,HapticFeedback降级 ----
|
||||
|
||||
static void _doLight() {
|
||||
try {
|
||||
if (_canVibrate) {
|
||||
Vibrate.feedback(FeedbackType.light);
|
||||
} else {
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
} catch (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
}
|
||||
|
||||
static void _doMedium() {
|
||||
try {
|
||||
if (_canVibrate) {
|
||||
Vibrate.feedback(FeedbackType.medium);
|
||||
} else {
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
} catch (_) {
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
}
|
||||
|
||||
static void _doHeavy() {
|
||||
try {
|
||||
if (_canVibrate) {
|
||||
Vibrate.feedback(FeedbackType.heavy);
|
||||
} else {
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
} catch (_) {
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
}
|
||||
|
||||
static void _doSelection() {
|
||||
try {
|
||||
if (_canVibrate) {
|
||||
Vibrate.feedback(FeedbackType.selection);
|
||||
} else {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
} catch (_) {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
}
|
||||
|
||||
static void _doSuccess() {
|
||||
try {
|
||||
if (_canVibrate) {
|
||||
Vibrate.feedback(FeedbackType.success);
|
||||
} else {
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
} catch (_) {
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
}
|
||||
|
||||
static void _doError() {
|
||||
try {
|
||||
if (_canVibrate) {
|
||||
Vibrate.feedback(FeedbackType.error);
|
||||
} else {
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
} catch (_) {
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 摇一摇检测器
|
||||
// 创建时间: 2026-05-20
|
||||
// 更新时间: 2026-05-23
|
||||
// 更新时间: 2026-05-29
|
||||
// 作用: 监听加速度传感器,检测摇一摇手势
|
||||
// 上次更新: 提高阈值+连续采样防误触,增加页面感知,非首页禁止触发
|
||||
// 上次更新: 改用处理器栈模式,仅当前可见页面的回调生效
|
||||
//
|
||||
// 摇一摇生命周期管理(处理器栈模式):
|
||||
//
|
||||
// ⚠️ StatefulShellRoute.indexedStack 不会调用子页面的 dispose()
|
||||
// 因此不能依赖 dispose() 来管理摇一摇状态
|
||||
//
|
||||
// 正确用法:
|
||||
// - 页面 initState 时: ShakeDetector.instance.pushHandler('/route', callback)
|
||||
// - 页面 deactivate 时: ShakeDetector.instance.popHandler('/route')
|
||||
// - 仅栈顶 handler 生效,确保只有当前可见页面响应摇一摇
|
||||
//
|
||||
// 错误用法(已废弃):
|
||||
// - setHomePageActive(true/false) — 在 IndexedStack 中永远为 true
|
||||
// - 在 dispose 中 stop() — 永远不会被调用
|
||||
// ============================================================
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:sensors_plus/sensors_plus.dart';
|
||||
import 'package:xianyan/core/utils/logger.dart';
|
||||
import 'package:xianyan/core/services/device/haptic_service.dart';
|
||||
|
||||
typedef VoidCallback = void Function();
|
||||
typedef ShakeCallback = void Function();
|
||||
|
||||
class _ShakeHandlerEntry {
|
||||
final String route;
|
||||
final ShakeCallback callback;
|
||||
_ShakeHandlerEntry(this.route, this.callback);
|
||||
}
|
||||
|
||||
class ShakeDetector {
|
||||
ShakeDetector._();
|
||||
@@ -24,23 +45,29 @@ class ShakeDetector {
|
||||
DateTime? _lastShakeTime;
|
||||
bool _isEnabled = false;
|
||||
int _consecutiveCount = 0;
|
||||
bool _isHomePageActive = false;
|
||||
|
||||
VoidCallback? onShake;
|
||||
final List<_ShakeHandlerEntry> _handlerStack = [];
|
||||
|
||||
bool get isEnabled => _isEnabled;
|
||||
bool get hasActiveHandler => _handlerStack.isNotEmpty;
|
||||
|
||||
void setHomePageActive(bool active) {
|
||||
_isHomePageActive = active;
|
||||
if (!active) {
|
||||
_consecutiveCount = 0;
|
||||
}
|
||||
void pushHandler(String route, ShakeCallback callback) {
|
||||
_handlerStack.removeWhere((e) => e.route == route);
|
||||
_handlerStack.add(_ShakeHandlerEntry(route, callback));
|
||||
Log.i(
|
||||
'ShakeDetector: pushHandler route=$route, stack=${_handlerStack.map((e) => e.route).toList()}',
|
||||
);
|
||||
}
|
||||
|
||||
void start({VoidCallback? onShakeCallback}) {
|
||||
if (onShakeCallback != null) {
|
||||
onShake = onShakeCallback;
|
||||
}
|
||||
void popHandler(String route) {
|
||||
_handlerStack.removeWhere((e) => e.route == route);
|
||||
_consecutiveCount = 0;
|
||||
Log.i(
|
||||
'ShakeDetector: popHandler route=$route, stack=${_handlerStack.map((e) => e.route).toList()}',
|
||||
);
|
||||
}
|
||||
|
||||
void start({ShakeCallback? onShakeCallback}) {
|
||||
if (_isEnabled) return;
|
||||
|
||||
_subscription?.cancel();
|
||||
@@ -51,7 +78,7 @@ class ShakeDetector {
|
||||
try {
|
||||
_subscription = userAccelerometerEventStream().listen(
|
||||
(event) {
|
||||
if (!_isEnabled || !_isHomePageActive) return;
|
||||
if (!_isEnabled || _handlerStack.isEmpty) return;
|
||||
|
||||
final acceleration =
|
||||
(event.x * event.x) + (event.y * event.y) + (event.z * event.z);
|
||||
@@ -64,7 +91,8 @@ class ShakeDetector {
|
||||
_lastShakeTime = now;
|
||||
_consecutiveCount = 0;
|
||||
Log.i('ShakeDetector: 检测到摇一摇 (acceleration=$acceleration)');
|
||||
onShake?.call();
|
||||
_handlerStack.last.callback.call();
|
||||
HapticService.medium();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -92,7 +120,6 @@ class ShakeDetector {
|
||||
|
||||
void dispose() {
|
||||
stop();
|
||||
onShake = null;
|
||||
_isHomePageActive = false;
|
||||
_handlerStack.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ mixin AppLifecycleGate on WidgetsBindingObserver {
|
||||
|
||||
try {
|
||||
if (ShakeDetector.instance.isEnabled &&
|
||||
ShakeDetector.instance.onShake != null) {
|
||||
ShakeDetector.instance.hasActiveHandler) {
|
||||
ShakeDetector.instance.start();
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
/// 颜色令牌 — 日间模式
|
||||
class LightColors {
|
||||
@@ -136,3 +136,47 @@ class DarkColors {
|
||||
// ---- 波纹色 ----
|
||||
static const Color ripple = Color(0xCB666666);
|
||||
}
|
||||
|
||||
/// 应用统一颜色常量
|
||||
///
|
||||
/// iOS 系统色、运势色、文档类型色、工具色等,
|
||||
/// 不随主题变化的固定色值。
|
||||
class AppColors {
|
||||
AppColors._();
|
||||
|
||||
// ---- iOS 系统色 ----
|
||||
static const Color iosBlue = Color(0xFF007AFF);
|
||||
static const Color iosGreen = Color(0xFF34C759);
|
||||
static const Color iosIndigo = Color(0xFF5856D6);
|
||||
static const Color iosOrange = Color(0xFFFF9500);
|
||||
static const Color iosPink = Color(0xFFFF2D55);
|
||||
static const Color iosPurple = Color(0xFF5856D6);
|
||||
static const Color iosLightPurple = Color(0xFFAF52DE);
|
||||
static const Color iosRed = Color(0xFFFF3B30);
|
||||
static const Color iosTeal = Color(0xFF5AC8FA);
|
||||
static const Color iosYellow = Color(0xFFFFCC00);
|
||||
static const Color iosGray = Color(0xFF8E8E93);
|
||||
static const Color iosLightBlue = Color(0xFF5AC8FA);
|
||||
static const Color iosLightGreen = Color(0xFF30D158);
|
||||
static const Color iosSkyBlue = Color(0xFF64D2FF);
|
||||
|
||||
// ---- 运势色 ----
|
||||
static const Color fortuneGreat = Color(0xFFD4A843);
|
||||
static const Color fortuneGood = Color(0xFF66BB6A);
|
||||
static const Color fortuneSlight = Color(0xFFAB47BC);
|
||||
static const Color fortuneFair = Color(0xFF42A5F5);
|
||||
static const Color fortuneMinor = Color(0xFFFFA726);
|
||||
static const Color fortuneBad = Color(0xFF5F27CD);
|
||||
static const Color fortuneTerrible = Color(0xFFE53935);
|
||||
|
||||
// ---- 文档类型色 ----
|
||||
static const Color docPdf = Color(0xFFFF3B30);
|
||||
static const Color docWord = Color(0xFF007AFF);
|
||||
static const Color docExcel = Color(0xFF34C759);
|
||||
static const Color docPpt = Color(0xFFFF9500);
|
||||
static const Color docArchive = Color(0xFFAF52DE);
|
||||
static const Color docTxt = Color(0xFF8E8E93);
|
||||
|
||||
// ---- 工具色 ----
|
||||
static const Color toolPurple = Color(0xFF6C63FF);
|
||||
}
|
||||
|
||||
@@ -142,6 +142,12 @@ class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
|
||||
// ---- 圆角风格 ----
|
||||
final String cornerRadiusId;
|
||||
|
||||
// ---- 语义颜色 getter ----
|
||||
Color get textOnCard => isDark ? CupertinoColors.white : const Color(0xFF1C1C1E);
|
||||
Color get overlayLight => CupertinoColors.white.withValues(alpha: 0.8);
|
||||
Color get overlayDark => CupertinoColors.black.withValues(alpha: 0.6);
|
||||
Color get dividerOnCard => textHint.withValues(alpha: 0.08);
|
||||
|
||||
@override
|
||||
AppThemeExtension copyWith({
|
||||
Color? bgPrimary,
|
||||
|
||||
Reference in New Issue
Block a user