Backup before removing hiddify references
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -6,11 +5,8 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/widget/animated_visibility.dart';
|
||||
import 'package:hiddify/core/widget/shimmer_skeleton.dart';
|
||||
import 'package:hiddify/features/connection/model/connection_status.dart';
|
||||
import 'package:hiddify/features/proxy/active/active_proxy_notifier.dart';
|
||||
import 'package:hiddify/features/system_tray/notifier/system_tray_notifier.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
|
||||
class ActiveProxyDelayIndicator extends HookConsumerWidget {
|
||||
const ActiveProxyDelayIndicator({super.key});
|
||||
|
||||
@@ -95,7 +95,7 @@ class ActiveProxyNotifier extends _$ActiveProxyNotifier with AppLogger {
|
||||
final _urlTestThrottler = Throttler(const Duration(seconds: 2));
|
||||
|
||||
Future<void> urlTest(String groupTag_) async {
|
||||
var groupTag = groupTag_;
|
||||
final groupTag = groupTag_;
|
||||
_urlTestThrottler(
|
||||
() async {
|
||||
if (state case AsyncData()) {
|
||||
|
||||
@@ -133,7 +133,7 @@ class IPCountryFlag extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: Center(
|
||||
child: CircleFlag(
|
||||
countryCode.toLowerCase() == "ir" ? "ir-shir" : countryCode),
|
||||
countryCode.toLowerCase() == "ir" ? "ir-shir" : countryCode,),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -29,10 +29,33 @@ class ProxyItemEntity with _$ProxyItemEntity {
|
||||
}) = _ProxyItemEntity;
|
||||
|
||||
String get name => _sanitizedTag(tag);
|
||||
String? get selectedName =>
|
||||
selectedTag == null ? null : _sanitizedTag(selectedTag!);
|
||||
String? get selectedName => selectedTag == null ? null : _sanitizedTag(selectedTag!);
|
||||
bool get isVisible => !tag.contains("§hide§");
|
||||
|
||||
/// Извлекает код страны из названия прокси (emoji флаг или текст)
|
||||
String get countryCode {
|
||||
// Парсинг emoji флагов (🇩🇪 = U+1F1E6-1F1FF региональные индикаторы)
|
||||
final runes = name.runes.toList();
|
||||
if (runes.length >= 2) {
|
||||
final first = runes[0];
|
||||
final second = runes[1];
|
||||
// Проверка Regional Indicator Symbols
|
||||
if (first >= 0x1F1E6 && first <= 0x1F1FF &&
|
||||
second >= 0x1F1E6 && second <= 0x1F1FF) {
|
||||
final cc1 = String.fromCharCode(first - 0x1F1E6 + 65); // A-Z
|
||||
final cc2 = String.fromCharCode(second - 0x1F1E6 + 65);
|
||||
return cc1 + cc2;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: текстовые коды в начале или конце названия
|
||||
final match = RegExp(r'^([A-Z]{2})[-_\s]|[-_\s]([A-Z]{2})$|^\[([A-Z]{2})\]').firstMatch(name);
|
||||
if (match != null) {
|
||||
return match.group(1) ?? match.group(2) ?? match.group(3) ?? 'XX';
|
||||
}
|
||||
|
||||
return 'XX'; // Неизвестная страна
|
||||
}
|
||||
}
|
||||
|
||||
String _sanitizedTag(String tag) =>
|
||||
tag.replaceFirst(RegExp(r"\§[^]*"), "").trimRight();
|
||||
String _sanitizedTag(String tag) => tag.replaceFirst(RegExp(r"\§[^]*"), "").trimRight();
|
||||
|
||||
@@ -139,6 +139,10 @@ class ProxiesOverviewNotifier extends _$ProxiesOverviewNotifier with AppLogger {
|
||||
),
|
||||
],
|
||||
).copyWithPrevious(state);
|
||||
|
||||
// Автоматически запускаем URL test после переключения
|
||||
// чтобы обновить задержки для всех прокси
|
||||
await urlTest(groupTag);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/model/failures.dart';
|
||||
import 'package:hiddify/features/common/nested_app_bar.dart';
|
||||
import 'package:hiddify/features/proxy/model/proxy_entity.dart';
|
||||
import 'package:hiddify/features/proxy/overview/proxies_overview_notifier.dart';
|
||||
import 'package:hiddify/features/proxy/widget/proxy_tile.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
@@ -65,65 +66,78 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
||||
);
|
||||
}
|
||||
|
||||
final group = groups.first;
|
||||
// Находим группы "select" и "auto"
|
||||
final selectGroup = groups.firstWhere(
|
||||
(g) => g.type.name == 'selector',
|
||||
orElse: () => groups.first,
|
||||
);
|
||||
final autoGroup = groups.firstWhere(
|
||||
(g) => g.type.name == 'urltest',
|
||||
orElse: () => groups.last,
|
||||
);
|
||||
|
||||
// Текущий выбор из группы "select"
|
||||
final currentSelection = selectGroup.selected;
|
||||
|
||||
// Все прокси берём из группы "auto"
|
||||
final allProxies = autoGroup.items;
|
||||
|
||||
// Группировка прокси по странам
|
||||
final proxiesByCountry = <String, List<ProxyItemEntity>>{};
|
||||
for (final proxy in allProxies) {
|
||||
final country = proxy.countryCode;
|
||||
proxiesByCountry.putIfAbsent(country, () => []).add(proxy);
|
||||
}
|
||||
|
||||
// Сортировка стран по средней задержке
|
||||
final sortedCountries = proxiesByCountry.keys.toList()
|
||||
..sort((a, b) {
|
||||
final avgA = _averageDelay(proxiesByCountry[a]!);
|
||||
final avgB = _averageDelay(proxiesByCountry[b]!);
|
||||
return avgA.compareTo(avgB);
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
appBar,
|
||||
SliverLayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width = constraints.crossAxisExtent;
|
||||
if (!PlatformUtils.isDesktop && width < 648) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.only(bottom: 86),
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = group.items[index];
|
||||
return ProxyTile(
|
||||
proxy,
|
||||
selected: group.selected == proxy.tag,
|
||||
onSelect: () async {
|
||||
if (selectActiveProxyMutation.state.isInProgress) {
|
||||
return;
|
||||
}
|
||||
selectActiveProxyMutation.setFuture(
|
||||
notifier.changeProxy(group.tag, proxy.tag),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
itemCount: group.items.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverGrid.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: (width / 268).floor(),
|
||||
mainAxisExtent: 68,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final proxy = group.items[index];
|
||||
return ProxyTile(
|
||||
proxy,
|
||||
selected: group.selected == proxy.tag,
|
||||
onSelect: () async {
|
||||
if (selectActiveProxyMutation.state.isInProgress) {
|
||||
return;
|
||||
}
|
||||
selectActiveProxyMutation.setFuture(
|
||||
notifier.changeProxy(
|
||||
group.tag,
|
||||
proxy.tag,
|
||||
),
|
||||
);
|
||||
},
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(bottom: 86, left: 12, right: 12, top: 8),
|
||||
sliver: SliverList.builder(
|
||||
// +1 для глобальной карточки "Авто по всем"
|
||||
itemCount: 1 + sortedCountries.length,
|
||||
itemBuilder: (context, index) {
|
||||
// Первая карточка - "Авто по всем локациям"
|
||||
if (index == 0) {
|
||||
return _GlobalAutoCard(
|
||||
currentSelection: currentSelection,
|
||||
avgDelay: autoGroup.items.firstOrNull?.urlTestDelay.toDouble() ?? 999999.0,
|
||||
selectGroup: selectGroup,
|
||||
selectActiveProxyMutation: selectActiveProxyMutation,
|
||||
notifier: notifier,
|
||||
t: t,
|
||||
);
|
||||
},
|
||||
itemCount: group.items.length,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
// Остальные карточки - страны
|
||||
final countryIndex = index - 1;
|
||||
final countryCode = sortedCountries[countryIndex];
|
||||
final proxies = proxiesByCountry[countryCode]!;
|
||||
final avgDelay = _averageDelay(proxies);
|
||||
|
||||
return _CountryGroupCard(
|
||||
countryCode: countryCode,
|
||||
proxies: proxies,
|
||||
avgDelay: avgDelay,
|
||||
selectGroup: selectGroup,
|
||||
currentSelection: currentSelection,
|
||||
selectActiveProxyMutation: selectActiveProxyMutation,
|
||||
notifier: notifier,
|
||||
countryFlag: _countryFlag(countryCode),
|
||||
delayColor: _delayColor(context, avgDelay),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -150,7 +164,7 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
||||
],
|
||||
),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () async => notifier.urlTest(group.tag),
|
||||
onPressed: () async => notifier.urlTest(selectGroup.tag),
|
||||
tooltip: t.proxies.delayTestTooltip,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
@@ -189,4 +203,316 @@ class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
||||
return const Scaffold();
|
||||
}
|
||||
}
|
||||
|
||||
// Вычисление средней задержки
|
||||
double _averageDelay(List<ProxyItemEntity> proxies) {
|
||||
if (proxies.isEmpty) return 999999.0;
|
||||
final total = proxies.fold<int>(0, (sum, p) => sum + p.urlTestDelay);
|
||||
return total / proxies.length;
|
||||
}
|
||||
|
||||
// Генерация emoji флага из кода страны
|
||||
String _countryFlag(String countryCode) {
|
||||
if (countryCode == 'XX' || countryCode.length != 2) {
|
||||
return '❓'; // Неизвестная страна
|
||||
}
|
||||
final first = 0x1F1E6 + (countryCode.codeUnitAt(0) - 65);
|
||||
final second = 0x1F1E6 + (countryCode.codeUnitAt(1) - 65);
|
||||
return String.fromCharCodes([first, second]);
|
||||
}
|
||||
|
||||
// Цвет задержки
|
||||
Color _delayColor(BuildContext context, double delay) {
|
||||
return _getDelayColor(context, delay);
|
||||
}
|
||||
}
|
||||
|
||||
// Глобальная функция для определения цвета задержки
|
||||
Color _getDelayColor(BuildContext context, double delay) {
|
||||
if (delay >= 999999) return Colors.grey;
|
||||
|
||||
if (Theme.of(context).brightness == Brightness.dark) {
|
||||
return switch (delay) {
|
||||
< 800 => Colors.lightGreen,
|
||||
< 1500 => Colors.orange,
|
||||
_ => Colors.redAccent,
|
||||
};
|
||||
}
|
||||
|
||||
return switch (delay) {
|
||||
< 800 => Colors.green,
|
||||
< 1500 => Colors.deepOrangeAccent,
|
||||
_ => Colors.red,
|
||||
};
|
||||
}
|
||||
|
||||
// Виджет глобальной карточки "Авто по всем локациям"
|
||||
class _GlobalAutoCard extends StatelessWidget {
|
||||
final String currentSelection;
|
||||
final double avgDelay;
|
||||
final ProxyGroupEntity selectGroup;
|
||||
final ({
|
||||
AsyncMutation state,
|
||||
ValueChanged<Future<void>> setFuture,
|
||||
ValueChanged<void Function(Object error)> setOnFailure,
|
||||
}) selectActiveProxyMutation;
|
||||
final ProxiesOverviewNotifier notifier;
|
||||
final Translations t;
|
||||
|
||||
const _GlobalAutoCard({
|
||||
required this.currentSelection,
|
||||
required this.avgDelay,
|
||||
required this.selectGroup,
|
||||
required this.selectActiveProxyMutation,
|
||||
required this.notifier,
|
||||
required this.t,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSelected = currentSelection.toLowerCase() == 'auto';
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
color: isSelected ? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3) : null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: isSelected
|
||||
? BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
)
|
||||
: BorderSide.none,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
if (selectActiveProxyMutation.state.isInProgress) return;
|
||||
// Выбираем "auto" - автовыбор по всем странам
|
||||
selectActiveProxyMutation.setFuture(
|
||||
notifier.changeProxy(selectGroup.tag, 'auto'),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
// Иконка глобуса
|
||||
const Text('🌍', style: TextStyle(fontSize: 32)),
|
||||
const SizedBox(width: 16),
|
||||
// Название и описание
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
t.proxies.globalAuto,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (isSelected)
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
t.proxies.globalAutoDesc,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Задержка
|
||||
Text(
|
||||
avgDelay >= 999999 ? 'timeout' : '${avgDelay.toInt()}ms',
|
||||
style: TextStyle(
|
||||
color: _getDelayColor(context, avgDelay),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Виджет карточки группы по стране с раскрывающимся списком
|
||||
class _CountryGroupCard extends StatefulWidget {
|
||||
final String countryCode;
|
||||
final List<ProxyItemEntity> proxies;
|
||||
final double avgDelay;
|
||||
final ProxyGroupEntity selectGroup;
|
||||
final String currentSelection;
|
||||
final ({
|
||||
AsyncMutation state,
|
||||
ValueChanged<Future<void>> setFuture,
|
||||
ValueChanged<void Function(Object error)> setOnFailure,
|
||||
}) selectActiveProxyMutation;
|
||||
final ProxiesOverviewNotifier notifier;
|
||||
final String countryFlag;
|
||||
final Color delayColor;
|
||||
|
||||
const _CountryGroupCard({
|
||||
required this.countryCode,
|
||||
required this.proxies,
|
||||
required this.avgDelay,
|
||||
required this.selectGroup,
|
||||
required this.currentSelection,
|
||||
required this.selectActiveProxyMutation,
|
||||
required this.notifier,
|
||||
required this.countryFlag,
|
||||
required this.delayColor,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_CountryGroupCard> createState() => _CountryGroupCardState();
|
||||
}
|
||||
|
||||
class _CountryGroupCardState extends State<_CountryGroupCard> {
|
||||
bool _isExpanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Проверяем выбран ли один из прокси этой страны
|
||||
final selectedProxyInCountry = widget.proxies.any(
|
||||
(p) => p.tag == widget.currentSelection,
|
||||
);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
color: selectedProxyInCountry ? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3) : null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: selectedProxyInCountry
|
||||
? BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
)
|
||||
: BorderSide.none,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Основная карточка - клик выбирает первый прокси из этой страны
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
if (widget.selectActiveProxyMutation.state.isInProgress) return;
|
||||
|
||||
// Выбираем первый прокси из этой страны (обычно лучший)
|
||||
final firstProxy = widget.proxies.firstOrNull;
|
||||
if (firstProxy != null) {
|
||||
widget.selectActiveProxyMutation.setFuture(
|
||||
widget.notifier.changeProxy(widget.selectGroup.tag, firstProxy.tag),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
// Флаг
|
||||
Text(
|
||||
widget.countryFlag,
|
||||
style: const TextStyle(fontSize: 32),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Название и количество
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.countryCode == 'XX' ? 'Other' : widget.countryCode,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'(${widget.proxies.length})',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.countryCode == 'XX')
|
||||
Text(
|
||||
'Автовыбор из всех локаций',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Задержка
|
||||
Text(
|
||||
widget.avgDelay >= 999999 ? 'timeout' : '${widget.avgDelay.toInt()}ms',
|
||||
style: TextStyle(
|
||||
color: widget.delayColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
|
||||
// Кнопка раскрытия (треугольник) - только если не XX
|
||||
if (widget.countryCode != 'XX')
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Раскрывающийся список прокси
|
||||
if (_isExpanded) ...[
|
||||
// Все прокси из этой страны
|
||||
...widget.proxies.map((proxy) {
|
||||
return ProxyTile(
|
||||
proxy,
|
||||
selected: widget.currentSelection == proxy.tag,
|
||||
onSelect: () async {
|
||||
if (widget.selectActiveProxyMutation.state.isInProgress) {
|
||||
return;
|
||||
}
|
||||
widget.selectActiveProxyMutation.setFuture(
|
||||
widget.notifier.changeProxy(widget.selectGroup.tag, proxy.tag),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class ProxyTile extends HookConsumerWidget with PresLogger {
|
||||
title: Text(
|
||||
proxy.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontFamily: FontFamily.emoji),
|
||||
style: const TextStyle(fontFamily: FontFamily.emoji),
|
||||
),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
|
||||
Reference in New Issue
Block a user