contact wiring, merge trunk

This commit is contained in:
erinn 2021-03-12 04:39:19 -08:00
commit 62736cefea
35 changed files with 2752 additions and 841 deletions

3
.gitignore vendored
View File

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

51
SPEC.md
View File

@ -6,13 +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
- [X] 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
@ -28,20 +44,25 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [X] Navigate to the Settings Pane (Settings Button in Action bar)
## Settings Pane
- [X] Save/Load
- [X] Switch Dark / Light Theme
- [ ] Switch Language
- [X] Switch Language
- [ ] Enable/Disable Experiments
- [ ] Accessibility Settings (Zoom etc. - needs a deep dive into flutter)
- [ ] Display Build & Version Info
- [ ] Acknowledgements & Credits
- [X] Display Build & Version Info
- [X] Acknowledgements & Credits
## Profile Management Pane
- [X] Update Profile Name
- [ ] Update Profile Password
- [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
- [ ] Copy Profile Onion Address
- [ ] Dialog Acknowledgement
- [ ] Require Old Password Gate
- [ ] Async Checking of Password
- [X] Copy Profile Onion Address
## Profile Pane (formally Contacts Pane)
@ -49,6 +70,9 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [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
@ -57,6 +81,7 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [ ] 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
@ -66,7 +91,7 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [ ] 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 Pane
## Message Overlay
- [X] Display Messages from Contacts
- [ ] Allowing copying the text of a specific message (on mobile)
@ -81,6 +106,13 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [ ] 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
@ -94,3 +126,4 @@ required - any new Cwtch work is beyond the scope of this initial spec.
- [ ] Get Group Invite
- [ ] Leave Group
- [ ] Pressing Back should go back to the message pane for the group

View File

@ -127,6 +127,15 @@ 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);
}
"SendAppEvent" -> {
val jsonEvent = (call.argument("jsonEvent") as? String) ?: "";
Cwtch.sendAppEvent(jsonEvent);
}
else -> result.notImplemented()
}
}

Binary file not shown.

BIN
assets/cwtch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
assets/knott.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -4,6 +4,8 @@ abstract class Cwtch {
void SelectProfile(String onion);
void CreateProfile(String nick, String pass);
void LoadProfiles(String pass);
void SendProfileEvent(String onion, String jsonEvent);
void SendAppEvent(String jsonEvent);
Future<String> ACNEvents();
Future<String> ContactEvents();
@ -14,4 +16,4 @@ abstract class Cwtch {
Future<int> NumMessages(String profile, String handle);
Future<String> GetMessage(String profile, String handle, int index);
Future<String> GetMessages(String profile, String handle, int start, int end);
}
}

View File

