Add Profile Form #13

Merged
erinn merged 14 commits from add_profile_form into trunk 2021-03-12 12:20:45 +00:00
16 changed files with 588 additions and 37 deletions

3
.gitignore vendored
View File

@ -39,3 +39,6 @@ app.*.symbols
# Obfuscation related
app.*.map.json
libCwtch.so
android/cwtch/cwtch.aar

126
SPEC.md Normal file
View File

@ -0,0 +1,126 @@
# Specification
This document outlines the minimal functionality necessary for us to consider Flwtch the canonical
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
- [X] Text Button Widget (for Copy)
## Home Pane (formally Profile Pane)
- [X] Unlock a profile with a password
- [X] Create a new Profile
- [X] With a password
- [X] Without a password
- [X] Display all unlocked profiles
- [X] Profile Picture
- [X] default images
- [ ] custom images
- [ ] coloured ring border
- [ ] Profile Name
- [X] local
- [ ] remote
- [X] Edit Button
- [ ] Unread messages badge
- [X] Navigate to a specific Profile Contacts Pane (when clicking on a Profile row)
- [X] Navigate to a specific Profile Management Pane (edit Button)
- [X] Navigate to the Settings Pane (Settings Button in Action bar)
## Settings Pane
- [X] Switch Dark / Light Theme
- [ ] Switch Language
- [ ] Enable/Disable Experiments
- [ ] Accessibility Settings (Zoom etc. - needs a deep dive into flutter)
- [ ] Display Build & Version Info
- [ ] Acknowledgements & Credits
## Profile Management Pane
- [X] Update Profile Name
- [X] Update Profile Password
- [ ] Error Message When Attempting to Update Password with Wrong Old Password
- [ ] Easy Transition from Unencrypted Profile -> Encrypted Profile
- [ ] Delete a Profile
- [X] Copy Profile Onion Address
## Profile Pane (formally Contacts Pane)
- [X] Display Profile-specific status
- [X] Profile Name
- [X] Online Status
- [X] Add Contact Button Navigates to Add Contact Pane
- [ ] Search Bar
- [ ] Search by name
- [ ] Search by Onion
- [ ] Display all Peer Contacts
- [X] Profile Picture
- [X] Name
- [X] Onion
- [X] Online Status
- [ ] Unread Messages Badge
- [ ] In Order of Most Recent Message / Activity
- [ ] With Accept / Reject Heart/Trash Bin Option
- [ ] Separate list area for Blocked Contacts
- [ ] Display all Group Contacts (if experiment is enabled)
- [X] Navigate to a specific Contact or Group Message Pane (Contact Row)
- [X] Pressing Back should go back to the home pane
## Add Contact Pane
- [ ] Allowing Copying the Profile Onion Address for Sharing
- [ ] Allowing Pasting a Peer Onion Address for adding to Contacts (with optional name field)
- [ ] Allowing Pasting a Group Invite / Server Address (if group experiment is enabled)
## Message Overlay
- [X] Display Messages from Contacts
- [ ] Allowing copying the text of a specific message (on mobile)
- [X] Send a message to the specific Contact / Group
- [~] Display the Acknowledgement status of a message
- [ ] Navigate to the specific Contact or Group Settings Pane ( Settings Button in Action bar)
- [ ] Emoji Support
- [ ] Display in-message emoji text labels e.g. `:label:` as emoji.
- [ ] Functional Emoji Drawer Widget for Selection
- [ ] Mutant Standard?
- [ ] Display a warning if Contact / Server is offline (Broken Heart)
- [ ] Display a warning for configuring peer history
- [X] Pressing Back should go back to the contacts pane
## List Overlay
- [ ] Add Item to List
- [ ] mark Item as Complete
- [ ] Delete Item from List
- [ ] Search List
## Contact Settings Pane
- [ ] Update local name of contact
- [ ] Copy contact onion address
- [ ] Block/Unblock a contact
- [ ] Configure Peer History Saving
- [X] Pressing Back should go back to the message pane
## Group Settings Pane (experimental)
- [ ] Gated behind group experiment
- [ ] Update local name of group
- [ ] Get Group Invite
- [ ] Leave Group
- [ ] Pressing Back should go back to the message pane for the group

