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/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.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.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/tor/presentation/screens/tor_proxy.dart';
|
||||||
import 'package:weblibre/features/user/domain/presentation/dialogs/select_profile.dart';
|
import 'package:weblibre/features/user/domain/presentation/dialogs/select_profile.dart';
|
||||||
import 'package:weblibre/features/user/domain/presentation/screens/profile_backup.dart';
|
import 'package:weblibre/features/user/domain/presentation/screens/profile_backup.dart';
|
||||||
|
|||||||
@@ -1795,6 +1795,13 @@ RouteBase get $torProxyRoute => GoRouteData.$route(
|
|||||||
path: '/tor',
|
path: '/tor',
|
||||||
name: 'TorProxyRoute',
|
name: 'TorProxyRoute',
|
||||||
factory: $TorProxyRoute._fromState,
|
factory: $TorProxyRoute._fromState,
|
||||||
|
routes: [
|
||||||
|
GoRouteData.$route(
|
||||||
|
path: 'country_picker',
|
||||||
|
name: 'TorCountryPickerRoute',
|
||||||
|
factory: $TorCountryPickerRoute._fromState,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
mixin $TorProxyRoute on GoRouteData {
|
mixin $TorProxyRoute on GoRouteData {
|
||||||
@@ -1816,3 +1823,34 @@ mixin $TorProxyRoute on GoRouteData {
|
|||||||
@override
|
@override
|
||||||
void replace(BuildContext context) => context.replace(location);
|
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';
|
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 {
|
class TorProxyRoute extends GoRouteData with $TorProxyRoute {
|
||||||
const TorProxyRoute();
|
const TorProxyRoute();
|
||||||
|
|
||||||
@@ -28,3 +37,18 @@ class TorProxyRoute extends GoRouteData with $TorProxyRoute {
|
|||||||
return const TorProxyScreen();
|
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
|
* 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/>.
|
* 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:country_flags/country_flags.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.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/core/design/app_colors.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||||
import 'package:weblibre/features/tor/domain/services/tor_proxy.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/data/models/tor_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/tor_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/tor_settings.dart';
|
||||||
import 'package:weblibre/presentation/hooks/on_initialization.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(
|
return Scaffold(
|
||||||
body: Theme(
|
body: Theme(
|
||||||
data: Theme.of(context).copyWith(
|
data: Theme.of(context).copyWith(
|
||||||
@@ -485,111 +463,71 @@ class TorProxyScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
ListTile(
|
||||||
padding: const EdgeInsets.only(
|
enabled: !torIsBusy,
|
||||||
left: 16.0,
|
leading: torSettings.entryNodeCountry.mapNotNull(
|
||||||
right: 24.0,
|
(code) => CountryFlag.fromCountryCode(
|
||||||
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(
|
|
||||||
code,
|
code,
|
||||||
theme: const EmojiTheme(size: 28),
|
theme: const EmojiTheme(size: 28),
|
||||||
),
|
),
|
||||||
),
|
) ??
|
||||||
),
|
const Icon(Icons.public, color: Colors.white),
|
||||||
trailingIcon: const Icon(
|
title: const Text('Entry Country'),
|
||||||
MdiIcons.chevronDown,
|
subtitle: Text(
|
||||||
color: Colors.white,
|
torSettings.entryNodeCountry ?? 'Automatic',
|
||||||
),
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
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(
|
ListTile(
|
||||||
padding: const EdgeInsets.only(
|
enabled: !torIsBusy,
|
||||||
left: 16.0,
|
leading: torSettings.exitNodeCountry.mapNotNull(
|
||||||
right: 24.0,
|
(code) => CountryFlag.fromCountryCode(
|
||||||
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(
|
|
||||||
code,
|
code,
|
||||||
theme: const EmojiTheme(size: 28),
|
theme: const EmojiTheme(size: 28),
|
||||||
),
|
),
|
||||||
),
|
) ??
|
||||||
),
|
const Icon(Icons.public, color: Colors.white),
|
||||||
trailingIcon: const Icon(
|
title: const Text('Exit Country'),
|
||||||
MdiIcons.chevronDown,
|
subtitle: Text(
|
||||||
color: Colors.white,
|
torSettings.exitNodeCountry ?? 'Automatic',
|
||||||
),
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
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