Add Profile Form #13
|
@ -39,3 +39,6 @@ app.*.symbols
|
|||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
libCwtch.so
|
||||
android/cwtch/cwtch.aar
|
|
@ -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
|
||||
|
|
@ -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.
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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});
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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()),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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()),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue