LCOV - code coverage report
Current view: top level - lib/views - profilemgrview.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 275 0.0 %
Date: 2026-08-31 21:57:20 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:io';
       2             : 
       3             : import 'package:cwtch/constants.dart';
       4             : import 'package:cwtch/controllers/enter_password.dart';
       5             : import 'package:cwtch/controllers/filesharing.dart';
       6             : import 'package:cwtch/cwtch_icons_icons.dart';
       7             : import 'package:cwtch/models/appstate.dart';
       8             : import 'package:cwtch/models/profile.dart';
       9             : import 'package:cwtch/models/profilelist.dart';
      10             : import 'package:flutter/material.dart';
      11             : import 'package:cwtch/settings.dart';
      12             : import 'package:cwtch/views/torstatusview.dart';
      13             : import 'package:cwtch/widgets/passwordfield.dart';
      14             : import 'package:cwtch/widgets/tor_icon.dart';
      15             : import 'package:flutter/services.dart';
      16             : import 'package:cwtch/l10n/app_localizations.dart';
      17             : import 'package:cwtch/widgets/profilerow.dart';
      18             : import 'package:provider/provider.dart';
      19             : import '../main.dart';
      20             : import '../torstatus.dart';
      21             : import 'addeditprofileview.dart';
      22             : import 'globalsettingsview.dart';
      23             : import 'serversview.dart';
      24             : import 'hybridgroupsview.dart';
      25             : 
      26             : class ProfileMgrView extends StatefulWidget {
      27           0 :   ProfileMgrView();
      28             : 
      29           0 :   @override
      30           0 :   _ProfileMgrViewState createState() => _ProfileMgrViewState();
      31             : }
      32             : 
      33             : class _ProfileMgrViewState extends State<ProfileMgrView> {
      34             :   final ctrlrPassword = TextEditingController();
      35             : 
      36           0 :   @override
      37             :   void dispose() {
      38           0 :     ctrlrPassword.dispose();
      39           0 :     super.dispose();
      40             :   }
      41             : 
      42           0 :   @override
      43             :   Widget build(BuildContext context) {
      44           0 :     return Consumer<Settings>(
      45             :       // Prevents Android back button from closing the app on the profile manager screen
      46             :       // (which would shutdown connections and all kinds of other expensive to generate things)
      47           0 :       builder: (context, settings, child) => WillPopScope(
      48           0 :         onWillPop: () async {
      49           0 :           _modalShutdown();
      50           0 :           return Provider.of<AppState>(context, listen: false).cwtchIsClosing;
      51             :         },
      52           0 :         child: Scaffold(
      53           0 :           key: Key("ProfileManagerView"),
      54           0 :           backgroundColor: settings.theme.backgroundMainColor,
      55           0 :           appBar: AppBar(
      56           0 :             title: Row(
      57           0 :               children: [
      58           0 :                 Icon(CwtchIcons.cwtch_knott, size: 36, color: settings.theme.mainTextColor),
      59           0 :                 SizedBox(width: 10),
      60           0 :                 Expanded(
      61           0 :                   child: Text(
      62           0 :                     MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.titleManageProfiles : AppLocalizations.of(context)!.titleManageProfilesShort,
      63           0 :                     style: TextStyle(color: settings.current().mainTextColor),
      64             :                   ),
      65             :                 ),
      66             :               ],
      67             :             ),
      68           0 :             actions: getActions(),
      69             :           ),
      70           0 :           floatingActionButton: FloatingActionButton(
      71           0 :             onPressed: _modalAddImportProfiles,
      72           0 :             tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
      73           0 :             child: Icon(Icons.add, semanticLabel: AppLocalizations.of(context)!.addNewProfileBtn, color: Provider.of<Settings>(context).theme.defaultButtonTextColor),
      74             :           ),
      75           0 :           body: _buildProfileManager(),
      76             :         ),
      77             :       ),
      78             :     );
      79             :   }
      80             : 
      81           0 :   List<Widget> getActions() {
      82           0 :     List<Widget> actions = new List<Widget>.empty(growable: true);
      83             : 
      84             :     // Tor Status
      85           0 :     actions.add(
      86           0 :       IconButton(
      87           0 :         icon: TorIcon(),
      88           0 :         onPressed: _pushTorStatus,
      89           0 :         splashRadius: Material.defaultSplashRadius / 2,
      90           0 :         color: Provider.of<ProfileListState>(context).profiles.isEmpty ? Provider.of<Settings>(context).theme.defaultButtonColor : Provider.of<Settings>(context).theme.mainTextColor,
      91           0 :         tooltip: Provider.of<TorStatus>(context).progress == 100
      92           0 :             ? AppLocalizations.of(context)!.networkStatusOnline
      93           0 :             : (Provider.of<TorStatus>(context).progress == 0 ? AppLocalizations.of(context)!.networkStatusDisconnected : AppLocalizations.of(context)!.networkStatusAttemptingTor),
      94             :       ),
      95             :     );
      96             : 
      97             :     // Unlock Profiles
      98           0 :     actions.add(
      99           0 :       IconButton(
     100           0 :         icon: Icon(CwtchIcons.lock_open_24px),
     101           0 :         splashRadius: Material.defaultSplashRadius / 2,
     102           0 :         color: Provider.of<ProfileListState>(context).profiles.isEmpty ? Provider.of<Settings>(context).theme.defaultButtonColor : Provider.of<Settings>(context).theme.mainTextColor,
     103           0 :         tooltip: AppLocalizations.of(context)!.tooltipUnlockProfiles,
     104           0 :         onPressed: _modalUnlockProfiles,
     105             :       ),
     106             :     );
     107             : 
     108             :     // Hybrid groups
     109           0 :     if (Provider.of<FlwtchState>(context, listen: false).cwtch.IsManagedGroupsCompiled() &&
     110           0 :         Provider.of<Settings>(context).isExperimentEnabled(GroupManagerExperiment) &&
     111           0 :         !Platform.isAndroid &&
     112           0 :         !Platform.isIOS) {
     113           0 :       actions.add(
     114           0 :         IconButton(icon: Icon(Icons.diversity_1), splashRadius: Material.defaultSplashRadius / 2, tooltip: AppLocalizations.of(context)!.hybridGroupsManagerTitleShort, onPressed: _pushHybridGroups),
     115             :       );
     116             :     }
     117             : 
     118             :     // Servers
     119           0 :     if (Provider.of<FlwtchState>(context, listen: false).cwtch.IsServersCompiled() &&
     120           0 :         Provider.of<Settings>(context).isExperimentEnabled(ServerManagementExperiment) &&
     121           0 :         !Platform.isAndroid &&
     122           0 :         !Platform.isIOS) {
     123           0 :       actions.add(
     124           0 :         IconButton(icon: Icon(CwtchIcons.dns_24px), splashRadius: Material.defaultSplashRadius / 2, tooltip: AppLocalizations.of(context)!.serversManagerTitleShort, onPressed: _pushServers),
     125             :       );
     126             :     }
     127             : 
     128             :     // Global Settings
     129           0 :     actions.add(
     130           0 :       IconButton(
     131           0 :         key: Key("OpenSettingsView"),
     132           0 :         icon: Icon(Icons.settings),
     133           0 :         tooltip: AppLocalizations.of(context)!.tooltipOpenSettings,
     134           0 :         splashRadius: Material.defaultSplashRadius / 2,
     135           0 :         onPressed: _pushGlobalSettings,
     136             :       ),
     137             :     );
     138             : 
     139             :     // shutdown cwtch
     140           0 :     actions.add(
     141           0 :       IconButton(
     142           0 :         key: Key("shutdownCwtch"),
     143           0 :         icon: Icon(Icons.close),
     144           0 :         tooltip: AppLocalizations.of(context)!.shutdownCwtchTooltip,
     145           0 :         splashRadius: Material.defaultSplashRadius / 2,
     146           0 :         onPressed: _modalShutdown,
     147             :       ),
     148             :     );
     149             : 
     150             :     return actions;
     151             :   }
     152             : 
     153           0 :   void _modalShutdown() {
     154           0 :     Provider.of<FlwtchState>(context, listen: false).modalShutdown(MethodCall(""));
     155             :   }
     156             : 
     157           0 :   void _pushGlobalSettings() {
     158           0 :     Navigator.of(context).push(
     159           0 :       PageRouteBuilder(
     160           0 :         pageBuilder: (bcontext, a1, a2) {
     161           0 :           return Provider(create: (_) => Provider.of<FlwtchState>(bcontext, listen: false), child: GlobalSettingsView());
     162             :         },
     163           0 :         transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
     164           0 :         transitionDuration: Duration(milliseconds: 200),
     165             :       ),
     166             :     );
     167             :   }
     168             : 
     169           0 :   void _pushHybridGroups() {
     170           0 :     Navigator.of(context).push(
     171           0 :       PageRouteBuilder(
     172           0 :         settings: RouteSettings(name: "hybridgroups"),
     173           0 :         pageBuilder: (bcontext, a1, a2) {
     174           0 :           return MultiProvider(
     175           0 :             providers: [
     176           0 :               ChangeNotifierProvider.value(value: globalHybridGroupsList),
     177           0 :               Provider.value(value: Provider.of<FlwtchState>(context)),
     178             :             ],
     179           0 :             child: HybridGroupsView(),
     180             :           );
     181             :         },
     182           0 :         transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
     183           0 :         transitionDuration: Duration(milliseconds: 200),
     184             :       ),
     185             :     );
     186             :   }
     187             : 
     188           0 :   void _pushServers() {
     189           0 :     Navigator.of(context).push(
     190           0 :       PageRouteBuilder(
     191           0 :         settings: RouteSettings(name: "servers"),
     192           0 :         pageBuilder: (bcontext, a1, a2) {
     193           0 :           return MultiProvider(
     194           0 :             providers: [
     195           0 :               ChangeNotifierProvider.value(value: globalServersList),
     196           0 :               Provider.value(value: Provider.of<FlwtchState>(context)),
     197             :             ],
     198           0 :             child: ServersView(),
     199             :           );
     200             :         },
     201           0 :         transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
     202           0 :         transitionDuration: Duration(milliseconds: 200),
     203             :       ),
     204             :     );
     205             :   }
     206             : 
     207           0 :   void _pushTorStatus() {
     208           0 :     Navigator.of(context).push(
     209           0 :       PageRouteBuilder(
     210           0 :         settings: RouteSettings(name: "torconfig"),
     211           0 :         pageBuilder: (bcontext, a1, a2) {
     212           0 :           return MultiProvider(
     213           0 :             providers: [Provider.value(value: Provider.of<FlwtchState>(context))],
     214           0 :             child: TorStatusView(),
     215             :           );
     216             :         },
     217           0 :         transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
     218           0 :         transitionDuration: Duration(milliseconds: 200),
     219             :       ),
     220             :     );
     221             :   }
     222             : 
     223           0 :   void _pushAddProfile(bcontext, {onion = ""}) {
     224           0 :     Navigator.popUntil(bcontext, (route) => route.isFirst);
     225             : 
     226           0 :     Navigator.of(context).push(
     227           0 :       PageRouteBuilder(
     228           0 :         pageBuilder: (bcontext, a1, a2) {
     229           0 :           return MultiProvider(
     230           0 :             providers: [ChangeNotifierProvider<ProfileInfoState>(create: (_) => ProfileInfoState(onion: onion))],
     231           0 :             builder: (context, widget) => AddEditProfileView(key: Key('addprofile')),
     232             :           );
     233             :         },
     234           0 :         transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
     235           0 :         transitionDuration: Duration(milliseconds: 200),
     236             :       ),
     237             :     );
     238             :   }
     239             : 
     240           0 :   void _modalAddImportProfiles() {
     241           0 :     showModalBottomSheet<void>(
     242           0 :       context: context,
     243             :       isScrollControlled: true,
     244           0 :       builder: (BuildContext context) {
     245           0 :         return Padding(
     246           0 :           padding: MediaQuery.of(context).viewInsets,
     247           0 :           child: RepaintBoundary(
     248           0 :             child: Container(
     249           0 :               height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
     250           0 :               child: Center(
     251           0 :                 child: Padding(
     252           0 :                   padding: EdgeInsets.all(10.0),
     253           0 :                   child: Column(
     254             :                     mainAxisAlignment: MainAxisAlignment.center,
     255             :                     crossAxisAlignment: CrossAxisAlignment.center,
     256             :                     mainAxisSize: MainAxisSize.min,
     257           0 :                     children: <Widget>[
     258           0 :                       SizedBox(height: 20),
     259           0 :                       Expanded(
     260           0 :                         child: ElevatedButton(
     261           0 :                           style: ElevatedButton.styleFrom(
     262           0 :                             minimumSize: Size(399, 20),
     263           0 :                             maximumSize: Size(400, 20),
     264           0 :                             shape: RoundedRectangleBorder(
     265           0 :                               borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
     266             :                             ),
     267             :                           ),
     268           0 :                           child: Text(
     269           0 :                             key: Key("addNewProfileActual"),
     270           0 :                             AppLocalizations.of(context)!.addProfileTitle,
     271           0 :                             semanticsLabel: AppLocalizations.of(context)!.addProfileTitle,
     272           0 :                             style: TextStyle(fontWeight: FontWeight.bold),
     273             :                           ),
     274           0 :                           onPressed: () {
     275           0 :                             _pushAddProfile(context);
     276             :                           },
     277             :                         ),
     278             :                       ),
     279           0 :                       SizedBox(height: 20),
     280           0 :                       Expanded(
     281           0 :                         child: Tooltip(
     282           0 :                           message: AppLocalizations.of(context)!.importProfileTooltip,
     283           0 :                           child: OutlinedButton(
     284           0 :                             style: ElevatedButton.styleFrom(
     285           0 :                               minimumSize: Size(399, 20),
     286           0 :                               maximumSize: Size(400, 20),
     287           0 :                               shape: RoundedRectangleBorder(
     288           0 :                                 side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
     289           0 :                                 borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
     290             :                               ),
     291             :                             ),
     292           0 :                             child: Text(AppLocalizations.of(context)!.importProfile, semanticsLabel: AppLocalizations.of(context)!.importProfile),
     293           0 :                             onPressed: () {
     294             :                               // 10GB profiles should be enough for anyone?
     295           0 :                               showFilePicker(
     296             :                                 context,
     297             :                                 MaxGeneralFileSharingSize,
     298           0 :                                 (file) {
     299           0 :                                   showPasswordDialog(context, AppLocalizations.of(context)!.importProfile, AppLocalizations.of(context)!.importProfile, (password) {
     300           0 :                                     Navigator.popUntil(context, (route) => route.isFirst);
     301           0 :                                     Provider.of<FlwtchState>(context, listen: false).cwtch.ImportProfile(file.path, password).then((value) {
     302           0 :                                       if (value == "") {
     303           0 :                                         final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullyImportedProfile.replaceFirst("%profile", file.path)));
     304           0 :                                         ScaffoldMessenger.of(context).showSnackBar(snackBar);
     305             :                                       } else {
     306           0 :                                         final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.failedToImportProfile));
     307           0 :                                         ScaffoldMessenger.of(context).showSnackBar(snackBar);
     308             :                                       }
     309             :                                     });
     310             :                                   });
     311             :                                 },
     312           0 :                                 () {},
     313           0 :                                 () {},
     314             :                               );
     315             :                             },
     316             :                           ),
     317             :                         ),
     318             :                       ),
     319           0 :                       SizedBox(height: 20),
     320             :                     ],
     321             :                   ),
     322             :                 ),
     323             :               ),
     324             :             ),
     325             :           ),
     326             :         );
     327             :       },
     328             :     );
     329             :   }
     330             : 
     331           0 :   void _modalUnlockProfiles() {
     332           0 :     showModalBottomSheet<void>(
     333           0 :       context: context,
     334             :       isScrollControlled: true,
     335           0 :       builder: (BuildContext context) {
     336           0 :         return Padding(
     337           0 :           padding: MediaQuery.of(context).viewInsets,
     338           0 :           child: RepaintBoundary(
     339           0 :             child: Container(
     340           0 :               height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
     341           0 :               child: Center(
     342           0 :                 child: Padding(
     343           0 :                   padding: EdgeInsets.all(10.0),
     344           0 :                   child: Column(
     345             :                     mainAxisAlignment: MainAxisAlignment.center,
     346             :                     mainAxisSize: MainAxisSize.min,
     347           0 :                     children: <Widget>[
     348           0 :                       Text(AppLocalizations.of(context)!.enterProfilePassword),
     349           0 :                       SizedBox(height: 20),
     350           0 :                       CwtchPasswordField(
     351           0 :                         key: Key("unlockPasswordProfileElement"),
     352             :                         autofocus: true,
     353           0 :                         controller: ctrlrPassword,
     354           0 :                         action: unlock,
     355           0 :                         validator: (value) {
     356             :                           return null;
     357             :                         },
     358             :                       ),
     359           0 :                       SizedBox(height: 20),
     360           0 :                       Row(
     361             :                         mainAxisAlignment: MainAxisAlignment.spaceEvenly,
     362           0 :                         children: [
     363           0 :                           Spacer(),
     364           0 :                           Expanded(
     365           0 :                             child: ElevatedButton(
     366           0 :                               child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock),
     367           0 :                               onPressed: () {
     368           0 :                                 unlock(ctrlrPassword.value.text);
     369             :                               },
     370             :                             ),
     371             :                           ),
     372           0 :                           Spacer(),
     373             :                         ],
     374             :                       ),
     375             :                     ],
     376             :                   ),
     377             :                 ),
     378             :               ),
     379             :             ),
     380             :           ),
     381             :         );
     382             :       },
     383             :     );
     384             :   }
     385             : 
     386           0 :   void unlock(String password) {
     387           0 :     Provider.of<FlwtchState>(context, listen: false).cwtch.LoadProfiles(password);
     388           0 :     ctrlrPassword.text = "";
     389           0 :     Navigator.pop(context);
     390             :   }
     391             : 
     392           0 :   Widget _buildProfileManager() {
     393           0 :     return Consumer<ProfileListState>(
     394           0 :       builder: (context, pls, child) {
     395           0 :         var tiles = pls.profiles.map((ProfileInfoState profile) {
     396           0 :           return ChangeNotifierProvider<ProfileInfoState>.value(value: profile, builder: (context, child) => ProfileRow());
     397             :         });
     398             : 
     399           0 :         List<ChangeNotifierProvider<ProfileInfoState>> widgetTiles = tiles.toList(growable: true);
     400           0 :         widgetTiles.add(
     401           0 :           ChangeNotifierProvider<ProfileInfoState>.value(
     402           0 :             value: ProfileInfoState(onion: ""),
     403           0 :             builder: (context, child) {
     404           0 :               return Container(
     405           0 :                 margin: EdgeInsets.only(top: 20),
     406           0 :                 width: MediaQuery.of(context).size.width,
     407           0 :                 child: Row(
     408             :                   mainAxisAlignment: MainAxisAlignment.spaceAround,
     409           0 :                   children: [
     410           0 :                     Tooltip(
     411           0 :                       message: AppLocalizations.of(context)!.tooltipUnlockProfiles,
     412           0 :                       child: FilledButton.icon(
     413           0 :                         icon: Icon(CwtchIcons.lock_open_24px),
     414           0 :                         style: TextButton.styleFrom(
     415           0 :                           minimumSize: Size(MediaQuery.of(context).size.width * 0.79, 80),
     416           0 :                           maximumSize: Size(MediaQuery.of(context).size.width * 0.8, 80),
     417           0 :                           shape: RoundedRectangleBorder(
     418           0 :                             borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
     419             :                           ),
     420             :                         ),
     421           0 :                         label: Text(
     422           0 :                           AppLocalizations.of(context)!.unlock,
     423           0 :                           semanticsLabel: AppLocalizations.of(context)!.unlock,
     424           0 :                           style: TextStyle(fontWeight: FontWeight.bold),
     425             :                         ),
     426           0 :                         onPressed: () {
     427           0 :                           _modalUnlockProfiles();
     428             :                         },
     429             :                       ),
     430             :                     ),
     431             :                   ],
     432             :                 ),
     433             :               );
     434             :             },
     435             :           ),
     436             :         );
     437             : 
     438           0 :         final divided = ListTile.divideTiles(context: context, color: Provider.of<Settings>(context).theme.backgroundMainColor, tiles: widgetTiles).toList();
     439             : 
     440             :         // Display the welcome message / unlock profiles button to new accounts
     441           0 :         if (tiles.isEmpty) {
     442           0 :           return Center(
     443           0 :             child: Column(
     444             :               mainAxisSize: MainAxisSize.min,
     445             :               mainAxisAlignment: MainAxisAlignment.center,
     446             :               crossAxisAlignment: CrossAxisAlignment.center,
     447           0 :               children: [
     448           0 :                 Text(AppLocalizations.of(context)!.unlockProfileTip, textAlign: TextAlign.center),
     449           0 :                 Container(
     450           0 :                   width: MediaQuery.of(context).size.width,
     451           0 :                   margin: EdgeInsets.only(top: 20),
     452           0 :                   child: Row(
     453             :                     mainAxisAlignment: MainAxisAlignment.spaceAround,
     454           0 :                     children: [
     455           0 :                       Tooltip(
     456           0 :                         message: AppLocalizations.of(context)!.addProfileTitle,
     457           0 :                         child: FilledButton.icon(
     458           0 :                           icon: Icon(Icons.add),
     459           0 :                           style: TextButton.styleFrom(
     460           0 :                             minimumSize: Size(MediaQuery.of(context).size.width * 0.79, 80),
     461           0 :                             maximumSize: Size(MediaQuery.of(context).size.width * 0.8, 80),
     462           0 :                             shape: RoundedRectangleBorder(
     463           0 :                               borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
     464             :                             ),
     465             :                           ),
     466           0 :                           label: Text(
     467           0 :                             AppLocalizations.of(context)!.addProfileTitle,
     468           0 :                             semanticsLabel: AppLocalizations.of(context)!.addProfileTitle,
     469           0 :                             style: TextStyle(fontWeight: FontWeight.bold),
     470             :                           ),
     471           0 :                           onPressed: () {
     472           0 :                             _modalAddImportProfiles();
     473             :                           },
     474             :                         ),
     475             :                       ),
     476             :                     ],
     477             :                   ),
     478             :                 ),
     479           0 :                 widgetTiles[0],
     480             :               ],
     481             :             ),
     482             :           );
     483             :         }
     484             : 
     485           0 :         return ListView(children: divided);
     486             :       },
     487             :     );
     488             :   }
     489             : }

Generated by: LCOV version 1.14