Files
xianyan/lib/features/desktop/window_close_handler.dart
Developer 6119918185 release: bump version to 6.6.25+2606241
主要变更:
1. 新增桌面端托盘图标支持深色/浅色主题切换
2. 重构应用锁、动画配置、小组件导航服务职责
3. 修复Riverpod初始化断言、防重复点击、工作台模式残留选中态问题
4. 优化诗词服务、阅读进度、搜索结果空状态体验
5. 完善macOS打包配置与错误静默处理逻辑
6. 新增快速卡片多语言适配与动画退出队列管理
2026-06-24 04:26:50 +08:00

342 lines
12 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 窗口关闭处理服务
/// 创建时间: 2026-06-22
/// 更新时间: 2026-06-23
/// 作用: 处理窗口关闭按钮点击,弹出关闭/最小化运行对话框,支持"不再提醒"
/// 上次更新: 关闭对话框支持点击外侧遮罩关闭barrierDismissible=true取消即不操作
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart';
import '../../../l10n/translation_resolver.dart';
import '../../../l10n/types/t_settings_desktop.dart';
import '../../core/storage/kv_storage.dart';
import '../../core/utils/logger.dart';
/// 关闭行为选项
enum CloseAction {
/// 完全退出应用(关闭后台)
quit,
/// 最小化运行(仅关闭窗口和任务栏图标,保留托盘)
minimizeToTray,
}
/// 窗口关闭处理服务
///
/// 点击窗口栏 X 按钮时:
/// 1. 若用户已选择"不再提醒",直接执行记住的行为
/// 2. 否则弹出对话框,让用户选择"关闭"或"最小化运行"
/// 3. 支持"不再提醒"勾选框,记住用户选择
class WindowCloseHandler {
WindowCloseHandler._();
// 存储键
static const String _kCloseActionPref = 'window_close_action';
static const String _kCloseDontRemind = 'window_close_dont_remind';
// 上次执行关闭操作的时间戳ISO8601 字符串),用于 7 天自动恢复提醒
static const String _kLastActionTime = 'window_close_last_action_time';
// 7 天时长(用于自动恢复"不再提醒"
static const Duration _kRemindResetDuration = Duration(days: 7);
/// 处理关闭按钮点击
///
/// [context] 用于弹出对话框的 BuildContext同时用于获取 translationsProvider
/// [onQuit] 完全退出回调(可选,默认调用 windowManager.destroy
/// [onMinimizeToTray] 最小化到托盘回调(可选,默认调用 _minimizeToTray
///
/// 自动恢复提醒:若距上次关闭操作超过 7 天,自动重置"不再提醒"
/// 让用户重新看到确认对话框(避免长期未使用后忘记已选行为)。
static Future<void> handleClose({
required BuildContext context,
Future<void> Function()? onQuit,
Future<void> Function()? onMinimizeToTray,
}) async {
// 7 天自动恢复提醒:检查上次操作时间,超期则重置"不再提醒"
await _maybeResetDontRemindAfter7Days();
// 读取用户偏好
final dontRemind = KvStorage.getBool(_kCloseDontRemind) ?? false;
final actionIndex = KvStorage.getInt(_kCloseActionPref) ?? -1;
// 已选择"不再提醒",直接执行记住的行为
if (dontRemind && actionIndex >= 0) {
final savedAction = CloseAction.values[actionIndex];
await _recordLastActionTime();
await _executeAction(
action: savedAction,
onQuit: onQuit,
onMinimizeToTray: onMinimizeToTray,
);
return;
}
// 弹出对话框
if (!context.mounted) return;
// 通过 ProviderScope 获取 translationshandleClose 为静态方法)
final t = ProviderScope.containerOf(context, listen: false)
.read(translationsProvider)
.settings
.desktop;
final result = await _showCloseDialog(context, t);
if (result == null) return; // 用户取消
final (action, remember) = result;
// 记住用户选择
if (remember) {
await KvStorage.setInt(_kCloseActionPref, action.index);
await KvStorage.setBool(_kCloseDontRemind, true);
}
// 记录本次操作时间(用于 7 天自动恢复判断)
await _recordLastActionTime();
// 执行行为
await _executeAction(
action: action,
onQuit: onQuit,
onMinimizeToTray: onMinimizeToTray,
);
}
/// macOS ⌘Q 快捷键处理入口
///
/// 项目中 macOS 原生菜单栏使用 `PlatformProvidedMenuItemType.quit`
/// 系统 ⌘Q 默认直接退出应用,不会经过关闭确认流程。
/// 此方法供 app.dart / main.dart 在自定义快捷键监听中调用,
/// 让 ⌘Q 也走 [handleClose] 的关闭确认流程(含"不再提醒"与 7 天重置)。
///
/// 使用示例(在根 widget 的 KeyboardListener 中):
/// ```dart
/// if (HardwareKeyboard.instance.isMetaPressed &&
/// event.logicalKey == LogicalKeyboardKey.keyQ) {
/// await WindowCloseHandler.handleCmdQ(context);
/// }
/// ```
static Future<void> handleCmdQ(BuildContext context) {
return handleClose(context: context);
}
/// 7 天自动恢复提醒
///
/// 读取上次关闭操作时间,若距今超过 [_kRemindResetDuration]7 天),
/// 则将 `window_close_dont_remind` 重置为 false让用户重新看到确认对话框。
/// 这样长时间未使用后,用户不会因遗忘的旧选择而困惑。
static Future<void> _maybeResetDontRemindAfter7Days() async {
try {
final lastTimeStr = KvStorage.getString(_kLastActionTime);
if (lastTimeStr == null || lastTimeStr.isEmpty) return;
final lastTime = DateTime.tryParse(lastTimeStr);
if (lastTime == null) return;
final elapsed = DateTime.now().difference(lastTime);
if (elapsed >= _kRemindResetDuration) {
await KvStorage.setBool(_kCloseDontRemind, false);
Log.i('WindowCloseHandler: 距上次关闭操作已 ${elapsed.inDays} 天,'
'自动恢复"不再提醒"');
}
} catch (e, st) {
Log.e('WindowCloseHandler._maybeResetDontRemindAfter7Days 失败', e, st);
}
}
/// 记录本次关闭操作时间ISO8601 字符串)
static Future<void> _recordLastActionTime() async {
try {
await KvStorage.setString(
_kLastActionTime,
DateTime.now().toIso8601String(),
);
} catch (e, st) {
Log.e('WindowCloseHandler._recordLastActionTime 失败', e, st);
}
}
/// 弹出关闭确认对话框
///
/// [t] 桌面端翻译文案TSettingsDesktop
/// 返回 (CloseAction, bool remember),用户取消返回 null
static Future<(CloseAction, bool)?> _showCloseDialog(
BuildContext context,
TSettingsDesktop t,
) async {
bool dontRemind = false;
return showCupertinoDialog<(CloseAction, bool)?>(
context: context,
barrierDismissible: true,
builder: (ctx) {
return StatefulBuilder(
builder: (ctx, setDialogState) {
return CupertinoAlertDialog(
title: Text(t.windowCloseTitle),
content: Padding(
padding: const EdgeInsets.only(top: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(t.windowCloseMessage),
const SizedBox(height: 12),
// 不再提醒勾选框
GestureDetector(
onTap: () => setDialogState(() {
dontRemind = !dontRemind;
}),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6.resolveFrom(ctx),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
dontRemind
? CupertinoIcons.checkmark_square_fill
: CupertinoIcons.square,
size: 20,
color: dontRemind
? CupertinoColors.systemGreen
: CupertinoColors.systemGrey,
),
const SizedBox(width: 8),
Text(
t.windowCloseDontRemind,
style: const TextStyle(fontSize: 14),
),
],
),
),
),
],
),
),
actions: [
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () {
Navigator.pop(ctx, (CloseAction.quit, dontRemind));
},
child: Text(t.windowCloseAction),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () {
Navigator.pop(
ctx,
(CloseAction.minimizeToTray, dontRemind),
);
},
child: Text(t.windowMinimizeAction),
),
],
);
},
);
},
);
}
/// 执行关闭行为
static Future<void> _executeAction({
required CloseAction action,
Future<void> Function()? onQuit,
Future<void> Function()? onMinimizeToTray,
}) async {
try {
switch (action) {
case CloseAction.quit:
// 完全退出应用Win 和 Mac 都关闭后台
if (onQuit != null) {
await onQuit();
} else {
await windowManager.destroy();
}
break;
case CloseAction.minimizeToTray:
// 最小化运行:仅关闭窗口和任务栏图标,保留托盘
if (onMinimizeToTray != null) {
await onMinimizeToTray();
} else {
await _minimizeToTray();
}
break;
}
} catch (e, st) {
Log.e('WindowCloseHandler._executeAction 失败', e, st);
}
}
/// 最小化到托盘
///
/// 使用 minimize + setSkipTaskbar 组合替代 hide()
/// 因为 windowManager.hide() 在 macOS 上与 macos_window_utils 的
/// NSVisualEffectView + titlebarAppearsTransparent 组合会导致原生崩溃。
static Future<void> _minimizeToTray() async {
try {
await windowManager.setSkipTaskbar(true);
await windowManager.minimize();
Log.i('窗口已最小化到托盘');
} catch (e, st) {
Log.e('WindowCloseHandler._minimizeToTray 失败', e, st);
// 回退:直接最小化
try {
await windowManager.minimize();
} catch (e2) {
Log.e('WindowCloseHandler 回退最小化也失败: $e2');
}
}
}
/// 重置关闭偏好(供设置页面调用)
///
/// 同时清理:关闭行为选择、"不再提醒"标记、上次操作时间戳。
static Future<void> resetPreference() async {
await KvStorage.remove(_kCloseActionPref);
await KvStorage.remove(_kCloseDontRemind);
await KvStorage.remove(_kLastActionTime);
}
/// 设置关闭偏好(供设置页面调用)
///
/// [action] 关闭行为:
/// - 非 null保存指定行为
/// - null清除已保存的行为回到"每次询问"状态
/// [dontRemind] 是否不再提醒
static Future<void> setPreference({
CloseAction? action,
required bool dontRemind,
}) async {
if (action != null) {
await KvStorage.setInt(_kCloseActionPref, action.index);
} else {
// 清除已保存的关闭行为,回到"每次询问"状态
await KvStorage.remove(_kCloseActionPref);
}
await KvStorage.setBool(_kCloseDontRemind, dontRemind);
}
/// 获取当前关闭偏好
///
/// 返回 (CloseAction?, bool dontRemind)
/// CloseAction 为 null 表示未设置
static (CloseAction?, bool) getPreference() {
final dontRemind = KvStorage.getBool(_kCloseDontRemind) ?? false;
final actionIndex = KvStorage.getInt(_kCloseActionPref) ?? -1;
if (actionIndex < 0 || actionIndex >= CloseAction.values.length) {
return (null, dontRemind);
}
return (CloseAction.values[actionIndex], dontRemind);
}
}