View File

@ -127,6 +127,11 @@ class MainActivity: FlutterActivity() {
val end = (call.argument("end") as? Long) ?: 0;
result.success(Cwtch.getMessages(profile, handle, start, end))
}
"SendProfileEvent" -> {
val onion = (call.argument("onion") as? String) ?: "";
val jsonEvent = (call.argument("jsonEvent") as? String) ?: "";
Cwtch.sendProfileEvent(onion, jsonEvent);
}
else -> result.notImplemented()
}
}

Binary file not shown.

View File

@ -4,6 +4,7 @@ abstract class Cwtch {
void SelectProfile(String onion);
void CreateProfile(String nick, String pass);
void LoadProfiles(String pass);
void SendProfileEvent(String onion, String jsonEvent);
Future<String> ACNEvents();
Future<String> ContactEvents();

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter/rendering.dart';
import 'package:flutter_app/cwtch/cwtchNotifier.dart';
import 'package:path/path.dart' as path;
@ -26,6 +27,9 @@ typedef NextEventFn = void Function();
typedef string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length);
typedef StringFn = void Function(Pointer<Utf8> dir, int);
typedef string_string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef get_json_blob_void_function = Pointer<Utf8> Function();
typedef GetJsonBlobVoidFn = Pointer<Utf8> Function();
@ -206,4 +210,12 @@ class CwtchFfi implements Cwtch {
String jsonMessages = Utf8.fromUtf8(jsonMessagesBytes);
return jsonMessages;
}
@override
void SendProfileEvent(String onion, String json) {
var sendAppBusEvent = library.lookup<NativeFunction<string_string_to_void_function>>("c_SendProfileEvent");
// ignore: non_constant_identifier_names
final SendAppBusEvent = sendAppBusEvent.asFunction<StringStringFn>();
SendAppBusEvent(Utf8.toUtf8(onion), onion.length, Utf8.toUtf8(json), json.length);
}
}

View File

@ -100,4 +100,10 @@ class CwtchGomobile implements Cwtch {
return cwtchPlatform.invokeMethod("GetMessage", {"profile" : profile, "contact": handle, "start": start, "end": end});
}
@override
void SendProfileEvent(String onion, String jsonEvent) {
cwtchPlatform.invokeMethod("SendProfileEvent", {"onion" : onion, "jsonEvent": jsonEvent});
}
}

View File

