2024-02-15 15:23:02 +03:30
|
|
|
|
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
2023-07-06 17:18:41 +03:30
|
|
|
|
import 'package:flutter/material.dart';
|
2026-01-17 13:09:20 +03:00
|
|
|
|
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
|
|
|
|
|
|
import 'package:umbrix/core/localization/translations.dart';
|
|
|
|
|
|
import 'package:umbrix/core/model/failures.dart';
|
|
|
|
|
|
import 'package:umbrix/core/router/routes.dart';
|
|
|
|
|
|
import 'package:umbrix/features/common/nested_app_bar.dart';
|
|
|
|
|
|
import 'package:umbrix/features/proxy/model/proxy_entity.dart';
|
|
|
|
|
|
import 'package:umbrix/features/proxy/overview/proxies_overview_notifier.dart';
|
|
|
|
|
|
import 'package:umbrix/features/proxy/widget/proxy_tile.dart';
|
|
|
|
|
|
import 'package:umbrix/utils/utils.dart';
|
2023-07-06 17:18:41 +03:30
|
|
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
|
|
|
|
|
2023-12-01 12:56:24 +03:30
|
|
|
|
class ProxiesOverviewPage extends HookConsumerWidget with PresLogger {
|
|
|
|
|
|
const ProxiesOverviewPage({super.key});
|
2023-07-06 17:18:41 +03:30
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
|
|
|
|
final t = ref.watch(translationsProvider);
|
|
|
|
|
|
|
2026-01-17 13:09:20 +03:00
|
|
|
|
final interceptBackToHome = !PlatformUtils.isDesktop;
|
|
|
|
|
|
Widget withBackToHome(Widget child) {
|
|
|
|
|
|
return PopScope<void>(
|
|
|
|
|
|
canPop: !interceptBackToHome,
|
|
|
|
|
|
onPopInvokedWithResult: (didPop, result) {
|
|
|
|
|
|
if (!didPop && interceptBackToHome) {
|
|
|
|
|
|
const HomeRoute().go(context);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
child: child,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-12-01 12:56:24 +03:30
|
|
|
|
final asyncProxies = ref.watch(proxiesOverviewNotifierProvider);
|
|
|
|
|
|
final notifier = ref.watch(proxiesOverviewNotifierProvider.notifier);
|
2023-09-17 14:24:25 +03:30
|
|
|
|
final sortBy = ref.watch(proxiesSortNotifierProvider);
|
2023-07-06 17:18:41 +03:30
|
|
|
|
|
|
|
|
|
|
final selectActiveProxyMutation = useMutation(
|
2025-12-26 02:39:35 +03:00
|
|
|
|
initialOnFailure: (error) => CustomToast.error(t.presentShortError(error)).show(context),
|
2023-07-06 17:18:41 +03:30
|
|
|
|
);
|
|
|
|
|
|
|
2023-12-31 10:49:37 +03:30
|
|
|
|
final appBar = NestedAppBar(
|
|
|
|
|
|
title: Text(t.proxies.pageTitle),
|
|
|
|
|
|
actions: [
|
|
|
|
|
|
PopupMenuButton<ProxiesSort>(
|
|
|
|
|
|
initialValue: sortBy,
|
|
|
|
|
|
onSelected: ref.read(proxiesSortNotifierProvider.notifier).update,
|
2024-02-15 15:23:02 +03:30
|
|
|
|
icon: const Icon(FluentIcons.arrow_sort_24_regular),
|
2023-12-31 10:49:37 +03:30
|
|
|
|
tooltip: t.proxies.sortTooltip,
|
|
|
|
|
|
itemBuilder: (context) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
...ProxiesSort.values.map(
|
|
|
|
|
|
(e) => PopupMenuItem(
|
|
|
|
|
|
value: e,
|
|
|
|
|
|
child: Text(e.present(t)),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
];
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2023-07-06 17:18:41 +03:30
|
|
|
|
switch (asyncProxies) {
|
2023-08-25 13:29:39 +03:30
|
|
|
|
case AsyncData(value: final groups):
|
|
|
|
|
|
if (groups.isEmpty) {
|
2026-01-17 13:09:20 +03:00
|
|
|
|
return withBackToHome(
|
|
|
|
|
|
Scaffold(
|
|
|
|
|
|
body: CustomScrollView(
|
|
|
|
|
|
slivers: [
|
|
|
|
|
|
appBar,
|
|
|
|
|
|
SliverFillRemaining(
|
|
|
|
|
|
child: Column(
|
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text(t.proxies.emptyProxiesMsg),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2023-07-06 17:18:41 +03:30
|
|
|
|
),
|
2026-01-17 13:09:20 +03:00
|
|
|
|
],
|
|
|
|
|
|
),
|
2023-07-06 17:18:41 +03:30
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 12:28:40 +03:00
|
|
|
|
// Находим группы "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);
|
|
|
|
|
|
});
|
2023-08-25 13:29:39 +03:30
|
|
|
|
|
2026-01-17 13:09:20 +03:00
|
|
|
|
return withBackToHome(
|
|
|
|
|
|
Scaffold(
|
|
|
|
|
|
body: CustomScrollView(
|
|
|
|
|
|
slivers: [
|
|
|
|
|
|
appBar,
|
|
|
|
|
|
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,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Остальные карточки - страны
|
|
|
|
|
|
final countryIndex = index - 1;
|
|
|
|
|
|
final countryCode = sortedCountries[countryIndex];
|
|
|
|
|
|
final proxies = proxiesByCountry[countryCode]!;
|
|
|
|
|
|
final avgDelay = _averageDelay(proxies);
|
|
|
|
|
|
|
|
|
|
|
|
return _CountryGroupCard(
|
|
|
|
|
|
countryCode: countryCode,
|
|
|
|
|
|
proxies: proxies,
|
|
|
|
|
|
avgDelay: avgDelay,
|
2026-01-15 12:28:40 +03:00
|
|
|
|
selectGroup: selectGroup,
|
2026-01-17 13:09:20 +03:00
|
|
|
|
currentSelection: currentSelection,
|
2026-01-15 12:28:40 +03:00
|
|
|
|
selectActiveProxyMutation: selectActiveProxyMutation,
|
|
|
|
|
|
notifier: notifier,
|
2026-01-17 13:09:20 +03:00
|
|
|
|
countryFlag: _countryFlag(countryCode),
|
|
|
|
|
|
delayColor: _delayColor(context, avgDelay),
|
2023-08-25 13:29:39 +03:30
|
|
|
|
);
|
2026-01-17 13:09:20 +03:00
|
|
|
|
},
|
|
|
|
|
|
),
|
2026-01-15 12:28:40 +03:00
|
|
|
|
),
|
2026-01-17 13:09:20 +03:00
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
floatingActionButton: Builder(
|
|
|
|
|
|
builder: (context) {
|
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
return Container(
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
|
colors: [
|
|
|
|
|
|
theme.colorScheme.primary,
|
|
|
|
|
|
theme.colorScheme.primary.withOpacity(0.8),
|
|
|
|
|
|
],
|
|
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
|
|
),
|
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
|
boxShadow: [
|
|
|
|
|
|
BoxShadow(
|
|
|
|
|
|
color: theme.colorScheme.primary.withOpacity(0.4),
|
|
|
|
|
|
blurRadius: 12,
|
|
|
|
|
|
offset: const Offset(0, 4),
|
|
|
|
|
|
),
|
2025-12-26 02:47:20 +03:00
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-01-17 13:09:20 +03:00
|
|
|
|
child: FloatingActionButton(
|
|
|
|
|
|
onPressed: () async => notifier.urlTest(selectGroup.tag),
|
|
|
|
|
|
tooltip: t.proxies.delayTestTooltip,
|
|
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
|
|
elevation: 0,
|
|
|
|
|
|
child: const Icon(FluentIcons.arrow_clockwise_24_filled),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
2023-07-06 17:18:41 +03:30
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
case AsyncError(:final error):
|
2026-01-17 13:09:20 +03:00
|
|
|
|
return withBackToHome(
|
|
|
|
|
|
Scaffold(
|
|
|
|
|
|
body: CustomScrollView(
|
|
|
|
|
|
slivers: [
|
|
|
|
|
|
appBar,
|
|
|
|
|
|
SliverErrorBodyPlaceholder(
|
|
|
|
|
|
t.presentShortError(error),
|
|
|
|
|
|
icon: null,
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2023-07-06 17:18:41 +03:30
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
case AsyncLoading():
|
2026-01-17 13:09:20 +03:00
|
|
|
|
return withBackToHome(
|
|
|
|
|
|
Scaffold(
|
|
|
|
|
|
body: CustomScrollView(
|
|
|
|
|
|
slivers: [
|
|
|
|
|
|
appBar,
|
|
|
|
|
|
const SliverLoadingBodyPlaceholder(),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2023-07-06 17:18:41 +03:30
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: remove
|
|
|
|
|
|
default:
|
|
|
|
|
|
return const Scaffold();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-15 12:28:40 +03:00
|
|
|
|
|
|
|
|
|
|
// Вычисление средней задержки
|
|
|
|
|
|
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),
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}),
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2023-07-06 17:18:41 +03:30
|
|
|
|
}
|