Add/Edit Profile Screen with Themeing and Changing Profile

This commit is contained in:
Sarah Jamie Lewis 2021-03-04 13:21:40 -08:00
parent 138d34af2a
commit f9d0c44c96
10 changed files with 186 additions and 42 deletions

3
.gitignore vendored
View File

@ -40,4 +40,5 @@ app.*.symbols
# Obfuscation related
app.*.map.json
libCwtch.so
libCwtch.so
android/cwtch/cwtch.aar

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

@ -1,4 +1,8 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.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';
@ -9,8 +13,10 @@ 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, this.profileDisplayName, this.profileImage="profiles/001-centaur.png"}) : super(key: key);
final String profileOnion;
final String profileDisplayName;
final String profileImage;
@override
_AddEditProfileViewState createState() => _AddEditProfileViewState();
@ -19,7 +25,7 @@ class AddEditProfileView extends StatefulWidget {
class _AddEditProfileViewState extends State<AddEditProfileView> {
final _formKey = GlobalKey<FormState>();
final ctrlrNick = TextEditingController();
final ctrlrNick = TextEditingController(text: "");
final ctrlrPass = TextEditingController(text: "");
final ctrlrPass2 = TextEditingController(text: "");
TextEditingController ctrlrOnion;
@ -30,6 +36,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
super.initState();
usePassword = true;
ctrlrOnion = TextEditingController(text: widget.profileOnion);
ctrlrNick.text = widget.profileDisplayName;
}
@override
@ -58,7 +65,29 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
child: Container(
margin: EdgeInsets.all(30),
padding: EdgeInsets.all(20),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
child: SingleChildScrollView (child:Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, children: [
Visibility(
visible: widget.profileOnion.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/" + widget.profileImage),
width: 100,
height: 100,
))),
),
)
])),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context).displayNameLabel),
SizedBox(
@ -75,31 +104,52 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
},
),
]),
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: <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()),),
]),
Visibility(
visible: widget.profileOnion != "",
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: widget.profileOnion == "",
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>[
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
CwtchLabel(label: AppLocalizations.of(context).password1Label),
SizedBox(
height: 20,
@ -107,7 +157,8 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
CwtchPasswordField(
controller: ctrlrPass,
validator: (value) {
if (value.isEmpty && usePassword) {
// Password field can be empty when just updating the profile, not on creation
if (widget.profileOnion == "" && value.isEmpty && usePassword) {
return AppLocalizations.of(context).passwordErrorEmpty;
}
if (value != ctrlrPass2.value.text) {
@ -126,7 +177,8 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
CwtchPasswordField(
controller: ctrlrPass2,
validator: (value) {
if (value.isEmpty && usePassword) {
// Password field can be empty when just updating the profile, not on creation
if (widget.profileOnion == "" && value.isEmpty && usePassword) {
return AppLocalizations.of(context).passwordErrorEmpty;
}
if (value != ctrlrPass.value.text) {
@ -144,27 +196,43 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
style: ElevatedButton.styleFrom(primary: theme.current().defaultButtonColor()),
child: Text(widget.profileOnion == "" ? AppLocalizations.of(context).addNewProfileBtn : AppLocalizations.of(context).saveProfileBtn),
)
])));
]))));
});
}
void _copyOnion() {
Clipboard.setData(new ClipboardData(text: widget.profileOnion));
// TODO Toast
}
void _createPressed() {
// 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<FlwtchState>(context, listen: false)
.cwtch
.CreateProfile(ctrlrNick.value.text, ctrlrPass.value.text);
Navigator.of(context).pop();
if (widget.profileOnion.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 {
Provider
.of<FlwtchState>(context, listen: false)
.cwtch
.CreateProfile(ctrlrNick.value.text, "be gay do crime");
Navigator.of(context).pop();
// 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(widget.profileOnion, json);
Navigator.of(context).pop();
} else {
// TODO Update Password
}
}
}
}

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() {

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

@ -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,14 @@ 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),
child: AddEditProfileView(profileOnion: onion, profileDisplayName: displayName, profileImage: profileImage,),
);
},
)