prepare for multiple apps

This commit is contained in:
Fabian Freund
2026-04-06 12:23:11 +02:00
parent bd1600e8dc
commit 5afc323f04
904 changed files with 29 additions and 29 deletions
@@ -0,0 +1,65 @@
/*
* 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:flutter/widgets.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/geckoview/domain/controllers/overlay.dart';
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
import 'package:weblibre/features/tor/presentation/widgets/tor_notification.dart';
part 'start_tor_proxy.g.dart';
@Riverpod(keepAlive: true)
class StartProxyController extends _$StartProxyController {
Future<bool> shouldPromptProxyStart() async {
final currentStatus = await ref
.read(torProxyServiceProvider.notifier)
.requestSync();
return !currentStatus.isRunning;
}
Future<void> startProxy() async {
if (state) return;
state = true;
try {
final connection = ref
.read(torProxyServiceProvider.notifier)
.startOrReconfigure(reconfigureIfRunning: false);
ref
.read(overlayControllerProvider.notifier)
.show(
(context) =>
const Positioned(top: 0, left: 0, child: TorNotification()),
);
await connection;
} finally {
state = false;
}
}
@override
bool build() {
return false;
}
}
@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'start_tor_proxy.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(StartProxyController)
final startProxyControllerProvider = StartProxyControllerProvider._();
final class StartProxyControllerProvider
extends $NotifierProvider<StartProxyController, bool> {
StartProxyControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'startProxyControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$startProxyControllerHash();
@$internal
@override
StartProxyController create() => StartProxyController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$startProxyControllerHash() =>
r'899b585bf7f220251ac92f11c79537cc07723241';
abstract class _$StartProxyController extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,139 @@
/*
* 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({
super.key,
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: EdgeInsets.zero,
),
onChanged: (value) => searchQuery.value = value,
),
),
),
),
body: SafeArea(
child: 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),
);
},
),
),
);
}
}
@@ -0,0 +1,581 @@
/*
* 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_flags/country_flags.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:nullability/nullability.dart';
import 'package:weblibre/core/design/app_colors.dart';
import 'package:weblibre/core/routing/routes.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/presentation/screens/country_picker.dart';
import 'package:weblibre/features/user/data/models/tor_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/tor_settings.dart';
import 'package:weblibre/presentation/hooks/on_initialization.dart';
import 'package:weblibre/utils/ui_helper.dart';
class TorProxyScreen extends HookConsumerWidget {
const TorProxyScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appColors = AppColors.of(context);
final bootstrapProgress = ref.watch(
torProxyServiceProvider.select(
(value) => value.value?.bootstrapProgress ?? 0,
),
);
final torPendingRequest = useState<bool?>(null);
ref.listen(torProxyServiceProvider, (previous, next) {
if (next.hasValue && torPendingRequest.value != null) {
if (next.requireValue.isRunning != previous?.value?.isRunning ||
next.requireValue.bootstrapProgress !=
previous?.value?.bootstrapProgress) {
if (torPendingRequest.value == true) {
if (next.requireValue.bootstrapProgress > 0) {
torPendingRequest.value = null;
}
} else {
torPendingRequest.value = null;
}
}
}
});
final torIsRunning = ref.watch(
torProxyServiceProvider.select(
(value) => value.value?.isRunning ?? false,
),
);
final torIsBootstrapped = ref.watch(
torProxyServiceProvider.select(
(value) => value.value?.bootstrapProgress == 100,
),
);
final torIsBusy =
torPendingRequest.value != null ||
bootstrapProgress > 0 && bootstrapProgress < 100;
final torSettings = ref.watch(torSettingsWithDefaultsProvider);
final showContainerUi = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.showContainerUi),
);
useOnInitialization(() async {
await ref.read(torProxyServiceProvider.notifier).requestSync();
});
ref.listen(torSettingsRepositoryProvider, (previous, next) async {
final torService = ref.read(torProxyServiceProvider.notifier);
final currentStatus = await torService.requestSync();
if (currentStatus.isRunning) {
await torService.startOrReconfigure(reconfigureIfRunning: true);
}
});
return Scaffold(
body: Theme(
data: Theme.of(context).copyWith(
listTileTheme: ListTileTheme.of(
context,
).copyWith(iconColor: Colors.white, textColor: Colors.white),
switchTheme: SwitchTheme.of(context).copyWith(
trackColor: WidgetStateProperty.resolveWith<Color?>((
Set<WidgetState> states,
) {
if (states.isEmpty) {
return appColors.torBackgroundGrey;
}
return null; // Use the default color.
}),
trackOutlineColor: WidgetStateProperty.resolveWith<Color?>((
Set<WidgetState> states,
) {
if (states.isEmpty) {
return Colors.white;
}
return null; // Use the default color.
}),
),
radioTheme: RadioTheme.of(context).copyWith(
fillColor: WidgetStateColor.resolveWith((states) {
return Colors.white;
}),
),
checkboxTheme: CheckboxTheme.of(context).copyWith(
fillColor: WidgetStateColor.resolveWith((states) {
return Colors.white;
}),
checkColor: WidgetStateProperty.all(appColors.torPurple),
),
iconTheme: const IconThemeData(color: Colors.white),
),
child: SafeArea(
child: ColoredBox(
color: appColors.torPurple,
child: CustomScrollView(
slivers: [
SliverAppBar(
pinned: true,
title: SwitchListTile.adaptive(
inactiveThumbColor: Colors.white,
activeThumbColor: appColors.torActiveGreen,
thumbIcon: WidgetStateProperty.resolveWith<Icon?>((
Set<WidgetState> states,
) {
if (states.contains(WidgetState.selected)) {
return const Icon(MdiIcons.axisArrowLock);
}
return null; // Use the default color.
}),
value: torPendingRequest.value ?? torIsRunning,
title: const Text('Tor™ Service'),
secondary: const Icon(MdiIcons.power),
onChanged: torIsBusy
? null
: (value) async {
if (value) {
torPendingRequest.value = true;
await ref
.read(torProxyServiceProvider.notifier)
.startOrReconfigure(
reconfigureIfRunning: false,
);
} else {
torPendingRequest.value = false;
await ref
.read(torProxyServiceProvider.notifier)
.disconnect();
}
},
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(4 + 40 + 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (torPendingRequest.value != false && torIsBusy)
LinearProgressIndicator(
backgroundColor: appColors.torBackgroundGrey,
color: appColors.torActiveGreen,
value: bootstrapProgress / 100,
),
Padding(
padding: const EdgeInsets.only(
top: 4.0,
right: 16,
left: 16,
bottom: 4,
),
child: SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed:
torIsRunning &&
torIsBootstrapped &&
!torIsBusy
? () async {
await ref
.read(
torProxyServiceProvider.notifier,
)
.requestNewIdentity();
if (context.mounted) {
showInfoMessage(
context,
'Requesting new Tor identity...',
);
}
}
: null,
icon: const Icon(MdiIcons.refresh),
label: const Text('Request New Identity'),
),
),
),
],
),
),
),
SliverList.list(
children: [
const SizedBox(height: 16),
const Padding(
padding: EdgeInsets.only(left: 24.0),
child: Text(
'Routing',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
RadioGroup(
groupValue: torSettings.proxyRegularTabsMode,
onChanged: (value) async {
if (value != null) {
await ref
.read(saveTorSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith
.proxyRegularTabsMode(value),
);
}
},
child: Column(
children: [
if (showContainerUi)
const RadioListTile.adaptive(
value: TorRegularTabProxyMode.container,
title: Text('Container-Based Routing'),
subtitle: Text(
'Route only tabs in Tor containers through the Tor network. Private tabs remain unaffected.',
),
),
const RadioListTile.adaptive(
value: TorRegularTabProxyMode.all,
title: Text('Global Routing'),
subtitle: Text(
'Route all regular tabs through the Tor network. Private tabs remain unaffected.',
),
),
if (!showContainerUi &&
torSettings.proxyRegularTabsMode ==
TorRegularTabProxyMode.container)
const Padding(
padding: EdgeInsets.only(
left: 56,
right: 24,
top: 4,
),
child: Text(
'Container-based routing is currently active but hidden because Container UI is disabled.',
),
),
],
),
),
SwitchListTile.adaptive(
inactiveThumbColor: Colors.white,
activeThumbColor: appColors.torActiveGreen,
thumbIcon: WidgetStateProperty.resolveWith<Icon?>((
Set<WidgetState> states,
) {
if (states.contains(WidgetState.selected)) {
return const Icon(MdiIcons.incognito);
}
return null; // Use the default color.
}),
value: torSettings.proxyPrivateTabsTor,
title: const Text('Proxy Private Tabs'),
subtitle: const Text(
'When enabled, all Private Tabs will be tunneled through Tor',
),
secondary: const Icon(MdiIcons.incognito),
onChanged: (value) async {
await ref
.read(saveTorSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith
.proxyPrivateTabsTor(value),
);
},
),
const SizedBox(height: 16),
const Padding(
padding: EdgeInsets.only(left: 24.0),
child: Text(
'Circumvention',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
SwitchListTile.adaptive(
inactiveThumbColor: Colors.white,
activeThumbColor: appColors.torActiveGreen,
thumbIcon: WidgetStateProperty.resolveWith<Icon?>((
Set<WidgetState> states,
) {
if (states.contains(WidgetState.selected)) {
return const Icon(MdiIcons.arrowDecisionAuto);
}
return null; // Use the default color.
}),
value: torSettings.config == TorConnectionConfig.auto,
title: const Text('Auto Configure Transport'),
subtitle: const Text(
'From some locations, it is necessary to use a pluggable transport to connect to Tor',
),
secondary: const Icon(MdiIcons.arrowDecisionAuto),
onChanged: torIsBusy
? null
: (value) async {
await ref
.read(
saveTorSettingsControllerProvider.notifier,
)
.save(
(currentSettings) =>
currentSettings.copyWith.config(
value
? TorConnectionConfig.auto
: TorConnectionConfig.direct,
),
);
},
),
if (torSettings.config == TorConnectionConfig.auto) ...[
SwitchListTile.adaptive(
inactiveThumbColor: Colors.white,
activeThumbColor: appColors.torActiveGreen,
value: torSettings.requireBridge,
contentPadding: const EdgeInsets.only(
left: 56,
right: 24,
),
onChanged: torIsBusy
? null
: (value) async {
await ref
.read(
saveTorSettingsControllerProvider
.notifier,
)
.save(
(currentSettings) => currentSettings
.copyWith
.requireBridge(value),
);
},
title: const Text(
"I'm sure I cannot connect without a bridge",
),
),
] else ...[
RadioGroup(
groupValue: torSettings.config,
onChanged: (value) async {
if (value != null) {
await ref
.read(
saveTorSettingsControllerProvider.notifier,
)
.save(
(currentSettings) =>
currentSettings.copyWith.config(value),
);
}
},
child: Column(
children: [
RadioListTile.adaptive(
value: TorConnectionConfig.direct,
enabled: !torIsBusy,
contentPadding: const EdgeInsets.only(
left: 56,
right: 24,
),
title: const Text('Direct Connection'),
subtitle: const Text(
'The best way to connect to Tor if Tor is not blocked',
),
),
RadioListTile.adaptive(
value: TorConnectionConfig.obfs4,
enabled: !torIsBusy,
contentPadding: const EdgeInsets.only(
left: 56,
right: 24,
),
title: const Text('obfs4'),
subtitle: const Text(
'Suitable for light censorship and high bandwidth needs',
),
),
RadioListTile.adaptive(
value: TorConnectionConfig.snowflake,
enabled: !torIsBusy,
contentPadding: const EdgeInsets.only(
left: 56,
right: 24,
),
title: const Text('Snowflake'),
subtitle: const Text(
'Suitable for heavy censorship',
),
),
],
),
),
CheckboxListTile.adaptive(
value: torSettings.fetchRemoteBridges,
controlAffinity: ListTileControlAffinity.leading,
enabled:
torSettings.config != TorConnectionConfig.direct,
contentPadding: const EdgeInsets.only(
left: 56,
right: 24,
),
onChanged: torIsBusy
? null
: (value) async {
if (value != null) {
await ref
.read(
saveTorSettingsControllerProvider
.notifier,
)
.save(
(currentSettings) => currentSettings
.copyWith
.fetchRemoteBridges(value),
);
}
},
title: const Text(
"Fetch fresh Bridges before connecting",
),
),
],
const SizedBox(height: 16),
const Padding(
padding: EdgeInsets.only(left: 24.0),
child: Text(
'Country Restrictions',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
ListTile(
enabled: !torIsBusy,
leading:
torSettings.entryNodeCountry.mapNotNull(
(code) => CountryFlag.fromCountryCode(
code,
theme: const EmojiTheme(size: 28),
),
) ??
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),
);
},
),
ListTile(
enabled: !torIsBusy,
leading:
torSettings.exitNodeCountry.mapNotNull(
(code) => CountryFlag.fromCountryCode(
code,
theme: const EmojiTheme(size: 28),
),
) ??
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),
);
},
),
],
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
top: 32.0,
right: 12.0,
left: 12.0,
bottom: 8.0,
),
child: Text(
'Tor is a trademark of The Tor Project; all rights reserved. WebLibre is not endorsed or sponsored by, or affiliated with, the Tor Project.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontStyle: FontStyle.italic,
color: Colors.white,
),
),
),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,53 @@
/*
* 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:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:weblibre/core/design/app_colors.dart';
import 'package:weblibre/presentation/icons/tor_icons.dart';
class TorDialog extends StatelessWidget {
const TorDialog({super.key});
@override
Widget build(BuildContext context) {
final appColors = AppColors.of(context);
return AlertDialog(
icon: Icon(TorIcons.onionAlt, color: appColors.torPurple),
title: const Text('Tor™ Proxy'),
content: const Text(
'This container requires a Tor proxy for secure connections, which is not currently running.',
),
actions: [
TextButton(
onPressed: () {
context.pop(false);
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
context.pop(true);
},
child: const Text('Enable'),
),
],
);
}
}
@@ -0,0 +1,107 @@
/*
* 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:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/design/app_colors.dart';
import 'package:weblibre/features/geckoview/domain/controllers/overlay.dart';
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
import 'package:weblibre/presentation/icons/tor_icons.dart';
import 'package:weblibre/presentation/widgets/animate_gradient_shader.dart';
class TorNotification extends HookConsumerWidget {
const TorNotification({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final appColors = AppColors.of(context);
ref.listen(torProxyServiceProvider, (previous, next) {
if (next.hasValue & next.requireValue.isRunning &&
next.requireValue.bootstrapProgress == 100) {
ref.read(overlayControllerProvider.notifier).dismiss();
}
});
return SafeArea(
child: ColoredBox(
color: appColors.torPurple,
child: SizedBox(
height: 56 + 12,
width: MediaQuery.of(context).size.width,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
AnimateGradientShader(
duration: const Duration(milliseconds: 500),
primaryEnd: Alignment.bottomLeft,
secondaryEnd: Alignment.topRight,
primaryColors: [
appColors.torActiveGreen,
appColors.torActiveGreen,
],
secondaryColors: const [Colors.white, Colors.white],
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Icon(TorIcons.onionAlt),
),
),
Expanded(
child: Text(
'Tor Proxy is connecting...',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: Colors.white),
),
),
IconButton(
onPressed: () {
ref.read(overlayControllerProvider.notifier).dismiss();
},
icon: const Icon(Icons.close, color: Colors.white),
),
],
),
Consumer(
builder: (context, ref, child) {
final bootstrapProgress = ref.watch(
torProxyServiceProvider.select(
(value) => value.value?.bootstrapProgress ?? 0,
),
);
return LinearProgressIndicator(
backgroundColor: AppColors.of(context).torBackgroundGrey,
color: AppColors.of(context).torActiveGreen,
value: bootstrapProgress / 100,
);
},
),
],
),
),
),
),
);
}
}