Files
FlClash/lib/manager/tray_manager.dart
T
chen08209 1e97716636 feat(platform): move desktop windows to plugins/window and rework the tray
plugins/window replaces the window_manager fork on every desktop.

Changelog: Tray hotkeys, node delays and a delay test
2026-09-30 11:39:58 +08:00

122 lines
3.1 KiB
Dart
Executable File

import 'dart:async';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/tray.dart';
import 'package:fl_clash/common/window.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/action.dart';
import 'package:fl_clash/providers/app.dart';
import 'package:fl_clash/providers/state.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tray/tray.dart';
class TrayManager extends ConsumerStatefulWidget {
final Widget child;
const TrayManager({super.key, required this.child});
@override
ConsumerState<TrayManager> createState() => _TrayManagerState();
}
class _TrayManagerState extends ConsumerState<TrayManager> {
StreamSubscription<TrayEvent>? _subscription;
bool _isUpdating = false;
bool _hasPendingUpdate = false;
@override
void initState() {
super.initState();
_subscription = Tray.instance.events.listen(_handleTrayEvent);
ref.listenManual(trayStateProvider, (prev, next) {
if (prev != next) {
_requestUpdate();
}
});
ref.listenManual(loadedLocaleProvider, (prev, next) {
if (prev != null && prev != next) {
_requestUpdate();
}
});
if (system.isMacOS) {
ref.listenManual(trayDelaysProvider, (prev, next) {
if (!const DeepCollectionEquality().equals(prev, next)) {
_requestUpdate();
}
});
ref.listenManual(trayTitleStateProvider, (prev, next) {
if (prev != next) {
_reportFailure(
appTray?.updateTitle(
showTrayTitle: next.showTrayTitle,
traffic: next.traffic,
),
);
}
});
}
}
/// A delay test changes the menu per proxy, so updates in flight coalesce.
void _requestUpdate() {
if (_isUpdating) {
_hasPendingUpdate = true;
return;
}
_isUpdating = true;
_reportFailure(_drainUpdates());
}
Future<void> _drainUpdates() async {
try {
do {
_hasPendingUpdate = false;
await ref.read(systemActionProvider.notifier).updateTray();
} while (_hasPendingUpdate && mounted);
} finally {
_isUpdating = false;
if (_hasPendingUpdate && mounted) {
_requestUpdate();
}
}
}
void _reportFailure(Future<void>? operation) {
if (operation == null) {
return;
}
unawaited(
operation.onError<Object>((error, stackTrace) {
commonPrint.log(
'Tray operation failed: ${compactError(error)}',
logLevel: LogLevel.error,
);
}),
);
}
void _handleTrayEvent(TrayEvent event) {
switch (event) {
case TrayIconActivated():
window?.show();
case TrayMenuRequested():
_reportFailure(Tray.instance.openMenu());
case TrayMenuItemSelected():
break;
}
}
@override
Widget build(BuildContext context) {
return widget.child;
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
}