@ -1,12 +1,20 @@
import 'dart:convert';
import 'package:provider/provider.dart';
import 'package:provider/provider.dart';
import '../model.dart';
import '../settings.dart';
// Class that handles libcwtch-go events (received either via ffi with an isolate or gomobile over a method channel from kotlin)
// Takes Notifiers and triggers them on appropriate events
class CwtchNotifier {
ProfileListState profileCN;
Settings settings;
CwtchNotifier(ProfileListState pcn) {
CwtchNotifier(ProfileListState pcn, Settings settingsCN) {
profileCN = pcn;
settings = settingsCN;
}
void handleMessage(String type, dynamic data) {
@ -42,6 +50,9 @@ class CwtchNotifier {
contact.status = data["ConnectionState"];
}
break;
case "UpdateGlobalSettings":
settings.handleUpdate(jsonDecode(data["Data"]));
break;
default:
print("unhandled event: $type");
}

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;
@ -14,38 +15,55 @@ import '../model.dart';
/// Cwtch API ///
/////////////////////
typedef start_cwtch_function = Void Function(Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StartCwtchFn = void Function(Pointer<Utf8> dir, int len, Pointer<Utf8> tor, int torLen);
typedef start_cwtch_function = Void Function(
Pointer<Utf8> str, Int32 length, Pointer<Utf8> str2, Int32 length2);
typedef StartCwtchFn = void Function(
Pointer<Utf8> dir, int len, Pointer<Utf8> tor, int torLen);
typedef void_from_string_string_function = Void Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringStringFn = void Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef void_from_string_string_function = Void Function(
Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef VoidFromStringStringFn = void Function(
Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef access_cwtch_eventbus_function = Void Function();
typedef NextEventFn = void Function();
typedef string_to_void_function = Void Function(Pointer<Utf8> str, Int32 length);
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();
typedef get_json_blob_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length);
typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str,int len);
typedef get_json_blob_string_function = Pointer<Utf8> Function(
Pointer<Utf8> str, Int32 length);
typedef GetJsonBlobStringFn = Pointer<Utf8> Function(
Pointer<Utf8> str, int len);
//func NumMessages(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int) (n C.int) {
typedef get_int_from_str_str_function = Int32 Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef GetIntFromStrStrFn = int Function(Pointer<Utf8>, int, Pointer<Utf8>, int);
typedef get_int_from_str_str_function = Int32 Function(
Pointer<Utf8>, Int32, Pointer<Utf8>, Int32);
typedef GetIntFromStrStrFn = int Function(
Pointer<Utf8>, int, Pointer<Utf8>, int);
//func GetMessage(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, message_index C.int) *C.char {
typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32);
typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int);
typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(
Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32);
typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(
Pointer<Utf8>, int, Pointer<Utf8>, int, int);
//func GetMessages(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, start C.int, end C.int) *C.char {
typedef get_json_blob_from_str_str_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32, Int32);
typedef GetJsonBlobFromStrStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int, int);
typedef get_json_blob_from_str_str_int_int_function = Pointer<Utf8> Function(
Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32, Int32);
typedef GetJsonBlobFromStrStrIntIntFn = Pointer<Utf8> Function(
Pointer<Utf8>, int, Pointer<Utf8>, int, int, int);
typedef acn_events_function = Pointer<Utf8> Function();
typedef ACNEventsFn = Pointer<Utf8> Function();
typedef ACNEventsFn = Pointer<Utf8> Function();
class CwtchFfi implements Cwtch {
DynamicLibrary library;
@ -68,14 +86,18 @@ class CwtchFfi implements Cwtch {
var cwtchDir = path.join(home, ".cwtch/dev/");
print("cwtchDir $cwtchDir");
var startCwtchC = library.lookup<NativeFunction<start_cwtch_function>>("c_StartCwtch");
var startCwtchC =
library.lookup<NativeFunction<start_cwtch_function>>("c_StartCwtch");
// ignore: non_constant_identifier_names
final StartCwtch = startCwtchC.asFunction<StartCwtchFn>();
StartCwtch(Utf8.toUtf8(cwtchDir), cwtchDir.length, Utf8.toUtf8(""), 0);
final ut8CwtchDir = cwtchDir.toNativeUtf8();
StartCwtch(ut8CwtchDir, ut8CwtchDir.length, "".toNativeUtf8(), 0);
// Spawn an isolate to listen to events from libcwtch-go and then dispatch them when received on main thread to cwtchNotifier
var _receivePort = ReceivePort();
cwtchIsolate = await Isolate.spawn(_checkAppbusEvents, _receivePort.sendPort);
cwtchIsolate =
await Isolate.spawn(_checkAppbusEvents, _receivePort.sendPort);
_receivePort.listen((message) {
var env = jsonDecode(message);
if (env["EventType"] != null) {//check if env["EventType"] exists
@ -106,109 +128,155 @@ class CwtchFfi implements Cwtch {
// Steam of appbus events. Call blocks in libcwtch-go GetAppbusEvent. Static so the isolate can use it
static Stream<String> pollAppbusEvents() async* {
var library = DynamicLibrary.open("libCwtch.so");
var getAppbusEventC = library.lookup<NativeFunction<acn_events_function>>("c_GetAppBusEvent");
var getAppbusEventC =
library.lookup<NativeFunction<acn_events_function>>("c_GetAppBusEvent");
final GetAppbusEvent = getAppbusEventC.asFunction<ACNEventsFn>();
while (true) {
Pointer<Utf8> result = GetAppbusEvent();
String event = Utf8.fromUtf8(result);
String event = result.toDartString();
yield event;
}
}
// ignore: non_constant_identifier_names
void SelectProfile(String onion) async {
var selectProfileC = library.lookup<NativeFunction<get_json_blob_string_function>>("c_SelectProfile");
var selectProfileC =
library.lookup<NativeFunction<get_json_blob_string_function>>(
"c_SelectProfile");
// ignore: non_constant_identifier_names
final SelectProfile = selectProfileC.asFunction<GetJsonBlobStringFn>();
SelectProfile(Utf8.toUtf8(onion), onion.length);
final ut8Onion = onion.toNativeUtf8();
SelectProfile(ut8Onion, ut8Onion.length);
}
// ignore: non_constant_identifier_names
void CreateProfile(String nick, String pass) {
var createProfileC = library.lookup<NativeFunction<void_from_string_string_function>>("c_CreateProfile");
var createProfileC =
library.lookup<NativeFunction<void_from_string_string_function>>(
"c_CreateProfile");
// ignore: non_constant_identifier_names
final CreateProfile = createProfileC.asFunction<VoidFromStringStringFn>();
CreateProfile(Utf8.toUtf8(nick), nick.length, Utf8.toUtf8(pass), pass.length);
final utf8nick = nick.toNativeUtf8();
final ut8pass = pass.toNativeUtf8();
CreateProfile(utf8nick, utf8nick.length, ut8pass, ut8pass.length);
}
// ignore: non_constant_identifier_names
void LoadProfiles(String pass) {
var loadProfileC = library.lookup<NativeFunction<string_to_void_function>>("c_LoadProfiles");
var loadProfileC = library
.lookup<NativeFunction<string_to_void_function>>("c_LoadProfiles");
// ignore: non_constant_identifier_names
final LoadProfiles = loadProfileC.asFunction<StringFn>();
LoadProfiles(Utf8.toUtf8(pass), pass.length);
final ut8pass = pass.toNativeUtf8();
LoadProfiles(ut8pass, ut8pass.length);
}
Future<String> ACNEvents() async {
var acnEventsC = library.lookup<NativeFunction<acn_events_function>>(
"c_ACNEvents");
var acnEventsC =
library.lookup<NativeFunction<acn_events_function>>("c_ACNEvents");
// ignore: non_constant_identifier_names
final ACNEvents = acnEventsC.asFunction<ACNEventsFn>();
Pointer<Utf8> result = ACNEvents();
String event = Utf8.fromUtf8(result);
String event = result.toDartString();
return event;
}
Future<String> ContactEvents() async {
var acnEventsC = library.lookup<NativeFunction<acn_events_function>>(
"c_ContactEvents");
var acnEventsC =
library.lookup<NativeFunction<acn_events_function>>("c_ContactEvents");
// ignore: non_constant_identifier_names
final ContactEvents = acnEventsC.asFunction<ACNEventsFn>();
Pointer<Utf8> result = ContactEvents();
String event = Utf8.fromUtf8(result);
String event = result.toDartString();
return event;
}
Future<String> GetProfiles() async {
var getProfilesC = library.lookup<NativeFunction<get_json_blob_void_function>>("c_GetProfiles");
var getProfilesC = library
.lookup<NativeFunction<get_json_blob_void_function>>("c_GetProfiles");
// ignore: non_constant_identifier_names
final GetProfiles = getProfilesC.asFunction<GetJsonBlobVoidFn>();
Pointer<Utf8> jsonProfilesBytes = GetProfiles();
String jsonProfiles = Utf8.fromUtf8(jsonProfilesBytes);
String jsonProfiles = jsonProfilesBytes.toDartString();
return jsonProfiles;
}
Future<String> GetContacts(String onion) async {
var getContactsC = library.lookup<NativeFunction<get_json_blob_string_function>>("c_GetContacts");
var getContactsC = library
.lookup<NativeFunction<get_json_blob_string_function>>("c_GetContacts");
// ignore: non_constant_identifier_names
final GetContacts = getContactsC.asFunction<GetJsonBlobStringFn>();
Pointer<Utf8> jsonContactBytes = GetContacts(Utf8.toUtf8(onion), onion.length);
String jsonContacts = Utf8.fromUtf8(jsonContactBytes);
final utf8onion = onion.toNativeUtf8();
Pointer<Utf8> jsonContactBytes = GetContacts(utf8onion, utf8onion.length);
String jsonContacts = jsonContactBytes.toDartString();
return jsonContacts;
}
Future<int> NumMessages(String profile, String handle) async {
var numMessagesC = library.lookup<NativeFunction<get_int_from_str_str_function>>("c_NumMessages");
var numMessagesC = library
.lookup<NativeFunction<get_int_from_str_str_function>>("c_NumMessages");
// ignore: non_constant_identifier_names
final NumMessages = numMessagesC.asFunction<GetIntFromStrStrFn>();
int num = NumMessages(Utf8.toUtf8(profile), profile.length, Utf8.toUtf8(handle), handle.length);
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
int num = NumMessages(
utf8profile, utf8profile.length, utf8handle, utf8handle.length);
return num;
}
Future<String> GetMessage(String profile, String handle, int index) async {
var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_str_int_function>>("c_GetMessage");
var getMessageC =
library.lookup<NativeFunction<get_json_blob_from_str_str_int_function>>(
"c_GetMessage");
// ignore: non_constant_identifier_names
final GetMessage = getMessageC.asFunction<GetJsonBlobFromStrStrIntFn>();
Pointer<Utf8> jsonMessageBytes = GetMessage(Utf8.toUtf8(profile), profile.length, Utf8.toUtf8(handle), handle.length, index);
String jsonMessage = Utf8.fromUtf8(jsonMessageBytes);
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessage(
utf8profile, utf8profile.length, utf8handle, utf8handle.length, index);
String jsonMessage = jsonMessageBytes.toDartString();
return jsonMessage;
}
Future<String> GetMessages(String profile, String handle, int start, int end) async {
var getMessagesC = library.lookup<NativeFunction<get_json_blob_from_str_str_int_int_function>>("c_GetMessages");
Future<String> GetMessages(
String profile, String handle, int start, int end) async {
var getMessagesC = library
.lookup<NativeFunction<get_json_blob_from_str_str_int_int_function>>(
"c_GetMessages");
// ignore: non_constant_identifier_names
final GetMessages = getMessagesC.asFunction<GetJsonBlobFromStrStrIntIntFn>();
Pointer<Utf8> jsonMessagesBytes = GetMessages(Utf8.toUtf8(profile), profile.length, Utf8.toUtf8(handle), handle.length, start, end);
String jsonMessages = Utf8.fromUtf8(jsonMessagesBytes);
final GetMessages =
getMessagesC.asFunction<GetJsonBlobFromStrStrIntIntFn>();
final utf8profile = profile.toNativeUtf8();
final utf8handle = handle.toNativeUtf8();
Pointer<Utf8> jsonMessagesBytes = GetMessages(utf8profile,
utf8profile.length, utf8handle, utf8handle.length, start, end);
String jsonMessages = jsonMessagesBytes.toDartString();
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>();
final utf8onion = onion.toNativeUtf8();
final utf8json = json.toNativeUtf8();
SendAppBusEvent(utf8onion, utf8onion.length, utf8json, utf8json.length);
}
@override
void SendAppEvent(String json) {
var sendAppBusEvent = library
.lookup<NativeFunction<string_to_void_function>>("c_SendAppEvent");
// ignore: non_constant_identifier_names
final SendAppBusEvent = sendAppBusEvent.asFunction<StringFn>();
final utf8json = json.toNativeUtf8();
SendAppBusEvent(utf8json, utf8json.length);
}
}

View File

@ -22,7 +22,8 @@ Future startCwtch() async {
*/
class CwtchGomobile implements Cwtch {
static const appInfoPlatform = const MethodChannel('test.flutter.dev/applicationInfo');
static const appInfoPlatform =
const MethodChannel('test.flutter.dev/applicationInfo');
static const cwtchPlatform = const MethodChannel('cwtch');
final appbusEventChannelName = 'test.flutter.dev/eventBus';
@ -47,7 +48,8 @@ class CwtchGomobile implements Cwtch {
var cwtchDir = path.join((await androidHomeDirectory).path, ".cwtch/dev/");
String torPath = path.join(await androidLibraryDir, "libtor.so");
print("gomobile.dart: Start invokeMethod Start($cwtchDir, $torPath)...");
cwtchPlatform.invokeMethod("Start", {"appDir": cwtchDir, "torPath": torPath});
cwtchPlatform
.invokeMethod("Start", {"appDir": cwtchDir, "torPath": torPath});
}
// Handle libcwtch-go events (received via kotlin) and dispatch to the cwtchNotifier
@ -58,7 +60,7 @@ class CwtchGomobile implements Cwtch {
}
void SelectProfile(String onion) {
cwtchPlatform.invokeMethod("SelectProfile", {"profile" : onion});
cwtchPlatform.invokeMethod("SelectProfile", {"profile": onion});
}
void CreateProfile(String nick, String pass) {
@ -83,20 +85,34 @@ class CwtchGomobile implements Cwtch {
}
Future<String> GetContacts(String onion) {
return cwtchPlatform.invokeMethod("GetContacts", {"profile" : onion});
return cwtchPlatform.invokeMethod("GetContacts", {"profile": onion});
}
Future<int> NumMessages(String profile, String handle) {
return cwtchPlatform.invokeMethod("NumMessages", {"profile" : profile, "contact": handle});
return cwtchPlatform
.invokeMethod("NumMessages", {"profile": profile, "contact": handle});
}
Future<String> GetMessage(String profile, String handle, int index) {
print("gomobile.dart GetMessage " + index.toString());
return cwtchPlatform.invokeMethod("GetMessage", {"profile" : profile, "contact": handle, "index": index});
return cwtchPlatform.invokeMethod(
"GetMessage", {"profile": profile, "contact": handle, "index": index});
}
Future<String> GetMessages(String profile, String handle, int start, int end) {
return cwtchPlatform.invokeMethod("GetMessage", {"profile" : profile, "contact": handle, "start": start, "end": end});
Future<String> GetMessages(
String profile, String handle, int start, int end) {
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});
}
@override
void SendAppEvent(String jsonEvent) {
cwtchPlatform.invokeMethod("SendAppEvent", {"jsonEvent": jsonEvent});
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter_app/cwtch/ffi.dart';
import 'package:flutter_app/cwtch/gomobile.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/settings.dart';
import 'package:flutter_app/views/triplecolview.dart';
import 'package:provider/provider.dart';
import 'cwtch/cwtch.dart';
@ -12,6 +13,8 @@ import 'dart:io' show Platform;
import 'opaque.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
var GlobalSettings = Settings(Locale("en", ''), Opaque.dark);
void main() => runApp(Flwtch());
class Flwtch extends StatefulWidget {
@ -36,8 +39,9 @@ class FlwtchState extends State<Flwtch> {
initState() {
super.initState();
cwtchInit = false;
profs = ProfileListState();
var cwtchNotifier = new CwtchNotifier(profs);
var cwtchNotifier = new CwtchNotifier(profs, GlobalSettings);
if (Platform.isAndroid) {
cwtch = CwtchGomobile(cwtchNotifier);
@ -54,25 +58,28 @@ class FlwtchState extends State<Flwtch> {
appStatus = AppModel(cwtch: cwtch);
}
ChangeNotifierProvider<OpaqueTheme> getOpaqueProvider() => ChangeNotifierProvider(create: (context) => OpaqueTheme(Opaque.dark));
Provider<FlwtchState> getFlwtchStateProvider() => Provider<FlwtchState>(create: (_) => this);
ChangeNotifierProvider<ProfileListState> getProfileListProvider() => ChangeNotifierProvider(create: (context) => profs);
ChangeNotifierProvider<Settings> getSettingsProvider() =>
ChangeNotifierProvider(create: (context) => GlobalSettings);
Provider<FlwtchState> getFlwtchStateProvider() =>
Provider<FlwtchState>(create: (_) => this);
ChangeNotifierProvider<ProfileListState> getProfileListProvider() =>
ChangeNotifierProvider(create: (context) => profs);
@override
Widget build(BuildContext context) {
appStatus = AppModel(cwtch: cwtch);
final newTextTheme = Theme.of(context).textTheme.apply(
bodyColor: Opaque.current().mainTextColor(),
displayColor: Opaque.current().mainTextColor(),
);
return MultiProvider(
providers: [getFlwtchStateProvider(), getProfileListProvider(), getOpaqueProvider()],
providers: [
getFlwtchStateProvider(),
getProfileListProvider(),
getSettingsProvider()
],
builder: (context, widget) {
return Consumer<OpaqueTheme>(
Provider.of<Settings>(context).initPackageInfo();
return Consumer<Settings>(
builder: (context, opaque, child) => MaterialApp(
locale: Locale("es",''),
locale: Provider.of<Settings>(context).locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
title: 'Cwtch',
@ -83,11 +90,47 @@ class FlwtchState extends State<Flwtch> {
canvasColor: opaque.current().backgroundPaneColor(),
accentColor: opaque.current().defaultButtonColor(),
buttonColor: opaque.current().defaultButtonColor(),
textTheme: newTextTheme,
backgroundColor: opaque.current().backgroundMainColor(),
iconTheme: IconThemeData(
color: opaque.current().mainTextColor(),
),
cardColor: opaque.current().backgroundMainColor(),
textButtonTheme: TextButtonThemeData(
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))),
),
dialogTheme: DialogTheme(
backgroundColor: opaque.current().backgroundPaneColor(),
titleTextStyle:
TextStyle(color: opaque.current().mainTextColor()),
contentTextStyle:
TextStyle(color: opaque.current().mainTextColor())),
textTheme: TextTheme(
headline1: TextStyle(color: opaque.current().mainTextColor()),
headline2: TextStyle(color: opaque.current().mainTextColor()),
headline3: TextStyle(color: opaque.current().mainTextColor()),
headline4: TextStyle(color: opaque.current().mainTextColor()),
headline5: TextStyle(color: opaque.current().mainTextColor()),
headline6: TextStyle(color: opaque.current().mainTextColor()),
bodyText1: TextStyle(color: opaque.current().mainTextColor()),
bodyText2: TextStyle(color: opaque.current().mainTextColor()),
subtitle1: TextStyle(color: opaque.current().mainTextColor()),
subtitle2: TextStyle(color: opaque.current().mainTextColor()),
caption: TextStyle(color: opaque.current().mainTextColor()),
button: TextStyle(color: opaque.current().mainTextColor()),
overline: TextStyle(color: opaque.current().mainTextColor())),
),
// from dan: home: cwtchInit == true ? ProfileMgrView(cwtch) : SplashView(),
// from erinn: home: columns.length == 3 ? TripleColumnView() : ProfileMgrView(),
home: cwtchInit == true ? (columns.length == 3 ? TripleColumnView() : ProfileMgrView()) : SplashView(),
home: cwtchInit == true
? (columns.length == 3 ? TripleColumnView() : ProfileMgrView())
: SplashView(),
),
);
},

View File

@ -29,7 +29,13 @@ class ContactModel {
String status;
String imagePath;
ContactModel({this.onion, this.nickname, this.status, this.isInvitation, this.isBlocked, this.imagePath});
ContactModel(
{this.onion,
this.nickname,
this.status,
this.isInvitation,
this.isBlocked,
this.imagePath});
}
//todo: delete
@ -52,8 +58,7 @@ class ChatMessage {
: o = json['o'],
d = json['d'];
Map<String, dynamic> toJson() =>
{
Map<String, dynamic> toJson() => {
'o': o,
'd': d,
};
@ -203,7 +208,16 @@ class ContactInfoState extends ChangeNotifier {
String _imagePath;
int _unreadMessages = 0;
ContactInfoState({this.profileOnion, this.onion, nickname = "", isGroup = false, isInvitation = false, isBlocked = false, status = "", imagePath = "",}) {
ContactInfoState({
this.profileOnion,
this.onion,
nickname = "",
isGroup = false,
isInvitation = false,
isBlocked = false,
status = "",
imagePath = "",
}) {
this._nickname = nickname;
this._isGroup = isGroup;
this._isInvitation = isInvitation;
@ -247,8 +261,6 @@ class ContactInfoState extends ChangeNotifier {
/// ACN ///
/////////////
class AppModel {
final Cwtch cwtch;
AppModel({this.cwtch});

File diff suppressed because it is too large Load Diff

71
lib/settings.dart Normal file
View File

@ -0,0 +1,71 @@
import 'dart:ui';
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'opaque.dart';
class Settings extends ChangeNotifier {
Locale locale;
PackageInfo packageInfo;
OpaqueThemeType theme;
void setDark() {
theme = Opaque.dark;
notifyListeners();
}
void setLight() {
theme = Opaque.light;
notifyListeners();
}
OpaqueThemeType current() {
return theme;
}
handleUpdate(dynamic settings) {
print("Settings ${settings}");
switchLocale(Locale(settings["Locale"]));
if (settings["Theme"] == "light") {
this.setLight();
} else {
this.setDark();
}
notifyListeners();
}
initPackageInfo() {
PackageInfo.fromPlatform().then((PackageInfo newPackageInfo) {
packageInfo = newPackageInfo;
notifyListeners();
});
}
switchLocale(Locale newLocale) {
locale = newLocale;
notifyListeners();
}
Settings(this.locale, this.theme);
dynamic asJson() {
var themeString = "light";
if (theme == Opaque.dark) {
themeString = "dark";
}
return {
"Locale": this.locale.languageCode,
"Theme": themeString,
"PreviousPid": -1,
"ExperimentsEnabled": false,
"Experiments": {},
"StateRootPane": 0,
"FirstTime": false
};
}
}

View File

@ -18,13 +18,14 @@ class _AddContactViewState extends State<AddContactView> {
}
Widget _buildForm() {
return Center(child:Wrap(
return Center(
child: Wrap(
direction: Axis.vertical,
spacing: 20.0,
runSpacing: 20.0,
children: <Widget>[
Text(AppLocalizations.of(context).profileName),
Text("peer handle or group invite or server bundle"),//todo
Text("peer handle or group invite or server bundle"), //todo
Text(AppLocalizations.of(context).createGroupBtn),
],
));

View File

@ -1,60 +1,505 @@
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';
import '../settings.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;
bool deleted;
@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<Settings>(builder: (context, theme, child) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints viewportConstraints) {
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<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),
),
Visibility(
visible: Provider.of<ProfileInfoState>(
context,
listen: false)
.onion
.isNotEmpty,
child: Column(
mainAxisAlignment:
MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
SizedBox(
height: 20,
),
ElevatedButton.icon(
onPressed: () {
showAlertDialog(context);
},
style: ElevatedButton.styleFrom(
primary: theme
.current()
.defaultButtonColor()),
icon: Icon(Icons.delete_forever),
label: Text(
AppLocalizations.of(context)
.deleteBtn),
)
]))
]))))));
});
});
}
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();
}
}
}
}
}
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(
title: Text(AppLocalizations.of(context).deleteProfileConfirmBtn),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}

View File

@ -14,34 +14,20 @@ class ContactsView extends StatefulWidget {
}
class _ContactsViewState extends State<ContactsView> {
_ContactsViewState();
// Map<String, ContactModel> _contacts = new HashMap<String, ContactModel>();
// @override
// void didChangeDependencies() {
// super.didChangeDependencies();
//
// Provider.of<ContactListState>(context).onions.forEach((contact) {
// _contacts.putIfAbsent(contact.onion, () => ContactModel(contact);
// });
// .cwtch.GetContacts(widget.profile.onion).then((jsonContacts) {
// print("got contact: $jsonContacts");
// setState(() {
// List<dynamic> contacts = jsonDecode(jsonContacts);
// contacts.forEach((onion) {
// _contacts.putIfAbsent(onion['onion'], () => ContactModel(onion: onion['onion'], nickname: onion['name'], status: onion['status']));
// });
// });
// });
// }
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("%1's contacts".replaceAll("%1", Provider.of<ProfileInfoState>(context).nickname ?? Provider.of<ProfileInfoState>(context).onion ?? '')),//todo
title: Text("%1's contacts".replaceAll(
"%1",
Provider.of<ProfileInfoState>(context).nickname ??
Provider.of<ProfileInfoState>(context).onion ??
'')), //todo
actions: [
IconButton(icon: Icon(Icons.copy), onPressed: _copyOnion,),
IconButton(
icon: Icon(Icons.copy),
onPressed: _copyOnion,
),
],
),
floatingActionButton: FloatingActionButton(
@ -54,33 +40,28 @@ class _ContactsViewState extends State<ContactsView> {
}
Widget _buildContactList() {
// return StreamBuilder<String>(
// stream: Provider.of<FlwtchState>(context).appStatus.contactEvents(),
// builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
final tiles = Provider.of<ContactListState>(context).contacts.map((ContactInfoState contact) {
return ChangeNotifierProvider<ContactInfoState>.value(value: contact, child: ContactRow());
});
final divided = ListTile.divideTiles(context: context, tiles: tiles, ).toList();
return ListView(children: divided);
// },
// );
final tiles = Provider.of<ContactListState>(context).contacts.map((ContactInfoState contact) {
return ChangeNotifierProvider<ContactInfoState>.value(value: contact, child: ContactRow());
});
final divided = ListTile.divideTiles(context: context, tiles: tiles, ).toList();
return ListView(children: divided);
}
void _pushAddContact() {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Provider (
create: (_) => Provider.of<FlwtchState>(context),
child: AddContactView(),
);
},
)
);
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return Provider(
create: (_) => Provider.of<FlwtchState>(context),
child: AddContactView(),
);
},
));
}
void _copyOnion() {
final snackBar = SnackBar(content: Text(AppLocalizations.of(context).copiedClipboardNotification));//todo
final snackBar = SnackBar(
content: Text(
AppLocalizations.of(context).copiedClipboardNotification)); //todo
// Find the Scaffold in the widget tree and use it to show a SnackBar.
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}

