From 27a7861bada697047bd906242d87a3772c231ef8 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 13 Sep 2023 09:49:12 -0600 Subject: [PATCH] refactor mobile tor settings --- .../tor_settings/tor_settings_view.dart | 583 +++++++++++------- 1 file changed, 356 insertions(+), 227 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart b/lib/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart index f76d29b3e..e970f0158 100644 --- a/lib/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart @@ -8,19 +8,23 @@ * */ +import 'dart:async'; + +import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/tor_service.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; @@ -37,122 +41,8 @@ class TorSettingsView extends ConsumerStatefulWidget { } class _TorSettingsViewState extends ConsumerState { - late TorConnectionStatus _networkStatus; - - Widget _buildTorIcon(TorConnectionStatus status) { - switch (status) { - case TorConnectionStatus.disconnected: - return GestureDetector( - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - SvgPicture.asset( - Assets.svg.tor, - color: - Theme.of(context).extension()!.textSubtitle3, - width: 200, - height: 200, - ), - Text( - "CONNECT", - style: STextStyles.smallMed14(context).copyWith( - color: - Theme.of(context).extension()!.popupBG), - ) - ], - ), - onTap: () async { - await connect(); - }); - case TorConnectionStatus.connected: - return GestureDetector( - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - SvgPicture.asset( - Assets.svg.tor, - color: Theme.of(context) - .extension()! - .accentColorGreen, - width: 200, - height: 200, - ), - Text( - "STOP", - style: STextStyles.smallMed14(context).copyWith( - color: - Theme.of(context).extension()!.popupBG), - ) - ], - ), - onTap: () async { - // TODO we could make this sync. - await disconnect(); // TODO we could do away with the Future here. - }); - case TorConnectionStatus.connecting: - return Stack( - alignment: AlignmentDirectional.center, - children: [ - SvgPicture.asset( - Assets.svg.tor, - color: - Theme.of(context).extension()!.accentColorYellow, - width: 200, - height: 200, - ), - Text( - "CONNECTING", - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context).extension()!.popupBG), - ) - ], - ); - } - } - - Widget _buildTorStatus(TorConnectionStatus status) { - switch (status) { - case TorConnectionStatus.disconnected: - return Text( - "Disconnected", - style: STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle3), - ); - case TorConnectionStatus.connected: - return Text( - "Connected", - style: STextStyles.itemSubtitle(context).copyWith( - color: - Theme.of(context).extension()!.accentColorGreen), - ); - case TorConnectionStatus.connecting: - return Text( - "Connecting", - style: STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorYellow), - ); - } - } - - @override - void initState() { - _networkStatus = ref.read(pTorService).enabled - ? TorConnectionStatus.connected - : TorConnectionStatus.disconnected; - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { - final isDesktop = Util.isDesktop; - return Background( child: Scaffold( backgroundColor: Colors.transparent, @@ -204,79 +94,19 @@ class _TorSettingsViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Row( + const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( - padding: const EdgeInsets.all(10.0), - child: _buildTorIcon(_networkStatus), + padding: EdgeInsets.all(10.0), + child: TorIcon(), ), ], ), const SizedBox( height: 30, ), - GestureDetector( - child: RoundedWhiteContainer( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - Text( - "Tor status", - style: STextStyles.titleBold12(context), - ), - const Spacer(), - _buildTorStatus(_networkStatus), - ], - ), - ), - ), - onTap: () async { - // Connect or disconnect when the user taps the status. - switch (_networkStatus) { - case TorConnectionStatus.disconnected: - // Update the UI. - setState(() { - _networkStatus = TorConnectionStatus.connecting; - }); - - try { - await connect(); - } catch (e, s) { - Logging.instance.log( - "Error starting tor: $e\n$s", - level: LogLevel.Error, - ); - rethrow; - } - - // Update the UI. - setState(() { - _networkStatus = TorConnectionStatus.connected; - }); - break; - case TorConnectionStatus.connected: - try { - await disconnect(); - } catch (e, s) { - Logging.instance.log( - "Error stopping tor: $e\n$s", - level: LogLevel.Error, - ); - rethrow; - } - - // Update the UI. - setState(() { - _networkStatus = TorConnectionStatus.disconnected; - }); - break; - case TorConnectionStatus.connecting: - // Do nothing. - break; - } - }), + const TorButton(), const SizedBox( height: 8, ), @@ -363,64 +193,363 @@ class _TorSettingsViewState extends ConsumerState { ), ); } +} - /// Connect to the Tor network. - /// - /// This method is called when the user taps the "Connect" button. - /// - /// Throws an exception if the Tor service fails to start. - /// - /// Returns a Future that completes when the Tor service has started. - Future connect() async { - // Init the Tor service if it hasn't already been. - ref.read(pTorService).init(); +class TorIcon extends ConsumerStatefulWidget { + const TorIcon({super.key}); - // Update the UI. - setState(() { - _networkStatus = TorConnectionStatus.connecting; - }); + @override + ConsumerState createState() => _TorIconState(); +} - // Start the Tor service. - try { - await ref.read(pTorService).start(); +class _TorIconState extends ConsumerState { + late TorConnectionStatus _status; - // Toggle the useTor preference on success. - ref.read(prefsChangeNotifierProvider).useTor = true; - } catch (e, s) { - Logging.instance.log( - "Error starting tor: $e\n$s", - level: LogLevel.Error, - ); + Color _color( + TorConnectionStatus status, + StackColors colors, + ) { + switch (status) { + case TorConnectionStatus.disconnected: + return colors.textSubtitle3; + + case TorConnectionStatus.connected: + return colors.accentColorGreen; + + case TorConnectionStatus.connecting: + return colors.accentColorYellow; } - - return; } - /// Disconnect from the Tor network. - /// - /// This method is called when the user taps the "Disconnect" button. - /// - /// Throws an exception if the Tor service fails to stop. - /// - /// Returns a Future that completes when the Tor service has stopped. - Future disconnect() async { - // Stop the Tor service. - try { - await ref.read(pTorService).stop(); + String _label( + TorConnectionStatus status, + StackColors colors, + ) { + switch (status) { + case TorConnectionStatus.disconnected: + return "CONNECT"; - // Toggle the useTor preference on success. - ref.read(prefsChangeNotifierProvider).useTor = false; - } catch (e, s) { - Logging.instance.log( - "Error stopping tor: $e\n$s", - level: LogLevel.Error, - ); + case TorConnectionStatus.connected: + return "STOP"; + + case TorConnectionStatus.connecting: + return "CONNECTING"; } + } - setState(() { - _networkStatus = TorConnectionStatus.disconnected; - }); + bool _tapLock = false; - return; + Future onTap() async { + if (_tapLock) { + return; + } + _tapLock = true; + try { + // Connect or disconnect when the user taps the status. + switch (_status) { + case TorConnectionStatus.disconnected: + await _connectTor(ref, context); + break; + + case TorConnectionStatus.connected: + await _disconnectTor(ref, context); + + break; + + case TorConnectionStatus.connecting: + // Do nothing. + break; + } + } catch (_) { + // any exceptions should already be handled with error dialogs + // this try catch is just extra protection to ensure _tapLock gets reset + // in the finally block in the event of an unknown error + } finally { + _tapLock = false; + } + } + + @override + void initState() { + _status = ref.read(pTorService).enabled + ? TorConnectionStatus.connected + : TorConnectionStatus.disconnected; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return _TorSubscriptionBase( + onTorStatusChanged: (status) { + setState(() { + _status = status; + }); + }, + child: ConditionalParent( + condition: _status != TorConnectionStatus.connecting, + builder: (child) => GestureDetector( + onTap: onTap, + child: child, + ), + child: SizedBox( + width: 220, + height: 220, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + SvgPicture.asset( + Assets.svg.tor, + color: _color( + _status, + Theme.of(context).extension()!, + ), + width: 200, + height: 200, + ), + Text( + _label( + _status, + Theme.of(context).extension()!, + ), + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension()!.popupBG, + ), + ), + ], + ), + ), + ), + ); + } +} + +class TorButton extends ConsumerStatefulWidget { + const TorButton({super.key}); + + @override + ConsumerState createState() => _TorButtonState(); +} + +class _TorButtonState extends ConsumerState { + late TorConnectionStatus _status; + + Color _color( + TorConnectionStatus status, + StackColors colors, + ) { + switch (status) { + case TorConnectionStatus.disconnected: + return colors.textSubtitle3; + + case TorConnectionStatus.connected: + return colors.accentColorGreen; + + case TorConnectionStatus.connecting: + return colors.accentColorYellow; + } + } + + String _label( + TorConnectionStatus status, + StackColors colors, + ) { + switch (status) { + case TorConnectionStatus.disconnected: + return "Disconnected"; + + case TorConnectionStatus.connected: + return "Connected"; + + case TorConnectionStatus.connecting: + return "Connecting"; + } + } + + bool _tapLock = false; + + Future onTap() async { + if (_tapLock) { + return; + } + _tapLock = true; + try { + // Connect or disconnect when the user taps the status. + switch (_status) { + case TorConnectionStatus.disconnected: + await _connectTor(ref, context); + break; + + case TorConnectionStatus.connected: + await _disconnectTor(ref, context); + + break; + + case TorConnectionStatus.connecting: + // Do nothing. + break; + } + } catch (_) { + // any exceptions should already be handled with error dialogs + // this try catch is just extra protection to ensure _tapLock gets reset + // in the finally block in the event of an unknown error + } finally { + _tapLock = false; + } + } + + @override + void initState() { + _status = ref.read(pTorService).enabled + ? TorConnectionStatus.connected + : TorConnectionStatus.disconnected; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return _TorSubscriptionBase( + onTorStatusChanged: (status) { + setState(() { + _status = status; + }); + }, + child: GestureDetector( + onTap: onTap, + child: RoundedWhiteContainer( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Text( + "Tor status", + style: STextStyles.titleBold12(context), + ), + const Spacer(), + Text( + _label( + _status, + Theme.of(context).extension()!, + ), + style: STextStyles.itemSubtitle(context).copyWith( + color: _color( + _status, + Theme.of(context).extension()!, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _TorSubscriptionBase extends ConsumerStatefulWidget { + const _TorSubscriptionBase({ + super.key, + required this.onTorStatusChanged, + this.eventBus, + required this.child, + }); + + final Widget child; + final void Function(TorConnectionStatus) onTorStatusChanged; + final EventBus? eventBus; + + @override + ConsumerState<_TorSubscriptionBase> createState() => + _TorSubscriptionBaseState(); +} + +class _TorSubscriptionBaseState extends ConsumerState<_TorSubscriptionBase> { + /// The global event bus. + late final EventBus eventBus; + + /// Subscription to the TorConnectionStatusChangedEvent. + late StreamSubscription + _torConnectionStatusSubscription; + + @override + void initState() { + // Initialize the global event bus. + eventBus = widget.eventBus ?? GlobalEventBus.instance; + + // Subscribe to the TorConnectionStatusChangedEvent. + _torConnectionStatusSubscription = + eventBus.on().listen( + (event) async { + widget.onTorStatusChanged.call(event.newStatus); + }, + ); + + super.initState(); + } + + @override + void dispose() { + // Clean up the TorConnectionStatusChangedEvent subscription. + _torConnectionStatusSubscription.cancel(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} + +/// Connect to the Tor network. +/// +/// This method is called when the user taps the "Connect" button. +/// +/// Throws an exception if the Tor service fails to start. +/// +/// Returns a Future that completes when the Tor service has started. +Future _connectTor(WidgetRef ref, BuildContext context) async { + // Init the Tor service if it hasn't already been. + ref.read(pTorService).init(); + + // Start the Tor service. + try { + await ref.read(pTorService).start(); + + // Toggle the useTor preference on success. + ref.read(prefsChangeNotifierProvider).useTor = true; + } catch (e, s) { + Logging.instance.log( + "Error starting tor: $e\n$s", + level: LogLevel.Error, + ); + // TODO: show dialog with error message + } + + return; +} + +/// Disconnect from the Tor network. +/// +/// This method is called when the user taps the "Disconnect" button. +/// +/// Throws an exception if the Tor service fails to stop. +/// +/// Returns a Future that completes when the Tor service has stopped. +Future _disconnectTor(WidgetRef ref, BuildContext context) async { + // Stop the Tor service. + try { + await ref.read(pTorService).stop(); + + // Toggle the useTor preference on success. + ref.read(prefsChangeNotifierProvider).useTor = false; + } catch (e, s) { + Logging.instance.log( + "Error stopping tor: $e\n$s", + level: LogLevel.Error, + ); + // TODO: show dialog with error message } }