improved tor ui

This commit is contained in:
Fabian Freund
2025-09-25 09:39:22 +02:00
parent dd16332c2d
commit d633106076
2 changed files with 458 additions and 341 deletions
@@ -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/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/controllers/readerable.dart';
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.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/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart';
@@ -327,7 +328,22 @@ class BrowserBottomAppBar extends HookConsumerWidget {
await TorProxyRoute().push(context); await TorProxyRoute().push(context);
}, },
leadingIcon: const Icon(TorIcons.onionAlt), 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(), const Divider(),
MenuItemButton( MenuItemButton(
@@ -17,6 +17,7 @@
* 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:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -49,286 +50,176 @@ class TorProxyScreen extends HookConsumerWidget {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Tor™ Proxy')), appBar: AppBar(title: const Text('Tor™ Proxy')),
body: SafeArea( body: Theme(
child: ColoredBox( data: Theme.of(context).copyWith(
color: const Color(0xFF7D4698), listTileTheme: ListTileTheme.of(
child: Column( context,
children: [ ).copyWith(iconColor: Colors.white, textColor: Colors.white),
Expanded( switchTheme: SwitchTheme.of(context).copyWith(
child: ListView( trackColor: WidgetStateProperty.resolveWith<Color?>((
children: [ Set<WidgetState> states,
const Icon(TorIcons.onionAlt, size: 84), ) {
const SizedBox(height: 16), if (states.isEmpty) {
ListTileTheme( return const Color(0xFF333A41);
iconColor: Colors.white, }
textColor: Colors.white, return null; // Use the default color.
child: SwitchListTile.adaptive( }),
inactiveThumbColor: Colors.white, trackOutlineColor: WidgetStateProperty.resolveWith<Color?>((
activeColor: const Color(0xFF68B030), Set<WidgetState> states,
trackColor: WidgetStateProperty.resolveWith<Color?>(( ) {
Set<WidgetState> states, if (states.isEmpty) {
) { return Colors.white;
if (states.isEmpty) { }
return const Color(0xFF333A41); return null; // Use the default color.
} }),
return null; // Use the default color. ),
}), radioTheme: RadioTheme.of(context).copyWith(
trackOutlineColor: fillColor: WidgetStateColor.resolveWith((states) {
WidgetStateProperty.resolveWith<Color?>(( return Colors.white;
Set<WidgetState> states, }),
) { ),
if (states.isEmpty) { checkboxTheme: CheckboxTheme.of(context).copyWith(
return Colors.white; fillColor: WidgetStateColor.resolveWith((states) {
} return Colors.white;
return null; // Use the default color. }),
}), ),
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(( ),
Set<WidgetState> states, child: SafeArea(
) { child: ColoredBox(
if (states.contains(WidgetState.selected)) { color: const Color(0xFF7D4698),
return const Icon(MdiIcons.axisArrowLock); child: Column(
} children: [
return null; // Use the default color. const SizedBox(height: 16),
}), const Icon(TorIcons.onionAlt, size: 84),
value: torProxyPort.value != null, const SizedBox(height: 16),
title: const Text('Tor™ Proxy'), Container(
secondary: const Icon(MdiIcons.power), margin: const EdgeInsets.symmetric(horizontal: 24),
onChanged: torProxyPort.isLoading decoration: BoxDecoration(
? null border: Border.all(color: Colors.white),
: (value) async { borderRadius: BorderRadius.circular(16),
if (value) { ),
await ref child: SwitchListTile.adaptive(
.read(torProxyServiceProvider.notifier) inactiveThumbColor: Colors.white,
.startOrReconfigure(); activeThumbColor: const Color(0xFF68B030),
} else { thumbIcon: WidgetStateProperty.resolveWith<Icon?>((
await ref Set<WidgetState> states,
.read(torProxyServiceProvider.notifier) ) {
.disconnect(); if (states.contains(WidgetState.selected)) {
} return const Icon(MdiIcons.axisArrowLock);
}, }
), return null; // Use the default color.
), }),
ListTileTheme( value: torProxyPort.value != null,
iconColor: Colors.white, title: const Text('Tor™ Proxy'),
textColor: Colors.white, secondary: const Icon(MdiIcons.power),
child: SwitchListTile.adaptive( onChanged: torProxyPort.isLoading
inactiveThumbColor: Colors.white, ? null
activeColor: const Color(0xFF68B030), : (value) async {
trackColor: WidgetStateProperty.resolveWith<Color?>(( if (value) {
Set<WidgetState> states, await ref
) { .read(torProxyServiceProvider.notifier)
if (states.isEmpty) { .startOrReconfigure();
return const Color(0xFF333A41); } else {
} await ref
return null; // Use the default color. .read(torProxyServiceProvider.notifier)
}), .disconnect();
trackOutlineColor: }
WidgetStateProperty.resolveWith<Color?>(( },
Set<WidgetState> states, ),
) { ),
if (states.isEmpty) { const SizedBox(height: 16),
return Colors.white; Expanded(
} child: FadingScroll(
return null; // Use the default color. fadingSize: 25,
}), builder: (context, controller) {
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(( return FadingScroll(
Set<WidgetState> states, controller: controller,
) { fadingSize: 25,
if (states.contains(WidgetState.selected)) { builder: (context, controller) {
return const Icon(MdiIcons.incognito); return ListView(
} controller: controller,
return null; // Use the default color. children: [
}), const SizedBox(height: 16),
value: torSettings.proxyPrivateTabsTor, const Padding(
title: const Text('Proxy Private Tabs'), padding: EdgeInsets.only(left: 24.0),
subtitle: const Text( child: Text(
'When enabled, all Private Tabs will be tunneled through Tor', 'Routing',
), style: TextStyle(
secondary: const Icon(MdiIcons.incognito), color: Colors.white,
onChanged: (value) async { fontWeight: FontWeight.bold,
await ref fontSize: 16,
.read(saveTorSettingsControllerProvider.notifier) ),
.save( ),
(currentSettings) => currentSettings.copyWith ),
.proxyPrivateTabsTor(value), RadioGroup(
); groupValue: torSettings.proxyRegularTabsMode,
}, onChanged: (value) async {
), if (value != null) {
), await ref
ListTileTheme( .read(
iconColor: Colors.white, saveTorSettingsControllerProvider
textColor: Colors.white, .notifier,
child: SwitchListTile.adaptive( )
inactiveThumbColor: Colors.white, .save(
activeColor: const Color(0xFF68B030), (currentSettings) => currentSettings
trackColor: WidgetStateProperty.resolveWith<Color?>(( .copyWith
Set<WidgetState> states, .proxyRegularTabsMode(value),
) { );
if (states.isEmpty) { }
return const Color(0xFF333A41); },
} child: const RadioListTile.adaptive(
return null; // Use the default color. value: TorRegularTabProxyMode.container,
}), title: Text('Container-Based Routing'),
trackOutlineColor: subtitle: Text(
WidgetStateProperty.resolveWith<Color?>(( 'Route only tabs in Tor containers through the Tor network. Private tabs remain unaffected.',
Set<WidgetState> states, ),
) { ),
if (states.isEmpty) { ),
return Colors.white; RadioGroup(
} groupValue: torSettings.proxyRegularTabsMode,
return null; // Use the default color. onChanged: (value) async {
}), if (value != null) {
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(( await ref
Set<WidgetState> states, .read(
) { saveTorSettingsControllerProvider
if (states.contains(WidgetState.selected)) { .notifier,
return const Icon(MdiIcons.arrowDecisionAuto); )
} .save(
return null; // Use the default color. (currentSettings) => currentSettings
}), .copyWith
value: torSettings.config == TorConnectionConfig.auto, .proxyRegularTabsMode(value),
title: const Text('Auto Configure Transport'), );
subtitle: const Text( }
'From some locations, it is necessary to use a pluggable transport to connect to Tor', },
), child: const RadioListTile.adaptive(
secondary: const Icon(MdiIcons.arrowDecisionAuto), value: TorRegularTabProxyMode.all,
onChanged: torProxyPort.isLoading title: Text('Global Routing'),
? null subtitle: Text(
: (value) async { 'Route all regular tabs through the Tor network. Private tabs remain unaffected.',
await ref ),
.read( ),
saveTorSettingsControllerProvider ),
.notifier, SwitchListTile.adaptive(
) inactiveThumbColor: Colors.white,
.save( activeThumbColor: const Color(0xFF68B030),
(currentSettings) => thumbIcon:
currentSettings.copyWith.config( WidgetStateProperty.resolveWith<Icon?>((
value Set<WidgetState> states,
? TorConnectionConfig.auto ) {
: TorConnectionConfig.direct, if (states.contains(
), WidgetState.selected,
); )) {
}, return const Icon(MdiIcons.incognito);
), }
), return null; // Use the default color.
if (torSettings.config == TorConnectionConfig.auto) ...[ }),
SwitchListTile.adaptive( value: torSettings.proxyPrivateTabsTor,
value: torSettings.requireBridge, title: const Text('Proxy Private Tabs'),
contentPadding: const EdgeInsets.only( subtitle: const Text(
left: 56, 'When enabled, all Private Tabs will be tunneled through Tor',
right: 24, ),
), secondary: const Icon(MdiIcons.incognito),
onChanged: torProxyPort.isLoading onChanged: (value) async {
? 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 await ref
.read( .read(
saveTorSettingsControllerProvider saveTorSettingsControllerProvider
@@ -337,77 +228,287 @@ class TorProxyScreen extends HookConsumerWidget {
.save( .save(
(currentSettings) => currentSettings (currentSettings) => currentSettings
.copyWith .copyWith
.fetchRemoteBridges(value), .proxyPrivateTabsTor(value),
); );
} },
}, ),
title: const Text( const SizedBox(height: 16),
"Fetch fresh Bridges before connecting", 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<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: 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( padding: const EdgeInsets.symmetric(vertical: 24.0),
padding: const EdgeInsets.symmetric(vertical: 24.0), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ if (torProxyPort.isLoading)
if (torProxyPort.isLoading) const Column(
const Column( mainAxisSize: MainAxisSize.min,
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,
children: [ children: [
const Icon( SizedBox(height: 8),
MdiIcons.shieldLockOutline, LinearProgressIndicator(
backgroundColor: Color(0xFF333A41),
color: Color(0xFF68B030), color: Color(0xFF68B030),
size: 32,
), ),
const SizedBox(width: 12), SizedBox(height: 8),
Text( Text(
'Connected to the Tor Network', 'Establishing connection...',
style: Theme.of(context).textTheme.titleMedium style: TextStyle(color: Colors.white),
?.copyWith(color: const Color(0xFF68B030)),
), ),
], ],
), ),
), 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( padding: const EdgeInsets.only(
padding: const EdgeInsets.only( right: 8.0,
right: 8.0, left: 8.0,
left: 8.0, bottom: 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),
),
),
],
), ),
), ),
), ),