From 0c797faf05300773687bde388bba9199fbe14c3d Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Thu, 18 Nov 2021 16:09:55 -0800 Subject: [PATCH] profile level server list and editor start --- lib/model.dart | 31 +++++- lib/models/profileservers.dart | 60 ++++++++++- lib/views/addeditservers.dart | 2 +- lib/views/contactsview.dart | 4 +- lib/views/profileServersView.dart | 50 --------- lib/views/profileserversview.dart | 59 +++++++++++ lib/views/remoteserverview.dart | 167 ++++++++++++++++++++++++++++++ lib/widgets/buttontextfield.dart | 5 +- lib/widgets/remoteserverrow.dart | 77 ++++++++++++++ lib/widgets/serverrow.dart | 6 +- 10 files changed, 400 insertions(+), 61 deletions(-) delete mode 100644 lib/views/profileServersView.dart create mode 100644 lib/views/profileserversview.dart create mode 100644 lib/views/remoteserverview.dart create mode 100644 lib/widgets/remoteserverrow.dart diff --git a/lib/model.dart b/lib/model.dart index 87ac2f36..d2f68bda 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -120,6 +120,7 @@ class ProfileListState extends ChangeNotifier { } class ContactListState extends ChangeNotifier { + ProfileServerListState? servers; List _contacts = []; String _filter = ""; int get num => _contacts.length; @@ -131,18 +132,36 @@ class ContactListState extends ChangeNotifier { notifyListeners(); } + void connectServers(ProfileServerListState servers) { + this.servers = servers; + } + List filteredList() { if (!isFiltered) return contacts; return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList(); } void addAll(Iterable newContacts) { + print("****** contactListState.addAll()... *********"); _contacts.addAll(newContacts); + servers?.clearGroups(); + print("contact len: ${_contacts.length}"); + _contacts.forEach((contact) { + //print("looking at contact ${contact.onion} (${contact.isGroup})..."); + if (contact.isGroup) { + print("contactList adding group ${contact.onion} to ${contact.server}"); + servers?.addGroup(contact); + } + }); notifyListeners(); } void add(ContactInfoState newContact) { _contacts.add(newContact); + if (newContact.isGroup) { + print("contactList adding group ${newContact.onion} to ${newContact.server}"); + servers?.addGroup(newContact); + } notifyListeners(); } @@ -213,8 +232,8 @@ class ContactListState extends ChangeNotifier { } class ProfileInfoState extends ChangeNotifier { - ContactListState _contacts = ContactListState(); ProfileServerListState _servers = ProfileServerListState(); + ContactListState _contacts = ContactListState(); final String onion; String _nickname = ""; String _imagePath = ""; @@ -242,7 +261,11 @@ class ProfileInfoState extends ChangeNotifier { this._online = online; this._encrypted = encrypted; + _contacts.connectServers(this._servers); + if (contactsJson != null && contactsJson != "" && contactsJson != "null") { + this.replaceServers(serversJson); + List contacts = jsonDecode(contactsJson); this._contacts.addAll(contacts.map((contact) { return ContactInfoState(this.onion, contact["identifier"], contact["onion"], @@ -265,7 +288,7 @@ class ProfileInfoState extends ChangeNotifier { } } - this.replaceServers(serversJson); + } // Parse out the server list json into our server info state struct... @@ -274,7 +297,7 @@ class ProfileInfoState extends ChangeNotifier { List servers = jsonDecode(serversJson); this._servers.replace(servers.map((server) { // TODO Keys... - return RemoteServerInfoState(onion: server["onion"], status: server["status"]); + return RemoteServerInfoState(onion: server["onion"], description: server["description"], status: server["status"]); })); notifyListeners(); } @@ -282,7 +305,7 @@ class ProfileInfoState extends ChangeNotifier { // void updateServerStatusCache(String server, String status) { - this._servers.updateServerCache(server, status); + this._servers.updateServerState(server, status); notifyListeners(); } diff --git a/lib/models/profileservers.dart b/lib/models/profileservers.dart index 51a1a34e..ebd1cf99 100644 --- a/lib/models/profileservers.dart +++ b/lib/models/profileservers.dart @@ -1,3 +1,4 @@ +import 'package:cwtch/model.dart'; import 'package:flutter/material.dart'; class ProfileServerListState extends ChangeNotifier { @@ -6,6 +7,7 @@ class ProfileServerListState extends ChangeNotifier { void replace(Iterable newServers) { _servers.clear(); _servers.addAll(newServers); + resort(); notifyListeners(); } @@ -14,16 +16,43 @@ class ProfileServerListState extends ChangeNotifier { return idx >= 0 ? _servers[idx] : null; } - void updateServerCache(String onion, String description, String status) { + void updateServerState(String onion, String status) { int idx = _servers.indexWhere((element) => element.onion == onion); if (idx >= 0) { - _servers[idx] = RemoteServerInfoState(onion: onion, description: description, status: status); + _servers[idx] = RemoteServerInfoState(onion: onion, description: _servers[idx].description, status: status); } else { print("Tried to update server cache without a starting state...this is probably an error"); } + resort(); notifyListeners(); } + void resort() { + _servers.sort((RemoteServerInfoState a, RemoteServerInfoState b) { + // return -1 = a first in list + // return 1 = b first in list + if (a.status == "Synced" && b.status != "Synced") { + return -1; + } else if (a.status != "Synced" && b.status == "Synced") { + return 1; + } + return 0; + }); + } + + void clearGroups() { + _servers.map((server) => server.clearGroups()); + } + + void addGroup(ContactInfoState group) { + print("serverList adding group ${group.onion} to ${group.server}"); + + int idx = _servers.indexWhere((element) => element.onion == group.server); + if (idx >= 0) { + _servers[idx].addGroup(group); + } + } + List get servers => _servers.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier } @@ -31,7 +60,32 @@ class ProfileServerListState extends ChangeNotifier { class RemoteServerInfoState extends ChangeNotifier { final String onion; final String status; - final String description; + String description; + List _groups = []; RemoteServerInfoState({required this.onion, required this.description, required this.status}); + + void updateDescription(String newDescription) { + this.description = newDescription; + notifyListeners(); + } + + void clearGroups() { + print("Server CLEARING group"); + description = "cleared groups"; + _groups = []; + } + + void addGroup(ContactInfoState group) { + print("server $onion adding group ${group.onion}"); + _groups.add(group); + print("now has ${_groups.length}"); + description = "i have ${_groups.length} groups"; + notifyListeners(); + } + + int get groupsLen => _groups.length; + + List get groups => _groups.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier + } diff --git a/lib/views/addeditservers.dart b/lib/views/addeditservers.dart index 2a1c8f46..b2a5cacf 100644 --- a/lib/views/addeditservers.dart +++ b/lib/views/addeditservers.dart @@ -110,7 +110,7 @@ class _AddEditServerViewState extends State { ), CwtchTextField( controller: ctrlrDesc, - labelText: "Description", + labelText: AppLocalizations.of(context)!.fieldDescriptionLabel, autofocus: false, ) ]), diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 9a19d982..af776607 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -1,4 +1,5 @@ import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/views/profileserversview.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/views/torstatusview.dart'; import 'package:cwtch/widgets/contactrow.dart'; @@ -171,10 +172,11 @@ class _ContactsViewState extends State { } void _pushServers() { + var profile = Provider.of(context); Navigator.of(context).push(MaterialPageRoute( builder: (BuildContext context) { return MultiProvider( - providers: [Provider.value(value: Provider.of(context))], + providers: [ChangeNotifierProvider(create: (context) => profile.serverList), Provider.value(value: Provider.of(context))], child: ProfileServersView(), ); }, diff --git a/lib/views/profileServersView.dart b/lib/views/profileServersView.dart deleted file mode 100644 index 2b9b99c9..00000000 --- a/lib/views/profileServersView.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - - -class ProfileServersView extends StatefulWidget { - @override - _ProfileServersView createState() => _ProfileServersView(); -} - -class _ProfileServersView extends State { - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text( MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort), - //actions: getActions(), - ), - body: Consumer( - builder: (context, svrs, child) { - final tiles = svrs.servers.map((ServerInfoState server) { - return ChangeNotifierProvider.value( - value: server, - builder: (context, child) => RepaintBoundary(child: ServerRow()), - ); - }, - ); - - final divided = ListTile.divideTiles( - context: context, - tiles: tiles, - ).toList(); - - if (tiles.isEmpty) { - return Center( - child: Text( - AppLocalizations.of(context)!.unlockServerTip, - textAlign: TextAlign.center, - )); - } - - return ListView(children: divided); - }, - )); - } \ No newline at end of file diff --git a/lib/views/profileserversview.dart b/lib/views/profileserversview.dart new file mode 100644 index 00000000..aad94f85 --- /dev/null +++ b/lib/views/profileserversview.dart @@ -0,0 +1,59 @@ +import 'package:cwtch/models/profileservers.dart'; +import 'package:cwtch/models/servers.dart'; +import 'package:cwtch/widgets/remoteserverrow.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; + +import '../model.dart'; + + +class ProfileServersView extends StatefulWidget { + @override + _ProfileServersView createState() => _ProfileServersView(); +} + +class _ProfileServersView extends State { + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + + return Scaffold( + appBar: AppBar( + title: Text(MediaQuery + .of(context) + .size + .width > 600 ? AppLocalizations.of(context)!.serversManagerTitleLong : AppLocalizations.of(context)!.serversManagerTitleShort), + //actions: getActions(), + ), + body: Consumer( + builder: (context, servers, child) { + final tiles = servers.servers.map((RemoteServerInfoState server) { + return ChangeNotifierProvider.value( + value: server, + builder: (context, child) => RepaintBoundary(child: RemoteServerRow()), + ); + }, + ); + + final divided = ListTile.divideTiles( + context: context, + tiles: tiles, + ).toList(); + + // TODO: add import row from global servers + divided.insert(0, Row( children: [Text("Import server from global list if any")])); + + return ListView(children: divided); + }, + )); + } + + + +} \ No newline at end of file diff --git a/lib/views/remoteserverview.dart b/lib/views/remoteserverview.dart new file mode 100644 index 00000000..22bee1bf --- /dev/null +++ b/lib/views/remoteserverview.dart @@ -0,0 +1,167 @@ +import 'dart:convert'; +import 'package:cwtch/cwtch/cwtch.dart'; +import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/profileservers.dart'; +import 'package:cwtch/models/servers.dart'; +import 'package:cwtch/widgets/buttontextfield.dart'; +import 'package:cwtch/widgets/contactrow.dart'; +import 'package:cwtch/widgets/cwtchlabel.dart'; +import 'package:cwtch/widgets/passwordfield.dart'; +import 'package:cwtch/widgets/textfield.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:cwtch/settings.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../errorHandler.dart'; +import '../main.dart'; +import '../config.dart'; +import '../model.dart'; + +/// Pane to add or edit a server +class RemoteServerView extends StatefulWidget { + const RemoteServerView(); + + @override + _RemoteServerViewState createState() => _RemoteServerViewState(); +} + +class _RemoteServerViewState extends State { + final _formKey = GlobalKey(); + + final ctrlrDesc = TextEditingController(text: ""); + + @override + void initState() { + super.initState(); + var serverInfoState = Provider.of(context, listen: false); + if (serverInfoState.description.isNotEmpty) { + ctrlrDesc.text = serverInfoState.description; + } + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer2(builder: (context, serverInfoState, settings, child) { + return Scaffold( + appBar: AppBar( + title: Text(ctrlrDesc.text.isNotEmpty ? ctrlrDesc.text : serverInfoState.onion) + ), + body: 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.fromLTRB(30, 0, 30, 10), + padding: EdgeInsets.fromLTRB(20, 0, 20, 10), + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + + Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + height: 20, + ), + CwtchLabel(label: AppLocalizations.of(context)!.serverAddress), + SizedBox( + height: 20, + ), + SelectableText( + serverInfoState.onion + ) + ]), + + // Description + Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + height: 20, + ), + CwtchLabel(label: AppLocalizations.of(context)!.serverDescriptionLabel), + Text(AppLocalizations.of(context)!.serverDescriptionDescription), + SizedBox( + height: 20, + ), + CwtchButtonTextField( + controller: ctrlrDesc, + readonly: false, + tooltip: "Save", //TODO localize + labelText: "Description", // TODO localize + icon: Icon(Icons.save), + onPressed: () { + // TODO save + }, + ) + ]), + + Text("Groups on this server"), + _buildGroupsList(serverInfoState), + + ])))))); + }),); + }); + } + + Widget _buildGroupsList(RemoteServerInfoState serverInfoState) { + print("groups: ${serverInfoState.groups} lenMethod: ${serverInfoState.groupsLen} len: ${serverInfoState.groups.length}"); + final tiles = serverInfoState.groups.map((ContactInfoState group) { + print("building group tile for ${group.onion}"); + return ChangeNotifierProvider.value(key: ValueKey(group.profileOnion + "" + group.onion), value: group, builder: (_, __) => RepaintBoundary(child: _buildGroupRow(group))); + }); + final divided = ListTile.divideTiles( + context: context, + tiles: tiles, + ).toList(); + return RepaintBoundary(child: ListView(children: divided)); + } + + void _savePressed() { + + var server = Provider.of(context, listen: false); + + Provider.of(context, listen: false) + .cwtch.SetServerAttribute(server.onion, "description", ctrlrDesc.text); + server.setDescription(ctrlrDesc.text); + + + if (_formKey.currentState!.validate()) { + // TODO support change password + } + Navigator.of(context).pop(); + } + + Widget _buildGroupRow(ContactInfoState group) { + return Column( + children: [ + Text( + group.nickname, + style: Provider.of(context).biggerFont.apply(color: Provider.of(context).theme.portraitOnlineBorderColor()), + softWrap: true, + overflow: TextOverflow.ellipsis, + ), + Visibility( + visible: !Provider.of(context).streamerMode, + child: ExcludeSemantics( + child: Text( + group.onion, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: Provider.of(context).theme.portraitOnlineBorderColor()), + ))) + ], + ); + } + +} + diff --git a/lib/widgets/buttontextfield.dart b/lib/widgets/buttontextfield.dart index 46e88796..cd1cdb09 100644 --- a/lib/widgets/buttontextfield.dart +++ b/lib/widgets/buttontextfield.dart @@ -5,12 +5,13 @@ import 'package:provider/provider.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({required this.controller, required this.onPressed, required this.icon, required this.tooltip, this.readonly = true}); + CwtchButtonTextField({required this.controller, required this.onPressed, required this.icon, required this.tooltip, this.readonly = true, this.labelText}); final TextEditingController controller; final Function()? onPressed; final Icon icon; final String tooltip; final bool readonly; + String? labelText; @override _CwtchButtonTextFieldState createState() => _CwtchButtonTextFieldState(); @@ -39,6 +40,8 @@ class _CwtchButtonTextFieldState extends State { focusNode: _focusNode, enableIMEPersonalizedLearning: false, decoration: InputDecoration( + labelText: widget.labelText, + labelStyle: TextStyle(color: theme.current().mainTextColor(), backgroundColor: theme.current().textfieldBackgroundColor()), suffixIcon: IconButton( onPressed: widget.onPressed, icon: widget.icon, diff --git a/lib/widgets/remoteserverrow.dart b/lib/widgets/remoteserverrow.dart new file mode 100644 index 00000000..0df655d2 --- /dev/null +++ b/lib/widgets/remoteserverrow.dart @@ -0,0 +1,77 @@ +import 'package:cwtch/main.dart'; +import 'package:cwtch/models/profileservers.dart'; +import 'package:cwtch/models/servers.dart'; +import 'package:cwtch/views/addeditservers.dart'; +import 'package:cwtch/views/remoteserverview.dart'; +import 'package:cwtch/widgets/profileimage.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../cwtch_icons_icons.dart'; +import '../errorHandler.dart'; +import '../model.dart'; +import '../settings.dart'; + +class RemoteServerRow extends StatefulWidget { + @override + _RemoteServerRowState createState() => _RemoteServerRowState(); +} + +class _RemoteServerRowState extends State { + @override + Widget build(BuildContext context) { + var server = Provider.of(context); + var description = server.description.isNotEmpty ? server.description : server.onion; + var running = server.status == "Synced"; + return Card(clipBehavior: Clip.antiAlias, + margin: EdgeInsets.all(0.0), + child: InkWell( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(6.0), //border size + child: Icon(CwtchIcons.dns_24px, + color: running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor(), + size: 64) + + ), + Expanded( + child: Column( + children: [ + Text( + description, + semanticsLabel: description, + style: Provider.of(context).biggerFont.apply(color: running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), + softWrap: true, + overflow: TextOverflow.ellipsis, + ), + Visibility( + visible: !Provider.of(context).streamerMode, + child: ExcludeSemantics( + child: Text( + server.onion, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), + ))) + ], + )), + + ]), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + settings: RouteSettings(name: "remoteserverview"), + builder: (BuildContext context) { + return MultiProvider( + providers: [ChangeNotifierProvider(create: (context) => server), Provider.value(value: Provider.of(context))], + child: RemoteServerView(), + ); + })); + } + )); + } + +} \ No newline at end of file diff --git a/lib/widgets/serverrow.dart b/lib/widgets/serverrow.dart index 9c12f477..a4e8bb05 100644 --- a/lib/widgets/serverrow.dart +++ b/lib/widgets/serverrow.dart @@ -73,7 +73,11 @@ class _ServerRowState extends State { _pushEditServer(server); }, ) - ]))); + ]), + onTap: () { + _pushEditServer(server); + } + )); } void _pushEditServer(ServerInfoState server) {