diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index 6ee8a912..98bbeb79 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -330,13 +330,16 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) : val serverOnion = (a.get("ServerOnion") as? String) ?: "" Cwtch.launchServer(serverOnion) } - "ShutdownServer" -> { + "StopServer" -> { val serverOnion = (a.get("ServerOnion") as? String) ?: "" Cwtch.shutdownServer(serverOnion) } - "ShutdownServers" -> { + "StopServers" -> { Cwtch.shutdownServers() } + "DestroyServers" -> { + Cwtch.destroyServers() + } "SetServerAttribute" -> { val serverOnion = (a.get("ServerOnion") as? String) ?: "" val key = (a.get("Key") as? String) ?: "" diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index d9763f43..8ee539fa 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -81,9 +81,11 @@ abstract class Cwtch { // ignore: non_constant_identifier_names void LaunchServer(String serverOnion); // ignore: non_constant_identifier_names - void ShutdownServer(String serverOnion); + void StopServer(String serverOnion); // ignore: non_constant_identifier_names - void ShutdownServers(); + void StopServers(); + // ignore: non_constant_identifier_names + void DestroyServers(); // ignore: non_constant_identifier_names void SetServerAttribute(String serverOnion, String key, String val); diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index 0ae5d98d..da1dd6f9 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -34,7 +34,6 @@ class CwtchNotifier { } void handleMessage(String type, dynamic data) { - print("EVENT $type $data"); switch (type) { case "CwtchStarted": appState.SetCwtchInit(); @@ -64,14 +63,21 @@ class CwtchNotifier { )); break; case "NewServer": - var serverData = jsonDecode(data["Data"]); + EnvironmentConfig.debugLog("NewServer $data"); serverListState.add( - serverData["onion"], - serverData["serverbundle"], - serverData["enabled"] == "true", - serverData["description"], - serverData["autostart"] == "true", - serverData["storageType"] == "storage-password"); + data["Onion"], + data["ServerBundle"], + data["Running"] == "true", + data["Description"], + data["Autostart"] == "true", + data["StorageType"] == "storage-password"); + break; + case "ServerIntentUpdate": + EnvironmentConfig.debugLog("ServerIntentUpdate $data"); + var server = serverListState.getServer(data["Identity"]); + if (server != null) { + server.setRunning(data["Intent"] == "running"); + } break; case "GroupCreated": diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index e022fe8a..0b775862 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -624,8 +624,8 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names - void ShutdownServer(String serverOnion) { - var shutdownServer = library.lookup>("c_ShutdownServer"); + void StopServer(String serverOnion) { + var shutdownServer = library.lookup>("c_StopServer"); // ignore: non_constant_identifier_names final ShutdownServer = shutdownServer.asFunction(); final u1 = serverOnion.toNativeUtf8(); @@ -635,13 +635,22 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names - void ShutdownServers() { - var shutdownServers = library.lookup>("c_ShutdownServers"); + void StopServers() { + var shutdownServers = library.lookup>("c_StopServers"); // ignore: non_constant_identifier_names final ShutdownServers = shutdownServers.asFunction(); ShutdownServers(); } + @override + // ignore: non_constant_identifier_names + void DestroyServers() { + var destroyServers = library.lookup>("c_DestroyServers"); + // ignore: non_constant_identifier_names + final DestroyServers = destroyServers.asFunction(); + DestroyServers(); + } + @override // ignore: non_constant_identifier_names void SetServerAttribute(String serverOnion, String key, String val) { diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 458defef..03efe699 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -245,14 +245,20 @@ class CwtchGomobile implements Cwtch { @override // ignore: non_constant_identifier_names - void ShutdownServer(String serverOnion) { - cwtchPlatform.invokeMethod("ShutdownServer", {"ServerOnion": serverOnion}); + void StopServer(String serverOnion) { + cwtchPlatform.invokeMethod("StopServer", {"ServerOnion": serverOnion}); } @override // ignore: non_constant_identifier_names - void ShutdownServers() { - cwtchPlatform.invokeMethod("ShutdownServers", {}); + void StopServers() { + cwtchPlatform.invokeMethod("StopServers", {}); + } + + @override + // ignore: non_constant_identifier_names + void DestroyServers() { + cwtchPlatform.invokeMethod("DestroyServers", {}); } @override diff --git a/lib/views/addeditservers.dart b/lib/views/addeditservers.dart new file mode 100644 index 00000000..32030c30 --- /dev/null +++ b/lib/views/addeditservers.dart @@ -0,0 +1,329 @@ +import 'dart:convert'; +import 'package:cwtch/cwtch/cwtch.dart'; +import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/servers.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'; + +/// Global Settings View provides access to modify all the Globally Relevant Settings including Locale, Theme and Experiments. +class AddEditServerView extends StatefulWidget { + const AddEditServerView(); + + @override + _AddEditServerViewState createState() => _AddEditServerViewState(); +} + +class _AddEditServerViewState extends State { + final _formKey = GlobalKey(); + + final ctrlrDesc = TextEditingController(text: ""); + final ctrlrOldPass = TextEditingController(text: ""); + final ctrlrPass = TextEditingController(text: ""); + final ctrlrPass2 = TextEditingController(text: ""); + final ctrlrOnion = TextEditingController(text: ""); + + late bool usePassword; + //late bool deleted; + + @override + void initState() { + super.initState(); + var serverInfoState = Provider.of(context, listen: false); + ctrlrOnion.text = serverInfoState.onion; + usePassword = serverInfoState.isEncrypted; + if (serverInfoState.description.isNotEmpty) { + ctrlrDesc.text = serverInfoState.description; + } + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + + return Scaffold( + appBar: AppBar( + title: ctrlrOnion.text.isEmpty ? Text("Add Server") : Text("Edit Server"), //AppLocalizations.of(context)!.cwtchSettingsTitle), + ), + body: _buildSettingsList(), + ); + } + + void _handleSwitchPassword(bool? value) { + setState(() { + usePassword = value!; + }); + } + + Widget _buildSettingsList() { + return Consumer2(builder: (context, serverInfoState, settings, 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.fromLTRB(30, 0, 30, 10), + padding: EdgeInsets.fromLTRB(20, 0 , 20, 10), + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + + // Onion + Visibility( + visible: serverInfoState.onion.isNotEmpty, + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + height: 20, + ), + CwtchLabel(label: "Onion"), //AppLocalizations.of(context)!.displayNameLabel), + SizedBox( + height: 20, + ), + SelectableText( + serverInfoState.onion + ) + ])), + + // Description + Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + height: 20, + ), + CwtchLabel(label: "Description"), //AppLocalizations.of(context)!.displayNameLabel), + SizedBox( + height: 20, + ), + CwtchTextField( + controller: ctrlrDesc, + labelText: "Description", + autofocus: false, + ) + ]), + + SizedBox( + height: 20, + ), + + // Enabled + Visibility( + visible: serverInfoState.onion.isNotEmpty, + child: SwitchListTile( + title: Text(/*AppLocalizations.of(context)!.blockUnknownLabel*/ "Enabled", style: TextStyle(color: settings.current().mainTextColor())), + subtitle: Text(AppLocalizations.of(context)!.descriptionBlockUnknownConnections), + value: serverInfoState.running, + onChanged: (bool value) { + serverInfoState.setRunning(value); + if (value) { + Provider.of(context, listen: false).cwtch.LaunchServer(serverInfoState.onion); + } else { + Provider.of(context, listen: false).cwtch.StopServer(serverInfoState.onion); + } + // ?? serverInfoState.enabled = value; + notify? + }, + activeTrackColor: settings.theme.defaultButtonActiveColor(), + inactiveTrackColor: settings.theme.defaultButtonDisabledColor(), + secondary: Icon(CwtchIcons.negative_heart_24px, color: settings.current().mainTextColor()), + )), + + // Auto start + SwitchListTile( + title: Text(/*AppLocalizations.of(context)!.blockUnknownLabel*/ "Autostart", style: TextStyle(color: settings.current().mainTextColor())), + subtitle: Text(AppLocalizations.of(context)!.descriptionBlockUnknownConnections), + value: serverInfoState.autoStart, + onChanged: (bool value) { + serverInfoState.setAutostart(value); + + if (! serverInfoState.onion.isEmpty) { + Provider.of(context, listen: false).cwtch.SetServerAttribute(serverInfoState.onion, "autostart", value ? "true" : "false"); + } + }, + activeTrackColor: settings.theme.defaultButtonActiveColor(), + inactiveTrackColor: settings.theme.defaultButtonDisabledColor(), + secondary: Icon(CwtchIcons.favorite_24dp, color: settings.current().mainTextColor()), + ), + + + // ***** Password ***** + + Visibility( + visible: serverInfoState.onion.isEmpty, + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + SizedBox( + height: 20, + ), + Checkbox( + value: usePassword, + fillColor: MaterialStateProperty.all(settings.current().defaultButtonColor()), + activeColor: settings.current().defaultButtonActiveColor(), + onChanged: _handleSwitchPassword, + ), + Text( + AppLocalizations.of(context)!.radioUsePassword, + style: TextStyle(color: settings.current().mainTextColor()), + ), + SizedBox( + height: 20, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Text( + usePassword ? AppLocalizations.of(context)!.encryptedProfileDescription : AppLocalizations.of(context)!.plainProfileDescription, + textAlign: TextAlign.center, + )) + ])), + SizedBox( + height: 20, + ), + Visibility( + visible: usePassword, + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + Visibility( + visible: serverInfoState.onion.isNotEmpty && serverInfoState.isEncrypted, + child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ + CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel), + SizedBox( + height: 20, + ), + CwtchPasswordField( + controller: ctrlrOldPass, + autoFillHints: [AutofillHints.newPassword], + validator: (value) { + // Password field can be empty when just updating the profile, not on creation + if (serverInfoState.isEncrypted && + serverInfoState.onion.isEmpty && + value.isEmpty && + usePassword) { + return AppLocalizations.of(context)!.passwordErrorEmpty; + } + if (Provider.of(context).deleteProfileError == true) { + return AppLocalizations.of(context)!.enterCurrentPasswordForDelete; + } + return null; + }, + ), + SizedBox( + height: 20, + ), + ])), + CwtchLabel(label: AppLocalizations.of(context)!.newPassword), + SizedBox( + height: 20, + ), + CwtchPasswordField( + controller: ctrlrPass, + validator: (value) { + // Password field can be empty when just updating the profile, not on creation + if (serverInfoState.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 (serverInfoState.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, + ), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: ElevatedButton( + onPressed: serverInfoState.onion.isEmpty ? _createPressed : _savePressed, + child: Text( + serverInfoState.onion.isEmpty ? "Add Server" : "Save Server",//AppLocalizations.of(context)!.addNewProfileBtn : AppLocalizations.of(context)!.saveProfileBtn, + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + + // ***** END Password ***** + + + + + + ])))))); + }); + }); + } + + void _createPressed() { + // This will run all the validations in the form including + // checking that display name is not empty, and an actual check that the passwords + // match (and are provided if the user has requested an encrypted profile). + if (_formKey.currentState!.validate()) { + if (usePassword) { + Provider + .of(context, listen: false) + .cwtch + .CreateServer(ctrlrPass.value.text, ctrlrDesc.value.text, Provider.of(context, listen: false).autoStart); + } else { + Provider + .of(context, listen: false) + .cwtch + .CreateServer(DefaultPassword, ctrlrDesc.value.text, Provider.of(context, listen: false).autoStart); + } + Navigator.of(context).pop(); + } + } + + 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 change password + } + Navigator.of(context).pop(); + } +} \ No newline at end of file diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index 8b19e27b..4525fd88 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/servers.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/settings.dart'; @@ -193,6 +194,7 @@ class _GlobalSettingsViewState extends State { subtitle: Text("Enable Servers"), //AppLocalizations.of(context)!.descriptionExperimentsGroups), value: settings.isExperimentEnabled(ServerManagementExperiment), onChanged: (bool value) { + Provider.of(context, listen: false).clear(); if (value) { settings.enableExperiment(ServerManagementExperiment); } else { diff --git a/lib/views/serversview.dart b/lib/views/serversview.dart new file mode 100644 index 00000000..e1bd89b4 --- /dev/null +++ b/lib/views/serversview.dart @@ -0,0 +1,79 @@ +import 'package:cwtch/models/servers.dart'; +import 'package:cwtch/views/addeditservers.dart'; +import 'package:cwtch/widgets/serverrow.dart'; +import 'package:flutter/material.dart'; +import 'package:cwtch/torstatus.dart'; +import 'package:cwtch/widgets/tor_icon.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../main.dart'; + +/// +class ServersView extends StatefulWidget { + @override + _ServersView createState() => _ServersView(); +} + +class _ServersView extends State { + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Servers you host"), //AppLocalizations.of(context)!.torNetworkStatus), + ), + floatingActionButton: FloatingActionButton( + onPressed: _pushAddServer, + tooltip: "Add new Server", //AppLocalizations.of(context)!.addNewProfileBtn, + child: Icon( + Icons.add, + semanticLabel: "Add new Server", //AppLocalizations.of(context)!.addNewProfileBtn, + ), + ), + 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 const Center( + child: const Text( + "Please create or unlock a server to begin!", + textAlign: TextAlign.center, + )); + } + + return ListView(children: divided); + }, + )); + } + + void _pushAddServer() { + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) { + return MultiProvider( + providers: [ChangeNotifierProvider( + create: (_) => ServerInfoState(onion: "", serverBundle: "", description: "", autoStart: true, running: false, isEncrypted: true), + )], + //ChangeNotifierProvider.value(value: Provider.of(context))], + child: AddEditServerView(), + ); + }, + )); + } +} diff --git a/lib/widgets/serverrow.dart b/lib/widgets/serverrow.dart new file mode 100644 index 00000000..0588df82 --- /dev/null +++ b/lib/widgets/serverrow.dart @@ -0,0 +1,95 @@ +import 'package:cwtch/main.dart'; +import 'package:cwtch/models/servers.dart'; +import 'package:cwtch/views/addeditservers.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 '../model.dart'; +import '../settings.dart'; + +class ServerRow extends StatefulWidget { + @override + _ServerRowState createState() => _ServerRowState(); +} + +class _ServerRowState extends State { + @override + Widget build(BuildContext context) { + var server = Provider.of(context); + 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: server.running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor(), + size: 64) + + ), + Expanded( + child: Column( + children: [ + Text( + server.description, + semanticsLabel: server.description, + style: Provider.of(context).biggerFont.apply(color: server.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: server.running ? Provider.of(context).theme.portraitOnlineBorderColor() : Provider.of(context).theme.portraitOfflineBorderColor()), + ))) + ], + )), + + // Copy server button + IconButton( + enableFeedback: true, + tooltip: AppLocalizations.of(context)!.editProfile + " " + server.onion, + icon: Icon(CwtchIcons.address_copy_2, color: Provider.of(context).current().mainTextColor()), + onPressed: () { + Clipboard.setData(new ClipboardData(text: server.serverBundle)); + }, + ), + + // Edit button + IconButton( + enableFeedback: true, + tooltip: AppLocalizations.of(context)!.editProfile + " " + server.onion, + icon: Icon(Icons.create, color: Provider.of(context).current().mainTextColor()), + onPressed: () { + _pushEditServer(server); + }, + ) + + + ]))); + } + + void _pushEditServer(ServerInfoState server ) { + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) { + return MultiProvider( + providers: [ChangeNotifierProvider( + create: (_) => server, + )], + //ChangeNotifierProvider.value(value: Provider.of(context))], + child: AddEditServerView(), + ); + }, + )); + } +} \ No newline at end of file