@ -72,7 +72,7 @@ class FlwtchState extends State<Flwtch> {
builder: (context, widget) {
return Consumer<OpaqueTheme>(
builder: (context, opaque, child) => MaterialApp(
locale: Locale("es",''),
locale: Locale("en",''),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
title: 'Cwtch',

View File

@ -1,60 +1,288 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_app/model.dart';
import 'package:flutter_app/widgets/buttontextfield.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);
final String profileOnion;
const AddEditProfileView({Key key}) : super(key: key);
@override
_AddEditProfileViewState createState() => _AddEditProfileViewState();
}
class _AddEditProfileViewState extends State<AddEditProfileView> {
final ctrlrNick = TextEditingController();
final ctrlrPass = TextEditingController(text:"be gay do crime");
TextEditingController ctrlrOnion;
final _formKey = GlobalKey<FormState>();
final ctrlrNick = TextEditingController(text: "");
final ctrlrOldPass = TextEditingController(text: "");
final ctrlrPass = TextEditingController(text: "");
final ctrlrPass2 = TextEditingController(text: "");
final ctrlrOnion = TextEditingController(text: "");
bool usePassword;
@override
void initState() {
super.initState();
ctrlrOnion = TextEditingController(text:widget.profileOnion);
usePassword = true;
}
@override
Widget build(BuildContext context) {
final nickname = Provider.of<ProfileInfoState>(context).nickname;
if (nickname.isNotEmpty) {
ctrlrNick.text = nickname;
}
ctrlrOnion.text = Provider.of<ProfileInfoState>(context).onion;
return Scaffold(
appBar: AppBar(
title: Text(widget.profileOnion == "" ? AppLocalizations.of(context).addProfileTitle : AppLocalizations.of(context).editProfileTitle),
title: Text(Provider.of<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context).addProfileTitle : AppLocalizations.of(context).editProfileTitle),
),
body: _buildForm(),
);
}
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: <Widget>[
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<OpaqueTheme>(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<ProfileInfoState>(context).onion.isNotEmpty,
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
SizedBox(
width: 120,
height: 120,
child: ClipOval(
child: SizedBox(
width: 120,
height: 120,
child: Container(
color: Colors.white,
width: 120,
height: 120,
child: Image(
image: AssetImage("assets/" + Provider.of<ProfileInfoState>(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<ProfileInfoState>(context).onion.isNotEmpty,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(
height: 20,
),
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<ProfileInfoState>(context).onion.isEmpty,
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
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: <Widget>[
Visibility(
visible: Provider.of<ProfileInfoState>(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<ProfileInfoState>(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<ProfileInfoState>(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<ProfileInfoState>(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<ProfileInfoState>(context).onion.isEmpty ? AppLocalizations.of(context).addNewProfileBtn : AppLocalizations.of(context).saveProfileBtn),
)
])))));
});
});
}
void _copyOnion() {
Clipboard.setData(new ClipboardData(text: Provider.of<ProfileInfoState>(context, listen: false).onion));
// TODO Toast
}
void _createPressed() {
Provider.of<FlwtchState>(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 (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty) {
if (usePassword == true) {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, ctrlrPass.value.text);
Navigator.of(context).pop();
} else {
Provider.of<FlwtchState>(context, listen: false).cwtch.CreateProfile(ctrlrNick.value.text, "be gay do crime");
Navigator.of(context).pop();
}
} else {
// Profile Editing
if (ctrlrPass.value.text.isEmpty) {
// Don't update password, only update name
final event = {
"EventType": "SetAttribute",
"Data": {"Key": "public.name", "Data": ctrlrNick.value.text}
};
final json = jsonEncode(event);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(Provider.of<ProfileInfoState>(context, listen: false).onion, json);
Navigator.of(context).pop();
} else {
// At this points passwords have been validated to be the same and not empty
// Update both password and name, even if name hasn't been changed...
final updateNameEvent = {
"EventType": "SetAttribute",
"Data": {"Key": "public.name", "Data": ctrlrNick.value.text}
};
final updateNameEventJson = jsonEncode(updateNameEvent);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(Provider.of<ProfileInfoState>(context, listen: false).onion, updateNameEventJson);
final updatePasswordEvent = {
"EventType": "ChangePassword",
"Data": {"Password": ctrlrOldPass.text, "NewPassword": ctrlrPass.text}
};
final updatePasswordEventJson = jsonEncode(updatePasswordEvent);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(Provider.of<ProfileInfoState>(context, listen: false).onion, updatePasswordEventJson);
Navigator.of(context).pop();
}
}
}
}
}

View File

@ -44,7 +44,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
}
void _testChangingContactInfo() {
Provider.of<ProfileListState>(context, listen:false).onions.first.nickname = "yay!";
Provider.of<ProfileListState>(context, listen:false).notifyListeners();
}
void _pushGlobalSettings() {
@ -64,9 +64,11 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Provider (
create: (_) => Provider.of<FlwtchState>(context, listen: false),
child: AddEditProfileView(profileOnion: onion),
return MultiProvider (
providers: [
ChangeNotifierProvider<ProfileInfoState>(create: (_) => ProfileInfoState(onion: onion),),
],
builder: (context, widget) => AddEditProfileView(),
);
},
)

View File

@ -0,0 +1,50 @@
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 CwtchButtonTextField extends StatefulWidget {
CwtchButtonTextField({this.controller, this.onPressed, this.icon, this.tooltip});
final TextEditingController controller;
final Function onPressed;
final Icon icon;
final String tooltip;
@override
_CwtchButtonTextFieldState createState() => _CwtchButtonTextFieldState();
}
class _CwtchButtonTextFieldState extends State<CwtchButtonTextField> {
@override
Widget build(BuildContext context) {
return Consumer<OpaqueTheme>(builder: (context, theme, child) {
return TextField(
controller: widget.controller,
readOnly: true,
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: widget.onPressed,
icon: widget.icon,
tooltip: widget.tooltip,
enableFeedback: true,
color: theme.current().mainTextColor(),
highlightColor: theme.current().defaultButtonColor(),
focusColor: theme.current().defaultButtonActiveColor(),
splashColor: theme.current().defaultButtonActiveColor(),
),
floatingLabelBehavior: FloatingLabelBehavior.never,
filled: true,
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
errorStyle: TextStyle (color: theme.current().textfieldErrorColor(), fontWeight: FontWeight.bold,),
fillColor: theme.current().textfieldBackgroundColor(),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))),
style: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),
);
});
}
}

