Files
app/lib/services/desktop_service.dart
T
gongyun 17673b2862
Android APK Release / Build Android APK (push) Successful in 55m29s
1.3.21.3.2
2026-05-26 16:32:18 +08:00

230 lines
6.1 KiB
Dart
Raw 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.
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';
import 'sync_service.dart';
import '../src/rust/api/ffi.dart' as ffi;
/// 桌面端服务(窗口管理 + 系统托盘)
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...');
// 同步引擎清理(WCF 模式下必须调用,同步释放占位符、注销 sync root)
try {
await SyncService.instance.stop();
AppLogger.d('DesktopService: Sync engine stopped');
} catch (e) {
AppLogger.e('DesktopService: Error stopping sync: $e');
}
// 进程退出前同步清理(确保 WCF 资源释放,不依赖 tokio runtime
try {
await ffi.syncShutdown();
AppLogger.d('DesktopService: Sync shutdown complete');
} catch (e) {
AppLogger.e('DesktopService: Error in sync shutdown: $e');
}
// 彻底解绑
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);
}
}
}