dedicated screen for tor countries
This commit is contained in:
@@ -70,6 +70,7 @@ import 'package:weblibre/features/settings/presentation/screens/tabs_behavior_se
|
||||
import 'package:weblibre/features/settings/presentation/screens/tracking_protection_exceptions.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart';
|
||||
import 'package:weblibre/features/tor/presentation/screens/country_picker.dart';
|
||||
import 'package:weblibre/features/tor/presentation/screens/tor_proxy.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/dialogs/select_profile.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_backup.dart';
|
||||
|
||||
@@ -1795,6 +1795,13 @@ RouteBase get $torProxyRoute => GoRouteData.$route(
|
||||
path: '/tor',
|
||||
name: 'TorProxyRoute',
|
||||
factory: $TorProxyRoute._fromState,
|
||||
routes: [
|
||||
GoRouteData.$route(
|
||||
path: 'country_picker',
|
||||
name: 'TorCountryPickerRoute',
|
||||
factory: $TorCountryPickerRoute._fromState,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
mixin $TorProxyRoute on GoRouteData {
|
||||
@@ -1816,3 +1823,34 @@ mixin $TorProxyRoute on GoRouteData {
|
||||
@override
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
mixin $TorCountryPickerRoute on GoRouteData {
|
||||
static TorCountryPickerRoute _fromState(GoRouterState state) =>
|
||||
TorCountryPickerRoute(
|
||||
title: state.uri.queryParameters['title']!,
|
||||
$extra: state.extra as String?,
|
||||
);
|
||||
|
||||
TorCountryPickerRoute get _self => this as TorCountryPickerRoute;
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location(
|
||||
'/tor/country_picker',
|
||||
queryParams: {'title': _self.title},
|
||||
);
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location, extra: _self.$extra);
|
||||
|
||||
@override
|
||||
Future<T?> push<T>(BuildContext context) =>
|
||||
context.push<T>(location, extra: _self.$extra);
|
||||
|
||||
@override
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location, extra: _self.$extra);
|
||||
|
||||
@override
|
||||
void replace(BuildContext context) =>
|
||||
context.replace(location, extra: _self.$extra);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,16 @@
|
||||
*/
|
||||
part of 'routes.dart';
|
||||
|
||||
@TypedGoRoute<TorProxyRoute>(name: 'TorProxyRoute', path: '/tor')
|
||||
@TypedGoRoute<TorProxyRoute>(
|
||||
name: 'TorProxyRoute',
|
||||
path: '/tor',
|
||||
routes: [
|
||||
TypedGoRoute<TorCountryPickerRoute>(
|
||||
name: 'TorCountryPickerRoute',
|
||||
path: 'country_picker',
|
||||
),
|
||||
],
|
||||
)
|
||||
class TorProxyRoute extends GoRouteData with $TorProxyRoute {
|
||||
const TorProxyRoute();
|
||||
|
||||
@@ -28,3 +37,18 @@ class TorProxyRoute extends GoRouteData with $TorProxyRoute {
|
||||
return const TorProxyScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class TorCountryPickerRoute extends GoRouteData with $TorCountryPickerRoute {
|
||||
final String title;
|
||||
final String? $extra;
|
||||
|
||||
const TorCountryPickerRoute({required this.title, this.$extra});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return CountryPickerScreen(
|
||||
title: title,
|
||||
selectedCountryCode: $extra,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:country_codes/country_codes.dart';
|
||||
import 'package:country_flags/country_flags.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Sentinel value returned when the user selects "Automatic" (no country).
|
||||
/// Distinguished from `null` which means the user navigated back without
|
||||
/// making a selection.
|
||||
const automaticCountry = '';
|
||||
|
||||
class CountryPickerScreen extends HookWidget {
|
||||
const CountryPickerScreen({
|
||||
required this.title,
|
||||
this.selectedCountryCode,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String? selectedCountryCode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final searchController = useTextEditingController();
|
||||
final searchQuery = useState('');
|
||||
|
||||
final countries = useMemoized(() {
|
||||
return CountryCodes.countryCodes().map((country) {
|
||||
final label =
|
||||
country.localizedName ??
|
||||
country.name ??
|
||||
country.alpha2Code ??
|
||||
country.countryCode ??
|
||||
'Unnamed Country';
|
||||
return (alpha2Code: country.alpha2Code, label: label);
|
||||
}).toList()
|
||||
..sort((a, b) => a.label.compareTo(b.label));
|
||||
});
|
||||
|
||||
final filteredCountries = useMemoized(
|
||||
() {
|
||||
if (searchQuery.value.isEmpty) return countries;
|
||||
final query = searchQuery.value.toLowerCase();
|
||||
return countries
|
||||
.where((c) => c.label.toLowerCase().contains(query))
|
||||
.toList();
|
||||
},
|
||||
[searchQuery.value, countries],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(56),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search countries...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: searchQuery.value.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
searchQuery.value = '';
|
||||
},
|
||||
)
|
||||
: null,
|
||||
filled: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||
),
|
||||
onChanged: (value) => searchQuery.value = value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemCount: filteredCountries.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
final isSelected = selectedCountryCode == null;
|
||||
return ListTile(
|
||||
leading: const SizedBox(
|
||||
width: 32,
|
||||
height: 24,
|
||||
child: Center(child: Icon(Icons.public)),
|
||||
),
|
||||
title: const Text('Automatic'),
|
||||
trailing: isSelected ? const Icon(Icons.check) : null,
|
||||
onTap: () => context.pop(automaticCountry),
|
||||
);
|
||||
}
|
||||
|
||||
final country = filteredCountries[index - 1];
|
||||
final isSelected = country.alpha2Code == selectedCountryCode;
|
||||
|
||||
return ListTile(
|
||||
leading: country.alpha2Code != null
|
||||
? CountryFlag.fromCountryCode(
|
||||
country.alpha2Code!,
|
||||
theme: const EmojiTheme(size: 28),
|
||||
)
|
||||
: const SizedBox(width: 32),
|
||||
title: Text(country.label),
|
||||
trailing: isSelected ? const Icon(Icons.check) : null,
|
||||
onTap: () => context.pop(country.alpha2Code),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:country_codes/country_codes.dart';
|
||||
import 'package:country_flags/country_flags.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@@ -27,6 +26,8 @@ import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/core/design/app_colors.dart';
|
||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/tor/presentation/screens/country_picker.dart';
|
||||
import 'package:weblibre/features/user/data/models/tor_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/tor_settings.dart';
|
||||
import 'package:weblibre/presentation/hooks/on_initialization.dart';
|
||||
@@ -92,29 +93,6 @@ class TorProxyScreen extends HookConsumerWidget {
|
||||
}
|
||||
});
|
||||
|
||||
final countryDropdownEntries = useMemoized(
|
||||
() => [
|
||||
const DropdownMenuEntry(value: null, label: 'Automatic'),
|
||||
...CountryCodes.countryCodes().map((country) {
|
||||
final label =
|
||||
country.localizedName ??
|
||||
country.name ??
|
||||
country.alpha2Code ??
|
||||
country.countryCode ??
|
||||
'Unnamed Country';
|
||||
|
||||
return DropdownMenuEntry(
|
||||
value: country.alpha2Code,
|
||||
label: label,
|
||||
labelWidget: Text(
|
||||
label,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
body: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
@@ -485,111 +463,71 @@ class TorProxyScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16.0,
|
||||
right: 24.0,
|
||||
top: 8.0,
|
||||
),
|
||||
child: DropdownMenu(
|
||||
enabled: !torIsBusy,
|
||||
initialSelection: torSettings.entryNodeCountry,
|
||||
menuHeight: 400,
|
||||
requestFocusOnTap: true,
|
||||
label: const Text('Entry Country'),
|
||||
textStyle: const TextStyle(color: Colors.white),
|
||||
leadingIcon: torSettings.entryNodeCountry.mapNotNull(
|
||||
(code) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: CountryFlag.fromCountryCode(
|
||||
ListTile(
|
||||
enabled: !torIsBusy,
|
||||
leading: torSettings.entryNodeCountry.mapNotNull(
|
||||
(code) => CountryFlag.fromCountryCode(
|
||||
code,
|
||||
theme: const EmojiTheme(size: 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
trailingIcon: const Icon(
|
||||
MdiIcons.chevronDown,
|
||||
color: Colors.white,
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
labelStyle: TextStyle(color: Colors.white),
|
||||
iconColor: Colors.white,
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white),
|
||||
),
|
||||
),
|
||||
// menuStyle: MenuStyle(
|
||||
// backgroundColor:
|
||||
// WidgetStateProperty.all<Color>(
|
||||
// AppColors.torPurple,
|
||||
// ),
|
||||
// ),
|
||||
dropdownMenuEntries: countryDropdownEntries,
|
||||
onSelected: (value) async {
|
||||
await ref
|
||||
.read(saveTorSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) => currentSettings.copyWith
|
||||
.entryNodeCountry(value),
|
||||
);
|
||||
},
|
||||
) ??
|
||||
const Icon(Icons.public, color: Colors.white),
|
||||
title: const Text('Entry Country'),
|
||||
subtitle: Text(
|
||||
torSettings.entryNodeCountry ?? 'Automatic',
|
||||
),
|
||||
trailing: const Icon(
|
||||
MdiIcons.chevronRight,
|
||||
color: Colors.white,
|
||||
),
|
||||
onTap: () async {
|
||||
final result = await TorCountryPickerRoute(
|
||||
title: 'Entry Country',
|
||||
$extra: torSettings.entryNodeCountry,
|
||||
).push<String>(context);
|
||||
if (result == null) return;
|
||||
final value =
|
||||
result == automaticCountry ? null : result;
|
||||
await ref
|
||||
.read(saveTorSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) => currentSettings.copyWith
|
||||
.entryNodeCountry(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16.0,
|
||||
right: 24.0,
|
||||
top: 8.0,
|
||||
),
|
||||
child: DropdownMenu(
|
||||
enabled: !torIsBusy,
|
||||
initialSelection: torSettings.exitNodeCountry,
|
||||
menuHeight: 400,
|
||||
requestFocusOnTap: true,
|
||||
label: const Text('Exit Country'),
|
||||
textStyle: const TextStyle(color: Colors.white),
|
||||
leadingIcon: torSettings.exitNodeCountry.mapNotNull(
|
||||
(code) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: CountryFlag.fromCountryCode(
|
||||
ListTile(
|
||||
enabled: !torIsBusy,
|
||||
leading: torSettings.exitNodeCountry.mapNotNull(
|
||||
(code) => CountryFlag.fromCountryCode(
|
||||
code,
|
||||
theme: const EmojiTheme(size: 28),
|
||||
),
|
||||
),
|
||||
),
|
||||
trailingIcon: const Icon(
|
||||
MdiIcons.chevronDown,
|
||||
color: Colors.white,
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
labelStyle: TextStyle(color: Colors.white),
|
||||
iconColor: Colors.white,
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white),
|
||||
),
|
||||
),
|
||||
// menuStyle: MenuStyle(
|
||||
// backgroundColor:
|
||||
// WidgetStateProperty.all<Color>(
|
||||
// AppColors.torPurple,
|
||||
// ),
|
||||
// ),
|
||||
dropdownMenuEntries: countryDropdownEntries,
|
||||
onSelected: (value) async {
|
||||
await ref
|
||||
.read(saveTorSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) => currentSettings.copyWith
|
||||
.exitNodeCountry(value),
|
||||
);
|
||||
},
|
||||
) ??
|
||||
const Icon(Icons.public, color: Colors.white),
|
||||
title: const Text('Exit Country'),
|
||||
subtitle: Text(
|
||||
torSettings.exitNodeCountry ?? 'Automatic',
|
||||
),
|
||||
trailing: const Icon(
|
||||
MdiIcons.chevronRight,
|
||||
color: Colors.white,
|
||||
),
|
||||
onTap: () async {
|
||||
final result = await TorCountryPickerRoute(
|
||||
title: 'Exit Country',
|
||||
$extra: torSettings.exitNodeCountry,
|
||||
).push<String>(context);
|
||||
if (result == null) return;
|
||||
final value =
|
||||
result == automaticCountry ? null : result;
|
||||
await ref
|
||||
.read(saveTorSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) => currentSettings.copyWith
|
||||
.exitNodeCountry(value),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user