View File

@ -15,19 +15,23 @@ class _DoubleColumnViewState extends State<DoubleColumnView> {
Widget build(BuildContext context) {
var flwtch = Provider.of<FlwtchState>(context);
return Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
flex: flwtch.columns[0],
child: ContactsView(),
),
Flexible(
flex: flwtch.columns[1],
child: flwtch.selectedConversation == "" ?
Center(child:Text("pick a contact")) : //dev
Container(child:MessageView(profile:flwtch.selectedProfile, conversationHandle:flwtch.selectedConversation)),
),
],
direction: Axis.horizontal,
children: <Widget>[
Flexible(
flex: flwtch.columns[0],
child: ContactsView(),
),
Flexible(
flex: flwtch.columns[1],
child: flwtch.selectedConversation == ""
? Center(child: Text("pick a contact"))
: //dev
Container(
child: MessageView(
profile: flwtch.selectedProfile,
conversationHandle: flwtch.selectedConversation)),
),
],
);
}
}

View File

@ -1,19 +1,22 @@
import 'dart:convert';
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/opaque.dart';
import 'package:flutter_app/settings.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
class GlobalSettingsView extends StatefulWidget {
@override
_GlobalSettingsViewState createState() => _GlobalSettingsViewState();
}
class _GlobalSettingsViewState extends State<GlobalSettingsView> {
final myController = TextEditingController();
@override
void dispose() {
myController.dispose();
super.dispose();
}
@ -28,40 +31,106 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
}
Widget _buildSettingsList() {
return Consumer<OpaqueTheme>(
builder: (context, theme, child) {
return Center(child: Column(
children: [
Text(AppLocalizations.of(context).settingLanguage),
TextField(
controller: myController,
onChanged: (text) {
print("First text field: $text");
},
),
Text(AppLocalizations.of(context).settingInterfaceZoom),
SwitchListTile(
title: Text('Theme',
style: TextStyle(color: theme.current().mainTextColor())),
value: theme.current() == Opaque.light,
onChanged: (bool value) {
if (value) {
theme.setLight();
} else {
theme.setDark();
}
},
secondary: Icon(Icons.lightbulb_outline,
color: theme.current().mainTextColor()),
),
Text(AppLocalizations.of(context).experimentsEnabled),
Text("Text magnification reference"),//dev
Text("Acknowledgements"),//todo
Text(AppLocalizations.of(context).version),
Text(AppLocalizations.of(context).builddate),
]
));
}
);
return Consumer<Settings>(builder: (context, theme, child) {
return Center(
child: Column(children: [
ListTile(
title: Text(AppLocalizations.of(context).settingLanguage,
style: TextStyle(color: theme.current().mainTextColor())),
leading:
Icon(Icons.language, color: theme.current().mainTextColor()),
trailing: DropdownButton(
value: Provider.of<Settings>(context).locale.languageCode,
onChanged: (String newValue) {
setState(() {
var settings =
Provider.of<Settings>(context, listen: false);
settings.switchLocale(Locale(newValue, ''));
saveSettings(context);
});
},
items: AppLocalizations.supportedLocales
.map<DropdownMenuItem<String>>((Locale value) {
return DropdownMenuItem<String>(
value: value.languageCode,
child: Text(getLanguageFull(context, value.languageCode)),
);
}).toList())),
SwitchListTile(
title: Text(AppLocalizations.of(context).settingTheme,
style: TextStyle(color: theme.current().mainTextColor())),
value: theme.current() == Opaque.light,
onChanged: (bool value) {
if (value) {
theme.setLight();
} else {
theme.setDark();
}
// Save Settings...
saveSettings(context);
},
secondary: Icon(Icons.lightbulb_outline,
color: theme.current().mainTextColor()),
),
AboutListTile(
icon: Icon(Icons.info, color: theme.current().mainTextColor()),
applicationIcon: Padding(
padding: EdgeInsets.all(20),
child: Image(
image: AssetImage("assets/knott.png"),
width: 128,
height: 128,
)),
applicationName: "Cwtch (Flutter UI)",
applicationVersion: AppLocalizations.of(context).version.replaceAll(
"%1",
constructVersionString(
Provider.of<Settings>(context).packageInfo)),
applicationLegalese: '\u{a9} 2021 Open Privacy Research Society',
),
]));
});
}
}
String constructVersionString(PackageInfo pinfo) {
if (pinfo == null) {
return "";
}
return pinfo.version + "." + pinfo.buildNumber;
}
String getLanguageFull(context, String languageCode) {
if (languageCode == "en") {
return AppLocalizations.of(context).localeEn;
}
if (languageCode == "es") {
return AppLocalizations.of(context).localeEs;
}
if (languageCode == "fr") {
return AppLocalizations.of(context).localeFr;
}
if (languageCode == "pt") {
return AppLocalizations.of(context).localePt;
}
if (languageCode == "de") {
return AppLocalizations.of(context).localeDe;
}
if (languageCode == "it") {
return AppLocalizations.of(context).localeIt;
}
return languageCode;
}
saveSettings(context) {
var settings = Provider.of<Settings>(context, listen: false);
final updateSettingsEvent = {
"EventType": "UpdateGlobalSettings",
"Data": {"Data": jsonEncode(settings.asJson())},
};
final updateSettingsEventJson = jsonEncode(updateSettingsEvent);
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.SendAppEvent(updateSettingsEventJson);
}

