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:
Developer
2026-05-29 10:06:55 +08:00
parent 5b9a0320d5
commit 5a49d20c8a
93 changed files with 17861 additions and 3340 deletions

View 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();
}
}

View 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,
),
),
),
),
],
);
}
}

View File

@@ -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;

View 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(),
),
],
);
}
}

View 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),
),
],
),
);
}
}

View 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);
}
}

View 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,
);

View File

@@ -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;

View File

@@ -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,

View 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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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,