Files
app/lib/services/desktop_service.dart
T
2026-05-15 09:03:09 +08:00

212 lines
5.4 KiB
Dart

import 'dart:io';
import 'package:flutter_acrylic/window.dart';
import 'package:flutter_acrylic/window_effect.dart';
import 'package:path/path.dart' as p;
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';
import 'package:tray_manager/tray_manager.dart';
import '../config/app_config.dart';
import '../core/utils/app_logger.dart';
import '../presentation/providers/theme_provider.dart';
/// 桌面端服务(窗口管理 + 系统托盘)
class DesktopService with TrayListener, WindowListener {
static DesktopService? _instance;
DesktopService._();
static DesktopService get instance {
_instance ??= DesktopService._();
return _instance!;
}
static bool get isDesktopPlatform =>
Platform.isWindows || Platform.isLinux;
bool _initialized = false;
/// 初始化桌面端服务,必须在 runApp 之前调用
Future<void> initialize() async {
if (!isDesktopPlatform || _initialized) return;
// 1. 初始化窗口管理器
await windowManager.ensureInitialized();
windowManager.addListener(this);
// --- 新增:初始化 flutter_acrylic ---
if (Platform.isWindows) {
var themeProvider = ThemeProvider();
await Window.initialize();
// 设置 Mica 效果
await Window.setEffect(
effect: WindowEffect.mica,
// 根据你的 ThemeProvider 判断是深色还是浅色 Mica
dark: themeProvider.isDark, // 建议这里先写死 false 测试,后续对接 ThemeProvider
);
}
// 2. 窗口选项设置
WindowOptions windowOptions = const WindowOptions(
size: Size(1280, 720),
center: true,
backgroundColor: Colors.transparent,
skipTaskbar: false,
titleBarStyle: TitleBarStyle.hidden,
);
// 3. 等待窗口准备就绪后执行居中和显示
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.setTitle(AppConfig.appName);
await windowManager.setMinimumSize(const Size(400, 300));
await windowManager.setPreventClose(true);
// 如果设置了 center: true 仍未生效(某些 Linux 环境),可以手动补刀:
// await windowManager.center();
await windowManager.show();
await windowManager.focus();
});
// 托盘管理器
trayManager.addListener(this);
final iconPath = await _getTrayIconPath();
AppLogger.d('DesktopService: tray icon path: $iconPath');
await trayManager.setIcon(iconPath);
try {
await trayManager.setToolTip(AppConfig.appName);
} catch (e) {
AppLogger.e('DesktopService: tray icon error: $e');
}
final menu = Menu(items: [
MenuItem(key: 'show', label: '显示主窗口'),
MenuItem.separator(),
MenuItem(key: 'quit', label: '退出'),
]);
await trayManager.setContextMenu(menu);
_initialized = true;
AppLogger.d('DesktopService initialized');
}
Future<String> _getTrayIconPath() async {
if (Platform.isWindows) {
// 获取当前可执行文件 (.exe) 所在的目录
String exePath = Platform.resolvedExecutable;
String exeDir = p.dirname(exePath);
// 拼接 Windows 下 Flutter Assets 的标准物理路径
// 注意:这里的路径必须与打包后的文件夹结构一致
return p.join(exeDir, 'data', 'flutter_assets', 'assets/icons/tray_icon.ico');
} else if (Platform.isLinux) {
return '/opt/cloudreve4/data/flutter_assets/assets/icons/tray_icon.png';
}
// 调试模式下通常直接用 assets 路径
return 'assets/icons/tray_icon.png';
}
// ========== TrayListener ==========
@override
void onTrayIconMouseDown() {
showWindow();
}
@override
void onTrayIconRightMouseDown() {
trayManager.popUpContextMenu();
}
@override
void onTrayIconRightMouseUp() {}
@override
void onTrayMenuItemClick(MenuItem menuItem) {
switch (menuItem.key) {
case 'show':
showWindow();
break;
case 'quit':
_quitApp();
break;
}
}
// ========== WindowListener ==========
@override
void onWindowClose() async {
AppLogger.d('DesktopService: onWindowClose -> hiding to tray');
await windowManager.hide();
}
@override
void onWindowFocus() {}
@override
void onWindowBlur() {}
@override
void onWindowMaximize() {}
@override
void onWindowUnmaximize() {}
@override
void onWindowMinimize() {}
@override
void onWindowRestore() {}
@override
void onWindowResize() {}
@override
void onWindowResized() {}
@override
void onWindowMove() {}
@override
void onWindowMoved() {}
@override
void onWindowEnterFullScreen() {}
@override
void onWindowLeaveFullScreen() {}
// ========== Private ==========
Future<void> showWindow() async {
await windowManager.show();
await windowManager.focus();
}
Future<void> _quitApp() async {
try {
AppLogger.d('DesktopService: Cleaning up before exit...');
// 彻底解绑
windowManager.removeListener(this);
trayManager.removeListener(this);
// 彻底销毁托盘(防止残留僵尸图标)
await trayManager.destroy();
// 允许关闭并销毁窗口
await windowManager.setPreventClose(false);
// 给系统一点点时间(50ms)处理最后的事件队列
await Future.delayed(const Duration(milliseconds: 50));
await windowManager.destroy();
} catch (e) {
AppLogger.e('Exit error: $e');
} finally {
exit(0);
}
}
}