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

Generated by: LCOV version 1.14