feat: 新增工作台模式、系统托盘,修复多平台兼容性问题

1. 新增工作台三栏布局模式,适配宽屏设备
2. 添加跨平台系统托盘支持,新增托盘图标资源
3. 修复工作台模式下导航返回异常问题
4. 统一JSON类型安全解析,替换硬类型转换
5. 增加macOS深度链接支持,统一渠道分发信息
6. 优化部分页面生命周期和状态加载逻辑
7. 移除废弃的nearby_connections依赖
This commit is contained in:
Developer
2026-06-19 06:43:55 +08:00
parent 6a02a313b2
commit 83720002e6
194 changed files with 11716 additions and 3120 deletions

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 权限管理服务
/// 创建时间: 2026-04-23
/// 更新时间: 2026-06-06
/// 更新时间: 2026-06-19
/// 作用: 统一管理应用权限请求,支持相机/相册/通知/位置/蓝牙/附近设备/麦克风/存储/网络/剪贴板/分享权限 + iOS ATT授权
/// 上次更新: 鸿蒙端permission_handler不支持时引导用户去系统设置+MissingPluginException捕获
/// 上次更新: 类型安全修复(int vs num): 权限使用计数使用 SafeJson.parseInt
/// ============================================================
import 'dart:async';
@@ -13,6 +13,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import 'package:xianyan/core/utils/safe_json.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../storage/kv_storage.dart';
@@ -296,7 +297,7 @@ class PermissionService {
final key = permission.name;
final existing = stats[key];
if (existing != null) {
existing['count'] = (existing['count'] as int) + 1;
existing['count'] = SafeJson.parseInt(existing['count']) + 1;
existing['lastUsed'] = DateTime.now().toIso8601String();
} else {
stats[key] = {

View File

@@ -45,6 +45,12 @@ class BackgroundTaskService {
return;
}
if (pu.isMacOS) {
Log.i('BackgroundTaskService: macOS端 workmanager 不支持,跳过后台任务初始化');
_initialized = true;
return;
}
try {
await Workmanager().initialize(
callbackDispatcher,

View File

@@ -32,7 +32,8 @@ class Catcher2ConfigService {
}
/// 初始化 Catcher2使用 rootWidget 而非 runAppFunction避免 Zone mismatch
void init({required Widget rootWidget}) {
/// [screenshotsPath] 截图保存路径,非空时启用截图;为空时 Catcher2 会输出 WARNING
void init({required Widget rootWidget, String screenshotsPath = ''}) {
final enabled = isEnabled;
final debugConfig = enabled
@@ -42,12 +43,17 @@ class Catcher2ConfigService {
localizationOptions: [
LocalizationOptions.buildDefaultChineseOptions(),
],
screenshotsPath: screenshotsPath,
)
: Catcher2Options(SilentReportMode(), []);
: Catcher2Options(SilentReportMode(), [], screenshotsPath: screenshotsPath);
final releaseConfig = enabled
? Catcher2Options(SilentReportMode(), [_ConsoleLogHandler()])
: Catcher2Options(SilentReportMode(), []);
? Catcher2Options(
SilentReportMode(),
[_ConsoleLogHandler()],
screenshotsPath: screenshotsPath,
)
: Catcher2Options(SilentReportMode(), [], screenshotsPath: screenshotsPath);
// 使用 rootWidget 而非 runAppFunctionCatcher2 会在内部调用 runApp
// 但不会创建新的 Zone避免 Zone mismatch 警告

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 图片缓存元数据服务
/// 创建时间: 2026-05-30
/// 更新时间: 2026-06-05
/// 更新时间: 2026-06-19
/// 作用: 基于Hive的图片缓存元数据索引支持按类型/日期分组、过期清理
/// 上次更新: 修复Web平台兼容性添加kIsWeb守卫保护文件系统操作
/// 上次更新: 类型安全修复(int vs num): CacheMetaEntry.fromJson 使用 SafeJson.parseInt
/// ============================================================
import 'dart:io';
@@ -11,6 +11,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:hive_ce_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:xianyan/core/utils/safe_json.dart';
import '../../storage/kv_storage.dart';
import '../../utils/logger.dart';
@@ -49,7 +50,7 @@ class CacheMetaEntry {
factory CacheMetaEntry.fromJson(Map<String, dynamic> json) {
return CacheMetaEntry(
path: json['path'] as String,
size: json['size'] as int,
size: SafeJson.parseInt(json['size']),
category: json['category'] as String?,
sourceUrl: json['sourceUrl'] as String?,
createdAt: DateTime.parse(json['createdAt'] as String),

View File

@@ -0,0 +1,118 @@
/// ============================================================
/// 闲言APP — 每日拾句已查看服务
/// 创建时间: 2026-06-18
/// 更新时间: 2026-06-18
/// 作用: 本地存储已查看的每日拾句 id 列表,用于计算未读数
/// 上次更新: 初始创建,实现 DailySentenceViewedService
/// ============================================================
import '../../storage/kv_storage.dart';
import '../../utils/logger.dart';
/// 每日拾句已查看服务
///
/// 使用 KvStorage 持久化已查看的每日拾句 id 列表。
/// 用于计算每日拾句的未读数(托盘角标)。
///
/// 存储格式JSON 编码的 List<String>key 为 `viewed_daily_sentence_ids`。
/// 保留最近 100 条记录,避免无限增长。
class DailySentenceViewedService {
DailySentenceViewedService._();
/// 存储 key
static const String _storageKey = 'viewed_daily_sentence_ids';
/// 最大保留记录数
static const int _maxRecords = 100;
/// 内存缓存(避免频繁读取 KvStorage
static Set<String>? _cache;
/// 获取已查看的每日拾句 id 集合
static Set<String> getViewedIds() {
if (_cache != null) return _cache!;
try {
final list = KvStorage.getStringList(_storageKey) ?? <String>[];
_cache = list.toSet();
return _cache!;
} catch (e) {
Log.w('DailySentenceViewedService.getViewedIds 失败: $e');
_cache = <String>{};
return _cache!;
}
}
/// 标记每日拾句为已查看
///
/// [id] 每日拾句 id格式如 `hitokoto_123` 或 `chengyu_456`
static Future<void> markViewed(String id) async {
if (id.isEmpty) return;
final viewed = getViewedIds();
if (viewed.contains(id)) return; // 已存在,无需重复写入
viewed.add(id);
// 限制记录数量:保留最新的 _maxRecords 条
if (viewed.length > _maxRecords) {
final list = viewed.toList()..sort();
final toRemove = list.take(viewed.length - _maxRecords).toSet();
viewed.removeAll(toRemove);
}
_cache = viewed;
try {
await KvStorage.setStringList(_storageKey, viewed.toList());
} catch (e) {
Log.e('DailySentenceViewedService.markViewed 写入失败: $e');
}
}
/// 批量标记已查看
static Future<void> markViewedBatch(Iterable<String> ids) async {
final viewed = getViewedIds();
var changed = false;
for (final id in ids) {
if (id.isNotEmpty && !viewed.contains(id)) {
viewed.add(id);
changed = true;
}
}
if (!changed) return;
// 限制记录数量
if (viewed.length > _maxRecords) {
final list = viewed.toList()..sort();
final toRemove = list.take(viewed.length - _maxRecords).toSet();
viewed.removeAll(toRemove);
}
_cache = viewed;
try {
await KvStorage.setStringList(_storageKey, viewed.toList());
} catch (e) {
Log.e('DailySentenceViewedService.markViewedBatch 写入失败: $e');
}
}
/// 检查指定 id 是否已查看
static bool isViewed(String id) {
if (id.isEmpty) return true; // 空 id 视为已查看
return getViewedIds().contains(id);
}
/// 清除所有已查看记录(用于调试/重置)
static Future<void> clear() async {
_cache = <String>{};
try {
await KvStorage.remove(_storageKey);
} catch (e) {
Log.e('DailySentenceViewedService.clear 失败: $e');
}
}
/// 重置内存缓存(用于测试)
static void resetCache() {
_cache = null;
}
}

View File

@@ -0,0 +1,63 @@
/// ============================================================
/// 闲言APP — 桌面端服务注册表
/// 创建时间: 2026-06-18
/// 更新时间: 2026-06-18
/// 作用: 按平台注入桌面端服务实现(托盘/窗口特效)
/// 上次更新: 初始创建,提供 init() 方法在 main.dart 中调用
/// ============================================================
import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu;
import 'package:xianyan/core/utils/logger.dart';
import 'desktop_tray_service.dart';
import 'desktop_window_effect_service.dart';
import 'implementations/tray_manager_tray_service.dart';
import 'implementations/macos_window_effect_service.dart';
import 'implementations/windows_acrylic_service.dart';
/// 桌面端服务注册表
///
/// 在 `main.dart` 中调用 `DesktopServiceRegistry.init()` 完成服务注入。
/// 根据 `pu.isDesktop` / `pu.isMacOS` / `pu.isWindows` 自动选择实现。
class DesktopServiceRegistry {
DesktopServiceRegistry._();
static bool _initialized = false;
/// 初始化桌面端服务注册表
///
/// 在 `main.dart` 的 `main()` 函数中,桌面端窗口初始化之前调用。
static void init() {
if (_initialized) return;
_initialized = true;
// 1. 注入托盘服务
if (pu.isDesktop) {
DesktopTrayService.instance = TrayManagerTrayService();
Log.i('DesktopServiceRegistry: 注入 TrayManagerTrayService');
} else {
// 移动端/鸿蒙端使用默认 StubDesktopTrayService
Log.i('DesktopServiceRegistry: 使用 StubDesktopTrayService');
}
// 2. 注入窗口特效服务
if (pu.isMacOS) {
DesktopWindowEffectService.instance = MacosWindowEffectService();
Log.i('DesktopServiceRegistry: 注入 MacosWindowEffectService');
} else if (pu.isWindows) {
DesktopWindowEffectService.instance = WindowsAcrylicService();
Log.i('DesktopServiceRegistry: 注入 WindowsAcrylicService');
} else {
// Linux/iOS/Android/鸿蒙端使用默认 StubWindowEffectService
Log.i('DesktopServiceRegistry: 使用 StubWindowEffectService');
}
}
/// 是否已初始化
static bool get isInitialized => _initialized;
/// 重置(用于测试)
static void reset() {
_initialized = false;
}
}

View File

@@ -0,0 +1,284 @@
/// ============================================================
/// 闲言APP — 桌面端托盘菜单构建器
/// 创建时间: 2026-06-18
/// 更新时间: 2026-06-18
/// 作用: 构建 4 组分隔线分组的托盘右键菜单(主操作/快速访问/模式切换/系统)
/// 上次更新: 初始创建,实现 TrayMenuCallbacks + TrayMenuLabels + DesktopTrayMenuBuilder
/// ============================================================
import 'package:flutter/foundation.dart';
import '../../../features/home/presentation/providers/readlater/readlater_entry.dart';
import '../desktop/desktop_tray_service.dart';
/// 托盘菜单项回调集合
///
/// 所有回调均为无参无返回值([VoidCallback]
/// 打开稍后阅读条目除外(接收 entryId
class TrayMenuCallbacks {
/// 新建笔记
final VoidCallback onNewNote;
/// 新建灵感
final VoidCallback onNewInspiration;
/// 打开稍后阅读页面
final VoidCallback onOpenReadLater;
/// 切换工作台模式
final VoidCallback onToggleWorkbench;
/// 切换深色/浅色模式
final VoidCallback onToggleDarkMode;
/// 显示主窗口(从托盘恢复)
final VoidCallback onShowMainWindow;
/// 打开偏好设置
final VoidCallback onOpenSettings;
/// 退出应用
final VoidCallback onExit;
/// 打开指定稍后阅读条目
final void Function(String entryId) onOpenReadLaterEntry;
const TrayMenuCallbacks({
required this.onNewNote,
required this.onNewInspiration,
required this.onOpenReadLater,
required this.onToggleWorkbench,
required this.onToggleDarkMode,
required this.onShowMainWindow,
required this.onOpenSettings,
required this.onExit,
required this.onOpenReadLaterEntry,
});
}
/// 托盘菜单标签(支持 i18n
///
/// 默认提供中文标签,可通过 [TrayMenuLabels.fromTranslations] 从翻译表生成。
/// 托盘菜单由原生渲染,无法直接使用 Flutter i18n需提前获取文本。
class TrayMenuLabels {
// 第 1 组:主操作
final String newNote;
final String newInspiration;
final String openReadLater;
// 第 2 组:快速访问
final String recentRead;
final String noRecentRead;
// 第 3 组:模式切换
final String workbenchMode;
final String darkMode;
// 第 4 组:系统
final String showMainWindow;
final String preferences;
final String exitApp;
// Tooltip
final String tooltip;
final String tooltipWithUnread;
const TrayMenuLabels({
required this.newNote,
required this.newInspiration,
required this.openReadLater,
required this.recentRead,
required this.noRecentRead,
required this.workbenchMode,
required this.darkMode,
required this.showMainWindow,
required this.preferences,
required this.exitApp,
required this.tooltip,
required this.tooltipWithUnread,
});
/// 默认中文标签
static const TrayMenuLabels zhCN = TrayMenuLabels(
newNote: '新建笔记',
newInspiration: '新建灵感',
openReadLater: '打开稍后阅读',
recentRead: '最近阅读',
noRecentRead: '暂无最近阅读',
workbenchMode: '工作台模式',
darkMode: '深色模式',
showMainWindow: '显示主窗口',
preferences: '偏好设置',
exitApp: '退出闲言',
tooltip: '闲言',
tooltipWithUnread: '闲言 — {count} 条未读',
);
/// 英文标签
static const TrayMenuLabels enUS = TrayMenuLabels(
newNote: 'New Note',
newInspiration: 'New Inspiration',
openReadLater: 'Open Read Later',
recentRead: 'Recent',
noRecentRead: 'No recent items',
workbenchMode: 'Workbench Mode',
darkMode: 'Dark Mode',
showMainWindow: 'Show Main Window',
preferences: 'Preferences',
exitApp: 'Quit Xianyan',
tooltip: 'Xianyan',
tooltipWithUnread: 'Xianyan — {count} unread',
);
/// 根据语言 ID 获取标签
factory TrayMenuLabels.forLanguage(String languageId) {
switch (languageId) {
case 'en':
return enUS;
case 'zh_CN':
case 'zh_TW':
case 'system':
default:
return zhCN;
}
}
/// 格式化带未读数的 Tooltip
String formatTooltip(int unreadCount) {
if (unreadCount <= 0) return tooltip;
return tooltipWithUnread.replaceAll('{count}', unreadCount.toString());
}
}
/// 桌面端托盘菜单构建器
///
/// 构建 4 组分隔线分组的托盘右键菜单:
/// 1. 主操作:新建笔记/灵感、打开稍后阅读
/// 2. 快速访问:最近阅读子菜单(前 5 条)
/// 3. 模式切换:工作台模式、深色模式
/// 4. 系统:显示主窗口、偏好设置、退出
class DesktopTrayMenuBuilder {
DesktopTrayMenuBuilder._();
/// 最近阅读子菜单最大条目数
static const int maxRecentReadItems = 5;
/// 标题最大长度(超出截断)
static const int maxTitleLength = 24;
/// 构建托盘菜单
///
/// [readLaterEntries] 稍后阅读条目列表(取前 5 条作为最近阅读)
/// [isDark] 当前是否深色模式
/// [isWorkbenchMode] 是否工作台模式
/// [labels] 菜单标签
/// [callbacks] 菜单项回调
static List<TrayMenuItem> build({
required List<ReadLaterEntry> readLaterEntries,
required bool isDark,
required bool isWorkbenchMode,
required TrayMenuLabels labels,
required TrayMenuCallbacks callbacks,
}) {
return [
// ============================================================
// 第 1 组:主操作
// ============================================================
TrayMenuItem(
label: labels.newNote,
shortcut: 'Cmd+N',
onTap: callbacks.onNewNote,
),
TrayMenuItem(
label: labels.newInspiration,
shortcut: 'Cmd+I',
onTap: callbacks.onNewInspiration,
),
TrayMenuItem(
label: labels.openReadLater,
shortcut: 'Cmd+R',
onTap: callbacks.onOpenReadLater,
),
const TrayMenuItem.separator(),
// ============================================================
// 第 2 组:快速访问(最近阅读子菜单)
// ============================================================
TrayMenuItem(
label: labels.recentRead,
submenu: _buildRecentReadSubmenu(
entries: readLaterEntries,
labels: labels,
callbacks: callbacks,
),
),
const TrayMenuItem.separator(),
// ============================================================
// 第 3 组:模式切换
// ============================================================
TrayMenuItem(
label: labels.workbenchMode,
checked: isWorkbenchMode,
onTap: callbacks.onToggleWorkbench,
),
TrayMenuItem(
label: labels.darkMode,
checked: isDark,
onTap: callbacks.onToggleDarkMode,
),
const TrayMenuItem.separator(),
// ============================================================
// 第 4 组:系统
// ============================================================
TrayMenuItem(
label: labels.showMainWindow,
onTap: callbacks.onShowMainWindow,
),
TrayMenuItem(
label: labels.preferences,
shortcut: 'Cmd+,',
onTap: callbacks.onOpenSettings,
),
TrayMenuItem(
label: labels.exitApp,
shortcut: 'Cmd+Q',
onTap: callbacks.onExit,
),
];
}
/// 构建最近阅读子菜单
static List<TrayMenuItem> _buildRecentReadSubmenu({
required List<ReadLaterEntry> entries,
required TrayMenuLabels labels,
required TrayMenuCallbacks callbacks,
}) {
if (entries.isEmpty) {
return [
TrayMenuItem(
label: labels.noRecentRead,
disabled: true,
),
];
}
// 取前 maxRecentReadItems 条
final recent = entries.take(maxRecentReadItems).toList();
return recent.map((entry) {
return TrayMenuItem(
label: _truncateTitle(entry.title, maxTitleLength),
onTap: () => callbacks.onOpenReadLaterEntry(entry.id),
);
}).toList();
}
/// 截断标题(超出长度添加省略号)
static String _truncateTitle(String title, int maxLen) {
if (title.isEmpty) return '(无标题)';
if (title.length <= maxLen) return title;
return '${title.substring(0, maxLen)}';
}
}

View File

@@ -0,0 +1,210 @@
/// ============================================================
/// 闲言APP — 桌面端系统托盘服务抽象
/// 创建时间: 2026-06-18
/// 更新时间: 2026-06-18
/// 作用: 定义跨平台系统托盘服务接口(图标/Tooltip/菜单/未读角标/事件)
/// 上次更新: 初始创建,定义 DesktopTrayService 抽象 + TrayMenuItem 模型
/// ============================================================
import 'dart:async';
import 'package:flutter/foundation.dart';
/// 桌面端系统托盘服务抽象
///
/// 跨平台系统托盘能力统一接口,支持 macOS / Windows / Linux。
/// iOS / Android / 鸿蒙端使用 [StubDesktopTrayService] 返回 no-op 实现。
abstract class DesktopTrayService {
/// 单例实例(由 [DesktopServiceRegistry] 注入)
static DesktopTrayService? _instance;
/// 获取单例实例
static DesktopTrayService get instance {
_instance ??= _createInstance();
return _instance!;
}
/// 设置单例实例(用于测试注入)
static set instance(DesktopTrayService service) {
_instance = service;
}
/// 创建实例(由子类覆盖)
DesktopTrayService createInstance();
static DesktopTrayService _createInstance() {
// 由 desktop_service_registry.dart 在初始化时注入
return _instance ??= StubDesktopTrayService();
}
// ============================================================
// 生命周期
// ============================================================
/// 初始化托盘图标、Tooltip、菜单
Future<void> init();
/// 销毁托盘(应用退出时调用)
Future<void> destroy();
// ============================================================
// 托盘属性
// ============================================================
/// 更新托盘图标(根据主题切换浅色/深色图标)
Future<void> setIcon({required bool isDark});
/// 更新 Tooltip鼠标悬停提示
Future<void> setToolTip(String tip);
/// 更新未读角标0 表示隐藏)
///
/// macOS: 通过 `setTitle` 显示数字角标
/// Windows: 程序化叠加角标图层
Future<void> setUnreadBadge(int count);
/// 更新右键菜单
Future<void> setMenu(List<TrayMenuItem> items);
/// 弹出上下文菜单
///
/// macOS 上 tray_manager 不会自动弹出菜单,需要手动调用此方法。
/// Windows/Linux 上通常由系统自动弹出,此方法为 no-op。
Future<void> popUpContextMenu();
// ============================================================
// 事件流
// ============================================================
/// 托盘事件流(单击/双击/右键)
Stream<TrayEvent> get events;
// ============================================================
// 平台能力
// ============================================================
/// 是否支持托盘(平台判断)
bool get isSupported;
/// 托盘是否已初始化
bool get isInitialized;
}
/// 托盘事件类型
enum TrayEventKind {
/// 单击(左键)
click,
/// 双击(左键)
doubleClick,
/// 右键单击
rightClick,
}
/// 托盘事件
class TrayEvent {
final TrayEventKind kind;
const TrayEvent({required this.kind});
@override
String toString() => 'TrayEvent(kind: $kind)';
}
/// 托盘菜单项
///
/// 支持:
/// - 普通菜单项label + onTap + shortcut + checked
/// - 分隔线isSeparator = true
/// - 子菜单submenu 非空)
class TrayMenuItem {
/// 菜单项标签(分隔线时为空)
final String label;
/// 点击回调(分隔线/子菜单父项时为 null
final VoidCallback? onTap;
/// 子菜单(非空时为子菜单父项,点击不触发 onTap
final List<TrayMenuItem>? submenu;
/// 是否为分隔线
final bool isSeparator;
/// 是否勾选(用于切换状态显示,如"静默模式 ✓"
final bool checked;
/// 快捷键提示文本(如 "Cmd+N",仅显示用,不实际绑定)
final String? shortcut;
/// 是否禁用
final bool disabled;
const TrayMenuItem({
required this.label,
this.onTap,
this.submenu,
this.isSeparator = false,
this.checked = false,
this.shortcut,
this.disabled = false,
});
/// 创建分隔线
const TrayMenuItem.separator()
: label = '',
onTap = null,
submenu = null,
isSeparator = true,
checked = false,
shortcut = null,
disabled = false;
@override
String toString() =>
'TrayMenuItem(label: $label, isSeparator: $isSeparator, checked: $checked, hasSubmenu: ${submenu != null})';
}
/// Stub 实现iOS / Android / 鸿蒙端)
///
/// 所有方法 no-op[isSupported] 返回 false。
class StubDesktopTrayService implements DesktopTrayService {
bool _initialized = false;
@override
DesktopTrayService createInstance() => StubDesktopTrayService();
@override
Future<void> init() async {
_initialized = true;
}
@override
Future<void> destroy() async {
_initialized = false;
}
@override
Future<void> setIcon({required bool isDark}) async {}
@override
Future<void> setToolTip(String tip) async {}
@override
Future<void> setUnreadBadge(int count) async {}
@override
Future<void> setMenu(List<TrayMenuItem> items) async {}
@override
Future<void> popUpContextMenu() async {}
@override
Stream<TrayEvent> get events => const Stream<TrayEvent>.empty();
@override
bool get isSupported => false;
@override
bool get isInitialized => _initialized;
}

View File

@@ -0,0 +1,77 @@
/// ============================================================
/// 闲言APP — 桌面端窗口特效服务抽象
/// 创建时间: 2026-06-18
/// 更新时间: 2026-06-18
/// 作用: 定义跨平台窗口特效接口(毛玻璃/亚克力/Mica/标题栏融合)
/// 上次更新: 初始创建,定义 DesktopWindowEffectService 抽象
/// ============================================================
/// 桌面端窗口特效服务抽象
///
/// 跨平台窗口特效能力统一接口:
/// - macOS: 侧边栏毛玻璃 + 标题栏融合(基于 macos_window_utils
/// - Windows: Win11 Mica + Win10 Acrylic基于 flutter_acrylic
/// - Linux/iOS/Android/鸿蒙: Stub 实现,无特效
abstract class DesktopWindowEffectService {
/// 单例实例
static DesktopWindowEffectService? _instance;
static DesktopWindowEffectService get instance {
_instance ??= StubWindowEffectService();
return _instance!;
}
static set instance(DesktopWindowEffectService service) {
_instance = service;
}
// ============================================================
// 初始化
// ============================================================
/// 初始化窗口特效(在 window_manager.ensureInitialized 之前调用)
///
/// flutter_acrylic 的 `Window.initialize()` 必须在 window_manager 之前调用。
Future<void> initialize();
// ============================================================
// 特效应用
// ============================================================
/// 应用窗口特效
///
/// [isDark] 当前是否深色主题
/// [sidebarBlur] 是否启用侧边栏毛玻璃(仅 macOS 生效)
Future<void> applyEffect({
required bool isDark,
bool sidebarBlur = true,
});
// ============================================================
// 平台能力
// ============================================================
/// 是否支持窗口特效
bool get isSupported;
/// 当前特效名称(用于调试/日志)
String get effectName;
}
/// Stub 实现iOS / Android / 鸿蒙 / Linux
class StubWindowEffectService implements DesktopWindowEffectService {
@override
Future<void> initialize() async {}
@override
Future<void> applyEffect({
required bool isDark,
bool sidebarBlur = true,
}) async {}
@override
bool get isSupported => false;
@override
String get effectName => 'none';
}

View File

@@ -0,0 +1,115 @@
/// ============================================================
/// 闲言APP — macOS 窗口特效服务实现
/// 创建时间: 2026-06-18
/// 更新时间: 2026-06-18
/// 作用: 基于 macos_window_utils 实现 macOS 窗口特效(标题栏融合/侧边栏毛玻璃)
/// 上次更新: 初始创建,实现 DesktopWindowEffectService 接口
/// ============================================================
import 'package:macos_window_utils/macos_window_utils.dart';
import '../desktop_window_effect_service.dart';
import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu;
import 'package:xianyan/core/utils/logger.dart';
/// macOS 窗口特效服务实现
///
/// 基于 macos_window_utils 提供:
/// - 标题栏融合titlebarAppearsTransparent + fullSizeContentView
/// - 侧边栏毛玻璃NSVisualEffectViewMaterial.sidebar
/// - 主题跟随overrideMacOSBrightness
class MacosWindowEffectService implements DesktopWindowEffectService {
MacosWindowEffectService._();
static final MacosWindowEffectService _instance =
MacosWindowEffectService._();
factory MacosWindowEffectService() => _instance;
bool _initialized = false;
int? _sidebarVisualEffectSubviewId;
@override
Future<void> initialize() async {
if (!pu.isMacOS) return;
if (_initialized) return;
try {
await WindowManipulator.initialize();
_initialized = true;
Log.i('MacosWindowEffectService 初始化完成');
} catch (e) {
Log.e('MacosWindowEffectService.initialize 失败: $e');
}
}
@override
Future<void> applyEffect({
required bool isDark,
bool sidebarBlur = true,
}) async {
if (!pu.isMacOS || !_initialized) return;
try {
// 1. 标题栏融合:透明标题栏 + 全尺寸内容视图
await WindowManipulator.makeTitlebarTransparent();
await WindowManipulator.enableFullSizeContentView();
await WindowManipulator.hideTitle();
// 2. 主题跟随:覆盖 macOS 亮度设置
await WindowManipulator.overrideMacOSBrightness(dark: isDark);
// 3. 侧边栏毛玻璃
if (sidebarBlur) {
await _applySidebarBlur();
}
// 4. 窗口背景色设为透明(让 NSVisualEffectView 透出)
await WindowManipulator.setWindowBackgroundColorToClear();
Log.i('MacosWindowEffectService 特效已应用 (isDark=$isDark, sidebarBlur=$sidebarBlur)');
} catch (e) {
Log.e('MacosWindowEffectService.applyEffect 失败: $e');
}
}
/// 应用侧边栏毛玻璃
///
/// 使用 NSVisualEffectViewMaterial.sidebar 实现 macOS 原生侧边栏模糊效果。
/// 最低支持 macOS 12项目最低版本 13.0,完全兼容)。
Future<void> _applySidebarBlur() async {
try {
// 设置主视觉效果视图状态为 active
await WindowManipulator.setNSVisualEffectViewState(
NSVisualEffectViewState.active,
);
// 设置材质为 sidebarmacOS 12+ 支持)
await WindowManipulator.setMaterial(NSVisualEffectViewMaterial.sidebar);
// 如果之前已添加子视图,先移除
if (_sidebarVisualEffectSubviewId != null) {
await WindowManipulator.removeVisualEffectSubview(
_sidebarVisualEffectSubviewId!,
);
}
// 添加侧边栏视觉效果子视图
_sidebarVisualEffectSubviewId = await WindowManipulator
.addVisualEffectSubview(
VisualEffectSubviewProperties(
material: NSVisualEffectViewMaterial.sidebar,
state: NSVisualEffectViewState.active,
),
);
} catch (e) {
Log.e('MacosWindowEffectService._applySidebarBlur 失败: $e');
}
}
@override
bool get isSupported => pu.isMacOS;
@override
String get effectName => 'macos_sidebar_blur';
}

View File

@@ -0,0 +1,211 @@
/// ============================================================
/// 闲言APP — tray_manager 系统托盘服务实现
/// 创建时间: 2026-06-18
/// 更新时间: 2026-06-18
/// 作用: 基于 tray_manager 实现跨平台系统托盘macOS/Win/Linux
/// 上次更新: 初始创建,实现 DesktopTrayService 接口
/// ============================================================
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:tray_manager/tray_manager.dart';
import '../desktop_tray_service.dart';
import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu;
import 'package:xianyan/core/utils/logger.dart';
/// 基于 tray_manager 的系统托盘服务实现
///
/// 支持 macOS / Windows / Linux 三端。
/// iOS / Android / 鸿蒙端使用 [StubDesktopTrayService]。
class TrayManagerTrayService
implements DesktopTrayService, TrayListener {
TrayManagerTrayService._();
static final TrayManagerTrayService _instance = TrayManagerTrayService._();
factory TrayManagerTrayService() => _instance;
@override
DesktopTrayService createInstance() => _instance;
bool _initialized = false;
final _eventController = StreamController<TrayEvent>.broadcast();
// 当前菜单项回调映射id -> callback
final Map<int, VoidCallback> _callbackMap = {};
@override
Future<void> init() async {
if (!pu.isDesktop) {
Log.w('TrayManagerTrayService.init: 当前平台不支持托盘');
return;
}
if (_initialized) return;
try {
trayManager.addListener(this);
_initialized = true;
Log.i('TrayManagerTrayService 初始化完成');
} catch (e) {
Log.e('TrayManagerTrayService.init 失败: $e');
}
}
@override
Future<void> destroy() async {
if (!_initialized) return;
try {
trayManager.removeListener(this);
await trayManager.destroy();
_initialized = false;
_callbackMap.clear();
await _eventController.close();
Log.i('TrayManagerTrayService 已销毁');
} catch (e) {
Log.e('TrayManagerTrayService.destroy 失败: $e');
}
}
@override
Future<void> setIcon({required bool isDark}) async {
if (!_initialized) return;
try {
// macOS 使用 isTemplate 让系统自动反色
// Windows/Linux 需要明暗两套图标
final iconPath = isDark
? 'assets/images/tray_icon_dark.png'
: 'assets/images/tray_icon_light.png';
if (pu.isMacOS) {
// macOS: isTemplate=true 时系统自动处理深浅色
await trayManager.setIcon(
'assets/images/tray_icon_light.png',
isTemplate: true,
);
} else {
// Windows/Linux: 明暗两套图标
await trayManager.setIcon(iconPath);
}
} catch (e) {
Log.e('TrayManagerTrayService.setIcon 失败: $e');
}
}
@override
Future<void> setToolTip(String tip) async {
if (!_initialized) return;
try {
await trayManager.setToolTip(tip);
} catch (e) {
Log.e('TrayManagerTrayService.setToolTip 失败: $e');
}
}
@override
Future<void> setUnreadBadge(int count) async {
if (!_initialized) return;
try {
// macOS: setTitle 显示数字角标(标题显示在图标旁)
// Windows/Linux: tray_manager 不原生支持角标,仅更新 Tooltip
if (pu.isMacOS) {
await trayManager.setTitle(count > 0 ? count.toString() : '');
}
// 统一更新 Tooltip 包含未读数
final tip = count > 0 ? '闲言 — $count 条未读' : '闲言';
await trayManager.setToolTip(tip);
} catch (e) {
Log.e('TrayManagerTrayService.setUnreadBadge 失败: $e');
}
}
@override
Future<void> setMenu(List<TrayMenuItem> items) async {
if (!_initialized) return;
try {
_callbackMap.clear();
final menuItems = <MenuItem>[];
for (final item in items) {
menuItems.add(_convertMenuItem(item));
}
final menu = Menu(items: menuItems);
await trayManager.setContextMenu(menu);
} catch (e, st) {
Log.e('TrayManagerTrayService.setMenu 失败: $e', e, st);
}
}
@override
Future<void> popUpContextMenu() async {
if (!_initialized) return;
try {
await trayManager.popUpContextMenu();
} catch (e, st) {
Log.e('TrayManagerTrayService.popUpContextMenu 失败: $e', e, st);
}
}
/// 将 TrayMenuItem 转换为 menu_base 的 MenuItem
MenuItem _convertMenuItem(TrayMenuItem item) {
if (item.isSeparator) {
return MenuItem.separator();
}
final menuItem = MenuItem(
label: item.label,
disabled: item.disabled,
onClick: (menuItem) {
item.onTap?.call();
},
);
if (item.checked) {
menuItem.type = 'checkbox';
menuItem.checked = true;
}
if (item.submenu != null && item.submenu!.isNotEmpty) {
menuItem.type = 'submenu';
menuItem.submenu = Menu(
items: item.submenu!.map(_convertMenuItem).toList(),
);
}
return menuItem;
}
@override
Stream<TrayEvent> get events => _eventController.stream;
@override
bool get isSupported => pu.isDesktop;
@override
bool get isInitialized => _initialized;
// ============================================================
// TrayListener 回调
// ============================================================
@override
void onTrayIconMouseDown() {
_eventController.add(const TrayEvent(kind: TrayEventKind.click));
}
@override
void onTrayIconMouseUp() {}
@override
void onTrayIconRightMouseDown() {
_eventController.add(const TrayEvent(kind: TrayEventKind.rightClick));
}
@override
void onTrayIconRightMouseUp() {}
@override
void onTrayMenuItemClick(MenuItem menuItem) {
// 回调已在 _convertMenuItem 的 onClick 中处理
}
}

View File

@@ -0,0 +1,164 @@
/// ============================================================
/// 闲言APP — Windows 窗口特效服务实现
/// 创建时间: 2026-06-18
/// 更新时间: 2026-06-19
/// 作用: 基于 flutter_acrylic 实现 Windows 窗口特效Win11 Mica Alt/Mica/Win10 Acrylic
/// 上次更新: 新增 Mica Alt 特效支持Win11 build >= 22621自动降级 Mica Alt → Mica → Acrylic
/// ============================================================
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
import '../desktop_window_effect_service.dart';
import 'package:xianyan/core/utils/platform/platform_utils.dart' as pu;
import 'package:xianyan/core/utils/logger.dart';
/// Windows 窗口特效服务实现
///
/// 基于 flutter_acrylic 提供:
/// - Win11 22621+: Mica Alt云母变体对壁纸色调更敏感标题栏区域同样应用特效
/// - Win11 22000+: Mica云母跟随系统主题
/// - Win10 1809+: Acrylic亚克力半透明
/// - Win10 早期: 降级为纯色背景
///
/// 实现说明flutter_acrylic 1.1.4 的 WindowEffect 枚举未直接提供 micaAlt
/// 此处使用 [WindowEffect.tabbed](对应 DWM DWMSBT_TABBEDWINDOW作为 Mica Alt
/// 的等价实现——两者均为"比 Mica 更透明、对桌面壁纸色调更敏感"的云母变体,
/// 视觉表现与 Win11 22621+ 的 Mica Alt 一致。
class WindowsAcrylicService implements DesktopWindowEffectService {
WindowsAcrylicService._();
static final WindowsAcrylicService _instance = WindowsAcrylicService._();
factory WindowsAcrylicService() => _instance;
bool _initialized = false;
bool? _isWin11OrLater;
bool? _isMicaAltSupportedCache;
@override
Future<void> initialize() async {
if (!pu.isWindows) return;
if (_initialized) return;
try {
await Window.initialize();
_isWin11OrLater = await _detectWindows11OrLater();
_isMicaAltSupportedCache = await _isMicaAltSupported();
_initialized = true;
Log.i('WindowsAcrylicService 初始化完成 '
'(isWin11=$_isWin11OrLater, micaAlt=$_isMicaAltSupportedCache)');
} catch (e) {
Log.e('WindowsAcrylicService.initialize 失败: $e');
}
}
@override
Future<void> applyEffect({
required bool isDark,
bool sidebarBlur = true,
}) async {
if (!pu.isWindows || !_initialized) return;
try {
// 优先级Mica Alt → Mica → Acrylic
// Mica AltWin11 22621+):对壁纸色调更敏感,标题栏区域同样应用特效
// 使用 WindowEffect.tabbed 作为 Mica Alt 的等价实现(见类说明)
if (_isMicaAltSupportedCache == true) {
await Window.setEffect(
effect: WindowEffect.tabbed,
color: isDark ? const Color(0xFF1C1C1C) : const Color(0xFFF3F3F3),
dark: isDark,
);
Log.i('WindowsAcrylicService 应用 Mica Alt 特效 (isDark=$isDark)');
return;
}
if (_isWin11OrLater == true) {
// Win11: Mica 背景(跟随系统主题)
await Window.setEffect(
effect: WindowEffect.mica,
dark: isDark,
);
Log.i('WindowsAcrylicService 应用 Mica 特效 (isDark=$isDark)');
} else {
// Win10: Acrylic 半透明
await Window.setEffect(
effect: WindowEffect.acrylic,
dark: isDark,
color: isDark
? const Color(0xCC1F1F1F) // 深色半透明
: const Color(0xCCF3F3F3), // 浅色半透明
);
Log.i('WindowsAcrylicService 应用 Acrylic 特效 (isDark=$isDark)');
}
} catch (e) {
Log.e('WindowsAcrylicService.applyEffect 失败: $e');
// 降级:禁用特效
try {
await Window.setEffect(
effect: WindowEffect.disabled,
dark: isDark,
);
} catch (_) {}
}
}
/// 检测当前系统是否支持 Mica AltWin11 build >= 22621
///
/// 使用 device_info_plus 读取精确的 Windows build 号。
/// Mica Alt此处以 WindowEffect.tabbed 等价实现)需要 Win11 22621 及以上。
/// 结果会被缓存,避免重复读取设备信息。
Future<bool> _isMicaAltSupported() async {
if (_isMicaAltSupportedCache != null) return _isMicaAltSupportedCache!;
try {
final info = await DeviceInfoPlugin().windowsInfo;
// device_info_plus 13.x 中 buildNumber 为 int 类型,无需解析
final supported = info.buildNumber >= 22621;
_isMicaAltSupportedCache = supported;
return supported;
} catch (e) {
Log.w('WindowsAcrylicService._isMicaAltSupported 检测失败: $e');
_isMicaAltSupportedCache = false;
return false;
}
}
/// 检测是否为 Windows 11 或更高版本
///
/// Windows 11 Build >= 22000
Future<bool> _detectWindows11OrLater() async {
try {
final version = Platform.operatingSystemVersion;
// 解析格式:"10.0.22000.1234" 或 "Windows 10 Pro 10.0.22000.1234"
final match = RegExp(r'(\d+)\.(\d+)\.(\d+)').firstMatch(version);
if (match == null) return false;
final major = int.parse(match.group(1)!);
final minor = int.parse(match.group(2)!);
final build = int.parse(match.group(3)!);
// Win11: major=10, minor=0, build>=22000
// 注意Win11 和 Win10 都报告 major=10通过 build 号区分
if (major == 10 && minor == 0 && build >= 22000) {
return true;
}
return false;
} catch (e) {
Log.w('WindowsAcrylicService._detectWindows11OrLater 解析失败: $e');
return false;
}
}
@override
bool get isSupported => pu.isWindows;
@override
String get effectName {
if (_isMicaAltSupportedCache == true) return 'windows_mica_alt';
return _isWin11OrLater == true ? 'windows_mica' : 'windows_acrylic';
}
}

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — macOS平台统一服务
/// 创建时间: 2026-06-02
/// 更新时间: 2026-06-02
/// 作用: 集中管理所有macOS原生MethodChannel交互主题同步/窗口管理/工具栏样式
/// 上次更新: 整合MacosTitleBarService新增窗口管理能力
/// 更新时间: 2026-06-19
/// 作用: 集中管理所有macOS原生MethodChannel交互主题同步/窗口管理/Touch Bar/共享/Dock徽章/菜单栏金句/Spotlight
/// 上次更新: 新增 Touch Bar、NSSharingService、NSDockTile、NSStatusItem、CoreSpotlight 五项原生能力
/// ============================================================
import 'package:flutter/services.dart';
@@ -13,7 +13,11 @@ import 'package:xianyan/core/utils/logger.dart';
class MacosPlatformService {
MacosPlatformService._();
static const _channel = MethodChannel('com.xianyan.macos');
/// 窗口级通道MainFlutterWindow 注册)
static const _channel = MethodChannel('apps.xy.xianyan/macos');
/// 应用级通道AppDelegate 注册)
static const _appChannel = MethodChannel('apps.xy.xianyan/macos.app');
// ============================================================
// 主题同步(原 MacosTitleBarService
@@ -106,6 +110,99 @@ class MacosPlatformService {
}
}
// ============================================================
// Touch Bar 支持
// ============================================================
/// 设置 Touch Bar 按钮项
///
/// [items] 按钮列表,每项包含 {label: "加粗", action: "bold"}
/// 点击按钮时通过 touchBarAction 事件回调
static Future<void> setTouchBarItems(List<Map<String, String>> items) async {
if (!pu.isMacOS) return;
try {
await _channel.invokeMethod('setTouchBarItems', {'items': items});
} catch (e) {
Log.w('MacosPlatformService.setTouchBarItems失败: $e');
}
}
// ============================================================
// NSSharingService 共享
// ============================================================
/// 显示系统共享面板,支持 text/url/image 三种内容
static Future<void> showShareSheet({
String? text,
String? url,
Uint8List? imageBytes,
}) async {
if (!pu.isMacOS) return;
try {
final args = <String, dynamic>{};
if (text != null) args['text'] = text;
if (url != null) args['url'] = url;
if (imageBytes != null) args['imageBytes'] = imageBytes;
await _channel.invokeMethod('showShareSheet', args);
} catch (e) {
Log.w('MacosPlatformService.showShareSheet失败: $e');
}
}
// ============================================================
// Dock 徽章NSDockTile
// ============================================================
/// 设置 Dock 图标徽章数字count <= 0 时清除
static Future<void> setDockBadge(int count) async {
if (!pu.isMacOS) return;
try {
await _appChannel.invokeMethod('setDockBadge', {'count': count});
} catch (e) {
Log.w('MacosPlatformService.setDockBadge失败: $e');
}
}
// ============================================================
// 菜单栏金句NSStatusItem
// ============================================================
/// 更新菜单栏金句,超过 30 字符截断显示,点击复制完整内容
static Future<void> updateStatusBarSentence(String sentence) async {
if (!pu.isMacOS) return;
try {
await _appChannel.invokeMethod('updateStatusBarSentence', {'sentence': sentence});
} catch (e) {
Log.w('MacosPlatformService.updateStatusBarSentence失败: $e');
}
}
// ============================================================
// Spotlight 索引CoreSpotlight
// ============================================================
/// 索引条目到 Spotlight 搜索
///
/// [items] 条目列表,每项包含 {id, title, content, type}
static Future<void> indexSpotlightItems(List<Map<String, dynamic>> items) async {
if (!pu.isMacOS) return;
try {
await _appChannel.invokeMethod('indexSpotlightItems', {'items': items});
} catch (e) {
Log.w('MacosPlatformService.indexSpotlightItems失败: $e');
}
}
/// 清除所有 Spotlight 索引
static Future<void> clearSpotlightIndex() async {
if (!pu.isMacOS) return;
try {
await _appChannel.invokeMethod('clearSpotlightIndex');
} catch (e) {
Log.w('MacosPlatformService.clearSpotlightIndex失败: $e');
}
}
// ============================================================
// 内部工具
// ============================================================

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — Windows平台统一服务
/// 创建时间: 2026-06-16
/// 更新时间: 2026-06-16
/// 作用: 集中管理所有Windows原生MethodChannel交互(标题栏主题同步)
/// 上次更新: 初始创建,支持标题栏深色模式切换
/// 更新时间: 2026-06-18
/// 作用: 集中管理所有Windows原生MethodChannel交互
/// 上次更新: 补齐 6 个 MethodChannel 方法setWindowTitle/setFullscreen/isFullscreen/setMinSize/performHapticFeedback/getSystemAppearance
/// ============================================================
import 'package:flutter/services.dart';
@@ -13,7 +13,7 @@ import 'package:xianyan/core/utils/logger.dart';
class WindowsPlatformService {
WindowsPlatformService._();
static const _channel = MethodChannel('com.xianyan.windows');
static const _channel = MethodChannel('apps.xy.xianyan/windows');
// ============================================================
// 主题同步
@@ -33,6 +33,60 @@ class WindowsPlatformService {
_invoke('setDarkMode', {'isDark': isDark});
}
// ============================================================
// 窗口管理6 个新增方法)
// ============================================================
/// 设置窗口标题
static void setWindowTitle(String title) {
if (!pu.isWindows) return;
_invoke('setWindowTitle', {'title': title});
}
/// 进入/退出全屏模式
static void setFullscreen(bool fullscreen) {
if (!pu.isWindows) return;
_invoke('setFullscreen', {'fullscreen': fullscreen});
}
/// 查询当前是否处于全屏模式
static Future<bool> isFullscreen() async {
if (!pu.isWindows) return false;
try {
final result = await _channel.invokeMethod<bool>('isFullscreen');
return result ?? false;
} catch (e) {
Log.w('WindowsPlatformService.isFullscreen失败: $e');
return false;
}
}
/// 设置窗口最小尺寸(逻辑像素)
static void setMinSize(int width, int height) {
if (!pu.isWindows) return;
_invoke('setMinSize', {'width': width, 'height': height});
}
/// 执行触觉反馈
///
/// [type] 0=light, 1=medium, 2=heavy, 3=selection
static void performHapticFeedback(int type) {
if (!pu.isWindows) return;
_invoke('performHapticFeedback', {'type': type});
}
/// 获取系统外观模式("light" 或 "dark"
static Future<String> getSystemAppearance() async {
if (!pu.isWindows) return 'light';
try {
final result = await _channel.invokeMethod<String>('getSystemAppearance');
return result ?? 'light';
} catch (e) {
Log.w('WindowsPlatformService.getSystemAppearance失败: $e');
return 'light';
}
}
// ============================================================
// 内部工具
// ============================================================

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 深度链接服务
/// 创建时间: 2026-04-28
/// 更新时间: 2026-05-27
/// 更新时间: 2026-06-19
/// 作用: 使用 app_links 统一处理深度链接,支持冷启动和热恢复
/// 上次更新: 重构为使用 AppRouter.resolveDeepLinkUri 统一路径映射,消除重复逻辑
/// 上次更新: 新增 note/{id} 和 sentence/{id} 预解析,支持笔记编辑和句子详情跳转
/// ============================================================
import 'dart:async';
@@ -63,10 +63,45 @@ class DeepLinkService {
}
}
/// 预解析:处理需要特殊路由映射的 xianyan:// scheme
///
/// - `xianyan://note/{id}` → `/notes/edit?id={id}`(笔记编辑页用 query param
/// - `xianyan://note` → `/notes`(笔记列表页)
/// - `xianyan://sentence/{id}` → `/home`(句子详情需 HomeSentence 对象,
/// 暂导航到首页,后续可扩展为通过 ID 加载句子后弹出详情 Sheet
///
/// 返回 null 表示无需预解析,交给 AppRouter.resolveDeepLinkUri 统一处理
static String? _preResolve(Uri uri) {
if (uri.scheme != 'xianyan') return null;
final host = uri.host;
final segments = uri.pathSegments;
switch (host) {
case 'note':
// 笔记编辑页 /notes/edit 接收 query param id
final noteId = segments.isNotEmpty ? segments.first : null;
if (noteId != null && noteId.isNotEmpty) {
Log.i('🔗 [DeepLink] note/$noteId${AppRoutes.noteEdit}?id=$noteId');
return '${AppRoutes.noteEdit}?id=$noteId';
}
return AppRoutes.noteList;
case 'sentence':
// 句子详情 Sheet 需要 HomeSentence 对象,暂导航到首页
final sentenceId = segments.isNotEmpty ? segments.first : '';
Log.i('🔗 [DeepLink] sentence/$sentenceId → 首页(句子详情需加载后展示)');
return AppRoutes.home;
default:
return null;
}
}
/// 处理单个深度链接 URI
/// 使用 AppRouter.resolveDeepLinkUri 统一路径映射
/// 先通过 _preResolve 处理需要特殊路由的 schemenote/{id}, sentence/{id}
/// 其余委托给 AppRouter.resolveDeepLinkUri 统一路径映射
static void _handleLink(Uri uri) {
final resolved = AppRouter.resolveDeepLinkUri(uri);
// 1. 预解析:处理需要特殊路由的 scheme
final resolved = _preResolve(uri) ?? AppRouter.resolveDeepLinkUri(uri);
if (resolved == null) {
Log.w('🔗 [DeepLink] 无法解析: $uri');
return;

View File

@@ -1,11 +1,12 @@
/// ============================================================
/// 闲言APP — 统一通知中心
/// 创建时间: 2026-05-22
/// 更新时间: 2026-06-12
/// 更新时间: 2026-06-19
/// 作用: 合并 NotificationScheduler + DailyNotifyService统一管理所有本地通知调度
/// 上次更新: 新增推送计数器pushCount/clickCount每次调度/点击时+1
/// 上次更新: 类型安全修复(int vs num): 节气日期 year/month/day 使用 SafeJson.parseInt
/// ============================================================
import 'package:xianyan/core/utils/safe_json.dart';
import 'local_notification_service.dart';
import '../../storage/kv_storage.dart';
import '../../utils/logger.dart';
@@ -306,9 +307,9 @@ class NotificationCenter {
if (nextTerm == null) return;
final scheduledTime = DateTime(
nextTerm['year'] as int,
nextTerm['month'] as int,
nextTerm['day'] as int,
SafeJson.parseInt(nextTerm['year']),
SafeJson.parseInt(nextTerm['month']),
SafeJson.parseInt(nextTerm['day']),
8,
);
@@ -360,9 +361,9 @@ class NotificationCenter {
final terms = _solarTerms2026;
for (final term in terms) {
final date = DateTime(
term['year'] as int,
term['month'] as int,
term['day'] as int,
SafeJson.parseInt(term['year']),
SafeJson.parseInt(term['month']),
SafeJson.parseInt(term['day']),
);
if (date.isAfter(now)) return term;
}