View File

@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import '../model.dart';
@ -6,7 +5,8 @@ import '../opaque.dart';
import '../widgets/messagelist.dart';
class MessageView extends StatefulWidget {
const MessageView({Key key, this.profile, this.conversationHandle}) : super(key: key);
const MessageView({Key key, this.profile, this.conversationHandle})
: super(key: key);
final ProfileInfoState profile;
final String conversationHandle;
@ -25,7 +25,7 @@ class _MessageViewState extends State<MessageView> {
@override
Widget build(BuildContext context) {
return Scaffold (
return Scaffold(
appBar: AppBar(
title: Text(widget.conversationHandle),
actions: [
@ -35,12 +35,14 @@ class _MessageViewState extends State<MessageView> {
IconButton(icon: Icon(Icons.settings), onPressed: _pushConvoSettings),
],
),
body: MessageList(profile: widget.profile, conversationHandle: widget.conversationHandle),
body: MessageList(
profile: widget.profile,
conversationHandle: widget.conversationHandle),
bottomSheet: _buildComposeBox(),
);
}
void _pushConvoSettings(){}
void _pushConvoSettings() {}
void _sendMessage() {
ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text);
@ -49,37 +51,38 @@ class _MessageViewState extends State<MessageView> {
}
Widget _buildComposeBox() {
return Container (
return Container(
color: Opaque.current().backgroundMainColor(),
height: 100,
child: Row(
children: <Widget>[
Expanded (
child:TextField(controller:ctrlrCompose)
),
Expanded(child: TextField(controller: ctrlrCompose)),
SizedBox(
width: 100,
height: 80,
child: Column(
children: <Widget>[
ElevatedButton(
child: Icon(Icons.send, color: Opaque.current().mainTextColor()),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()),
), onPressed: _sendMessage,
child: Column(children: <Widget>[
ElevatedButton(
child:
Icon(Icons.send, color: Opaque.current().mainTextColor()),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Opaque.current().defaultButtonColor()),
),
Row (
children: <Widget>[
SizedBox(width:45, child:ElevatedButton(
child: Icon(Icons.emoji_emotions_outlined, color: Opaque.current().mainTextColor())
)),
SizedBox(width:45, child:ElevatedButton(
child: Icon(Icons.attach_file, color: Opaque.current().mainTextColor())
)),
]
)
]
),
onPressed: _sendMessage,
),
Row(children: <Widget>[
SizedBox(
width: 45,
child: ElevatedButton(
child: Icon(Icons.emoji_emotions_outlined,
color: Opaque.current().mainTextColor()))),
SizedBox(
width: 45,
child: ElevatedButton(
child: Icon(Icons.attach_file,
color: Opaque.current().mainTextColor()))),
])
]),
),
],
),

View File

@ -25,13 +25,19 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
@override
Widget build(BuildContext context) {
return Scaffold (
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).profileName),
actions: [
IconButton(icon: Icon(Icons.bug_report_outlined), onPressed: _testChangingContactInfo),
IconButton(icon: Icon(Icons.lock_open), onPressed: _modalUnlockProfiles,),
IconButton(icon: Icon(Icons.settings), onPressed: _pushGlobalSettings),
IconButton(
icon: Icon(Icons.bug_report_outlined),
onPressed: _testChangingContactInfo),
IconButton(
icon: Icon(Icons.lock_open),
onPressed: _modalUnlockProfiles,
),
IconButton(
icon: Icon(Icons.settings), onPressed: _pushGlobalSettings),
],
),
floatingActionButton: FloatingActionButton(
@ -39,7 +45,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
tooltip: AppLocalizations.of(context).addNewProfileBtn,
child: const Icon(Icons.add),
),
body: _buildProfileManager(),//_buildSuggestions(),
body: _buildProfileManager(), //_buildSuggestions(),
);
}
@ -48,64 +54,65 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
}
void _pushGlobalSettings() {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Provider (
create: (_) => Provider.of<FlwtchState>(context),
child: GlobalSettingsView(),
);
},
)
);
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return Provider(
create: (_) => Provider.of<FlwtchState>(context, listen: false),
child: GlobalSettingsView(),
);
},
));
}
void _pushAddEditProfile({onion: ""}) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Provider (
create: (_) => Provider.of<FlwtchState>(context, listen: false),
child: AddEditProfileView(profileOnion: onion),
);
},
)
);
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<ProfileInfoState>(
create: (_) => ProfileInfoState(onion: onion),
),
],
builder: (context, widget) => AddEditProfileView(),
);
},
));
}
void _modalUnlockProfiles() {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return Container(
height: 200, // bespoke value courtesy of the [TextField] docs
color: Colors.pink[50],
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(context).enterProfilePassword),
TextField(
obscureText: true,
controller: ctrlrPassword,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: AppLocalizations.of(context).password1Label,
return Container(
height: 200, // bespoke value courtesy of the [TextField] docs
color: Colors.pink[50],
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(AppLocalizations.of(context).enterProfilePassword),
TextField(
obscureText: true,
controller: ctrlrPassword,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: AppLocalizations.of(context).password1Label,
),
),
),
ElevatedButton(
child: Text(AppLocalizations.of(context).unlock),
onPressed: () {
Provider.of<FlwtchState>(context, listen: false).cwtch.LoadProfiles(ctrlrPassword.value.text);
Navigator.pop(context);
},
),
],
)
),
);
});
ElevatedButton(
child: Text(AppLocalizations.of(context).unlock),
onPressed: () {
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.LoadProfiles(ctrlrPassword.value.text);
Navigator.pop(context);
},
),
],
)),
);
});
}
Widget _buildProfileManager() {
@ -125,4 +132,4 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
return ListView(children: divided);
}
}
}

