diff --git a/SPEC.md b/SPEC.md index 7ed21bd..bda036d 100644 --- a/SPEC.md +++ b/SPEC.md @@ -59,6 +59,9 @@ required - any new Cwtch work is beyond the scope of this initial spec. - [ ] Error Message When Attempting to Update Password with Wrong Old Password - [ ] Easy Transition from Unencrypted Profile -> Encrypted Profile - [ ] Delete a Profile + - [ ] Dialog Acknowledgement + - [ ] Require Old Password Gate + - [ ] Async Checking of Password - [X] Copy Profile Onion Address ## Profile Pane (formally Contacts Pane) diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 9223a72..41652de 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -29,6 +29,7 @@ class _AddEditProfileViewState extends State { final ctrlrPass2 = TextEditingController(text: ""); final ctrlrOnion = TextEditingController(text: ""); bool usePassword; + bool deleted; @override void initState() { @@ -63,170 +64,187 @@ class _AddEditProfileViewState extends State { Widget _buildForm() { return Consumer(builder: (context, theme, child) { return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { - return SingleChildScrollView( - clipBehavior: Clip.antiAliasWithSaveLayer, - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Form( - key: _formKey, - child: Container( - margin: EdgeInsets.all(30), - padding: EdgeInsets.all(20), - child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Visibility( - visible: Provider.of(context).onion.isNotEmpty, - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - SizedBox( - width: 120, - height: 120, - child: ClipOval( - child: SizedBox( - width: 120, - height: 120, - child: Container( - color: Colors.white, + return Scrollbar( + isAlwaysShown: true, + child: SingleChildScrollView( + clipBehavior: Clip.antiAlias, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Form( + key: _formKey, + child: Container( + margin: EdgeInsets.all(30), + padding: EdgeInsets.all(20), + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Visibility( + visible: Provider.of(context).onion.isNotEmpty, + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + SizedBox( + width: 120, + height: 120, + child: ClipOval( + child: SizedBox( width: 120, height: 120, - child: Image( - image: AssetImage("assets/" + Provider.of(context).imagePath), - width: 100, - height: 100, - ))), - ), - ) - ])), - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - CwtchLabel(label: AppLocalizations.of(context).displayNameLabel), - SizedBox( - height: 20, - ), - CwtchTextField( - controller: ctrlrNick, - labelText: AppLocalizations.of(context).yourDisplayName, - validator: (value) { - if (value.isEmpty) { - return "Please enter a display name"; - } - return null; - }, - ), - ]), - Visibility( - visible: Provider.of(context).onion.isNotEmpty, - child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + child: Container( + color: Colors.white, + width: 120, + height: 120, + child: Image( + image: AssetImage("assets/" + Provider.of(context).imagePath), + width: 100, + height: 100, + ))), + ), + ) + ])), + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + CwtchLabel(label: AppLocalizations.of(context).displayNameLabel), SizedBox( height: 20, ), - CwtchLabel(label: AppLocalizations.of(context).addressLabel), - SizedBox( - height: 20, + CwtchTextField( + controller: ctrlrNick, + labelText: AppLocalizations.of(context).yourDisplayName, + validator: (value) { + if (value.isEmpty) { + return "Please enter a display name"; + } + return null; + }, ), - CwtchButtonTextField( - controller: ctrlrOnion, - onPressed: _copyOnion, - icon: Icon(Icons.copy), - tooltip: AppLocalizations.of(context).copyBtn, - ) - ])), - // We only allow setting password types on profile creation - Visibility( - visible: Provider.of(context).onion.isEmpty, - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - Radio( - value: false, - groupValue: usePassword, - onChanged: _handleSwitchPassword, - ), - Text( - AppLocalizations.of(context).radioNoPassword, - style: TextStyle(color: theme.current().mainTextColor()), - ), - Radio( - value: true, - groupValue: usePassword, - onChanged: _handleSwitchPassword, - ), - Text( - AppLocalizations.of(context).radioUsePassword, - style: TextStyle(color: theme.current().mainTextColor()), - ), - ])), - SizedBox( - height: 20, - ), - Visibility( - visible: usePassword, - child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + ]), Visibility( - visible: Provider.of(context, listen: false).onion.isNotEmpty, + visible: Provider.of(context).onion.isNotEmpty, child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - CwtchLabel(label: AppLocalizations.of(context).currentPasswordLabel), SizedBox( height: 20, ), - CwtchPasswordField( - controller: ctrlrOldPass, + CwtchLabel(label: AppLocalizations.of(context).addressLabel), + SizedBox( + height: 20, + ), + CwtchButtonTextField( + controller: ctrlrOnion, + onPressed: _copyOnion, + icon: Icon(Icons.copy), + tooltip: AppLocalizations.of(context).copyBtn, + ) + ])), + // We only allow setting password types on profile creation + Visibility( + visible: Provider.of(context).onion.isEmpty, + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Radio( + value: false, + groupValue: usePassword, + onChanged: _handleSwitchPassword, + ), + Text( + AppLocalizations.of(context).radioNoPassword, + style: TextStyle(color: theme.current().mainTextColor()), + ), + Radio( + value: true, + groupValue: usePassword, + onChanged: _handleSwitchPassword, + ), + Text( + AppLocalizations.of(context).radioUsePassword, + style: TextStyle(color: theme.current().mainTextColor()), + ), + ])), + SizedBox( + height: 20, + ), + Visibility( + visible: usePassword, + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + Visibility( + visible: Provider.of(context, listen: false).onion.isNotEmpty, + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + CwtchLabel(label: AppLocalizations.of(context).currentPasswordLabel), + SizedBox( + height: 20, + ), + CwtchPasswordField( + controller: ctrlrOldPass, + validator: (value) { + // Password field can be empty when just updating the profile, not on creation + if (Provider.of(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) { + return AppLocalizations.of(context).passwordErrorEmpty; + } + return null; + }, + ), + SizedBox( + height: 20, + ), + ])), + CwtchLabel(label: AppLocalizations.of(context).password1Label), + SizedBox( + height: 20, + ), + CwtchPasswordField( + controller: ctrlrPass, + validator: (value) { + // Password field can be empty when just updating the profile, not on creation + if (Provider.of(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) { + return AppLocalizations.of(context).passwordErrorEmpty; + } + if (value != ctrlrPass2.value.text) { + return AppLocalizations.of(context).passwordErrorMatch; + } + return null; + }, + ), + SizedBox( + height: 20, + ), + CwtchLabel(label: AppLocalizations.of(context).password2Label), + SizedBox( + height: 20, + ), + CwtchPasswordField( + controller: ctrlrPass2, validator: (value) { // Password field can be empty when just updating the profile, not on creation if (Provider.of(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) { return AppLocalizations.of(context).passwordErrorEmpty; } + if (value != ctrlrPass.value.text) { + return AppLocalizations.of(context).passwordErrorMatch; + } return null; - }, - ), + }), + ]), + ), + SizedBox( + height: 20, + ), + ElevatedButton( + onPressed: _createPressed, + style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()), + child: Text(Provider.of(context).onion.isEmpty ? AppLocalizations.of(context).addNewProfileBtn : AppLocalizations.of(context).saveProfileBtn), + ), + Visibility( + visible: Provider.of(context, listen: false).onion.isNotEmpty, + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [ SizedBox( height: 20, ), - ])), - CwtchLabel(label: AppLocalizations.of(context).password1Label), - SizedBox( - height: 20, - ), - CwtchPasswordField( - controller: ctrlrPass, - validator: (value) { - // Password field can be empty when just updating the profile, not on creation - if (Provider.of(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) { - return AppLocalizations.of(context).passwordErrorEmpty; - } - if (value != ctrlrPass2.value.text) { - return AppLocalizations.of(context).passwordErrorMatch; - } - return null; - }, - ), - SizedBox( - height: 20, - ), - CwtchLabel(label: AppLocalizations.of(context).password2Label), - SizedBox( - height: 20, - ), - CwtchPasswordField( - controller: ctrlrPass2, - validator: (value) { - // Password field can be empty when just updating the profile, not on creation - if (Provider.of(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) { - return AppLocalizations.of(context).passwordErrorEmpty; - } - if (value != ctrlrPass.value.text) { - return AppLocalizations.of(context).passwordErrorMatch; - } - return null; - }), - ]), - ), - SizedBox( - height: 20, - ), - ElevatedButton( - onPressed: _createPressed, - style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()), - child: Text(Provider.of(context).onion.isEmpty ? AppLocalizations.of(context).addNewProfileBtn : AppLocalizations.of(context).saveProfileBtn), - ) - ]))))); + ElevatedButton.icon( + onPressed: () { + showAlertDialog(context); + }, + style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()), + icon: Icon(Icons.delete_forever), + label: Text(AppLocalizations.of(context).deleteBtn), + ) + ])) + ])))))); }); }); } @@ -286,3 +304,54 @@ class _AddEditProfileViewState extends State { } } } + +showAlertDialog(BuildContext context) { + // set up the buttons + Widget cancelButton = TextButton( + child: Text("Cancel"), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()), + foregroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonTextColor()), + overlayColor: MaterialStateProperty.all(Opaque.current().defaultButtonActiveColor()), + padding: MaterialStateProperty.all(EdgeInsets.all(20))), + onPressed: () { + Navigator.of(context).pop(); // dismiss dialog + }, + ); + Widget continueButton = TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()), + foregroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonTextColor()), + overlayColor: MaterialStateProperty.all(Opaque.current().defaultButtonActiveColor()), + padding: MaterialStateProperty.all(EdgeInsets.all(20))), + child: Text(AppLocalizations.of(context).deleteProfileConfirmBtn), + onPressed: () { + // TODO Actually Delete the Peer + Navigator.of(context).pop(); // dismiss dialog + }, + ); + + // set up the AlertDialog + AlertDialog alert = AlertDialog( + backgroundColor: Provider.of(context, listen: false).current().backgroundPaneColor(), + title: Text(AppLocalizations.of(context).deleteProfileConfirmBtn), + titleTextStyle: TextStyle( + color: Provider.of(context, listen: false).current().mainTextColor(), + ), + contentTextStyle: TextStyle( + color: Provider.of(context, listen: false).current().mainTextColor(), + ), + actions: [ + cancelButton, + continueButton, + ], + ); + + // show the dialog + showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }, + ); +}