diff --git a/.gitignore b/.gitignore index 9d532b1..6147231 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ app.*.symbols # Obfuscation related app.*.map.json + +libCwtch.so \ No newline at end of file diff --git a/SPEC.md b/SPEC.md index 6b13483..22ae056 100644 --- a/SPEC.md +++ b/SPEC.md @@ -6,17 +6,29 @@ Cwtch UI implementation. This functionality is implemented in libCwtch and so this work captures just the UI work required - any new Cwtch work is beyond the scope of this initial spec. +# Functional Requirements +- [ ] Kill all processes / isolates on exit +- [ ] Android Service? + # Splash Screen - [ ] Android - [ ] Investigate Lottie [example implementation blog](https://medium.com/swlh/native-splash-screen-in-flutter-using-lottie-121ce2b9b0a4) - [ ] Desktop +# Custom Styled Widgets +- [/] Label Widget + - [X] Initial + - [ ] With Accessibility / Zoom Integration +- [X] Text Field Widget +- [X] Password Widget +- [ ] Text Button Widget (for Copy) + ## Home Pane (formally Profile Pane) - [X] Unlock a profile with a password -- [ ] Create a new Profile +- [X] Create a new Profile - [X] With a password - - [ ] Without a password + - [X] Without a password - [X] Display all unlocked profiles - [X] Profile Picture - [X] default images diff --git a/lib/main.dart b/lib/main.dart index 0ab3ad9..057e59d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -72,7 +72,7 @@ class FlwtchState extends State { builder: (context, widget) { return Consumer( builder: (context, opaque, child) => MaterialApp( - locale: Locale("es",''), + locale: Locale("en",''), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, title: 'Cwtch', diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 8b6363a..a2e12c4 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_app/widgets/cwtchlabel.dart'; +import 'package:flutter_app/widgets/passwordfield.dart'; +import 'package:flutter_app/widgets/textfield.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../main.dart'; +import '../opaque.dart'; class AddEditProfileView extends StatefulWidget { - const AddEditProfileView({Key key, this.profileOnion}): super(key: key); + const AddEditProfileView({Key key, this.profileOnion}) : super(key: key); final String profileOnion; @override @@ -13,14 +17,19 @@ class AddEditProfileView extends StatefulWidget { } class _AddEditProfileViewState extends State { + final _formKey = GlobalKey(); + final ctrlrNick = TextEditingController(); - final ctrlrPass = TextEditingController(text:"be gay do crime"); + final ctrlrPass = TextEditingController(text: ""); + final ctrlrPass2 = TextEditingController(text: ""); TextEditingController ctrlrOnion; + bool usePassword; @override void initState() { super.initState(); - ctrlrOnion = TextEditingController(text:widget.profileOnion); + usePassword = true; + ctrlrOnion = TextEditingController(text: widget.profileOnion); } @override @@ -33,28 +42,130 @@ class _AddEditProfileViewState extends State { ); } + void _handleSwitchPassword(bool value) { + setState(() { + usePassword = value; + }); + } + + // A few implementation notes + // We use Visibility to hide optional structures when they are not requested. + // We used SizedBox for inter-widget height padding in columns, otherwise elements can render a little too close together. Widget _buildForm() { - return Center(child:Wrap( - direction: Axis.vertical, - spacing: 20.0, - runSpacing: 20.0, - children: [ - Text(AppLocalizations.of(context).displayNameLabel), - SizedBox(width:200, height: 60, child: TextField(controller: ctrlrNick,)), - widget.profileOnion == "" ? SizedBox(width:1,height:1,) : Text(AppLocalizations.of(context).addressLabel), - widget.profileOnion == "" ? SizedBox(width:1,height:1,) : SizedBox(width:200,height:60,child:TextField(controller: ctrlrOnion)), - Text(AppLocalizations.of(context).radioUsePassword), - Text(AppLocalizations.of(context).radioNoPassword), - Text(AppLocalizations.of(context).password1Label), - SizedBox(width:200, height: 60, child: TextField(controller: ctrlrPass,)), - Text(AppLocalizations.of(context).password2Label), - ElevatedButton(onPressed: _createPressed, child: Text(widget.profileOnion == "" ? AppLocalizations.of(context).addNewProfileBtn : AppLocalizations.of(context).saveProfileBtn),), - ], - )); + return Consumer(builder: (context, theme, child) { + return Form( + key: _formKey, + child: Container( + margin: EdgeInsets.all(30), + padding: EdgeInsets.all(20), + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + 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: widget.profileOnion != "", child: Text(AppLocalizations.of(context).addressLabel)), + Visibility(visible: widget.profileOnion != "", child: TextField(controller: ctrlrOnion)), + SizedBox( + height: 20, + ), + 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: [ + CwtchLabel(label: AppLocalizations.of(context).password1Label), + SizedBox( + height: 20, + ), + CwtchPasswordField( + controller: ctrlrPass, + validator: (value) { + if (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) { + if (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(widget.profileOnion == "" ? AppLocalizations.of(context).addNewProfileBtn : AppLocalizations.of(context).saveProfileBtn), + ) + ]))); + }); } void _createPressed() { - Provider.of(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, ctrlrPass.value.text); - Navigator.of(context).pop(); + // This will run all the validations in the form including + // checking that display name is not empty, and an actual check that the passwords + // match (and are provided if the user has requested an encrypted profile). + if (_formKey.currentState.validate()) { + if (usePassword == true) { + Provider + .of(context, listen: false) + .cwtch + .CreateProfile(ctrlrNick.value.text, ctrlrPass.value.text); + Navigator.of(context).pop(); + } else { + Provider + .of(context, listen: false) + .cwtch + .CreateProfile(ctrlrNick.value.text, "be gay do crime"); + Navigator.of(context).pop(); + } + } } } diff --git a/lib/widgets/cwtchlabel.dart b/lib/widgets/cwtchlabel.dart new file mode 100644 index 0000000..ea5d9f4 --- /dev/null +++ b/lib/widgets/cwtchlabel.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../opaque.dart'; + +// Provides a styled Label +// Callers must provide a label text +// TODO: Integrate this with a settings "zoom" / accessibility setting +class CwtchLabel extends StatefulWidget { + CwtchLabel({ this.label}); + final String label; + + @override + _CwtchLabelState createState() => _CwtchLabelState(); +} + +class _CwtchLabelState extends State { + + + @override + Widget build(BuildContext context) { + return Consumer ( + builder: (context, theme, child) { + return Text( + widget.label, + style: TextStyle(fontSize: 20, color: theme.current().mainTextColor()), + ); + }); + } +} diff --git a/lib/widgets/passwordfield.dart b/lib/widgets/passwordfield.dart new file mode 100644 index 0000000..efba916 --- /dev/null +++ b/lib/widgets/passwordfield.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../opaque.dart'; + +// Provides a styled Password Input Field for use in Form Widgets. +// Callers must provide a text controller, label helper text and a validator. +class CwtchPasswordField extends StatefulWidget { + CwtchPasswordField({this.controller, this.validator}); + final TextEditingController controller; + final FormFieldValidator validator; + + @override + _CwtchTextFieldState createState() => _CwtchTextFieldState(); +} + +class _CwtchTextFieldState extends State { + + + @override + Widget build(BuildContext context) { + return Consumer ( + builder: (context, theme, child) { + return TextFormField( + controller: widget.controller, + validator: widget.validator, + obscureText: true, enableSuggestions: false, autocorrect: false, + decoration: InputDecoration( + filled: true, + fillColor: theme.current().textfieldBackgroundColor(), + border: OutlineInputBorder(borderSide: BorderSide(color: theme.current().textfieldBorderColor()), borderRadius: BorderRadius.circular(15))), + style: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()), + ); + }); + } +} diff --git a/lib/widgets/textfield.dart b/lib/widgets/textfield.dart new file mode 100644 index 0000000..c49d768 --- /dev/null +++ b/lib/widgets/textfield.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../opaque.dart'; + + +// Provides a styled Text Field for use in Form Widgets. +// Callers must provide a text controller, label helper text and a validator. +class CwtchTextField extends StatefulWidget { + CwtchTextField({this.controller, this.labelText, this.validator}); + final TextEditingController controller; + final String labelText; + final FormFieldValidator validator; + + @override + _CwtchTextFieldState createState() => _CwtchTextFieldState(); +} + +class _CwtchTextFieldState extends State { + @override + Widget build(BuildContext context) { + return Consumer ( + builder: (context, theme, child) { + return TextFormField( + controller: widget.controller, + validator: widget.validator, + decoration: InputDecoration( + labelText: widget.labelText, + labelStyle: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()), + filled: true, + fillColor: theme.current().textfieldBackgroundColor(), + border: OutlineInputBorder(borderSide: BorderSide(color: theme.current().textfieldBorderColor()), borderRadius: BorderRadius.circular(15))), + style: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()), + ); + }); + } +}