Files
FlClash/lib/common/function.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

161 lines
4.0 KiB
Dart

import 'dart:async';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
void _applyAndReport(dynamic tag, Function func, List<dynamic>? args) {
void report(Object error, StackTrace stackTrace) {
commonPrint.log(
'Scheduled task $tag failed: ${compactError(error)}, $stackTrace',
logLevel: LogLevel.warning,
);
}
try {
final result = Function.apply(func, args);
if (result is Future) {
result.then<void>((_) {}, onError: report);
}
} catch (error, stackTrace) {
report(error, stackTrace);
}
}
class Debouncer {
final Map<dynamic, Timer> _operations = {};
final Map<dynamic, ({Function func, List<dynamic>? args})> _pending = {};
void call(
dynamic tag,
Function func, {
List<dynamic>? args,
Duration? duration,
}) {
_operations.remove(tag)?.cancel();
_pending[tag] = (func: func, args: args);
_operations[tag] = Timer(duration ?? const Duration(milliseconds: 600), () {
_operations.remove(tag);
final pending = _pending.remove(tag);
if (pending == null) {
return;
}
_applyAndReport(tag, pending.func, pending.args);
});
}
void flush(dynamic tag) {
final timer = _operations.remove(tag);
timer?.cancel();
final pending = _pending.remove(tag);
if (timer == null || pending == null) {
return;
}
_applyAndReport(tag, pending.func, pending.args);
}
void cancel(dynamic tag) {
_operations.remove(tag)?.cancel();
_pending.remove(tag);
}
}
class SerialTaskScheduler {
Future<void> _serialTail = Future<void>.value();
Future<T> run<T>(Future<T> Function() task) {
final completer = Completer<T>();
_serialTail = _serialTail.then((_) async {
try {
completer.complete(await task());
} catch (error, stackTrace) {
completer.completeError(error, stackTrace);
}
});
return completer.future;
}
}
class Throttler {
final Map<dynamic, Timer> _operations = {};
bool call(
dynamic tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
bool fire = false,
}) {
if (_operations.containsKey(tag)) {
return true;
}
if (fire) {
_applyAndReport(tag, func, args);
_operations[tag] = Timer(duration, () {
_operations.remove(tag);
});
} else {
_operations[tag] = Timer(duration, () {
_applyAndReport(tag, func, args);
_operations.remove(tag);
});
}
return false;
}
void cancel(dynamic tag) {
_operations.remove(tag)?.cancel();
}
}
Future<T> retry<T>({
required Future<T> Function() task,
int maxAttempts = 3,
required bool Function(T res) retryIf,
Duration delay = midDuration,
}) async {
int attempts = 0;
while (attempts < maxAttempts) {
final res = await task();
attempts++;
if (!retryIf(res) || attempts >= maxAttempts) {
return res;
}
await Future.delayed(delay);
}
throw TimeoutException('retry gave up after $maxAttempts attempts');
}
final debouncer = Debouncer();
final throttler = Throttler();
/// Frames stop while the window is hidden, so a post-frame callback alone
/// would hold tray and hotkey work back until the window is shown again.
void runAfterFrame(VoidCallback callback) {
final binding = SchedulerBinding.instance;
if (!binding.framesEnabled) {
Timer.run(callback);
return;
}
binding.addPostFrameCallback((_) => callback());
binding.scheduleFrame();
}
FutureOr<T> handleWatch<T>({
required Function function,
required void Function() onStart,
required void Function(T data, int elapsedMilliseconds) onEnd,
}) async {
if (kDebugMode && watchExecution) {
onStart();
final stopwatch = Stopwatch()..start();
final res = await function();
stopwatch.stop();
onEnd(res, stopwatch.elapsedMilliseconds);
return res;
}
return await function();
}