Backup before removing hiddify references

This commit is contained in:
Hiddify User
2026-01-15 12:28:40 +03:00
parent f54603d129
commit 36d9e31236
231 changed files with 6648 additions and 1832 deletions

View File

@@ -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});

View File

@@ -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()) {

View File

@@ -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,),
),
),
);

View File

@@ -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();

View File

@@ -139,6 +139,10 @@ class ProxiesOverviewNotifier extends _$ProxiesOverviewNotifier with AppLogger {
),
],
).copyWithPrevious(state);
// Автоматически запускаем URL test после переключения
// чтобы обновить задержки для всех прокси
await urlTest(groupTag);
}
}

View File

@@ -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),
);
},
);
}),
],
],
),
);
}
}

View File

@@ -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),