View File

@ -6,13 +6,8 @@ class SplashView extends StatelessWidget {
Widget build(BuildContext context) {
print("SplashView build()");
return Scaffold(
appBar: AppBar( title: Text("Cwtch")),
body: Center(
child: Column(
children: <Widget>[
Text("Loading Cwtch...")
])
),
appBar: AppBar(title: Text("Cwtch")),
body: Center(child: Column(children: <Widget>[Text("Loading Cwtch...")])),
);
}
}

View File

@ -15,24 +15,27 @@ class _TripleColumnViewState extends State<TripleColumnView> {
@override
Widget build(BuildContext context) {
var flwtch = Provider.of<FlwtchState>(context);
return Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
flex: flwtch.columns[0],
child: ProfileMgrView(),
),
Flexible(
flex: flwtch.columns[1],
child: flwtch.selectedProfile == null ? Center(child:Text("pick a profile")) : ContactsView(),//dev
),
Flexible(
flex: flwtch.columns[2],
child: flwtch.selectedConversation == "" ?
Center(child:Text("pick a contact")) : //dev
Container(child:MessageView(profile:flwtch.selectedProfile, conversationHandle:flwtch.selectedConversation)),
),
]
);
return Flex(direction: Axis.horizontal, children: <Widget>[
Flexible(
flex: flwtch.columns[0],
child: ProfileMgrView(),
),
Flexible(
flex: flwtch.columns[1],
child: flwtch.selectedProfile == null
? Center(child: Text("pick a profile"))
: ContactsView(), //dev
),
Flexible(
flex: flwtch.columns[2],
child: flwtch.selectedConversation == ""
? Center(child: Text("pick a contact"))
: //dev
Container(
child: MessageView(
profile: flwtch.selectedProfile,
conversationHandle: flwtch.selectedConversation)),
),
]);
}
}