View File

@ -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<CwtchLabel> {
@override
Widget build(BuildContext context) {
return Consumer<OpaqueTheme> (
builder: (context, theme, child) {
return Text(
widget.label,
style: TextStyle(fontSize: 20, color: theme.current().mainTextColor()),
);
});
}
}

View File

@ -0,0 +1,43 @@
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<CwtchPasswordField> {
@override
Widget build(BuildContext context) {
return Consumer<OpaqueTheme>(builder: (context, theme, child) {
return TextFormField(
controller: widget.controller,
validator: widget.validator,
obscureText: true,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
errorStyle: TextStyle(
color: theme.current().textfieldErrorColor(),
fontWeight: FontWeight.bold,
),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
filled: true,
fillColor: theme.current().textfieldBackgroundColor(),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
),
style: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),
);
});
}
}

View File

@ -27,7 +27,7 @@ class _ProfileRowState extends State<ProfileRow> {
) ,
trailing: IconButton(
icon: Icon(Icons.create, color: Provider.of<OpaqueTheme>(context).current().mainTextColor()),
onPressed: () { _pushAddEditProfile(onion: profile.onion); },
onPressed: () { _pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath); },
),//(nb: Icons.create is a pencil and we use it for "edit", not create)
title: Text(
profile.nickname,
@ -68,13 +68,16 @@ class _ProfileRowState extends State<ProfileRow> {
);
}
void _pushAddEditProfile({onion: ""}) {
void _pushAddEditProfile({onion: "", displayName: "", profileImage: ""}) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Provider (
create: (_) => Provider.of<FlwtchState>(context, listen: false),
child: AddEditProfileView(profileOnion: onion),
return MultiProvider (
providers: [
ChangeNotifierProvider<ProfileInfoState>(create: (_) => ProfileInfoState(onion: onion, nickname:displayName, imagePath: profileImage),),
],
builder: (context, widget) => AddEditProfileView(),
);
},
)

View File

@ -0,0 +1,42 @@
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<CwtchTextField> {
@override
Widget build(BuildContext context) {
return Consumer<OpaqueTheme>(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()),
floatingLabelBehavior: FloatingLabelBehavior.never,
filled: true,
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0)),
focusedErrorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldErrorColor(), width: 3.0)),
errorStyle: TextStyle (color: theme.current().textfieldErrorColor(), fontWeight: FontWeight.bold,),
fillColor: theme.current().textfieldBackgroundColor(),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15.0), borderSide: BorderSide(color: theme.current().textfieldBorderColor(), width: 3.0))),
style: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()),
);
});
}
}

View File

@ -82,7 +82,7 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
linux-x64 ${CMAKE_BUILD_TYPE}
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS