230 lines
6.1 KiB
Dart
230 lines
6.1 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';
|
||
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);
|
||
}
|
||
}
|
||
}
|