View File

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_app/settings.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<Settings>(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

@ -23,7 +23,13 @@ class _ContactRowState extends State<ContactRow> {
child: SizedBox(width:60, height:60, child:Container(color:Colors.white, width: 60, height: 60, child: Image(image: AssetImage("assets/"+contact.imagePath), width:50,height:50,))),
),
),
trailing: contact.isInvitation != null && contact.isInvitation ? Column(children:<Widget>[Icon(Icons.favorite, color: Opaque.current().mainTextColor()),Icon(Icons.delete, color: Opaque.current().mainTextColor())]) : Text("99+"),//(nb: Icons.create is a pencil and we use it for "edit", not create)
trailing: contact.isInvitation != null && contact.isInvitation
? Column(children: <Widget>[
Icon(Icons.favorite, color: Opaque.current().mainTextColor()),
Icon(Icons.delete, color: Opaque.current().mainTextColor())
])
: Text(
"99+"), //(nb: Icons.create is a pencil and we use it for "edit", not create)
title: Text(
contact.nickname,
style: Provider.of<FlwtchState>(context).biggerFont,
@ -31,7 +37,7 @@ class _ContactRowState extends State<ContactRow> {
subtitle: Text(contact.status),
onTap: () {
setState(() {
var flwtch = Provider.of<FlwtchState>(context, listen:false);
var flwtch = Provider.of<FlwtchState>(context, listen: false);
flwtch.setState(() => flwtch.selectedConversation = contact.onion);
// case 2/3 handled by Double/TripleColumnView respectively
@ -46,7 +52,10 @@ class _ContactRowState extends State<ContactRow> {
MaterialPageRoute<void>(
builder: (BuildContext builderContext) {
return MultiProvider(
providers: [ChangeNotifierProvider<ProfileInfoState>(create: (_) => Provider.of<ProfileInfoState>(context)),],
providers: [
ChangeNotifierProvider<ProfileInfoState>(
create: (_) => Provider.of<ProfileInfoState>(context)),
],
child: MessageView(conversationHandle: handle),
);
},

View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../opaque.dart';
import '../settings.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<Settings>(builder: (context, theme, child) {
return Text(
widget.label,
style: TextStyle(fontSize: 20, color: theme.current().mainTextColor()),
);
});
}
}

View File

@ -16,16 +16,23 @@ class MessageBubble extends StatefulWidget {
}
class _MessageBubbleState extends State<MessageBubble> {
String d="", ts="";
bool ack=false;
String d = "", ts = "";
bool ack = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("requesting message " + widget.messageIndex.toString());
Provider.of<FlwtchState>(context).cwtch.GetMessage(widget.profile.onion, widget.contactOnion, widget.messageIndex).then((jsonMessage){
print("got message: " + widget.messageIndex.toString() + ": " + jsonMessage);
Provider.of<FlwtchState>(context)
.cwtch
.GetMessage(
widget.profile.onion, widget.contactOnion, widget.messageIndex)
.then((jsonMessage) {
print("got message: " +
widget.messageIndex.toString() +
": " +
jsonMessage);
dynamic messageWrapper = jsonDecode(jsonMessage);
dynamic message = jsonDecode(messageWrapper['Message']);
setState(() {
@ -57,11 +64,12 @@ class _MessageBubbleState extends State<MessageBubble> {
),
subtitle: Row(
children: [
Text(""+widget.messageIndex.toString()),
ack ? Icon(Icons.check_circle_outline,
color: Opaque.current().mainTextColor()) : Icon(
Icons.hourglass_bottom_outlined,
color: Opaque.current().mainTextColor())
Text("" + widget.messageIndex.toString()),
ack
? Icon(Icons.check_circle_outline,
color: Opaque.current().mainTextColor())
: Icon(Icons.hourglass_bottom_outlined,
color: Opaque.current().mainTextColor())
],
),
),

View File

@ -10,7 +10,8 @@ class MessageList extends StatefulWidget {
final ProfileInfoState profile;
final String conversationHandle;
const MessageList({Key key, this.profile, this.conversationHandle}) : super(key: key);
const MessageList({Key key, this.profile, this.conversationHandle})
: super(key: key);
@override
_MessageListState createState() => _MessageListState();
@ -30,18 +31,17 @@ class _MessageListState extends State<MessageList> {
}
return ProxyProvider0(
update: (_, __) => MessageCounter(conversationNumMessages),
child: ListView.builder(
itemCount: conversationNumMessages,
itemBuilder: (context, index) {
return MessageBubble(
profile: Provider.of<ProfileInfoState>(context),
contactOnion: widget.conversationHandle,
messageIndex: index,
);
},
)
);
update: (_, __) => MessageCounter(conversationNumMessages),
child: ListView.builder(
itemCount: conversationNumMessages,
itemBuilder: (context, index) {
return MessageBubble(
profile: Provider.of<ProfileInfoState>(context),
contactOnion: widget.conversationHandle,
messageIndex: index,
);
},
));
}
Future _updateMessageCount(BuildContext context) async {
@ -50,8 +50,14 @@ class _MessageListState extends State<MessageList> {
return;
}
Provider.of<FlwtchState>(context, listen: false).cwtch.NumMessages(Provider.of<ProfileInfoState>(context, listen: false).onion, widget.conversationHandle).then((n) {
if (n != conversationNumMessages) setState(() => conversationNumMessages = n);
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.NumMessages(
Provider.of<ProfileInfoState>(context, listen: false).onion,
widget.conversationHandle)
.then((n) {
if (n != conversationNumMessages)
setState(() => conversationNumMessages = n);
});
}
}
@ -59,4 +65,4 @@ class _MessageListState extends State<MessageList> {
class MessageCounter {
MessageCounter(this.x);
int x = 0;
}
}

View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../opaque.dart';
import '../settings.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<Settings>(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

@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import '../main.dart';
import '../model.dart';
import '../opaque.dart';
import '../settings.dart';
class ProfileRow extends StatefulWidget {
@override
@ -22,13 +23,30 @@ class _ProfileRowState extends State<ProfileRow> {
width: 60,
height: 60,
child: ClipOval(
child: SizedBox(width:60, height:60, child:Container(color:Colors.white, width: 60, height: 60, child: Image(image: AssetImage("assets/" + profile.imagePath), width:50,height:50,))),
child: SizedBox(
width: 60,
height: 60,
child: Container(
color: Colors.white,
width: 60,
height: 60,
child: Image(
image: AssetImage("assets/" + profile.imagePath),
width: 50,
height: 50,
))),
),
) ,
),
trailing: IconButton(
icon: Icon(Icons.create, color: Provider.of<OpaqueTheme>(context).current().mainTextColor()),
onPressed: () { _pushAddEditProfile(onion: profile.onion); },
),//(nb: Icons.create is a pencil and we use it for "edit", not create)
icon: Icon(Icons.create,
color: Provider.of<Settings>(context).current().mainTextColor()),
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,
style: Provider.of<FlwtchState>(context).biggerFont,
@ -36,7 +54,7 @@ class _ProfileRowState extends State<ProfileRow> {
subtitle: Text(profile.onion),
onTap: () {
setState(() {
var flwtch = Provider.of<FlwtchState>(context, listen:false);
var flwtch = Provider.of<FlwtchState>(context, listen: false);
flwtch.cwtch.SelectProfile(profile.onion);
flwtch.setState(() {
flwtch.selectedProfile = profile;
@ -44,8 +62,12 @@ class _ProfileRowState extends State<ProfileRow> {
});
switch (flwtch.columns.length) {
case 1: _pushContactList(profile, false); break;
case 2: _pushContactList(profile, true); break;
case 1:
_pushContactList(profile, false);
break;
case 2:
_pushContactList(profile, true);
break;
} // case 3: handled by TripleColumnView
});
},
@ -61,23 +83,27 @@ class _ProfileRowState extends State<ProfileRow> {
ChangeNotifierProvider<ProfileInfoState>.value(value: profile),
ChangeNotifierProvider<ContactListState>.value(value: profile.contactList),
],
builder: (context, widget) => includeDoublePane ? DoubleColumnView() : ContactsView(),
builder: (context, widget) =>
includeDoublePane ? DoubleColumnView() : ContactsView(),
);
},
),
);
}
void _pushAddEditProfile({onion: ""}) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Provider (
create: (_) => Provider.of<FlwtchState>(context, listen: false),
child: AddEditProfileView(profileOnion: onion),
);
},
)
);
void _pushAddEditProfile({onion: "", displayName: "", profileImage: ""}) {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<ProfileInfoState>(
create: (_) => ProfileInfoState(
onion: onion, nickname: displayName, imagePath: profileImage),
),
],
builder: (context, widget) => AddEditProfileView(),
);
},
));
}
}

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../opaque.dart';
import '../settings.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<Settings>(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

