From d6331060762274ff28b66e9288548edd4ee89936 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 25 Sep 2025 09:39:22 +0200 Subject: [PATCH] improved tor ui --- .../browser_modules/bottom_app_bar.dart | 18 +- .../tor/presentation/screens/tor_proxy.dart | 781 ++++++++++-------- 2 files changed, 458 insertions(+), 341 deletions(-) diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index 3e80685a..a488af32 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -43,6 +43,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart'; import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart'; import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart'; +import 'package:weblibre/features/tor/domain/services/tor_proxy.dart'; import 'package:weblibre/features/user/data/models/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; @@ -327,7 +328,22 @@ class BrowserBottomAppBar extends HookConsumerWidget { await TorProxyRoute().push(context); }, leadingIcon: const Icon(TorIcons.onionAlt), - child: const Text('Tor™ Proxy'), + child: Consumer( + child: const Text('Tor™ Proxy'), + builder: (context, ref, child) { + final torConnected = ref.watch( + torProxyServiceProvider.select( + (value) => value.value != null, + ), + ); + + return Badge( + isLabelVisible: torConnected, + backgroundColor: const Color(0xFF68B030), + child: child, + ); + }, + ), ), const Divider(), MenuItemButton( diff --git a/app/lib/features/tor/presentation/screens/tor_proxy.dart b/app/lib/features/tor/presentation/screens/tor_proxy.dart index b4fc319f..e3b566c5 100644 --- a/app/lib/features/tor/presentation/screens/tor_proxy.dart +++ b/app/lib/features/tor/presentation/screens/tor_proxy.dart @@ -17,6 +17,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -49,286 +50,176 @@ class TorProxyScreen extends HookConsumerWidget { return Scaffold( appBar: AppBar(title: const Text('Tor™ Proxy')), - body: SafeArea( - child: ColoredBox( - color: const Color(0xFF7D4698), - child: Column( - children: [ - Expanded( - child: ListView( - children: [ - const Icon(TorIcons.onionAlt, size: 84), - const SizedBox(height: 16), - ListTileTheme( - iconColor: Colors.white, - textColor: Colors.white, - child: SwitchListTile.adaptive( - inactiveThumbColor: Colors.white, - activeColor: const Color(0xFF68B030), - trackColor: WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.isEmpty) { - return const Color(0xFF333A41); - } - return null; // Use the default color. - }), - trackOutlineColor: - WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.isEmpty) { - return Colors.white; - } - return null; // Use the default color. - }), - thumbIcon: WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.contains(WidgetState.selected)) { - return const Icon(MdiIcons.axisArrowLock); - } - return null; // Use the default color. - }), - value: torProxyPort.value != null, - title: const Text('Tor™ Proxy'), - secondary: const Icon(MdiIcons.power), - onChanged: torProxyPort.isLoading - ? null - : (value) async { - if (value) { - await ref - .read(torProxyServiceProvider.notifier) - .startOrReconfigure(); - } else { - await ref - .read(torProxyServiceProvider.notifier) - .disconnect(); - } - }, - ), - ), - ListTileTheme( - iconColor: Colors.white, - textColor: Colors.white, - child: SwitchListTile.adaptive( - inactiveThumbColor: Colors.white, - activeColor: const Color(0xFF68B030), - trackColor: WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.isEmpty) { - return const Color(0xFF333A41); - } - return null; // Use the default color. - }), - trackOutlineColor: - WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.isEmpty) { - return Colors.white; - } - return null; // Use the default color. - }), - thumbIcon: WidgetStateProperty.resolveWith(( - Set 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), - ); - }, - ), - ), - ListTileTheme( - iconColor: Colors.white, - textColor: Colors.white, - child: SwitchListTile.adaptive( - inactiveThumbColor: Colors.white, - activeColor: const Color(0xFF68B030), - trackColor: WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.isEmpty) { - return const Color(0xFF333A41); - } - return null; // Use the default color. - }), - trackOutlineColor: - WidgetStateProperty.resolveWith(( - Set states, - ) { - if (states.isEmpty) { - return Colors.white; - } - return null; // Use the default color. - }), - thumbIcon: WidgetStateProperty.resolveWith(( - Set 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: torProxyPort.isLoading - ? 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( - value: torSettings.requireBridge, - contentPadding: const EdgeInsets.only( - left: 56, - right: 24, - ), - onChanged: torProxyPort.isLoading - ? 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: RadioListTile.adaptive( - value: TorConnectionConfig.direct, - enabled: !torProxyPort.isLoading, - 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', - ), - ), - ), - RadioGroup( - groupValue: torSettings.config, - onChanged: (value) async { - if (value != null) { - await ref - .read( - saveTorSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => - currentSettings.copyWith.config(value), - ); - } - }, - child: RadioListTile.adaptive( - value: TorConnectionConfig.obfs4, - enabled: !torProxyPort.isLoading, - contentPadding: const EdgeInsets.only( - left: 56, - right: 24, - ), - title: const Text('obfs4'), - subtitle: const Text( - 'Suitable for light censorship and high bandwidth needs', - ), - ), - ), - RadioGroup( - groupValue: torSettings.config, - onChanged: (value) async { - if (value != null) { - await ref - .read( - saveTorSettingsControllerProvider.notifier, - ) - .save( - (currentSettings) => - currentSettings.copyWith.config(value), - ); - } - }, - child: RadioListTile.adaptive( - value: TorConnectionConfig.snowflake, - enabled: !torProxyPort.isLoading, - 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: torProxyPort.isLoading - ? null - : (value) async { - if (value != null) { + 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(( + Set states, + ) { + if (states.isEmpty) { + return const Color(0xFF333A41); + } + return null; // Use the default color. + }), + trackOutlineColor: WidgetStateProperty.resolveWith(( + Set 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; + }), + ), + ), + child: SafeArea( + child: ColoredBox( + color: const Color(0xFF7D4698), + child: Column( + children: [ + const SizedBox(height: 16), + const Icon(TorIcons.onionAlt, size: 84), + const SizedBox(height: 16), + Container( + margin: const EdgeInsets.symmetric(horizontal: 24), + decoration: BoxDecoration( + border: Border.all(color: Colors.white), + borderRadius: BorderRadius.circular(16), + ), + child: SwitchListTile.adaptive( + inactiveThumbColor: Colors.white, + activeThumbColor: const Color(0xFF68B030), + thumbIcon: WidgetStateProperty.resolveWith(( + Set states, + ) { + if (states.contains(WidgetState.selected)) { + return const Icon(MdiIcons.axisArrowLock); + } + return null; // Use the default color. + }), + value: torProxyPort.value != null, + title: const Text('Tor™ Proxy'), + secondary: const Icon(MdiIcons.power), + onChanged: torProxyPort.isLoading + ? null + : (value) async { + if (value) { + await ref + .read(torProxyServiceProvider.notifier) + .startOrReconfigure(); + } else { + await ref + .read(torProxyServiceProvider.notifier) + .disconnect(); + } + }, + ), + ), + const SizedBox(height: 16), + Expanded( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return FadingScroll( + controller: controller, + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + 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: 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.', + ), + ), + ), + RadioGroup( + groupValue: torSettings.proxyRegularTabsMode, + onChanged: (value) async { + if (value != null) { + await ref + .read( + saveTorSettingsControllerProvider + .notifier, + ) + .save( + (currentSettings) => currentSettings + .copyWith + .proxyRegularTabsMode(value), + ); + } + }, + child: const RadioListTile.adaptive( + value: TorRegularTabProxyMode.all, + title: Text('Global Routing'), + subtitle: Text( + 'Route all regular tabs through the Tor network. Private tabs remain unaffected.', + ), + ), + ), + SwitchListTile.adaptive( + inactiveThumbColor: Colors.white, + activeThumbColor: const Color(0xFF68B030), + thumbIcon: + WidgetStateProperty.resolveWith(( + Set 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 @@ -337,77 +228,287 @@ class TorProxyScreen extends HookConsumerWidget { .save( (currentSettings) => currentSettings .copyWith - .fetchRemoteBridges(value), + .proxyPrivateTabsTor(value), ); - } - }, - title: const Text( - "Fetch fresh Bridges before connecting", - ), - ), - ], - ], + }, + ), + 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: const Color(0xFF68B030), + thumbIcon: + WidgetStateProperty.resolveWith(( + Set 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: torProxyPort.isLoading + ? 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: const Color(0xFF68B030), + value: torSettings.requireBridge, + contentPadding: const EdgeInsets.only( + left: 56, + right: 24, + ), + onChanged: torProxyPort.isLoading + ? 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: RadioListTile.adaptive( + value: TorConnectionConfig.direct, + enabled: !torProxyPort.isLoading, + 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', + ), + ), + ), + RadioGroup( + groupValue: torSettings.config, + onChanged: (value) async { + if (value != null) { + await ref + .read( + saveTorSettingsControllerProvider + .notifier, + ) + .save( + (currentSettings) => currentSettings + .copyWith + .config(value), + ); + } + }, + child: RadioListTile.adaptive( + value: TorConnectionConfig.obfs4, + enabled: !torProxyPort.isLoading, + contentPadding: const EdgeInsets.only( + left: 56, + right: 24, + ), + title: const Text('obfs4'), + subtitle: const Text( + 'Suitable for light censorship and high bandwidth needs', + ), + ), + ), + RadioGroup( + groupValue: torSettings.config, + onChanged: (value) async { + if (value != null) { + await ref + .read( + saveTorSettingsControllerProvider + .notifier, + ) + .save( + (currentSettings) => currentSettings + .copyWith + .config(value), + ); + } + }, + child: RadioListTile.adaptive( + value: TorConnectionConfig.snowflake, + enabled: !torProxyPort.isLoading, + 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: torProxyPort.isLoading + ? 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", + ), + ), + ], + ], + ); + }, + ); + }, + ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (torProxyPort.isLoading) - const Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 8), - LinearProgressIndicator( - backgroundColor: Color(0xFF333A41), - color: Color(0xFF68B030), - ), - SizedBox(height: 8), - Text( - 'Establishing connection...', - style: TextStyle(color: Colors.white), - ), - ], - ), - if (torProxyPort.value != null) - Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (torProxyPort.isLoading) + const Column( + mainAxisSize: MainAxisSize.min, children: [ - const Icon( - MdiIcons.shieldLockOutline, + SizedBox(height: 8), + LinearProgressIndicator( + backgroundColor: Color(0xFF333A41), color: Color(0xFF68B030), - size: 32, ), - const SizedBox(width: 12), + SizedBox(height: 8), Text( - 'Connected to the Tor Network', - style: Theme.of(context).textTheme.titleMedium - ?.copyWith(color: const Color(0xFF68B030)), + 'Establishing connection...', + style: TextStyle(color: Colors.white), ), ], ), - ), - ], + if (torProxyPort.value != null) + Padding( + padding: const EdgeInsets.only(top: 24.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + MdiIcons.shieldLockOutline, + color: Color(0xFF68B030), + size: 32, + ), + const SizedBox(width: 12), + Text( + 'Connected to the Tor Network', + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(color: const Color(0xFF68B030)), + ), + ], + ), + ), + ], + ), ), - ), - Padding( - padding: const EdgeInsets.only( - right: 8.0, - left: 8.0, - bottom: 8.0, + Padding( + padding: const EdgeInsets.only( + right: 8.0, + left: 8.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, + ), + ), ), - 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), - ), - ), - ], + ], + ), ), ), ),