Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions lib/common/compute.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,39 @@ List<Group> computeSort({
return List.of(proxies)..sort((a, b) => a.name.compareTo(b.name));
}

List<Proxy> moveSelectedProxyToFirst({
required List<Proxy> proxies,
required String? selectedProxyName,
}) {
if (selectedProxyName == null || selectedProxyName.isEmpty) {
return proxies;
}
return List<Proxy>.from(proxies)..sort((a, b) {
if (a.name == selectedProxyName && b.name != selectedProxyName) {
return -1;
}
if (b.name == selectedProxyName && a.name != selectedProxyName) {
return 1;
}
return 0;
});
}

return groups.map((group) {
final proxies = group.all;
final newProxies = switch (sortType) {
ProxiesSortType.none => proxies,
ProxiesSortType.delay => sortOfDelay(
groups: groups,
proxies: proxies,
delayMap: delayMap,
selectedMap: selectedMap,
testUrl: group.testUrl.takeFirstValid([defaultTestUrl]),
ProxiesSortType.delay => moveSelectedProxyToFirst(
proxies: sortOfDelay(
groups: groups,
proxies: proxies,
delayMap: delayMap,
selectedMap: selectedMap,
testUrl: group.testUrl.takeFirstValid([defaultTestUrl]),
),
selectedProxyName: group.getCurrentSelectedName(
selectedMap[group.name] ?? '',
),
),
ProxiesSortType.name => sortOfName(proxies),
};
Expand Down
300 changes: 287 additions & 13 deletions lib/common/tray.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';

import 'package:fl_clash/controller.dart';
Expand All @@ -14,6 +15,13 @@ import 'window.dart';

class Tray {
static Tray? _instance;
bool _keepMenuOpen = false;
bool _pendingReopenOnClose = false;
int _keepMenuOpenSessionId = 0;
final Set<String> _delayTriggeredGroups = {};
final Set<String> _testingProxyMenuKeys = {};
final Map<String, int> _proxyMenuItemIdMap = {};
final Map<String, int> _groupDelayActionItemIdMap = {};

Tray._internal();

Expand Down Expand Up @@ -69,6 +77,8 @@ class Tray {
tunEnable: trayState.tunEnable,
);
}
_proxyMenuItemIdMap.clear();
_groupDelayActionItemIdMap.clear();
List<MenuItem> menuItems = [];
final showMenuItem = MenuItem(
label: appLocalizations.show,
Expand Down Expand Up @@ -111,21 +121,50 @@ class Tray {
if (system.isMacOS) {
for (final group in trayState.groups) {
List<MenuItem> subMenuItems = [];
for (final proxy in group.all) {
subMenuItems.add(
MenuItem.checkbox(
label: proxy.name,
checked:
appController.getSelectedProxyName(group.name) == proxy.name,
onClick: (_) {
appController.updateCurrentSelectedMap(group.name, proxy.name);
appController.changeProxy(
groupName: group.name,
proxyName: proxy.name,
);
},
final testUrl = group.testUrl;
final selectedProxyName = appController.getSelectedProxyName(group.name);
final hasDelayResult =
_hasDelayResultForGroup(group) ||
_delayTriggeredGroups.contains(group.name);
final delayActionItem = MenuItem(
key: 'keep-open:delay-test:${group.name}',
label: _buildDelayTestActionLabel(
hasDelayResult: hasDelayResult,
),
onClick: (_) {
_startDelayTestAndKeepMenuOpen([group]);
},
);
subMenuItems.add(delayActionItem);
_groupDelayActionItemIdMap[group.name] = delayActionItem.id;
subMenuItems.add(MenuItem.separator());
final orderedProxies = _sortProxiesForTray(
proxies: group.all,
selectedProxyName: selectedProxyName,
);
for (final proxy in orderedProxies) {
final proxyItem = MenuItem.checkbox(
// 在 macOS 托盘菜单中直观展示当前测速结果。
label: _buildProxyMenuLabel(
proxy,
groupName: group.name,
testUrl: testUrl,
),
checked: selectedProxyName == proxy.name,
onClick: (_) {
appController.updateCurrentSelectedMap(group.name, proxy.name);
appController.changeProxy(
groupName: group.name,
proxyName: proxy.name,
);
},
);
subMenuItems.add(proxyItem);
_proxyMenuItemIdMap[_buildProxyKey(
groupName: group.name,
proxyName: proxy.name,
testUrl: testUrl,
)] = proxyItem.id;
}
menuItems.add(
MenuItem.submenu(
Expand Down Expand Up @@ -191,6 +230,10 @@ class Tray {
);
}
updateTrayTitle(showTrayTitle: trayState.showTrayTitle, traffic: traffic);
if (_keepMenuOpen) {
// 菜单刷新会导致系统收起,这里登记一次“下次关闭后重开”。
_pendingReopenOnClose = true;
}
}

Future<void> updateTrayTitle({
Expand All @@ -216,6 +259,237 @@ class Tray {

await Clipboard.setData(ClipboardData(text: cmdline));
}

void _startDelayTestAndKeepMenuOpen(List<Group> groups) {
final sessionId = ++_keepMenuOpenSessionId;
_keepMenuOpen = true;
_pendingReopenOnClose = false;
for (final group in groups) {
_delayTriggeredGroups.add(group.name);
unawaited(_updateDelayActionLabel(group));
for (final proxy in group.all) {
_testingProxyMenuKeys.add(
_buildProxyKey(
groupName: group.name,
proxyName: proxy.name,
testUrl: group.testUrl,
),
);
unawaited(
_updateProxyMenuLabel(
groupName: group.name,
proxyName: proxy.name,
testUrl: group.testUrl,
),
);
}
}
unawaited(() async {
try {
await appController.delayTestForTrayGroups(
groups,
refreshTrayOnProgress: false,
refreshTrayOnDone: false,
onDelayUpdated: (proxyName, testUrl) {
for (final group in groups) {
if (group.testUrl != testUrl) {
continue;
}
_testingProxyMenuKeys.remove(
_buildProxyKey(
groupName: group.name,
proxyName: proxyName,
testUrl: group.testUrl,
),
);
unawaited(
_updateProxyMenuLabel(
groupName: group.name,
proxyName: proxyName,
testUrl: group.testUrl,
),
);
}
},
);
} finally {
if (_keepMenuOpenSessionId == sessionId) {
_keepMenuOpen = false;
_pendingReopenOnClose = false;
}
for (final group in groups) {
for (final proxy in group.all) {
_testingProxyMenuKeys.remove(
_buildProxyKey(
groupName: group.name,
proxyName: proxy.name,
testUrl: group.testUrl,
),
);
unawaited(
_updateProxyMenuLabel(
groupName: group.name,
proxyName: proxy.name,
testUrl: group.testUrl,
),
);
}
}
}
}());
}

void handleMenuDidClose() {
if (!system.isMacOS || !_keepMenuOpen) {
return;
}
if (!_pendingReopenOnClose) {
_cancelKeepMenuOpen();
return;
}
_pendingReopenOnClose = false;
_reopenMenuIfSessionValid(_keepMenuOpenSessionId);
}

void _reopenMenuIfSessionValid(int sessionId) {
if (!system.isMacOS || !_keepMenuOpen || _keepMenuOpenSessionId != sessionId) {
return;
}
unawaited(trayManager.popUpContextMenu());
}

void _cancelKeepMenuOpen() {
_keepMenuOpen = false;
_pendingReopenOnClose = false;
}

List<Proxy> _sortProxiesForTray({
required List<Proxy> proxies,
required String? selectedProxyName,
}) {
if (selectedProxyName == null || selectedProxyName.isEmpty) {
return proxies;
}
final sortedProxies = List<Proxy>.from(proxies);
sortedProxies.sort((a, b) {
if (a.name == selectedProxyName && b.name != selectedProxyName) {
return -1;
}
if (b.name == selectedProxyName && a.name != selectedProxyName) {
return 1;
}
return 0;
});
return sortedProxies;
}

String _buildProxyKey({
required String groupName,
required String proxyName,
required String? testUrl,
}) {
return '$groupName|$proxyName|${testUrl ?? ''}';
}

Future<void> _updateDelayActionLabel(Group group) async {
final itemId = _groupDelayActionItemIdMap[group.name];
if (itemId == null) {
return;
}
await trayManager.updateMenuItemLabel(
id: itemId,
label: _buildDelayTestActionLabel(
hasDelayResult:
_hasDelayResultForGroup(group) ||
_delayTriggeredGroups.contains(group.name),
),
);
}

Future<void> _updateProxyMenuLabel({
required String groupName,
required String proxyName,
required String? testUrl,
}) async {
final itemId = _proxyMenuItemIdMap[_buildProxyKey(
groupName: groupName,
proxyName: proxyName,
testUrl: testUrl,
)];
if (itemId == null) {
return;
}
await trayManager.updateMenuItemLabel(
id: itemId,
label: _buildProxyMenuLabelByName(
proxyName,
testUrl: testUrl,
testingKey: _buildProxyKey(
groupName: groupName,
proxyName: proxyName,
testUrl: testUrl,
),
),
);
}

String _buildProxyMenuLabel(
Proxy proxy, {
required String groupName,
String? testUrl,
}) {
return _buildProxyMenuLabelByName(
proxy.name,
testUrl: testUrl,
testingKey: _buildProxyKey(
groupName: groupName,
proxyName: proxy.name,
testUrl: testUrl,
),
);
}

String _buildProxyMenuLabelByName(
String proxyName, {
String? testUrl,
String? testingKey,
}) {
if (testingKey != null && _testingProxyMenuKeys.contains(testingKey)) {
return '$proxyName (...)';
}
final delay = appController.getDelayForProxy(proxyName, testUrl: testUrl);
if (delay == null) {
return proxyName;
}
final delayText = switch (delay) {
0 => '...',
> 0 => '$delay ms',
_ => 'Timeout',
};
return '$proxyName ($delayText)';
}

bool _hasDelayResultForGroup(Group group) {
for (final proxy in group.all) {
final delay = appController.getDelayForProxy(
proxy.name,
testUrl: group.testUrl,
);
if (delay != null) {
return true;
}
}
return false;
}

String _buildDelayTestActionLabel({
required bool hasDelayResult,
}) {
if (hasDelayResult) {
return appLocalizations.retest;
}
return appLocalizations.delayTest;
}
}

final tray = system.isDesktop ? Tray() : null;
Loading