@ -15,17 +15,15 @@ class _TorStatusState extends State<TorStatusLabel> {
Widget build(BuildContext context) {
return Builder(
builder: (context2) => StreamBuilder<String>(
stream: Provider.of<FlwtchState>(context).appStatus.torStatus(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return Text(
snapshot.hasData ?
snapshot.data : AppLocalizations.of(context).loadingTor,
style: Theme
.of(context)
.textTheme
.headline4,
);
},
));
stream: Provider.of<FlwtchState>(context).appStatus.torStatus(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return Text(
snapshot.hasData
? snapshot.data
: AppLocalizations.of(context).loadingTor,
style: Theme.of(context).textTheme.headline4,
);
},
));
}
}

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

View File

@ -21,42 +21,42 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0-nullsafety.3"
version: "2.5.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
version: "2.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.5"
version: "1.1.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.3"
version: "1.2.0"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.3"
version: "1.1.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0-nullsafety.5"
version: "1.15.0"
convert:
dependency: transitive
description:
@ -84,21 +84,21 @@ packages:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.3"
version: "1.2.0"
ffi:
dependency: "direct main"
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
version: "1.0.0"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.0-nullsafety.4"
version: "6.1.0"
flutter:
dependency: "direct main"
description: flutter
@ -115,12 +115,17 @@ packages:
name: flutter_lokalise
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1"
version: "0.1.3"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
freezed_annotation:
dependency: transitive
description:
@ -134,21 +139,28 @@ packages:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.2"
version: "0.13.0"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.4"
version: "4.0.0"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0-nullsafety.2"
version: "0.17.0"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
json_annotation:
dependency: transitive
description:
@ -169,14 +181,14 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10-nullsafety.3"
version: "0.12.10"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.6"
version: "1.3.0"
nested:
dependency: transitive
description:
@ -184,48 +196,90 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
package_info_plus_linux:
dependency: transitive
description:
name: package_info_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
package_info_plus_macos:
dependency: transitive
description:
name: package_info_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
package_info_plus_web:
dependency: transitive
description:
name: package_info_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
package_info_plus_windows:
dependency: transitive
description:
name: package_info_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.3"
version: "1.8.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.27"
version: "2.0.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1+2"
version: "2.0.0"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4+8"
version: "2.0.0"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
version: "2.0.1"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4+3"
version: "2.0.0"
pedantic:
dependency: transitive
description:
@ -246,14 +300,14 @@ packages:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
version: "2.0.0"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0-nullsafety.4"
version: "4.1.0"
provider:
dependency: "direct main"
description:
@ -279,28 +333,28 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.4"
version: "1.8.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0-nullsafety.6"
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.3"
version: "1.1.0"
string_unescape:
dependency: transitive
description:
@ -314,42 +368,42 @@ packages:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.3"
version: "1.2.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19-nullsafety.6"
version: "0.2.19"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.5"
version: "1.3.0"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.5"
version: "2.1.0"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.4"
version: "2.0.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
version: "0.2.0"
yaml:
dependency: transitive
description:
@ -358,5 +412,5 @@ packages:
source: hosted
version: "2.2.1"
sdks:
dart: ">=2.12.0-0.0 <3.0.0"
flutter: ">=1.16.0"
dart: ">=2.12.0-259.9.beta <3.0.0"
flutter: ">=1.20.0"

View File

@ -24,6 +24,7 @@ dependencies:
flutter:
sdk: flutter
provider: "4.3.2+3"
package_info_plus: ^1.0.0
#intl_translation: any
flutter_localizations:
sdk: flutter
@ -31,8 +32,9 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.0
ffi: ^0.1.3
path_provider: ^1.6.27
ffi: ^1.0.0
path_provider: ^2.0.0
dev_dependencies:
flutter_test:
@ -75,6 +77,7 @@ flutter:
# https://flutter.dev/assets-and-images/#from-packages
assets:
- assets/
- assets/profiles/
# To add custom fonts to your application, add a fonts section here,