From d380637c5e7bb8c089dbac77bb2cd2fe12405623 Mon Sep 17 00:00:00 2001 From: Curtly Critchlow Date: Mon, 4 Sep 2023 12:25:32 -0400 Subject: [PATCH 01/95] fix: home screen and new config screen refactored to use sshnoports backend methods and show appropriate properties on the respective screens --- .../sshnoports/lib/sshnp/sshnp_params.dart | 16 +- packages/sshnp_gui/lib/l10n/app_en.arb | 19 ++- .../controllers/home_screen_controller.dart | 22 +-- .../lib/src/controllers/minor_providers.dart | 9 +- .../src/presentation/screens/home_screen.dart | 34 +--- .../widgets/app_navigation_rail.dart | 4 +- .../widgets/delete_alert_dialog.dart | 10 +- .../widgets/new_connection_form.dart | 155 ++++++------------ packages/sshnp_gui/lib/src/utils/sizes.dart | 6 +- 9 files changed, 94 insertions(+), 181 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index df36eb296..e293637f6 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -230,17 +230,17 @@ class SSHNPPartialParams { this.clientAtSign, this.sshnpdAtSign, this.host, - this.device, - this.port, - this.localPort, + this.device = SSHNP.defaultDevice, + this.port = SSHNP.defaultPort, + this.localPort = SSHNP.defaultLocalPort, this.atKeysFilePath, - this.sendSshPublicKey, + this.sendSshPublicKey = SSHNP.defaultSendSshPublicKey, this.localSshOptions = SSHNP.defaultLocalSshOptions, - this.rsa, + this.rsa = SSHNP.defaultRsa, this.remoteUsername, - this.verbose, - this.rootDomain, - this.localSshdPort, + this.verbose = SSHNP.defaultVerbose, + this.rootDomain = SSHNP.defaultRootDomain, + this.localSshdPort = SSHNP.defaultLocalSshdPort, this.listDevices = SSHNP.defaultListDevices, this.legacyDaemon = SSHNP.defaultLegacyDaemon, }); diff --git a/packages/sshnp_gui/lib/l10n/app_en.arb b/packages/sshnp_gui/lib/l10n/app_en.arb index 3831a80a9..ccaaa977d 100644 --- a/packages/sshnp_gui/lib/l10n/app_en.arb +++ b/packages/sshnp_gui/lib/l10n/app_en.arb @@ -2,6 +2,7 @@ "currentConnections" : "Current Connections", "addNewConnection" : "Add New Connection", "add" : "Add", + "submit" : "Submit", "hostSelection" : "Host Selection", "host" : "Host", "sourcePort" : "Source Port", @@ -14,28 +15,30 @@ "newText" : "New", "profileName" : "Profile Name", "clientAtsign" : "Client atsign", - "sshnpdAtSign" : "SSHNP atsign", + "sshnpdAtSign" : "Device Address", "sshnpdAtSignHint" : "The atSign of the sshnpd we wish to communicate with", - "device" : "Device", + "device" : "Device Name", "deviceHint" : "The device name of the sshnpd we wish to communicate with", "username" : "Username", "usernameHint": "The user name on this host", "homeDirectory" : "Home Directory", "homeDirectoryHint" : "The home directory on this host", "sessionId" : "Session ID", - "sendSshPublicKey" : "Send SSH Public Key", - "rsa" : "RSA", + "sendSshPublicKey" : "SSH Public Key", + "rsa" : "Use RSA key Format", "keyFile" : "Key File", "from" : "From", "to" : "To", "host" : "Host", - "port" : "Port", + "port" : "Remote Port", "localPort": "Local Port", + "localSshdPort": "Local SSHD Port", "sshPublicKey" : "SSH Public Key", "localSshOptions" : "Local SSH Options", - "verbose" : "verbose", - "remoteUserName" : "Remote User Name", - "atKeysFilePath" : "atKeys File Path", + "localSshOptionsHint" : "Use \",\" to separate options", + "verbose" : "verbose Logging", + "remoteUserName" : "Remote Username", + "atKeysFilePath" : "atKeys File", "rootDomain" : "Root Domain", "listDevices" : "List Devices", "availableConnections" : "Available Connections", diff --git a/packages/sshnp_gui/lib/src/controllers/home_screen_controller.dart b/packages/sshnp_gui/lib/src/controllers/home_screen_controller.dart index 4917fd0f5..9e4d2d511 100644 --- a/packages/sshnp_gui/lib/src/controllers/home_screen_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/home_screen_controller.dart @@ -3,10 +3,8 @@ import 'dart:io'; import 'package:at_client_mobile/at_client_mobile.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:path/path.dart' as path; import 'package:sshnoports/common/utils.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/minor_providers.dart'; /// A Controller class that controls the UI update when the [AtDataRepository] methods are called. class HomeScreenController extends StateNotifier>> { @@ -41,14 +39,10 @@ class HomeScreenController extends StateNotifier>> } /// Deletes the [AtKey] associated with the [AtData]. - Future delete(int index) async { + Future delete(SSHNPParams sshnpParams) async { state = const AsyncValue.loading(); - final directory = getDefaultSshnpConfigDirectory(getHomeDirectory()!); - var configDir = await Directory(directory).list().toList(); - // remove non env file so the index of the config file in the UI matches the index of the configDir env files. - //TODO @CurtlyCritchlow this is no longer needed, you can now use [SSHNPParams.deleteFile()] - configDir.removeWhere((element) => path.extension(element.path) != '.env'); - configDir[index].delete(); + + sshnpParams.deleteFile(); await getConfigFiles(); } @@ -81,11 +75,11 @@ class HomeScreenController extends StateNotifier>> Future updateConfigFile({required SSHNPParams sshnpParams}) async { state = const AsyncValue.loading(); - final directory = getDefaultSshnpConfigDirectory(getHomeDirectory()!); - var configDir = await Directory(directory).list().toList(); - configDir.removeWhere((element) => path.extension(element.path) != '.env'); - final index = ref.read(sshnpParamsUpdateIndexProvider); - log('path is:${configDir[index].path}'); + // final directory = getDefaultSshnpConfigDirectory(getHomeDirectory()!); + // var configDir = await Directory(directory).list().toList(); + // configDir.removeWhere((element) => path.extension(element.path) != '.env'); + // final index = ref.read(sshnpParamsUpdateIndexProvider); + // log('path is:${configDir[index].path}'); // await Directory(configDir).create(recursive: true); //.env sshnpParams.toFile(overwrite: true); diff --git a/packages/sshnp_gui/lib/src/controllers/minor_providers.dart b/packages/sshnp_gui/lib/src/controllers/minor_providers.dart index b89e649dd..8aa0b1035 100644 --- a/packages/sshnp_gui/lib/src/controllers/minor_providers.dart +++ b/packages/sshnp_gui/lib/src/controllers/minor_providers.dart @@ -4,13 +4,8 @@ import 'package:sshnp_gui/src/utils/enum.dart'; final currentNavIndexProvider = StateProvider((ref) => 0); -final sshnpParamsProvider = StateProvider( - (ref) => SSHNPParams(clientAtSign: '', sshnpdAtSign: '', host: '', legacyDaemon: true), -); - -/// index for the config file that is being updated -final sshnpParamsUpdateIndexProvider = StateProvider( - (ref) => 0, +final sshnpPartialParamsProvider = StateProvider( + (ref) => SSHNPPartialParams(), ); final configFileWriteStateProvider = StateProvider( diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index b034605f8..e615053af 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -81,7 +81,9 @@ class _HomeScreenState extends ConsumerState { } void updateConfigFile(SSHNPParams sshnpParams) { - ref.read(sshnpParamsProvider.notifier).update((state) => sshnpParams); + ref + .read(sshnpPartialParamsProvider.notifier) + .update((state) => SSHNPPartialParams.fromArgMap(sshnpParams.toArgs())); // change value to 1 to update navigation rail selcted icon. ref.read(currentNavIndexProvider.notifier).update((state) => AppRoute.newConnection.index - 1); // Change value to update to trigger the update functionality on the new connection form. @@ -129,8 +131,6 @@ class _HomeScreenState extends ConsumerState { 2: IntrinsicColumnWidth(), 3: IntrinsicColumnWidth(), 4: IntrinsicColumnWidth(), - 5: IntrinsicColumnWidth(), - 6: FixedColumnWidth(150), }, children: [ TableRow( @@ -138,13 +138,10 @@ class _HomeScreenState extends ConsumerState { const BoxDecoration(border: Border(bottom: BorderSide(color: Colors.white))), children: [ CustomTableCell.text(text: strings.actions), - CustomTableCell.text( - text: strings.clientAtsign), // todo change this to strings.profileName + CustomTableCell.text(text: strings.profileName), CustomTableCell.text(text: strings.sshnpdAtSign), CustomTableCell.text(text: strings.device), - CustomTableCell.text(text: strings.port), - CustomTableCell.text(text: strings.localPort), - CustomTableCell.text(text: strings.localSshOptions), + CustomTableCell.text(text: strings.host), ], ), ...state.value! @@ -162,10 +159,6 @@ class _HomeScreenState extends ConsumerState { ), IconButton( onPressed: () { - // get the index of the config file so it can be updated - ref - .read(sshnpParamsUpdateIndexProvider.notifier) - .update((value) => state.value!.indexOf(e)); updateConfigFile(e); }, icon: const Icon(Icons.edit), @@ -176,7 +169,7 @@ class _HomeScreenState extends ConsumerState { context: context, barrierDismissible: false, builder: (BuildContext context) => - DeleteAlertDialog(index: state.value!.indexOf(e)), + DeleteAlertDialog(sshnpParams: e), ); }, icon: const Icon(Icons.delete_forever), @@ -186,20 +179,7 @@ class _HomeScreenState extends ConsumerState { CustomTableCell.text(text: e.profileName ?? ''), CustomTableCell.text(text: e.sshnpdAtSign ?? ''), CustomTableCell.text(text: e.device), - CustomTableCell.text(text: e.port.toString()), - CustomTableCell.text(text: e.localPort.toString()), - CustomTableCell.text(text: e.localSshOptions.join(',')), - // CustomTableCell( - // child: Row( - // children: [ - // TextButton.icon( - // onPressed: () { - // context.pushNamed(StudentRoute.details.name, params: {'id': e.id}); - // }, - // icon: const Icon(Icons.visibility_outlined), - // label: Text(strings.view)), - // ], - // )) + CustomTableCell.text(text: e.host ?? ''), ], ), ) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/app_navigation_rail.dart b/packages/sshnp_gui/lib/src/presentation/widgets/app_navigation_rail.dart index d5aa3576f..c5ed92d3c 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/app_navigation_rail.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/app_navigation_rail.dart @@ -53,9 +53,7 @@ class AppNavigationRail extends ConsumerWidget { break; case 1: // set value to default create to trigger the create functionality on - ref - .read(sshnpParamsProvider.notifier) - .update((state) => SSHNPParams(clientAtSign: '', sshnpdAtSign: '', host: '', legacyDaemon: true)); + ref.read(sshnpPartialParamsProvider.notifier).update((state) => SSHNPPartialParams.empty()); context.goNamed(AppRoute.newConnection.name); break; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart index a6e4dda5f..cec6db5da 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart @@ -1,15 +1,14 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/home_screen_controller.dart'; import '../../utils/sizes.dart'; class DeleteAlertDialog extends ConsumerWidget { - const DeleteAlertDialog({required this.index, super.key}); - final int index; + const DeleteAlertDialog({required this.sshnpParams, super.key}); + final SSHNPParams sshnpParams; @override Widget build( @@ -18,7 +17,6 @@ class DeleteAlertDialog extends ConsumerWidget { ) { final strings = AppLocalizations.of(context)!; final data = ref.watch(homeScreenControllerProvider); - log(index.toString()); return Padding( padding: const EdgeInsets.only(left: 0), @@ -53,7 +51,7 @@ class DeleteAlertDialog extends ConsumerWidget { ), ElevatedButton( onPressed: () async { - await ref.read(homeScreenControllerProvider.notifier).delete(index); + await ref.read(homeScreenControllerProvider.notifier).delete(sshnpParams); if (context.mounted) Navigator.of(context).pop(); }, diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart index d0be24d13..48dfdb434 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart @@ -24,69 +24,21 @@ class NewConnectionForm extends ConsumerStatefulWidget { class _NewConnectionFormState extends ConsumerState { final GlobalKey _formkey = GlobalKey(); - late String? sshnpdAtSign; - late String? host; - late String? profileName; - - /// Optional Arguments - late String device; - late int port; - late int localPort; - late String sendSshPublicKey; - late List localSshOptions; - late bool verbose; - late bool rsa; - late String? remoteUsername; - late String? atKeysFilePath; - late String rootDomain; - late bool listDevices; - late bool legacyDaemon; + late SSHNPPartialParams oldConfig; @override void initState() { super.initState(); - - final oldConfig = ref.read(sshnpParamsProvider); - - sshnpdAtSign = oldConfig.sshnpdAtSign; - host = oldConfig.host; - profileName = oldConfig.profileName; - - /// Optional Arguments - device = oldConfig.device; - port = oldConfig.port; - localPort = oldConfig.localPort; - sendSshPublicKey = oldConfig.sendSshPublicKey; - localSshOptions = oldConfig.localSshOptions; - verbose = oldConfig.verbose; - rsa = oldConfig.rsa; - remoteUsername = oldConfig.remoteUsername; - atKeysFilePath = oldConfig.atKeysFilePath; - rootDomain = oldConfig.rootDomain; - listDevices = oldConfig.listDevices; - legacyDaemon = oldConfig.legacyDaemon; + oldConfig = ref.read(sshnpPartialParamsProvider); } void createNewConnection() async { if (_formkey.currentState!.validate()) { _formkey.currentState!.save(); + oldConfig.clientAtSign ??= AtClientManager.getInstance().atClient.getCurrentAtSign(); + // reset the partial params to empty so that the next time the user clicks on new connection the form is empty. + ref.read(sshnpPartialParamsProvider.notifier).update((state) => SSHNPPartialParams.empty()); - final sshnpParams = SSHNPParams( - profileName: 'default_profile', - clientAtSign: AtClientManager.getInstance().atClient.getCurrentAtSign(), - sshnpdAtSign: sshnpdAtSign, - host: host, - device: device, - port: port, - localPort: localPort, - sendSshPublicKey: sendSshPublicKey, - localSshOptions: localSshOptions, - verbose: verbose, - rsa: rsa, - remoteUsername: remoteUsername, - atKeysFilePath: atKeysFilePath, - rootDomain: rootDomain, - listDevices: listDevices, - legacyDaemon: legacyDaemon); + final sshnpParams = SSHNPParams.fromPartial(oldConfig); switch (ref.read(configFileWriteStateProvider)) { case ConfigFileWriteState.create: await ref.read(homeScreenControllerProvider.notifier).createConfigFile(sshnpParams); @@ -98,10 +50,6 @@ class _NewConnectionFormState extends ConsumerState { ref.read(configFileWriteStateProvider.notifier).update((state) => ConfigFileWriteState.create); break; } - // Reset value to default value. - ref - .read(sshnpParamsProvider.notifier) - .update((state) => SSHNPParams(clientAtSign: '', sshnpdAtSign: '', host: '', legacyDaemon: true)); if (context.mounted) { ref.read(currentNavIndexProvider.notifier).update((state) => AppRoute.home.index - 1); context.pushReplacementNamed(AppRoute.home.name); @@ -118,130 +66,127 @@ class _NewConnectionFormState extends ConsumerState { child: Row( children: [ Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - // TODO @CurtlyCritchlow - // * remove clientAtSign from the form (if clientAtsign is null then use the AtClient.getCurrentAtSign) - // * add profileName to the form CustomTextFormField( - initialValue: profileName, + initialValue: oldConfig.profileName, labelText: strings.profileName, - onSaved: (value) => profileName = value!, + onSaved: (value) => + oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(profileName: value!)), validator: Validator.validateRequiredField, ), gapH10, CustomTextFormField( - initialValue: host, + initialValue: oldConfig.host, labelText: strings.host, - onSaved: (value) => host = value, + onSaved: (value) => oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(host: value!)), validator: Validator.validateRequiredField, ), gapH10, CustomTextFormField( - initialValue: port.toString(), + initialValue: oldConfig.port.toString(), labelText: strings.port, - onSaved: (value) => port = int.parse(value!), + onSaved: (value) => + oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(port: int.parse(value!))), validator: Validator.validateRequiredField, ), gapH10, CustomTextFormField( - initialValue: sendSshPublicKey, + initialValue: oldConfig.sendSshPublicKey, labelText: strings.sendSshPublicKey, - onSaved: (value) => sendSshPublicKey = value!, + onSaved: (value) => + oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(sendSshPublicKey: value!)), validator: Validator.validateRequiredField, ), gapH10, Row( children: [ Text(strings.verbose), - gapW12, + gapW8, Switch( - value: verbose, + value: oldConfig.verbose ?? false, onChanged: (newValue) { setState(() { - verbose = newValue; + oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(verbose: newValue)); }); }), ], ), gapH10, CustomTextFormField( - initialValue: remoteUsername, + initialValue: oldConfig.remoteUsername, labelText: strings.remoteUserName, - onSaved: (value) => remoteUsername = value!, + onSaved: (value) => + oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(remoteUsername: value!)), ), gapH10, CustomTextFormField( - initialValue: rootDomain, + initialValue: oldConfig.rootDomain, labelText: strings.rootDomain, - onSaved: (value) => rootDomain = value!, + onSaved: (value) => + oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(rootDomain: value!)), ), gapH20, - // TODO the edit screen also says "add", can we change the wording to be dynamic, or use "submit" ElevatedButton( onPressed: createNewConnection, - child: Text(strings.add), + child: Text(strings.submit), ), ]), gapW12, Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTextFormField( - initialValue: sshnpdAtSign, + initialValue: oldConfig.sshnpdAtSign, labelText: strings.sshnpdAtSign, - onSaved: (value) => sshnpdAtSign = value, + onSaved: (value) => + oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(sshnpdAtSign: value!)), validator: Validator.validateAtsignField, ), gapH10, CustomTextFormField( - initialValue: device, + initialValue: oldConfig.device, labelText: strings.device, - onSaved: (value) => device = value!, + onSaved: (value) => oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(device: value!)), ), gapH10, CustomTextFormField( - initialValue: localPort.toString(), + initialValue: oldConfig.localPort.toString(), labelText: strings.localPort, - onSaved: (value) => localPort = int.parse(value!), + onSaved: (value) => + oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(localPort: int.parse(value!))), ), gapH10, - // TODO add a note that says multiple options can be specified by separating them with a comma. CustomTextFormField( - initialValue: localSshOptions.join(','), + initialValue: oldConfig.localSshOptions.join(','), + hintText: strings.localSshOptionsHint, labelText: strings.localSshOptions, - onSaved: (value) => localSshOptions = value!.split(','), + onSaved: (value) => oldConfig = + SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(localSshOptions: value!.split(','))), ), gapH10, Row( children: [ Text(strings.rsa), - gapW12, + gapW8, Switch( - value: rsa, + value: oldConfig.rsa ?? false, onChanged: (newValue) { setState(() { - rsa = newValue; + oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(rsa: newValue)); }); }), ], ), gapH10, CustomTextFormField( - initialValue: atKeysFilePath, + initialValue: oldConfig.atKeysFilePath, labelText: strings.atKeysFilePath, - onSaved: (value) => atKeysFilePath = value, + onSaved: (value) => + oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(atKeysFilePath: value!)), ), gapH10, - // TODO remove listDevices from the form - Row( - children: [ - Text(strings.listDevices), - gapW12, - Switch( - value: listDevices, - onChanged: (newValue) { - setState(() { - listDevices = newValue; - }); - }), - ], + CustomTextFormField( + initialValue: oldConfig.localSshdPort.toString(), + labelText: strings.localSshdPort, + onSaved: (value) => oldConfig = + SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(localSshdPort: int.parse(value!))), ), gapH20, TextButton( diff --git a/packages/sshnp_gui/lib/src/utils/sizes.dart b/packages/sshnp_gui/lib/src/utils/sizes.dart index 5569f28fb..4f7acd158 100644 --- a/packages/sshnp_gui/lib/src/utils/sizes.dart +++ b/packages/sshnp_gui/lib/src/utils/sizes.dart @@ -5,7 +5,7 @@ class Sizes { static const p2 = 2.0; static const p3 = 3.0; static const p4 = 4.0; - // static const p8 = 8.0; + static const p8 = 8.0; static const p10 = 10.0; static const p12 = 12.0; // static const p14 = 14.0; @@ -26,7 +26,7 @@ class Sizes { const gap0 = SizedBox(); /// Constant gap widths -// const gapW8 = SizedBox(width: Sizes.p8); +const gapW8 = SizedBox(width: Sizes.p8); const gapW12 = SizedBox(width: Sizes.p12); const gapW16 = SizedBox(width: Sizes.p16); // const gapW20 = SizedBox(width: Sizes.p20); @@ -37,7 +37,7 @@ const gapW16 = SizedBox(width: Sizes.p16); // /// Constant gap heights const gapH4 = SizedBox(height: Sizes.p4); -// const gapH8 = SizedBox(height: Sizes.p8); +const gapH8 = SizedBox(height: Sizes.p8); const gapH10 = SizedBox(height: Sizes.p10); const gapH12 = SizedBox(height: Sizes.p12); const gapH16 = SizedBox(height: Sizes.p16); From eee0b31b5a095a7fc5ea4408938b233a38e6a226 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 5 Sep 2023 16:33:10 +0800 Subject: [PATCH 02/95] chore: clean up sshnp params and implement merge and empty on the full class --- .../sshnoports/lib/sshnp/sshnp_params.dart | 155 +++++++++++------- 1 file changed, 100 insertions(+), 55 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index e293637f6..42ef260eb 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -6,28 +6,29 @@ class SSHNPParams { /// Since there are multiple sources for these values, we cannot validate /// that they will be provided. If any are null, then the caller must /// handle the error. - late final String? clientAtSign; - late final String? sshnpdAtSign; - late final String? host; + final String? clientAtSign; + final String? sshnpdAtSign; + final String? host; /// Optional Arguments - late final String device; - late final int port; - late final int localPort; + final String device; + final int port; + final int localPort; late final String username; late final String homeDirectory; late final String atKeysFilePath; - late final String sendSshPublicKey; - late final List localSshOptions; - late final bool rsa; - late final String? remoteUsername; - late final bool verbose; - late final String rootDomain; - late final int localSshdPort; - late final bool legacyDaemon; + final String sendSshPublicKey; + final List localSshOptions; + final bool rsa; + final String? remoteUsername; + final bool verbose; + final String rootDomain; + final int localSshdPort; + final bool legacyDaemon; /// Special Arguments - late final String? profileName; // automatically populated with the filename if from a configFile + late final String? + profileName; // automatically populated with the filename if from a configFile late final bool listDevices; SSHNPParams({ @@ -57,7 +58,40 @@ class SSHNPParams { // Use default atKeysFilePath if not provided - this.atKeysFilePath = atKeysFilePath ?? getDefaultAtKeysFilePath(homeDirectory, clientAtSign); + this.atKeysFilePath = + atKeysFilePath ?? getDefaultAtKeysFilePath(homeDirectory, clientAtSign); + } + + factory SSHNPParams.merge(SSHNPParams params1, + [SSHNPPartialParams? params2]) { + params2 ??= SSHNPPartialParams.empty(); + return SSHNPParams( + profileName: params2.profileName ?? params1.profileName, + clientAtSign: params2.clientAtSign ?? params1.clientAtSign, + sshnpdAtSign: params2.sshnpdAtSign ?? params1.sshnpdAtSign, + host: params2.host ?? params1.host, + device: params2.device ?? params1.device, + port: params2.port ?? params1.port, + localPort: params2.localPort ?? params1.localPort, + atKeysFilePath: params2.atKeysFilePath ?? params1.atKeysFilePath, + sendSshPublicKey: params2.sendSshPublicKey ?? params1.sendSshPublicKey, + localSshOptions: params1.localSshOptions + params2.localSshOptions, + rsa: params2.rsa ?? params1.rsa, + remoteUsername: params2.remoteUsername ?? params1.remoteUsername, + verbose: params2.verbose ?? params1.verbose, + rootDomain: params2.rootDomain ?? params1.rootDomain, + localSshdPort: params2.localSshdPort ?? params1.localSshdPort, + listDevices: params2.listDevices ?? params1.listDevices, + legacyDaemon: params2.legacyDaemon ?? params1.legacyDaemon, + ); + } + + factory SSHNPParams.empty() { + return SSHNPParams( + clientAtSign: null, + sshnpdAtSign: null, + host: null, + ); } factory SSHNPParams.fromPartial(SSHNPPartialParams partial) { @@ -77,7 +111,8 @@ class SSHNPParams { device: partial.device ?? SSHNP.defaultDevice, port: partial.port ?? SSHNP.defaultPort, localPort: partial.localPort ?? SSHNP.defaultLocalPort, - sendSshPublicKey: partial.sendSshPublicKey ?? SSHNP.defaultSendSshPublicKey, + sendSshPublicKey: + partial.sendSshPublicKey ?? SSHNP.defaultSendSshPublicKey, localSshOptions: partial.localSshOptions, rsa: partial.rsa ?? SSHNP.defaultRsa, verbose: partial.verbose ?? SSHNP.defaultRsa, @@ -85,7 +120,7 @@ class SSHNPParams { atKeysFilePath: partial.atKeysFilePath, rootDomain: partial.rootDomain ?? SSHNP.defaultRootDomain, localSshdPort: partial.localSshdPort ?? SSHNP.defaultLocalSshdPort, - listDevices: partial.listDevices, + listDevices: partial.listDevices ?? SSHNP.defaultListDevices, legacyDaemon: partial.legacyDaemon ?? SSHNP.defaultLegacyDaemon, ); } @@ -94,7 +129,8 @@ class SSHNPParams { return SSHNPParams.fromPartial(SSHNPPartialParams.fromConfig(fileName)); } - static Future> getConfigFilesFromDirectory([String? directory]) async { + static Future> getConfigFilesFromDirectory( + [String? directory]) async { var params = []; var homeDirectory = getHomeDirectory(throwIfNull: true)!; @@ -132,7 +168,8 @@ class SSHNPParams { var exists = await file.exists(); if (exists && !overwrite) { - throw Exception('Failed to write config file: ${file.path} already exists'); + throw Exception( + 'Failed to write config file: ${file.path} already exists'); } // FileMode.write will create the file if it does not exist @@ -140,7 +177,7 @@ class SSHNPParams { return file.writeAsString(toConfig(), mode: FileMode.write); } - Future deleteFile({String? directory, bool overwrite = false}) async { + Future deleteFile({String? directory}) async { if (profileName == null || profileName!.isEmpty) { throw Exception('profileName is null or empty'); } @@ -201,26 +238,26 @@ class SSHNPParams { /// e.g. default values from a config file and the rest from the command line class SSHNPPartialParams { /// Main Params - late final String? profileName; - late final String? clientAtSign; - late final String? sshnpdAtSign; - late final String? host; - late final String? device; - late final int? port; - late final int? localPort; - late final int? localSshdPort; - late final String? atKeysFilePath; - late final String? sendSshPublicKey; - late final List localSshOptions; - late final bool? rsa; - late final String? remoteUsername; - late final bool? verbose; - late final String? rootDomain; - late final bool? legacyDaemon; + String? profileName; + String? clientAtSign; + String? sshnpdAtSign; + String? host; + String? device; + int? port; + int? localPort; + int? localSshdPort; + String? atKeysFilePath; + String? sendSshPublicKey; + List localSshOptions; + bool? rsa; + String? remoteUsername; + bool? verbose; + String? rootDomain; + bool? legacyDaemon; /// Special Params // N.B. config file is a meta param and doesn't need to be included - late final bool listDevices; + bool? listDevices; // Non param variables static final ArgParser parser = _createArgParser(); @@ -230,19 +267,19 @@ class SSHNPPartialParams { this.clientAtSign, this.sshnpdAtSign, this.host, - this.device = SSHNP.defaultDevice, - this.port = SSHNP.defaultPort, - this.localPort = SSHNP.defaultLocalPort, + this.device, + this.port, + this.localPort, this.atKeysFilePath, - this.sendSshPublicKey = SSHNP.defaultSendSshPublicKey, + this.sendSshPublicKey, this.localSshOptions = SSHNP.defaultLocalSshOptions, - this.rsa = SSHNP.defaultRsa, + this.rsa, this.remoteUsername, - this.verbose = SSHNP.defaultVerbose, - this.rootDomain = SSHNP.defaultRootDomain, - this.localSshdPort = SSHNP.defaultLocalSshdPort, - this.listDevices = SSHNP.defaultListDevices, - this.legacyDaemon = SSHNP.defaultLegacyDaemon, + this.verbose, + this.rootDomain, + this.localSshdPort, + this.listDevices, + this.legacyDaemon, }); factory SSHNPPartialParams.empty() { @@ -252,7 +289,8 @@ class SSHNPPartialParams { /// Merge two SSHNPPartialParams objects together /// Params in params2 take precedence over params1 /// - localSshOptions are concatenated together as (params1 + params2) - factory SSHNPPartialParams.merge(SSHNPPartialParams params1, [SSHNPPartialParams? params2]) { + factory SSHNPPartialParams.merge(SSHNPPartialParams params1, + [SSHNPPartialParams? params2]) { params2 ??= SSHNPPartialParams.empty(); return SSHNPPartialParams( profileName: params2.profileName ?? params1.profileName, @@ -270,7 +308,7 @@ class SSHNPPartialParams { verbose: params2.verbose ?? params1.verbose, rootDomain: params2.rootDomain ?? params1.rootDomain, localSshdPort: params2.localSshdPort ?? params1.localSshdPort, - listDevices: params2.listDevices || params1.listDevices, + listDevices: params2.listDevices ?? params1.listDevices, legacyDaemon: params2.legacyDaemon ?? params1.legacyDaemon, ); } @@ -286,7 +324,8 @@ class SSHNPPartialParams { localPort: args['local-port'], atKeysFilePath: args['key-file'], sendSshPublicKey: args['ssh-public-key'], - localSshOptions: args['local-ssh-options'] ?? SSHNP.defaultLocalSshOptions, + localSshOptions: + args['local-ssh-options'] ?? SSHNP.defaultLocalSshOptions, rsa: args['rsa'], remoteUsername: args['remote-user-name'], verbose: args['verbose'], @@ -299,7 +338,8 @@ class SSHNPPartialParams { factory SSHNPPartialParams.fromConfig(String fileName) { var args = _parseConfigFile(fileName); - args['profile-name'] = path.basenameWithoutExtension(fileName).replaceAll('_', ' '); + args['profile-name'] = + path.basenameWithoutExtension(fileName).replaceAll('_', ' '); return SSHNPPartialParams.fromArgMap(args); } @@ -319,7 +359,9 @@ class SSHNPPartialParams { } // THIS IS A WORKAROUND IN ORDER TO BE TYPE SAFE IN SSHNPPartialParams.fromArgMap - Map parsedArgsMap = {for (var e in (parsedArgs.options)) e: parsedArgs[e]}; + Map parsedArgsMap = { + for (var e in (parsedArgs.options)) e: parsedArgs[e] + }; return SSHNPPartialParams.merge( params, @@ -341,7 +383,9 @@ class SSHNPPartialParams { arg.name, abbr: arg.abbr, mandatory: arg.mandatory, - defaultsTo: withDefaults ? (arg.defaultsTo != null ? '${arg.defaultsTo}' : null) : null, + defaultsTo: withDefaults + ? (arg.defaultsTo != null ? '${arg.defaultsTo}' : null) + : null, help: arg.help, ); break; @@ -366,7 +410,8 @@ class SSHNPPartialParams { if (withConfig) { parser.addOption( 'config-file', - help: 'Read args from a config file\nMandatory args are not required if already supplied in the config file', + help: + 'Read args from a config file\nMandatory args are not required if already supplied in the config file', ); } if (withListDevices) { From 3fdac47839613816a33f4fc1951a5a89817ecf99 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 5 Sep 2023 16:33:51 +0800 Subject: [PATCH 03/95] feat: implement sshnp config controller family --- .../lib/src/controllers/minor_providers.dart | 4 -- .../controllers/sshnp_config_controller.dart | 55 +++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart diff --git a/packages/sshnp_gui/lib/src/controllers/minor_providers.dart b/packages/sshnp_gui/lib/src/controllers/minor_providers.dart index 8aa0b1035..52bfb4acc 100644 --- a/packages/sshnp_gui/lib/src/controllers/minor_providers.dart +++ b/packages/sshnp_gui/lib/src/controllers/minor_providers.dart @@ -4,10 +4,6 @@ import 'package:sshnp_gui/src/utils/enum.dart'; final currentNavIndexProvider = StateProvider((ref) => 0); -final sshnpPartialParamsProvider = StateProvider( - (ref) => SSHNPPartialParams(), -); - final configFileWriteStateProvider = StateProvider( (ref) => ConfigFileWriteState.create, ); diff --git a/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart b/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart new file mode 100644 index 000000000..979638d27 --- /dev/null +++ b/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; + +final sshnpConfigController = AutoDisposeAsyncNotifierProviderFamily< + SSHNPConfigContoller, + Map, + String>(SSHNPConfigContoller.new); + +class SSHNPConfigContoller + extends AutoDisposeFamilyAsyncNotifier, String> { + @override + Future> build(String arg) async { + return Map.fromIterable( + await SSHNPParams.getConfigFilesFromDirectory(), + key: (e) => e + .profileName!, // Profile name should never be null when using getConfigFilesFromDirectory + ); + } + + void addConfig(SSHNPParams params) { + update((p0) async { + if (p0.containsKey(params.profileName!)) { + throw Exception('Profile ${params.profileName} already exists'); + } + await params.toFile(); + p0[params.profileName!] = params; + return p0; + }); + } + + void updateConfig(String profileName, SSHNPPartialParams newParams) { + update((p0) async { + if (!p0.containsKey(profileName)) { + throw Exception('Profile $profileName does not exist'); + } + var params = SSHNPParams.merge(p0[profileName]!, newParams); + await params.toFile(overwrite: true); + p0[profileName] = params; + return p0; + }); + } + + void deleteConfig(String profileName) { + update((p0) async { + if (!p0.containsKey(profileName)) { + throw Exception('Profile $profileName does not exist'); + } + await p0[profileName]!.deleteFile(); + p0.remove(profileName); + return p0; + }); + } +} From d57ff866c289b9be5b381c960fa4638aed58063b Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 5 Sep 2023 19:41:53 +0800 Subject: [PATCH 04/95] refactor: add sshnp params controller --- .../sshnoports/lib/sshnp/sshnp_params.dart | 44 ++- .../src/controllers/home_screen_actions.dart | 0 .../controllers/home_screen_controller.dart | 92 ----- .../lib/src/controllers/minor_providers.dart | 5 - .../controllers/sshnp_config_controller.dart | 95 +++--- .../src/presentation/screens/home_screen.dart | 214 ++++-------- .../presentation/screens/terminal_screen.dart | 5 - .../widgets/app_navigation_rail.dart | 4 - .../widgets/delete_alert_dialog.dart | 53 +-- .../home_screen_delete.dart | 22 ++ .../home_screen_actions/home_screen_edit.dart | 47 +++ .../home_screen_actions/home_screen_run.dart | 74 ++++ .../home_screen_table_actions.dart | 31 ++ .../home_screen_table_header.dart | 15 + .../home_screen_table_text.dart | 69 ++++ .../widgets/new_connection_form.dart | 321 +++++++++--------- packages/sshnp_gui/macos/Podfile.lock | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- packages/sshnp_gui/pubspec.lock | 42 ++- packages/sshnp_gui/pubspec.yaml | 1 - 21 files changed, 628 insertions(+), 512 deletions(-) create mode 100644 packages/sshnp_gui/lib/src/controllers/home_screen_actions.dart delete mode 100644 packages/sshnp_gui/lib/src/controllers/home_screen_controller.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_delete.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_edit.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_run.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_header.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_text.dart diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index 42ef260eb..dddea16f9 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -88,9 +88,9 @@ class SSHNPParams { factory SSHNPParams.empty() { return SSHNPParams( - clientAtSign: null, - sshnpdAtSign: null, - host: null, + clientAtSign: '', + sshnpdAtSign: '', + host: '', ); } @@ -129,9 +129,8 @@ class SSHNPParams { return SSHNPParams.fromPartial(SSHNPPartialParams.fromConfig(fileName)); } - static Future> getConfigFilesFromDirectory( - [String? directory]) async { - var params = []; + static Future> listFiles([String? directory]) async { + var fileNames = {}; var homeDirectory = getHomeDirectory(throwIfNull: true)!; directory ??= getDefaultSshnpConfigDirectory(homeDirectory); @@ -140,17 +139,38 @@ class SSHNPParams { await files.forEach((file) { if (file is! File) return; if (path.extension(file.path) != '.env') return; + fileNames.add(_fileToProfileName(file.path)); try { var p = SSHNPParams.fromConfigFile(file.path); - - params.add(p); + fileNames.add(p.profileName!); } catch (e) { print('Error reading config file: ${file.path}'); print(e); } }); - return params; + return fileNames; + } + + static Future fromFile(String profileName, + [String? directory]) async { + var homeDirectory = getHomeDirectory(throwIfNull: true)!; + directory ??= getDefaultSshnpConfigDirectory(homeDirectory); + var fileName = path.join( + directory, + '$profileName.env', + ); + return SSHNPParams.fromConfigFile(fileName); + } + + static Future fileExists(String profileName, [String? directory]) { + var homeDirectory = getHomeDirectory(throwIfNull: true)!; + directory ??= getDefaultSshnpConfigDirectory(homeDirectory); + var fileName = path.join( + directory, + '$profileName.env', + ); + return File(fileName).exists(); } Future toFile({String? directory, bool overwrite = false}) async { @@ -338,8 +358,7 @@ class SSHNPPartialParams { factory SSHNPPartialParams.fromConfig(String fileName) { var args = _parseConfigFile(fileName); - args['profile-name'] = - path.basenameWithoutExtension(fileName).replaceAll('_', ' '); + args['profile-name'] = _fileToProfileName(fileName); return SSHNPPartialParams.fromArgMap(args); } @@ -480,3 +499,6 @@ class SSHNPPartialParams { } } } + +String _fileToProfileName(String fileName) => + path.basenameWithoutExtension(fileName).replaceAll('_', ' '); diff --git a/packages/sshnp_gui/lib/src/controllers/home_screen_actions.dart b/packages/sshnp_gui/lib/src/controllers/home_screen_actions.dart new file mode 100644 index 000000000..e69de29bb diff --git a/packages/sshnp_gui/lib/src/controllers/home_screen_controller.dart b/packages/sshnp_gui/lib/src/controllers/home_screen_controller.dart deleted file mode 100644 index 9e4d2d511..000000000 --- a/packages/sshnp_gui/lib/src/controllers/home_screen_controller.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'dart:developer'; -import 'dart:io'; - -import 'package:at_client_mobile/at_client_mobile.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sshnoports/common/utils.dart'; -import 'package:sshnoports/sshnp/sshnp.dart'; - -/// A Controller class that controls the UI update when the [AtDataRepository] methods are called. -class HomeScreenController extends StateNotifier>> { - final Ref ref; - - HomeScreenController({required this.ref}) : super(const AsyncValue.loading()); - - /// Get list of config files associated with the current astign. - Future getConfigFiles() async { - state = const AsyncValue.loading(); - state = await AsyncValue.guard(() async { - try { - var sshnpParams = await SSHNPParams.getConfigFilesFromDirectory(); - for (var element in sshnpParams.toList()) { - log(element.sshnpdAtSign.toString()); - } - return sshnpParams.toList(); - } on PathNotFoundException { - log('Path Not Found'); - return []; - } - }); - } - - Future getPublicKeyFromDirectory() async { - var homeDirectory = getHomeDirectory(throwIfNull: true)!; - - var files = Directory('$homeDirectory/.ssh').list(); - final publickey = await files.firstWhere((element) => element.path.contains('sshnp.pub')); - - return publickey.path.split('.ssh/').last; - } - - /// Deletes the [AtKey] associated with the [AtData]. - Future delete(SSHNPParams sshnpParams) async { - state = const AsyncValue.loading(); - - sshnpParams.deleteFile(); - await getConfigFiles(); - } - - // /// Deletes all [AtData] associated with the current atsign. - // Future deleteAllData() async { - // state = const AsyncValue.loading(); - // final directory = getDefaultSshnpConfigDirectory(getHomeDirectory()!); - // var configDir = await Directory(directory).list().toList(); - // configDir.map((e) => e.delete()); - // await getConfigFiles(); - // } - - /// create or update config files. - Future createConfigFile( - SSHNPParams sshnpParams, - ) async { - state = const AsyncValue.loading(); - final homeDir = getHomeDirectory()!; - - log(homeDir); - final configDir = getDefaultSshnpConfigDirectory(homeDir); - log(configDir); - await Directory(configDir).create(recursive: true); - //.env - sshnpParams.toFile(overwrite: false); - await getConfigFiles(); - } - - /// create or update config files. - Future updateConfigFile({required SSHNPParams sshnpParams}) async { - state = const AsyncValue.loading(); - - // final directory = getDefaultSshnpConfigDirectory(getHomeDirectory()!); - // var configDir = await Directory(directory).list().toList(); - // configDir.removeWhere((element) => path.extension(element.path) != '.env'); - // final index = ref.read(sshnpParamsUpdateIndexProvider); - // log('path is:${configDir[index].path}'); - // await Directory(configDir).create(recursive: true); - //.env - sshnpParams.toFile(overwrite: true); - await getConfigFiles(); - } -} - -/// A provider that exposes the [HomeScreenController] to the app. -final homeScreenControllerProvider = - StateNotifierProvider>>((ref) => HomeScreenController(ref: ref)); diff --git a/packages/sshnp_gui/lib/src/controllers/minor_providers.dart b/packages/sshnp_gui/lib/src/controllers/minor_providers.dart index 52bfb4acc..d54d43506 100644 --- a/packages/sshnp_gui/lib/src/controllers/minor_providers.dart +++ b/packages/sshnp_gui/lib/src/controllers/minor_providers.dart @@ -1,12 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/utils/enum.dart'; final currentNavIndexProvider = StateProvider((ref) => 0); -final configFileWriteStateProvider = StateProvider( - (ref) => ConfigFileWriteState.create, -); final terminalSSHCommandProvider = StateProvider( (ref) => '', ); diff --git a/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart b/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart index 979638d27..4878f0f61 100644 --- a/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart @@ -1,55 +1,72 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnoports/common/utils.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnp_gui/src/utils/enum.dart'; -final sshnpConfigController = AutoDisposeAsyncNotifierProviderFamily< - SSHNPConfigContoller, - Map, - String>(SSHNPConfigContoller.new); +/// Controller instance for the current [SSHNPParams] being edited +final currentParamsController = AutoDisposeNotifierProvider< + CurrentSSHNPParamsController, CurrentSSHNPParamsModel>( + () => CurrentSSHNPParamsController(), +); -class SSHNPConfigContoller - extends AutoDisposeFamilyAsyncNotifier, String> { +/// Controller instance for the list of all profileNames for each config file +final paramsListController = AutoDisposeAsyncNotifierProvider< + SSHNPParamsListController, Iterable>(SSHNPParamsListController.new); + +/// Controller instance for the family of [SSHNPParams] controllers +final paramsFamilyController = AutoDisposeAsyncNotifierProviderFamily< + SSHNPParamsFamilyController, + SSHNPParams, + String>(SSHNPParamsFamilyController.new); + +/// Holder model for the current [SSHNPParams] being edited +class CurrentSSHNPParamsModel { + final String profileName; + final ConfigFileWriteState configFileWriteState; + + CurrentSSHNPParamsModel( + {required this.profileName, required this.configFileWriteState}); +} + +/// Controller for the current [SSHNPParams] being edited +class CurrentSSHNPParamsController + extends AutoDisposeNotifier { @override - Future> build(String arg) async { - return Map.fromIterable( - await SSHNPParams.getConfigFilesFromDirectory(), - key: (e) => e - .profileName!, // Profile name should never be null when using getConfigFilesFromDirectory + CurrentSSHNPParamsModel build() { + return CurrentSSHNPParamsModel( + profileName: '', + configFileWriteState: ConfigFileWriteState.create, ); } - void addConfig(SSHNPParams params) { - update((p0) async { - if (p0.containsKey(params.profileName!)) { - throw Exception('Profile ${params.profileName} already exists'); - } - await params.toFile(); - p0[params.profileName!] = params; - return p0; - }); + void update(CurrentSSHNPParamsModel model) { + state = model; } +} - void updateConfig(String profileName, SSHNPPartialParams newParams) { - update((p0) async { - if (!p0.containsKey(profileName)) { - throw Exception('Profile $profileName does not exist'); - } - var params = SSHNPParams.merge(p0[profileName]!, newParams); - await params.toFile(overwrite: true); - p0[profileName] = params; - return p0; - }); +/// Controller for the family of [SSHNPParams] controllers +class SSHNPParamsFamilyController + extends AutoDisposeFamilyAsyncNotifier { + @override + Future build(String arg) async { + return (await SSHNPParams.fileExists(arg)) + ? await SSHNPParams.fromFile(arg) + : SSHNPParams.empty(); + } +} + +/// Controller for the list of all profileNames for each config file +class SSHNPParamsListController + extends AutoDisposeAsyncNotifier> { + @override + Future> build() { + return SSHNPParams.listFiles(); } - void deleteConfig(String profileName) { - update((p0) async { - if (!p0.containsKey(profileName)) { - throw Exception('Profile $profileName does not exist'); - } - await p0[profileName]!.deleteFile(); - p0.remove(profileName); - return p0; - }); + Future refresh() async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() => build()); } } diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index e615053af..a9003ec1a 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -1,21 +1,14 @@ -import 'package:at_client_mobile/at_client_mobile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnoports/sshrv/sshrv.dart'; -import 'package:sshnp_gui/src/controllers/minor_providers.dart'; -import 'package:sshnp_gui/src/presentation/widgets/sshnp_result_alert_dialog.dart'; -import 'package:sshnp_gui/src/utils/enum.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_table_header.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_table_text.dart'; -import '../../controllers/home_screen_controller.dart'; -import '../../utils/app_router.dart'; import '../../utils/sizes.dart'; import '../widgets/app_navigation_rail.dart'; -import '../widgets/custom_table_cell.dart'; -import '../widgets/delete_alert_dialog.dart'; // * Once the onboarding process is completed you will be taken to this screen class HomeScreen extends ConsumerStatefulWidget { @@ -29,76 +22,17 @@ class _HomeScreenState extends ConsumerState { @override void initState() { WidgetsBinding.instance.addPostFrameCallback((_) async { - await ref.read(homeScreenControllerProvider.notifier).getConfigFiles(); + await ref.read(paramsListController.notifier).refresh(); }); super.initState(); } - Future ssh(SSHNPParams sshnpParams) async { - if (mounted) { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) => const Center(child: CircularProgressIndicator()), - ); - } - - try { - final sshnp = await SSHNP.fromParams( - sshnpParams, - atClient: AtClientManager.getInstance().atClient, - sshrvGenerator: SSHRV.pureDart, - ); - - await sshnp.init(); - final sshnpResult = await sshnp.run(); - - if (mounted) { - // pop to remove circular progress indicator - context.pop(); - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) => SSHNPResultAlertDialog( - result: sshnpResult.toString(), - title: 'Success', - ), - ); - } - } catch (e) { - if (mounted) { - context.pop(); - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) => SSHNPResultAlertDialog( - result: e.toString(), - title: 'Failed', - ), - ); - } - } - } - - void updateConfigFile(SSHNPParams sshnpParams) { - ref - .read(sshnpPartialParamsProvider.notifier) - .update((state) => SSHNPPartialParams.fromArgMap(sshnpParams.toArgs())); - // change value to 1 to update navigation rail selcted icon. - ref.read(currentNavIndexProvider.notifier).update((state) => AppRoute.newConnection.index - 1); - // Change value to update to trigger the update functionality on the new connection form. - ref.read(configFileWriteStateProvider.notifier).update((state) => ConfigFileWriteState.update); - context.replaceNamed( - AppRoute.newConnection.name, - ); - } - @override Widget build(BuildContext context) { // * Getting the AtClientManager instance to use below final strings = AppLocalizations.of(context)!; - final state = ref.watch(homeScreenControllerProvider); + final profileNames = ref.watch(paramsListController); return Scaffold( body: SafeArea( @@ -109,87 +43,56 @@ class _HomeScreenState extends ConsumerState { Expanded( child: Padding( padding: const EdgeInsets.only(left: Sizes.p36, top: Sizes.p21), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - 'assets/images/noports_light.svg', - ), - gapH24, - Text(strings.availableConnections), - state.isLoading - ? const Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + 'assets/images/noports_light.svg', + ), + gapH24, + Text(strings.availableConnections), + profileNames.when( + loading: () => const Center( child: CircularProgressIndicator(), - ) - : state.value!.isNotEmpty - ? Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - columnWidths: const { - 0: IntrinsicColumnWidth(), - 1: IntrinsicColumnWidth(), - 2: IntrinsicColumnWidth(), - 3: IntrinsicColumnWidth(), - 4: IntrinsicColumnWidth(), - }, - children: [ - TableRow( - decoration: - const BoxDecoration(border: Border(bottom: BorderSide(color: Colors.white))), - children: [ - CustomTableCell.text(text: strings.actions), - CustomTableCell.text(text: strings.profileName), - CustomTableCell.text(text: strings.sshnpdAtSign), - CustomTableCell.text(text: strings.device), - CustomTableCell.text(text: strings.host), - ], - ), - ...state.value! - .map( - (e) => TableRow( - children: [ - CustomTableCell( - child: Row( - children: [ - IconButton( - onPressed: () async { - await ssh(e); - }, - icon: const Icon(Icons.connect_without_contact_outlined), - ), - IconButton( - onPressed: () { - updateConfigFile(e); - }, - icon: const Icon(Icons.edit), - ), - IconButton( - onPressed: () async { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) => - DeleteAlertDialog(sshnpParams: e), - ); - }, - icon: const Icon(Icons.delete_forever), - ), - ], - )), - CustomTableCell.text(text: e.profileName ?? ''), - CustomTableCell.text(text: e.sshnpdAtSign ?? ''), - CustomTableCell.text(text: e.device), - CustomTableCell.text(text: e.host ?? ''), - ], - ), - ) - .toList() - ], - ), + ), + error: (e, s) => Text(e.toString()), + data: (profiles) { + if (profiles.isEmpty) { + return const Text('No SSHNP Configurations Found'); + } + return Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Table( + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + columnWidths: const { + 0: IntrinsicColumnWidth(), + 1: IntrinsicColumnWidth(), + 2: IntrinsicColumnWidth(), + 3: IntrinsicColumnWidth(), + 4: IntrinsicColumnWidth(), + }, + children: [ + getHomeScreenTableHeader(strings), + ...profiles.map((e) { + final params = + ref.watch(paramsFamilyController(e)); + return TableRow(children: [ + HomeScreenTableActions(params), + HomeScreenTableProfileNameText(params), + HomeScreenTableSshnpdAtSignText(params), + HomeScreenTableDeviceText(params), + HomeScreenTableHostText(params), + ]); + }).toList() + ], ), - ) - : const Text('No SSHNP Configurations Found') - ]), + ), + ); + }, + ) + ]), ), ), ], @@ -198,3 +101,12 @@ class _HomeScreenState extends ConsumerState { ); } } + +class HomeScreenBodyWrapper extends StatelessWidget { + const HomeScreenBodyWrapper({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index 3ad3e9e41..bc01ec374 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -2,14 +2,12 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sshnp_gui/src/controllers/minor_providers.dart'; import 'package:xterm/xterm.dart'; -import '../../controllers/home_screen_controller.dart'; import '../../utils/sizes.dart'; import '../widgets/app_navigation_rail.dart'; @@ -72,9 +70,6 @@ class _TerminalScreenState extends ConsumerState { Widget build(BuildContext context) { // * Getting the AtClientManager instance to use below - final strings = AppLocalizations.of(context)!; - final state = ref.watch(homeScreenControllerProvider); - return Scaffold( body: SafeArea( child: Row( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/app_navigation_rail.dart b/packages/sshnp_gui/lib/src/presentation/widgets/app_navigation_rail.dart index c5ed92d3c..69a6d517e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/app_navigation_rail.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/app_navigation_rail.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnoports/sshnp/sshnp.dart'; import '../../controllers/minor_providers.dart'; import '../../utils/app_router.dart'; @@ -52,9 +51,6 @@ class AppNavigationRail extends ConsumerWidget { context.goNamed(AppRoute.home.name); break; case 1: - // set value to default create to trigger the create functionality on - ref.read(sshnpPartialParamsProvider.notifier).update((state) => SSHNPPartialParams.empty()); - context.goNamed(AppRoute.newConnection.name); break; case 2: diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart index cec6db5da..0c3b0ef8e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/home_screen_controller.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; import '../../utils/sizes.dart'; @@ -16,7 +16,6 @@ class DeleteAlertDialog extends ConsumerWidget { WidgetRef ref, ) { final strings = AppLocalizations.of(context)!; - final data = ref.watch(homeScreenControllerProvider); return Padding( padding: const EdgeInsets.only(left: 0), @@ -33,7 +32,10 @@ class DeleteAlertDialog extends ConsumerWidget { children: [ TextSpan( text: strings.note, - style: Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w700), + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(fontWeight: FontWeight.w700), ), TextSpan( text: strings.noteMessage, @@ -47,28 +49,33 @@ class DeleteAlertDialog extends ConsumerWidget { OutlinedButton( onPressed: () => Navigator.of(context).pop(false), child: Text(strings.cancelButton, - style: Theme.of(context).textTheme.bodyLarge!.copyWith(decoration: TextDecoration.underline)), + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(decoration: TextDecoration.underline)), ), ElevatedButton( - onPressed: () async { - await ref.read(homeScreenControllerProvider.notifier).delete(sshnpParams); - - if (context.mounted) Navigator.of(context).pop(); - }, - style: Theme.of(context).elevatedButtonTheme.style!.copyWith( - backgroundColor: MaterialStateProperty.all(Colors.black), - ), - child: !data.isLoading - ? Text( - strings.deleteButton, - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith(fontWeight: FontWeight.w700, color: Colors.white), - ) - : const CircularProgressIndicator( - color: Colors.white, - )), + onPressed: () { + ref + .read(paramsFamilyController(sshnpParams.profileName!)) + .whenData( + (value) async { + await value.deleteFile(); + if (context.mounted) Navigator.of(context).pop(); + }, + ); + }, + style: Theme.of(context).elevatedButtonTheme.style!.copyWith( + backgroundColor: MaterialStateProperty.all(Colors.black), + ), + child: Text( + strings.deleteButton, + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(fontWeight: FontWeight.w700, color: Colors.white), + ), + ) ], ), ), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_delete.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_delete.dart new file mode 100644 index 000000000..af70a0dde --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_delete.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnp_gui/src/presentation/widgets/delete_alert_dialog.dart'; + +class HomeScreenDeleteAction extends StatelessWidget { + final SSHNPParams params; + const HomeScreenDeleteAction(this.params, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () async { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => DeleteAlertDialog(sshnpParams: params), + ); + }, + icon: const Icon(Icons.delete_forever), + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_edit.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_edit.dart new file mode 100644 index 000000000..9376e40fb --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_edit.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/utils/app_router.dart'; +import 'package:sshnp_gui/src/utils/enum.dart'; + +class HomeScreenEditAction extends ConsumerStatefulWidget { + final SSHNPParams params; + const HomeScreenEditAction(this.params, {Key? key}) : super(key: key); + + @override + ConsumerState createState() => + _HomeScreenEditActionState(); +} + +class _HomeScreenEditActionState extends ConsumerState { + void updateConfigFile(SSHNPParams sshnpParams) { + // Change value to update to trigger the update functionality on the new connection form. + ref.read(currentParamsController.notifier).update( + CurrentSSHNPParamsModel( + profileName: sshnpParams.profileName!, + configFileWriteState: ConfigFileWriteState.update, + ), + ); + // change value to 1 to update navigation rail selcted icon. + ref + .read(currentNavIndexProvider.notifier) + .update((state) => AppRoute.newConnection.index - 1); + context.replaceNamed( + AppRoute.newConnection.name, + ); + } + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () { + updateConfigFile(widget.params); + }, + icon: const Icon(Icons.edit), + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_run.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_run.dart new file mode 100644 index 000000000..6f3112aec --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_run.dart @@ -0,0 +1,74 @@ +import 'package:at_client_mobile/at_client_mobile.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnoports/sshrv/sshrv.dart'; +import 'package:sshnp_gui/src/presentation/widgets/sshnp_result_alert_dialog.dart'; + +class HomeScreenRunAction extends StatefulWidget { + final SSHNPParams params; + const HomeScreenRunAction(this.params, {Key? key}) : super(key: key); + + @override + State createState() => _HomeScreenRunActionState(); +} + +class _HomeScreenRunActionState extends State { + +Future onPressed(SSHNPParams sshnpParams) async { + if (mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => + const Center(child: CircularProgressIndicator()), + ); + } + + try { + final sshnp = await SSHNP.fromParams( + sshnpParams, + atClient: AtClientManager.getInstance().atClient, + sshrvGenerator: SSHRV.pureDart, + ); + + await sshnp.init(); + final sshnpResult = await sshnp.run(); + + if (mounted) { + // pop to remove circular progress indicator + context.pop(); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => SSHNPResultAlertDialog( + result: sshnpResult.toString(), + title: 'Success', + ), + ); + } + } catch (e) { + if (mounted) { + context.pop(); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => SSHNPResultAlertDialog( + result: e.toString(), + title: 'Failed', + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () async { + await onPressed(widget.params); + }, + icon: const Icon(Icons.play_arrow), + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart new file mode 100644 index 000000000..5b2ad9305 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnp_gui/src/presentation/widgets/custom_table_cell.dart'; +import 'package:sshnp_gui/src/presentation/widgets/delete_alert_dialog.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_delete.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_edit.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_run.dart'; + +class HomeScreenTableActions extends StatelessWidget { + final AsyncValue params; + const HomeScreenTableActions(this.params, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return params.when( + data: (p) => CustomTableCell( + child: Row( + children: [ + HomeScreenRunAction(p), + HomeScreenEditAction(p), + HomeScreenDeleteAction(p), + ], + ), + ), + error: (e, s) => + const CustomTableCell.text(text: 'Error fetching data...'), + loading: () => const CustomTableCell.text(text: '...'), + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_header.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_header.dart new file mode 100644 index 000000000..a355c6600 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_header.dart @@ -0,0 +1,15 @@ +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter/material.dart'; +import 'package:sshnp_gui/src/presentation/widgets/custom_table_cell.dart'; + +TableRow getHomeScreenTableHeader(AppLocalizations strings) => TableRow( + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.white))), + children: [ + CustomTableCell.text(text: strings.actions), + CustomTableCell.text(text: strings.profileName), + CustomTableCell.text(text: strings.sshnpdAtSign), + CustomTableCell.text(text: strings.device), + CustomTableCell.text(text: strings.host), + ], + ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_text.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_text.dart new file mode 100644 index 000000000..a25e3bdb6 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_text.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnp_gui/src/presentation/widgets/custom_table_cell.dart'; + +class HomeScreenTableProfileNameText extends StatelessWidget { + final AsyncValue params; + const HomeScreenTableProfileNameText(this.params, {Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return params.when( + data: (p) => CustomTableCell.text(text: p.profileName!), + error: (e, s) => + const CustomTableCell.text(text: 'Error fetching data...'), + loading: () => const CustomTableCell.text(text: '...'), + ); + } +} + +class HomeScreenTableSshnpdAtSignText extends StatelessWidget { + final AsyncValue params; + const HomeScreenTableSshnpdAtSignText(this.params, {Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return params.when( + data: (p) => CustomTableCell.text(text: p.sshnpdAtSign!), + error: (e, s) => + const CustomTableCell.text(text: 'Error fetching data...'), + loading: () => const CustomTableCell.text(text: '...'), + ); + } +} + +class HomeScreenTableDeviceText extends StatelessWidget { + final AsyncValue params; + const HomeScreenTableDeviceText(this.params, {Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return params.when( + data: (p) => CustomTableCell.text(text: p.device), + error: (e, s) => + const CustomTableCell.text(text: 'Error fetching data...'), + loading: () => const CustomTableCell.text(text: '...'), + ); + } +} + +class HomeScreenTableHostText extends StatelessWidget { + final AsyncValue params; + const HomeScreenTableHostText(this.params, {Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return params.when( + data: (p) => CustomTableCell.text(text: p.host!), + error: (e, s) => + const CustomTableCell.text(text: 'Error fetching data...'), + loading: () => const CustomTableCell.text(text: '...'), + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart index 48dfdb434..637f5f404 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart @@ -1,13 +1,11 @@ -import 'dart:developer'; - -import 'package:at_client_mobile/at_client_mobile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; + import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/home_screen_controller.dart'; import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; import 'package:sshnp_gui/src/utils/validator.dart'; @@ -24,34 +22,23 @@ class NewConnectionForm extends ConsumerStatefulWidget { class _NewConnectionFormState extends ConsumerState { final GlobalKey _formkey = GlobalKey(); - late SSHNPPartialParams oldConfig; + late CurrentSSHNPParamsModel currentProfile; @override void initState() { + currentProfile = ref.read(currentParamsController); super.initState(); - oldConfig = ref.read(sshnpPartialParamsProvider); } - void createNewConnection() async { + void onSubmit(config) async { if (_formkey.currentState!.validate()) { _formkey.currentState!.save(); - oldConfig.clientAtSign ??= AtClientManager.getInstance().atClient.getCurrentAtSign(); - // reset the partial params to empty so that the next time the user clicks on new connection the form is empty. - ref.read(sshnpPartialParamsProvider.notifier).update((state) => SSHNPPartialParams.empty()); - - final sshnpParams = SSHNPParams.fromPartial(oldConfig); - switch (ref.read(configFileWriteStateProvider)) { - case ConfigFileWriteState.create: - await ref.read(homeScreenControllerProvider.notifier).createConfigFile(sshnpParams); - break; - case ConfigFileWriteState.update: - log('update_worked'); - await ref.read(homeScreenControllerProvider.notifier).updateConfigFile(sshnpParams: sshnpParams); - // set value to default create so trigger the create functionality on - ref.read(configFileWriteStateProvider.notifier).update((state) => ConfigFileWriteState.create); - break; - } + bool overwrite = + currentProfile.configFileWriteState == ConfigFileWriteState.update; + await config.toFile(overwrite: overwrite); if (context.mounted) { - ref.read(currentNavIndexProvider.notifier).update((state) => AppRoute.home.index - 1); + ref + .read(currentNavIndexProvider.notifier) + .update((state) => AppRoute.home.index - 1); context.pushReplacementNamed(AppRoute.home.name); } } @@ -60,143 +47,155 @@ class _NewConnectionFormState extends ConsumerState { @override Widget build(BuildContext context) { final strings = AppLocalizations.of(context)!; - return SingleChildScrollView( - child: Form( - key: _formkey, - child: Row( - children: [ - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomTextFormField( - initialValue: oldConfig.profileName, - labelText: strings.profileName, - onSaved: (value) => - oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(profileName: value!)), - validator: Validator.validateRequiredField, - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.host, - labelText: strings.host, - onSaved: (value) => oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(host: value!)), - validator: Validator.validateRequiredField, - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.port.toString(), - labelText: strings.port, - onSaved: (value) => - oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(port: int.parse(value!))), - validator: Validator.validateRequiredField, - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.sendSshPublicKey, - labelText: strings.sendSshPublicKey, - onSaved: (value) => - oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(sendSshPublicKey: value!)), - validator: Validator.validateRequiredField, - ), - gapH10, - Row( - children: [ - Text(strings.verbose), - gapW8, - Switch( - value: oldConfig.verbose ?? false, - onChanged: (newValue) { - setState(() { - oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(verbose: newValue)); - }); - }), - ], - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.remoteUsername, - labelText: strings.remoteUserName, - onSaved: (value) => - oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(remoteUsername: value!)), - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.rootDomain, - labelText: strings.rootDomain, - onSaved: (value) => - oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(rootDomain: value!)), - ), - gapH20, - ElevatedButton( - onPressed: createNewConnection, - child: Text(strings.submit), - ), - ]), - gapW12, - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomTextFormField( - initialValue: oldConfig.sshnpdAtSign, - labelText: strings.sshnpdAtSign, - onSaved: (value) => - oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(sshnpdAtSign: value!)), - validator: Validator.validateAtsignField, - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.device, - labelText: strings.device, - onSaved: (value) => oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(device: value!)), - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.localPort.toString(), - labelText: strings.localPort, - onSaved: (value) => - oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(localPort: int.parse(value!))), - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.localSshOptions.join(','), - hintText: strings.localSshOptionsHint, - labelText: strings.localSshOptions, - onSaved: (value) => oldConfig = - SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(localSshOptions: value!.split(','))), - ), - gapH10, - Row( - children: [ - Text(strings.rsa), - gapW8, - Switch( - value: oldConfig.rsa ?? false, - onChanged: (newValue) { - setState(() { - oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(rsa: newValue)); - }); - }), - ], - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.atKeysFilePath, - labelText: strings.atKeysFilePath, - onSaved: (value) => - oldConfig = SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(atKeysFilePath: value!)), - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.localSshdPort.toString(), - labelText: strings.localSshdPort, - onSaved: (value) => oldConfig = - SSHNPPartialParams.merge(oldConfig, SSHNPPartialParams(localSshdPort: int.parse(value!))), - ), - gapH20, - TextButton( - onPressed: () { - ref.read(currentNavIndexProvider.notifier).update((state) => AppRoute.home.index - 1); - context.pushReplacementNamed(AppRoute.home.name); - }, - child: Text(strings.cancel)) - ]), - ], + final oldConfig = + ref.read(paramsFamilyController(currentProfile.profileName)); + return oldConfig.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text(error.toString())), + data: (config) => SingleChildScrollView( + child: Form( + key: _formkey, + child: Row( + children: [ + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + CustomTextFormField( + initialValue: config.profileName, + labelText: strings.profileName, + onSaved: (value) => config = SSHNPParams.merge( + config, SSHNPPartialParams(profileName: value!)), + validator: Validator.validateRequiredField, + ), + gapH10, + CustomTextFormField( + initialValue: config.host, + labelText: strings.host, + onSaved: (value) => config = SSHNPParams.merge( + config, SSHNPPartialParams(host: value!)), + validator: Validator.validateRequiredField, + ), + gapH10, + CustomTextFormField( + initialValue: config.port.toString(), + labelText: strings.port, + onSaved: (value) => config = SSHNPParams.merge( + config, SSHNPPartialParams(port: int.parse(value!))), + validator: Validator.validateRequiredField, + ), + gapH10, + CustomTextFormField( + initialValue: config.sendSshPublicKey, + labelText: strings.sendSshPublicKey, + onSaved: (value) => config = SSHNPParams.merge( + config, SSHNPPartialParams(sendSshPublicKey: value!)), + validator: Validator.validateRequiredField, + ), + gapH10, + Row( + children: [ + Text(strings.verbose), + gapW8, + Switch( + value: config.verbose, + onChanged: (newValue) { + setState(() { + config = SSHNPParams.merge( + config, SSHNPPartialParams(verbose: newValue)); + }); + }), + ], + ), + gapH10, + CustomTextFormField( + initialValue: config.remoteUsername, + labelText: strings.remoteUserName, + onSaved: (value) => config = SSHNPParams.merge( + config, SSHNPPartialParams(remoteUsername: value!)), + ), + gapH10, + CustomTextFormField( + initialValue: config.rootDomain, + labelText: strings.rootDomain, + onSaved: (value) => config = SSHNPParams.merge( + config, SSHNPPartialParams(rootDomain: value!)), + ), + gapH20, + ElevatedButton( + onPressed: () => onSubmit(config), + child: Text(strings.submit), + ), + ]), + gapW12, + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + CustomTextFormField( + initialValue: config.sshnpdAtSign, + labelText: strings.sshnpdAtSign, + onSaved: (value) => config = SSHNPParams.merge( + config, SSHNPPartialParams(sshnpdAtSign: value!)), + validator: Validator.validateAtsignField, + ), + gapH10, + CustomTextFormField( + initialValue: config.device, + labelText: strings.device, + onSaved: (value) => config = SSHNPParams.merge( + config, SSHNPPartialParams(device: value!)), + ), + gapH10, + CustomTextFormField( + initialValue: config.localPort.toString(), + labelText: strings.localPort, + onSaved: (value) => config = SSHNPParams.merge( + config, SSHNPPartialParams(localPort: int.parse(value!))), + ), + gapH10, + CustomTextFormField( + initialValue: config.localSshOptions.join(','), + hintText: strings.localSshOptionsHint, + labelText: strings.localSshOptions, + onSaved: (value) => config = SSHNPParams.merge(config, + SSHNPPartialParams(localSshOptions: value!.split(','))), + ), + gapH10, + Row( + children: [ + Text(strings.rsa), + gapW8, + Switch( + value: config.rsa, + onChanged: (newValue) { + setState(() { + config = SSHNPParams.merge( + config, SSHNPPartialParams(rsa: newValue)); + }); + }), + ], + ), + gapH10, + CustomTextFormField( + initialValue: config.atKeysFilePath, + labelText: strings.atKeysFilePath, + onSaved: (value) => config = SSHNPParams.merge( + config, SSHNPPartialParams(atKeysFilePath: value!)), + ), + gapH10, + CustomTextFormField( + initialValue: config.localSshdPort.toString(), + labelText: strings.localSshdPort, + onSaved: (value) => config = SSHNPParams.merge(config, + SSHNPPartialParams(localSshdPort: int.parse(value!))), + ), + gapH20, + TextButton( + onPressed: () { + ref + .read(currentNavIndexProvider.notifier) + .update((state) => AppRoute.home.index - 1); + context.pushReplacementNamed(AppRoute.home.name); + }, + child: Text(strings.cancel)) + ]), + ], + ), ), ), ); diff --git a/packages/sshnp_gui/macos/Podfile.lock b/packages/sshnp_gui/macos/Podfile.lock index 94f2b8a78..db7c7f42c 100644 --- a/packages/sshnp_gui/macos/Podfile.lock +++ b/packages/sshnp_gui/macos/Podfile.lock @@ -87,4 +87,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.12.1 +COCOAPODS: 1.11.3 diff --git a/packages/sshnp_gui/macos/Runner.xcodeproj/project.pbxproj b/packages/sshnp_gui/macos/Runner.xcodeproj/project.pbxproj index e4ca216eb..ed6209e3e 100644 --- a/packages/sshnp_gui/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/sshnp_gui/macos/Runner.xcodeproj/project.pbxproj @@ -258,7 +258,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/packages/sshnp_gui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/sshnp_gui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ed36e2c19..93c98a106 100644 --- a/packages/sshnp_gui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/sshnp_gui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.0.0 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.10.0" diff --git a/packages/sshnp_gui/pubspec.yaml b/packages/sshnp_gui/pubspec.yaml index 6a5fbdfc3..89d4b4ac7 100644 --- a/packages/sshnp_gui/pubspec.yaml +++ b/packages/sshnp_gui/pubspec.yaml @@ -31,7 +31,6 @@ dependencies: shared_preferences: ^2.2.0 sshnoports: path: ../sshnoports/ - xterm: ^3.5.0 dev_dependencies: From 93c4ead1eda1545f9f263413d4e32c2835e28bef Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 5 Sep 2023 22:36:10 +0800 Subject: [PATCH 05/95] fix: bugs due to previous refactoring --- .../controllers/sshnp_config_controller.dart | 52 ++- .../src/presentation/screens/home_screen.dart | 7 - .../presentation/screens/terminal_screen.dart | 2 +- .../widgets/delete_alert_dialog.dart | 15 +- .../custom_table_cell.dart | 0 .../custom_text_form_field.dart | 6 +- .../home_screen_delete.dart | 0 .../home_screen_edit.dart | 10 +- .../home_screen_run.dart | 3 +- .../home_screen_table_actions.dart | 9 +- .../home_screen_table_header.dart | 2 +- .../home_screen_table_text.dart | 2 +- .../widgets/new_connection_form.dart | 360 ++++++++++-------- 13 files changed, 269 insertions(+), 199 deletions(-) rename packages/sshnp_gui/lib/src/presentation/widgets/{ => home_screen_table}/custom_table_cell.dart (100%) rename packages/sshnp_gui/lib/src/presentation/widgets/{ => home_screen_table}/custom_text_form_field.dart (91%) rename packages/sshnp_gui/lib/src/presentation/widgets/{home_screen_actions => home_screen_table}/home_screen_delete.dart (100%) rename packages/sshnp_gui/lib/src/presentation/widgets/{home_screen_actions => home_screen_table}/home_screen_edit.dart (83%) rename packages/sshnp_gui/lib/src/presentation/widgets/{home_screen_actions => home_screen_table}/home_screen_run.dart (97%) diff --git a/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart b/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart index 4878f0f61..dc9d94c99 100644 --- a/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart @@ -1,19 +1,18 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sshnoports/common/utils.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; /// Controller instance for the current [SSHNPParams] being edited final currentParamsController = AutoDisposeNotifierProvider< - CurrentSSHNPParamsController, CurrentSSHNPParamsModel>( - () => CurrentSSHNPParamsController(), -); + CurrentSSHNPParamsController, + CurrentSSHNPParamsModel>(CurrentSSHNPParamsController.new); /// Controller instance for the list of all profileNames for each config file -final paramsListController = AutoDisposeAsyncNotifierProvider< - SSHNPParamsListController, Iterable>(SSHNPParamsListController.new); +final paramsListController = + AutoDisposeAsyncNotifierProvider>( + SSHNPParamsListController.new); /// Controller instance for the family of [SSHNPParams] controllers final paramsFamilyController = AutoDisposeAsyncNotifierProviderFamily< @@ -41,7 +40,7 @@ class CurrentSSHNPParamsController ); } - void update(CurrentSSHNPParamsModel model) { + void setState(CurrentSSHNPParamsModel model) { state = model; } } @@ -55,18 +54,49 @@ class SSHNPParamsFamilyController ? await SSHNPParams.fromFile(arg) : SSHNPParams.empty(); } + + Future refresh(String arg) async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() => build(arg)); + } + + Future create(SSHNPParams params) async { + print('create'); + await params.toFile(); + state = AsyncValue.data(params); + ref.read(paramsListController.notifier).add(params.profileName!); + } + + Future edit(SSHNPParams params) async { + print('edit'); + await params.toFile(overwrite: true); + state = AsyncValue.data(params); + } + + Future delete() async { + await state.value?.deleteFile(); + state = const AsyncError('File deleted', StackTrace.empty); + ref.read(paramsListController.notifier).remove(state.value!.profileName!); + } } /// Controller for the list of all profileNames for each config file -class SSHNPParamsListController - extends AutoDisposeAsyncNotifier> { +class SSHNPParamsListController extends AutoDisposeAsyncNotifier> { @override - Future> build() { - return SSHNPParams.listFiles(); + Future> build() async { + return (await SSHNPParams.listFiles()).toSet(); } Future refresh() async { state = const AsyncLoading(); state = await AsyncValue.guard(() => build()); } + + void add(String profileName) { + state = AsyncValue.data({...state.value ?? [], profileName}); + } + + void remove(String profileName) { + state = AsyncData(state.value?.difference({profileName}) ?? {}); + } } diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index a9003ec1a..eb7c025fe 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -19,13 +19,6 @@ class HomeScreen extends ConsumerStatefulWidget { } class _HomeScreenState extends ConsumerState { - @override - void initState() { - WidgetsBinding.instance.addPostFrameCallback((_) async { - await ref.read(paramsListController.notifier).refresh(); - }); - super.initState(); - } @override Widget build(BuildContext context) { diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index bc01ec374..e5c6de9ea 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -57,7 +57,7 @@ class _TerminalScreenState extends ConsumerState { // write ssh result command to terminal pty.write(const Utf8Encoder().convert(ref.watch(terminalSSHCommandProvider))); // reset provider - ref.read(terminalSSHCommandProvider.notifier).update((state) => ''); + ref.watch(terminalSSHCommandProvider.notifier).update((state) => ''); } @override diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart index 0c3b0ef8e..4f77b24cd 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart @@ -55,15 +55,12 @@ class DeleteAlertDialog extends ConsumerWidget { .copyWith(decoration: TextDecoration.underline)), ), ElevatedButton( - onPressed: () { - ref - .read(paramsFamilyController(sshnpParams.profileName!)) - .whenData( - (value) async { - await value.deleteFile(); - if (context.mounted) Navigator.of(context).pop(); - }, - ); + onPressed: () async { + await ref + .read(paramsFamilyController(sshnpParams.profileName!) + .notifier) + .delete(); + if (context.mounted) Navigator.of(context).pop(); }, style: Theme.of(context).elevatedButtonTheme.style!.copyWith( backgroundColor: MaterialStateProperty.all(Colors.black), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/custom_table_cell.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/custom_table_cell.dart similarity index 100% rename from packages/sshnp_gui/lib/src/presentation/widgets/custom_table_cell.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/custom_table_cell.dart diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/custom_text_form_field.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/custom_text_form_field.dart similarity index 91% rename from packages/sshnp_gui/lib/src/presentation/widgets/custom_text_form_field.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/custom_text_form_field.dart index 8936b9dc2..87cca806d 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/custom_text_form_field.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/custom_text_form_field.dart @@ -6,7 +6,7 @@ class CustomTextFormField extends StatelessWidget { required this.labelText, this.initialValue, this.validator, - this.onSaved, + this.onChanged, this.hintText, this.width = 192, this.height = 33, @@ -17,7 +17,7 @@ class CustomTextFormField extends StatelessWidget { final String? initialValue; final double width; final double height; - final void Function(String?)? onSaved; + final void Function(String?)? onChanged; final String? Function(String?)? validator; @override @@ -35,7 +35,7 @@ class CustomTextFormField extends StatelessWidget { hintText: hintText, hintStyle: Theme.of(context).textTheme.bodyLarge, ), - onSaved: onSaved, + onChanged: onChanged, validator: validator, ), ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_delete.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_delete.dart similarity index 100% rename from packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_delete.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_delete.dart diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_edit.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_edit.dart similarity index 83% rename from packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_edit.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_edit.dart index 9376e40fb..b152f6bda 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_edit.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_edit.dart @@ -18,18 +18,18 @@ class HomeScreenEditAction extends ConsumerStatefulWidget { } class _HomeScreenEditActionState extends ConsumerState { - void updateConfigFile(SSHNPParams sshnpParams) { + void updateConfigFile(SSHNPParams params) { // Change value to update to trigger the update functionality on the new connection form. - ref.read(currentParamsController.notifier).update( + ref.watch(currentParamsController.notifier).setState( CurrentSSHNPParamsModel( - profileName: sshnpParams.profileName!, + profileName: params.profileName!, configFileWriteState: ConfigFileWriteState.update, ), ); // change value to 1 to update navigation rail selcted icon. ref - .read(currentNavIndexProvider.notifier) - .update((state) => AppRoute.newConnection.index - 1); + .watch(currentNavIndexProvider.notifier) + .update((_) => AppRoute.newConnection.index - 1); context.replaceNamed( AppRoute.newConnection.name, ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_run.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_run.dart similarity index 97% rename from packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_run.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_run.dart index 6f3112aec..c4ed2e470 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_run.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_run.dart @@ -14,8 +14,7 @@ class HomeScreenRunAction extends StatefulWidget { } class _HomeScreenRunActionState extends State { - -Future onPressed(SSHNPParams sshnpParams) async { + Future onPressed(SSHNPParams sshnpParams) async { if (mounted) { showDialog( context: context, diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart index 5b2ad9305..36a525f77 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/presentation/widgets/custom_table_cell.dart'; -import 'package:sshnp_gui/src/presentation/widgets/delete_alert_dialog.dart'; -import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_delete.dart'; -import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_edit.dart'; -import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_run.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/custom_table_cell.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_delete.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_edit.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_run.dart'; class HomeScreenTableActions extends StatelessWidget { final AsyncValue params; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_header.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_header.dart index a355c6600..85992447e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_header.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_header.dart @@ -1,6 +1,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter/material.dart'; -import 'package:sshnp_gui/src/presentation/widgets/custom_table_cell.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/custom_table_cell.dart'; TableRow getHomeScreenTableHeader(AppLocalizations strings) => TableRow( decoration: const BoxDecoration( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_text.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_text.dart index a25e3bdb6..933b72286 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_text.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_text.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/presentation/widgets/custom_table_cell.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/custom_table_cell.dart'; class HomeScreenTableProfileNameText extends StatelessWidget { final AsyncValue params; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart index 637f5f404..53e75babf 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart @@ -1,3 +1,4 @@ +import 'package:at_client_mobile/at_client_mobile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -11,7 +12,7 @@ import 'package:sshnp_gui/src/utils/enum.dart'; import 'package:sshnp_gui/src/utils/validator.dart'; import '../../utils/sizes.dart'; -import 'custom_text_form_field.dart'; +import 'home_screen_table/custom_text_form_field.dart'; class NewConnectionForm extends ConsumerStatefulWidget { const NewConnectionForm({super.key}); @@ -23,18 +24,39 @@ class NewConnectionForm extends ConsumerStatefulWidget { class _NewConnectionFormState extends ConsumerState { final GlobalKey _formkey = GlobalKey(); late CurrentSSHNPParamsModel currentProfile; + SSHNPPartialParams newConfig = SSHNPPartialParams.empty(); @override void initState() { - currentProfile = ref.read(currentParamsController); super.initState(); } - void onSubmit(config) async { + void onSubmit(SSHNPParams oldConfig, SSHNPPartialParams newConfig) async { if (_formkey.currentState!.validate()) { _formkey.currentState!.save(); + final controller = ref.read(paramsFamilyController( + newConfig.profileName ?? oldConfig.profileName!) + .notifier); bool overwrite = currentProfile.configFileWriteState == ConfigFileWriteState.update; - await config.toFile(overwrite: overwrite); + bool rename = newConfig.profileName.isNotNull && + newConfig.profileName!.isNotEmpty && + oldConfig.profileName.isNotNull && + oldConfig.profileName!.isNotEmpty && + newConfig.profileName != oldConfig.profileName; + SSHNPParams config = SSHNPParams.merge(oldConfig, newConfig); + if (rename) { + // delete old config file and write the new one + await ref + .read(paramsFamilyController(oldConfig.profileName!).notifier) + .delete(); + await controller.create(config); + } else if (overwrite) { + // overwrite the existing file + await controller.edit(config); + } else { + // create new config file + await controller.create(config); + } if (context.mounted) { ref .read(currentNavIndexProvider.notifier) @@ -47,157 +69,187 @@ class _NewConnectionFormState extends ConsumerState { @override Widget build(BuildContext context) { final strings = AppLocalizations.of(context)!; + currentProfile = ref.watch(currentParamsController); + final oldConfig = - ref.read(paramsFamilyController(currentProfile.profileName)); + ref.watch(paramsFamilyController(currentProfile.profileName)); + final configController = + ref.watch(paramsFamilyController(currentProfile.profileName).notifier); return oldConfig.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) => Center(child: Text(error.toString())), - data: (config) => SingleChildScrollView( - child: Form( - key: _formkey, - child: Row( - children: [ - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomTextFormField( - initialValue: config.profileName, - labelText: strings.profileName, - onSaved: (value) => config = SSHNPParams.merge( - config, SSHNPPartialParams(profileName: value!)), - validator: Validator.validateRequiredField, - ), - gapH10, - CustomTextFormField( - initialValue: config.host, - labelText: strings.host, - onSaved: (value) => config = SSHNPParams.merge( - config, SSHNPPartialParams(host: value!)), - validator: Validator.validateRequiredField, - ), - gapH10, - CustomTextFormField( - initialValue: config.port.toString(), - labelText: strings.port, - onSaved: (value) => config = SSHNPParams.merge( - config, SSHNPPartialParams(port: int.parse(value!))), - validator: Validator.validateRequiredField, - ), - gapH10, - CustomTextFormField( - initialValue: config.sendSshPublicKey, - labelText: strings.sendSshPublicKey, - onSaved: (value) => config = SSHNPParams.merge( - config, SSHNPPartialParams(sendSshPublicKey: value!)), - validator: Validator.validateRequiredField, - ), - gapH10, - Row( - children: [ - Text(strings.verbose), - gapW8, - Switch( - value: config.verbose, - onChanged: (newValue) { - setState(() { - config = SSHNPParams.merge( - config, SSHNPPartialParams(verbose: newValue)); - }); - }), - ], - ), - gapH10, - CustomTextFormField( - initialValue: config.remoteUsername, - labelText: strings.remoteUserName, - onSaved: (value) => config = SSHNPParams.merge( - config, SSHNPPartialParams(remoteUsername: value!)), - ), - gapH10, - CustomTextFormField( - initialValue: config.rootDomain, - labelText: strings.rootDomain, - onSaved: (value) => config = SSHNPParams.merge( - config, SSHNPPartialParams(rootDomain: value!)), - ), - gapH20, - ElevatedButton( - onPressed: () => onSubmit(config), - child: Text(strings.submit), - ), - ]), - gapW12, - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomTextFormField( - initialValue: config.sshnpdAtSign, - labelText: strings.sshnpdAtSign, - onSaved: (value) => config = SSHNPParams.merge( - config, SSHNPPartialParams(sshnpdAtSign: value!)), - validator: Validator.validateAtsignField, - ), - gapH10, - CustomTextFormField( - initialValue: config.device, - labelText: strings.device, - onSaved: (value) => config = SSHNPParams.merge( - config, SSHNPPartialParams(device: value!)), - ), - gapH10, - CustomTextFormField( - initialValue: config.localPort.toString(), - labelText: strings.localPort, - onSaved: (value) => config = SSHNPParams.merge( - config, SSHNPPartialParams(localPort: int.parse(value!))), - ), - gapH10, - CustomTextFormField( - initialValue: config.localSshOptions.join(','), - hintText: strings.localSshOptionsHint, - labelText: strings.localSshOptions, - onSaved: (value) => config = SSHNPParams.merge(config, - SSHNPPartialParams(localSshOptions: value!.split(','))), - ), - gapH10, - Row( - children: [ - Text(strings.rsa), - gapW8, - Switch( - value: config.rsa, - onChanged: (newValue) { - setState(() { - config = SSHNPParams.merge( - config, SSHNPPartialParams(rsa: newValue)); - }); - }), - ], - ), - gapH10, - CustomTextFormField( - initialValue: config.atKeysFilePath, - labelText: strings.atKeysFilePath, - onSaved: (value) => config = SSHNPParams.merge( - config, SSHNPPartialParams(atKeysFilePath: value!)), - ), - gapH10, - CustomTextFormField( - initialValue: config.localSshdPort.toString(), - labelText: strings.localSshdPort, - onSaved: (value) => config = SSHNPParams.merge(config, - SSHNPPartialParams(localSshdPort: int.parse(value!))), - ), - gapH20, - TextButton( - onPressed: () { - ref - .read(currentNavIndexProvider.notifier) - .update((state) => AppRoute.home.index - 1); - context.pushReplacementNamed(AppRoute.home.name); - }, - child: Text(strings.cancel)) - ]), - ], - ), - ), - ), - ); + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text(error.toString())), + data: (config) { + return SingleChildScrollView( + child: Form( + key: _formkey, + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTextFormField( + initialValue: config.profileName, + labelText: strings.profileName, + onChanged: (value) { + newConfig = SSHNPPartialParams.merge(newConfig, + SSHNPPartialParams(profileName: value!)); + }, + validator: Validator.validateRequiredField, + ), + gapH10, + CustomTextFormField( + initialValue: config.host, + labelText: strings.host, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge( + newConfig, SSHNPPartialParams(host: value!)), + validator: Validator.validateRequiredField, + ), + gapH10, + CustomTextFormField( + initialValue: config.port.toString(), + labelText: strings.port, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge(newConfig, + SSHNPPartialParams(port: int.parse(value!))), + validator: Validator.validateRequiredField, + ), + gapH10, + CustomTextFormField( + initialValue: config.sendSshPublicKey, + labelText: strings.sendSshPublicKey, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge(newConfig, + SSHNPPartialParams(sendSshPublicKey: value!)), + validator: Validator.validateRequiredField, + ), + gapH10, + Row( + children: [ + Text(strings.verbose), + gapW8, + Switch( + value: config.verbose, + onChanged: (newValue) { + setState(() { + newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(verbose: newValue)); + }); + }), + ], + ), + gapH10, + CustomTextFormField( + initialValue: config.remoteUsername, + labelText: strings.remoteUserName, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge(newConfig, + SSHNPPartialParams(remoteUsername: value!)), + ), + gapH10, + CustomTextFormField( + initialValue: config.rootDomain, + labelText: strings.rootDomain, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge(newConfig, + SSHNPPartialParams(rootDomain: value!)), + ), + gapH20, + ElevatedButton( + onPressed: () => onSubmit(config, newConfig), + child: Text(strings.submit), + ), + ]), + gapW12, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTextFormField( + initialValue: config.sshnpdAtSign, + labelText: strings.sshnpdAtSign, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge(newConfig, + SSHNPPartialParams(sshnpdAtSign: value!)), + validator: Validator.validateAtsignField, + ), + gapH10, + CustomTextFormField( + initialValue: config.device, + labelText: strings.device, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge(newConfig, + SSHNPPartialParams(device: value!)), + ), + gapH10, + CustomTextFormField( + initialValue: config.localPort.toString(), + labelText: strings.localPort, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams( + localPort: int.parse(value!))), + ), + gapH10, + CustomTextFormField( + initialValue: config.localSshOptions.join(','), + hintText: strings.localSshOptionsHint, + labelText: strings.localSshOptions, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams( + localSshOptions: value!.split(','))), + ), + gapH10, + Row( + children: [ + Text(strings.rsa), + gapW8, + Switch( + value: config.rsa, + onChanged: (newValue) { + setState(() { + newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(rsa: newValue)); + }); + }), + ], + ), + gapH10, + CustomTextFormField( + initialValue: config.atKeysFilePath, + labelText: strings.atKeysFilePath, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge(newConfig, + SSHNPPartialParams(atKeysFilePath: value!)), + ), + gapH10, + CustomTextFormField( + initialValue: config.localSshdPort.toString(), + labelText: strings.localSshdPort, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams( + localSshdPort: int.parse(value!))), + ), + gapH20, + TextButton( + onPressed: () { + ref + .read(currentNavIndexProvider.notifier) + .update((state) => AppRoute.home.index - 1); + context.pushReplacementNamed(AppRoute.home.name); + }, + child: Text(strings.cancel)) + ]), + ], + ), + ), + ); + }); } } From 5ade3c7a3b834efa3ccbe2705b051203ba721044 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 5 Sep 2023 22:37:22 +0800 Subject: [PATCH 06/95] chore: cleanup naming --- .../widgets/new_connection_form.dart | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart index 53e75babf..62c9e91d0 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart @@ -71,14 +71,12 @@ class _NewConnectionFormState extends ConsumerState { final strings = AppLocalizations.of(context)!; currentProfile = ref.watch(currentParamsController); - final oldConfig = + final asyncOldConfig = ref.watch(paramsFamilyController(currentProfile.profileName)); - final configController = - ref.watch(paramsFamilyController(currentProfile.profileName).notifier); - return oldConfig.when( + return asyncOldConfig.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text(error.toString())), - data: (config) { + data: (oldConfig) { return SingleChildScrollView( child: Form( key: _formkey, @@ -88,7 +86,7 @@ class _NewConnectionFormState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTextFormField( - initialValue: config.profileName, + initialValue: oldConfig.profileName, labelText: strings.profileName, onChanged: (value) { newConfig = SSHNPPartialParams.merge(newConfig, @@ -98,7 +96,7 @@ class _NewConnectionFormState extends ConsumerState { ), gapH10, CustomTextFormField( - initialValue: config.host, + initialValue: oldConfig.host, labelText: strings.host, onChanged: (value) => newConfig = SSHNPPartialParams.merge( @@ -107,7 +105,7 @@ class _NewConnectionFormState extends ConsumerState { ), gapH10, CustomTextFormField( - initialValue: config.port.toString(), + initialValue: oldConfig.port.toString(), labelText: strings.port, onChanged: (value) => newConfig = SSHNPPartialParams.merge(newConfig, @@ -116,7 +114,7 @@ class _NewConnectionFormState extends ConsumerState { ), gapH10, CustomTextFormField( - initialValue: config.sendSshPublicKey, + initialValue: oldConfig.sendSshPublicKey, labelText: strings.sendSshPublicKey, onChanged: (value) => newConfig = SSHNPPartialParams.merge(newConfig, @@ -129,7 +127,7 @@ class _NewConnectionFormState extends ConsumerState { Text(strings.verbose), gapW8, Switch( - value: config.verbose, + value: oldConfig.verbose, onChanged: (newValue) { setState(() { newConfig = SSHNPPartialParams.merge( @@ -141,7 +139,7 @@ class _NewConnectionFormState extends ConsumerState { ), gapH10, CustomTextFormField( - initialValue: config.remoteUsername, + initialValue: oldConfig.remoteUsername, labelText: strings.remoteUserName, onChanged: (value) => newConfig = SSHNPPartialParams.merge(newConfig, @@ -149,7 +147,7 @@ class _NewConnectionFormState extends ConsumerState { ), gapH10, CustomTextFormField( - initialValue: config.rootDomain, + initialValue: oldConfig.rootDomain, labelText: strings.rootDomain, onChanged: (value) => newConfig = SSHNPPartialParams.merge(newConfig, @@ -157,7 +155,7 @@ class _NewConnectionFormState extends ConsumerState { ), gapH20, ElevatedButton( - onPressed: () => onSubmit(config, newConfig), + onPressed: () => onSubmit(oldConfig, newConfig), child: Text(strings.submit), ), ]), @@ -166,7 +164,7 @@ class _NewConnectionFormState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTextFormField( - initialValue: config.sshnpdAtSign, + initialValue: oldConfig.sshnpdAtSign, labelText: strings.sshnpdAtSign, onChanged: (value) => newConfig = SSHNPPartialParams.merge(newConfig, @@ -175,7 +173,7 @@ class _NewConnectionFormState extends ConsumerState { ), gapH10, CustomTextFormField( - initialValue: config.device, + initialValue: oldConfig.device, labelText: strings.device, onChanged: (value) => newConfig = SSHNPPartialParams.merge(newConfig, @@ -183,7 +181,7 @@ class _NewConnectionFormState extends ConsumerState { ), gapH10, CustomTextFormField( - initialValue: config.localPort.toString(), + initialValue: oldConfig.localPort.toString(), labelText: strings.localPort, onChanged: (value) => newConfig = SSHNPPartialParams.merge( @@ -193,7 +191,7 @@ class _NewConnectionFormState extends ConsumerState { ), gapH10, CustomTextFormField( - initialValue: config.localSshOptions.join(','), + initialValue: oldConfig.localSshOptions.join(','), hintText: strings.localSshOptionsHint, labelText: strings.localSshOptions, onChanged: (value) => newConfig = @@ -208,7 +206,7 @@ class _NewConnectionFormState extends ConsumerState { Text(strings.rsa), gapW8, Switch( - value: config.rsa, + value: oldConfig.rsa, onChanged: (newValue) { setState(() { newConfig = SSHNPPartialParams.merge( @@ -220,7 +218,7 @@ class _NewConnectionFormState extends ConsumerState { ), gapH10, CustomTextFormField( - initialValue: config.atKeysFilePath, + initialValue: oldConfig.atKeysFilePath, labelText: strings.atKeysFilePath, onChanged: (value) => newConfig = SSHNPPartialParams.merge(newConfig, @@ -228,7 +226,7 @@ class _NewConnectionFormState extends ConsumerState { ), gapH10, CustomTextFormField( - initialValue: config.localSshdPort.toString(), + initialValue: oldConfig.localSshdPort.toString(), labelText: strings.localSshdPort, onChanged: (value) => newConfig = SSHNPPartialParams.merge( From 28ebc1469bd4b2c96013ba7f7d9448a7231c35e7 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 5 Sep 2023 22:39:59 +0800 Subject: [PATCH 07/95] chore: make sshnp partial params all final again --- .../sshnoports/lib/sshnp/sshnp_params.dart | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index dddea16f9..c6a176a85 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -258,26 +258,26 @@ class SSHNPParams { /// e.g. default values from a config file and the rest from the command line class SSHNPPartialParams { /// Main Params - String? profileName; - String? clientAtSign; - String? sshnpdAtSign; - String? host; - String? device; - int? port; - int? localPort; - int? localSshdPort; - String? atKeysFilePath; - String? sendSshPublicKey; - List localSshOptions; - bool? rsa; - String? remoteUsername; - bool? verbose; - String? rootDomain; - bool? legacyDaemon; + final String? profileName; + final String? clientAtSign; + final String? sshnpdAtSign; + final String? host; + final String? device; + final int? port; + final int? localPort; + final int? localSshdPort; + final String? atKeysFilePath; + final String? sendSshPublicKey; + final List localSshOptions; + final bool? rsa; + final String? remoteUsername; + final bool? verbose; + final String? rootDomain; + final bool? legacyDaemon; /// Special Params // N.B. config file is a meta param and doesn't need to be included - bool? listDevices; + final bool? listDevices; // Non param variables static final ArgParser parser = _createArgParser(); From b14259f9b7142a5f516d22a571527339a1c208ea Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 11:58:06 +0800 Subject: [PATCH 08/95] chore: reorganize widgets folder --- .../sshnoports/lib/sshnp/sshnp_params.dart | 39 +++++++------------ .../controllers/sshnp_config_controller.dart | 31 +++++++-------- .../src/presentation/screens/home_screen.dart | 2 +- .../screens/new_connection_screen.dart | 2 +- .../presentation/screens/settings_screen.dart | 8 ++-- .../presentation/screens/terminal_screen.dart | 2 +- .../{ => dialog}/delete_alert_dialog.dart | 2 +- .../sshnp_result_alert_dialog.dart | 4 +- .../home_screen_table/home_screen_delete.dart | 2 +- .../home_screen_table/home_screen_run.dart | 2 +- .../{ => navigation}/app_navigation_rail.dart | 4 +- .../custom_snack_bar.dart} | 30 ++++++-------- .../{ => utility}/reset_app_button.dart | 6 +-- .../{ => utility}/settings_button.dart | 2 +- .../widgets/{ => utility}/switch_atsign.dart | 10 ++--- .../repository/authentication_repository.dart | 4 +- packages/sshnp_gui/pubspec.yaml | 1 + 17 files changed, 64 insertions(+), 87 deletions(-) rename packages/sshnp_gui/lib/src/presentation/widgets/{ => dialog}/delete_alert_dialog.dart (98%) rename packages/sshnp_gui/lib/src/presentation/widgets/{ => dialog}/sshnp_result_alert_dialog.dart (97%) rename packages/sshnp_gui/lib/src/presentation/widgets/{ => navigation}/app_navigation_rail.dart (96%) rename packages/sshnp_gui/lib/src/presentation/widgets/{snackbars.dart => utility/custom_snack_bar.dart} (51%) rename packages/sshnp_gui/lib/src/presentation/widgets/{ => utility}/reset_app_button.dart (98%) rename packages/sshnp_gui/lib/src/presentation/widgets/{ => utility}/settings_button.dart (96%) rename packages/sshnp_gui/lib/src/presentation/widgets/{ => utility}/switch_atsign.dart (94%) diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index c6a176a85..1b15ea9a1 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -27,8 +27,7 @@ class SSHNPParams { final bool legacyDaemon; /// Special Arguments - late final String? - profileName; // automatically populated with the filename if from a configFile + late final String? profileName; // automatically populated with the filename if from a configFile late final bool listDevices; SSHNPParams({ @@ -58,12 +57,10 @@ class SSHNPParams { // Use default atKeysFilePath if not provided - this.atKeysFilePath = - atKeysFilePath ?? getDefaultAtKeysFilePath(homeDirectory, clientAtSign); + this.atKeysFilePath = atKeysFilePath ?? getDefaultAtKeysFilePath(homeDirectory, clientAtSign); } - factory SSHNPParams.merge(SSHNPParams params1, - [SSHNPPartialParams? params2]) { + factory SSHNPParams.merge(SSHNPParams params1, [SSHNPPartialParams? params2]) { params2 ??= SSHNPPartialParams.empty(); return SSHNPParams( profileName: params2.profileName ?? params1.profileName, @@ -111,8 +108,7 @@ class SSHNPParams { device: partial.device ?? SSHNP.defaultDevice, port: partial.port ?? SSHNP.defaultPort, localPort: partial.localPort ?? SSHNP.defaultLocalPort, - sendSshPublicKey: - partial.sendSshPublicKey ?? SSHNP.defaultSendSshPublicKey, + sendSshPublicKey: partial.sendSshPublicKey ?? SSHNP.defaultSendSshPublicKey, localSshOptions: partial.localSshOptions, rsa: partial.rsa ?? SSHNP.defaultRsa, verbose: partial.verbose ?? SSHNP.defaultRsa, @@ -152,8 +148,7 @@ class SSHNPParams { return fileNames; } - static Future fromFile(String profileName, - [String? directory]) async { + static Future fromFile(String profileName, [String? directory]) async { var homeDirectory = getHomeDirectory(throwIfNull: true)!; directory ??= getDefaultSshnpConfigDirectory(homeDirectory); var fileName = path.join( @@ -188,8 +183,7 @@ class SSHNPParams { var exists = await file.exists(); if (exists && !overwrite) { - throw Exception( - 'Failed to write config file: ${file.path} already exists'); + throw Exception('Failed to write config file: ${file.path} already exists'); } // FileMode.write will create the file if it does not exist @@ -309,8 +303,7 @@ class SSHNPPartialParams { /// Merge two SSHNPPartialParams objects together /// Params in params2 take precedence over params1 /// - localSshOptions are concatenated together as (params1 + params2) - factory SSHNPPartialParams.merge(SSHNPPartialParams params1, - [SSHNPPartialParams? params2]) { + factory SSHNPPartialParams.merge(SSHNPPartialParams params1, [SSHNPPartialParams? params2]) { params2 ??= SSHNPPartialParams.empty(); return SSHNPPartialParams( profileName: params2.profileName ?? params1.profileName, @@ -344,8 +337,7 @@ class SSHNPPartialParams { localPort: args['local-port'], atKeysFilePath: args['key-file'], sendSshPublicKey: args['ssh-public-key'], - localSshOptions: - args['local-ssh-options'] ?? SSHNP.defaultLocalSshOptions, + localSshOptions: args['local-ssh-options'] ?? SSHNP.defaultLocalSshOptions, rsa: args['rsa'], remoteUsername: args['remote-user-name'], verbose: args['verbose'], @@ -359,6 +351,7 @@ class SSHNPPartialParams { factory SSHNPPartialParams.fromConfig(String fileName) { var args = _parseConfigFile(fileName); args['profile-name'] = _fileToProfileName(fileName); + print('profile-name: ${args['profile-name']}'); return SSHNPPartialParams.fromArgMap(args); } @@ -378,9 +371,7 @@ class SSHNPPartialParams { } // THIS IS A WORKAROUND IN ORDER TO BE TYPE SAFE IN SSHNPPartialParams.fromArgMap - Map parsedArgsMap = { - for (var e in (parsedArgs.options)) e: parsedArgs[e] - }; + Map parsedArgsMap = {for (var e in (parsedArgs.options)) e: parsedArgs[e]}; return SSHNPPartialParams.merge( params, @@ -402,9 +393,7 @@ class SSHNPPartialParams { arg.name, abbr: arg.abbr, mandatory: arg.mandatory, - defaultsTo: withDefaults - ? (arg.defaultsTo != null ? '${arg.defaultsTo}' : null) - : null, + defaultsTo: withDefaults ? (arg.defaultsTo != null ? '${arg.defaultsTo}' : null) : null, help: arg.help, ); break; @@ -429,8 +418,7 @@ class SSHNPPartialParams { if (withConfig) { parser.addOption( 'config-file', - help: - 'Read args from a config file\nMandatory args are not required if already supplied in the config file', + help: 'Read args from a config file\nMandatory args are not required if already supplied in the config file', ); } if (withListDevices) { @@ -500,5 +488,4 @@ class SSHNPPartialParams { } } -String _fileToProfileName(String fileName) => - path.basenameWithoutExtension(fileName).replaceAll('_', ' '); +String _fileToProfileName(String fileName) => path.basenameWithoutExtension(fileName).replaceAll('_', ' '); diff --git a/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart b/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart index dc9d94c99..70afef729 100644 --- a/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart @@ -1,37 +1,32 @@ import 'dart:async'; +import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; /// Controller instance for the current [SSHNPParams] being edited -final currentParamsController = AutoDisposeNotifierProvider< - CurrentSSHNPParamsController, - CurrentSSHNPParamsModel>(CurrentSSHNPParamsController.new); +final currentParamsController = AutoDisposeNotifierProvider( + CurrentSSHNPParamsController.new); /// Controller instance for the list of all profileNames for each config file final paramsListController = - AutoDisposeAsyncNotifierProvider>( - SSHNPParamsListController.new); + AutoDisposeAsyncNotifierProvider>(SSHNPParamsListController.new); /// Controller instance for the family of [SSHNPParams] controllers -final paramsFamilyController = AutoDisposeAsyncNotifierProviderFamily< - SSHNPParamsFamilyController, - SSHNPParams, - String>(SSHNPParamsFamilyController.new); +final paramsFamilyController = AutoDisposeAsyncNotifierProviderFamily( + SSHNPParamsFamilyController.new); /// Holder model for the current [SSHNPParams] being edited class CurrentSSHNPParamsModel { final String profileName; final ConfigFileWriteState configFileWriteState; - CurrentSSHNPParamsModel( - {required this.profileName, required this.configFileWriteState}); + CurrentSSHNPParamsModel({required this.profileName, required this.configFileWriteState}); } /// Controller for the current [SSHNPParams] being edited -class CurrentSSHNPParamsController - extends AutoDisposeNotifier { +class CurrentSSHNPParamsController extends AutoDisposeNotifier { @override CurrentSSHNPParamsModel build() { return CurrentSSHNPParamsModel( @@ -46,13 +41,15 @@ class CurrentSSHNPParamsController } /// Controller for the family of [SSHNPParams] controllers -class SSHNPParamsFamilyController - extends AutoDisposeFamilyAsyncNotifier { +class SSHNPParamsFamilyController extends AutoDisposeFamilyAsyncNotifier { @override Future build(String arg) async { return (await SSHNPParams.fileExists(arg)) ? await SSHNPParams.fromFile(arg) - : SSHNPParams.empty(); + : SSHNPParams.merge( + SSHNPParams.empty(), + SSHNPPartialParams(clientAtSign: AtClientManager.getInstance().atClient.getCurrentAtSign()!), + ); } Future refresh(String arg) async { @@ -61,14 +58,12 @@ class SSHNPParamsFamilyController } Future create(SSHNPParams params) async { - print('create'); await params.toFile(); state = AsyncValue.data(params); ref.read(paramsListController.notifier).add(params.profileName!); } Future edit(SSHNPParams params) async { - print('edit'); await params.toFile(overwrite: true); state = AsyncValue.data(params); } diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index eb7c025fe..87832f5e3 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -8,7 +8,7 @@ import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_table_text.dart'; import '../../utils/sizes.dart'; -import '../widgets/app_navigation_rail.dart'; +import '../widgets/navigation/app_navigation_rail.dart'; // * Once the onboarding process is completed you will be taken to this screen class HomeScreen extends ConsumerStatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart index 2058e921c..e5ec1bcec 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:sshnp_gui/src/presentation/widgets/new_connection_form.dart'; import '../../utils/sizes.dart'; -import '../widgets/app_navigation_rail.dart'; +import '../widgets/navigation/app_navigation_rail.dart'; // * Once the onboarding process is completed you will be taken to this screen class NewConnectionScreen extends StatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart index b5ac56a7d..bf5cc25f0 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart @@ -2,14 +2,14 @@ import 'package:at_backupkey_flutter/at_backupkey_flutter.dart'; import 'package:at_contacts_flutter/services/contact_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:sshnp_gui/src/presentation/widgets/app_navigation_rail.dart'; -import 'package:sshnp_gui/src/presentation/widgets/reset_app_button.dart'; +import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/presentation/widgets/utility/reset_app_button.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../repository/navigation_service.dart'; import '../../utils/sizes.dart'; -import '../widgets/settings_button.dart'; -import '../widgets/switch_atsign.dart'; +import '../widgets/utility/settings_button.dart'; +import '../widgets/utility/switch_atsign.dart'; class SettingsScreen extends StatelessWidget { const SettingsScreen({Key? key}) : super(key: key); diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index e5c6de9ea..e66b269df 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -9,7 +9,7 @@ import 'package:sshnp_gui/src/controllers/minor_providers.dart'; import 'package:xterm/xterm.dart'; import '../../utils/sizes.dart'; -import '../widgets/app_navigation_rail.dart'; +import '../widgets/navigation/app_navigation_rail.dart'; // * Once the onboarding process is completed you will be taken to this screen class TerminalScreen extends ConsumerStatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart similarity index 98% rename from packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart index 4f77b24cd..97e8ce017 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/delete_alert_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; -import '../../utils/sizes.dart'; +import '../../../utils/sizes.dart'; class DeleteAlertDialog extends ConsumerWidget { const DeleteAlertDialog({required this.sshnpParams, super.key}); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/sshnp_result_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart similarity index 97% rename from packages/sshnp_gui/lib/src/presentation/widgets/sshnp_result_alert_dialog.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart index 6d8a4e509..f62814448 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/sshnp_result_alert_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart @@ -4,8 +4,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import '../../controllers/minor_providers.dart'; -import '../../utils/app_router.dart'; +import '../../../controllers/minor_providers.dart'; +import '../../../utils/app_router.dart'; class SSHNPResultAlertDialog extends ConsumerWidget { const SSHNPResultAlertDialog({required this.result, required this.title, super.key}); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_delete.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_delete.dart index af70a0dde..1d1c5746f 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_delete.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_delete.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/presentation/widgets/delete_alert_dialog.dart'; +import 'package:sshnp_gui/src/presentation/widgets/dialog/delete_alert_dialog.dart'; class HomeScreenDeleteAction extends StatelessWidget { final SSHNPParams params; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_run.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_run.dart index c4ed2e470..74622a3a8 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_run.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_run.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; -import 'package:sshnp_gui/src/presentation/widgets/sshnp_result_alert_dialog.dart'; +import 'package:sshnp_gui/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart'; class HomeScreenRunAction extends StatefulWidget { final SSHNPParams params; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/app_navigation_rail.dart b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart similarity index 96% rename from packages/sshnp_gui/lib/src/presentation/widgets/app_navigation_rail.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart index 69a6d517e..34d47d764 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/app_navigation_rail.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart @@ -3,8 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import '../../controllers/minor_providers.dart'; -import '../../utils/app_router.dart'; +import '../../../controllers/minor_providers.dart'; +import '../../../utils/app_router.dart'; class AppNavigationRail extends ConsumerWidget { const AppNavigationRail({super.key}); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/snackbars.dart b/packages/sshnp_gui/lib/src/presentation/widgets/utility/custom_snack_bar.dart similarity index 51% rename from packages/sshnp_gui/lib/src/presentation/widgets/snackbars.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/utility/custom_snack_bar.dart index b7f1791d2..a387f02c1 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/snackbars.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/utility/custom_snack_bar.dart @@ -1,27 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:sshnp_gui/src/repository/navigation_service.dart'; -import '../../repository/navigation_service.dart'; - -final _context = NavigationService.navKey.currentContext!; - -class SnackBars extends StatelessWidget { - const SnackBars({Key? key}) : super(key: key); - static void errorSnackBar({ +class CustomSnackBar { + static void error({ required String content, }) { - ScaffoldMessenger.of(_context).showSnackBar(SnackBar( + final context = NavigationService.navKey.currentContext!; + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( content, textAlign: TextAlign.center, ), - backgroundColor: Theme.of(_context).colorScheme.error, + backgroundColor: Theme.of(context).colorScheme.error, )); } - static void successSnackBar({ + static void success({ required String content, }) { - ScaffoldMessenger.of(_context).showSnackBar(SnackBar( + final context = NavigationService.navKey.currentContext!; + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( content, textAlign: TextAlign.center, @@ -30,12 +28,13 @@ class SnackBars extends StatelessWidget { )); } - static void notificationSnackBar({ + static void notification({ required String content, SnackBarAction? action, Duration duration = const Duration(seconds: 2), }) { - ScaffoldMessenger.of(_context).showSnackBar(SnackBar( + final context = NavigationService.navKey.currentContext!; + ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( content, textAlign: TextAlign.center, @@ -45,9 +44,4 @@ class SnackBars extends StatelessWidget { // backgroundColor: kDataStorageColor, )); } - - @override - Widget build(BuildContext context) { - return Container(); - } } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/reset_app_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/utility/reset_app_button.dart similarity index 98% rename from packages/sshnp_gui/lib/src/presentation/widgets/reset_app_button.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/utility/reset_app_button.dart index e072c8760..84f65ad72 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/reset_app_button.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/utility/reset_app_button.dart @@ -2,9 +2,9 @@ import 'package:at_onboarding_flutter/services/sdk_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../../../main.dart'; -import '../../utils/at_error_dialog.dart'; -import '../../utils/sizes.dart'; +import '../../../../main.dart'; +import '../../../utils/at_error_dialog.dart'; +import '../../../utils/sizes.dart'; import 'settings_button.dart'; /// Custom reset button widget is to reset an atsign from keychain list, diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/settings_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/utility/settings_button.dart similarity index 96% rename from packages/sshnp_gui/lib/src/presentation/widgets/settings_button.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/utility/settings_button.dart index 4300bc0e9..7fc375962 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/settings_button.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/utility/settings_button.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:sshnp_gui/src/utils/constants.dart'; -import '../../utils/sizes.dart'; +import '../../../utils/sizes.dart'; class SettingsButton extends StatelessWidget { const SettingsButton({ diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/switch_atsign.dart b/packages/sshnp_gui/lib/src/presentation/widgets/utility/switch_atsign.dart similarity index 94% rename from packages/sshnp_gui/lib/src/presentation/widgets/switch_atsign.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/utility/switch_atsign.dart index a61eb0237..fd5f1c170 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/switch_atsign.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/utility/switch_atsign.dart @@ -6,15 +6,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../controllers/authentication_controller.dart'; -import '../../repository/authentication_repository.dart'; -import 'snackbars.dart'; +import '../../../controllers/authentication_controller.dart'; +import '../../../repository/authentication_repository.dart'; +import 'custom_snack_bar.dart'; class AtSignBottomSheet extends ConsumerStatefulWidget { const AtSignBottomSheet({Key? key}) : super(key: key); @override - _AtSignBottomSheetState createState() => _AtSignBottomSheetState(); + ConsumerState createState() => _AtSignBottomSheetState(); } class _AtSignBottomSheetState extends ConsumerState { @@ -91,7 +91,7 @@ class _AtSignBottomSheetState extends ConsumerState { } else if (!snapshot.hasData) { return const CircularProgressIndicator(); } else { - SnackBars.errorSnackBar(content: strings.error); + CustomSnackBar.error(content: strings.error); return const SizedBox(); } })); diff --git a/packages/sshnp_gui/lib/src/repository/authentication_repository.dart b/packages/sshnp_gui/lib/src/repository/authentication_repository.dart index 43cee100b..a3b3b73aa 100644 --- a/packages/sshnp_gui/lib/src/repository/authentication_repository.dart +++ b/packages/sshnp_gui/lib/src/repository/authentication_repository.dart @@ -13,7 +13,7 @@ import 'package:go_router/go_router.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../presentation/widgets/snackbars.dart'; +import '../presentation/widgets/utility/custom_snack_bar.dart'; // import '../utils/my_sync_progress_listener.dart'; import '../utils/app_router.dart'; import 'navigation_service.dart'; @@ -94,7 +94,7 @@ class AuthenticationRepository { case AtOnboardingResultStatus.error: _logger.severe('Onboarding throws ${result.message} error'); - SnackBars.errorSnackBar(content: result.message ?? ''); + CustomSnackBar.error(content: result.message ?? ''); break; case AtOnboardingResultStatus.cancel: diff --git a/packages/sshnp_gui/pubspec.yaml b/packages/sshnp_gui/pubspec.yaml index 89d4b4ac7..34242021a 100644 --- a/packages/sshnp_gui/pubspec.yaml +++ b/packages/sshnp_gui/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: at_app_flutter: ^5.0.1 at_client_mobile: ^3.2.6 + at_common_flutter: ^2.0.12 at_contact: ^3.0.7 at_contacts_flutter: ^4.0.5 at_onboarding_flutter: ^6.1.0 From aadcb2704d87c9b8da562b3135add2ab2d01ee32 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 11:59:25 +0800 Subject: [PATCH 09/95] chore: rename connection form to profile form --- .../screens/new_connection_screen.dart | 4 +- .../widgets/new_connection_form.dart | 253 ------------------ .../widgets/profile/new_profile_form.dart | 217 +++++++++++++++ 3 files changed, 219 insertions(+), 255 deletions(-) delete mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile/new_profile_form.dart diff --git a/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart index e5ec1bcec..409e5dc4d 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:sshnp_gui/src/presentation/widgets/new_connection_form.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile/new_profile_form.dart'; import '../../utils/sizes.dart'; import '../widgets/navigation/app_navigation_rail.dart'; @@ -32,7 +32,7 @@ class _NewConnectionScreenState extends State { style: Theme.of(context).textTheme.titleMedium, ), gapH10, - const Expanded(child: NewConnectionForm()) + const Expanded(child: NewProfileForm()) ]), ), ), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart deleted file mode 100644 index 62c9e91d0..000000000 --- a/packages/sshnp_gui/lib/src/presentation/widgets/new_connection_form.dart +++ /dev/null @@ -1,253 +0,0 @@ -import 'package:at_client_mobile/at_client_mobile.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; - -import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/minor_providers.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; -import 'package:sshnp_gui/src/utils/app_router.dart'; -import 'package:sshnp_gui/src/utils/enum.dart'; -import 'package:sshnp_gui/src/utils/validator.dart'; - -import '../../utils/sizes.dart'; -import 'home_screen_table/custom_text_form_field.dart'; - -class NewConnectionForm extends ConsumerStatefulWidget { - const NewConnectionForm({super.key}); - - @override - ConsumerState createState() => _NewConnectionFormState(); -} - -class _NewConnectionFormState extends ConsumerState { - final GlobalKey _formkey = GlobalKey(); - late CurrentSSHNPParamsModel currentProfile; - SSHNPPartialParams newConfig = SSHNPPartialParams.empty(); - @override - void initState() { - super.initState(); - } - - void onSubmit(SSHNPParams oldConfig, SSHNPPartialParams newConfig) async { - if (_formkey.currentState!.validate()) { - _formkey.currentState!.save(); - final controller = ref.read(paramsFamilyController( - newConfig.profileName ?? oldConfig.profileName!) - .notifier); - bool overwrite = - currentProfile.configFileWriteState == ConfigFileWriteState.update; - bool rename = newConfig.profileName.isNotNull && - newConfig.profileName!.isNotEmpty && - oldConfig.profileName.isNotNull && - oldConfig.profileName!.isNotEmpty && - newConfig.profileName != oldConfig.profileName; - SSHNPParams config = SSHNPParams.merge(oldConfig, newConfig); - if (rename) { - // delete old config file and write the new one - await ref - .read(paramsFamilyController(oldConfig.profileName!).notifier) - .delete(); - await controller.create(config); - } else if (overwrite) { - // overwrite the existing file - await controller.edit(config); - } else { - // create new config file - await controller.create(config); - } - if (context.mounted) { - ref - .read(currentNavIndexProvider.notifier) - .update((state) => AppRoute.home.index - 1); - context.pushReplacementNamed(AppRoute.home.name); - } - } - } - - @override - Widget build(BuildContext context) { - final strings = AppLocalizations.of(context)!; - currentProfile = ref.watch(currentParamsController); - - final asyncOldConfig = - ref.watch(paramsFamilyController(currentProfile.profileName)); - return asyncOldConfig.when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) => Center(child: Text(error.toString())), - data: (oldConfig) { - return SingleChildScrollView( - child: Form( - key: _formkey, - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomTextFormField( - initialValue: oldConfig.profileName, - labelText: strings.profileName, - onChanged: (value) { - newConfig = SSHNPPartialParams.merge(newConfig, - SSHNPPartialParams(profileName: value!)); - }, - validator: Validator.validateRequiredField, - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.host, - labelText: strings.host, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge( - newConfig, SSHNPPartialParams(host: value!)), - validator: Validator.validateRequiredField, - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.port.toString(), - labelText: strings.port, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge(newConfig, - SSHNPPartialParams(port: int.parse(value!))), - validator: Validator.validateRequiredField, - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.sendSshPublicKey, - labelText: strings.sendSshPublicKey, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge(newConfig, - SSHNPPartialParams(sendSshPublicKey: value!)), - validator: Validator.validateRequiredField, - ), - gapH10, - Row( - children: [ - Text(strings.verbose), - gapW8, - Switch( - value: oldConfig.verbose, - onChanged: (newValue) { - setState(() { - newConfig = SSHNPPartialParams.merge( - newConfig, - SSHNPPartialParams(verbose: newValue)); - }); - }), - ], - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.remoteUsername, - labelText: strings.remoteUserName, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge(newConfig, - SSHNPPartialParams(remoteUsername: value!)), - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.rootDomain, - labelText: strings.rootDomain, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge(newConfig, - SSHNPPartialParams(rootDomain: value!)), - ), - gapH20, - ElevatedButton( - onPressed: () => onSubmit(oldConfig, newConfig), - child: Text(strings.submit), - ), - ]), - gapW12, - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomTextFormField( - initialValue: oldConfig.sshnpdAtSign, - labelText: strings.sshnpdAtSign, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge(newConfig, - SSHNPPartialParams(sshnpdAtSign: value!)), - validator: Validator.validateAtsignField, - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.device, - labelText: strings.device, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge(newConfig, - SSHNPPartialParams(device: value!)), - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.localPort.toString(), - labelText: strings.localPort, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge( - newConfig, - SSHNPPartialParams( - localPort: int.parse(value!))), - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.localSshOptions.join(','), - hintText: strings.localSshOptionsHint, - labelText: strings.localSshOptions, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge( - newConfig, - SSHNPPartialParams( - localSshOptions: value!.split(','))), - ), - gapH10, - Row( - children: [ - Text(strings.rsa), - gapW8, - Switch( - value: oldConfig.rsa, - onChanged: (newValue) { - setState(() { - newConfig = SSHNPPartialParams.merge( - newConfig, - SSHNPPartialParams(rsa: newValue)); - }); - }), - ], - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.atKeysFilePath, - labelText: strings.atKeysFilePath, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge(newConfig, - SSHNPPartialParams(atKeysFilePath: value!)), - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.localSshdPort.toString(), - labelText: strings.localSshdPort, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge( - newConfig, - SSHNPPartialParams( - localSshdPort: int.parse(value!))), - ), - gapH20, - TextButton( - onPressed: () { - ref - .read(currentNavIndexProvider.notifier) - .update((state) => AppRoute.home.index - 1); - context.pushReplacementNamed(AppRoute.home.name); - }, - child: Text(strings.cancel)) - ]), - ], - ), - ), - ); - }); - } -} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/new_profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile/new_profile_form.dart new file mode 100644 index 000000000..e309f152f --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile/new_profile_form.dart @@ -0,0 +1,217 @@ +import 'package:at_client_mobile/at_client_mobile.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/utils/app_router.dart'; +import 'package:sshnp_gui/src/utils/enum.dart'; +import 'package:sshnp_gui/src/utils/validator.dart'; + +import '../../../utils/sizes.dart'; +import '../home_screen_table/custom_text_form_field.dart'; + +class NewProfileForm extends ConsumerStatefulWidget { + const NewProfileForm({super.key}); + + @override + ConsumerState createState() => _NewProfileFormState(); +} + +class _NewProfileFormState extends ConsumerState { + final GlobalKey _formkey = GlobalKey(); + late CurrentSSHNPParamsModel currentProfile; + SSHNPPartialParams newConfig = SSHNPPartialParams.empty(); + @override + void initState() { + super.initState(); + } + + void onSubmit(SSHNPParams oldConfig, SSHNPPartialParams newConfig) async { + if (_formkey.currentState!.validate()) { + _formkey.currentState!.save(); + final controller = ref.read(paramsFamilyController(newConfig.profileName ?? oldConfig.profileName!).notifier); + bool overwrite = currentProfile.configFileWriteState == ConfigFileWriteState.update; + bool rename = newConfig.profileName.isNotNull && + newConfig.profileName!.isNotEmpty && + oldConfig.profileName.isNotNull && + oldConfig.profileName!.isNotEmpty && + newConfig.profileName != oldConfig.profileName; + SSHNPParams config = SSHNPParams.merge(oldConfig, newConfig); + if (rename) { + // delete old config file and write the new one + await ref.read(paramsFamilyController(oldConfig.profileName!).notifier).delete(); + await controller.create(config); + } else if (overwrite) { + // overwrite the existing file + await controller.edit(config); + } else { + // create new config file + await controller.create(config); + } + if (context.mounted) { + ref.read(currentNavIndexProvider.notifier).update((state) => AppRoute.home.index - 1); + context.pushReplacementNamed(AppRoute.home.name); + } + } + } + + @override + Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; + currentProfile = ref.watch(currentParamsController); + + final asyncOldConfig = ref.watch(paramsFamilyController(currentProfile.profileName)); + return asyncOldConfig.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text(error.toString())), + data: (oldConfig) { + return SingleChildScrollView( + child: Form( + key: _formkey, + child: Row( + children: [ + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + CustomTextFormField( + initialValue: oldConfig.profileName, + labelText: strings.profileName, + onChanged: (value) { + newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(profileName: value!)); + }, + validator: Validator.validateRequiredField, + ), + gapH10, + CustomTextFormField( + initialValue: oldConfig.host, + labelText: strings.host, + onChanged: (value) => + newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(host: value!)), + validator: Validator.validateRequiredField, + ), + gapH10, + CustomTextFormField( + initialValue: oldConfig.port.toString(), + labelText: strings.port, + onChanged: (value) => + newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(port: int.parse(value!))), + validator: Validator.validateRequiredField, + ), + gapH10, + CustomTextFormField( + initialValue: oldConfig.sendSshPublicKey, + labelText: strings.sendSshPublicKey, + onChanged: (value) => + newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(sendSshPublicKey: value!)), + validator: Validator.validateRequiredField, + ), + gapH10, + Row( + children: [ + Text(strings.verbose), + gapW8, + Switch( + value: oldConfig.verbose, + onChanged: (newValue) { + setState(() { + newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(verbose: newValue)); + }); + }), + ], + ), + gapH10, + CustomTextFormField( + initialValue: oldConfig.remoteUsername, + labelText: strings.remoteUserName, + onChanged: (value) => + newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(remoteUsername: value!)), + ), + gapH10, + CustomTextFormField( + initialValue: oldConfig.rootDomain, + labelText: strings.rootDomain, + onChanged: (value) => + newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(rootDomain: value!)), + ), + gapH20, + ElevatedButton( + onPressed: () => onSubmit(oldConfig, newConfig), + child: Text(strings.submit), + ), + ]), + gapW12, + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + CustomTextFormField( + initialValue: oldConfig.sshnpdAtSign, + labelText: strings.sshnpdAtSign, + onChanged: (value) => + newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(sshnpdAtSign: value!)), + validator: Validator.validateAtsignField, + ), + gapH10, + CustomTextFormField( + initialValue: oldConfig.device, + labelText: strings.device, + onChanged: (value) => + newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(device: value!)), + ), + gapH10, + CustomTextFormField( + initialValue: oldConfig.localPort.toString(), + labelText: strings.localPort, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(localPort: int.parse(value!))), + ), + gapH10, + CustomTextFormField( + initialValue: oldConfig.localSshOptions.join(','), + hintText: strings.localSshOptionsHint, + labelText: strings.localSshOptions, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(localSshOptions: value!.split(','))), + ), + gapH10, + Row( + children: [ + Text(strings.rsa), + gapW8, + Switch( + value: oldConfig.rsa, + onChanged: (newValue) { + setState(() { + newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(rsa: newValue)); + }); + }), + ], + ), + gapH10, + CustomTextFormField( + initialValue: oldConfig.atKeysFilePath, + labelText: strings.atKeysFilePath, + onChanged: (value) => + newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(atKeysFilePath: value!)), + ), + gapH10, + CustomTextFormField( + initialValue: oldConfig.localSshdPort.toString(), + labelText: strings.localSshdPort, + onChanged: (value) => newConfig = + SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(localSshdPort: int.parse(value!))), + ), + gapH20, + TextButton( + onPressed: () { + ref.read(currentNavIndexProvider.notifier).update((state) => AppRoute.home.index - 1); + context.pushReplacementNamed(AppRoute.home.name); + }, + child: Text(strings.cancel)) + ]), + ], + ), + ), + ); + }); + } +} From a2d3a87fe58a4c290fa80c46242f48a1b5a36881 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 12:08:03 +0800 Subject: [PATCH 10/95] chore: rename profile actions --- .../home_screen_table/home_screen_table_actions.dart | 6 +++--- .../actions/profile_delete_action.dart} | 0 .../actions/profile_edit_action.dart} | 0 .../actions/profile_run_action.dart} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/sshnp_gui/lib/src/presentation/widgets/{home_screen_table/home_screen_delete.dart => profile/actions/profile_delete_action.dart} (100%) rename packages/sshnp_gui/lib/src/presentation/widgets/{home_screen_table/home_screen_edit.dart => profile/actions/profile_edit_action.dart} (100%) rename packages/sshnp_gui/lib/src/presentation/widgets/{home_screen_table/home_screen_run.dart => profile/actions/profile_run_action.dart} (100%) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart index 36a525f77..c9effa11c 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/custom_table_cell.dart'; -import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_delete.dart'; -import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_edit.dart'; -import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_run.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile/actions/profile_delete_action.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile/actions/profile_edit_action.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile/actions/profile_run_action.dart'; class HomeScreenTableActions extends StatelessWidget { final AsyncValue params; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_delete.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_delete_action.dart similarity index 100% rename from packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_delete.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_delete_action.dart diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_edit.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_edit_action.dart similarity index 100% rename from packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_edit.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_edit_action.dart diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_run.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_run_action.dart similarity index 100% rename from packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_run.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_run_action.dart From f7cf6875af348353f92e727767d12f45da291741 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 12:11:27 +0800 Subject: [PATCH 11/95] chore: rename profile actions --- .../src/presentation/screens/home_screen.dart | 75 ++++++------------- .../actions/profile_actions.dart} | 9 +-- .../actions/profile_delete_action.dart | 4 +- .../profile/actions/profile_edit_action.dart | 13 ++-- .../profile/actions/profile_run_action.dart | 11 ++- .../widgets/profile/profile_bar.dart | 28 +++++++ 6 files changed, 68 insertions(+), 72 deletions(-) rename packages/sshnp_gui/lib/src/presentation/widgets/{home_screen_table/home_screen_table_actions.dart => profile/actions/profile_actions.dart} (82%) create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_bar.dart diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index 87832f5e3..f65a0d32a 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -3,9 +3,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; -import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile/actions/profile_actions.dart'; import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_table_header.dart'; import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_table_text.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile/profile_bar.dart'; import '../../utils/sizes.dart'; import '../widgets/navigation/app_navigation_rail.dart'; @@ -19,7 +20,6 @@ class HomeScreen extends ConsumerStatefulWidget { } class _HomeScreenState extends ConsumerState { - @override Widget build(BuildContext context) { // * Getting the AtClientManager instance to use below @@ -36,56 +36,29 @@ class _HomeScreenState extends ConsumerState { Expanded( child: Padding( padding: const EdgeInsets.only(left: Sizes.p36, top: Sizes.p21), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - 'assets/images/noports_light.svg', - ), - gapH24, - Text(strings.availableConnections), - profileNames.when( - loading: () => const Center( - child: CircularProgressIndicator(), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SvgPicture.asset( + 'assets/images/noports_light.svg', + ), + gapH24, + Text(strings.availableConnections), + profileNames.when( + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (e, s) => Text(e.toString()), + data: (profiles) { + if (profiles.isEmpty) { + return const Text('No SSHNP Configurations Found'); + } + return Expanded( + child: ListView( + children: profiles.map((profileName) => ProfileBar(profileName)).toList(), ), - error: (e, s) => Text(e.toString()), - data: (profiles) { - if (profiles.isEmpty) { - return const Text('No SSHNP Configurations Found'); - } - return Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - columnWidths: const { - 0: IntrinsicColumnWidth(), - 1: IntrinsicColumnWidth(), - 2: IntrinsicColumnWidth(), - 3: IntrinsicColumnWidth(), - 4: IntrinsicColumnWidth(), - }, - children: [ - getHomeScreenTableHeader(strings), - ...profiles.map((e) { - final params = - ref.watch(paramsFamilyController(e)); - return TableRow(children: [ - HomeScreenTableActions(params), - HomeScreenTableProfileNameText(params), - HomeScreenTableSshnpdAtSignText(params), - HomeScreenTableDeviceText(params), - HomeScreenTableHostText(params), - ]); - }).toList() - ], - ), - ), - ); - }, - ) - ]), + ); + }, + ) + ]), ), ), ], diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_actions.dart similarity index 82% rename from packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_actions.dart index c9effa11c..bd1f58435 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_actions.dart @@ -16,14 +16,13 @@ class HomeScreenTableActions extends StatelessWidget { data: (p) => CustomTableCell( child: Row( children: [ - HomeScreenRunAction(p), - HomeScreenEditAction(p), - HomeScreenDeleteAction(p), + ProfileRunAction(p), + ProfileEditAction(p), + ProfileDeleteAction(p), ], ), ), - error: (e, s) => - const CustomTableCell.text(text: 'Error fetching data...'), + error: (e, s) => const CustomTableCell.text(text: 'Error fetching data...'), loading: () => const CustomTableCell.text(text: '...'), ); } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_delete_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_delete_action.dart index 1d1c5746f..21d9ac266 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_delete_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_delete_action.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/presentation/widgets/dialog/delete_alert_dialog.dart'; -class HomeScreenDeleteAction extends StatelessWidget { +class ProfileDeleteAction extends StatelessWidget { final SSHNPParams params; - const HomeScreenDeleteAction(this.params, {Key? key}) : super(key: key); + const ProfileDeleteAction(this.params, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_edit_action.dart index b152f6bda..20b11f50e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_edit_action.dart @@ -8,16 +8,15 @@ import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; -class HomeScreenEditAction extends ConsumerStatefulWidget { +class ProfileEditAction extends ConsumerStatefulWidget { final SSHNPParams params; - const HomeScreenEditAction(this.params, {Key? key}) : super(key: key); + const ProfileEditAction(this.params, {Key? key}) : super(key: key); @override - ConsumerState createState() => - _HomeScreenEditActionState(); + ConsumerState createState() => _ProfileEditActionState(); } -class _HomeScreenEditActionState extends ConsumerState { +class _ProfileEditActionState extends ConsumerState { void updateConfigFile(SSHNPParams params) { // Change value to update to trigger the update functionality on the new connection form. ref.watch(currentParamsController.notifier).setState( @@ -27,9 +26,7 @@ class _HomeScreenEditActionState extends ConsumerState { ), ); // change value to 1 to update navigation rail selcted icon. - ref - .watch(currentNavIndexProvider.notifier) - .update((_) => AppRoute.newConnection.index - 1); + ref.watch(currentNavIndexProvider.notifier).update((_) => AppRoute.newConnection.index - 1); context.replaceNamed( AppRoute.newConnection.name, ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_run_action.dart index 74622a3a8..12923616f 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_run_action.dart @@ -5,22 +5,21 @@ import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; import 'package:sshnp_gui/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart'; -class HomeScreenRunAction extends StatefulWidget { +class ProfileRunAction extends StatefulWidget { final SSHNPParams params; - const HomeScreenRunAction(this.params, {Key? key}) : super(key: key); + const ProfileRunAction(this.params, {Key? key}) : super(key: key); @override - State createState() => _HomeScreenRunActionState(); + State createState() => _ProfileRunActionState(); } -class _HomeScreenRunActionState extends State { +class _ProfileRunActionState extends State { Future onPressed(SSHNPParams sshnpParams) async { if (mounted) { showDialog( context: context, barrierDismissible: false, - builder: (BuildContext context) => - const Center(child: CircularProgressIndicator()), + builder: (BuildContext context) => const Center(child: CircularProgressIndicator()), ); } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_bar.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_bar.dart new file mode 100644 index 000000000..69f54b252 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_bar.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; + +class ProfileBar extends ConsumerStatefulWidget { + final String profileName; + const ProfileBar(this.profileName, {Key? key}) : super(key: key); + + @override + ConsumerState createState() => _ProfileBarState(); +} + +class _ProfileBarState extends ConsumerState { + @override + Widget build(BuildContext context) { + final controller = ref.watch(paramsFamilyController(widget.profileName)); + return controller.when( + error: (error, stackTrace) => Container(), + loading: () => const LinearProgressIndicator(), + data: (params) => const Row( + children: [ + + ], + ), + ); + } +} From dc4eccb099ce0d1b24606b3466e786412f5edfde Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 12:12:27 +0800 Subject: [PATCH 12/95] chore: update profile_actions --- .../profile/actions/profile_actions.dart | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_actions.dart index bd1f58435..55eb34724 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_actions.dart @@ -1,29 +1,21 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/custom_table_cell.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile/actions/profile_delete_action.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile/actions/profile_edit_action.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile/actions/profile_run_action.dart'; -class HomeScreenTableActions extends StatelessWidget { - final AsyncValue params; - const HomeScreenTableActions(this.params, {Key? key}) : super(key: key); +class ProfileActions extends StatelessWidget { + final SSHNPParams params; + const ProfileActions(this.params, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return params.when( - data: (p) => CustomTableCell( - child: Row( - children: [ - ProfileRunAction(p), - ProfileEditAction(p), - ProfileDeleteAction(p), - ], - ), - ), - error: (e, s) => const CustomTableCell.text(text: 'Error fetching data...'), - loading: () => const CustomTableCell.text(text: '...'), + return Row( + children: [ + ProfileRunAction(params), + ProfileEditAction(params), + ProfileDeleteAction(params), + ], ); } } From fad4ade9168c643f6a1694f78037a07d80a5cae8 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 12:13:00 +0800 Subject: [PATCH 13/95] chore: rename profile form --- .../lib/src/presentation/screens/new_connection_screen.dart | 2 +- .../profile/{new_profile_form.dart => profile_form.dart} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/sshnp_gui/lib/src/presentation/widgets/profile/{new_profile_form.dart => profile_form.dart} (100%) diff --git a/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart index 409e5dc4d..6aea2d7a4 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile/new_profile_form.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile/profile_form.dart'; import '../../utils/sizes.dart'; import '../widgets/navigation/app_navigation_rail.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/new_profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_form.dart similarity index 100% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile/new_profile_form.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_form.dart From 6d8f87b1b0353c38627556baff5272544c4e1cac Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 12:13:20 +0800 Subject: [PATCH 14/95] chore: rename profile form --- .../src/presentation/screens/new_connection_screen.dart | 2 +- .../src/presentation/widgets/profile/profile_form.dart | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart index 6aea2d7a4..93e510893 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart @@ -32,7 +32,7 @@ class _NewConnectionScreenState extends State { style: Theme.of(context).textTheme.titleMedium, ), gapH10, - const Expanded(child: NewProfileForm()) + const Expanded(child: ProfileForm()) ]), ), ), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_form.dart index e309f152f..cc203b2ae 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_form.dart @@ -14,14 +14,14 @@ import 'package:sshnp_gui/src/utils/validator.dart'; import '../../../utils/sizes.dart'; import '../home_screen_table/custom_text_form_field.dart'; -class NewProfileForm extends ConsumerStatefulWidget { - const NewProfileForm({super.key}); +class ProfileForm extends ConsumerStatefulWidget { + const ProfileForm({super.key}); @override - ConsumerState createState() => _NewProfileFormState(); + ConsumerState createState() => _ProfileFormState(); } -class _NewProfileFormState extends ConsumerState { +class _ProfileFormState extends ConsumerState { final GlobalKey _formkey = GlobalKey(); late CurrentSSHNPParamsModel currentProfile; SSHNPPartialParams newConfig = SSHNPPartialParams.empty(); From 05585b8420b6a2ffa78ab7744c81b4fe27460cea Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 12:26:48 +0800 Subject: [PATCH 15/95] fix: profileName with spaces --- .../sshnoports/lib/sshnp/sshnp_params.dart | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index 1b15ea9a1..cdf2457d0 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -144,27 +144,17 @@ class SSHNPParams { print(e); } }); - + print('fileNames: $fileNames'); return fileNames; } static Future fromFile(String profileName, [String? directory]) async { - var homeDirectory = getHomeDirectory(throwIfNull: true)!; - directory ??= getDefaultSshnpConfigDirectory(homeDirectory); - var fileName = path.join( - directory, - '$profileName.env', - ); + var fileName = _profileToFileName(profileName, directory); return SSHNPParams.fromConfigFile(fileName); } static Future fileExists(String profileName, [String? directory]) { - var homeDirectory = getHomeDirectory(throwIfNull: true)!; - directory ??= getDefaultSshnpConfigDirectory(homeDirectory); - var fileName = path.join( - directory, - '$profileName.env', - ); + var fileName = _profileToFileName(profileName, directory); return File(fileName).exists(); } @@ -173,12 +163,8 @@ class SSHNPParams { throw Exception('profileName is null or empty'); } - var fileName = profileName!.replaceAll(' ', '_'); - - var file = File(path.join( - directory ?? getDefaultSshnpConfigDirectory(homeDirectory), - '$fileName.env', - )); + var fileName = _profileToFileName(profileName!, directory); + var file = File(fileName); var exists = await file.exists(); @@ -196,12 +182,8 @@ class SSHNPParams { throw Exception('profileName is null or empty'); } - var fileName = profileName!.replaceAll(' ', '_'); - - var file = File(path.join( - directory ?? getDefaultSshnpConfigDirectory(homeDirectory), - '$fileName.env', - )); + var fileName = _profileToFileName(profileName!, directory); + var file = File(fileName); var exists = await file.exists(); @@ -489,3 +471,10 @@ class SSHNPPartialParams { } String _fileToProfileName(String fileName) => path.basenameWithoutExtension(fileName).replaceAll('_', ' '); +String _profileToFileName(String profileName, [String? directory]) { + var fileName = profileName.replaceAll(' ', '_'); + return path.join( + directory ?? getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), + '$fileName.env', + ); +} From 5cab0d15564060b7ef051a3d5e642471cd35a9d7 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 12:28:08 +0800 Subject: [PATCH 16/95] chore: ignore '.env' --- packages/sshnoports/lib/sshnp/sshnp_params.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index cdf2457d0..7217235ed 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -135,6 +135,7 @@ class SSHNPParams { await files.forEach((file) { if (file is! File) return; if (path.extension(file.path) != '.env') return; + if (path.basenameWithoutExtension(file.path).isEmpty) return; // ignore '.env' file - empty profileName fileNames.add(_fileToProfileName(file.path)); try { var p = SSHNPParams.fromConfigFile(file.path); From 91a33863f6696f8765a60a9553518470bbe1f3c4 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 12:38:35 +0800 Subject: [PATCH 17/95] chore: clean up profile bar and profile form --- .../src/presentation/screens/home_screen.dart | 5 +- .../screens/new_connection_screen.dart | 2 +- .../home_screen_table/custom_table_cell.dart | 30 -------- .../home_screen_table_header.dart | 15 ---- .../home_screen_table_text.dart | 69 ------------------ .../actions/profile_actions.dart | 8 ++- .../actions/profile_delete_action.dart | 0 .../actions/profile_edit_action.dart | 0 .../actions/profile_run_action.dart | 0 .../actions/profile_terminal_action.dart | 72 +++++++++++++++++++ .../{profile => profile_bar}/profile_bar.dart | 21 ++++-- .../profile_bar/stats/profile_stats.dart | 0 .../custom_text_form_field.dart | 0 .../profile_form.dart | 4 +- 14 files changed, 97 insertions(+), 129 deletions(-) delete mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/custom_table_cell.dart delete mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_header.dart delete mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_text.dart rename packages/sshnp_gui/lib/src/presentation/widgets/{profile => profile_bar}/actions/profile_actions.dart (50%) rename packages/sshnp_gui/lib/src/presentation/widgets/{profile => profile_bar}/actions/profile_delete_action.dart (100%) rename packages/sshnp_gui/lib/src/presentation/widgets/{profile => profile_bar}/actions/profile_edit_action.dart (100%) rename packages/sshnp_gui/lib/src/presentation/widgets/{profile => profile_bar}/actions/profile_run_action.dart (100%) create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart rename packages/sshnp_gui/lib/src/presentation/widgets/{profile => profile_bar}/profile_bar.dart (58%) create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/stats/profile_stats.dart rename packages/sshnp_gui/lib/src/presentation/widgets/{home_screen_table => profile_form}/custom_text_form_field.dart (100%) rename packages/sshnp_gui/lib/src/presentation/widgets/{profile => profile_form}/profile_form.dart (98%) diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index f65a0d32a..37cefb453 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -3,10 +3,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile/actions/profile_actions.dart'; -import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_table_header.dart'; -import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/home_screen_table_text.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile/profile_bar.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar.dart'; import '../../utils/sizes.dart'; import '../widgets/navigation/app_navigation_rail.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart index 93e510893..ccc42e84e 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile/profile_form.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_form/profile_form.dart'; import '../../utils/sizes.dart'; import '../widgets/navigation/app_navigation_rail.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/custom_table_cell.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/custom_table_cell.dart deleted file mode 100644 index b1c5bb248..000000000 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/custom_table_cell.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomTableCell extends StatelessWidget { - const CustomTableCell({ - required this.child, - this.text = '', - super.key, - }); - - const CustomTableCell.text({ - super.key, - this.child = const SizedBox(), - required this.text, - }); - - final Widget child; - final String text; - - @override - Widget build(BuildContext context) { - return TableCell( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: text.isNotEmpty ? Text(text) : child, - ), - ), - ); - } -} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_header.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_header.dart deleted file mode 100644 index 85992447e..000000000 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_header.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter/material.dart'; -import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/custom_table_cell.dart'; - -TableRow getHomeScreenTableHeader(AppLocalizations strings) => TableRow( - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.white))), - children: [ - CustomTableCell.text(text: strings.actions), - CustomTableCell.text(text: strings.profileName), - CustomTableCell.text(text: strings.sshnpdAtSign), - CustomTableCell.text(text: strings.device), - CustomTableCell.text(text: strings.host), - ], - ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_text.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_text.dart deleted file mode 100644 index 933b72286..000000000 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/home_screen_table_text.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/presentation/widgets/home_screen_table/custom_table_cell.dart'; - -class HomeScreenTableProfileNameText extends StatelessWidget { - final AsyncValue params; - const HomeScreenTableProfileNameText(this.params, {Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return params.when( - data: (p) => CustomTableCell.text(text: p.profileName!), - error: (e, s) => - const CustomTableCell.text(text: 'Error fetching data...'), - loading: () => const CustomTableCell.text(text: '...'), - ); - } -} - -class HomeScreenTableSshnpdAtSignText extends StatelessWidget { - final AsyncValue params; - const HomeScreenTableSshnpdAtSignText(this.params, {Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return params.when( - data: (p) => CustomTableCell.text(text: p.sshnpdAtSign!), - error: (e, s) => - const CustomTableCell.text(text: 'Error fetching data...'), - loading: () => const CustomTableCell.text(text: '...'), - ); - } -} - -class HomeScreenTableDeviceText extends StatelessWidget { - final AsyncValue params; - const HomeScreenTableDeviceText(this.params, {Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return params.when( - data: (p) => CustomTableCell.text(text: p.device), - error: (e, s) => - const CustomTableCell.text(text: 'Error fetching data...'), - loading: () => const CustomTableCell.text(text: '...'), - ); - } -} - -class HomeScreenTableHostText extends StatelessWidget { - final AsyncValue params; - const HomeScreenTableHostText(this.params, {Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return params.when( - data: (p) => CustomTableCell.text(text: p.host!), - error: (e, s) => - const CustomTableCell.text(text: 'Error fetching data...'), - loading: () => const CustomTableCell.text(text: '...'), - ); - } -} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_actions.dart similarity index 50% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_actions.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_actions.dart index 55eb34724..a58b4c584 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_actions.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile/actions/profile_delete_action.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile/actions/profile_edit_action.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile/actions/profile_run_action.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_bar/actions/profile_delete_action.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_bar/actions/profile_run_action.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart'; class ProfileActions extends StatelessWidget { final SSHNPParams params; @@ -13,6 +14,7 @@ class ProfileActions extends StatelessWidget { return Row( children: [ ProfileRunAction(params), + ProfileTerminalAction(params), ProfileEditAction(params), ProfileDeleteAction(params), ], diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_delete_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_delete_action.dart similarity index 100% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_delete_action.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_delete_action.dart diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart similarity index 100% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_edit_action.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_run_action.dart similarity index 100% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile/actions/profile_run_action.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_run_action.dart diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart new file mode 100644 index 000000000..0c189e2d3 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart @@ -0,0 +1,72 @@ +import 'package:at_client_mobile/at_client_mobile.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnoports/sshrv/sshrv.dart'; +import 'package:sshnp_gui/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart'; + +class ProfileTerminalAction extends StatefulWidget { + final SSHNPParams params; + const ProfileTerminalAction(this.params, {Key? key}) : super(key: key); + + @override + State createState() => _ProfileTerminalActionState(); +} + +class _ProfileTerminalActionState extends State { + Future onPressed(SSHNPParams sshnpParams) async { + if (mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => const Center(child: CircularProgressIndicator()), + ); + } + + try { + final sshnp = await SSHNP.fromParams( + sshnpParams, + atClient: AtClientManager.getInstance().atClient, + sshrvGenerator: SSHRV.pureDart, + ); + + await sshnp.init(); + final sshnpResult = await sshnp.run(); + + if (mounted) { + // pop to remove circular progress indicator + context.pop(); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => SSHNPResultAlertDialog( + result: sshnpResult.toString(), + title: 'Success', + ), + ); + } + } catch (e) { + if (mounted) { + context.pop(); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => SSHNPResultAlertDialog( + result: e.toString(), + title: 'Failed', + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () async { + await onPressed(widget.params); + }, + icon: const Icon(Icons.terminal), + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_bar.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart similarity index 58% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_bar.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart index 69f54b252..6088f3c17 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_bar.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:macos_ui/macos_ui.dart'; import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_bar/actions/profile_actions.dart'; class ProfileBar extends ConsumerStatefulWidget { final String profileName; @@ -18,10 +18,21 @@ class _ProfileBarState extends ConsumerState { return controller.when( error: (error, stackTrace) => Container(), loading: () => const LinearProgressIndicator(), - data: (params) => const Row( - children: [ - - ], + data: (params) => Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: Theme.of(context).dividerColor, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(params.profileName ?? ''), + ProfileActions(params), + ], + ), ), ); } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/stats/profile_stats.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/stats/profile_stats.dart new file mode 100644 index 000000000..e69de29bb diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/custom_text_form_field.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/custom_text_form_field.dart similarity index 100% rename from packages/sshnp_gui/lib/src/presentation/widgets/home_screen_table/custom_text_form_field.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_form/custom_text_form_field.dart diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart similarity index 98% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_form.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index cc203b2ae..d3a5bda36 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -7,12 +7,12 @@ import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/minor_providers.dart'; import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_form/custom_text_form_field.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; import 'package:sshnp_gui/src/utils/validator.dart'; +import 'package:sshnp_gui/src/utils/sizes.dart'; -import '../../../utils/sizes.dart'; -import '../home_screen_table/custom_text_form_field.dart'; class ProfileForm extends ConsumerStatefulWidget { const ProfileForm({super.key}); From 957414654ec5d1dc622acc7b672f6ab91d6e9e4e Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 12:39:59 +0800 Subject: [PATCH 18/95] chore: rename profile editor screen --- ..._connection_screen.dart => profile_editor_screen.dart} | 8 ++++---- packages/sshnp_gui/lib/src/utils/app_router.dart | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) rename packages/sshnp_gui/lib/src/presentation/screens/{new_connection_screen.dart => profile_editor_screen.dart} (88%) diff --git a/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart similarity index 88% rename from packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart rename to packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart index ccc42e84e..4ff4daf9c 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/new_connection_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart @@ -6,14 +6,14 @@ import '../../utils/sizes.dart'; import '../widgets/navigation/app_navigation_rail.dart'; // * Once the onboarding process is completed you will be taken to this screen -class NewConnectionScreen extends StatefulWidget { - const NewConnectionScreen({Key? key}) : super(key: key); +class ProfileEditorScreen extends StatefulWidget { + const ProfileEditorScreen({Key? key}) : super(key: key); @override - State createState() => _NewConnectionScreenState(); + State createState() => _ProfileEditorScreenState(); } -class _NewConnectionScreenState extends State { +class _ProfileEditorScreenState extends State { @override Widget build(BuildContext context) { final strings = AppLocalizations.of(context)!; diff --git a/packages/sshnp_gui/lib/src/utils/app_router.dart b/packages/sshnp_gui/lib/src/utils/app_router.dart index e3668f3ec..808f778b4 100644 --- a/packages/sshnp_gui/lib/src/utils/app_router.dart +++ b/packages/sshnp_gui/lib/src/utils/app_router.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/presentation/screens/new_connection_screen.dart'; +import 'package:sshnp_gui/src/presentation/screens/profile_editor_screen.dart'; import 'package:sshnp_gui/src/repository/navigation_service.dart'; import '../presentation/screens/home_screen.dart'; @@ -41,7 +41,7 @@ final goRouterProvider = Provider((ref) => GoRouter( name: AppRoute.newConnection.name, pageBuilder: (context, state) => CustomTransitionPage( key: state.pageKey, - child: const NewConnectionScreen(), + child: const ProfileEditorScreen(), transitionsBuilder: ((context, animation, secondaryAnimation, child) => FadeTransition(opacity: animation, child: child))), ), From 7228445529d9f24e28ccd3b64b14920a50c8b35d Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 12:46:56 +0800 Subject: [PATCH 19/95] chore: organize imports --- packages/sshnp_gui/lib/main.dart | 7 +++---- .../lib/src/controllers/authentication_controller.dart | 3 +-- .../lib/src/presentation/screens/home_screen.dart | 5 ++--- .../src/presentation/screens/profile_editor_screen.dart | 5 ++--- .../lib/src/presentation/screens/settings_screen.dart | 9 ++++----- .../lib/src/presentation/screens/terminal_screen.dart | 5 ++--- .../presentation/widgets/dialog/delete_alert_dialog.dart | 3 +-- .../widgets/dialog/sshnp_result_alert_dialog.dart | 5 ++--- .../widgets/navigation/app_navigation_rail.dart | 5 ++--- .../presentation/widgets/profile_form/profile_form.dart | 3 +-- .../presentation/widgets/utility/reset_app_button.dart | 9 ++++----- .../presentation/widgets/utility/settings_button.dart | 3 +-- .../src/presentation/widgets/utility/switch_atsign.dart | 8 +++----- .../lib/src/repository/authentication_repository.dart | 9 +++------ packages/sshnp_gui/lib/src/utils/app_router.dart | 9 ++++----- packages/sshnp_gui/lib/src/utils/theme.dart | 3 +-- packages/sshnp_gui/lib/src/utils/validator.dart | 2 +- packages/sshnp_gui/pubspec.yaml | 2 ++ 18 files changed, 39 insertions(+), 56 deletions(-) diff --git a/packages/sshnp_gui/lib/main.dart b/packages/sshnp_gui/lib/main.dart index 1741e862b..6afe47b27 100644 --- a/packages/sshnp_gui/lib/main.dart +++ b/packages/sshnp_gui/lib/main.dart @@ -6,10 +6,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:macos_ui/macos_ui.dart'; - -import 'src/utils/app_router.dart'; -import 'src/utils/theme.dart'; -import 'src/utils/util.dart'; +import 'package:sshnp_gui/src/utils/app_router.dart'; +import 'package:sshnp_gui/src/utils/theme.dart'; +import 'package:sshnp_gui/src/utils/util.dart'; final AtSignLogger _logger = AtSignLogger(AtEnv.appNamespace); diff --git a/packages/sshnp_gui/lib/src/controllers/authentication_controller.dart b/packages/sshnp_gui/lib/src/controllers/authentication_controller.dart index ad9db6d7d..887ea5342 100644 --- a/packages/sshnp_gui/lib/src/controllers/authentication_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/authentication_controller.dart @@ -1,7 +1,6 @@ import 'package:at_contact/at_contact.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../repository/authentication_repository.dart'; +import 'package:sshnp_gui/src/repository/authentication_repository.dart'; /// A controller class that controls the UI update when the [AuthenticationRepository] methods are called. class AuthenticationController extends StateNotifier?>> { diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index 37cefb453..be00fdf09 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -3,10 +3,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar.dart'; - -import '../../utils/sizes.dart'; -import '../widgets/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/utils/sizes.dart'; // * Once the onboarding process is completed you will be taken to this screen class HomeScreen extends ConsumerStatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart index 4ff4daf9c..6208f5ca4 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_form/profile_form.dart'; - -import '../../utils/sizes.dart'; -import '../widgets/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/utils/sizes.dart'; // * Once the onboarding process is completed you will be taken to this screen class ProfileEditorScreen extends StatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart index bf5cc25f0..15cc81bfd 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart @@ -4,13 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/reset_app_button.dart'; +import 'package:sshnp_gui/src/presentation/widgets/utility/settings_button.dart'; +import 'package:sshnp_gui/src/presentation/widgets/utility/switch_atsign.dart'; +import 'package:sshnp_gui/src/repository/navigation_service.dart'; +import 'package:sshnp_gui/src/utils/sizes.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../repository/navigation_service.dart'; -import '../../utils/sizes.dart'; -import '../widgets/utility/settings_button.dart'; -import '../widgets/utility/switch_atsign.dart'; - class SettingsScreen extends StatelessWidget { const SettingsScreen({Key? key}) : super(key: key); static String route = 'settingsScreen'; diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index e66b269df..9322ff6a1 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -6,11 +6,10 @@ import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/utils/sizes.dart'; import 'package:xterm/xterm.dart'; -import '../../utils/sizes.dart'; -import '../widgets/navigation/app_navigation_rail.dart'; - // * Once the onboarding process is completed you will be taken to this screen class TerminalScreen extends ConsumerStatefulWidget { const TerminalScreen({Key? key}) : super(key: key); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart index 97e8ce017..83f92a21c 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart @@ -3,8 +3,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; - -import '../../../utils/sizes.dart'; +import 'package:sshnp_gui/src/utils/sizes.dart'; class DeleteAlertDialog extends ConsumerWidget { const DeleteAlertDialog({required this.sshnpParams, super.key}); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart index f62814448..0f844c707 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart @@ -3,9 +3,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; - -import '../../../controllers/minor_providers.dart'; -import '../../../utils/app_router.dart'; +import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/utils/app_router.dart'; class SSHNPResultAlertDialog extends ConsumerWidget { const SSHNPResultAlertDialog({required this.result, required this.title, super.key}); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart index 34d47d764..7f551145f 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart @@ -2,9 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; - -import '../../../controllers/minor_providers.dart'; -import '../../../utils/app_router.dart'; +import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/utils/app_router.dart'; class AppNavigationRail extends ConsumerWidget { const AppNavigationRail({super.key}); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index d3a5bda36..796b987fe 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -3,15 +3,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; - import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/minor_providers.dart'; import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_form/custom_text_form_field.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; -import 'package:sshnp_gui/src/utils/validator.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; +import 'package:sshnp_gui/src/utils/validator.dart'; class ProfileForm extends ConsumerStatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/utility/reset_app_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/utility/reset_app_button.dart index 84f65ad72..c91b19993 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/utility/reset_app_button.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/utility/reset_app_button.dart @@ -1,11 +1,10 @@ import 'package:at_onboarding_flutter/services/sdk_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import '../../../../main.dart'; -import '../../../utils/at_error_dialog.dart'; -import '../../../utils/sizes.dart'; -import 'settings_button.dart'; +import 'package:sshnp_gui/main.dart'; +import 'package:sshnp_gui/src/presentation/widgets/utility/settings_button.dart'; +import 'package:sshnp_gui/src/utils/at_error_dialog.dart'; +import 'package:sshnp_gui/src/utils/sizes.dart'; /// Custom reset button widget is to reset an atsign from keychain list, diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/utility/settings_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/utility/settings_button.dart index 7fc375962..151fc4387 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/utility/settings_button.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/utility/settings_button.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:sshnp_gui/src/utils/constants.dart'; - -import '../../../utils/sizes.dart'; +import 'package:sshnp_gui/src/utils/sizes.dart'; class SettingsButton extends StatelessWidget { const SettingsButton({ diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/utility/switch_atsign.dart b/packages/sshnp_gui/lib/src/presentation/widgets/utility/switch_atsign.dart index fd5f1c170..2a7f32a66 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/utility/switch_atsign.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/utility/switch_atsign.dart @@ -1,14 +1,12 @@ import 'package:at_common_flutter/services/size_config.dart'; import 'package:at_contact/at_contact.dart'; import 'package:at_contacts_flutter/widgets/circular_contacts.dart'; - import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../../../controllers/authentication_controller.dart'; -import '../../../repository/authentication_repository.dart'; -import 'custom_snack_bar.dart'; +import 'package:sshnp_gui/src/controllers/authentication_controller.dart'; +import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; +import 'package:sshnp_gui/src/repository/authentication_repository.dart'; class AtSignBottomSheet extends ConsumerStatefulWidget { const AtSignBottomSheet({Key? key}) : super(key: key); diff --git a/packages/sshnp_gui/lib/src/repository/authentication_repository.dart b/packages/sshnp_gui/lib/src/repository/authentication_repository.dart index a3b3b73aa..1f66ff409 100644 --- a/packages/sshnp_gui/lib/src/repository/authentication_repository.dart +++ b/packages/sshnp_gui/lib/src/repository/authentication_repository.dart @@ -1,4 +1,3 @@ -// 🎯 Dart imports: import 'dart:async'; import 'package:at_app_flutter/at_app_flutter.dart'; @@ -12,11 +11,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; - -import '../presentation/widgets/utility/custom_snack_bar.dart'; -// import '../utils/my_sync_progress_listener.dart'; -import '../utils/app_router.dart'; -import 'navigation_service.dart'; +import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; +import 'package:sshnp_gui/src/repository/navigation_service.dart'; +import 'package:sshnp_gui/src/utils/app_router.dart'; /// A singleton that makes all the network calls to the @platform. class AuthenticationRepository { diff --git a/packages/sshnp_gui/lib/src/utils/app_router.dart b/packages/sshnp_gui/lib/src/utils/app_router.dart index 808f778b4..6c8b40f2b 100644 --- a/packages/sshnp_gui/lib/src/utils/app_router.dart +++ b/packages/sshnp_gui/lib/src/utils/app_router.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:sshnp_gui/src/presentation/screens/home_screen.dart'; +import 'package:sshnp_gui/src/presentation/screens/onboarding_screen.dart'; import 'package:sshnp_gui/src/presentation/screens/profile_editor_screen.dart'; +import 'package:sshnp_gui/src/presentation/screens/settings_screen.dart'; +import 'package:sshnp_gui/src/presentation/screens/terminal_screen.dart'; import 'package:sshnp_gui/src/repository/navigation_service.dart'; -import '../presentation/screens/home_screen.dart'; -import '../presentation/screens/onboarding_screen.dart'; -import '../presentation/screens/settings_screen.dart'; -import '../presentation/screens/terminal_screen.dart'; - enum AppRoute { onboarding, home, diff --git a/packages/sshnp_gui/lib/src/utils/theme.dart b/packages/sshnp_gui/lib/src/utils/theme.dart index 7fcd6988e..1963d4b95 100644 --- a/packages/sshnp_gui/lib/src/utils/theme.dart +++ b/packages/sshnp_gui/lib/src/utils/theme.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:macos_ui/macos_ui.dart'; +import 'package:sshnp_gui/src/utils/constants.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; -import 'constants.dart'; - class AppTheme { static TextTheme lightTextTheme = const TextTheme( // displayLarge: TextStyle( diff --git a/packages/sshnp_gui/lib/src/utils/validator.dart b/packages/sshnp_gui/lib/src/utils/validator.dart index fbd00fa7a..d190939e1 100644 --- a/packages/sshnp_gui/lib/src/utils/validator.dart +++ b/packages/sshnp_gui/lib/src/utils/validator.dart @@ -1,4 +1,4 @@ -import 'constants.dart'; +import 'package:sshnp_gui/src/utils/constants.dart'; class Validator { static String? validateRequiredField(String? value) { diff --git a/packages/sshnp_gui/pubspec.yaml b/packages/sshnp_gui/pubspec.yaml index 34242021a..d493efb47 100644 --- a/packages/sshnp_gui/pubspec.yaml +++ b/packages/sshnp_gui/pubspec.yaml @@ -8,6 +8,7 @@ environment: dependencies: at_app_flutter: ^5.0.1 + at_backupkey_flutter: ^4.0.10 at_client_mobile: ^3.2.6 at_common_flutter: ^2.0.12 at_contact: ^3.0.7 @@ -32,6 +33,7 @@ dependencies: shared_preferences: ^2.2.0 sshnoports: path: ../sshnoports/ + url_launcher: ^6.1.14 xterm: ^3.5.0 dev_dependencies: From d0cd618f814e47363a2124f074f828301f3c5a7b Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 13:51:05 +0800 Subject: [PATCH 20/95] chore: cleanup profile_form --- .../sshnoports/lib/sshnp/sshnp_params.dart | 11 +- packages/sshnp_gui/lib/l10n/app_en.arb | 4 +- .../src/presentation/screens/home_screen.dart | 12 +- .../home_screen_actions.dart | 13 + .../new_profile_action.dart | 41 +++ .../navigation/app_navigation_rail.dart | 13 +- .../actions/profile_edit_action.dart | 10 +- .../actions/profile_run_action.dart | 6 +- .../actions/profile_terminal_action.dart | 6 +- .../profile_form/custom_text_form_field.dart | 2 +- .../widgets/profile_form/profile_form.dart | 320 +++++++++++------- .../sshnp_gui/lib/src/utils/app_router.dart | 4 +- 12 files changed, 277 insertions(+), 165 deletions(-) create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_actions.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index 7217235ed..31be35430 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -72,7 +72,7 @@ class SSHNPParams { localPort: params2.localPort ?? params1.localPort, atKeysFilePath: params2.atKeysFilePath ?? params1.atKeysFilePath, sendSshPublicKey: params2.sendSshPublicKey ?? params1.sendSshPublicKey, - localSshOptions: params1.localSshOptions + params2.localSshOptions, + localSshOptions: params2.localSshOptions ?? params1.localSshOptions, rsa: params2.rsa ?? params1.rsa, remoteUsername: params2.remoteUsername ?? params1.remoteUsername, verbose: params2.verbose ?? params1.verbose, @@ -109,7 +109,7 @@ class SSHNPParams { port: partial.port ?? SSHNP.defaultPort, localPort: partial.localPort ?? SSHNP.defaultLocalPort, sendSshPublicKey: partial.sendSshPublicKey ?? SSHNP.defaultSendSshPublicKey, - localSshOptions: partial.localSshOptions, + localSshOptions: partial.localSshOptions ?? SSHNP.defaultLocalSshOptions, rsa: partial.rsa ?? SSHNP.defaultRsa, verbose: partial.verbose ?? SSHNP.defaultRsa, remoteUsername: partial.remoteUsername, @@ -221,6 +221,7 @@ class SSHNPParams { var key = SSHNPArg.fromName(entry.key).bashName; if (key.isEmpty) continue; var value = entry.value; + if (value == null) continue; if (value is List) { value = value.join(','); } @@ -245,7 +246,7 @@ class SSHNPPartialParams { final int? localSshdPort; final String? atKeysFilePath; final String? sendSshPublicKey; - final List localSshOptions; + final List? localSshOptions; final bool? rsa; final String? remoteUsername; final bool? verbose; @@ -269,7 +270,7 @@ class SSHNPPartialParams { this.localPort, this.atKeysFilePath, this.sendSshPublicKey, - this.localSshOptions = SSHNP.defaultLocalSshOptions, + this.localSshOptions, this.rsa, this.remoteUsername, this.verbose, @@ -298,7 +299,7 @@ class SSHNPPartialParams { localPort: params2.localPort ?? params1.localPort, atKeysFilePath: params2.atKeysFilePath ?? params1.atKeysFilePath, sendSshPublicKey: params2.sendSshPublicKey ?? params1.sendSshPublicKey, - localSshOptions: params1.localSshOptions + params2.localSshOptions, + localSshOptions: params2.localSshOptions ?? params1.localSshOptions, rsa: params2.rsa ?? params1.rsa, remoteUsername: params2.remoteUsername ?? params1.remoteUsername, verbose: params2.verbose ?? params1.verbose, diff --git a/packages/sshnp_gui/lib/l10n/app_en.arb b/packages/sshnp_gui/lib/l10n/app_en.arb index ccaaa977d..d74f94799 100644 --- a/packages/sshnp_gui/lib/l10n/app_en.arb +++ b/packages/sshnp_gui/lib/l10n/app_en.arb @@ -25,7 +25,7 @@ "homeDirectoryHint" : "The home directory on this host", "sessionId" : "Session ID", "sendSshPublicKey" : "SSH Public Key", - "rsa" : "Use RSA key Format", + "rsa" : "Legacy RSA Key", "keyFile" : "Key File", "from" : "From", "to" : "To", @@ -36,7 +36,7 @@ "sshPublicKey" : "SSH Public Key", "localSshOptions" : "Local SSH Options", "localSshOptionsHint" : "Use \",\" to separate options", - "verbose" : "verbose Logging", + "verbose" : "Verbose Logging", "remoteUserName" : "Remote Username", "atKeysFilePath" : "atKeys File", "rootDomain" : "Root Domain", diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index be00fdf09..b8885742d 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_actions.dart'; import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; @@ -33,11 +34,18 @@ class _HomeScreenState extends ConsumerState { child: Padding( padding: const EdgeInsets.only(left: Sizes.p36, top: Sizes.p21), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - 'assets/images/noports_light.svg', + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SvgPicture.asset( + 'assets/images/noports_light.svg', + ), + const HomeScreenActions(), + ], ), gapH24, Text(strings.availableConnections), + gapH8, profileNames.when( loading: () => const Center( child: CircularProgressIndicator(), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_actions.dart new file mode 100644 index 000000000..2c9a92840 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_actions.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/new_profile_action.dart'; + +class HomeScreenActions extends StatelessWidget { + const HomeScreenActions({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Row( + children: [NewProfileAction()], + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart new file mode 100644 index 000000000..a017068e8 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/utils/app_router.dart'; +import 'package:sshnp_gui/src/utils/enum.dart'; + +class NewProfileAction extends ConsumerStatefulWidget { + const NewProfileAction({Key? key}) : super(key: key); + + @override + ConsumerState createState() => _NewProfileActionState(); +} + +class _NewProfileActionState extends ConsumerState { + void onPressed() { + // Change value to update to trigger the update functionality on the new connection form. + ref.watch(currentParamsController.notifier).setState( + CurrentSSHNPParamsModel( + profileName: '', + configFileWriteState: ConfigFileWriteState.create, + ), + ); + // change value to 1 to update navigation rail selcted icon. + ref.watch(currentNavIndexProvider.notifier).update((_) => AppRoute.profileForm.index - 1); + context.replaceNamed( + AppRoute.profileForm.name, + ); + } + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () { + onPressed(); + }, + icon: const Icon(Icons.add), + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart index 7f551145f..24fedcfe0 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart @@ -24,18 +24,12 @@ class AppNavigationRail extends ConsumerWidget { ), NavigationRailDestination( icon: currentIndex == 1 - ? SvgPicture.asset('assets/images/nav_icons/new_selected.svg') - : SvgPicture.asset('assets/images/nav_icons/new_unselected.svg'), - label: const Text(''), - ), - NavigationRailDestination( - icon: currentIndex == 2 ? SvgPicture.asset('assets/images/nav_icons/pican_selected.svg') : SvgPicture.asset('assets/images/nav_icons/pican_unselected.svg'), label: const Text(''), ), NavigationRailDestination( - icon: currentIndex == 3 + icon: currentIndex == 2 ? SvgPicture.asset('assets/images/nav_icons/settings_selected.svg') : SvgPicture.asset('assets/images/nav_icons/settings_unselected.svg'), label: const Text(''), @@ -50,12 +44,9 @@ class AppNavigationRail extends ConsumerWidget { context.goNamed(AppRoute.home.name); break; case 1: - context.goNamed(AppRoute.newConnection.name); - break; - case 2: context.goNamed(AppRoute.terminal.name); break; - case 3: + case 2: context.goNamed(AppRoute.settings.name); break; } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart index 20b11f50e..81e415df0 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart @@ -17,18 +17,18 @@ class ProfileEditAction extends ConsumerStatefulWidget { } class _ProfileEditActionState extends ConsumerState { - void updateConfigFile(SSHNPParams params) { + void onPressed() { // Change value to update to trigger the update functionality on the new connection form. ref.watch(currentParamsController.notifier).setState( CurrentSSHNPParamsModel( - profileName: params.profileName!, + profileName: widget.params.profileName!, configFileWriteState: ConfigFileWriteState.update, ), ); // change value to 1 to update navigation rail selcted icon. - ref.watch(currentNavIndexProvider.notifier).update((_) => AppRoute.newConnection.index - 1); + ref.watch(currentNavIndexProvider.notifier).update((_) => AppRoute.profileForm.index - 1); context.replaceNamed( - AppRoute.newConnection.name, + AppRoute.profileForm.name, ); } @@ -36,7 +36,7 @@ class _ProfileEditActionState extends ConsumerState { Widget build(BuildContext context) { return IconButton( onPressed: () { - updateConfigFile(widget.params); + onPressed(); }, icon: const Icon(Icons.edit), ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_run_action.dart index 12923616f..60adf1083 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_run_action.dart @@ -14,7 +14,7 @@ class ProfileRunAction extends StatefulWidget { } class _ProfileRunActionState extends State { - Future onPressed(SSHNPParams sshnpParams) async { + Future onPressed() async { if (mounted) { showDialog( context: context, @@ -25,7 +25,7 @@ class _ProfileRunActionState extends State { try { final sshnp = await SSHNP.fromParams( - sshnpParams, + widget.params, atClient: AtClientManager.getInstance().atClient, sshrvGenerator: SSHRV.pureDart, ); @@ -64,7 +64,7 @@ class _ProfileRunActionState extends State { Widget build(BuildContext context) { return IconButton( onPressed: () async { - await onPressed(widget.params); + await onPressed(); }, icon: const Icon(Icons.play_arrow), ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart index 0c189e2d3..3f3d4c466 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart @@ -14,7 +14,7 @@ class ProfileTerminalAction extends StatefulWidget { } class _ProfileTerminalActionState extends State { - Future onPressed(SSHNPParams sshnpParams) async { + Future onPressed() async { if (mounted) { showDialog( context: context, @@ -25,7 +25,7 @@ class _ProfileTerminalActionState extends State { try { final sshnp = await SSHNP.fromParams( - sshnpParams, + widget.params, atClient: AtClientManager.getInstance().atClient, sshrvGenerator: SSHRV.pureDart, ); @@ -64,7 +64,7 @@ class _ProfileTerminalActionState extends State { Widget build(BuildContext context) { return IconButton( onPressed: () async { - await onPressed(widget.params); + await onPressed(); }, icon: const Icon(Icons.terminal), ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/custom_text_form_field.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/custom_text_form_field.dart index 87cca806d..41b3244ef 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/custom_text_form_field.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/custom_text_form_field.dart @@ -17,7 +17,7 @@ class CustomTextFormField extends StatelessWidget { final String? initialValue; final double width; final double height; - final void Function(String?)? onChanged; + final void Function(String)? onChanged; final String? Function(String?)? validator; @override diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index 796b987fe..b658c895e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -12,7 +12,6 @@ import 'package:sshnp_gui/src/utils/enum.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; import 'package:sshnp_gui/src/utils/validator.dart'; - class ProfileForm extends ConsumerStatefulWidget { const ProfileForm({super.key}); @@ -71,142 +70,201 @@ class _ProfileFormState extends ConsumerState { return SingleChildScrollView( child: Form( key: _formkey, - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomTextFormField( - initialValue: oldConfig.profileName, - labelText: strings.profileName, - onChanged: (value) { - newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(profileName: value!)); - }, - validator: Validator.validateRequiredField, - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.host, - labelText: strings.host, - onChanged: (value) => - newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(host: value!)), - validator: Validator.validateRequiredField, - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.port.toString(), - labelText: strings.port, - onChanged: (value) => - newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(port: int.parse(value!))), - validator: Validator.validateRequiredField, - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.sendSshPublicKey, - labelText: strings.sendSshPublicKey, - onChanged: (value) => - newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(sendSshPublicKey: value!)), - validator: Validator.validateRequiredField, - ), - gapH10, - Row( - children: [ - Text(strings.verbose), - gapW8, - Switch( - value: oldConfig.verbose, - onChanged: (newValue) { - setState(() { - newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(verbose: newValue)); - }); - }), - ], - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.remoteUsername, - labelText: strings.remoteUserName, - onChanged: (value) => - newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(remoteUsername: value!)), - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.rootDomain, - labelText: strings.rootDomain, - onChanged: (value) => - newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(rootDomain: value!)), - ), - gapH20, - ElevatedButton( - onPressed: () => onSubmit(oldConfig, newConfig), - child: Text(strings.submit), - ), - ]), - gapW12, - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomTextFormField( - initialValue: oldConfig.sshnpdAtSign, - labelText: strings.sshnpdAtSign, - onChanged: (value) => - newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(sshnpdAtSign: value!)), - validator: Validator.validateAtsignField, - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.device, - labelText: strings.device, - onChanged: (value) => - newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(device: value!)), - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.localPort.toString(), - labelText: strings.localPort, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(localPort: int.parse(value!))), - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.localSshOptions.join(','), - hintText: strings.localSshOptionsHint, - labelText: strings.localSshOptions, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(localSshOptions: value!.split(','))), - ), - gapH10, - Row( - children: [ - Text(strings.rsa), - gapW8, - Switch( - value: oldConfig.rsa, - onChanged: (newValue) { - setState(() { - newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(rsa: newValue)); - }); - }), - ], - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.atKeysFilePath, - labelText: strings.atKeysFilePath, - onChanged: (value) => - newConfig = SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(atKeysFilePath: value!)), - ), - gapH10, - CustomTextFormField( - initialValue: oldConfig.localSshdPort.toString(), - labelText: strings.localSshdPort, - onChanged: (value) => newConfig = - SSHNPPartialParams.merge(newConfig, SSHNPPartialParams(localSshdPort: int.parse(value!))), + Row( + children: [ + CustomTextFormField( + initialValue: oldConfig.profileName ?? '', + labelText: strings.profileName, + onChanged: (value) { + newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(profileName: value), + ); + }, + validator: Validator.validateRequiredField, + ), + gapW8, + CustomTextFormField( + initialValue: oldConfig.device, + labelText: strings.device, + onChanged: (value) => newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(device: value), + ), + ), + ], + ), + gapH10, + Row( + children: [ + CustomTextFormField( + initialValue: oldConfig.sshnpdAtSign ?? '', + labelText: strings.sshnpdAtSign, + onChanged: (value) => newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(sshnpdAtSign: value), + ), + validator: Validator.validateAtsignField, + ), + gapW8, + CustomTextFormField( + initialValue: oldConfig.host ?? '', + labelText: strings.host, + onChanged: (value) => newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(host: value), + ), + validator: Validator.validateRequiredField, + ), + ], + ), + gapH10, + Row( + children: [ + CustomTextFormField( + initialValue: oldConfig.sendSshPublicKey, + labelText: strings.sendSshPublicKey, + onChanged: (value) => newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(sendSshPublicKey: value), + ), + validator: Validator.validateRequiredField, + ), + gapW8, + Row( + children: [ + Text(strings.rsa), + gapW8, + Switch( + value: newConfig.rsa ?? oldConfig.rsa, + onChanged: (newValue) { + setState(() { + newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(rsa: newValue), + ); + }); + }), + ], + ), + ], + ), + gapH10, + Row( + children: [ + CustomTextFormField( + initialValue: oldConfig.remoteUsername ?? '', + labelText: strings.remoteUserName, + onChanged: (value) { + print('remoteUsername: $value'); + newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(remoteUsername: value), + ); + }), + gapW8, + CustomTextFormField( + initialValue: oldConfig.port.toString(), + labelText: strings.port, + onChanged: (value) => newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(port: int.tryParse(value)), + ), + validator: Validator.validateRequiredField, + ), + ], + ), + gapH10, + Row( + children: [ + CustomTextFormField( + initialValue: oldConfig.localPort.toString(), + labelText: strings.localPort, + onChanged: (value) => newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(localPort: int.tryParse(value)), + ), + ), + gapW8, + CustomTextFormField( + initialValue: oldConfig.localSshdPort.toString(), + labelText: strings.localSshdPort, + onChanged: (value) => newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(localSshdPort: int.tryParse(value)), + ), + ), + ], + ), + gapH10, + CustomTextFormField( + initialValue: oldConfig.localSshOptions.join(','), + hintText: strings.localSshOptionsHint, + labelText: strings.localSshOptions, + width: 192 * 2 + 10, + onChanged: (value) => newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(localSshOptions: value.split(',')), ), - gapH20, - TextButton( + ), + gapH10, + Row( + children: [ + CustomTextFormField( + initialValue: oldConfig.atKeysFilePath, + labelText: strings.atKeysFilePath, + onChanged: (value) => newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(atKeysFilePath: value), + ), + ), + gapW8, + CustomTextFormField( + initialValue: oldConfig.rootDomain, + labelText: strings.rootDomain, + onChanged: (value) => newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(rootDomain: value), + ), + ), + ], + ), + gapH10, + Row( + children: [ + Text(strings.verbose), + gapW8, + Switch( + value: newConfig.verbose ?? oldConfig.verbose, + onChanged: (newValue) { + setState(() { + newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(verbose: newValue), + ); + }); + }), + ], + ), + Row( + children: [ + ElevatedButton( + onPressed: () => onSubmit(oldConfig, newConfig), + child: Text(strings.submit), + ), + gapW8, + TextButton( onPressed: () { ref.read(currentNavIndexProvider.notifier).update((state) => AppRoute.home.index - 1); context.pushReplacementNamed(AppRoute.home.name); }, - child: Text(strings.cancel)) - ]), + child: Text(strings.cancel), + ), + ], + ), ], ), ), diff --git a/packages/sshnp_gui/lib/src/utils/app_router.dart b/packages/sshnp_gui/lib/src/utils/app_router.dart index 6c8b40f2b..43c91e1e4 100644 --- a/packages/sshnp_gui/lib/src/utils/app_router.dart +++ b/packages/sshnp_gui/lib/src/utils/app_router.dart @@ -11,7 +11,7 @@ import 'package:sshnp_gui/src/repository/navigation_service.dart'; enum AppRoute { onboarding, home, - newConnection, + profileForm, terminal, settings, } @@ -37,7 +37,7 @@ final goRouterProvider = Provider((ref) => GoRouter( ), GoRoute( path: 'new', - name: AppRoute.newConnection.name, + name: AppRoute.profileForm.name, pageBuilder: (context, state) => CustomTransitionPage( key: state.pageKey, child: const ProfileEditorScreen(), From 3b94ad7ec44b11f346b2d86f9b7ea083046cd870 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 16:24:19 +0800 Subject: [PATCH 21/95] chore: cleanup navigation controller --- .../controllers/authentication_controller.dart | 8 ++++---- .../lib/src/controllers/home_screen_actions.dart | 0 .../lib/src/controllers/minor_providers.dart | 7 ------- .../src/controllers/nav_index_controller.dart | 16 ++++++++++++++++ .../presentation/screens/settings_screen.dart | 4 ++-- .../presentation/screens/terminal_screen.dart | 2 +- .../dialog/sshnp_result_alert_dialog.dart | 4 ++-- .../home_screen_actions/new_profile_action.dart | 4 ++-- .../widgets/navigation/app_navigation_rail.dart | 9 ++++----- .../profile_bar/actions/profile_edit_action.dart | 4 ++-- .../widgets/profile_form/profile_form.dart | 6 +++--- .../widgets/utility/custom_snack_bar.dart | 8 ++++---- .../repository/authentication_repository.dart | 6 +++--- ...n_service.dart => navigation_repository.dart} | 4 +--- packages/sshnp_gui/lib/src/utils/app_router.dart | 4 ++-- 15 files changed, 46 insertions(+), 40 deletions(-) delete mode 100644 packages/sshnp_gui/lib/src/controllers/home_screen_actions.dart delete mode 100644 packages/sshnp_gui/lib/src/controllers/minor_providers.dart create mode 100644 packages/sshnp_gui/lib/src/controllers/nav_index_controller.dart rename packages/sshnp_gui/lib/src/repository/{navigation_service.dart => navigation_repository.dart} (52%) diff --git a/packages/sshnp_gui/lib/src/controllers/authentication_controller.dart b/packages/sshnp_gui/lib/src/controllers/authentication_controller.dart index 887ea5342..684ae3ee1 100644 --- a/packages/sshnp_gui/lib/src/controllers/authentication_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/authentication_controller.dart @@ -2,6 +2,10 @@ import 'package:at_contact/at_contact.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnp_gui/src/repository/authentication_repository.dart'; +/// A provider that exposes the [AuthenticationController] to the app. +final authenticationController = StateNotifierProvider?>>( + (ref) => AuthenticationController(ref: ref)); + /// A controller class that controls the UI update when the [AuthenticationRepository] methods are called. class AuthenticationController extends StateNotifier?>> { final Ref ref; @@ -29,7 +33,3 @@ class AuthenticationController extends StateNotifier?>> return await ref.watch(authenticationRepositoryProvider).getCurrentAtContact(); } } - -/// A provider that exposes the [AuthenticationController] to the app. -final authenticationController = StateNotifierProvider?>>( - (ref) => AuthenticationController(ref: ref)); diff --git a/packages/sshnp_gui/lib/src/controllers/home_screen_actions.dart b/packages/sshnp_gui/lib/src/controllers/home_screen_actions.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/sshnp_gui/lib/src/controllers/minor_providers.dart b/packages/sshnp_gui/lib/src/controllers/minor_providers.dart deleted file mode 100644 index d54d43506..000000000 --- a/packages/sshnp_gui/lib/src/controllers/minor_providers.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final currentNavIndexProvider = StateProvider((ref) => 0); - -final terminalSSHCommandProvider = StateProvider( - (ref) => '', -); diff --git a/packages/sshnp_gui/lib/src/controllers/nav_index_controller.dart b/packages/sshnp_gui/lib/src/controllers/nav_index_controller.dart new file mode 100644 index 000000000..fb536517d --- /dev/null +++ b/packages/sshnp_gui/lib/src/controllers/nav_index_controller.dart @@ -0,0 +1,16 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnp_gui/src/utils/app_router.dart'; + +final navIndexProvider = AutoDisposeNotifierProvider(NavIndexController.new); + +class NavIndexController extends AutoDisposeNotifier { + @override + int build() => 0; + + void goTo(AppRoute route) => state = route.index - 1; + void goToIndex(int index) => state = index; +} + +final terminalSSHCommandProvider = StateProvider( + (ref) => '', +); diff --git a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart index 15cc81bfd..b2141aedb 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart @@ -6,7 +6,7 @@ import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rai import 'package:sshnp_gui/src/presentation/widgets/utility/reset_app_button.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/settings_button.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/switch_atsign.dart'; -import 'package:sshnp_gui/src/repository/navigation_service.dart'; +import 'package:sshnp_gui/src/repository/navigation_repository.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -73,7 +73,7 @@ class SettingsScreen extends StatelessWidget { title: strings.switchAtsign, onTap: () async { await showModalBottomSheet( - context: NavigationService.navKey.currentContext!, + context: NavigationRepository.navKey.currentContext!, builder: (context) => const AtSignBottomSheet()); }, ), diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index 9322ff6a1..377b68729 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; import 'package:xterm/xterm.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart index 0f844c707..68b4c0bec 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; class SSHNPResultAlertDialog extends ConsumerWidget { @@ -27,7 +27,7 @@ class SSHNPResultAlertDialog extends ConsumerWidget { required WidgetRef ref, required BuildContext context, }) { - ref.read(currentNavIndexProvider.notifier).update((state) => AppRoute.terminal.index - 1); + ref.read(navIndexProvider.notifier).goTo(AppRoute.terminal); ref.read(terminalSSHCommandProvider.notifier).update((state) => result); context.pushReplacementNamed(AppRoute.terminal.name); } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart index a017068e8..1d6fe68c5 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; @@ -23,7 +23,7 @@ class _NewProfileActionState extends ConsumerState { ), ); // change value to 1 to update navigation rail selcted icon. - ref.watch(currentNavIndexProvider.notifier).update((_) => AppRoute.profileForm.index - 1); + ref.watch(navIndexProvider.notifier).goTo(AppRoute.profileForm); context.replaceNamed( AppRoute.profileForm.name, ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart index 24fedcfe0..b320b292e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; class AppNavigationRail extends ConsumerWidget { @@ -10,7 +10,7 @@ class AppNavigationRail extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final currentIndex = ref.watch(currentNavIndexProvider); + final currentIndex = ref.watch(navIndexProvider); return NavigationRail( destinations: [ @@ -35,10 +35,9 @@ class AppNavigationRail extends ConsumerWidget { label: const Text(''), ), ], - selectedIndex: ref.watch(currentNavIndexProvider), + selectedIndex: ref.watch(navIndexProvider), onDestinationSelected: (int selectedIndex) { - ref.read(currentNavIndexProvider.notifier).update((state) => selectedIndex); - + ref.read(navIndexProvider.notifier).goToIndex(selectedIndex); switch (selectedIndex) { case 0: context.goNamed(AppRoute.home.name); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart index 81e415df0..a6827ed59 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; @@ -26,7 +26,7 @@ class _ProfileEditActionState extends ConsumerState { ), ); // change value to 1 to update navigation rail selcted icon. - ref.watch(currentNavIndexProvider.notifier).update((_) => AppRoute.profileForm.index - 1); + ref.watch(navIndexProvider.notifier).goTo(AppRoute.profileForm); context.replaceNamed( AppRoute.profileForm.name, ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index b658c895e..25acb8eb7 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -4,7 +4,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/minor_providers.dart'; +import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_form/custom_text_form_field.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; @@ -51,7 +51,7 @@ class _ProfileFormState extends ConsumerState { await controller.create(config); } if (context.mounted) { - ref.read(currentNavIndexProvider.notifier).update((state) => AppRoute.home.index - 1); + ref.read(navIndexProvider.notifier).goTo(AppRoute.home); context.pushReplacementNamed(AppRoute.home.name); } } @@ -258,7 +258,7 @@ class _ProfileFormState extends ConsumerState { gapW8, TextButton( onPressed: () { - ref.read(currentNavIndexProvider.notifier).update((state) => AppRoute.home.index - 1); + ref.read(navIndexProvider.notifier).goTo(AppRoute.home); context.pushReplacementNamed(AppRoute.home.name); }, child: Text(strings.cancel), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/utility/custom_snack_bar.dart b/packages/sshnp_gui/lib/src/presentation/widgets/utility/custom_snack_bar.dart index a387f02c1..b2d9525db 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/utility/custom_snack_bar.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/utility/custom_snack_bar.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:sshnp_gui/src/repository/navigation_service.dart'; +import 'package:sshnp_gui/src/repository/navigation_repository.dart'; class CustomSnackBar { static void error({ required String content, }) { - final context = NavigationService.navKey.currentContext!; + final context = NavigationRepository.navKey.currentContext!; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( content, @@ -18,7 +18,7 @@ class CustomSnackBar { static void success({ required String content, }) { - final context = NavigationService.navKey.currentContext!; + final context = NavigationRepository.navKey.currentContext!; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( content, @@ -33,7 +33,7 @@ class CustomSnackBar { SnackBarAction? action, Duration duration = const Duration(seconds: 2), }) { - final context = NavigationService.navKey.currentContext!; + final context = NavigationRepository.navKey.currentContext!; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( content, diff --git a/packages/sshnp_gui/lib/src/repository/authentication_repository.dart b/packages/sshnp_gui/lib/src/repository/authentication_repository.dart index 1f66ff409..f01fabd31 100644 --- a/packages/sshnp_gui/lib/src/repository/authentication_repository.dart +++ b/packages/sshnp_gui/lib/src/repository/authentication_repository.dart @@ -12,7 +12,7 @@ import 'package:go_router/go_router.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; -import 'package:sshnp_gui/src/repository/navigation_service.dart'; +import 'package:sshnp_gui/src/repository/navigation_repository.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; /// A singleton that makes all the network calls to the @platform. @@ -66,7 +66,7 @@ class AuthenticationRepository { /// Signs user into the @platform. void handleSwitchAtsign(String? atsign) async { final result = await AtOnboarding.onboard( - context: NavigationService.navKey.currentContext!, + context: NavigationRepository.navKey.currentContext!, isSwitchingAtsign: true, atsign: atsign, config: AtOnboardingConfig( @@ -82,7 +82,7 @@ class AuthenticationRepository { // DudeService.getInstance().monitorNotifications(NavigationService.navKey.currentContext!); // AtClientManager.getInstance().atClient.syncService.addProgressListener(MySyncProgressListener()); initializeContactsService(rootDomain: AtEnv.rootDomain); - final context = NavigationService.navKey.currentContext!; + final context = NavigationRepository.navKey.currentContext!; if (context.mounted) { context.goNamed(AppRoute.home.name); } diff --git a/packages/sshnp_gui/lib/src/repository/navigation_service.dart b/packages/sshnp_gui/lib/src/repository/navigation_repository.dart similarity index 52% rename from packages/sshnp_gui/lib/src/repository/navigation_service.dart rename to packages/sshnp_gui/lib/src/repository/navigation_repository.dart index 5dcea19cb..06b073c04 100644 --- a/packages/sshnp_gui/lib/src/repository/navigation_service.dart +++ b/packages/sshnp_gui/lib/src/repository/navigation_repository.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -class NavigationService { +class NavigationRepository { static GlobalKey navKey = GlobalKey(); - - static GlobalKey nesteNavKey = GlobalKey(); } diff --git a/packages/sshnp_gui/lib/src/utils/app_router.dart b/packages/sshnp_gui/lib/src/utils/app_router.dart index 43c91e1e4..102cd9c5d 100644 --- a/packages/sshnp_gui/lib/src/utils/app_router.dart +++ b/packages/sshnp_gui/lib/src/utils/app_router.dart @@ -6,7 +6,7 @@ import 'package:sshnp_gui/src/presentation/screens/onboarding_screen.dart'; import 'package:sshnp_gui/src/presentation/screens/profile_editor_screen.dart'; import 'package:sshnp_gui/src/presentation/screens/settings_screen.dart'; import 'package:sshnp_gui/src/presentation/screens/terminal_screen.dart'; -import 'package:sshnp_gui/src/repository/navigation_service.dart'; +import 'package:sshnp_gui/src/repository/navigation_repository.dart'; enum AppRoute { onboarding, @@ -17,7 +17,7 @@ enum AppRoute { } final goRouterProvider = Provider((ref) => GoRouter( - navigatorKey: NavigationService.navKey, + navigatorKey: NavigationRepository.navKey, initialLocation: '/', debugLogDiagnostics: false, routes: [ From c41ba32ec12812d758330fdaf638efe3c93219a7 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 16:26:57 +0800 Subject: [PATCH 22/95] chore: cleanup naming for params controllers --- ...ller.dart => sshnp_params_controller.dart} | 17 +++++++------- .../src/presentation/screens/home_screen.dart | 4 ++-- .../widgets/dialog/delete_alert_dialog.dart | 23 +++++-------------- .../new_profile_action.dart | 4 ++-- .../actions/profile_edit_action.dart | 4 ++-- .../widgets/profile_bar/profile_bar.dart | 4 ++-- .../widgets/profile_form/profile_form.dart | 11 +++++---- 7 files changed, 29 insertions(+), 38 deletions(-) rename packages/sshnp_gui/lib/src/controllers/{sshnp_config_controller.dart => sshnp_params_controller.dart} (82%) diff --git a/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart b/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart similarity index 82% rename from packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart rename to packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart index 70afef729..8efe3a115 100644 --- a/packages/sshnp_gui/lib/src/controllers/sshnp_config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart @@ -6,16 +6,17 @@ import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; /// Controller instance for the current [SSHNPParams] being edited -final currentParamsController = AutoDisposeNotifierProvider( - CurrentSSHNPParamsController.new); +final sshnpParamsController = + AutoDisposeNotifierProvider(SSHNPParamsController.new); /// Controller instance for the list of all profileNames for each config file -final paramsListController = +final sshnpParamsListController = AutoDisposeAsyncNotifierProvider>(SSHNPParamsListController.new); /// Controller instance for the family of [SSHNPParams] controllers -final paramsFamilyController = AutoDisposeAsyncNotifierProviderFamily( - SSHNPParamsFamilyController.new); +final sshnpParamsFamilyController = + AutoDisposeAsyncNotifierProviderFamily( + SSHNPParamsFamilyController.new); /// Holder model for the current [SSHNPParams] being edited class CurrentSSHNPParamsModel { @@ -26,7 +27,7 @@ class CurrentSSHNPParamsModel { } /// Controller for the current [SSHNPParams] being edited -class CurrentSSHNPParamsController extends AutoDisposeNotifier { +class SSHNPParamsController extends AutoDisposeNotifier { @override CurrentSSHNPParamsModel build() { return CurrentSSHNPParamsModel( @@ -60,7 +61,7 @@ class SSHNPParamsFamilyController extends AutoDisposeFamilyAsyncNotifier create(SSHNPParams params) async { await params.toFile(); state = AsyncValue.data(params); - ref.read(paramsListController.notifier).add(params.profileName!); + ref.read(sshnpParamsListController.notifier).add(params.profileName!); } Future edit(SSHNPParams params) async { @@ -71,7 +72,7 @@ class SSHNPParamsFamilyController extends AutoDisposeFamilyAsyncNotifier delete() async { await state.value?.deleteFile(); state = const AsyncError('File deleted', StackTrace.empty); - ref.read(paramsListController.notifier).remove(state.value!.profileName!); + ref.read(sshnpParamsListController.notifier).remove(state.value!.profileName!); } } diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index b8885742d..b810a37aa 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_actions.dart'; import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar.dart'; @@ -22,7 +22,7 @@ class _HomeScreenState extends ConsumerState { // * Getting the AtClientManager instance to use below final strings = AppLocalizations.of(context)!; - final profileNames = ref.watch(paramsListController); + final profileNames = ref.watch(sshnpParamsListController); return Scaffold( body: SafeArea( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart index 83f92a21c..ec164e113 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; class DeleteAlertDialog extends ConsumerWidget { @@ -31,10 +31,7 @@ class DeleteAlertDialog extends ConsumerWidget { children: [ TextSpan( text: strings.note, - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith(fontWeight: FontWeight.w700), + style: Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w700), ), TextSpan( text: strings.noteMessage, @@ -48,17 +45,11 @@ class DeleteAlertDialog extends ConsumerWidget { OutlinedButton( onPressed: () => Navigator.of(context).pop(false), child: Text(strings.cancelButton, - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith(decoration: TextDecoration.underline)), + style: Theme.of(context).textTheme.bodyLarge!.copyWith(decoration: TextDecoration.underline)), ), ElevatedButton( onPressed: () async { - await ref - .read(paramsFamilyController(sshnpParams.profileName!) - .notifier) - .delete(); + await ref.read(sshnpParamsFamilyController(sshnpParams.profileName!).notifier).delete(); if (context.mounted) Navigator.of(context).pop(); }, style: Theme.of(context).elevatedButtonTheme.style!.copyWith( @@ -66,10 +57,8 @@ class DeleteAlertDialog extends ConsumerWidget { ), child: Text( strings.deleteButton, - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith(fontWeight: FontWeight.w700, color: Colors.white), + style: + Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w700, color: Colors.white), ), ) ], diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart index 1d6fe68c5..a0f22d994 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; @@ -16,7 +16,7 @@ class NewProfileAction extends ConsumerStatefulWidget { class _NewProfileActionState extends ConsumerState { void onPressed() { // Change value to update to trigger the update functionality on the new connection form. - ref.watch(currentParamsController.notifier).setState( + ref.watch(sshnpParamsController.notifier).setState( CurrentSSHNPParamsModel( profileName: '', configFileWriteState: ConfigFileWriteState.create, diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart index a6827ed59..05d81f631 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; @@ -19,7 +19,7 @@ class ProfileEditAction extends ConsumerStatefulWidget { class _ProfileEditActionState extends ConsumerState { void onPressed() { // Change value to update to trigger the update functionality on the new connection form. - ref.watch(currentParamsController.notifier).setState( + ref.watch(sshnpParamsController.notifier).setState( CurrentSSHNPParamsModel( profileName: widget.params.profileName!, configFileWriteState: ConfigFileWriteState.update, diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart index 6088f3c17..34b496610 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_bar/actions/profile_actions.dart'; class ProfileBar extends ConsumerStatefulWidget { @@ -14,7 +14,7 @@ class ProfileBar extends ConsumerStatefulWidget { class _ProfileBarState extends ConsumerState { @override Widget build(BuildContext context) { - final controller = ref.watch(paramsFamilyController(widget.profileName)); + final controller = ref.watch(sshnpParamsFamilyController(widget.profileName)); return controller.when( error: (error, stackTrace) => Container(), loading: () => const LinearProgressIndicator(), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index 25acb8eb7..760b5d50d 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_config_controller.dart'; +import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_form/custom_text_form_field.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; @@ -31,7 +31,8 @@ class _ProfileFormState extends ConsumerState { void onSubmit(SSHNPParams oldConfig, SSHNPPartialParams newConfig) async { if (_formkey.currentState!.validate()) { _formkey.currentState!.save(); - final controller = ref.read(paramsFamilyController(newConfig.profileName ?? oldConfig.profileName!).notifier); + final controller = + ref.read(sshnpParamsFamilyController(newConfig.profileName ?? oldConfig.profileName!).notifier); bool overwrite = currentProfile.configFileWriteState == ConfigFileWriteState.update; bool rename = newConfig.profileName.isNotNull && newConfig.profileName!.isNotEmpty && @@ -41,7 +42,7 @@ class _ProfileFormState extends ConsumerState { SSHNPParams config = SSHNPParams.merge(oldConfig, newConfig); if (rename) { // delete old config file and write the new one - await ref.read(paramsFamilyController(oldConfig.profileName!).notifier).delete(); + await ref.read(sshnpParamsFamilyController(oldConfig.profileName!).notifier).delete(); await controller.create(config); } else if (overwrite) { // overwrite the existing file @@ -60,9 +61,9 @@ class _ProfileFormState extends ConsumerState { @override Widget build(BuildContext context) { final strings = AppLocalizations.of(context)!; - currentProfile = ref.watch(currentParamsController); + currentProfile = ref.watch(sshnpParamsController); - final asyncOldConfig = ref.watch(paramsFamilyController(currentProfile.profileName)); + final asyncOldConfig = ref.watch(sshnpParamsFamilyController(currentProfile.profileName)); return asyncOldConfig.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text(error.toString())), From 40362857ca73dd03e63eb720d0fa60c48e2ec566 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 16:31:49 +0800 Subject: [PATCH 23/95] chore: reorder sshnp params controller --- .../controllers/sshnp_params_controller.dart | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart b/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart index 8efe3a115..24bb4d9d2 100644 --- a/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart @@ -41,6 +41,27 @@ class SSHNPParamsController extends AutoDisposeNotifier } } +/// Controller for the list of all profileNames for each config file +class SSHNPParamsListController extends AutoDisposeAsyncNotifier> { + @override + Future> build() async { + return (await SSHNPParams.listFiles()).toSet(); + } + + Future refresh() async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() => build()); + } + + void add(String profileName) { + state = AsyncValue.data({...state.value ?? [], profileName}); + } + + void remove(String profileName) { + state = AsyncData(state.value?.difference({profileName}) ?? {}); + } +} + /// Controller for the family of [SSHNPParams] controllers class SSHNPParamsFamilyController extends AutoDisposeFamilyAsyncNotifier { @override @@ -75,24 +96,3 @@ class SSHNPParamsFamilyController extends AutoDisposeFamilyAsyncNotifier> { - @override - Future> build() async { - return (await SSHNPParams.listFiles()).toSet(); - } - - Future refresh() async { - state = const AsyncLoading(); - state = await AsyncValue.guard(() => build()); - } - - void add(String profileName) { - state = AsyncValue.data({...state.value ?? [], profileName}); - } - - void remove(String profileName) { - state = AsyncData(state.value?.difference({profileName}) ?? {}); - } -} From 955cf3a9b5a3656e4f38bf4d0dfd6d47d210d0d8 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 16:35:55 +0800 Subject: [PATCH 24/95] chore: cleanup sshnp params controller --- .../controllers/sshnp_params_controller.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart b/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart index 24bb4d9d2..9c8536c3a 100644 --- a/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart @@ -5,18 +5,21 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; -/// Controller instance for the current [SSHNPParams] being edited -final sshnpParamsController = - AutoDisposeNotifierProvider(SSHNPParamsController.new); +/// A provider that exposes the [SSHNPParamsController] to the app. +final sshnpParamsController = AutoDisposeNotifierProvider( + SSHNPParamsController.new, +); -/// Controller instance for the list of all profileNames for each config file -final sshnpParamsListController = - AutoDisposeAsyncNotifierProvider>(SSHNPParamsListController.new); +/// A provider that exposes the [SSHNPParamsListController] to the app. +final sshnpParamsListController = AutoDisposeAsyncNotifierProvider>( + SSHNPParamsListController.new, +); -/// Controller instance for the family of [SSHNPParams] controllers +/// A provider that exposes the [SSHNPParamsFamilyController] to the app. final sshnpParamsFamilyController = AutoDisposeAsyncNotifierProviderFamily( - SSHNPParamsFamilyController.new); + SSHNPParamsFamilyController.new, +); /// Holder model for the current [SSHNPParams] being edited class CurrentSSHNPParamsModel { From 06fccfc16683d31d3c9329fd077880dadf8c5fd3 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 16:36:03 +0800 Subject: [PATCH 25/95] feat: add terminal session controller --- .../terminal_session_controller.dart | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart new file mode 100644 index 000000000..2ca26d63b --- /dev/null +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -0,0 +1,42 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:xterm/xterm.dart'; + +/// A provider that exposes the [TerminalSessionController] to the app. +final terminalSessionController = AutoDisposeNotifierProvider( + TerminalSessionController.new, +); + +/// A provider that exposes the [TerminalSessionListController] to the app. +final terminalSessionListController = AutoDisposeNotifierProvider>( + TerminalSessionListController.new, +); + +/// A provider that exposes the [TerminalSessionFamilyController] to the app. +final terminalSessionFamilyController = + AutoDisposeNotifierProviderFamily( + TerminalSessionFamilyController.new, +); + +/// Controller for the id of the currently active terminal session +class TerminalSessionController extends AutoDisposeNotifier { + @override + String build() => ''; + + void setState(String sessionId) { + state = sessionId; + } +} + +/// Controller for the list of all terminal session ids +class TerminalSessionListController extends AutoDisposeNotifier> { + @override + Set build() => {}; +} + +/// Controller for the family of terminal session [TerminalController]s +class TerminalSessionFamilyController extends AutoDisposeFamilyNotifier { + @override + TerminalController build(String arg) { + return TerminalController(); + } +} From 778ef9d900f5ec44fa9b256f7e621a2891f1257f Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 18:30:23 +0800 Subject: [PATCH 26/95] chore: cleanup some bugs --- .../sshnoports/lib/sshnp/sshnp_result.dart | 46 ++++++----- .../src/controllers/nav_index_controller.dart | 16 ---- .../src/controllers/nav_route_controller.dart | 11 +++ .../terminal_session_controller.dart | 76 ++++++++++++++++-- .../presentation/screens/terminal_screen.dart | 44 +++-------- .../dialog/sshnp_result_alert_dialog.dart | 9 ++- .../new_profile_action.dart | 4 +- .../navigation/app_navigation_rail.dart | 79 ++++++++++--------- .../actions/profile_edit_action.dart | 4 +- .../actions/profile_terminal_action.dart | 42 ++++++---- .../widgets/profile_form/profile_form.dart | 6 +- packages/sshnp_gui/pubspec.yaml | 1 + 12 files changed, 194 insertions(+), 144 deletions(-) delete mode 100644 packages/sshnp_gui/lib/src/controllers/nav_index_controller.dart create mode 100644 packages/sshnp_gui/lib/src/controllers/nav_route_controller.dart diff --git a/packages/sshnoports/lib/sshnp/sshnp_result.dart b/packages/sshnoports/lib/sshnp/sshnp_result.dart index a0fe9ccfb..69982cb29 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_result.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_result.dart @@ -2,12 +2,14 @@ part of 'sshnp.dart'; abstract class SSHNPResult {} -const _optionsWithPrivateKey = [ - '-o StrictHostKeyChecking=accept-new', - '-o IdentitiesOnly=yes' -]; +abstract class SSHNPCommandResult implements SSHNPResult { + String get command; + List get args; +} + +const _optionsWithPrivateKey = ['-o StrictHostKeyChecking=accept-new', '-o IdentitiesOnly=yes']; -class SSHNPFailed extends SSHNPResult { +class SSHNPFailed implements SSHNPResult { final String message; final Object? exception; final StackTrace? stackTrace; @@ -20,8 +22,9 @@ class SSHNPFailed extends SSHNPResult { } } -class SSHCommand extends SSHNPResult { - static const String command = 'ssh'; +class SSHCommand implements SSHNPCommandResult { + @override + final String command = 'ssh'; final int localPort; final String? remoteUsername; @@ -35,31 +38,26 @@ class SSHCommand extends SSHNPResult { required this.remoteUsername, required this.host, this.privateKeyFileName, - }) : sshOptions = (shouldIncludePrivateKey(privateKeyFileName) - ? _optionsWithPrivateKey - : []); + }) : sshOptions = (shouldIncludePrivateKey(privateKeyFileName) ? _optionsWithPrivateKey : []); static bool shouldIncludePrivateKey(String? privateKeyFileName) => - privateKeyFileName != null && - privateKeyFileName.isNotEmpty; + privateKeyFileName != null && privateKeyFileName.isNotEmpty; + + @override + List get args => [ + '-p $localPort', + ...sshOptions, + if (remoteUsername != null) '$remoteUsername@$host', + if (remoteUsername == null) host, + if (shouldIncludePrivateKey(privateKeyFileName)) '-i $privateKeyFileName', + ]; @override String toString() { final sb = StringBuffer(); sb.write(command); sb.write(' '); - sb.write('-p $localPort'); - sb.write(' '); - sb.write(sshOptions.join(' ')); - sb.write(' '); - if (remoteUsername != null) { - sb.write('$remoteUsername@'); - } - sb.write(host); - if (shouldIncludePrivateKey(privateKeyFileName)) { - sb.write(' '); - sb.write('-i $privateKeyFileName'); - } + sb.write(args.join(' ')); return sb.toString(); } } diff --git a/packages/sshnp_gui/lib/src/controllers/nav_index_controller.dart b/packages/sshnp_gui/lib/src/controllers/nav_index_controller.dart deleted file mode 100644 index fb536517d..000000000 --- a/packages/sshnp_gui/lib/src/controllers/nav_index_controller.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sshnp_gui/src/utils/app_router.dart'; - -final navIndexProvider = AutoDisposeNotifierProvider(NavIndexController.new); - -class NavIndexController extends AutoDisposeNotifier { - @override - int build() => 0; - - void goTo(AppRoute route) => state = route.index - 1; - void goToIndex(int index) => state = index; -} - -final terminalSSHCommandProvider = StateProvider( - (ref) => '', -); diff --git a/packages/sshnp_gui/lib/src/controllers/nav_route_controller.dart b/packages/sshnp_gui/lib/src/controllers/nav_route_controller.dart new file mode 100644 index 000000000..850e16f33 --- /dev/null +++ b/packages/sshnp_gui/lib/src/controllers/nav_route_controller.dart @@ -0,0 +1,11 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnp_gui/src/utils/app_router.dart'; + +final navRouteController = AutoDisposeNotifierProvider(NavRouteController.new); + +class NavRouteController extends AutoDisposeNotifier { + @override + AppRoute build() => AppRoute.home; + + void goTo(AppRoute route) => state = route; +} diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart index 2ca26d63b..da1b2c3be 100644 --- a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -1,4 +1,9 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:uuid/uuid.dart'; import 'package:xterm/xterm.dart'; /// A provider that exposes the [TerminalSessionController] to the app. @@ -13,7 +18,7 @@ final terminalSessionListController = AutoDisposeNotifierProvider( + AutoDisposeNotifierProviderFamily( TerminalSessionFamilyController.new, ); @@ -22,8 +27,10 @@ class TerminalSessionController extends AutoDisposeNotifier { @override String build() => ''; - void setState(String sessionId) { - state = sessionId; + String createSession() { + state = const Uuid().v4(); + ref.read(terminalSessionListController.notifier).add(state); + return state; } } @@ -31,12 +38,69 @@ class TerminalSessionController extends AutoDisposeNotifier { class TerminalSessionListController extends AutoDisposeNotifier> { @override Set build() => {}; + + void add(String sessionId) { + state.add(sessionId); + } + + void remove(String sessionId) { + state.remove(sessionId); + } +} + +class TerminalSession { + final String sessionId; + final Terminal terminal; + + late Pty pty; + bool isRunning = false; + String? command; + List args = const []; + + TerminalSession(this.sessionId) : terminal = Terminal(); } /// Controller for the family of terminal session [TerminalController]s -class TerminalSessionFamilyController extends AutoDisposeFamilyNotifier { +class TerminalSessionFamilyController extends AutoDisposeFamilyNotifier { @override - TerminalController build(String arg) { - return TerminalController(); + TerminalSession build(String arg) { + return TerminalSession(arg); + } + + void setProcess({String? command, List args = const []}) { + state.command = command; + state.args = args; + } + + void startProcess() { + state.isRunning = true; + print('running ${state.command!} ${state.args.join(' ')}'); + state.pty = Pty.start( + state.command ?? Platform.environment['SHELL'] ?? 'bash', + arguments: state.args, + columns: state.terminal.viewWidth, + rows: state.terminal.viewHeight, + ); + + // Write stdout of the process to the terminal + state.pty.output.cast>().transform(const Utf8Decoder()).listen(state.terminal.write); + + // Write exit code of the process to the terminal + state.pty.exitCode.then((code) => state.terminal.write('The process exited with code: $code')); + + // Write the terminal output to the process + state.terminal.onOutput = (data) { + state.pty.write(const Utf8Encoder().convert(data)); + }; + + // Resize the terminal when the window is resized + state.terminal.onResize = (w, h, pw, ph) { + state.pty.resize(h, w); + }; + } + + void killProcess() { + state.pty.kill(); + state.isRunning = false; } } diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index 377b68729..329635e0b 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -5,7 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; +import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; +import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; import 'package:xterm/xterm.dart'; @@ -19,46 +20,21 @@ class TerminalScreen extends ConsumerStatefulWidget { } class _TerminalScreenState extends ConsumerState { - var terminal = Terminal(); final terminalController = TerminalController(); late final Pty pty; @override void initState() { super.initState(); + final sessionId = ref.read(terminalSessionController); + print('sessionId in initState: $sessionId'); + + final sessionController = ref.read(terminalSessionFamilyController(sessionId).notifier); WidgetsBinding.instance.endOfFrame.then((value) { - if (mounted) _startPty(); + sessionController.startProcess(); }); } - void _startPty({String? command, List? args}) { - pty = Pty.start( - command ?? Platform.environment['SHELL'] ?? 'bash', - arguments: args ?? [], - columns: terminal.viewWidth, - rows: terminal.viewHeight, - ); - - pty.output.cast>().transform(const Utf8Decoder()).listen(terminal.write); - - pty.exitCode.then( - (code) => terminal.write('the process exited with code $code'), - ); - - terminal.onOutput = (data) { - pty.write(const Utf8Encoder().convert(data)); - }; - - terminal.onResize = (w, h, pw, ph) { - pty.resize(h, w); - }; - - // write ssh result command to terminal - pty.write(const Utf8Encoder().convert(ref.watch(terminalSSHCommandProvider))); - // reset provider - ref.watch(terminalSSHCommandProvider.notifier).update((state) => ''); - } - @override void dispose() { terminalController.dispose(); @@ -68,7 +44,9 @@ class _TerminalScreenState extends ConsumerState { @override Widget build(BuildContext context) { // * Getting the AtClientManager instance to use below - + final sessionId = ref.watch(terminalSessionController); + print('sessionId in build: $sessionId'); + final terminalSession = ref.watch(terminalSessionFamilyController(sessionId)); return Scaffold( body: SafeArea( child: Row( @@ -85,7 +63,7 @@ class _TerminalScreenState extends ConsumerState { gapH24, Expanded( child: TerminalView( - terminal, + terminalSession.terminal, controller: terminalController, autofocus: true, ), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart index 68b4c0bec..05e05717f 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart @@ -3,7 +3,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; +import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; +import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; class SSHNPResultAlertDialog extends ConsumerWidget { @@ -27,8 +28,10 @@ class SSHNPResultAlertDialog extends ConsumerWidget { required WidgetRef ref, required BuildContext context, }) { - ref.read(navIndexProvider.notifier).goTo(AppRoute.terminal); - ref.read(terminalSSHCommandProvider.notifier).update((state) => result); + final sessionId = ref.read(terminalSessionController.notifier).createSession(); + final sessionController = ref.read(terminalSessionFamilyController(sessionId).notifier); + sessionController.setProcess(); + ref.read(navRouteController.notifier).goTo(AppRoute.terminal); context.pushReplacementNamed(AppRoute.terminal.name); } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart index a0f22d994..6c07c95a2 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; +import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; @@ -23,7 +23,7 @@ class _NewProfileActionState extends ConsumerState { ), ); // change value to 1 to update navigation rail selcted icon. - ref.watch(navIndexProvider.notifier).goTo(AppRoute.profileForm); + ref.watch(navRouteController.notifier).goTo(AppRoute.profileForm); context.replaceNamed( AppRoute.profileForm.name, ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart index b320b292e..56efe2b3a 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart @@ -2,53 +2,54 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; +import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; class AppNavigationRail extends ConsumerWidget { const AppNavigationRail({super.key}); + static const indexedRoutes = [ + AppRoute.home, + AppRoute.terminal, + AppRoute.settings, + ]; + + static int getRouteIndex(AppRoute route) { + return indexedRoutes.indexOf(route); + } + + static var activatedIcons = [ + SvgPicture.asset('assets/images/nav_icons/home_selected.svg'), + SvgPicture.asset('assets/images/nav_icons/pican_selected.svg'), + SvgPicture.asset('assets/images/nav_icons/settings_selected.svg') + ]; + + static var deactivatedIcons = [ + SvgPicture.asset('assets/images/nav_icons/home_unselected.svg'), + SvgPicture.asset('assets/images/nav_icons/pican_unselected.svg'), + SvgPicture.asset('assets/images/nav_icons/settings_unselected.svg'), + ]; + @override Widget build(BuildContext context, WidgetRef ref) { - final currentIndex = ref.watch(navIndexProvider); + final currentIndex = getRouteIndex(ref.watch(navRouteController)); return NavigationRail( - destinations: [ - NavigationRailDestination( - icon: currentIndex == 0 - ? SvgPicture.asset('assets/images/nav_icons/home_selected.svg') - : SvgPicture.asset( - 'assets/images/nav_icons/home_unselected.svg', - ), - label: const Text(''), - ), - NavigationRailDestination( - icon: currentIndex == 1 - ? SvgPicture.asset('assets/images/nav_icons/pican_selected.svg') - : SvgPicture.asset('assets/images/nav_icons/pican_unselected.svg'), - label: const Text(''), - ), - NavigationRailDestination( - icon: currentIndex == 2 - ? SvgPicture.asset('assets/images/nav_icons/settings_selected.svg') - : SvgPicture.asset('assets/images/nav_icons/settings_unselected.svg'), - label: const Text(''), - ), - ], - selectedIndex: ref.watch(navIndexProvider), - onDestinationSelected: (int selectedIndex) { - ref.read(navIndexProvider.notifier).goToIndex(selectedIndex); - switch (selectedIndex) { - case 0: - context.goNamed(AppRoute.home.name); - break; - case 1: - context.goNamed(AppRoute.terminal.name); - break; - case 2: - context.goNamed(AppRoute.settings.name); - break; - } - }); + destinations: indexedRoutes + .map( + (i) => NavigationRailDestination( + icon: (currentIndex == getRouteIndex(i)) + ? activatedIcons[getRouteIndex(i)] + : deactivatedIcons[getRouteIndex(i)], + label: const Text(''), + ), + ) + .toList(), + selectedIndex: indexedRoutes.indexOf(ref.watch(navRouteController)), + onDestinationSelected: (int selectedIndex) { + ref.read(navRouteController.notifier).goTo(indexedRoutes[selectedIndex]); + context.goNamed(indexedRoutes[selectedIndex].name); + }, + ); } } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart index 05d81f631..a7c6afa36 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; +import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; @@ -26,7 +26,7 @@ class _ProfileEditActionState extends ConsumerState { ), ); // change value to 1 to update navigation rail selcted icon. - ref.watch(navIndexProvider.notifier).goTo(AppRoute.profileForm); + ref.watch(navRouteController.notifier).goTo(AppRoute.profileForm); context.replaceNamed( AppRoute.profileForm.name, ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart index 3f3d4c466..f6fb1a466 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart @@ -1,19 +1,23 @@ import 'package:at_client_mobile/at_client_mobile.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; +import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; +import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart'; +import 'package:sshnp_gui/src/utils/app_router.dart'; -class ProfileTerminalAction extends StatefulWidget { +class ProfileTerminalAction extends ConsumerStatefulWidget { final SSHNPParams params; const ProfileTerminalAction(this.params, {Key? key}) : super(key: key); @override - State createState() => _ProfileTerminalActionState(); + ConsumerState createState() => _ProfileTerminalActionState(); } -class _ProfileTerminalActionState extends State { +class _ProfileTerminalActionState extends ConsumerState { Future onPressed() async { if (mounted) { showDialog( @@ -31,19 +35,25 @@ class _ProfileTerminalActionState extends State { ); await sshnp.init(); - final sshnpResult = await sshnp.run(); + final result = await sshnp.run(); + if (result is SSHNPFailed) { + throw result; + } - if (mounted) { - // pop to remove circular progress indicator - context.pop(); - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) => SSHNPResultAlertDialog( - result: sshnpResult.toString(), - title: 'Success', - ), - ); + /// Issue a new session id + final sessionId = ref.watch(terminalSessionController.notifier).createSession(); + print('sessionId in onPressed: $sessionId'); + + /// Create the session controller for the new session id + final sessionController = ref.watch(terminalSessionFamilyController(sessionId).notifier); + + if (result is SSHNPCommandResult) { + /// Set the command for the new session + sessionController.setProcess(command: result.command, args: result.args); + ref.read(navRouteController.notifier).goTo(AppRoute.terminal); + if (mounted) { + context.pushReplacementNamed(AppRoute.terminal.name); + } } } catch (e) { if (mounted) { @@ -53,7 +63,7 @@ class _ProfileTerminalActionState extends State { barrierDismissible: false, builder: (BuildContext context) => SSHNPResultAlertDialog( result: e.toString(), - title: 'Failed', + title: 'SSHNP Failed', ), ); } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index 760b5d50d..ccdc8986d 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -4,7 +4,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/nav_index_controller.dart'; +import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_form/custom_text_form_field.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; @@ -52,7 +52,7 @@ class _ProfileFormState extends ConsumerState { await controller.create(config); } if (context.mounted) { - ref.read(navIndexProvider.notifier).goTo(AppRoute.home); + ref.read(navRouteController.notifier).goTo(AppRoute.home); context.pushReplacementNamed(AppRoute.home.name); } } @@ -259,7 +259,7 @@ class _ProfileFormState extends ConsumerState { gapW8, TextButton( onPressed: () { - ref.read(navIndexProvider.notifier).goTo(AppRoute.home); + ref.read(navRouteController.notifier).goTo(AppRoute.home); context.pushReplacementNamed(AppRoute.home.name); }, child: Text(strings.cancel), diff --git a/packages/sshnp_gui/pubspec.yaml b/packages/sshnp_gui/pubspec.yaml index d493efb47..4463abe34 100644 --- a/packages/sshnp_gui/pubspec.yaml +++ b/packages/sshnp_gui/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: sshnoports: path: ../sshnoports/ url_launcher: ^6.1.14 + uuid: ^4.0.0 xterm: ^3.5.0 dev_dependencies: From 6c6886523748a31383f3a64c6ecd302beba9456d Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 18:34:16 +0800 Subject: [PATCH 27/95] chore: cleanup logging --- packages/sshnoports/bin/activate_cli.dart | 2 +- packages/sshnoports/bin/sshnp.dart | 18 ++++++++---------- packages/sshnoports/bin/sshrv.dart | 2 +- .../sshnoports/lib/sshnp/sshnp_params.dart | 6 ++---- packages/sshnoports/lib/sshrv/sshrv.dart | 1 + packages/sshnoports/lib/sshrv/sshrv_impl.dart | 2 +- .../terminal_session_controller.dart | 1 - .../presentation/screens/terminal_screen.dart | 2 -- .../actions/profile_terminal_action.dart | 1 - .../widgets/profile_form/profile_form.dart | 1 - 10 files changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/sshnoports/bin/activate_cli.dart b/packages/sshnoports/bin/activate_cli.dart index ba1ea6718..9baf29390 100644 --- a/packages/sshnoports/bin/activate_cli.dart +++ b/packages/sshnoports/bin/activate_cli.dart @@ -7,7 +7,7 @@ Future main(List args) async { try { await activate_cli.main(args); } catch (e) { - print(e.toString()); + stdout.writeln(e.toString()); } exit(0); } diff --git a/packages/sshnoports/bin/sshnp.dart b/packages/sshnoports/bin/sshnp.dart index 5fff01797..b427e9853 100644 --- a/packages/sshnoports/bin/sshnp.dart +++ b/packages/sshnoports/bin/sshnp.dart @@ -35,20 +35,18 @@ void main(List args) async { await runZonedGuarded(() async { if (params.listDevices) { - print('Searching for devices...'); + stdout.writeln('Searching for devices...'); var (active, off, info) = await sshnp.listDevices(); if (active.isEmpty && off.isEmpty) { - print('[X] No devices found\n'); - print( - 'Note: only devices with sshnpd version 3.4.0 or higher are supported by this command.'); - print( - 'Please update your devices to sshnpd version >= 3.4.0 and try again.'); + stdout.writeln('[X] No devices found\n'); + stdout.writeln('Note: only devices with sshnpd version 3.4.0 or higher are supported by this command.'); + stdout.writeln('Please update your devices to sshnpd version >= 3.4.0 and try again.'); exit(0); } - print('Active Devices:'); + stdout.writeln('Active Devices:'); _printDevices(active, info); - print('Inactive Devices:'); + stdout.writeln('Inactive Devices:'); _printDevices(off, info); exit(0); } @@ -78,10 +76,10 @@ void main(List args) async { void _printDevices(Iterable devices, Map info) { if (devices.isEmpty) { - print(' [X] No devices found'); + stdout.writeln(' [X] No devices found'); return; } for (var device in devices) { - print(' $device - v${info[device]?['version']}'); + stdout.writeln(' $device - v${info[device]?['version']}'); } } diff --git a/packages/sshnoports/bin/sshrv.dart b/packages/sshnoports/bin/sshrv.dart index 4433afe68..f4baa101a 100644 --- a/packages/sshnoports/bin/sshrv.dart +++ b/packages/sshnoports/bin/sshrv.dart @@ -4,7 +4,7 @@ import 'package:sshnoports/sshrv/sshrv.dart'; Future main(List args) async { if (args.length < 2 || args.length > 3) { - print('sshrv [localhost sshd port, defaults to 22]'); + stdout.writeln('sshrv [localhost sshd port, defaults to 22]'); exit(-1); } diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index 31be35430..b1e419bbd 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -141,11 +141,10 @@ class SSHNPParams { var p = SSHNPParams.fromConfigFile(file.path); fileNames.add(p.profileName!); } catch (e) { - print('Error reading config file: ${file.path}'); - print(e); + stderr.writeln('Error reading config file: ${file.path}'); + stderr.writeln(e); } }); - print('fileNames: $fileNames'); return fileNames; } @@ -335,7 +334,6 @@ class SSHNPPartialParams { factory SSHNPPartialParams.fromConfig(String fileName) { var args = _parseConfigFile(fileName); args['profile-name'] = _fileToProfileName(fileName); - print('profile-name: ${args['profile-name']}'); return SSHNPPartialParams.fromArgMap(args); } diff --git a/packages/sshnoports/lib/sshrv/sshrv.dart b/packages/sshnoports/lib/sshrv/sshrv.dart index 2e4e7dfff..9c26828ae 100644 --- a/packages/sshnoports/lib/sshrv/sshrv.dart +++ b/packages/sshnoports/lib/sshrv/sshrv.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:at_utils/at_utils.dart'; import 'package:meta/meta.dart'; import 'package:socket_connector/socket_connector.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; diff --git a/packages/sshnoports/lib/sshrv/sshrv_impl.dart b/packages/sshnoports/lib/sshrv/sshrv_impl.dart index bc70ee10a..015b035f2 100644 --- a/packages/sshnoports/lib/sshrv/sshrv_impl.dart +++ b/packages/sshnoports/lib/sshrv/sshrv_impl.dart @@ -65,7 +65,7 @@ class SSHRVImplPureDart implements SSHRV { verbose: false, ); } catch (e) { - print('sshrv error: ${e.toString()}'); + AtSignLogger('sshrv').severe(e.toString()); rethrow; } } diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart index da1b2c3be..dd0df32c0 100644 --- a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -74,7 +74,6 @@ class TerminalSessionFamilyController extends AutoDisposeFamilyNotifier { void initState() { super.initState(); final sessionId = ref.read(terminalSessionController); - print('sessionId in initState: $sessionId'); final sessionController = ref.read(terminalSessionFamilyController(sessionId).notifier); WidgetsBinding.instance.endOfFrame.then((value) { @@ -45,7 +44,6 @@ class _TerminalScreenState extends ConsumerState { Widget build(BuildContext context) { // * Getting the AtClientManager instance to use below final sessionId = ref.watch(terminalSessionController); - print('sessionId in build: $sessionId'); final terminalSession = ref.watch(terminalSessionFamilyController(sessionId)); return Scaffold( body: SafeArea( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart index f6fb1a466..16f7295d3 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart @@ -42,7 +42,6 @@ class _ProfileTerminalActionState extends ConsumerState { /// Issue a new session id final sessionId = ref.watch(terminalSessionController.notifier).createSession(); - print('sessionId in onPressed: $sessionId'); /// Create the session controller for the new session id final sessionController = ref.watch(terminalSessionFamilyController(sessionId).notifier); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index ccdc8986d..8dda97237 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -160,7 +160,6 @@ class _ProfileFormState extends ConsumerState { initialValue: oldConfig.remoteUsername ?? '', labelText: strings.remoteUserName, onChanged: (value) { - print('remoteUsername: $value'); newConfig = SSHNPPartialParams.merge( newConfig, SSHNPPartialParams(remoteUsername: value), From b885e5cc5a8210c37dda2d420154d5e8f028b2c8 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 18:55:45 +0800 Subject: [PATCH 28/95] fix: nav rail for non rail routes --- .../lib/src/controllers/nav_rail_controller.dart | 13 +++++++++++++ .../src/controllers/nav_route_controller.dart | 11 ----------- .../controllers/terminal_session_controller.dart | 4 ++++ .../presentation/screens/terminal_screen.dart | 6 ++++-- .../dialog/sshnp_result_alert_dialog.dart | 4 ++-- .../home_screen_actions/new_profile_action.dart | 4 +--- .../widgets/navigation/app_navigation_rail.dart | 16 ++++++++-------- .../profile_bar/actions/profile_edit_action.dart | 4 +--- .../actions/profile_terminal_action.dart | 4 ++-- .../widgets/profile_form/profile_form.dart | 6 +++--- 10 files changed, 38 insertions(+), 34 deletions(-) create mode 100644 packages/sshnp_gui/lib/src/controllers/nav_rail_controller.dart delete mode 100644 packages/sshnp_gui/lib/src/controllers/nav_route_controller.dart diff --git a/packages/sshnp_gui/lib/src/controllers/nav_rail_controller.dart b/packages/sshnp_gui/lib/src/controllers/nav_rail_controller.dart new file mode 100644 index 000000000..89948da53 --- /dev/null +++ b/packages/sshnp_gui/lib/src/controllers/nav_rail_controller.dart @@ -0,0 +1,13 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnp_gui/src/utils/app_router.dart'; + +final navRailController = AutoDisposeNotifierProvider(NavRailController.new); + +class NavRailController extends AutoDisposeNotifier { + @override + AppRoute build() => AppRoute.home; + + void setRoute(AppRoute route) { + state = route; + } +} diff --git a/packages/sshnp_gui/lib/src/controllers/nav_route_controller.dart b/packages/sshnp_gui/lib/src/controllers/nav_route_controller.dart deleted file mode 100644 index 850e16f33..000000000 --- a/packages/sshnp_gui/lib/src/controllers/nav_route_controller.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sshnp_gui/src/utils/app_router.dart'; - -final navRouteController = AutoDisposeNotifierProvider(NavRouteController.new); - -class NavRouteController extends AutoDisposeNotifier { - @override - AppRoute build() => AppRoute.home; - - void goTo(AppRoute route) => state = route; -} diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart index dd0df32c0..19466a1e9 100644 --- a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -79,8 +79,12 @@ class TerminalSessionFamilyController extends AutoDisposeFamilyNotifier>().transform(const Utf8Decoder()).listen(state.terminal.write); diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index 0cfea9d1a..7e5311b53 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -1,11 +1,9 @@ -import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; @@ -45,6 +43,10 @@ class _TerminalScreenState extends ConsumerState { // * Getting the AtClientManager instance to use below final sessionId = ref.watch(terminalSessionController); final terminalSession = ref.watch(terminalSessionFamilyController(sessionId)); + if (sessionId.isEmpty) { + // for now, just return a normal shell prompt + terminalSession.command = Platform.environment['SHELL'] ?? 'bash'; + } return Scaffold( body: SafeArea( child: Row( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart index 05e05717f..bda9b9e89 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; +import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; @@ -31,7 +31,7 @@ class SSHNPResultAlertDialog extends ConsumerWidget { final sessionId = ref.read(terminalSessionController.notifier).createSession(); final sessionController = ref.read(terminalSessionFamilyController(sessionId).notifier); sessionController.setProcess(); - ref.read(navRouteController.notifier).goTo(AppRoute.terminal); + ref.read(navRailController.notifier).setRoute(AppRoute.terminal); context.pushReplacementNamed(AppRoute.terminal.name); } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart index 6c07c95a2..9feb4a29c 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; +import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; @@ -22,8 +22,6 @@ class _NewProfileActionState extends ConsumerState { configFileWriteState: ConfigFileWriteState.create, ), ); - // change value to 1 to update navigation rail selcted icon. - ref.watch(navRouteController.notifier).goTo(AppRoute.profileForm); context.replaceNamed( AppRoute.profileForm.name, ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart index 56efe2b3a..cd226ec13 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart @@ -2,20 +2,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; +import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; class AppNavigationRail extends ConsumerWidget { const AppNavigationRail({super.key}); - static const indexedRoutes = [ + static const routes = [ AppRoute.home, AppRoute.terminal, AppRoute.settings, ]; static int getRouteIndex(AppRoute route) { - return indexedRoutes.indexOf(route); + return routes.indexOf(route); } static var activatedIcons = [ @@ -32,10 +32,10 @@ class AppNavigationRail extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final currentIndex = getRouteIndex(ref.watch(navRouteController)); + final currentIndex = getRouteIndex(ref.watch(navRailController)); return NavigationRail( - destinations: indexedRoutes + destinations: routes .map( (i) => NavigationRailDestination( icon: (currentIndex == getRouteIndex(i)) @@ -45,10 +45,10 @@ class AppNavigationRail extends ConsumerWidget { ), ) .toList(), - selectedIndex: indexedRoutes.indexOf(ref.watch(navRouteController)), + selectedIndex: routes.indexOf(ref.watch(navRailController)), onDestinationSelected: (int selectedIndex) { - ref.read(navRouteController.notifier).goTo(indexedRoutes[selectedIndex]); - context.goNamed(indexedRoutes[selectedIndex].name); + ref.read(navRailController.notifier).setRoute(routes[selectedIndex]); + context.goNamed(routes[selectedIndex].name); }, ); } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart index a7c6afa36..357c13915 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; +import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; @@ -25,8 +25,6 @@ class _ProfileEditActionState extends ConsumerState { configFileWriteState: ConfigFileWriteState.update, ), ); - // change value to 1 to update navigation rail selcted icon. - ref.watch(navRouteController.notifier).goTo(AppRoute.profileForm); context.replaceNamed( AppRoute.profileForm.name, ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart index 16f7295d3..421b9acc9 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; -import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; +import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; @@ -49,7 +49,7 @@ class _ProfileTerminalActionState extends ConsumerState { if (result is SSHNPCommandResult) { /// Set the command for the new session sessionController.setProcess(command: result.command, args: result.args); - ref.read(navRouteController.notifier).goTo(AppRoute.terminal); + ref.read(navRailController.notifier).setRoute(AppRoute.terminal); if (mounted) { context.pushReplacementNamed(AppRoute.terminal.name); } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index 8dda97237..3c7ec746c 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -4,7 +4,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/nav_route_controller.dart'; +import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_form/custom_text_form_field.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; @@ -52,7 +52,7 @@ class _ProfileFormState extends ConsumerState { await controller.create(config); } if (context.mounted) { - ref.read(navRouteController.notifier).goTo(AppRoute.home); + ref.read(navRailController.notifier).setRoute(AppRoute.home); context.pushReplacementNamed(AppRoute.home.name); } } @@ -258,7 +258,7 @@ class _ProfileFormState extends ConsumerState { gapW8, TextButton( onPressed: () { - ref.read(navRouteController.notifier).goTo(AppRoute.home); + ref.read(navRailController.notifier).setRoute(AppRoute.home); context.pushReplacementNamed(AppRoute.home.name); }, child: Text(strings.cancel), From a036fbae225f4e53c5863998c5bb6080b21f55ac Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 19:12:11 +0800 Subject: [PATCH 29/95] chore: final cleanup --- packages/sshnoports/lib/sshnp/sshnp_result.dart | 2 +- .../widgets/home_screen_actions/new_profile_action.dart | 1 - .../widgets/profile_bar/actions/profile_edit_action.dart | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_result.dart b/packages/sshnoports/lib/sshnp/sshnp_result.dart index 69982cb29..7a9392f72 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_result.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_result.dart @@ -49,7 +49,7 @@ class SSHCommand implements SSHNPCommandResult { ...sshOptions, if (remoteUsername != null) '$remoteUsername@$host', if (remoteUsername == null) host, - if (shouldIncludePrivateKey(privateKeyFileName)) '-i $privateKeyFileName', + if (shouldIncludePrivateKey(privateKeyFileName)) ...['-i', '$privateKeyFileName'], ]; @override diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart index 9feb4a29c..dcf44af9f 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart index 357c13915..b7b2347e5 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; From 23a75bcf1836c4029160e82351e1aafdf73f1977 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 19:24:11 +0800 Subject: [PATCH 30/95] feat: show the command being run by the terminal --- .../lib/src/controllers/terminal_session_controller.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart index 19466a1e9..62208bc94 100644 --- a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -83,7 +83,11 @@ class TerminalSessionFamilyController extends AutoDisposeFamilyNotifier>().transform(const Utf8Decoder()).listen(state.terminal.write); From 4065e3e432f6144f700779399e79993a835761a5 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 19:37:50 +0800 Subject: [PATCH 31/95] chore: no autodispose on terminal controllers --- .../controllers/terminal_session_controller.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart index 62208bc94..4fbb69e79 100644 --- a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -7,23 +7,23 @@ import 'package:uuid/uuid.dart'; import 'package:xterm/xterm.dart'; /// A provider that exposes the [TerminalSessionController] to the app. -final terminalSessionController = AutoDisposeNotifierProvider( +final terminalSessionController = NotifierProvider( TerminalSessionController.new, ); /// A provider that exposes the [TerminalSessionListController] to the app. -final terminalSessionListController = AutoDisposeNotifierProvider>( +final terminalSessionListController = NotifierProvider>( TerminalSessionListController.new, ); /// A provider that exposes the [TerminalSessionFamilyController] to the app. final terminalSessionFamilyController = - AutoDisposeNotifierProviderFamily( + NotifierProviderFamily( TerminalSessionFamilyController.new, ); /// Controller for the id of the currently active terminal session -class TerminalSessionController extends AutoDisposeNotifier { +class TerminalSessionController extends Notifier { @override String build() => ''; @@ -35,9 +35,9 @@ class TerminalSessionController extends AutoDisposeNotifier { } /// Controller for the list of all terminal session ids -class TerminalSessionListController extends AutoDisposeNotifier> { +class TerminalSessionListController extends Notifier> { @override - Set build() => {}; + List build() => []; void add(String sessionId) { state.add(sessionId); @@ -61,7 +61,7 @@ class TerminalSession { } /// Controller for the family of terminal session [TerminalController]s -class TerminalSessionFamilyController extends AutoDisposeFamilyNotifier { +class TerminalSessionFamilyController extends FamilyNotifier { @override TerminalSession build(String arg) { return TerminalSession(arg); @@ -73,6 +73,7 @@ class TerminalSessionFamilyController extends AutoDisposeFamilyNotifier Date: Thu, 7 Sep 2023 19:38:01 +0800 Subject: [PATCH 32/95] feat: sort home screen profiles --- .../sshnp_gui/lib/src/presentation/screens/home_screen.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index b810a37aa..06c897f13 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -55,9 +55,11 @@ class _HomeScreenState extends ConsumerState { if (profiles.isEmpty) { return const Text('No SSHNP Configurations Found'); } + final sortedProfiles = profiles.toList(); + sortedProfiles.sort(); return Expanded( child: ListView( - children: profiles.map((profileName) => ProfileBar(profileName)).toList(), + children: sortedProfiles.map((profileName) => ProfileBar(profileName)).toList(), ), ); }, From 0c7ae84996da06e038f452cf14fe9677ebd2a43d Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 7 Sep 2023 19:42:21 +0800 Subject: [PATCH 33/95] feat: default 10000 max lines for the terminal --- .../lib/src/controllers/terminal_session_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart index 4fbb69e79..9f2b72120 100644 --- a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -57,7 +57,7 @@ class TerminalSession { String? command; List args = const []; - TerminalSession(this.sessionId) : terminal = Terminal(); + TerminalSession(this.sessionId) : terminal = Terminal(maxLines: 10000); } /// Controller for the family of terminal session [TerminalController]s From e83e62334b84d3fe6a692ebbaec7a50f45da5b0b Mon Sep 17 00:00:00 2001 From: Curtly Critchlow Date: Thu, 7 Sep 2023 14:55:41 -0400 Subject: [PATCH 34/95] feat: new terminal shown on terminal screen when terminal icon is pressed on the home screen --- .../presentation/screens/terminal_screen.dart | 62 +++++-- .../Flutter/GeneratedPluginRegistrant.swift | 2 +- packages/sshnp_gui/macos/Podfile.lock | 14 +- packages/sshnp_gui/pubspec.lock | 168 +++++++++--------- packages/sshnp_gui/pubspec.yaml | 12 +- .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 7 files changed, 148 insertions(+), 114 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index 7e5311b53..3e6b45779 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -43,6 +43,7 @@ class _TerminalScreenState extends ConsumerState { // * Getting the AtClientManager instance to use below final sessionId = ref.watch(terminalSessionController); final terminalSession = ref.watch(terminalSessionFamilyController(sessionId)); + final terminalList = ref.watch(terminalSessionListController); if (sessionId.isEmpty) { // for now, just return a normal shell prompt terminalSession.command = Platform.environment['SHELL'] ?? 'bash'; @@ -56,19 +57,56 @@ class _TerminalScreenState extends ConsumerState { Expanded( child: Padding( padding: const EdgeInsets.only(left: Sizes.p36, top: Sizes.p21, right: Sizes.p36), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - 'assets/images/noports_light.svg', - ), - gapH24, - Expanded( - child: TerminalView( - terminalSession.terminal, - controller: terminalController, - autofocus: true, + child: DefaultTabController( + length: terminalList.length, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SvgPicture.asset( + 'assets/images/noports_light.svg', ), - ), - ]), + gapH24, + TabBar( + isScrollable: true, + tabs: terminalList + .map( + (e) => Tab( + // text: e, + child: Row( + children: [ + Text(e), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + ref.read(terminalSessionListController.notifier).remove(e); + setState(() {}); + }, + ) + ], + )), + ) + .toList(), + ), + Expanded( + child: TabBarView( + children: terminalList.map((e) { + return TerminalView( + terminalSession.terminal, + // ref.read(terminalSessionFamilyController(e).terminal), + controller: terminalController, + autofocus: true, + ); + }).toList(), + ), + ), + // SizedBox( + // height: MediaQuery.of(context).size.height - 200, + // child: TerminalView( + // terminalSession.terminal, + // controller: terminalController, + // autofocus: true, + // ), + // ), + ]), + ), ), ), ], diff --git a/packages/sshnp_gui/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/sshnp_gui/macos/Flutter/GeneratedPluginRegistrant.swift index 92e8470dd..4369eba15 100644 --- a/packages/sshnp_gui/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/sshnp_gui/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,7 +13,7 @@ import macos_ui import macos_window_utils import package_info_plus import path_provider_foundation -import share_plus_macos +import share_plus import shared_preferences_foundation import url_launcher_macos diff --git a/packages/sshnp_gui/macos/Podfile.lock b/packages/sshnp_gui/macos/Podfile.lock index db7c7f42c..b1eda3d11 100644 --- a/packages/sshnp_gui/macos/Podfile.lock +++ b/packages/sshnp_gui/macos/Podfile.lock @@ -19,7 +19,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - share_plus_macos (0.0.1): + - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter @@ -38,7 +38,7 @@ DEPENDENCIES: - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -63,8 +63,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin - share_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin url_launcher_macos: @@ -74,17 +74,17 @@ SPEC CHECKSUMS: at_file_saver: 1fc6ed722f17c7a20ce79cce168d1100fcad4b95 biometric_storage: 43caa6e7ef00e8e19c074216e7e1786dacda9e76 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f - file_selector_macos: f1b08a781e66103e3ba279fd5d4024a2478b3af6 + file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 flutter_pty: 41b6f848ade294be726a6b94cdd4a67c3bc52f59 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 macos_ui: 6229a8922cd97bafb7d9636c8eb8dfb0744183ca macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 + share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 diff --git a/packages/sshnp_gui/pubspec.lock b/packages/sshnp_gui/pubspec.lock index 24c9673ff..f5b80b462 100644 --- a/packages/sshnp_gui/pubspec.lock +++ b/packages/sshnp_gui/pubspec.lock @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: at_backupkey_flutter - sha256: "012fa7e497052477348d68aa03b0ffe298d3a91b113a5f1f864bcff0a1d54488" + sha256: "5e0eb67988f99f435076db43e2fdd96bb27647764243e1f74fe51dff529634c9" url: "https://pub.dev" source: hosted - version: "4.0.9" + version: "4.0.10" at_base2e15: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: "direct main" description: name: at_client_mobile - sha256: "4e3cfdd22c0edac09540e080ddc8b7cb01c0302bf1fa316656d5126815c2ad3c" + sha256: c9614ad3704c55e637d18352427fc13478ef39a7ae827edf45665c23b9d57c7e url: "https://pub.dev" source: hosted - version: "3.2.10" + version: "3.2.12" at_common_flutter: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: "direct main" description: name: at_onboarding_flutter - sha256: aab27143f31bb13ec6993be0c3ed5610f025e5e15df14f2f9ecf9b9e6c97e630 + sha256: f1da8c11c915117e96f145c63756e87aeaa78556b0990bc40756460d360ca18b url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.1.3" at_persistence_secondary_server: dependency: transitive description: @@ -210,13 +210,13 @@ packages: source: hosted version: "3.0.15" biometric_storage: - dependency: "direct main" + dependency: transitive description: name: biometric_storage - sha256: f6d7f5f4c28323797658423e4c5982c9dee42e18f59a8a8d4bc5df38eaf2e2f1 + sha256: "2bae7ce64d4e3a390f8adfd0373ed1a82d567e3692e16a1bd0f72f91fb962ae3" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "5.0.0+4" boolean_selector: dependency: transitive description: @@ -333,10 +333,10 @@ packages: dependency: transitive description: name: device_info_plus - sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903 + sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659" url: "https://pub.dev" source: hosted - version: "8.2.2" + version: "9.0.3" device_info_plus_platform_interface: dependency: transitive description: @@ -405,34 +405,50 @@ packages: dependency: transitive description: name: file_picker - sha256: b85eb92b175767fdaa0c543bf3b0d1f610fe966412ea72845fe5ba7801e763ff + sha256: "21145c9c268d54b1f771d8380c195d2d6f655e0567dc1ca2f9c134c02c819e0a" url: "https://pub.dev" source: hosted - version: "5.2.10" + version: "5.3.3" file_selector: dependency: transitive description: name: file_selector - sha256: "9e34368bfacdf644e2c8a59e2b241cfb722bcbbd09876410e8775ae4905d6a49" + sha256: "1d2fde93dddf634a9c3c0faa748169d7ac0d83757135555707e52f02c017ad4f" url: "https://pub.dev" source: hosted - version: "0.8.4+3" + version: "0.9.5" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: d41e165d6f798ca941d536e5dc93494d50e78c571c28ad60cfe0b0fefeb9f1e7 + url: "https://pub.dev" + source: hosted + version: "0.5.0+3" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: b3fbdda64aa2e335df6e111f6b0f1bb968402ed81d2dd1fa4274267999aa32c2 + url: "https://pub.dev" + source: hosted + version: "0.5.1+6" file_selector_linux: dependency: transitive description: name: file_selector_linux - sha256: c06249f2082e88aca55f4aad9e4c70ff0f2b61d753c1577d51adeab88b3f0178 + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" url: "https://pub.dev" source: hosted - version: "0.0.3" + version: "0.9.2+1" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: e87311d719039da30d26ae829aab3ae66f82deb3318cd70ffecb608c99e3da68 + sha256: "182c3f8350cee659f7b115e956047ee3dc672a96665883a545e81581b9a82c72" url: "https://pub.dev" source: hosted - version: "0.8.2+2" + version: "0.9.3+2" file_selector_platform_interface: dependency: transitive description: @@ -445,18 +461,18 @@ packages: dependency: transitive description: name: file_selector_web - sha256: bf166d08f4c3f79286774cdfa39ed301e076c5a903c435f5199818288f24a66d + sha256: dc6622c4d66cb1bee623ddcc029036603c6cc45c85e4a775bb06008d61c809c1 url: "https://pub.dev" source: hosted - version: "0.8.1+5" + version: "0.9.2+1" file_selector_windows: dependency: transitive description: name: file_selector_windows - sha256: "8bbcc82fe0d3cdf5ae5c289492ddfd703ec028028d9f194dbceae04cfbde1c48" + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 url: "https://pub.dev" source: hosted - version: "0.8.2+2" + version: "0.9.3+1" fixnum: dependency: transitive description: @@ -606,13 +622,13 @@ packages: source: hosted version: "4.0.2" image: - dependency: "direct overridden" + dependency: transitive description: name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.0.17" internet_connection_checker: dependency: transitive description: @@ -761,10 +777,10 @@ packages: dependency: transitive description: name: package_info_plus - sha256: cbff87676c352d97116af6dbea05aa28c4d65eb0f6d5677a520c11a69ca9a24d + sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.1.0" package_info_plus_platform_interface: dependency: transitive description: @@ -801,66 +817,66 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.2.0" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" permission_handler: dependency: transitive description: name: permission_handler - sha256: "5749ebeb7ec0c3865ea17e3eb337174b87747be816dab582c551e1aff6f6bbf3" + sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 url: "https://pub.dev" source: hosted - version: "9.2.0" + version: "10.4.5" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: a512e0fa8abcb0659d938ec2df93a70eb1df1fdea5fdc6d79a866bfd858a28fc + sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" url: "https://pub.dev" source: hosted - version: "9.0.2+1" + version: "10.3.6" permission_handler_apple: dependency: transitive description: @@ -897,10 +913,10 @@ packages: dependency: transitive description: name: pin_code_fields - sha256: c8652519d14688f3fe2a8288d86910a46aa0b9046d728f292d3bf6067c31b4c7 + sha256: "4c0db7fbc889e622e7c71ea54b9ee624bb70c7365b532abea0271b17ea75b729" url: "https://pub.dev" source: hosted - version: "7.4.0" + version: "8.0.1" pinenacl: dependency: transitive description: @@ -993,50 +1009,18 @@ packages: dependency: transitive description: name: share_plus - sha256: f582d5741930f3ad1bf0211d358eddc0508cc346e5b4b248bd1e569c995ebb7a + sha256: "6cec740fa0943a826951223e76218df002804adb588235a8910dc3d6b0654e11" url: "https://pub.dev" source: hosted - version: "4.5.3" - share_plus_linux: - dependency: transitive - description: - name: share_plus_linux - sha256: dc32bf9f1151b9864bb86a997c61a487967a08f2e0b4feaa9a10538712224da4 - url: "https://pub.dev" - source: hosted - version: "3.0.1" - share_plus_macos: - dependency: transitive - description: - name: share_plus_macos - sha256: "44daa946f2845045ecd7abb3569b61cd9a55ae9cc4cbec9895b2067b270697ae" - url: "https://pub.dev" - source: hosted - version: "3.0.1" + version: "7.1.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - share_plus_web: - dependency: transitive - description: - name: share_plus_web - sha256: eaef05fa8548b372253e772837dd1fbe4ce3aca30ea330765c945d7d4f7c9935 + sha256: "357412af4178d8e11d14f41723f80f12caea54cf0d5cd29af9dcdab85d58aea7" url: "https://pub.dev" source: hosted - version: "3.1.0" - share_plus_windows: - dependency: transitive - description: - name: share_plus_windows - sha256: "3a21515ae7d46988d42130cd53294849e280a5de6ace24bae6912a1bffd757d4" - url: "https://pub.dev" - source: hosted - version: "3.0.1" + version: "3.3.0" shared_preferences: dependency: "direct main" description: @@ -1129,10 +1113,10 @@ packages: dependency: transitive description: name: showcaseview - sha256: "09b534d806572135c38e06901de4b36b2bbd61739ec56c5fa9242d10748e19df" + sha256: dc62ce38820dead4a27ce39d9e6c98384be89c2f2b4da3255238a59b041c7ccd url: "https://pub.dev" source: hosted - version: "1.1.8" + version: "2.0.3" sky_engine: dependency: transitive description: flutter @@ -1277,10 +1261,10 @@ packages: dependency: transitive description: name: url_launcher - sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" + sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" url: "https://pub.dev" source: hosted - version: "6.1.12" + version: "6.1.14" url_launcher_android: dependency: transitive description: @@ -1461,10 +1445,18 @@ packages: dependency: transitive description: name: win32 - sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 + sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 + url: "https://pub.dev" + source: hosted + version: "5.0.6" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9 url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "1.1.1" xdg_directories: dependency: transitive description: diff --git a/packages/sshnp_gui/pubspec.yaml b/packages/sshnp_gui/pubspec.yaml index 4463abe34..0243040ec 100644 --- a/packages/sshnp_gui/pubspec.yaml +++ b/packages/sshnp_gui/pubspec.yaml @@ -8,14 +8,14 @@ environment: dependencies: at_app_flutter: ^5.0.1 - at_backupkey_flutter: ^4.0.10 + # at_backupkey_flutter: ^4.0.10 at_client_mobile: ^3.2.6 - at_common_flutter: ^2.0.12 + # at_common_flutter: ^2.0.12 at_contact: ^3.0.7 at_contacts_flutter: ^4.0.5 at_onboarding_flutter: ^6.1.0 at_utils: ^3.0.11 - biometric_storage: ^4.1.3 + # biometric_storage: ^4.1.3 flutter: sdk: flutter flutter_dotenv: ^5.0.2 @@ -33,8 +33,8 @@ dependencies: shared_preferences: ^2.2.0 sshnoports: path: ../sshnoports/ - url_launcher: ^6.1.14 - uuid: ^4.0.0 + # url_launcher: ^6.1.14 + # uuid: ^3.0.7 xterm: ^3.5.0 dev_dependencies: @@ -44,7 +44,7 @@ dev_dependencies: dependency_overrides: intl: ^0.17.0-nullsafety.2 - image: ^3.1.3 + # image: ^3.1.3 zxing2: ^0.2.0 flutter: uses-material-design: true diff --git a/packages/sshnp_gui/windows/flutter/generated_plugin_registrant.cc b/packages/sshnp_gui/windows/flutter/generated_plugin_registrant.cc index 762731d61..036f08c22 100644 --- a/packages/sshnp_gui/windows/flutter/generated_plugin_registrant.cc +++ b/packages/sshnp_gui/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/packages/sshnp_gui/windows/flutter/generated_plugins.cmake b/packages/sshnp_gui/windows/flutter/generated_plugins.cmake index 37ff62e99..b91db13cc 100644 --- a/packages/sshnp_gui/windows/flutter/generated_plugins.cmake +++ b/packages/sshnp_gui/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST at_file_saver file_selector_windows permission_handler_windows + share_plus url_launcher_windows ) From 24ef186d2ebc2b47c027ffc62e5bbfdab2071b77 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 11:47:05 +0800 Subject: [PATCH 35/95] refactor: create settings actions --- .../navigation/app_navigation_rail.dart | 0 .../src/presentation/screens/home_screen.dart | 2 +- .../screens/profile_editor_screen.dart | 2 +- .../presentation/screens/settings_screen.dart | 82 ++-------------- .../presentation/screens/terminal_screen.dart | 2 +- .../dialog/sshnp_result_alert_dialog.dart | 97 ------------------- .../profile_action_button.dart | 19 ++++ .../profile_actions/profile_actions.dart | 4 + .../profile_delete_action.dart} | 20 ++++ .../profile_edit_action.dart | 7 +- .../profile_run_action.dart | 29 ++---- .../profile_terminal_action.dart | 14 +-- .../profile_bar/actions/profile_actions.dart | 23 ----- .../actions/profile_delete_action.dart | 22 ----- .../widgets/profile_bar/profile_bar.dart | 6 +- .../profile_bar/profile_bar_actions.dart | 20 ++++ .../profile_bar/profile_bar_stats.dart | 13 +++ .../profile_bar/stats/profile_stats.dart | 0 .../settings_action_button.dart} | 4 +- .../settings_actions/settings_actions.dart | 6 ++ .../settings_backup_keys_action.dart | 21 ++++ .../settings_contact_action.dart | 26 +++++ .../settings_actions/settings_faq_action.dart | 23 +++++ .../settings_privacy_policy_action.dart | 24 +++++ .../settings_reset_app_action.dart} | 13 +-- .../settings_switch_atsign_action.dart} | 28 +++++- packages/sshnp_gui/macos/Podfile.lock | 2 +- 27 files changed, 240 insertions(+), 269 deletions(-) rename packages/sshnp_gui/lib/src/presentation/{widgets => }/navigation/app_navigation_rail.dart (100%) delete mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_button.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_actions.dart rename packages/sshnp_gui/lib/src/presentation/widgets/{dialog/delete_alert_dialog.dart => profile_actions/profile_delete_action.dart} (80%) rename packages/sshnp_gui/lib/src/presentation/widgets/{profile_bar/actions => profile_actions}/profile_edit_action.dart (88%) rename packages/sshnp_gui/lib/src/presentation/widgets/{profile_bar/actions => profile_actions}/profile_run_action.dart (65%) rename packages/sshnp_gui/lib/src/presentation/widgets/{profile_bar/actions => profile_actions}/profile_terminal_action.dart (86%) delete mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_actions.dart delete mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_delete_action.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_stats.dart delete mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/stats/profile_stats.dart rename packages/sshnp_gui/lib/src/presentation/widgets/{utility/settings_button.dart => settings_actions/settings_action_button.dart} (92%) create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_actions.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_backup_keys_action.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_contact_action.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_faq_action.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_privacy_policy_action.dart rename packages/sshnp_gui/lib/src/presentation/widgets/{utility/reset_app_button.dart => settings_actions/settings_reset_app_action.dart} (96%) rename packages/sshnp_gui/lib/src/presentation/widgets/{utility/switch_atsign.dart => settings_actions/settings_switch_atsign_action.dart} (85%) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart b/packages/sshnp_gui/lib/src/presentation/navigation/app_navigation_rail.dart similarity index 100% rename from packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart rename to packages/sshnp_gui/lib/src/presentation/navigation/app_navigation_rail.dart diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index 06c897f13..78b3d452c 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_actions.dart'; -import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/presentation/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart index 6208f5ca4..3c239a7b7 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/presentation/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_form/profile_form.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart index b2141aedb..05a17a70b 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart @@ -2,13 +2,11 @@ import 'package:at_backupkey_flutter/at_backupkey_flutter.dart'; import 'package:at_contacts_flutter/services/contact_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; -import 'package:sshnp_gui/src/presentation/widgets/utility/reset_app_button.dart'; -import 'package:sshnp_gui/src/presentation/widgets/utility/settings_button.dart'; -import 'package:sshnp_gui/src/presentation/widgets/utility/switch_atsign.dart'; +import 'package:sshnp_gui/src/presentation/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_actions.dart'; +import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_action_button.dart'; import 'package:sshnp_gui/src/repository/navigation_repository.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; -import 'package:url_launcher/url_launcher.dart'; class SettingsScreen extends StatelessWidget { const SettingsScreen({Key? key}) : super(key: key); @@ -39,82 +37,20 @@ class SettingsScreen extends StatelessWidget { style: Theme.of(context).textTheme.headlineMedium, ), ), - // Text( - // ContactService().currentAtsign, - // style: Theme.of(context).textTheme.bodyLarge, - // ), - // Text( - // ContactService().loggedInUserDetails!.tags!['name'] ?? '', - // style: Theme.of(context).textTheme.displaySmall, - // ), - // const SizedBox( - // height: 30, - // ), - // SettingsButton( - // icon: Icons.block_outlined, - // title: 'Blocked Contacts', - // onTap: () { - // Navigator.of(context).pushNamed(CustomBlockedScreen.routeName); - // }, - // ), const SizedBox( height: 59, ), - SettingsButton( - icon: Icons.bookmark_outline, - title: strings.backupYourKeys, - onTap: () { - BackupKeyWidget(atsign: ContactService().currentAtsign).showBackupDialog(context); - }, - ), + const SettingsBackupKeyAction(), gapH16, - SettingsButton( - icon: Icons.logout_rounded, - title: strings.switchAtsign, - onTap: () async { - await showModalBottomSheet( - context: NavigationRepository.navKey.currentContext!, - builder: (context) => const AtSignBottomSheet()); - }, - ), + const SettingsSwitchAtsignAction(), gapH16, - const ResetAppButton(), + const SettingsResetAppAction(), gapH36, - SettingsButton( - icon: Icons.help_center_outlined, - title: strings.faq, - onTap: () async { - final Uri url = Uri.parse('https://atsign.com/faqs/'); - if (!await launchUrl(url)) { - throw Exception('Could not launch $url'); - } - }, - ), + const SettingsFaqAction(), gapH16, - SettingsButton( - icon: Icons.forum_outlined, - title: strings.contactUs, - onTap: () async { - Uri emailUri = Uri( - scheme: 'mailto', - path: 'atDataBrowser@atsign.com', - ); - if (!await launchUrl(emailUri)) { - throw Exception('Could not launch $emailUri'); - } - }, - ), + const SettingsContactAction(), gapH16, - SettingsButton( - icon: Icons.account_balance_wallet_outlined, - title: strings.privacyPolicy, - onTap: () async { - final Uri url = Uri.parse('https://atsign.com/apps/atdatabrowser-privacy-policy/'); - if (!await launchUrl(url)) { - throw Exception('Could not launch $url'); - } - }, - ), + const SettingsPrivacyPolicyAction(), ], ), ), diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index 3e6b45779..3abda43c4 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -5,7 +5,7 @@ import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; -import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/presentation/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; import 'package:xterm/xterm.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart deleted file mode 100644 index bda9b9e89..000000000 --- a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; -import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; -import 'package:sshnp_gui/src/utils/app_router.dart'; - -class SSHNPResultAlertDialog extends ConsumerWidget { - const SSHNPResultAlertDialog({required this.result, required this.title, super.key}); - - final String result; - final String title; - - void copyToClipBoard({ - required BuildContext context, - required String clipboardSuccessText, - }) { - Clipboard.setData(ClipboardData(text: result)).then((value) => ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(clipboardSuccessText), - ), - )); - } - - void ssh({ - required WidgetRef ref, - required BuildContext context, - }) { - final sessionId = ref.read(terminalSessionController.notifier).createSession(); - final sessionController = ref.read(terminalSessionFamilyController(sessionId).notifier); - sessionController.setProcess(); - ref.read(navRailController.notifier).setRoute(AppRoute.terminal); - context.pushReplacementNamed(AppRoute.terminal.name); - } - - @override - Widget build( - BuildContext context, - WidgetRef ref, - ) { - final strings = AppLocalizations.of(context)!; - - return Padding( - padding: const EdgeInsets.only(left: 72), - child: Center( - child: AlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded(child: Center(child: Text(title))), - result.contains('ssh') - ? IconButton( - icon: const Icon(Icons.copy_outlined), - onPressed: () => copyToClipBoard( - context: context, - clipboardSuccessText: strings.copiedToClipboard, - ), - ) - : const SizedBox.shrink() - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text.rich( - TextSpan( - children: [ - TextSpan( - text: result, - style: Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w700), - ), - ], - ), - ) - ], - ), - actions: [ - OutlinedButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(strings.closeButton, - style: Theme.of(context).textTheme.bodyLarge!.copyWith(decoration: TextDecoration.underline)), - ), - result.contains('ssh') - ? OutlinedButton( - onPressed: () => ssh(context: context, ref: ref), - child: Text(strings.sshButton, - style: Theme.of(context).textTheme.bodyLarge!.copyWith(decoration: TextDecoration.underline)), - ) - : const SizedBox.shrink(), - ], - ), - ), - ); - } -} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_button.dart new file mode 100644 index 000000000..fcac5bae0 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_button.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class ProfileActionButton extends StatelessWidget { + final void Function() onPressed; + final Widget icon; + const ProfileActionButton({ + required this.onPressed, + required this.icon, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: onPressed, + icon: icon, + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_actions.dart new file mode 100644 index 000000000..60a819c1b --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_actions.dart @@ -0,0 +1,4 @@ +export 'profile_delete_action.dart'; +export 'profile_edit_action.dart'; +export 'profile_run_action.dart'; +export 'profile_terminal_action.dart'; \ No newline at end of file diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart similarity index 80% rename from packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart index ec164e113..f71a3c534 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/dialog/delete_alert_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart @@ -3,8 +3,28 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; +class ProfileDeleteAction extends StatelessWidget { + final SSHNPParams params; + const ProfileDeleteAction(this.params, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ProfileActionButton( + onPressed: () async { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => DeleteAlertDialog(sshnpParams: params), + ); + }, + icon: const Icon(Icons.delete_forever), + ); + } +} + class DeleteAlertDialog extends ConsumerWidget { const DeleteAlertDialog({required this.sshnpParams, super.key}); final SSHNPParams sshnpParams; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart similarity index 88% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart index b7b2347e5..2aba5add4 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; @@ -31,10 +32,8 @@ class _ProfileEditActionState extends ConsumerState { @override Widget build(BuildContext context) { - return IconButton( - onPressed: () { - onPressed(); - }, + return ProfileActionButton( + onPressed: onPressed, icon: const Icon(Icons.edit), ); } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart similarity index 65% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_run_action.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index 60adf1083..571e7e28f 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -3,7 +3,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; -import 'package:sshnp_gui/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; +import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; class ProfileRunAction extends StatefulWidget { final SSHNPParams params; @@ -32,37 +33,21 @@ class _ProfileRunActionState extends State { await sshnp.init(); final sshnpResult = await sshnp.run(); - + // TODO + } catch (e) { if (mounted) { - // pop to remove circular progress indicator - context.pop(); - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) => SSHNPResultAlertDialog( - result: sshnpResult.toString(), - title: 'Success', - ), - ); + CustomSnackBar.error(content: e.toString()); } - } catch (e) { + } finally { if (mounted) { context.pop(); - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) => SSHNPResultAlertDialog( - result: e.toString(), - title: 'Failed', - ), - ); } } } @override Widget build(BuildContext context) { - return IconButton( + return ProfileActionButton( onPressed: () async { await onPressed(); }, diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart similarity index 86% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart index 421b9acc9..180a0dfd4 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart @@ -6,7 +6,8 @@ import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; -import 'package:sshnp_gui/src/presentation/widgets/dialog/sshnp_result_alert_dialog.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; +import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; import 'package:sshnp_gui/src/utils/app_router.dart'; class ProfileTerminalAction extends ConsumerStatefulWidget { @@ -57,21 +58,14 @@ class _ProfileTerminalActionState extends ConsumerState { } catch (e) { if (mounted) { context.pop(); - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) => SSHNPResultAlertDialog( - result: e.toString(), - title: 'SSHNP Failed', - ), - ); + CustomSnackBar.error(content: e.toString()); } } } @override Widget build(BuildContext context) { - return IconButton( + return ProfileActionButton( onPressed: () async { await onPressed(); }, diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_actions.dart deleted file mode 100644 index a58b4c584..000000000 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_actions.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_bar/actions/profile_delete_action.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_bar/actions/profile_edit_action.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_bar/actions/profile_run_action.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_bar/actions/profile_terminal_action.dart'; - -class ProfileActions extends StatelessWidget { - final SSHNPParams params; - const ProfileActions(this.params, {Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - ProfileRunAction(params), - ProfileTerminalAction(params), - ProfileEditAction(params), - ProfileDeleteAction(params), - ], - ); - } -} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_delete_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_delete_action.dart deleted file mode 100644 index 21d9ac266..000000000 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/actions/profile_delete_action.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/presentation/widgets/dialog/delete_alert_dialog.dart'; - -class ProfileDeleteAction extends StatelessWidget { - final SSHNPParams params; - const ProfileDeleteAction(this.params, {Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return IconButton( - onPressed: () async { - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => DeleteAlertDialog(sshnpParams: params), - ); - }, - icon: const Icon(Icons.delete_forever), - ); - } -} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart index 34b496610..3e3895c7d 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_bar/actions/profile_actions.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar_actions.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar_stats.dart'; class ProfileBar extends ConsumerStatefulWidget { final String profileName; @@ -30,7 +31,8 @@ class _ProfileBarState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(params.profileName ?? ''), - ProfileActions(params), + const ProfileBarStats(), + ProfileBarActions(params), ], ), ), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart new file mode 100644 index 000000000..491a7192a --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_actions.dart'; + +class ProfileBarActions extends StatelessWidget { + final SSHNPParams params; + const ProfileBarActions(this.params, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + ProfileRunAction(params), + ProfileTerminalAction(params), + ProfileEditAction(params), + ProfileDeleteAction(params), + ], + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_stats.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_stats.dart new file mode 100644 index 000000000..714a27388 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_stats.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class ProfileBarStats extends StatelessWidget { + const ProfileBarStats({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const SizedBox( + width: 0, + height: 0, + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/stats/profile_stats.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/stats/profile_stats.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/utility/settings_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_action_button.dart similarity index 92% rename from packages/sshnp_gui/lib/src/presentation/widgets/utility/settings_button.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_action_button.dart index 151fc4387..dc29456d3 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/utility/settings_button.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_action_button.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:sshnp_gui/src/utils/constants.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; -class SettingsButton extends StatelessWidget { - const SettingsButton({ +class SettingsActionButton extends StatelessWidget { + const SettingsActionButton({ required this.icon, required this.title, required this.onTap, diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_actions.dart new file mode 100644 index 000000000..9e15d92b4 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_actions.dart @@ -0,0 +1,6 @@ +export 'settings_backup_keys_action.dart'; +export 'settings_contact_action.dart'; +export 'settings_faq_action.dart'; +export 'settings_switch_atsign_action.dart'; +export 'settings_privacy_policy_action.dart'; +export 'settings_reset_app_action.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_backup_keys_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_backup_keys_action.dart new file mode 100644 index 000000000..ff8a69b10 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_backup_keys_action.dart @@ -0,0 +1,21 @@ +import 'package:at_contacts_flutter/services/contact_service.dart'; +import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_action_button.dart'; + +class SettingsBackupKeyAction extends StatelessWidget { + const SettingsBackupKeyAction({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; + return SettingsActionButton( + icon: Icons.bookmark_outline, + title: strings.backupYourKeys, + onTap: () { + BackupKeyWidget(atsign: ContactService().currentAtsign).showBackupDialog(context); + }, + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_contact_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_contact_action.dart new file mode 100644 index 000000000..0b75b0f92 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_contact_action.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_action_button.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class SettingsContactAction extends StatelessWidget { + const SettingsContactAction({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; + return SettingsActionButton( + icon: Icons.forum_outlined, + title: strings.contactUs, + onTap: () async { + Uri emailUri = Uri( + scheme: 'mailto', + path: 'atDataBrowser@atsign.com', + ); + if (!await launchUrl(emailUri)) { + throw Exception('Could not launch $emailUri'); + } + }, + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_faq_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_faq_action.dart new file mode 100644 index 000000000..797be28d2 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_faq_action.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_action_button.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SettingsFaqAction extends StatelessWidget { + const SettingsFaqAction({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; + return SettingsActionButton( + icon: Icons.help_center_outlined, + title: strings.faq, + onTap: () async { + final Uri url = Uri.parse('https://atsign.com/faqs/'); + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } + }, + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_privacy_policy_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_privacy_policy_action.dart new file mode 100644 index 000000000..fac90a2a3 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_privacy_policy_action.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_action_button.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SettingsPrivacyPolicyAction extends StatelessWidget { + const SettingsPrivacyPolicyAction({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; + return SettingsActionButton( + icon: Icons.account_balance_wallet_outlined, + title: strings.privacyPolicy, + onTap: () async { + final Uri url = Uri.parse('https://atsign.com/apps/atdatabrowser-privacy-policy/'); + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } + }, + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/utility/reset_app_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_reset_app_action.dart similarity index 96% rename from packages/sshnp_gui/lib/src/presentation/widgets/utility/reset_app_button.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_reset_app_action.dart index c91b19993..b28e78002 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/utility/reset_app_button.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_reset_app_action.dart @@ -2,33 +2,33 @@ import 'package:at_onboarding_flutter/services/sdk_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:sshnp_gui/main.dart'; -import 'package:sshnp_gui/src/presentation/widgets/utility/settings_button.dart'; +import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_action_button.dart'; import 'package:sshnp_gui/src/utils/at_error_dialog.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; /// Custom reset button widget is to reset an atsign from keychain list, -class ResetAppButton extends StatefulWidget { +class SettingsResetAppAction extends StatefulWidget { final String? buttonText; final bool isOnboardingScreen; - const ResetAppButton({ + const SettingsResetAppAction({ Key? key, this.buttonText, this.isOnboardingScreen = false, }) : super(key: key); @override - State createState() => _ResetAppButtonState(); + State createState() => _SettingsResetAppActionState(); } -class _ResetAppButtonState extends State { +class _SettingsResetAppActionState extends State { bool? loading = false; @override Widget build(BuildContext context) { if (!widget.isOnboardingScreen) { - return SettingsButton( + return SettingsActionButton( icon: Icons.restart_alt_outlined, title: 'Reset atsign', onTap: _showResetDialog, @@ -232,3 +232,4 @@ class _ResetAppButtonState extends State { }); } } + diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/utility/switch_atsign.dart b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_switch_atsign_action.dart similarity index 85% rename from packages/sshnp_gui/lib/src/presentation/widgets/utility/switch_atsign.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_switch_atsign_action.dart index 2a7f32a66..a21a3b3eb 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/utility/switch_atsign.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_switch_atsign_action.dart @@ -5,17 +5,37 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnp_gui/src/controllers/authentication_controller.dart'; +import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_action_button.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; import 'package:sshnp_gui/src/repository/authentication_repository.dart'; +import 'package:sshnp_gui/src/repository/navigation_repository.dart'; -class AtSignBottomSheet extends ConsumerStatefulWidget { - const AtSignBottomSheet({Key? key}) : super(key: key); +class SettingsSwitchAtsignAction extends StatelessWidget { + const SettingsSwitchAtsignAction({Key? key}) : super(key: key); @override - ConsumerState createState() => _AtSignBottomSheetState(); + Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; + return SettingsActionButton( + icon: Icons.logout_rounded, + title: strings.switchAtsign, + onTap: () async { + await showModalBottomSheet( + context: NavigationRepository.navKey.currentContext!, + builder: (context) => const SwitchAtSignBottomSheet()); + }, + ); + } +} + +class SwitchAtSignBottomSheet extends ConsumerStatefulWidget { + const SwitchAtSignBottomSheet({Key? key}) : super(key: key); + + @override + ConsumerState createState() => _AtSignBottomSheetState(); } -class _AtSignBottomSheetState extends ConsumerState { +class _AtSignBottomSheetState extends ConsumerState { bool isLoading = false; @override diff --git a/packages/sshnp_gui/macos/Podfile.lock b/packages/sshnp_gui/macos/Podfile.lock index b1eda3d11..2d6c4cf07 100644 --- a/packages/sshnp_gui/macos/Podfile.lock +++ b/packages/sshnp_gui/macos/Podfile.lock @@ -87,4 +87,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.12.1 +COCOAPODS: 1.11.3 From cc424db5512f5173b17cd657cda6d1afc104c007 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 11:50:13 +0800 Subject: [PATCH 36/95] deps: cleanup dependencies --- .../presentation/screens/settings_screen.dart | 4 - packages/sshnp_gui/pubspec.lock | 186 +++++++++--------- packages/sshnp_gui/pubspec.yaml | 12 +- 3 files changed, 99 insertions(+), 103 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart index 05a17a70b..70973f906 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart @@ -1,11 +1,7 @@ -import 'package:at_backupkey_flutter/at_backupkey_flutter.dart'; -import 'package:at_contacts_flutter/services/contact_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:sshnp_gui/src/presentation/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_actions.dart'; -import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_action_button.dart'; -import 'package:sshnp_gui/src/repository/navigation_repository.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; class SettingsScreen extends StatelessWidget { diff --git a/packages/sshnp_gui/pubspec.lock b/packages/sshnp_gui/pubspec.lock index f5b80b462..c383b5a35 100644 --- a/packages/sshnp_gui/pubspec.lock +++ b/packages/sshnp_gui/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: archive - sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e" url: "https://pub.dev" source: hosted - version: "3.3.7" + version: "3.3.8" args: dependency: transitive description: @@ -58,7 +58,7 @@ packages: source: hosted version: "5.2.0" at_backupkey_flutter: - dependency: transitive + dependency: "direct main" description: name: at_backupkey_flutter sha256: "5e0eb67988f99f435076db43e2fdd96bb27647764243e1f74fe51dff529634c9" @@ -98,7 +98,7 @@ packages: source: hosted version: "3.2.12" at_common_flutter: - dependency: transitive + dependency: "direct main" description: name: at_common_flutter sha256: "7e2c9f9cee67651d61b7009d9c14148858ce8e251ed8239cf75e9fbc6cccd45e" @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: at_commons - sha256: d58706ff327a1ddb6fe96d93d41739ce454638b99c61f5475b4c1a4ab2c9de8e + sha256: f56c1828705593662060d9ba1fbc909f50e7f4e109fbc41482a5901232c260de url: "https://pub.dev" source: hosted - version: "3.0.53" + version: "3.0.54" at_contact: dependency: "direct main" description: @@ -125,10 +125,10 @@ packages: dependency: "direct main" description: name: at_contacts_flutter - sha256: "55931be963adb001606bdc7d6020cfa3171ac31d4633adab3cea418a62ad4448" + sha256: "966cc7094ffdf4973c21249d107e3d97e5f5b612dc2f1c6633e9618c9ab69351" url: "https://pub.dev" source: hosted - version: "4.0.5" + version: "4.0.11" at_file_saver: dependency: transitive description: @@ -165,18 +165,18 @@ packages: dependency: transitive description: name: at_persistence_secondary_server - sha256: a1b0e9819d6d22072caf15e52ea3bf459c8b161404ed92bb199bfd32f5ff63a9 + sha256: "016a98b48d43db19dcb8601c43cdecbcbe0ed60c298d1cfd3d6425f7439e7c30" url: "https://pub.dev" source: hosted - version: "3.0.52" + version: "3.0.57" at_persistence_spec: dependency: transitive description: name: at_persistence_spec - sha256: "2ee8f0433783633d2375dba2acf27f8778bcbcd40dda8659bf54f80925db224f" + sha256: ea8e550368ccee9150247ae7abdc256dccf78467bb48c11d1d0d66b843b21ba7 url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.0.14" at_server_status: dependency: transitive description: @@ -210,7 +210,7 @@ packages: source: hosted version: "3.0.15" biometric_storage: - dependency: transitive + dependency: "direct main" description: name: biometric_storage sha256: "2bae7ce64d4e3a390f8adfd0373ed1a82d567e3692e16a1bd0f72f91fb962ae3" @@ -293,10 +293,10 @@ packages: dependency: transitive description: name: cross_file - sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb url: "https://pub.dev" source: hosted - version: "0.3.3+4" + version: "0.3.3+5" crypto: dependency: transitive description: @@ -357,10 +357,10 @@ packages: dependency: transitive description: name: elliptic - sha256: "8c7396126c81c574fe970ac4afe9ba919b1ca754da20b509664be2345ffb2845" + sha256: "98e2fa89a714c649174553c823db2612dc9581814477fe1264a499d448237b6b" url: "https://pub.dev" source: hosted - version: "0.3.8" + version: "0.3.10" encrypt: dependency: transitive description: @@ -389,10 +389,10 @@ packages: dependency: transitive description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" file: dependency: transitive description: @@ -453,10 +453,10 @@ packages: dependency: transitive description: name: file_selector_platform_interface - sha256: "412705a646a0ae90f33f37acfae6a0f7cbc02222d6cd34e479421c3e74d3853c" + sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.6.1" file_selector_web: dependency: transitive description: @@ -498,18 +498,18 @@ packages: dependency: transitive description: name: flutter_keychain - sha256: "777ea8d3e1f55536bc8489a9ced73a912da4065645d9a1f751aae3548825b140" + sha256: f41a276e877453e70afcbf8e77e33203eab6f60a42f8296f9f3994e69fa6214c url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" flutter_localizations: dependency: "direct main" description: flutter @@ -519,10 +519,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.16" flutter_pty: dependency: "direct main" description: @@ -535,18 +535,18 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: b83ac5827baadefd331ea1d85110f34645827ea234ccabf53a655f41901a9bf4 + sha256: "1bd39b04f1bcd217a969589777ca6bd642d116e3e5de65c3e6a8e8bdd8b178ec" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.4.0" flutter_slidable: dependency: transitive description: name: flutter_slidable - sha256: c7607eb808cdef19c8468246e95a133308aeaeb3971cdd9edfb9d5e31cedfbe9 + sha256: cc4231579e3eae41ae166660df717f4bad1359c87f4a4322ad8ba1befeb3d2be url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "3.0.0" flutter_svg: dependency: "direct main" description: @@ -585,10 +585,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: b33a88c67816312597e5e0f5906c5139a0b9bd9bb137346e872c788da7af8ea0 + sha256: edbceb4c06758652fc9d12e58edb371cd977011b032e298dae0eb357167baad2 url: "https://pub.dev" source: hosted - version: "9.0.3" + version: "9.1.1" hive: dependency: transitive description: @@ -697,10 +697,10 @@ packages: dependency: transitive description: name: macos_window_utils - sha256: "43a90473f8786f00f07203e6819dab67e032f8896dafa4a6f85fbc71fba32c0b" + sha256: c51aba4b8517d1b3dc3d3b0653eff4f84f318040d0f01ea0da4e64298ac76024 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: @@ -889,10 +889,10 @@ packages: dependency: transitive description: name: permission_handler_platform_interface - sha256: "7c6b1500385dd1d2ca61bb89e2488ca178e274a69144d26bbd65e33eae7c02a9" + sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2 url: "https://pub.dev" source: hosted - version: "3.11.3" + version: "3.11.5" permission_handler_windows: dependency: transitive description: @@ -929,10 +929,10 @@ packages: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" platform_info: dependency: transitive description: @@ -945,10 +945,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.6" pointycastle: dependency: transitive description: @@ -1001,10 +1001,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "80e48bebc83010d5e67a11c9514af6b44bbac1ec77b4333c8ea65dbc79e2d8ef" + sha256: a600120d6f213a9922860eea1abc32597436edd5b2c4e73b91410f8c2af67d22 url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.4.0" share_plus: dependency: transitive description: @@ -1025,58 +1025,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4 + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" + sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d + sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" shelf: dependency: transitive description: @@ -1181,10 +1181,10 @@ packages: dependency: transitive description: name: state_notifier - sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb url: "https://pub.dev" source: hosted - version: "0.7.2+1" + version: "1.0.0" stream_channel: dependency: transitive description: @@ -1258,7 +1258,7 @@ packages: source: hosted version: "1.3.2" url_launcher: - dependency: transitive + dependency: "direct main" description: name: url_launcher sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" @@ -1269,60 +1269,60 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03" + sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 url: "https://pub.dev" source: hosted - version: "6.0.36" + version: "6.1.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.1.5" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea + sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 + sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" url: "https://pub.dev" source: hosted - version: "2.0.18" + version: "2.0.20" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" + sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.0.8" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" @@ -1373,10 +1373,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b8c67f5fa3897b122cf60fe9ff314f7b0ef71eab25c5f8b771480bc338f48823 + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 url: "https://pub.dev" source: hosted - version: "11.7.2" + version: "11.10.0" watcher: dependency: transitive description: @@ -1405,50 +1405,50 @@ packages: dependency: transitive description: name: webkit_inspection_protocol - sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" webview_flutter: dependency: transitive description: name: webview_flutter - sha256: "789d52bd789373cc1e100fb634af2127e86c99cf9abde09499743270c5de8d00" + sha256: "82f6787d5df55907aa01e49bd9644f4ed1cc82af7a8257dd9947815959d2e755" url: "https://pub.dev" source: hosted - version: "4.2.2" + version: "4.2.4" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: "27ad6a99c4b2d5e1ffd2b993a10f738b6b4979f139b4d64c34ac511595fcd748" + sha256: "0d8f5ac96a155e672129bf94c7abf625de01241d44d269dbaff083f1b4deb1aa" url: "https://pub.dev" source: hosted - version: "3.9.0" + version: "3.9.5" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "564ef378cafc1a0e29f1d76ce175ef517a0a6115875dff7b43fccbef2b0aeb30" + sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.6.0" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: "369fdf6160944a7db660ff15fa048c2bd681b09557907beaef1f95e8557d21dc" + sha256: d2f7241849582da80b79acb03bb936422412ce5c0c79fb5f6a1de5421a5aecc4 url: "https://pub.dev" source: hosted - version: "3.7.0" + version: "3.7.4" win32: dependency: transitive description: name: win32 - sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 + sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" url: "https://pub.dev" source: hosted - version: "5.0.6" + version: "5.0.7" win32_registry: dependency: transitive description: @@ -1461,10 +1461,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.3" xml: dependency: transitive description: @@ -1498,5 +1498,5 @@ packages: source: hosted version: "0.2.0" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.10.0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" diff --git a/packages/sshnp_gui/pubspec.yaml b/packages/sshnp_gui/pubspec.yaml index 0243040ec..ccb6d4b22 100644 --- a/packages/sshnp_gui/pubspec.yaml +++ b/packages/sshnp_gui/pubspec.yaml @@ -8,14 +8,14 @@ environment: dependencies: at_app_flutter: ^5.0.1 - # at_backupkey_flutter: ^4.0.10 - at_client_mobile: ^3.2.6 - # at_common_flutter: ^2.0.12 + at_backupkey_flutter: ^4.0.10 + at_client_mobile: ^3.2.11 + at_common_flutter: ^2.0.12 at_contact: ^3.0.7 at_contacts_flutter: ^4.0.5 at_onboarding_flutter: ^6.1.0 at_utils: ^3.0.11 - # biometric_storage: ^4.1.3 + biometric_storage: ^5.0.0+4 flutter: sdk: flutter flutter_dotenv: ^5.0.2 @@ -33,8 +33,8 @@ dependencies: shared_preferences: ^2.2.0 sshnoports: path: ../sshnoports/ - # url_launcher: ^6.1.14 - # uuid: ^3.0.7 + url_launcher: ^6.1.14 + uuid: ^3.0.7 xterm: ^3.5.0 dev_dependencies: From 10d51442ccd184244efa4c12876383946325d7aa Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 12:06:47 +0800 Subject: [PATCH 37/95] refactor: nav rail controller --- packages/sshnp_gui/lib/main.dart | 6 +- .../src/controllers/nav_rail_controller.dart | 13 ---- .../controllers/navigation_controller.dart | 74 +++++++++++++++++++ .../navigation_rail_controller.dart | 48 ++++++++++++ .../navigation/app_navigation_rail.dart | 33 +++------ .../src/presentation/screens/home_screen.dart | 2 +- .../screens/onboarding_screen.dart | 2 +- .../screens/profile_editor_screen.dart | 2 +- .../presentation/screens/settings_screen.dart | 2 +- .../presentation/screens/terminal_screen.dart | 2 +- .../new_profile_action.dart | 2 +- .../profile_actions/profile_edit_action.dart | 2 +- .../profile_terminal_action.dart | 6 +- .../widgets/profile_form/profile_form.dart | 8 +- .../repository/authentication_repository.dart | 2 +- .../sshnp_gui/lib/src/utils/app_router.dart | 67 ----------------- 16 files changed, 152 insertions(+), 119 deletions(-) delete mode 100644 packages/sshnp_gui/lib/src/controllers/nav_rail_controller.dart create mode 100644 packages/sshnp_gui/lib/src/controllers/navigation_controller.dart create mode 100644 packages/sshnp_gui/lib/src/controllers/navigation_rail_controller.dart rename packages/sshnp_gui/lib/src/{presentation => }/navigation/app_navigation_rail.dart (56%) delete mode 100644 packages/sshnp_gui/lib/src/utils/app_router.dart diff --git a/packages/sshnp_gui/lib/main.dart b/packages/sshnp_gui/lib/main.dart index 6afe47b27..45f159cf8 100644 --- a/packages/sshnp_gui/lib/main.dart +++ b/packages/sshnp_gui/lib/main.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:macos_ui/macos_ui.dart'; -import 'package:sshnp_gui/src/utils/app_router.dart'; +import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; import 'package:sshnp_gui/src/utils/theme.dart'; import 'package:sshnp_gui/src/utils/util.dart'; @@ -45,7 +45,7 @@ class MyApp extends ConsumerWidget { title: 'SSHNP', localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - routerConfig: ref.watch(goRouterProvider), + routerConfig: ref.watch(navigationController), theme: AppTheme.dark(), // * The onboarding screen (first screen)p[] ); @@ -61,7 +61,7 @@ class MyMacApp extends ConsumerWidget { title: 'SSHNP', localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - routerConfig: ref.watch(goRouterProvider), + routerConfig: ref.watch(navigationController), theme: AppTheme.macosDark(), darkTheme: AppTheme.macosDark(), themeMode: ThemeMode.dark, diff --git a/packages/sshnp_gui/lib/src/controllers/nav_rail_controller.dart b/packages/sshnp_gui/lib/src/controllers/nav_rail_controller.dart deleted file mode 100644 index 89948da53..000000000 --- a/packages/sshnp_gui/lib/src/controllers/nav_rail_controller.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sshnp_gui/src/utils/app_router.dart'; - -final navRailController = AutoDisposeNotifierProvider(NavRailController.new); - -class NavRailController extends AutoDisposeNotifier { - @override - AppRoute build() => AppRoute.home; - - void setRoute(AppRoute route) { - state = route; - } -} diff --git a/packages/sshnp_gui/lib/src/controllers/navigation_controller.dart b/packages/sshnp_gui/lib/src/controllers/navigation_controller.dart new file mode 100644 index 000000000..a2117546c --- /dev/null +++ b/packages/sshnp_gui/lib/src/controllers/navigation_controller.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:sshnp_gui/src/presentation/screens/home_screen.dart'; +import 'package:sshnp_gui/src/presentation/screens/onboarding_screen.dart'; +import 'package:sshnp_gui/src/presentation/screens/profile_editor_screen.dart'; +import 'package:sshnp_gui/src/presentation/screens/settings_screen.dart'; +import 'package:sshnp_gui/src/presentation/screens/terminal_screen.dart'; +import 'package:sshnp_gui/src/repository/navigation_repository.dart'; + +enum AppRoute { + onboarding, + home, + profileForm, + terminal, + settings, +} + +final navigationController = Provider( + (ref) => GoRouter( + navigatorKey: NavigationRepository.navKey, + initialLocation: '/', + debugLogDiagnostics: false, + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const OnboardingScreen(), + name: AppRoute.onboarding.name, + routes: [ + GoRoute( + path: 'home', + name: AppRoute.home.name, + pageBuilder: (context, state) => CustomTransitionPage( + key: state.pageKey, + child: const HomeScreen(), + transitionsBuilder: ((context, animation, secondaryAnimation, child) => + FadeTransition(opacity: animation, child: child)), + ), + ), + GoRoute( + path: 'new', + name: AppRoute.profileForm.name, + pageBuilder: (context, state) => CustomTransitionPage( + key: state.pageKey, + child: const ProfileEditorScreen(), + transitionsBuilder: ((context, animation, secondaryAnimation, child) => + FadeTransition(opacity: animation, child: child)), + ), + ), + GoRoute( + path: 'terminal', + name: AppRoute.terminal.name, + pageBuilder: (context, state) => CustomTransitionPage( + key: state.pageKey, + child: const TerminalScreen(), + transitionsBuilder: ((context, animation, secondaryAnimation, child) => + FadeTransition(opacity: animation, child: child)), + ), + ), + GoRoute( + path: 'settings', + name: AppRoute.settings.name, + pageBuilder: (context, state) => CustomTransitionPage( + key: state.pageKey, + child: const SettingsScreen(), + transitionsBuilder: ((context, animation, secondaryAnimation, child) => + FadeTransition(opacity: animation, child: child)), + ), + ), + ], + ), + ], + ), +); diff --git a/packages/sshnp_gui/lib/src/controllers/navigation_rail_controller.dart b/packages/sshnp_gui/lib/src/controllers/navigation_rail_controller.dart new file mode 100644 index 000000000..6ec233f6e --- /dev/null +++ b/packages/sshnp_gui/lib/src/controllers/navigation_rail_controller.dart @@ -0,0 +1,48 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; + +final navigationRailController = + AutoDisposeNotifierProvider(NavigationRailController.new); + +class NavigationRailController extends AutoDisposeNotifier { + @override + AppRoute build() => AppRoute.home; + + final routes = [ + AppRoute.home, + AppRoute.terminal, + AppRoute.settings, + ]; + + AppRoute getRoute(int index) { + return routes[index]; + } + + int indexOf(AppRoute route) { + return routes.indexOf(route); + } + + bool isCurrentIndex(AppRoute route) { + return state == route; + } + + int getCurrentIndex() { + return indexOf(state); + } + + AppRoute getCurrentRoute() { + return getRoute(getCurrentIndex()); + } + + bool setIndex(int index) { + if (index < 0 || index >= routes.length) return false; + state = routes[index]; + return true; + } + + bool setRoute(AppRoute route) { + if (!routes.contains(route)) return false; + state = route; + return true; + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/navigation/app_navigation_rail.dart b/packages/sshnp_gui/lib/src/navigation/app_navigation_rail.dart similarity index 56% rename from packages/sshnp_gui/lib/src/presentation/navigation/app_navigation_rail.dart rename to packages/sshnp_gui/lib/src/navigation/app_navigation_rail.dart index cd226ec13..896865e52 100644 --- a/packages/sshnp_gui/lib/src/presentation/navigation/app_navigation_rail.dart +++ b/packages/sshnp_gui/lib/src/navigation/app_navigation_rail.dart @@ -2,22 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; -import 'package:sshnp_gui/src/utils/app_router.dart'; +import 'package:sshnp_gui/src/controllers/navigation_rail_controller.dart'; +import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; class AppNavigationRail extends ConsumerWidget { const AppNavigationRail({super.key}); - static const routes = [ - AppRoute.home, - AppRoute.terminal, - AppRoute.settings, - ]; - - static int getRouteIndex(AppRoute route) { - return routes.indexOf(route); - } - static var activatedIcons = [ SvgPicture.asset('assets/images/nav_icons/home_selected.svg'), SvgPicture.asset('assets/images/nav_icons/pican_selected.svg'), @@ -32,23 +22,24 @@ class AppNavigationRail extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final currentIndex = getRouteIndex(ref.watch(navRailController)); + final controller = ref.watch(navigationRailController.notifier); + final currentIndex = controller.getCurrentIndex(); return NavigationRail( - destinations: routes + destinations: controller.routes .map( - (i) => NavigationRailDestination( - icon: (currentIndex == getRouteIndex(i)) - ? activatedIcons[getRouteIndex(i)] - : deactivatedIcons[getRouteIndex(i)], + (AppRoute route) => NavigationRailDestination( + icon: (controller.isCurrentIndex(route)) + ? activatedIcons[controller.indexOf(route)] + : deactivatedIcons[controller.indexOf(route)], label: const Text(''), ), ) .toList(), - selectedIndex: routes.indexOf(ref.watch(navRailController)), + selectedIndex: currentIndex, onDestinationSelected: (int selectedIndex) { - ref.read(navRailController.notifier).setRoute(routes[selectedIndex]); - context.goNamed(routes[selectedIndex].name); + controller.setIndex(selectedIndex); + context.goNamed(controller.getCurrentRoute().name); }, ); } diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index 78b3d452c..8a3de6879 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_actions.dart'; -import 'package:sshnp_gui/src/presentation/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/screens/onboarding_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/onboarding_screen.dart index 5f4d6bc45..0c07f485b 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/onboarding_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/onboarding_screen.dart @@ -8,7 +8,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:macos_ui/macos_ui.dart'; import 'package:path_provider/path_provider.dart' show getApplicationSupportDirectory; -import 'package:sshnp_gui/src/utils/app_router.dart'; +import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; class OnboardingScreen extends StatefulWidget { const OnboardingScreen({super.key}); diff --git a/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart index 3c239a7b7..7d0edc82d 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:sshnp_gui/src/presentation/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_form/profile_form.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart index 70973f906..33a010323 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:sshnp_gui/src/presentation/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_actions.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index 3abda43c4..0e9878fb6 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -5,7 +5,7 @@ import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; -import 'package:sshnp_gui/src/presentation/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; import 'package:xterm/xterm.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart index dcf44af9f..99699dc16 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; -import 'package:sshnp_gui/src/utils/app_router.dart'; +import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; class NewProfileAction extends ConsumerStatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart index 2aba5add4..ff2be4e0d 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart @@ -5,7 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; -import 'package:sshnp_gui/src/utils/app_router.dart'; +import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; class ProfileEditAction extends ConsumerStatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart index 180a0dfd4..40306c48d 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart @@ -4,11 +4,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; -import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; +import 'package:sshnp_gui/src/controllers/navigation_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; -import 'package:sshnp_gui/src/utils/app_router.dart'; +import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; class ProfileTerminalAction extends ConsumerStatefulWidget { final SSHNPParams params; @@ -50,7 +50,7 @@ class _ProfileTerminalActionState extends ConsumerState { if (result is SSHNPCommandResult) { /// Set the command for the new session sessionController.setProcess(command: result.command, args: result.args); - ref.read(navRailController.notifier).setRoute(AppRoute.terminal); + ref.read(navigationRailController.notifier).setRoute(AppRoute.terminal); if (mounted) { context.pushReplacementNamed(AppRoute.terminal.name); } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index 3c7ec746c..df5e92edb 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -4,10 +4,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/nav_rail_controller.dart'; +import 'package:sshnp_gui/src/controllers/navigation_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_form/custom_text_form_field.dart'; -import 'package:sshnp_gui/src/utils/app_router.dart'; +import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; import 'package:sshnp_gui/src/utils/enum.dart'; import 'package:sshnp_gui/src/utils/sizes.dart'; import 'package:sshnp_gui/src/utils/validator.dart'; @@ -52,7 +52,7 @@ class _ProfileFormState extends ConsumerState { await controller.create(config); } if (context.mounted) { - ref.read(navRailController.notifier).setRoute(AppRoute.home); + ref.read(navigationRailController.notifier).setRoute(AppRoute.home); context.pushReplacementNamed(AppRoute.home.name); } } @@ -258,7 +258,7 @@ class _ProfileFormState extends ConsumerState { gapW8, TextButton( onPressed: () { - ref.read(navRailController.notifier).setRoute(AppRoute.home); + ref.read(navigationRailController.notifier).setRoute(AppRoute.home); context.pushReplacementNamed(AppRoute.home.name); }, child: Text(strings.cancel), diff --git a/packages/sshnp_gui/lib/src/repository/authentication_repository.dart b/packages/sshnp_gui/lib/src/repository/authentication_repository.dart index f01fabd31..58e6552e7 100644 --- a/packages/sshnp_gui/lib/src/repository/authentication_repository.dart +++ b/packages/sshnp_gui/lib/src/repository/authentication_repository.dart @@ -12,8 +12,8 @@ import 'package:go_router/go_router.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; +import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; import 'package:sshnp_gui/src/repository/navigation_repository.dart'; -import 'package:sshnp_gui/src/utils/app_router.dart'; /// A singleton that makes all the network calls to the @platform. class AuthenticationRepository { diff --git a/packages/sshnp_gui/lib/src/utils/app_router.dart b/packages/sshnp_gui/lib/src/utils/app_router.dart deleted file mode 100644 index 102cd9c5d..000000000 --- a/packages/sshnp_gui/lib/src/utils/app_router.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/presentation/screens/home_screen.dart'; -import 'package:sshnp_gui/src/presentation/screens/onboarding_screen.dart'; -import 'package:sshnp_gui/src/presentation/screens/profile_editor_screen.dart'; -import 'package:sshnp_gui/src/presentation/screens/settings_screen.dart'; -import 'package:sshnp_gui/src/presentation/screens/terminal_screen.dart'; -import 'package:sshnp_gui/src/repository/navigation_repository.dart'; - -enum AppRoute { - onboarding, - home, - profileForm, - terminal, - settings, -} - -final goRouterProvider = Provider((ref) => GoRouter( - navigatorKey: NavigationRepository.navKey, - initialLocation: '/', - debugLogDiagnostics: false, - routes: [ - GoRoute( - path: '/', - builder: (context, state) => const OnboardingScreen(), - name: AppRoute.onboarding.name, - routes: [ - GoRoute( - path: 'home', - name: AppRoute.home.name, - pageBuilder: (context, state) => CustomTransitionPage( - key: state.pageKey, - child: const HomeScreen(), - transitionsBuilder: ((context, animation, secondaryAnimation, child) => - FadeTransition(opacity: animation, child: child))), - ), - GoRoute( - path: 'new', - name: AppRoute.profileForm.name, - pageBuilder: (context, state) => CustomTransitionPage( - key: state.pageKey, - child: const ProfileEditorScreen(), - transitionsBuilder: ((context, animation, secondaryAnimation, child) => - FadeTransition(opacity: animation, child: child))), - ), - GoRoute( - path: 'terminal', - name: AppRoute.terminal.name, - pageBuilder: (context, state) => CustomTransitionPage( - key: state.pageKey, - child: const TerminalScreen(), - transitionsBuilder: ((context, animation, secondaryAnimation, child) => - FadeTransition(opacity: animation, child: child))), - ), - GoRoute( - path: 'settings', - name: AppRoute.settings.name, - pageBuilder: (context, state) => CustomTransitionPage( - key: state.pageKey, - child: const SettingsScreen(), - transitionsBuilder: ((context, animation, secondaryAnimation, child) => - FadeTransition(opacity: animation, child: child))), - ), - ]), - ], - )); From be53df04f0114b07db8278cae498063272b854bf Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 14:01:37 +0800 Subject: [PATCH 38/95] refactor: navigation and terminal tabs --- packages/sshnp_gui/lib/main.dart | 54 ++---------- .../controllers/sshnp_params_controller.dart | 4 +- .../terminal_session_controller.dart | 71 +++++++++++++-- .../src/presentation/screens/home_screen.dart | 4 +- .../screens/profile_editor_screen.dart | 4 +- .../presentation/screens/settings_screen.dart | 4 +- .../presentation/screens/terminal_screen.dart | 87 ++++++++++++------- .../new_profile_action.dart | 1 - .../navigation/app_navigation_rail.dart | 0 .../profile_delete_action.dart | 2 +- .../profile_actions/profile_edit_action.dart | 1 - .../profile_terminal_action.dart | 1 + .../widgets/profile_form/profile_form.dart | 15 ++-- .../settings_action_button.dart | 4 +- .../settings_reset_app_action.dart | 10 +-- .../widgets/utility}/at_error_dialog.dart | 0 .../theme.dart => utility/app_theme.dart} | 5 +- .../lib/src/{utils => utility}/constants.dart | 0 .../form_validator.dart} | 4 +- .../default_platform_utility.dart | 35 ++++++++ .../platform_utility/macos_utility.dart | 49 +++++++++++ .../platform_utility/platform_utililty.dart | 29 +++++++ .../lib/src/{utils => utility}/sizes.dart | 0 packages/sshnp_gui/lib/src/utils/enum.dart | 1 - packages/sshnp_gui/lib/src/utils/util.dart | 13 --- 25 files changed, 272 insertions(+), 126 deletions(-) rename packages/sshnp_gui/lib/src/{ => presentation/widgets}/navigation/app_navigation_rail.dart (100%) rename packages/sshnp_gui/lib/src/{utils => presentation/widgets/utility}/at_error_dialog.dart (100%) rename packages/sshnp_gui/lib/src/{utils/theme.dart => utility/app_theme.dart} (98%) rename packages/sshnp_gui/lib/src/{utils => utility}/constants.dart (100%) rename packages/sshnp_gui/lib/src/{utils/validator.dart => utility/form_validator.dart} (84%) create mode 100644 packages/sshnp_gui/lib/src/utility/platform_utility/default_platform_utility.dart create mode 100644 packages/sshnp_gui/lib/src/utility/platform_utility/macos_utility.dart create mode 100644 packages/sshnp_gui/lib/src/utility/platform_utility/platform_utililty.dart rename packages/sshnp_gui/lib/src/{utils => utility}/sizes.dart (100%) delete mode 100644 packages/sshnp_gui/lib/src/utils/enum.dart delete mode 100644 packages/sshnp_gui/lib/src/utils/util.dart diff --git a/packages/sshnp_gui/lib/main.dart b/packages/sshnp_gui/lib/main.dart index 45f159cf8..08d7b6796 100644 --- a/packages/sshnp_gui/lib/main.dart +++ b/packages/sshnp_gui/lib/main.dart @@ -7,8 +7,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:macos_ui/macos_ui.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; -import 'package:sshnp_gui/src/utils/theme.dart'; -import 'package:sshnp_gui/src/utils/util.dart'; +import 'package:sshnp_gui/src/utility/app_theme.dart'; +import 'package:sshnp_gui/src/utility/platform_utility/platform_utililty.dart'; final AtSignLogger _logger = AtSignLogger(AtEnv.appNamespace); @@ -21,51 +21,7 @@ Future main() async { _logger.finer('Environment failed to load from .env: ', e); } - /// This method initializes macos_window_utils and styles the window. - Future _configureMacosWindowUtils() async { - const config = MacosWindowUtilsConfig(toolbarStyle: NSWindowToolbarStyle.unified); - await config.apply(); - } - - if (Util.isMacos()) { - // await _configureMacosWindowUtils(); - - runApp(const ProviderScope(child: MyApp())); - } else { - runApp(const ProviderScope(child: MyApp())); - } -} - -class MyApp extends ConsumerWidget { - const MyApp({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return MaterialApp.router( - title: 'SSHNP', - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - routerConfig: ref.watch(navigationController), - theme: AppTheme.dark(), - // * The onboarding screen (first screen)p[] - ); - } -} - -class MyMacApp extends ConsumerWidget { - const MyMacApp({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return MacosApp.router( - title: 'SSHNP', - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - routerConfig: ref.watch(navigationController), - theme: AppTheme.macosDark(), - darkTheme: AppTheme.macosDark(), - themeMode: ThemeMode.dark, - // * The onboarding screen (first screen)p[] - ); - } + PlatformUtility platformUtility = PlatformUtility.current(); + await platformUtility.configurePlatform(); + runApp(ProviderScope(child: platformUtility.app)); } diff --git a/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart b/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart index 9c8536c3a..c23eb5545 100644 --- a/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart @@ -3,7 +3,9 @@ import 'dart:async'; import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/utils/enum.dart'; + +enum ConfigFileWriteState { create, update } + /// A provider that exposes the [SSHNPParamsController] to the app. final sshnpParamsController = AutoDisposeNotifierProvider( diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart index 9f2b72120..fc0883d9a 100644 --- a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -22,6 +22,11 @@ final terminalSessionFamilyController = TerminalSessionFamilyController.new, ); +final terminalSessionProfileNameFamilyCounter = + NotifierProviderFamily( + TerminalSessionProfileNameFamilyCounter.new, +); + /// Controller for the id of the currently active terminal session class TerminalSessionController extends Notifier { @override @@ -29,9 +34,13 @@ class TerminalSessionController extends Notifier { String createSession() { state = const Uuid().v4(); - ref.read(terminalSessionListController.notifier).add(state); + ref.read(terminalSessionListController.notifier)._add(state); return state; } + + void setSession(String sessionId) { + state = sessionId; + } } /// Controller for the list of all terminal session ids @@ -39,11 +48,11 @@ class TerminalSessionListController extends Notifier> { @override List build() => []; - void add(String sessionId) { + void _add(String sessionId) { state.add(sessionId); } - void remove(String sessionId) { + void _remove(String sessionId) { state.remove(sessionId); } } @@ -52,12 +61,17 @@ class TerminalSession { final String sessionId; final Terminal terminal; + String? _profileName; + String displayName; + late Pty pty; bool isRunning = false; String? command; List args = const []; - TerminalSession(this.sessionId) : terminal = Terminal(maxLines: 10000); + TerminalSession(this.sessionId) + : terminal = Terminal(maxLines: 10000), + displayName = sessionId; } /// Controller for the family of terminal session [TerminalController]s @@ -67,6 +81,14 @@ class TerminalSessionFamilyController extends FamilyNotifier state.displayName; + + void issueDisplayName(String profileName) { + state._profileName = profileName; + state.displayName = + ref.read(terminalSessionProfileNameFamilyCounter(profileName).notifier)._addSession(state.sessionId); + } + void setProcess({String? command, List args = const []}) { state.command = command; state.args = args; @@ -107,8 +129,47 @@ class TerminalSessionFamilyController extends FamilyNotifier { + @override + int build(String arg) => 0; + + final List _sessionQueue = []; + + String _addSession(String sessionId) { + state++; + for (int i = 0; i < _sessionQueue.length; i++) { + if (_sessionQueue[i] == null) { + _sessionQueue[i] = sessionId; + return '$arg-${i + 1}'; + } + } + _sessionQueue.add(sessionId); + return '$arg-${_sessionQueue.length}'; + } + + bool _removeSession(String sessionId) { + for (int i = 0; i < _sessionQueue.length; i++) { + if (_sessionQueue[i] == sessionId) { + _sessionQueue[i] = null; + state--; + return true; + } + } + return false; + } } diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index 8a3de6879..04ae88541 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -4,9 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_actions.dart'; -import 'package:sshnp_gui/src/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar.dart'; -import 'package:sshnp_gui/src/utils/sizes.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; // * Once the onboarding process is completed you will be taken to this screen class HomeScreen extends ConsumerStatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart index 7d0edc82d..4514ce0ce 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/profile_editor_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:sshnp_gui/src/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_form/profile_form.dart'; -import 'package:sshnp_gui/src/utils/sizes.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; // * Once the onboarding process is completed you will be taken to this screen class ProfileEditorScreen extends StatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart index 33a010323..0446fa381 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:sshnp_gui/src/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_actions.dart'; -import 'package:sshnp_gui/src/utils/sizes.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; class SettingsScreen extends StatelessWidget { const SettingsScreen({Key? key}) : super(key: key); diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index 0e9878fb6..6245d98ed 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -5,8 +5,8 @@ import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; -import 'package:sshnp_gui/src/navigation/app_navigation_rail.dart'; -import 'package:sshnp_gui/src/utils/sizes.dart'; +import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; import 'package:xterm/xterm.dart'; // * Once the onboarding process is completed you will be taken to this screen @@ -17,7 +17,7 @@ class TerminalScreen extends ConsumerStatefulWidget { ConsumerState createState() => _TerminalScreenState(); } -class _TerminalScreenState extends ConsumerState { +class _TerminalScreenState extends ConsumerState with TickerProviderStateMixin { final terminalController = TerminalController(); late final Pty pty; @@ -38,16 +38,44 @@ class _TerminalScreenState extends ConsumerState { super.dispose(); } + void deleteTab(String sessionId) { + final controller = ref.read(terminalSessionFamilyController(sessionId).notifier); + final terminalList = ref.watch(terminalSessionListController); + final currentSessionId = ref.read(terminalSessionController); + final currentIndex = terminalList.indexOf(currentSessionId); + + // If the session we are deleting is the active session + // we need to set a new active session + if (currentSessionId == sessionId) { + if (currentIndex > 0) { + // set active terminal to the one immediately to the left + ref.read(terminalSessionController.notifier).setSession(terminalList[currentIndex - 1]); + } else if (terminalList.length > 1) { + // set active terminal to the one immediately to the right + ref.read(terminalSessionController.notifier).setSession(terminalList[currentIndex + 1]); + } else { + // no other sessions available, set active terminal to empty string + ref.read(terminalSessionController.notifier).setSession(''); + } + } + + controller.dispose(); + setState(() {}); + } + @override Widget build(BuildContext context) { // * Getting the AtClientManager instance to use below - final sessionId = ref.watch(terminalSessionController); - final terminalSession = ref.watch(terminalSessionFamilyController(sessionId)); final terminalList = ref.watch(terminalSessionListController); - if (sessionId.isEmpty) { - // for now, just return a normal shell prompt - terminalSession.command = Platform.environment['SHELL'] ?? 'bash'; + final currentSessionId = ref.watch(terminalSessionController); + late final int currentIndex; + if (terminalList.isEmpty) { + currentIndex = 0; + } else { + currentIndex = terminalList.indexOf(currentSessionId); } + final tabController = TabController(initialIndex: currentIndex, length: terminalList.length, vsync: this); + return Scaffold( body: SafeArea( child: Row( @@ -65,32 +93,33 @@ class _TerminalScreenState extends ConsumerState { ), gapH24, TabBar( + controller: tabController, isScrollable: true, - tabs: terminalList - .map( - (e) => Tab( - // text: e, - child: Row( - children: [ - Text(e), - IconButton( - icon: const Icon(Icons.close), - onPressed: () { - ref.read(terminalSessionListController.notifier).remove(e); - setState(() {}); - }, - ) - ], - )), - ) - .toList(), + onTap: (index) { + ref.read(terminalSessionController.notifier).setSession(terminalList[index]); + }, + tabs: terminalList.map((String sessionId) { + final displayName = ref.read(terminalSessionFamilyController(sessionId).notifier).displayName; + return Tab( + // text: e, + child: Row( + children: [ + Text(displayName), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => deleteTab(sessionId), + ) + ], + ), + ); + }).toList(), ), Expanded( child: TabBarView( - children: terminalList.map((e) { + controller: tabController, + children: terminalList.map((String sessionId) { return TerminalView( - terminalSession.terminal, - // ref.read(terminalSessionFamilyController(e).terminal), + ref.watch(terminalSessionFamilyController(sessionId)).terminal, controller: terminalController, autofocus: true, ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart index 99699dc16..3e25b261a 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart @@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; -import 'package:sshnp_gui/src/utils/enum.dart'; class NewProfileAction extends ConsumerStatefulWidget { const NewProfileAction({Key? key}) : super(key: key); diff --git a/packages/sshnp_gui/lib/src/navigation/app_navigation_rail.dart b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart similarity index 100% rename from packages/sshnp_gui/lib/src/navigation/app_navigation_rail.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart index f71a3c534..3e2f47b56 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; -import 'package:sshnp_gui/src/utils/sizes.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; class ProfileDeleteAction extends StatelessWidget { final SSHNPParams params; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart index ff2be4e0d..133ae156c 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart @@ -6,7 +6,6 @@ import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; -import 'package:sshnp_gui/src/utils/enum.dart'; class ProfileEditAction extends ConsumerStatefulWidget { final SSHNPParams params; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart index 40306c48d..dcfa84bb2 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart @@ -50,6 +50,7 @@ class _ProfileTerminalActionState extends ConsumerState { if (result is SSHNPCommandResult) { /// Set the command for the new session sessionController.setProcess(command: result.command, args: result.args); + sessionController.issueDisplayName(widget.params.profileName!); ref.read(navigationRailController.notifier).setRoute(AppRoute.terminal); if (mounted) { context.pushReplacementNamed(AppRoute.terminal.name); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index df5e92edb..9044a85d9 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -8,9 +8,8 @@ import 'package:sshnp_gui/src/controllers/navigation_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_form/custom_text_form_field.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; -import 'package:sshnp_gui/src/utils/enum.dart'; -import 'package:sshnp_gui/src/utils/sizes.dart'; -import 'package:sshnp_gui/src/utils/validator.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; +import 'package:sshnp_gui/src/utility/form_validator.dart'; class ProfileForm extends ConsumerStatefulWidget { const ProfileForm({super.key}); @@ -85,7 +84,7 @@ class _ProfileFormState extends ConsumerState { SSHNPPartialParams(profileName: value), ); }, - validator: Validator.validateRequiredField, + validator: FormValidator.validateRequiredField, ), gapW8, CustomTextFormField( @@ -108,7 +107,7 @@ class _ProfileFormState extends ConsumerState { newConfig, SSHNPPartialParams(sshnpdAtSign: value), ), - validator: Validator.validateAtsignField, + validator: FormValidator.validateAtsignField, ), gapW8, CustomTextFormField( @@ -118,7 +117,7 @@ class _ProfileFormState extends ConsumerState { newConfig, SSHNPPartialParams(host: value), ), - validator: Validator.validateRequiredField, + validator: FormValidator.validateRequiredField, ), ], ), @@ -132,7 +131,7 @@ class _ProfileFormState extends ConsumerState { newConfig, SSHNPPartialParams(sendSshPublicKey: value), ), - validator: Validator.validateRequiredField, + validator: FormValidator.validateRequiredField, ), gapW8, Row( @@ -173,7 +172,7 @@ class _ProfileFormState extends ConsumerState { newConfig, SSHNPPartialParams(port: int.tryParse(value)), ), - validator: Validator.validateRequiredField, + validator: FormValidator.validateRequiredField, ), ], ), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_action_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_action_button.dart index dc29456d3..1bb1b2b2f 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_action_button.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_action_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:sshnp_gui/src/utils/constants.dart'; -import 'package:sshnp_gui/src/utils/sizes.dart'; +import 'package:sshnp_gui/src/utility/constants.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; class SettingsActionButton extends StatelessWidget { const SettingsActionButton({ diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_reset_app_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_reset_app_action.dart index b28e78002..14dfcf166 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_reset_app_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/settings_actions/settings_reset_app_action.dart @@ -1,10 +1,10 @@ import 'package:at_onboarding_flutter/services/sdk_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:sshnp_gui/main.dart'; import 'package:sshnp_gui/src/presentation/widgets/settings_actions/settings_action_button.dart'; -import 'package:sshnp_gui/src/utils/at_error_dialog.dart'; -import 'package:sshnp_gui/src/utils/sizes.dart'; +import 'package:sshnp_gui/src/presentation/widgets/utility/at_error_dialog.dart'; +import 'package:sshnp_gui/src/utility/platform_utility/platform_utililty.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; /// Custom reset button widget is to reset an atsign from keychain list, @@ -212,9 +212,10 @@ class _SettingsResetAppActionState extends State { List? atsignsList = await SDKService().getAtsignList(); if (atsignsList == null || atsignsList.length < 2) { if (mounted) { + PlatformUtility platformUtility = PlatformUtility.current(); await Navigator.of(context).pushReplacement( MaterialPageRoute( - builder: (BuildContext context) => const MyApp(), + builder: (BuildContext context) => platformUtility.app, ), ); } @@ -232,4 +233,3 @@ class _SettingsResetAppActionState extends State { }); } } - diff --git a/packages/sshnp_gui/lib/src/utils/at_error_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/utility/at_error_dialog.dart similarity index 100% rename from packages/sshnp_gui/lib/src/utils/at_error_dialog.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/utility/at_error_dialog.dart diff --git a/packages/sshnp_gui/lib/src/utils/theme.dart b/packages/sshnp_gui/lib/src/utility/app_theme.dart similarity index 98% rename from packages/sshnp_gui/lib/src/utils/theme.dart rename to packages/sshnp_gui/lib/src/utility/app_theme.dart index 1963d4b95..e100b1828 100644 --- a/packages/sshnp_gui/lib/src/utils/theme.dart +++ b/packages/sshnp_gui/lib/src/utility/app_theme.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:macos_ui/macos_ui.dart'; -import 'package:sshnp_gui/src/utils/constants.dart'; -import 'package:sshnp_gui/src/utils/sizes.dart'; +import 'package:sshnp_gui/src/utility/constants.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; class AppTheme { + static TextTheme lightTextTheme = const TextTheme( // displayLarge: TextStyle( // fontSize: 80, diff --git a/packages/sshnp_gui/lib/src/utils/constants.dart b/packages/sshnp_gui/lib/src/utility/constants.dart similarity index 100% rename from packages/sshnp_gui/lib/src/utils/constants.dart rename to packages/sshnp_gui/lib/src/utility/constants.dart diff --git a/packages/sshnp_gui/lib/src/utils/validator.dart b/packages/sshnp_gui/lib/src/utility/form_validator.dart similarity index 84% rename from packages/sshnp_gui/lib/src/utils/validator.dart rename to packages/sshnp_gui/lib/src/utility/form_validator.dart index d190939e1..49700f57b 100644 --- a/packages/sshnp_gui/lib/src/utils/validator.dart +++ b/packages/sshnp_gui/lib/src/utility/form_validator.dart @@ -1,6 +1,6 @@ -import 'package:sshnp_gui/src/utils/constants.dart'; +import 'package:sshnp_gui/src/utility/constants.dart'; -class Validator { +class FormValidator { static String? validateRequiredField(String? value) { if (value!.isEmpty) { return kEmptyFieldValidationError; diff --git a/packages/sshnp_gui/lib/src/utility/platform_utility/default_platform_utility.dart b/packages/sshnp_gui/lib/src/utility/platform_utility/default_platform_utility.dart new file mode 100644 index 000000000..ec8f5fab3 --- /dev/null +++ b/packages/sshnp_gui/lib/src/utility/platform_utility/default_platform_utility.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; +import 'package:sshnp_gui/src/utility/app_theme.dart'; +import 'package:sshnp_gui/src/utility/platform_utility/platform_utililty.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class DefaultPlatformUtility implements PlatformUtility { + const DefaultPlatformUtility(); + + @override + void configurePlatform() {} + + @override + bool isPlatform() => true; + + @override + Widget get app => const _MyApp(); +} + +class _MyApp extends ConsumerWidget { + const _MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return MaterialApp.router( + title: 'SSHNP', + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + routerConfig: ref.watch(navigationController), + theme: AppTheme.dark(), + // * The onboarding screen (first screen)p[] + ); + } +} diff --git a/packages/sshnp_gui/lib/src/utility/platform_utility/macos_utility.dart b/packages/sshnp_gui/lib/src/utility/platform_utility/macos_utility.dart new file mode 100644 index 000000000..3429dab01 --- /dev/null +++ b/packages/sshnp_gui/lib/src/utility/platform_utility/macos_utility.dart @@ -0,0 +1,49 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; +import 'package:sshnp_gui/src/utility/app_theme.dart'; +import 'package:sshnp_gui/src/utility/platform_utility/default_platform_utility.dart'; +import 'package:sshnp_gui/src/utility/platform_utility/platform_utililty.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class MacosUtility implements PlatformUtility { + const MacosUtility(); + + @override + Future configurePlatform() async { + return; + const config = MacosWindowUtilsConfig(toolbarStyle: NSWindowToolbarStyle.unified); + await config.apply(); + } + + @override + bool isPlatform() { + return Platform.isMacOS && !kIsWeb; + } + + @override + Widget get app => const DefaultPlatformUtility().app; //const _MyApp(); +} + +class _MyApp extends ConsumerWidget { + const _MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return MacosApp.router( + title: 'SSHNP', + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + routerConfig: ref.watch(navigationController), + theme: AppTheme.macosDark(), + darkTheme: AppTheme.macosDark(), + themeMode: ThemeMode.dark, + // * The onboarding screen (first screen)p[] + ); + } +} \ No newline at end of file diff --git a/packages/sshnp_gui/lib/src/utility/platform_utility/platform_utililty.dart b/packages/sshnp_gui/lib/src/utility/platform_utility/platform_utililty.dart new file mode 100644 index 000000000..55ae7093f --- /dev/null +++ b/packages/sshnp_gui/lib/src/utility/platform_utility/platform_utililty.dart @@ -0,0 +1,29 @@ +import 'dart:io'; +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:macos_ui/macos_ui.dart'; + +import 'package:flutter/material.dart'; +import 'package:sshnp_gui/src/utility/platform_utility/default_platform_utility.dart'; + +import 'package:sshnp_gui/src/utility/platform_utility/macos_utility.dart'; + +abstract class PlatformUtility { + bool isPlatform(); + FutureOr configurePlatform(); + Widget get app; + + static const _platforms = [ + MacosUtility(), + ]; + + factory PlatformUtility.current() { + for (var platform in _platforms) { + if (platform.isPlatform()) { + return platform; + } + } + return const DefaultPlatformUtility(); + } +} diff --git a/packages/sshnp_gui/lib/src/utils/sizes.dart b/packages/sshnp_gui/lib/src/utility/sizes.dart similarity index 100% rename from packages/sshnp_gui/lib/src/utils/sizes.dart rename to packages/sshnp_gui/lib/src/utility/sizes.dart diff --git a/packages/sshnp_gui/lib/src/utils/enum.dart b/packages/sshnp_gui/lib/src/utils/enum.dart deleted file mode 100644 index 1a0598820..000000000 --- a/packages/sshnp_gui/lib/src/utils/enum.dart +++ /dev/null @@ -1 +0,0 @@ -enum ConfigFileWriteState { create, update } diff --git a/packages/sshnp_gui/lib/src/utils/util.dart b/packages/sshnp_gui/lib/src/utils/util.dart deleted file mode 100644 index 45a25c4a4..000000000 --- a/packages/sshnp_gui/lib/src/utils/util.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; - -class Util { - static bool isMacos() { - if (Platform.isMacOS && !kIsWeb) { - return true; - } else { - return false; - } - } -} From 1de21a22d75880aafb7fce412ef275b4569669af Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 14:02:16 +0800 Subject: [PATCH 39/95] chore: cleanup imports --- .../lib/src/presentation/screens/terminal_screen.dart | 2 -- .../lib/src/utility/platform_utility/platform_utililty.dart | 5 ----- 2 files changed, 7 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index 6245d98ed..3875b7f1b 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/packages/sshnp_gui/lib/src/utility/platform_utility/platform_utililty.dart b/packages/sshnp_gui/lib/src/utility/platform_utility/platform_utililty.dart index 55ae7093f..bb8d2462a 100644 --- a/packages/sshnp_gui/lib/src/utility/platform_utility/platform_utililty.dart +++ b/packages/sshnp_gui/lib/src/utility/platform_utility/platform_utililty.dart @@ -1,12 +1,7 @@ -import 'dart:io'; import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:macos_ui/macos_ui.dart'; - import 'package:flutter/material.dart'; import 'package:sshnp_gui/src/utility/platform_utility/default_platform_utility.dart'; - import 'package:sshnp_gui/src/utility/platform_utility/macos_utility.dart'; abstract class PlatformUtility { From 71c0b81bf009c618516ee18d713aec9747ad4efc Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 14:17:57 +0800 Subject: [PATCH 40/95] fix: test widget pumper --- packages/sshnp_gui/test/widget_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sshnp_gui/test/widget_test.dart b/packages/sshnp_gui/test/widget_test.dart index 6a216681b..c64221d56 100644 --- a/packages/sshnp_gui/test/widget_test.dart +++ b/packages/sshnp_gui/test/widget_test.dart @@ -8,12 +8,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:sshnp_gui/main.dart'; +import 'package:sshnp_gui/src/utility/platform_utility/default_platform_utility.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const DefaultPlatformUtility().app); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); From 3ed6cccadb602e04183c6e7a2850a6d3a3534377 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 15:25:12 +0800 Subject: [PATCH 41/95] feat: add background session controller --- .../background_session_controller.dart | 30 ++++++++++++ .../profile_actions/profile_run_action.dart | 46 +++++++++++++++---- 2 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 packages/sshnp_gui/lib/src/controllers/background_session_controller.dart diff --git a/packages/sshnp_gui/lib/src/controllers/background_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/background_session_controller.dart new file mode 100644 index 000000000..7b09aed9a --- /dev/null +++ b/packages/sshnp_gui/lib/src/controllers/background_session_controller.dart @@ -0,0 +1,30 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +enum BackgroundSessionStatus { stopped, loading, running } + +final backgroundSessionFamilyController = + NotifierProviderFamily( + BackgroundSessionFamilyController.new, +); + +class BackgroundSession { + final String profileName; + BackgroundSessionStatus status = BackgroundSessionStatus.stopped; + + BackgroundSession(this.profileName); +} + +class BackgroundSessionFamilyController extends FamilyNotifier { + @override + BackgroundSession build(String arg) { + return BackgroundSession(arg); + } + + BackgroundSessionStatus get status => state.status; + + void setStatus(BackgroundSessionStatus status) => state.status = status; + + void start() => setStatus(BackgroundSessionStatus.loading); + void endStartUp() => setStatus(BackgroundSessionStatus.running); + void stop() => setStatus(BackgroundSessionStatus.stopped); +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index 571e7e28f..8ec9aab6b 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -1,21 +1,30 @@ import 'package:at_client_mobile/at_client_mobile.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; +import 'package:sshnp_gui/src/controllers/background_session_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; -class ProfileRunAction extends StatefulWidget { +class ProfileRunAction extends ConsumerStatefulWidget { final SSHNPParams params; const ProfileRunAction(this.params, {Key? key}) : super(key: key); @override - State createState() => _ProfileRunActionState(); + ConsumerState createState() => _ProfileRunActionState(); } -class _ProfileRunActionState extends State { - Future onPressed() async { +class _ProfileRunActionState extends ConsumerState { + SSHNP? sshnp; + + @override + void initState() async { + super.initState(); + } + + Future onStart() async { if (mounted) { showDialog( context: context, @@ -25,14 +34,14 @@ class _ProfileRunActionState extends State { } try { - final sshnp = await SSHNP.fromParams( + sshnp = await SSHNP.fromParams( widget.params, atClient: AtClientManager.getInstance().atClient, sshrvGenerator: SSHRV.pureDart, ); - await sshnp.init(); - final sshnpResult = await sshnp.run(); + await sshnp!.init(); + final sshnpResult = await sshnp!.run(); // TODO } catch (e) { if (mounted) { @@ -45,13 +54,32 @@ class _ProfileRunActionState extends State { } } + Future onStop() async { + } + + static const Map _iconMap = { + BackgroundSessionStatus.stopped: Icon(Icons.play_arrow), + BackgroundSessionStatus.loading: CircularProgressIndicator(), + BackgroundSessionStatus.running: Icon(Icons.stop), + }; + @override Widget build(BuildContext context) { + final status = ref.watch(backgroundSessionFamilyController(widget.params.profileName!)).status; return ProfileActionButton( onPressed: () async { - await onPressed(); + switch (status) { + case BackgroundSessionStatus.stopped: + await onStart(); + break; + case BackgroundSessionStatus.loading: + break; + case BackgroundSessionStatus.running: + await onStop(); + break; + } }, - icon: const Icon(Icons.play_arrow), + icon: _iconMap[status]!, ); } } From ab08f4f30f2021bafe392f588ecbc02f8e5f3f19 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 17:15:06 +0800 Subject: [PATCH 42/95] chore: remove async from initState --- .../widgets/profile_actions/profile_run_action.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index 8ec9aab6b..45cdab4e4 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -20,7 +20,7 @@ class _ProfileRunActionState extends ConsumerState { SSHNP? sshnp; @override - void initState() async { + void initState() { super.initState(); } From 2814ab40207c28f414cf34c2045ac12dbb304f2c Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 17:52:54 +0800 Subject: [PATCH 43/95] docs: note the changes required for run_action --- .../widgets/profile_actions/profile_run_action.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index 45cdab4e4..62b72bf52 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -40,9 +40,11 @@ class _ProfileRunActionState extends ConsumerState { sshrvGenerator: SSHRV.pureDart, ); + // TODO set --single-session, --timeout + await sshnp!.init(); final sshnpResult = await sshnp!.run(); - // TODO + // TODO throw away bad results } catch (e) { if (mounted) { CustomSnackBar.error(content: e.toString()); @@ -55,6 +57,7 @@ class _ProfileRunActionState extends ConsumerState { } Future onStop() async { + // need to implement SSHNP.stop } static const Map _iconMap = { From 826c9a7a4e5fb8ec2d81cf428072ed5fdd794888 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 17:59:32 +0800 Subject: [PATCH 44/95] chore: disable run action for now --- .../presentation/widgets/profile_bar/profile_bar_actions.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart index 491a7192a..556165fbc 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart @@ -10,7 +10,7 @@ class ProfileBarActions extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - ProfileRunAction(params), + // ProfileRunAction(params), ProfileTerminalAction(params), ProfileEditAction(params), ProfileDeleteAction(params), From 416868c9cc38d979919002587a8603aac749ea3b Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 18:07:34 +0800 Subject: [PATCH 45/95] chore: add no-sessions prompt for terminal screen --- packages/sshnp_gui/lib/l10n/app_en.arb | 4 +- .../presentation/screens/terminal_screen.dart | 68 ++++++++++--------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/packages/sshnp_gui/lib/l10n/app_en.arb b/packages/sshnp_gui/lib/l10n/app_en.arb index d74f94799..2ee95ca18 100644 --- a/packages/sshnp_gui/lib/l10n/app_en.arb +++ b/packages/sshnp_gui/lib/l10n/app_en.arb @@ -68,5 +68,7 @@ "resetErrorText":"Please select atleast one atSign to reset", "resetWarningText":"Warning: This action cannot be undone", "removeButton":"Remove", - "error" : "Error" + "error" : "Error", + "noTerminalSessions" : "No active terminal sessions", + "noTerminalSessionsHelp" : "Create a new session from the home screen" } \ No newline at end of file diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index 3875b7f1b..3aaf10ab5 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -6,6 +6,7 @@ import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/utility/sizes.dart'; import 'package:xterm/xterm.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // * Once the onboarding process is completed you will be taken to this screen class TerminalScreen extends ConsumerStatefulWidget { @@ -63,7 +64,7 @@ class _TerminalScreenState extends ConsumerState with TickerProv @override Widget build(BuildContext context) { - // * Getting the AtClientManager instance to use below + final strings = AppLocalizations.of(context)!; final terminalList = ref.watch(terminalSessionListController); final currentSessionId = ref.watch(terminalSessionController); late final int currentIndex; @@ -90,40 +91,45 @@ class _TerminalScreenState extends ConsumerState with TickerProv 'assets/images/noports_light.svg', ), gapH24, - TabBar( - controller: tabController, - isScrollable: true, - onTap: (index) { - ref.read(terminalSessionController.notifier).setSession(terminalList[index]); - }, - tabs: terminalList.map((String sessionId) { - final displayName = ref.read(terminalSessionFamilyController(sessionId).notifier).displayName; - return Tab( - // text: e, - child: Row( - children: [ - Text(displayName), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => deleteTab(sessionId), - ) - ], - ), - ); - }).toList(), - ), - Expanded( - child: TabBarView( + if (terminalList.isEmpty) Text(strings.noTerminalSessions, textScaleFactor: 2), + if (terminalList.isEmpty) Text(strings.noTerminalSessionsHelp), + if (terminalList.isNotEmpty) + TabBar( controller: tabController, - children: terminalList.map((String sessionId) { - return TerminalView( - ref.watch(terminalSessionFamilyController(sessionId)).terminal, - controller: terminalController, - autofocus: true, + isScrollable: true, + onTap: (index) { + ref.read(terminalSessionController.notifier).setSession(terminalList[index]); + }, + tabs: terminalList.map((String sessionId) { + final displayName = ref.read(terminalSessionFamilyController(sessionId).notifier).displayName; + return Tab( + // text: e, + child: Row( + children: [ + Text(displayName), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => deleteTab(sessionId), + ) + ], + ), ); }).toList(), ), - ), + if (terminalList.isNotEmpty) gapH24, + if (terminalList.isNotEmpty) + Expanded( + child: TabBarView( + controller: tabController, + children: terminalList.map((String sessionId) { + return TerminalView( + ref.watch(terminalSessionFamilyController(sessionId)).terminal, + controller: terminalController, + autofocus: true, + ); + }).toList(), + ), + ), // SizedBox( // height: MediaQuery.of(context).size.height - 200, // child: TerminalView( From 37c46a0cb575fbb96d6650866e2d976d1a368bb2 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 18:35:58 +0800 Subject: [PATCH 46/95] feat: add callback for terminal tab to close on process exit --- .../terminal_session_controller.dart | 25 ++++++++++++++++--- .../presentation/screens/terminal_screen.dart | 6 ++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart index fc0883d9a..966d77bc7 100644 --- a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; @@ -49,7 +50,7 @@ class TerminalSessionListController extends Notifier> { List build() => []; void _add(String sessionId) { - state.add(sessionId); + state = state + [sessionId]; } void _remove(String sessionId) { @@ -94,7 +95,7 @@ class TerminalSessionFamilyController extends FamilyNotifier>().transform(const Utf8Decoder()).listen(state.terminal.write); // Write exit code of the process to the terminal - state.pty.exitCode.then((code) => state.terminal.write('The process exited with code: $code')); + state.pty.exitCode.then((code) async { + state.terminal.write('The process exited with code: $code'); + int delay = 5; + + /// Count down to closing the terminal + for (int i = 0; i < delay; i++) { + state.terminal.write('Closing the terminal in ${delay - i} seconds...'); + await Future.delayed(const Duration(seconds: 1), () { + state.terminal.eraseLine(); + }); + } + + /// Close the terminal after [delay] seconds + state.isRunning = false; + dispose(); + exitCallback?.call(code); + }); // Write the terminal output to the process state.terminal.onOutput = (data) { @@ -135,7 +152,7 @@ class TerminalSessionFamilyController extends FamilyNotifier with TickerProv final sessionController = ref.read(terminalSessionFamilyController(sessionId).notifier); WidgetsBinding.instance.endOfFrame.then((value) { - sessionController.startProcess(); + sessionController.startProcess(exitCallback: (int exitCode) { + setState(() {}); + }); }); } @@ -104,6 +106,7 @@ class _TerminalScreenState extends ConsumerState with TickerProv final displayName = ref.read(terminalSessionFamilyController(sessionId).notifier).displayName; return Tab( // text: e, + key: Key('terminal-tab-$sessionId'), child: Row( children: [ Text(displayName), @@ -123,6 +126,7 @@ class _TerminalScreenState extends ConsumerState with TickerProv controller: tabController, children: terminalList.map((String sessionId) { return TerminalView( + key: Key('terminal-view-$sessionId'), ref.watch(terminalSessionFamilyController(sessionId)).terminal, controller: terminalController, autofocus: true, From ac6c88bbee823e34a36d59799d3df87f60d22bdd Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 18:52:01 +0800 Subject: [PATCH 47/95] chore: cleanup unused imports --- packages/sshnp_gui/lib/main.dart | 4 ---- .../lib/src/controllers/terminal_session_controller.dart | 1 - 2 files changed, 5 deletions(-) diff --git a/packages/sshnp_gui/lib/main.dart b/packages/sshnp_gui/lib/main.dart index 08d7b6796..1a69f505c 100644 --- a/packages/sshnp_gui/lib/main.dart +++ b/packages/sshnp_gui/lib/main.dart @@ -3,11 +3,7 @@ import 'dart:async'; import 'package:at_app_flutter/at_app_flutter.dart' show AtEnv; import 'package:at_utils/at_logger.dart' show AtSignLogger; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:macos_ui/macos_ui.dart'; -import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; -import 'package:sshnp_gui/src/utility/app_theme.dart'; import 'package:sshnp_gui/src/utility/platform_utility/platform_utililty.dart'; final AtSignLogger _logger = AtSignLogger(AtEnv.appNamespace); diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart index 966d77bc7..5e6a1d0d5 100644 --- a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:io'; -import 'package:flutter/material.dart'; import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; From a318e1ff3e9d8715a98576c66ae2a1f79c7ca071 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 18:52:14 +0800 Subject: [PATCH 48/95] style: make home title larger --- .../sshnp_gui/lib/src/presentation/screens/home_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index 04ae88541..fad629759 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -44,7 +44,7 @@ class _HomeScreenState extends ConsumerState { ], ), gapH24, - Text(strings.availableConnections), + Text(strings.availableConnections, textScaleFactor: 2), gapH8, profileNames.when( loading: () => const Center( From b464dbc1a52a03d1524230a7ce531cf6caf7960e Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 18:54:48 +0800 Subject: [PATCH 49/95] chore: sort localizations --- packages/sshnp_gui/lib/l10n/app_en.arb | 104 ++++++++++++------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/packages/sshnp_gui/lib/l10n/app_en.arb b/packages/sshnp_gui/lib/l10n/app_en.arb index 2ee95ca18..99dafa1fb 100644 --- a/packages/sshnp_gui/lib/l10n/app_en.arb +++ b/packages/sshnp_gui/lib/l10n/app_en.arb @@ -1,74 +1,74 @@ { - "currentConnections" : "Current Connections", - "addNewConnection" : "Add New Connection", + "actions" : "Actions", "add" : "Add", - "submit" : "Submit", - "hostSelection" : "Host Selection", - "host" : "Host", - "sourcePort" : "Source Port", - "status" : "Status", - "destination" : "Destination", - "dest" : "Dest.", - "options" : "Options", - "connect" : "Connect", + "addNewConnection" : "Add New Connection", + "atKeysFilePath" : "atKeys File", + "availableConnections" : "Available Connections", + "backupYourKeys" : "BackupYourKeys", "cancel" : "Cancel", - "newText" : "New", - "profileName" : "Profile Name", + "cancelButton":"Cancel", "clientAtsign" : "Client atsign", - "sshnpdAtSign" : "Device Address", - "sshnpdAtSignHint" : "The atSign of the sshnpd we wish to communicate with", + "closeButton" : "Close", + "connect" : "Connect", + "contactUs" : "Contact Us", + "copiedToClipboard": "Copied to Clipboard", + "currentConnections" : "Current Connections", + "deleteButton" : "Delete", + "dest" : "Dest.", + "destination" : "Destination", "device" : "Device Name", "deviceHint" : "The device name of the sshnpd we wish to communicate with", - "username" : "Username", - "usernameHint": "The user name on this host", + "error" : "Error", + "failed": "Failed", + "faq" : "FAQ", + "from" : "From", "homeDirectory" : "Home Directory", "homeDirectoryHint" : "The home directory on this host", - "sessionId" : "Session ID", - "sendSshPublicKey" : "SSH Public Key", - "rsa" : "Legacy RSA Key", - "keyFile" : "Key File", - "from" : "From", - "to" : "To", "host" : "Host", - "port" : "Remote Port", + "host" : "Host", + "hostSelection" : "Host Selection", + "keyFile" : "Key File", + "listDevices" : "List Devices", "localPort": "Local Port", "localSshdPort": "Local SSHD Port", - "sshPublicKey" : "SSH Public Key", "localSshOptions" : "Local SSH Options", "localSshOptionsHint" : "Use \",\" to separate options", - "verbose" : "Verbose Logging", - "remoteUserName" : "Remote Username", - "atKeysFilePath" : "atKeys File", - "rootDomain" : "Root Domain", - "listDevices" : "List Devices", - "availableConnections" : "Available Connections", - "actions" : "Actions", - "warningMessage" : " Are you sure you want to delete this configuration", - "warning": "Warning", + "newText" : "New", + "noAtsignToReset": "No atSigns are paired to reset.", "note" : "Note", "noteMessage" : ": You cannot undo this action.", - "cancelButton":"Cancel", - "deleteButton" : "Delete", + "noTerminalSessions" : "No active terminal sessions", + "noTerminalSessionsHelp" : "Create a new session from the home screen", "okButton" : "Ok", - "sshButton" : "ssh", - "closeButton" : "Close", - "success": "Success", - "failed": "Failed", - "result" : "Result", - "copiedToClipboard": "Copied to Clipboard", - "settings" : "Settings", - "backupYourKeys" : "BackupYourKeys", - "switchAtsign" : "Switch atSign", - "faq" : "FAQ", - "contactUs" : "Contact Us", + "options" : "Options", + "port" : "Remote Port", "privacyPolicy" : "Privacy Policy", + "profileName" : "Profile Name", + "remoteUserName" : "Remote Username", + "removeButton":"Remove", "reset" : "Reset", "resetDescription":"This will remove the selected atSign and its details from this app only.", - "noAtsignToReset": "No atSigns are paired to reset.", "resetErrorText":"Please select atleast one atSign to reset", "resetWarningText":"Warning: This action cannot be undone", - "removeButton":"Remove", - "error" : "Error", - "noTerminalSessions" : "No active terminal sessions", - "noTerminalSessionsHelp" : "Create a new session from the home screen" + "result" : "Result", + "rootDomain" : "Root Domain", + "rsa" : "Legacy RSA Key", + "sendSshPublicKey" : "SSH Public Key", + "sessionId" : "Session ID", + "settings" : "Settings", + "sourcePort" : "Source Port", + "sshButton" : "ssh", + "sshnpdAtSign" : "Device Address", + "sshnpdAtSignHint" : "The atSign of the sshnpd we wish to communicate with", + "sshPublicKey" : "SSH Public Key", + "status" : "Status", + "submit" : "Submit", + "success": "Success", + "switchAtsign" : "Switch atSign", + "to" : "To", + "username" : "Username", + "usernameHint": "The user name on this host", + "verbose" : "Verbose Logging", + "warning": "Warning", + "warningMessage" : " Are you sure you want to delete this configuration" } \ No newline at end of file From 854ce830953eefffe1da76e6137906110aa339a0 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 18:56:02 +0800 Subject: [PATCH 50/95] fix: backup keys string --- packages/sshnp_gui/lib/l10n/app_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sshnp_gui/lib/l10n/app_en.arb b/packages/sshnp_gui/lib/l10n/app_en.arb index 99dafa1fb..96496efd8 100644 --- a/packages/sshnp_gui/lib/l10n/app_en.arb +++ b/packages/sshnp_gui/lib/l10n/app_en.arb @@ -4,7 +4,7 @@ "addNewConnection" : "Add New Connection", "atKeysFilePath" : "atKeys File", "availableConnections" : "Available Connections", - "backupYourKeys" : "BackupYourKeys", + "backupYourKeys" : "Backup Your Keys", "cancel" : "Cancel", "cancelButton":"Cancel", "clientAtsign" : "Client atsign", From a2e3b942f4e4f39cc6421d55740385133ac72b3e Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Fri, 8 Sep 2023 21:27:54 +0800 Subject: [PATCH 51/95] fix: use SSHNPD.namespace as the default namespace for sshnp_gui --- .../lib/src/repository/authentication_repository.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/sshnp_gui/lib/src/repository/authentication_repository.dart b/packages/sshnp_gui/lib/src/repository/authentication_repository.dart index 58e6552e7..2d9f55e40 100644 --- a/packages/sshnp_gui/lib/src/repository/authentication_repository.dart +++ b/packages/sshnp_gui/lib/src/repository/authentication_repository.dart @@ -11,6 +11,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:sshnoports/sshnpd/sshnpd.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; import 'package:sshnp_gui/src/repository/navigation_repository.dart'; @@ -53,14 +54,10 @@ class AuthenticationRepository { return AtClientPreference() ..rootDomain = AtEnv.rootDomain - ..namespace = AtEnv.appNamespace + ..namespace = SSHNPD.namespace ..hiveStoragePath = dir.path ..commitLogPath = dir.path ..isLocalStoreRequired = true; - // TODO - // * By default, this configuration is suitable for most applications - // * In advanced cases you may need to modify [AtClientPreference] - // * Read more here: https://pub.dev/documentation/at_client/latest/at_client/AtClientPreference-class.html } /// Signs user into the @platform. From 3a8fed678c76d1318c981e28f4283cb282e3bda2 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Sat, 9 Sep 2023 13:28:22 +0800 Subject: [PATCH 52/95] feat: cleanup terminal behaviors --- .../terminal_session_controller.dart | 45 ++++++++++++++---- .../presentation/screens/terminal_screen.dart | 46 +++---------------- 2 files changed, 42 insertions(+), 49 deletions(-) diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart index 5e6a1d0d5..6c5098d3a 100644 --- a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -66,6 +66,7 @@ class TerminalSession { late Pty pty; bool isRunning = false; + bool isDisposed = true; String? command; List args = const []; @@ -94,9 +95,10 @@ class TerminalSessionFamilyController extends FamilyNotifier>().transform(const Utf8Decoder()).listen(state.terminal.write); // Write exit code of the process to the terminal state.pty.exitCode.then((code) async { - state.terminal.write('The process exited with code: $code'); + state.terminal.write('\n[The process exited with code: $code]\r\n\n'); + int delay = 5; /// Count down to closing the terminal for (int i = 0; i < delay; i++) { - state.terminal.write('Closing the terminal in ${delay - i} seconds...'); - await Future.delayed(const Duration(seconds: 1), () { - state.terminal.eraseLine(); - }); + String message = 'Closing terminal session in ${delay - i} seconds...\r'; + state.terminal.write(message); + await Future.delayed(const Duration(seconds: 1)); } /// Close the terminal after [delay] seconds state.isRunning = false; dispose(); - exitCallback?.call(code); }); // Write the terminal output to the process @@ -141,7 +142,7 @@ class TerminalSessionFamilyController extends FamilyNotifier 0) { + // set active terminal to the one immediately to the left + ref.read(terminalSessionController.notifier).setSession(terminalList[currentIndex - 1]); + } else if (terminalList.length > 1) { + // set active terminal to the one immediately to the right + ref.read(terminalSessionController.notifier).setSession(terminalList[currentIndex + 1]); + } else { + // no other sessions available, set active terminal to empty string + ref.read(terminalSessionController.notifier).setSession(''); + } + } + + /// 3. Remove the session from the list of sessions ref.read(terminalSessionListController.notifier)._remove(state.sessionId); + + /// 4. Remove the session from the profile name counter if (state._profileName != null) { ref.read(terminalSessionProfileNameFamilyCounter(state._profileName!).notifier)._removeSession(state.sessionId); } diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index 54f49b589..5a07ea611 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -19,7 +19,6 @@ class TerminalScreen extends ConsumerStatefulWidget { class _TerminalScreenState extends ConsumerState with TickerProviderStateMixin { final terminalController = TerminalController(); late final Pty pty; - @override void initState() { super.initState(); @@ -27,9 +26,7 @@ class _TerminalScreenState extends ConsumerState with TickerProv final sessionController = ref.read(terminalSessionFamilyController(sessionId).notifier); WidgetsBinding.instance.endOfFrame.then((value) { - sessionController.startProcess(exitCallback: (int exitCode) { - setState(() {}); - }); + sessionController.startProcess(); }); } @@ -39,29 +36,10 @@ class _TerminalScreenState extends ConsumerState with TickerProv super.dispose(); } - void deleteTab(String sessionId) { + void closeSession(String sessionId) { + // Remove the session from the list of sessions final controller = ref.read(terminalSessionFamilyController(sessionId).notifier); - final terminalList = ref.watch(terminalSessionListController); - final currentSessionId = ref.read(terminalSessionController); - final currentIndex = terminalList.indexOf(currentSessionId); - - // If the session we are deleting is the active session - // we need to set a new active session - if (currentSessionId == sessionId) { - if (currentIndex > 0) { - // set active terminal to the one immediately to the left - ref.read(terminalSessionController.notifier).setSession(terminalList[currentIndex - 1]); - } else if (terminalList.length > 1) { - // set active terminal to the one immediately to the right - ref.read(terminalSessionController.notifier).setSession(terminalList[currentIndex + 1]); - } else { - // no other sessions available, set active terminal to empty string - ref.read(terminalSessionController.notifier).setSession(''); - } - } - controller.dispose(); - setState(() {}); } @override @@ -69,12 +47,7 @@ class _TerminalScreenState extends ConsumerState with TickerProv final strings = AppLocalizations.of(context)!; final terminalList = ref.watch(terminalSessionListController); final currentSessionId = ref.watch(terminalSessionController); - late final int currentIndex; - if (terminalList.isEmpty) { - currentIndex = 0; - } else { - currentIndex = terminalList.indexOf(currentSessionId); - } + final int currentIndex = (terminalList.isEmpty) ? 0 : terminalList.indexOf(currentSessionId); final tabController = TabController(initialIndex: currentIndex, length: terminalList.length, vsync: this); return Scaffold( @@ -112,7 +85,7 @@ class _TerminalScreenState extends ConsumerState with TickerProv Text(displayName), IconButton( icon: const Icon(Icons.close), - onPressed: () => deleteTab(sessionId), + onPressed: () => closeSession(sessionId), ) ], ), @@ -130,18 +103,11 @@ class _TerminalScreenState extends ConsumerState with TickerProv ref.watch(terminalSessionFamilyController(sessionId)).terminal, controller: terminalController, autofocus: true, + autoResize: true, ); }).toList(), ), ), - // SizedBox( - // height: MediaQuery.of(context).size.height - 200, - // child: TerminalView( - // terminalSession.terminal, - // controller: terminalController, - // autofocus: true, - // ), - // ), ]), ), ), From e6888d9e70a50d5bef3b41efc21cefd11f0a2203 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Sat, 9 Sep 2023 13:39:38 +0800 Subject: [PATCH 53/95] chore: make cursorInvisible when doing exit countdown --- .../lib/src/controllers/terminal_session_controller.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart index 6c5098d3a..e9ab17296 100644 --- a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -120,6 +120,7 @@ class TerminalSessionFamilyController extends FamilyNotifier Date: Mon, 11 Sep 2023 08:48:40 -0400 Subject: [PATCH 54/95] fix: minimum desktop size set other minor changes to all correct sizing on macos. --- .../presentation/screens/settings_screen.dart | 16 +++---- .../presentation/screens/terminal_screen.dart | 2 +- .../navigation/app_navigation_rail.dart | 43 +++++++++++-------- packages/sshnp_gui/macos/Podfile.lock | 2 +- .../macos/Runner/Base.lproj/MainMenu.xib | 12 +++--- 5 files changed, 42 insertions(+), 33 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart index 0446fa381..de1ee0b2a 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/settings_screen.dart @@ -22,9 +22,7 @@ class SettingsScreen extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.only(left: Sizes.p36, top: Sizes.p21), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, + child: ListView( children: [ Padding( padding: const EdgeInsets.symmetric(vertical: Sizes.p20), @@ -36,17 +34,17 @@ class SettingsScreen extends StatelessWidget { const SizedBox( height: 59, ), - const SettingsBackupKeyAction(), + const Center(child: SettingsBackupKeyAction()), gapH16, - const SettingsSwitchAtsignAction(), + const Center(child: SettingsSwitchAtsignAction()), gapH16, - const SettingsResetAppAction(), + const Center(child: SettingsResetAppAction()), gapH36, - const SettingsFaqAction(), + const Center(child: SettingsFaqAction()), gapH16, - const SettingsContactAction(), + const Center(child: SettingsContactAction()), gapH16, - const SettingsPrivacyPolicyAction(), + const Center(child: SettingsPrivacyPolicyAction()), ], ), ), diff --git a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart index 54f49b589..eea106bde 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/terminal_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_pty/flutter_pty.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -6,7 +7,6 @@ import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/utility/sizes.dart'; import 'package:xterm/xterm.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // * Once the onboarding process is completed you will be taken to this screen class TerminalScreen extends ConsumerStatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart index 896865e52..275bba6d3 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/navigation/app_navigation_rail.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/navigation_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; +import 'package:sshnp_gui/src/controllers/navigation_rail_controller.dart'; class AppNavigationRail extends ConsumerWidget { const AppNavigationRail({super.key}); @@ -25,22 +25,31 @@ class AppNavigationRail extends ConsumerWidget { final controller = ref.watch(navigationRailController.notifier); final currentIndex = controller.getCurrentIndex(); - return NavigationRail( - destinations: controller.routes - .map( - (AppRoute route) => NavigationRailDestination( - icon: (controller.isCurrentIndex(route)) - ? activatedIcons[controller.indexOf(route)] - : deactivatedIcons[controller.indexOf(route)], - label: const Text(''), - ), - ) - .toList(), - selectedIndex: currentIndex, - onDestinationSelected: (int selectedIndex) { - controller.setIndex(selectedIndex); - context.goNamed(controller.getCurrentRoute().name); - }, + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height, + ), + child: IntrinsicHeight( + child: NavigationRail( + destinations: controller.routes + .map( + (AppRoute route) => NavigationRailDestination( + icon: (controller.isCurrentIndex(route)) + ? activatedIcons[controller.indexOf(route)] + : deactivatedIcons[controller.indexOf(route)], + label: const Text(''), + ), + ) + .toList(), + selectedIndex: currentIndex, + onDestinationSelected: (int selectedIndex) { + controller.setIndex(selectedIndex); + context.goNamed(controller.getCurrentRoute().name); + }, + ), + ), + ), ); } } diff --git a/packages/sshnp_gui/macos/Podfile.lock b/packages/sshnp_gui/macos/Podfile.lock index 2d6c4cf07..b1eda3d11 100644 --- a/packages/sshnp_gui/macos/Podfile.lock +++ b/packages/sshnp_gui/macos/Podfile.lock @@ -87,4 +87,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 diff --git a/packages/sshnp_gui/macos/Runner/Base.lproj/MainMenu.xib b/packages/sshnp_gui/macos/Runner/Base.lproj/MainMenu.xib index 80e867a4e..a8292c3e7 100644 --- a/packages/sshnp_gui/macos/Runner/Base.lproj/MainMenu.xib +++ b/packages/sshnp_gui/macos/Runner/Base.lproj/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -13,7 +13,7 @@ - + @@ -330,14 +330,16 @@ - + - + + + From 0780a45825e67b7067e42904bb5a4835345c35f2 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 12 Sep 2023 12:32:05 +0800 Subject: [PATCH 55/95] chore: rename sshnpparams to config in controller --- .../controllers/sshnp_params_controller.dart | 40 +++++++++---------- .../src/presentation/screens/home_screen.dart | 2 +- .../new_profile_action.dart | 4 +- .../profile_delete_action.dart | 2 +- .../profile_actions/profile_edit_action.dart | 4 +- .../widgets/profile_bar/profile_bar.dart | 2 +- .../widgets/profile_form/profile_form.dart | 11 +++-- 7 files changed, 31 insertions(+), 34 deletions(-) diff --git a/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart b/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart index c23eb5545..9902afd71 100644 --- a/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart @@ -6,48 +6,46 @@ import 'package:sshnoports/sshnp/sshnp.dart'; enum ConfigFileWriteState { create, update } - -/// A provider that exposes the [SSHNPParamsController] to the app. -final sshnpParamsController = AutoDisposeNotifierProvider( - SSHNPParamsController.new, +/// A provider that exposes the [CurrentConfigController] to the app. +final currentConfigController = AutoDisposeNotifierProvider( + CurrentConfigController.new, ); -/// A provider that exposes the [SSHNPParamsListController] to the app. -final sshnpParamsListController = AutoDisposeAsyncNotifierProvider>( - SSHNPParamsListController.new, +/// A provider that exposes the [ConfigListController] to the app. +final configListController = AutoDisposeAsyncNotifierProvider>( + ConfigListController.new, ); -/// A provider that exposes the [SSHNPParamsFamilyController] to the app. -final sshnpParamsFamilyController = - AutoDisposeAsyncNotifierProviderFamily( - SSHNPParamsFamilyController.new, +/// A provider that exposes the [ConfigFamilyController] to the app. +final configFamilyController = AutoDisposeAsyncNotifierProviderFamily( + ConfigFamilyController.new, ); /// Holder model for the current [SSHNPParams] being edited -class CurrentSSHNPParamsModel { +class CurrentConfigState { final String profileName; final ConfigFileWriteState configFileWriteState; - CurrentSSHNPParamsModel({required this.profileName, required this.configFileWriteState}); + CurrentConfigState({required this.profileName, required this.configFileWriteState}); } /// Controller for the current [SSHNPParams] being edited -class SSHNPParamsController extends AutoDisposeNotifier { +class CurrentConfigController extends AutoDisposeNotifier { @override - CurrentSSHNPParamsModel build() { - return CurrentSSHNPParamsModel( + CurrentConfigState build() { + return CurrentConfigState( profileName: '', configFileWriteState: ConfigFileWriteState.create, ); } - void setState(CurrentSSHNPParamsModel model) { + void setState(CurrentConfigState model) { state = model; } } /// Controller for the list of all profileNames for each config file -class SSHNPParamsListController extends AutoDisposeAsyncNotifier> { +class ConfigListController extends AutoDisposeAsyncNotifier> { @override Future> build() async { return (await SSHNPParams.listFiles()).toSet(); @@ -68,7 +66,7 @@ class SSHNPParamsListController extends AutoDisposeAsyncNotifier> { } /// Controller for the family of [SSHNPParams] controllers -class SSHNPParamsFamilyController extends AutoDisposeFamilyAsyncNotifier { +class ConfigFamilyController extends AutoDisposeFamilyAsyncNotifier { @override Future build(String arg) async { return (await SSHNPParams.fileExists(arg)) @@ -87,7 +85,7 @@ class SSHNPParamsFamilyController extends AutoDisposeFamilyAsyncNotifier create(SSHNPParams params) async { await params.toFile(); state = AsyncValue.data(params); - ref.read(sshnpParamsListController.notifier).add(params.profileName!); + ref.read(configListController.notifier).add(params.profileName!); } Future edit(SSHNPParams params) async { @@ -98,6 +96,6 @@ class SSHNPParamsFamilyController extends AutoDisposeFamilyAsyncNotifier delete() async { await state.value?.deleteFile(); state = const AsyncError('File deleted', StackTrace.empty); - ref.read(sshnpParamsListController.notifier).remove(state.value!.profileName!); + ref.read(configListController.notifier).remove(state.value!.profileName!); } } diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index fad629759..d8c61c686 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -22,7 +22,7 @@ class _HomeScreenState extends ConsumerState { // * Getting the AtClientManager instance to use below final strings = AppLocalizations.of(context)!; - final profileNames = ref.watch(sshnpParamsListController); + final profileNames = ref.watch(configListController); return Scaffold( body: SafeArea( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart index 3e25b261a..9cf8605a6 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart @@ -14,8 +14,8 @@ class NewProfileAction extends ConsumerStatefulWidget { class _NewProfileActionState extends ConsumerState { void onPressed() { // Change value to update to trigger the update functionality on the new connection form. - ref.watch(sshnpParamsController.notifier).setState( - CurrentSSHNPParamsModel( + ref.watch(currentConfigController.notifier).setState( + CurrentConfigState( profileName: '', configFileWriteState: ConfigFileWriteState.create, ), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart index 3e2f47b56..40a545551 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart @@ -69,7 +69,7 @@ class DeleteAlertDialog extends ConsumerWidget { ), ElevatedButton( onPressed: () async { - await ref.read(sshnpParamsFamilyController(sshnpParams.profileName!).notifier).delete(); + await ref.read(configFamilyController(sshnpParams.profileName!).notifier).delete(); if (context.mounted) Navigator.of(context).pop(); }, style: Theme.of(context).elevatedButtonTheme.style!.copyWith( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart index 133ae156c..a31bc64d0 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart @@ -18,8 +18,8 @@ class ProfileEditAction extends ConsumerStatefulWidget { class _ProfileEditActionState extends ConsumerState { void onPressed() { // Change value to update to trigger the update functionality on the new connection form. - ref.watch(sshnpParamsController.notifier).setState( - CurrentSSHNPParamsModel( + ref.watch(currentConfigController.notifier).setState( + CurrentConfigState( profileName: widget.params.profileName!, configFileWriteState: ConfigFileWriteState.update, ), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart index 3e3895c7d..5effa4914 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart @@ -15,7 +15,7 @@ class ProfileBar extends ConsumerStatefulWidget { class _ProfileBarState extends ConsumerState { @override Widget build(BuildContext context) { - final controller = ref.watch(sshnpParamsFamilyController(widget.profileName)); + final controller = ref.watch(configFamilyController(widget.profileName)); return controller.when( error: (error, stackTrace) => Container(), loading: () => const LinearProgressIndicator(), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index 9044a85d9..96e0ec049 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -20,7 +20,7 @@ class ProfileForm extends ConsumerStatefulWidget { class _ProfileFormState extends ConsumerState { final GlobalKey _formkey = GlobalKey(); - late CurrentSSHNPParamsModel currentProfile; + late CurrentConfigState currentProfile; SSHNPPartialParams newConfig = SSHNPPartialParams.empty(); @override void initState() { @@ -30,8 +30,7 @@ class _ProfileFormState extends ConsumerState { void onSubmit(SSHNPParams oldConfig, SSHNPPartialParams newConfig) async { if (_formkey.currentState!.validate()) { _formkey.currentState!.save(); - final controller = - ref.read(sshnpParamsFamilyController(newConfig.profileName ?? oldConfig.profileName!).notifier); + final controller = ref.read(configFamilyController(newConfig.profileName ?? oldConfig.profileName!).notifier); bool overwrite = currentProfile.configFileWriteState == ConfigFileWriteState.update; bool rename = newConfig.profileName.isNotNull && newConfig.profileName!.isNotEmpty && @@ -41,7 +40,7 @@ class _ProfileFormState extends ConsumerState { SSHNPParams config = SSHNPParams.merge(oldConfig, newConfig); if (rename) { // delete old config file and write the new one - await ref.read(sshnpParamsFamilyController(oldConfig.profileName!).notifier).delete(); + await ref.read(configFamilyController(oldConfig.profileName!).notifier).delete(); await controller.create(config); } else if (overwrite) { // overwrite the existing file @@ -60,9 +59,9 @@ class _ProfileFormState extends ConsumerState { @override Widget build(BuildContext context) { final strings = AppLocalizations.of(context)!; - currentProfile = ref.watch(sshnpParamsController); + currentProfile = ref.watch(currentConfigController); - final asyncOldConfig = ref.watch(sshnpParamsFamilyController(currentProfile.profileName)); + final asyncOldConfig = ref.watch(configFamilyController(currentProfile.profileName)); return asyncOldConfig.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text(error.toString())), From 65a52b13ab03c76e83294b748fc368b2e0b17ab2 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 12 Sep 2023 12:32:34 +0800 Subject: [PATCH 56/95] chore: rename sshnp_params_controller.dart to config_controller.dart --- .../{sshnp_params_controller.dart => config_controller.dart} | 0 .../sshnp_gui/lib/src/presentation/screens/home_screen.dart | 2 +- .../widgets/home_screen_actions/new_profile_action.dart | 2 +- .../widgets/profile_actions/profile_delete_action.dart | 2 +- .../widgets/profile_actions/profile_edit_action.dart | 2 +- .../lib/src/presentation/widgets/profile_bar/profile_bar.dart | 2 +- .../lib/src/presentation/widgets/profile_form/profile_form.dart | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename packages/sshnp_gui/lib/src/controllers/{sshnp_params_controller.dart => config_controller.dart} (100%) diff --git a/packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart b/packages/sshnp_gui/lib/src/controllers/config_controller.dart similarity index 100% rename from packages/sshnp_gui/lib/src/controllers/sshnp_params_controller.dart rename to packages/sshnp_gui/lib/src/controllers/config_controller.dart diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index d8c61c686..3ed1c85c7 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; +import 'package:sshnp_gui/src/controllers/config_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_actions.dart'; import 'package:sshnp_gui/src/presentation/widgets/navigation/app_navigation_rail.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart index 9cf8605a6..1ff376637 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/new_profile_action.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; +import 'package:sshnp_gui/src/controllers/config_controller.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; class NewProfileAction extends ConsumerStatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart index 40a545551..47ef9e7a1 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; +import 'package:sshnp_gui/src/controllers/config_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; import 'package:sshnp_gui/src/utility/sizes.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart index a31bc64d0..b57fb6ed5 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; +import 'package:sshnp_gui/src/controllers/config_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart index 5effa4914..0e4266cc8 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; +import 'package:sshnp_gui/src/controllers/config_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar_actions.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar_stats.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index 96e0ec049..d14867262 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/navigation_rail_controller.dart'; -import 'package:sshnp_gui/src/controllers/sshnp_params_controller.dart'; +import 'package:sshnp_gui/src/controllers/config_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_form/custom_text_form_field.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; import 'package:sshnp_gui/src/utility/sizes.dart'; From 3bc8be7d60f143fb0c80459909788fa4ac67e55b Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 12 Sep 2023 13:41:29 +0800 Subject: [PATCH 57/95] feat: add ConfigSource and covariants --- .../sshnoports/lib/sshnp/sshnp_params.dart | 37 +++++--- .../config_source/config_file_source.dart | 95 +++++++++++++++++++ .../config_source/config_source.dart | 35 +++++++ .../config_source/config_sync_source.dart | 58 +++++++++++ 4 files changed, 211 insertions(+), 14 deletions(-) create mode 100644 packages/sshnp_gui/lib/src/repository/config_source/config_file_source.dart create mode 100644 packages/sshnp_gui/lib/src/repository/config_source/config_source.dart create mode 100644 packages/sshnp_gui/lib/src/repository/config_source/config_sync_source.dart diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index e77ae39e3..88939beb6 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -121,11 +121,13 @@ class SSHNPParams { ); } + factory SSHNPParams.fromJson(String json) => SSHNPParams.fromPartial(SSHNPPartialParams.fromJson(json)); + factory SSHNPParams.fromConfigFile(String fileName) { return SSHNPParams.fromPartial(SSHNPPartialParams.fromConfig(fileName)); } - static Future> listFiles([String? directory]) async { + static Future> listFiles({String? directory}) async { var fileNames = {}; var homeDirectory = getHomeDirectory(throwIfNull: true)!; @@ -148,22 +150,30 @@ class SSHNPParams { return fileNames; } - static Future fromFile(String profileName, [String? directory]) async { - var fileName = _profileToFileName(profileName, directory); + static Future fromFile(String profileName, {String? directory}) async { + var fileName = getFileName(profileName, directory: directory); return SSHNPParams.fromConfigFile(fileName); } - static Future fileExists(String profileName, [String? directory]) { - var fileName = _profileToFileName(profileName, directory); + static Future fileExists(String profileName, {String? directory}) { + var fileName = getFileName(profileName, directory: directory); return File(fileName).exists(); } + static String getFileName(String profileName, {String? directory, bool replaceSpaces = true}) { + var fileName = profileName.replaceAll(' ', '_'); + return path.join( + directory ?? getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), + '$fileName.env', + ); + } + Future toFile({String? directory, bool overwrite = false}) async { if (profileName == null || profileName!.isEmpty) { throw Exception('profileName is null or empty'); } - var fileName = _profileToFileName(profileName!, directory); + var fileName = getFileName(profileName!, directory: directory); var file = File(fileName); var exists = await file.exists(); @@ -182,7 +192,7 @@ class SSHNPParams { throw Exception('profileName is null or empty'); } - var fileName = _profileToFileName(profileName!, directory); + var fileName = getFileName(profileName!, directory: directory); var file = File(fileName); var exists = await file.exists(); @@ -214,6 +224,10 @@ class SSHNPParams { }; } + String toJson() { + return jsonEncode(toArgs()); + } + String toConfig() { var lines = []; for (var entry in toArgs().entries) { @@ -331,6 +345,8 @@ class SSHNPPartialParams { ); } + factory SSHNPPartialParams.fromJson(String json) => SSHNPPartialParams.fromArgMap(jsonDecode(json)); + factory SSHNPPartialParams.fromConfig(String fileName) { var args = _parseConfigFile(fileName); args['profile-name'] = _fileToProfileName(fileName); @@ -474,10 +490,3 @@ class SSHNPPartialParams { } String _fileToProfileName(String fileName) => path.basenameWithoutExtension(fileName).replaceAll('_', ' '); -String _profileToFileName(String profileName, [String? directory]) { - var fileName = profileName.replaceAll(' ', '_'); - return path.join( - directory ?? getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), - '$fileName.env', - ); -} diff --git a/packages/sshnp_gui/lib/src/repository/config_source/config_file_source.dart b/packages/sshnp_gui/lib/src/repository/config_source/config_file_source.dart new file mode 100644 index 000000000..7a0c59b30 --- /dev/null +++ b/packages/sshnp_gui/lib/src/repository/config_source/config_file_source.dart @@ -0,0 +1,95 @@ +part of 'config_source.dart'; + +/// [ConfigSource] covariant from a [File] +abstract class ConfigFileSource implements ConfigSource { + late final String profileName; + late final String? directory; + late final String? fileName; + late final File file; + + SSHNPParams? _params; + + @override + SSHNPParams get params => _params ?? SSHNPParams.empty(); + + ConfigFileSource._(this.profileName, {this.directory, this.fileName}) + : file = File( + SSHNPParams.getFileName( + fileName ?? profileName, + directory: directory, + replaceSpaces: (fileName == null), // only replace spaces for [profileName] not [fileName] + ), + ); + + factory ConfigFileSource.sandboxed(String profileName) => SandboxedConfigFileSource(profileName); + + factory ConfigFileSource.imported(String profileName, String directory, {String? fileName}) => + ImportedConfigFileSource(profileName, directory, fileName: fileName); + + factory ConfigFileSource.exported(String profileName, String directory, {String? fileName}) => + ExportedConfigFileSource(profileName, directory, fileName: fileName); + + @override + DateTime get lastModified => file.lastModifiedSync(); + + @override + Future create(SSHNPParams params) async { + if (params.profileName != profileName) { + throw ArgumentError.value(params.profileName, 'params.profileName', 'must be $profileName'); + } + await params.toFile(directory: directory); + } + + @override + Future read() async { + try { + var params = SSHNPParams.fromConfigFile(file.path); + _params = params; + } catch (e) { + _params = null; + } + return params; + } + + @override + Future update(SSHNPParams params) async { + params.toFile(directory: directory, overwrite: true); + } + + @override + Future delete(SSHNPParams params) async { + await file.delete(); + } +} + +class SandboxedConfigFileSource extends ConfigFileSource { + SandboxedConfigFileSource(String profileName) : super._(profileName); +} + +class ImportedConfigFileSource extends ConfigFileSource { + /// External Config File Source uses an external, user-selected file path + ImportedConfigFileSource(String profileName, String directory, {String? fileName}) + : super._(profileName, directory: directory, fileName: fileName); + + // no-op + @override + Future create(SSHNPParams params) => throw UnsupportedError('Cannot create an imported config file'); + @override + Future update(SSHNPParams params) => throw UnsupportedError('Cannot update an imported config file'); + @override + Future delete(SSHNPParams params) => throw UnsupportedError('Cannot delete an imported config file'); +} + +class ExportedConfigFileSource extends ConfigFileSource { + /// Exported Config File Source uses an external, user-selected file path + ExportedConfigFileSource(String profileName, String directory, {String? fileName}) + : super._(profileName, directory: directory, fileName: fileName); + + // no-op + @override + DateTime get lastModified => throw UnsupportedError('Cannot get lastModified of an exported config file'); + @override + Future read() => throw UnsupportedError('Cannot read an exported config file'); + @override + Future delete(SSHNPParams params) => throw UnsupportedError('Cannot delete an exported config file'); +} diff --git a/packages/sshnp_gui/lib/src/repository/config_source/config_source.dart b/packages/sshnp_gui/lib/src/repository/config_source/config_source.dart new file mode 100644 index 000000000..7bc6c5e5f --- /dev/null +++ b/packages/sshnp_gui/lib/src/repository/config_source/config_source.dart @@ -0,0 +1,35 @@ +import 'dart:io'; + +import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnoports/sshnpd/sshnpd.dart'; + +part 'config_sync_source.dart'; +part 'config_file_source.dart'; + +/// Generic Type +abstract class ConfigSource { + DateTime get lastModified; + SSHNPParams get params; + + Future create(SSHNPParams params); + Future read(); + Future update(SSHNPParams params); + Future delete(SSHNPParams params); + + factory ConfigSource.synced(String profileName, {AtClient? atClient}) { + return ConfigSyncSource.synced(profileName, atClient: atClient); + } + + factory ConfigSource.exported(String profileName, directory) { + return ConfigFileSource.exported(profileName, directory); + } + + factory ConfigSource.imported(String profileName, directory) { + return ConfigFileSource.imported(profileName, directory); + } + + factory ConfigSource.sandboxed(String profileName) { + return ConfigFileSource.sandboxed(profileName); + } +} diff --git a/packages/sshnp_gui/lib/src/repository/config_source/config_sync_source.dart b/packages/sshnp_gui/lib/src/repository/config_source/config_sync_source.dart new file mode 100644 index 000000000..1236e88de --- /dev/null +++ b/packages/sshnp_gui/lib/src/repository/config_source/config_sync_source.dart @@ -0,0 +1,58 @@ +part of 'config_source.dart'; + +/// [ConfigSource] covariant from an [AtKey] +class ConfigSyncSource implements ConfigSource { + late final AtKey atKey; + late final AtClient atClient; + + SSHNPParams? _params; + + @override + SSHNPParams get params => _params ?? SSHNPParams.empty(); + + ConfigSyncSource._(this.atKey, this.atClient); + + factory ConfigSyncSource.synced(String profileName, {AtClient? atClient}) { + AtKey atKey = AtKey.self( + 'profile_$profileName', + namespace: SSHNPD.namespace, + ).build(); + + atClient ??= AtClientManager.getInstance().atClient; + return ConfigSyncSource._(atKey, atClient); + } + + void _updateTimestamp() { + atKey.metadata = Metadata()..updatedAt = DateTime.now(); + } + + @override + DateTime get lastModified => atKey.metadata?.updatedAt ?? DateTime(0); + + @override + Future create(SSHNPParams params) => update(params); + + @override + Future read() async { + var atValue = await atClient.get(atKey, getRequestOptions: GetRequestOptions()..bypassCache); + + try { + _params = SSHNPParams.fromJson(atValue.value); + } catch (e) { + _params = SSHNPParams.empty(); + } + + return params; + } + + @override + Future update(SSHNPParams params) { + _updateTimestamp(); + return atClient.put(atKey, params.toJson()); + } + + @override + Future delete(SSHNPParams params) { + return atClient.delete(atKey, deleteRequestOptions: DeleteRequestOptions()..useRemoteAtServer = true); + } +} From 9cee99354e1e5af687e7cb248730622c9e0a5658 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 12 Sep 2023 14:04:37 +0800 Subject: [PATCH 58/95] format: sort members of sshnp_params.dart --- .../sshnoports/lib/sshnp/sshnp_params.dart | 303 +++++++++--------- 1 file changed, 152 insertions(+), 151 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index 88939beb6..ef6354f0c 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -1,5 +1,7 @@ part of 'sshnp.dart'; +String _fileToProfileName(String fileName) => path.basenameWithoutExtension(fileName).replaceAll('_', ' '); + class SSHNPParams { /// Required Arguments /// These arguments do not have fallback values and must be provided. @@ -60,29 +62,6 @@ class SSHNPParams { this.atKeysFilePath = atKeysFilePath ?? getDefaultAtKeysFilePath(homeDirectory, clientAtSign); } - factory SSHNPParams.merge(SSHNPParams params1, [SSHNPPartialParams? params2]) { - params2 ??= SSHNPPartialParams.empty(); - return SSHNPParams( - profileName: params2.profileName ?? params1.profileName, - clientAtSign: params2.clientAtSign ?? params1.clientAtSign, - sshnpdAtSign: params2.sshnpdAtSign ?? params1.sshnpdAtSign, - host: params2.host ?? params1.host, - device: params2.device ?? params1.device, - port: params2.port ?? params1.port, - localPort: params2.localPort ?? params1.localPort, - atKeysFilePath: params2.atKeysFilePath ?? params1.atKeysFilePath, - sendSshPublicKey: params2.sendSshPublicKey ?? params1.sendSshPublicKey, - localSshOptions: params2.localSshOptions ?? params1.localSshOptions, - rsa: params2.rsa ?? params1.rsa, - remoteUsername: params2.remoteUsername ?? params1.remoteUsername, - verbose: params2.verbose ?? params1.verbose, - rootDomain: params2.rootDomain ?? params1.rootDomain, - localSshdPort: params2.localSshdPort ?? params1.localSshdPort, - listDevices: params2.listDevices ?? params1.listDevices, - legacyDaemon: params2.legacyDaemon ?? params1.legacyDaemon, - ); - } - factory SSHNPParams.empty() { return SSHNPParams( clientAtSign: '', @@ -91,6 +70,12 @@ class SSHNPParams { ); } + factory SSHNPParams.fromConfig(String fileName) { + return SSHNPParams.fromPartial(SSHNPPartialParams.fromConfig(fileName)); + } + + factory SSHNPParams.fromJson(String json) => SSHNPParams.fromPartial(SSHNPPartialParams.fromJson(json)); + factory SSHNPParams.fromPartial(SSHNPPartialParams partial) { AtSignLogger logger = AtSignLogger(' SSHNPParams '); @@ -121,72 +106,29 @@ class SSHNPParams { ); } - factory SSHNPParams.fromJson(String json) => SSHNPParams.fromPartial(SSHNPPartialParams.fromJson(json)); - - factory SSHNPParams.fromConfigFile(String fileName) { - return SSHNPParams.fromPartial(SSHNPPartialParams.fromConfig(fileName)); - } - - static Future> listFiles({String? directory}) async { - var fileNames = {}; - - var homeDirectory = getHomeDirectory(throwIfNull: true)!; - directory ??= getDefaultSshnpConfigDirectory(homeDirectory); - var files = Directory(directory).list(); - - await files.forEach((file) { - if (file is! File) return; - if (path.extension(file.path) != '.env') return; - if (path.basenameWithoutExtension(file.path).isEmpty) return; // ignore '.env' file - empty profileName - fileNames.add(_fileToProfileName(file.path)); - try { - var p = SSHNPParams.fromConfigFile(file.path); - fileNames.add(p.profileName!); - } catch (e) { - stderr.writeln('Error reading config file: ${file.path}'); - stderr.writeln(e); - } - }); - return fileNames; - } - - static Future fromFile(String profileName, {String? directory}) async { - var fileName = getFileName(profileName, directory: directory); - return SSHNPParams.fromConfigFile(fileName); - } - - static Future fileExists(String profileName, {String? directory}) { - var fileName = getFileName(profileName, directory: directory); - return File(fileName).exists(); - } - - static String getFileName(String profileName, {String? directory, bool replaceSpaces = true}) { - var fileName = profileName.replaceAll(' ', '_'); - return path.join( - directory ?? getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), - '$fileName.env', + factory SSHNPParams.merge(SSHNPParams params1, [SSHNPPartialParams? params2]) { + params2 ??= SSHNPPartialParams.empty(); + return SSHNPParams( + profileName: params2.profileName ?? params1.profileName, + clientAtSign: params2.clientAtSign ?? params1.clientAtSign, + sshnpdAtSign: params2.sshnpdAtSign ?? params1.sshnpdAtSign, + host: params2.host ?? params1.host, + device: params2.device ?? params1.device, + port: params2.port ?? params1.port, + localPort: params2.localPort ?? params1.localPort, + atKeysFilePath: params2.atKeysFilePath ?? params1.atKeysFilePath, + sendSshPublicKey: params2.sendSshPublicKey ?? params1.sendSshPublicKey, + localSshOptions: params2.localSshOptions ?? params1.localSshOptions, + rsa: params2.rsa ?? params1.rsa, + remoteUsername: params2.remoteUsername ?? params1.remoteUsername, + verbose: params2.verbose ?? params1.verbose, + rootDomain: params2.rootDomain ?? params1.rootDomain, + localSshdPort: params2.localSshdPort ?? params1.localSshdPort, + listDevices: params2.listDevices ?? params1.listDevices, + legacyDaemon: params2.legacyDaemon ?? params1.legacyDaemon, ); } - Future toFile({String? directory, bool overwrite = false}) async { - if (profileName == null || profileName!.isEmpty) { - throw Exception('profileName is null or empty'); - } - - var fileName = getFileName(profileName!, directory: directory); - var file = File(fileName); - - var exists = await file.exists(); - - if (exists && !overwrite) { - throw Exception('Failed to write config file: ${file.path} already exists'); - } - - // FileMode.write will create the file if it does not exist - // and overwrite existing files if it does exist - return file.writeAsString(toConfig(), mode: FileMode.write); - } - Future deleteFile({String? directory}) async { if (profileName == null || profileName!.isEmpty) { throw Exception('profileName is null or empty'); @@ -224,10 +166,6 @@ class SSHNPParams { }; } - String toJson() { - return jsonEncode(toArgs()); - } - String toConfig() { var lines = []; for (var entry in toArgs().entries) { @@ -242,12 +180,79 @@ class SSHNPParams { } return lines.join('\n'); } + + Future toFile({String? directory, bool overwrite = false}) async { + if (profileName == null || profileName!.isEmpty) { + throw Exception('profileName is null or empty'); + } + + var fileName = getFileName(profileName!, directory: directory); + var file = File(fileName); + + var exists = await file.exists(); + + if (exists && !overwrite) { + throw Exception('Failed to write config file: ${file.path} already exists'); + } + + // FileMode.write will create the file if it does not exist + // and overwrite existing files if it does exist + return file.writeAsString(toConfig(), mode: FileMode.write); + } + + String toJson() { + return jsonEncode(toArgs()); + } + + static Future fileExists(String profileName, {String? directory}) { + var fileName = getFileName(profileName, directory: directory); + return File(fileName).exists(); + } + + static Future fromFile(String profileName, {String? directory}) async { + var fileName = getFileName(profileName, directory: directory); + return SSHNPParams.fromConfig(fileName); + } + + static String getFileName(String profileName, {String? directory, bool replaceSpaces = true}) { + var fileName = profileName.replaceAll(' ', '_'); + return path.join( + directory ?? getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), + '$fileName.env', + ); + } + + static Future> listFiles({String? directory}) async { + var fileNames = {}; + + var homeDirectory = getHomeDirectory(throwIfNull: true)!; + directory ??= getDefaultSshnpConfigDirectory(homeDirectory); + var files = Directory(directory).list(); + + await files.forEach((file) { + if (file is! File) return; + if (path.extension(file.path) != '.env') return; + if (path.basenameWithoutExtension(file.path).isEmpty) return; // ignore '.env' file - empty profileName + fileNames.add(_fileToProfileName(file.path)); + try { + var p = SSHNPParams.fromConfig(file.path); + fileNames.add(p.profileName!); + } catch (e) { + stderr.writeln('Error reading config file: ${file.path}'); + stderr.writeln(e); + } + }); + return fileNames; + } } /// A class which contains a subset of the SSHNPParams /// This may be used when part of the params come from separate sources /// e.g. default values from a config file and the rest from the command line class SSHNPPartialParams { + // Non param variables + static final ArgParser parser = _createArgParser(); + /// Main Params final String? profileName; final String? clientAtSign; @@ -264,15 +269,13 @@ class SSHNPPartialParams { final String? remoteUsername; final bool? verbose; final String? rootDomain; + final bool? legacyDaemon; /// Special Params // N.B. config file is a meta param and doesn't need to be included final bool? listDevices; - // Non param variables - static final ArgParser parser = _createArgParser(); - SSHNPPartialParams({ this.profileName, this.clientAtSign, @@ -297,33 +300,42 @@ class SSHNPPartialParams { return SSHNPPartialParams(); } - /// Merge two SSHNPPartialParams objects together - /// Params in params2 take precedence over params1 - /// - localSshOptions are concatenated together as (params1 + params2) - factory SSHNPPartialParams.merge(SSHNPPartialParams params1, [SSHNPPartialParams? params2]) { - params2 ??= SSHNPPartialParams.empty(); - return SSHNPPartialParams( - profileName: params2.profileName ?? params1.profileName, - clientAtSign: params2.clientAtSign ?? params1.clientAtSign, - sshnpdAtSign: params2.sshnpdAtSign ?? params1.sshnpdAtSign, - host: params2.host ?? params1.host, - device: params2.device ?? params1.device, - port: params2.port ?? params1.port, - localPort: params2.localPort ?? params1.localPort, - atKeysFilePath: params2.atKeysFilePath ?? params1.atKeysFilePath, - sendSshPublicKey: params2.sendSshPublicKey ?? params1.sendSshPublicKey, - localSshOptions: params2.localSshOptions ?? params1.localSshOptions, - rsa: params2.rsa ?? params1.rsa, - remoteUsername: params2.remoteUsername ?? params1.remoteUsername, - verbose: params2.verbose ?? params1.verbose, - rootDomain: params2.rootDomain ?? params1.rootDomain, - localSshdPort: params2.localSshdPort ?? params1.localSshdPort, - listDevices: params2.listDevices ?? params1.listDevices, - legacyDaemon: params2.legacyDaemon ?? params1.legacyDaemon, + /// Parses args from command line + /// first merges from a config file if provided via --config-file + factory SSHNPPartialParams.fromArgs(List args) { + var params = SSHNPPartialParams.empty(); + + var parsedArgs = _createArgParser(withDefaults: false).parse(args); + + if (parsedArgs.wasParsed('config-file')) { + var configFileName = parsedArgs['config-file'] as String; + params = SSHNPPartialParams.merge( + params, + SSHNPPartialParams.fromConfig(configFileName), + ); + } + + // THIS IS A WORKAROUND IN ORDER TO BE TYPE SAFE IN SSHNPPartialParams.fromArgMap + Map parsedArgsMap = { + for (var e in parsedArgs.options) + e: SSHNPArg.fromName(e).type == ArgType.integer ? int.tryParse(parsedArgs[e]) : parsedArgs[e] + }; + + return SSHNPPartialParams.merge( + params, + SSHNPPartialParams.fromMap(parsedArgsMap), ); } - factory SSHNPPartialParams.fromArgMap(Map args) { + factory SSHNPPartialParams.fromConfig(String fileName) { + var args = _parseConfigFile(fileName); + args['profile-name'] = _fileToProfileName(fileName); + return SSHNPPartialParams.fromMap(args); + } + + factory SSHNPPartialParams.fromJson(String json) => SSHNPPartialParams.fromMap(jsonDecode(json)); + + factory SSHNPPartialParams.fromMap(Map args) { return SSHNPPartialParams( profileName: args['profile-name'], clientAtSign: args['from'], @@ -345,38 +357,29 @@ class SSHNPPartialParams { ); } - factory SSHNPPartialParams.fromJson(String json) => SSHNPPartialParams.fromArgMap(jsonDecode(json)); - - factory SSHNPPartialParams.fromConfig(String fileName) { - var args = _parseConfigFile(fileName); - args['profile-name'] = _fileToProfileName(fileName); - return SSHNPPartialParams.fromArgMap(args); - } - - /// Parses args from command line - /// first merges from a config file if provided via --config-file - factory SSHNPPartialParams.fromArgs(List args) { - var params = SSHNPPartialParams.empty(); - - var parsedArgs = _createArgParser(withDefaults: false).parse(args); - - if (parsedArgs.wasParsed('config-file')) { - var configFileName = parsedArgs['config-file'] as String; - params = SSHNPPartialParams.merge( - params, - SSHNPPartialParams.fromConfig(configFileName), - ); - } - - // THIS IS A WORKAROUND IN ORDER TO BE TYPE SAFE IN SSHNPPartialParams.fromArgMap - Map parsedArgsMap = { - for (var e in parsedArgs.options) - e: SSHNPArg.fromName(e).type == ArgType.integer ? int.tryParse(parsedArgs[e]) : parsedArgs[e] - }; - - return SSHNPPartialParams.merge( - params, - SSHNPPartialParams.fromArgMap(parsedArgsMap), + /// Merge two SSHNPPartialParams objects together + /// Params in params2 take precedence over params1 + /// - localSshOptions are concatenated together as (params1 + params2) + factory SSHNPPartialParams.merge(SSHNPPartialParams params1, [SSHNPPartialParams? params2]) { + params2 ??= SSHNPPartialParams.empty(); + return SSHNPPartialParams( + profileName: params2.profileName ?? params1.profileName, + clientAtSign: params2.clientAtSign ?? params1.clientAtSign, + sshnpdAtSign: params2.sshnpdAtSign ?? params1.sshnpdAtSign, + host: params2.host ?? params1.host, + device: params2.device ?? params1.device, + port: params2.port ?? params1.port, + localPort: params2.localPort ?? params1.localPort, + atKeysFilePath: params2.atKeysFilePath ?? params1.atKeysFilePath, + sendSshPublicKey: params2.sendSshPublicKey ?? params1.sendSshPublicKey, + localSshOptions: params2.localSshOptions ?? params1.localSshOptions, + rsa: params2.rsa ?? params1.rsa, + remoteUsername: params2.remoteUsername ?? params1.remoteUsername, + verbose: params2.verbose ?? params1.verbose, + rootDomain: params2.rootDomain ?? params1.rootDomain, + localSshdPort: params2.localSshdPort ?? params1.localSshdPort, + listDevices: params2.listDevices ?? params1.listDevices, + legacyDaemon: params2.legacyDaemon ?? params1.legacyDaemon, ); } @@ -488,5 +491,3 @@ class SSHNPPartialParams { } } } - -String _fileToProfileName(String fileName) => path.basenameWithoutExtension(fileName).replaceAll('_', ' '); From 5c0ec6d478698eef77d2f296eab0beaaf97fac35 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 12 Sep 2023 14:23:34 +0800 Subject: [PATCH 59/95] chore: move config_source to sshnoports package --- .../config_source/config_file_source.dart | 54 +++++++++++ .../sshnp/config_source/config_source.dart | 21 ++++ .../config_source/config_sync_source.dart | 0 .../config_source/config_file_source.dart | 95 ------------------- .../config_source/config_source.dart | 35 ------- 5 files changed, 75 insertions(+), 130 deletions(-) create mode 100644 packages/sshnoports/lib/sshnp/config_source/config_file_source.dart create mode 100644 packages/sshnoports/lib/sshnp/config_source/config_source.dart rename packages/{sshnp_gui/lib/src/repository => sshnoports/lib/sshnp}/config_source/config_sync_source.dart (100%) delete mode 100644 packages/sshnp_gui/lib/src/repository/config_source/config_file_source.dart delete mode 100644 packages/sshnp_gui/lib/src/repository/config_source/config_source.dart diff --git a/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart b/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart new file mode 100644 index 000000000..93b876d8f --- /dev/null +++ b/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart @@ -0,0 +1,54 @@ +part of 'config_source.dart'; + +/// [ConfigSource] covariant from a [File] +class ConfigFileSource implements ConfigSource { + late final String profileName; + late final String? directory; + late final String? fileName; + late final File file; + + SSHNPParams? _params; + + @override + SSHNPParams get params => _params ?? SSHNPParams.empty(); + + ConfigFileSource._(this.profileName, {this.directory, this.fileName}) + : file = File( + profileNameToConfigFileName( + fileName ?? profileName, + directory: directory, + replaceSpaces: (fileName == null), // only replace spaces for [profileName] not [fileName] + ), + ); + @override + DateTime get lastModified => file.lastModifiedSync(); + + @override + Future create(SSHNPParams params) async { + if (params.profileName != profileName) { + throw ArgumentError.value(params.profileName, 'params.profileName', 'must be $profileName'); + } + await params.toFile(directory: directory); + } + + @override + Future read() async { + try { + var params = SSHNPParams.fromConfig(file.path); + _params = params; + } catch (e) { + _params = null; + } + return params; + } + + @override + Future update(SSHNPParams params) async { + params.toFile(directory: directory, overwrite: true); + } + + @override + Future delete(SSHNPParams params) async { + await file.delete(); + } +} diff --git a/packages/sshnoports/lib/sshnp/config_source/config_source.dart b/packages/sshnoports/lib/sshnp/config_source/config_source.dart new file mode 100644 index 000000000..ebd3e318a --- /dev/null +++ b/packages/sshnoports/lib/sshnp/config_source/config_source.dart @@ -0,0 +1,21 @@ +import 'dart:io'; + +import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnoports/sshnp/utils.dart'; +import 'package:sshnoports/sshnpd/sshnpd.dart'; + +part 'config_sync_source.dart'; +part 'config_file_source.dart'; + +/// Generic Type +abstract class ConfigSource { + DateTime get lastModified; + SSHNPParams get params; + + Future create(SSHNPParams params); + Future read(); + Future update(SSHNPParams params); + Future delete(SSHNPParams params); + +} diff --git a/packages/sshnp_gui/lib/src/repository/config_source/config_sync_source.dart b/packages/sshnoports/lib/sshnp/config_source/config_sync_source.dart similarity index 100% rename from packages/sshnp_gui/lib/src/repository/config_source/config_sync_source.dart rename to packages/sshnoports/lib/sshnp/config_source/config_sync_source.dart diff --git a/packages/sshnp_gui/lib/src/repository/config_source/config_file_source.dart b/packages/sshnp_gui/lib/src/repository/config_source/config_file_source.dart deleted file mode 100644 index 7a0c59b30..000000000 --- a/packages/sshnp_gui/lib/src/repository/config_source/config_file_source.dart +++ /dev/null @@ -1,95 +0,0 @@ -part of 'config_source.dart'; - -/// [ConfigSource] covariant from a [File] -abstract class ConfigFileSource implements ConfigSource { - late final String profileName; - late final String? directory; - late final String? fileName; - late final File file; - - SSHNPParams? _params; - - @override - SSHNPParams get params => _params ?? SSHNPParams.empty(); - - ConfigFileSource._(this.profileName, {this.directory, this.fileName}) - : file = File( - SSHNPParams.getFileName( - fileName ?? profileName, - directory: directory, - replaceSpaces: (fileName == null), // only replace spaces for [profileName] not [fileName] - ), - ); - - factory ConfigFileSource.sandboxed(String profileName) => SandboxedConfigFileSource(profileName); - - factory ConfigFileSource.imported(String profileName, String directory, {String? fileName}) => - ImportedConfigFileSource(profileName, directory, fileName: fileName); - - factory ConfigFileSource.exported(String profileName, String directory, {String? fileName}) => - ExportedConfigFileSource(profileName, directory, fileName: fileName); - - @override - DateTime get lastModified => file.lastModifiedSync(); - - @override - Future create(SSHNPParams params) async { - if (params.profileName != profileName) { - throw ArgumentError.value(params.profileName, 'params.profileName', 'must be $profileName'); - } - await params.toFile(directory: directory); - } - - @override - Future read() async { - try { - var params = SSHNPParams.fromConfigFile(file.path); - _params = params; - } catch (e) { - _params = null; - } - return params; - } - - @override - Future update(SSHNPParams params) async { - params.toFile(directory: directory, overwrite: true); - } - - @override - Future delete(SSHNPParams params) async { - await file.delete(); - } -} - -class SandboxedConfigFileSource extends ConfigFileSource { - SandboxedConfigFileSource(String profileName) : super._(profileName); -} - -class ImportedConfigFileSource extends ConfigFileSource { - /// External Config File Source uses an external, user-selected file path - ImportedConfigFileSource(String profileName, String directory, {String? fileName}) - : super._(profileName, directory: directory, fileName: fileName); - - // no-op - @override - Future create(SSHNPParams params) => throw UnsupportedError('Cannot create an imported config file'); - @override - Future update(SSHNPParams params) => throw UnsupportedError('Cannot update an imported config file'); - @override - Future delete(SSHNPParams params) => throw UnsupportedError('Cannot delete an imported config file'); -} - -class ExportedConfigFileSource extends ConfigFileSource { - /// Exported Config File Source uses an external, user-selected file path - ExportedConfigFileSource(String profileName, String directory, {String? fileName}) - : super._(profileName, directory: directory, fileName: fileName); - - // no-op - @override - DateTime get lastModified => throw UnsupportedError('Cannot get lastModified of an exported config file'); - @override - Future read() => throw UnsupportedError('Cannot read an exported config file'); - @override - Future delete(SSHNPParams params) => throw UnsupportedError('Cannot delete an exported config file'); -} diff --git a/packages/sshnp_gui/lib/src/repository/config_source/config_source.dart b/packages/sshnp_gui/lib/src/repository/config_source/config_source.dart deleted file mode 100644 index 7bc6c5e5f..000000000 --- a/packages/sshnp_gui/lib/src/repository/config_source/config_source.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:io'; - -import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; -import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnoports/sshnpd/sshnpd.dart'; - -part 'config_sync_source.dart'; -part 'config_file_source.dart'; - -/// Generic Type -abstract class ConfigSource { - DateTime get lastModified; - SSHNPParams get params; - - Future create(SSHNPParams params); - Future read(); - Future update(SSHNPParams params); - Future delete(SSHNPParams params); - - factory ConfigSource.synced(String profileName, {AtClient? atClient}) { - return ConfigSyncSource.synced(profileName, atClient: atClient); - } - - factory ConfigSource.exported(String profileName, directory) { - return ConfigFileSource.exported(profileName, directory); - } - - factory ConfigSource.imported(String profileName, directory) { - return ConfigFileSource.imported(profileName, directory); - } - - factory ConfigSource.sandboxed(String profileName) { - return ConfigFileSource.sandboxed(profileName); - } -} From 3b3ddaa493080470f21421cf9b1b43c77f567358 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 12 Sep 2023 14:24:35 +0800 Subject: [PATCH 60/95] chore: more cleanup --- .../config_source/config_file_source.dart | 2 +- .../sshnp/config_source/config_source.dart | 4 +--- .../sshnoports/lib/sshnp/sshnp_params.dart | 22 +++++-------------- packages/sshnoports/lib/sshnp/utils.dart | 20 ++++++++++++----- .../repository/authentication_repository.dart | 8 +++---- 5 files changed, 25 insertions(+), 31 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart b/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart index 93b876d8f..f84f0d061 100644 --- a/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart +++ b/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart @@ -44,7 +44,7 @@ class ConfigFileSource implements ConfigSource { @override Future update(SSHNPParams params) async { - params.toFile(directory: directory, overwrite: true); + await params.toFile(directory: directory, overwrite: true); } @override diff --git a/packages/sshnoports/lib/sshnp/config_source/config_source.dart b/packages/sshnoports/lib/sshnp/config_source/config_source.dart index ebd3e318a..8b3982c25 100644 --- a/packages/sshnoports/lib/sshnp/config_source/config_source.dart +++ b/packages/sshnoports/lib/sshnp/config_source/config_source.dart @@ -1,6 +1,5 @@ import 'dart:io'; - -import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; +import 'package:at_client/at_client.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshnp/utils.dart'; import 'package:sshnoports/sshnpd/sshnpd.dart'; @@ -17,5 +16,4 @@ abstract class ConfigSource { Future read(); Future update(SSHNPParams params); Future delete(SSHNPParams params); - } diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index ef6354f0c..7199bdb33 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -1,7 +1,5 @@ part of 'sshnp.dart'; -String _fileToProfileName(String fileName) => path.basenameWithoutExtension(fileName).replaceAll('_', ' '); - class SSHNPParams { /// Required Arguments /// These arguments do not have fallback values and must be provided. @@ -134,7 +132,7 @@ class SSHNPParams { throw Exception('profileName is null or empty'); } - var fileName = getFileName(profileName!, directory: directory); + var fileName = profileNameToConfigFileName(profileName!, directory: directory); var file = File(fileName); var exists = await file.exists(); @@ -186,7 +184,7 @@ class SSHNPParams { throw Exception('profileName is null or empty'); } - var fileName = getFileName(profileName!, directory: directory); + var fileName = profileNameToConfigFileName(profileName!, directory: directory); var file = File(fileName); var exists = await file.exists(); @@ -205,23 +203,15 @@ class SSHNPParams { } static Future fileExists(String profileName, {String? directory}) { - var fileName = getFileName(profileName, directory: directory); + var fileName = profileNameToConfigFileName(profileName, directory: directory); return File(fileName).exists(); } static Future fromFile(String profileName, {String? directory}) async { - var fileName = getFileName(profileName, directory: directory); + var fileName = profileNameToConfigFileName(profileName, directory: directory); return SSHNPParams.fromConfig(fileName); } - static String getFileName(String profileName, {String? directory, bool replaceSpaces = true}) { - var fileName = profileName.replaceAll(' ', '_'); - return path.join( - directory ?? getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), - '$fileName.env', - ); - } - static Future> listFiles({String? directory}) async { var fileNames = {}; @@ -233,7 +223,7 @@ class SSHNPParams { if (file is! File) return; if (path.extension(file.path) != '.env') return; if (path.basenameWithoutExtension(file.path).isEmpty) return; // ignore '.env' file - empty profileName - fileNames.add(_fileToProfileName(file.path)); + fileNames.add(configFileNameToProfileName(file.path)); try { var p = SSHNPParams.fromConfig(file.path); fileNames.add(p.profileName!); @@ -329,7 +319,7 @@ class SSHNPPartialParams { factory SSHNPPartialParams.fromConfig(String fileName) { var args = _parseConfigFile(fileName); - args['profile-name'] = _fileToProfileName(fileName); + args['profile-name'] = configFileNameToProfileName(fileName); return SSHNPPartialParams.fromMap(args); } diff --git a/packages/sshnoports/lib/sshnp/utils.dart b/packages/sshnoports/lib/sshnp/utils.dart index d63285d84..d29adc67f 100644 --- a/packages/sshnoports/lib/sshnp/utils.dart +++ b/packages/sshnoports/lib/sshnp/utils.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:sshnoports/common/utils.dart'; import 'package:at_utils/at_logger.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:path/path.dart' as path; Future cleanUpAfterReverseSsh(SSHNP sshnp) async { if (!sshnp.initialized) { @@ -24,10 +25,8 @@ Future cleanUpAfterReverseSsh(SSHNP sshnp) async { sshnp.logger.info('Tidying up files'); // Delete the generated RSA keys and remove the entry from ~/.ssh/authorized_keys await deleteFile('$sshHomeDirectory${sshnp.sessionId}_sshnp', sshnp.logger); - await deleteFile( - '$sshHomeDirectory${sshnp.sessionId}_sshnp.pub', sshnp.logger); - await removeFromAuthorizedKeys( - sshHomeDirectory, sshnp.sessionId, sshnp.logger); + await deleteFile('$sshHomeDirectory${sshnp.sessionId}_sshnp.pub', sshnp.logger); + await removeFromAuthorizedKeys(sshHomeDirectory, sshnp.sessionId, sshnp.logger); } Future deleteFile(String fileName, AtSignLogger logger) async { @@ -42,8 +41,7 @@ Future deleteFile(String fileName, AtSignLogger logger) async { } } -Future removeFromAuthorizedKeys( - String sshHomeDirectory, String sessionId, AtSignLogger logger) async { +Future removeFromAuthorizedKeys(String sshHomeDirectory, String sessionId, AtSignLogger logger) async { try { final File file = File('${sshHomeDirectory}authorized_keys'); // read into List of strings @@ -86,3 +84,13 @@ bool useDirectSsh(bool legacyDaemon, String host) { } } } + +String configFileNameToProfileName(String fileName) => path.basenameWithoutExtension(fileName).replaceAll('_', ' '); + +String profileNameToConfigFileName(String profileName, {String? directory, bool replaceSpaces = true}) { + var fileName = profileName.replaceAll(' ', '_'); + return path.join( + directory ?? getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), + '$fileName.env', + ); +} diff --git a/packages/sshnp_gui/lib/src/repository/authentication_repository.dart b/packages/sshnp_gui/lib/src/repository/authentication_repository.dart index 2d9f55e40..a58cc896e 100644 --- a/packages/sshnp_gui/lib/src/repository/authentication_repository.dart +++ b/packages/sshnp_gui/lib/src/repository/authentication_repository.dart @@ -16,6 +16,9 @@ import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; import 'package:sshnp_gui/src/repository/navigation_repository.dart'; +/// A provider that exposes an [AuthenticationRepository] instance to the app. +final authenticationRepositoryProvider = Provider((ref) => AuthenticationRepository()); + /// A singleton that makes all the network calls to the @platform. class AuthenticationRepository { AuthenticationRepository(); @@ -147,8 +150,3 @@ class AuthenticationRepository { return await getAtSignDetails(atSign!); } } - -/// A provider that exposes an [AuthenticationRepository] instance to the app. -final authenticationRepositoryProvider = Provider((ref) { - return AuthenticationRepository(); -}); From 9b8332ce4cfe63626fd622f7ed142f44d6b96485 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 12 Sep 2023 18:25:32 +0800 Subject: [PATCH 61/95] chore: checkin config file management --- .../lib/sshnp/config_file_utils.dart | 172 ++++++++++++++++ .../sshnoports/lib/sshnp/config_manager.dart | 159 +++++++++++++++ .../config_source/config_file_source.dart | 13 +- .../sshnp/config_source/config_source.dart | 14 +- .../config_source/config_sync_source.dart | 27 +-- packages/sshnoports/lib/sshnp/sshnp.dart | 1 + packages/sshnoports/lib/sshnp/sshnp_arg.dart | 74 +++++-- .../sshnoports/lib/sshnp/sshnp_params.dart | 191 +----------------- packages/sshnoports/lib/sshnp/utils.dart | 11 - .../src/controllers/config_controller.dart | 52 +++-- .../profile_delete_action.dart | 2 +- .../widgets/profile_bar/profile_bar.dart | 6 +- .../widgets/profile_form/profile_form.dart | 38 ++-- packages/sshnp_gui/macos/Podfile.lock | 2 +- 14 files changed, 479 insertions(+), 283 deletions(-) create mode 100644 packages/sshnoports/lib/sshnp/config_file_utils.dart create mode 100644 packages/sshnoports/lib/sshnp/config_manager.dart diff --git a/packages/sshnoports/lib/sshnp/config_file_utils.dart b/packages/sshnoports/lib/sshnp/config_file_utils.dart new file mode 100644 index 000000000..aa9be83bc --- /dev/null +++ b/packages/sshnoports/lib/sshnp/config_file_utils.dart @@ -0,0 +1,172 @@ +import 'dart:io'; + +import 'package:at_client/at_client.dart'; +import 'package:sshnoports/common/utils.dart'; +import 'package:sshnoports/sshnp/config_manager.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnoports/sshnp/sshnp_arg.dart'; +import 'package:path/path.dart' as path; + +String configFileNameToProfileName(String fileName, {bool replaceSpaces = true}) { + var profileName = path.basenameWithoutExtension(fileName); + if (replaceSpaces) profileName = profileName.replaceAll('_', ' '); + return profileName; +} + +String profileNameToConfigFileName(String profileName, {String? directory, bool replaceSpaces = true}) { + var fileName = profileName; + if (replaceSpaces) fileName = fileName.replaceAll(' ', '_'); + return path.join( + directory ?? getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), + '$fileName.env', + ); +} + +const String _keyPrefix = 'profile_'; + +String atKeyToProfileName(AtKey atKey, {bool replaceSpaces = true}) { + var profileName = atKey.key!.split('.').first; + print('r1: $profileName'); + profileName = profileName.replaceFirst(_keyPrefix, ''); + print('r2: $profileName'); + if (replaceSpaces) profileName = profileName.replaceAll('_', ' '); + print('r3: $profileName'); + return profileName; +} + +AtKey profileNameToAtKey(String profileName, {String sharedBy = '', bool replaceSpaces = true}) { + if (replaceSpaces) profileName = profileName.replaceAll(' ', '_'); + return AtKey.self( + '$_keyPrefix$profileName', + namespace: ConfigManager.namespace, + sharedBy: sharedBy, + ).build(); +} + +Future createConfigDirectory({String? directory}) async { + directory ??= getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!); + var dir = Directory(directory); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + return dir; +} + +Future> listProfilesFromDirectory({String? directory}) async { + var profileNames = {}; + + var homeDirectory = getHomeDirectory(throwIfNull: true)!; + directory ??= getDefaultSshnpConfigDirectory(homeDirectory); + var files = Directory(directory).list(); + + await files.forEach((file) { + if (file is! File) return; + if (path.extension(file.path) != '.env') return; + if (path.basenameWithoutExtension(file.path).isEmpty) return; // ignore '.env' file - empty profileName + profileNames.add(configFileNameToProfileName(file.path)); + }); + return profileNames; +} + +Map parseConfigFile(String fileName) { + Map args = {}; + + if (path.normalize(fileName).contains('/') || path.normalize(fileName).contains(r'\')) { + fileName = path.normalize(path.absolute(fileName)); + } else { + fileName = + path.normalize(path.absolute(getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), fileName)); + } + + File file = File(fileName); + + if (!file.existsSync()) { + throw Exception('Config file does not exist: $fileName'); + } + try { + List lines = file.readAsLinesSync(); + + for (String line in lines) { + if (line.startsWith('#')) continue; + + var parts = line.split('='); + if (parts.length != 2) continue; + + var key = parts[0].trim(); + var value = parts[1].trim(); + + SSHNPArg arg = SSHNPArg.fromBashName(key); + if (arg.name.isEmpty) continue; + + switch (arg.format) { + case ArgFormat.flag: + if (value.toLowerCase() == 'true') { + args[arg.name] = true; + } + continue; + case ArgFormat.multiOption: + var values = value.split(','); + args.putIfAbsent(arg.name, () => []); + for (String val in values) { + if (val.isEmpty) continue; + args[arg.name].add(val); + } + continue; + case ArgFormat.option: + if (value.isEmpty) continue; + if (arg.type == ArgType.integer) { + args[arg.name] = int.tryParse(value); + } else { + args[arg.name] = value; + } + continue; + } + } + return args; + } on FileSystemException { + throw Exception('Error reading config file: $fileName'); + } catch (e) { + throw Exception('Error parsing config file: $fileName'); + } +} + +Future sshnpParamsFromFile(String profileName, {String? directory}) async { + var fileName = profileNameToConfigFileName(profileName, directory: directory); + return SSHNPParams.fromConfigFile(fileName); +} + +Future sshnpParamsToFile(SSHNPParams params, {String? directory, bool overwrite = false}) async { + if (params.profileName == null || params.profileName!.isEmpty) { + throw Exception('profileName is null or empty'); + } + + var fileName = profileNameToConfigFileName(params.profileName!, directory: directory); + var file = File(fileName); + + var exists = await file.exists(); + + if (exists && !overwrite) { + throw Exception('Failed to write config file: ${file.path} already exists'); + } + + // FileMode.write will create the file if it does not exist + // and overwrite existing files if it does exist + return file.writeAsString(params.toConfig(), mode: FileMode.write); +} + +Future deleteFile(SSHNPParams params, {String? directory}) async { + if (params.profileName == null || params.profileName!.isEmpty) { + throw Exception('profileName is null or empty'); + } + + var fileName = profileNameToConfigFileName(params.profileName!, directory: directory); + var file = File(fileName); + + var exists = await file.exists(); + + if (!exists) { + throw Exception('Cannot delete ${file.path}, file does not exist'); + } + + return file.delete(); +} diff --git a/packages/sshnoports/lib/sshnp/config_manager.dart b/packages/sshnoports/lib/sshnp/config_manager.dart new file mode 100644 index 000000000..66d8af432 --- /dev/null +++ b/packages/sshnoports/lib/sshnp/config_manager.dart @@ -0,0 +1,159 @@ +import 'dart:async'; + +import 'package:at_client/at_client.dart'; +import 'package:sshnoports/sshnp/config_file_utils.dart'; +import 'package:sshnoports/sshnp/config_source/config_source.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnoports/sshnpd/sshnpd.dart'; + +class ConfigManager { + static const namespace = 'profiles.${SSHNPD.namespace}'; + + final Map _configFamilyManagers = {}; + Map get managers => _configFamilyManagers; + + final Completer _initialized = Completer(); + Future get initialized => _initialized.future; + + final AtClient _atClient; + + ConfigManager(this._atClient, {bool useRemoteConfig = true, bool useLocalConfig = true}) { + _init(useRemoteConfig: useRemoteConfig, useLocalConfig: useLocalConfig); + } + + Future _init({bool useRemoteConfig = true, bool useLocalConfig = true}) async { + await Future.wait([ + if (useLocalConfig) _loadFiles(), + if (useRemoteConfig) _loadKeys(), + ]); + print('loaded config'); + await Future.wait(managers.values.map((manager) { + return Future.wait([ + manager._init(), + // manager.syncToRemote(_atClient), + // manager.syncToLocal(), + // TOdo sync based on which is latest / more correct + ]); + })); + print('synced config'); + _initialized.complete(true); + } + + Future _loadFiles() async { + await createConfigDirectory(); + var profiles = await listProfilesFromDirectory(); + for (var fileName in profiles) { + var profileName = configFileNameToProfileName(fileName); + if (!_configFamilyManagers.containsKey(profileName)) { + _configFamilyManagers[profileName] = ConfigFamilyManager(profileName); + } + _configFamilyManagers[profileName]!.sources.add(ConfigSource.file(profileName)); + } + } + + Future _loadKeys() async { + var keys = await _atClient.getAtKeys(regex: namespace); + for (var atKey in keys) { + var profileName = atKeyToProfileName(atKey); + print('load: $atKey, $profileName'); + if (!_configFamilyManagers.containsKey(profileName)) { + _configFamilyManagers[profileName] = ConfigFamilyManager(profileName); + } + _configFamilyManagers[profileName]!.sources.add(ConfigSource.sync(profileName, _atClient)); + } + } + + Future operator [](String key) async { + _configFamilyManagers[key] ??= ConfigFamilyManager(key); + await _configFamilyManagers[key]!._init(); + return _configFamilyManagers[key]!; + } +} + +class ConfigFamilyManager { + final String profileName; + final List sources = []; + + ConfigFamilyManager(this.profileName); + + final Completer initialized = Completer(); + bool _initCalled = false; + + late SSHNPParams _params; + SSHNPParams get params => _params; + + Future _init() async { + print('called init for $profileName'); + if (_initCalled) return; + _initCalled = true; + + await Future.wait(sources.map((s) => s.read())); + print('done read for $profileName'); + var latestModified = DateTime(0); + + ConfigSource? latestSource; + for (var source in sources) { + var modified = await source.getLastModified(refresh: false); + if (modified.isAfter(latestModified)) { + latestModified = modified; + latestSource = source; + } + } + print('done getLastestSource for $profileName'); + + _params = latestSource?.params ?? SSHNPParams.empty(); + initialized.complete(true); + } + + Future create(SSHNPParams params) async { + await initialized.future; + _params = params; + await Future.wait(sources.map((s) => s.create(params))); + } + + Future update(SSHNPParams params) async { + await initialized.future; + _params = params; + await Future.wait(sources.map((s) => s.update(params))); + } + + Future delete() async { + if (!initialized.isCompleted) { + await _init(); + } + await Future.wait(sources.map((s) => s.delete())); + } + + Future syncToRemote(AtClient atClient) async { + print('starting sync'); + await initialized.future; + print('init sync local'); + var remotes = sources.whereType(); + if (remotes.isNotEmpty) { + await Future.wait(remotes.map((s) => s.update(params))); + print('done sync refresh'); + return; + } + var source = ConfigSource.sync(profileName, atClient); + await source.create(params); + print('done sync'); + sources.add(source); + } + + Future syncToLocal() async { + print('starting sync local'); + await initialized.future; + print('init sync local'); + var locals = sources.whereType(); + if (locals.isNotEmpty) { + await Future.wait(locals.map((s) => s.update(params))); + print('done sync refresh local'); + return; + } + print('comp: $profileName, ${params.toJson()}'); + var source = ConfigSource.file(profileName); + await source.create(params); + print('done sync local'); + sources.add(source); + } +} diff --git a/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart b/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart index f84f0d061..280b3fca7 100644 --- a/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart +++ b/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart @@ -12,7 +12,7 @@ class ConfigFileSource implements ConfigSource { @override SSHNPParams get params => _params ?? SSHNPParams.empty(); - ConfigFileSource._(this.profileName, {this.directory, this.fileName}) + ConfigFileSource(this.profileName, {this.directory, this.fileName}) : file = File( profileNameToConfigFileName( fileName ?? profileName, @@ -20,21 +20,22 @@ class ConfigFileSource implements ConfigSource { replaceSpaces: (fileName == null), // only replace spaces for [profileName] not [fileName] ), ); + @override - DateTime get lastModified => file.lastModifiedSync(); + DateTime getLastModified({bool refresh = true}) => file.lastModifiedSync(); @override Future create(SSHNPParams params) async { if (params.profileName != profileName) { throw ArgumentError.value(params.profileName, 'params.profileName', 'must be $profileName'); } - await params.toFile(directory: directory); + await sshnpParamsToFile(params, directory: directory); } @override Future read() async { try { - var params = SSHNPParams.fromConfig(file.path); + var params = SSHNPParams.fromConfigFile(file.path); _params = params; } catch (e) { _params = null; @@ -44,11 +45,11 @@ class ConfigFileSource implements ConfigSource { @override Future update(SSHNPParams params) async { - await params.toFile(directory: directory, overwrite: true); + await sshnpParamsToFile(params, directory: directory, overwrite: true); } @override - Future delete(SSHNPParams params) async { + Future delete() async { await file.delete(); } } diff --git a/packages/sshnoports/lib/sshnp/config_source/config_source.dart b/packages/sshnoports/lib/sshnp/config_source/config_source.dart index 8b3982c25..09d918b28 100644 --- a/packages/sshnoports/lib/sshnp/config_source/config_source.dart +++ b/packages/sshnoports/lib/sshnp/config_source/config_source.dart @@ -1,19 +1,25 @@ +import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:at_client/at_client.dart'; +import 'package:sshnoports/sshnp/config_file_utils.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnoports/sshnp/utils.dart'; -import 'package:sshnoports/sshnpd/sshnpd.dart'; part 'config_sync_source.dart'; part 'config_file_source.dart'; /// Generic Type abstract class ConfigSource { - DateTime get lastModified; SSHNPParams get params; + FutureOr getLastModified({bool refresh = true}); Future create(SSHNPParams params); Future read(); Future update(SSHNPParams params); - Future delete(SSHNPParams params); + Future delete(); + + factory ConfigSource.file(String profileName, {String? directory, String? fileName}) => + ConfigFileSource(profileName, directory: directory, fileName: fileName); + + factory ConfigSource.sync(String profileName, AtClient atClient) => ConfigSyncSource(profileName, atClient); } diff --git a/packages/sshnoports/lib/sshnp/config_source/config_sync_source.dart b/packages/sshnoports/lib/sshnp/config_source/config_sync_source.dart index 1236e88de..fe003dc87 100644 --- a/packages/sshnoports/lib/sshnp/config_source/config_sync_source.dart +++ b/packages/sshnoports/lib/sshnp/config_source/config_sync_source.dart @@ -12,13 +12,9 @@ class ConfigSyncSource implements ConfigSource { ConfigSyncSource._(this.atKey, this.atClient); - factory ConfigSyncSource.synced(String profileName, {AtClient? atClient}) { - AtKey atKey = AtKey.self( - 'profile_$profileName', - namespace: SSHNPD.namespace, - ).build(); - - atClient ??= AtClientManager.getInstance().atClient; + factory ConfigSyncSource(String profileName, AtClient atClient) { + AtKey atKey = profileNameToAtKey(profileName, sharedBy: atClient.getCurrentAtSign()!); + print('sync src: $profileName, $atKey'); return ConfigSyncSource._(atKey, atClient); } @@ -27,17 +23,22 @@ class ConfigSyncSource implements ConfigSource { } @override - DateTime get lastModified => atKey.metadata?.updatedAt ?? DateTime(0); + Future getLastModified({bool refresh = true}) async { + if (refresh) await atClient.get(atKey); + return atKey.metadata?.updatedAt ?? DateTime(0); + } @override Future create(SSHNPParams params) => update(params); @override Future read() async { - var atValue = await atClient.get(atKey, getRequestOptions: GetRequestOptions()..bypassCache); - + print('called read for $atKey'); + var atValue = await atClient.get(atKey); + print('done read'); try { - _params = SSHNPParams.fromJson(atValue.value); + print("json: ${atValue.value}"); + _params = SSHNPParams.fromJson(atValue.value!); } catch (e) { _params = SSHNPParams.empty(); } @@ -52,7 +53,7 @@ class ConfigSyncSource implements ConfigSource { } @override - Future delete(SSHNPParams params) { - return atClient.delete(atKey, deleteRequestOptions: DeleteRequestOptions()..useRemoteAtServer = true); + Future delete() { + return atClient.delete(atKey); } } diff --git a/packages/sshnoports/lib/sshnp/sshnp.dart b/packages/sshnoports/lib/sshnp/sshnp.dart index 48e8b5177..b529ef99c 100644 --- a/packages/sshnoports/lib/sshnp/sshnp.dart +++ b/packages/sshnoports/lib/sshnp/sshnp.dart @@ -13,6 +13,7 @@ import 'package:path/path.dart' as path; import 'package:sshnoports/common/create_at_client_cli.dart'; import 'package:sshnoports/common/supported_ssh_clients.dart'; import 'package:sshnoports/common/utils.dart'; +import 'package:sshnoports/sshnp/config_file_utils.dart'; import 'package:sshnoports/sshnp/sshnp_arg.dart'; import 'package:sshnoports/sshnp/utils.dart'; import 'package:sshnoports/sshnpd/sshnpd.dart'; diff --git a/packages/sshnoports/lib/sshnp/sshnp_arg.dart b/packages/sshnoports/lib/sshnp/sshnp_arg.dart index a9d59c8b2..cad8d5936 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_arg.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_arg.dart @@ -1,3 +1,5 @@ +import 'package:args/args.dart'; + import 'sshnp.dart'; enum ArgFormat { @@ -84,24 +86,20 @@ class SSHNPArg { SSHNPArg( name: 'port', abbr: 'p', - help: - 'TCP port to connect back to (only required if --host specified a FQDN/IP)', + help: 'TCP port to connect back to (only required if --host specified a FQDN/IP)', defaultsTo: SSHNP.defaultPort, type: ArgType.integer, ), SSHNPArg( - name: 'local-port', - abbr: 'l', - help: - 'Reverse ssh port to listen on, on your local machine, by sshnp default finds a spare port', - defaultsTo: SSHNP.defaultLocalPort, - type: ArgType.integer - ), + name: 'local-port', + abbr: 'l', + help: 'Reverse ssh port to listen on, on your local machine, by sshnp default finds a spare port', + defaultsTo: SSHNP.defaultLocalPort, + type: ArgType.integer), SSHNPArg( name: 'ssh-public-key', abbr: 's', - help: - 'Public key file from ~/.ssh to be appended to authorized_hosts on the remote device', + help: 'Public key file from ~/.ssh to be appended to authorized_hosts on the remote device', defaultsTo: SSHNP.defaultSendSshPublicKey, ), SSHNPArg( @@ -145,7 +143,6 @@ class SSHNPArg { format: ArgFormat.option, type: ArgType.integer, ), - SSHNPArg( name: 'legacy-daemon', defaultsTo: SSHNP.defaultLegacyDaemon, @@ -159,3 +156,56 @@ class SSHNPArg { return 'SSHNPArg{format: $format, name: $name, abbr: $abbr, help: $help, mandatory: $mandatory, defaultsTo: $defaultsTo, type: $type}'; } } + +ArgParser createArgParser({ + bool withConfig = true, + bool withDefaults = true, + bool withListDevices = true, +}) { + var parser = ArgParser(); + // Basic arguments + for (SSHNPArg arg in SSHNPArg.args) { + switch (arg.format) { + case ArgFormat.option: + parser.addOption( + arg.name, + abbr: arg.abbr, + mandatory: arg.mandatory, + defaultsTo: withDefaults ? arg.defaultsTo?.toString() : null, + help: arg.help, + ); + break; + case ArgFormat.multiOption: + parser.addMultiOption( + arg.name, + abbr: arg.abbr, + defaultsTo: withDefaults ? arg.defaultsTo as List? : null, + help: arg.help, + ); + break; + case ArgFormat.flag: + parser.addFlag( + arg.name, + abbr: arg.abbr, + defaultsTo: withDefaults ? arg.defaultsTo as bool? : null, + help: arg.help, + ); + break; + } + } + if (withConfig) { + parser.addOption( + 'config-file', + help: 'Read args from a config file\nMandatory args are not required if already supplied in the config file', + ); + } + if (withListDevices) { + parser.addFlag( + 'list-devices', + aliases: ['ls'], + negatable: false, + help: 'List available devices', + ); + } + return parser; +} diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index 7199bdb33..fc56aa987 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -68,8 +68,8 @@ class SSHNPParams { ); } - factory SSHNPParams.fromConfig(String fileName) { - return SSHNPParams.fromPartial(SSHNPPartialParams.fromConfig(fileName)); + factory SSHNPParams.fromConfigFile(String fileName) { + return SSHNPParams.fromPartial(SSHNPPartialParams.fromConfigFile(fileName)); } factory SSHNPParams.fromJson(String json) => SSHNPParams.fromPartial(SSHNPPartialParams.fromJson(json)); @@ -127,23 +127,6 @@ class SSHNPParams { ); } - Future deleteFile({String? directory}) async { - if (profileName == null || profileName!.isEmpty) { - throw Exception('profileName is null or empty'); - } - - var fileName = profileNameToConfigFileName(profileName!, directory: directory); - var file = File(fileName); - - var exists = await file.exists(); - - if (!exists) { - throw Exception('Cannot delete ${file.path}, file does not exist'); - } - - return file.delete(); - } - Map toArgs() { return { 'profile-name': profileName, @@ -179,61 +162,9 @@ class SSHNPParams { return lines.join('\n'); } - Future toFile({String? directory, bool overwrite = false}) async { - if (profileName == null || profileName!.isEmpty) { - throw Exception('profileName is null or empty'); - } - - var fileName = profileNameToConfigFileName(profileName!, directory: directory); - var file = File(fileName); - - var exists = await file.exists(); - - if (exists && !overwrite) { - throw Exception('Failed to write config file: ${file.path} already exists'); - } - - // FileMode.write will create the file if it does not exist - // and overwrite existing files if it does exist - return file.writeAsString(toConfig(), mode: FileMode.write); - } - String toJson() { return jsonEncode(toArgs()); } - - static Future fileExists(String profileName, {String? directory}) { - var fileName = profileNameToConfigFileName(profileName, directory: directory); - return File(fileName).exists(); - } - - static Future fromFile(String profileName, {String? directory}) async { - var fileName = profileNameToConfigFileName(profileName, directory: directory); - return SSHNPParams.fromConfig(fileName); - } - - static Future> listFiles({String? directory}) async { - var fileNames = {}; - - var homeDirectory = getHomeDirectory(throwIfNull: true)!; - directory ??= getDefaultSshnpConfigDirectory(homeDirectory); - var files = Directory(directory).list(); - - await files.forEach((file) { - if (file is! File) return; - if (path.extension(file.path) != '.env') return; - if (path.basenameWithoutExtension(file.path).isEmpty) return; // ignore '.env' file - empty profileName - fileNames.add(configFileNameToProfileName(file.path)); - try { - var p = SSHNPParams.fromConfig(file.path); - fileNames.add(p.profileName!); - } catch (e) { - stderr.writeln('Error reading config file: ${file.path}'); - stderr.writeln(e); - } - }); - return fileNames; - } } /// A class which contains a subset of the SSHNPParams @@ -241,7 +172,7 @@ class SSHNPParams { /// e.g. default values from a config file and the rest from the command line class SSHNPPartialParams { // Non param variables - static final ArgParser parser = _createArgParser(); + static final ArgParser parser = createArgParser(); /// Main Params final String? profileName; @@ -295,13 +226,13 @@ class SSHNPPartialParams { factory SSHNPPartialParams.fromArgs(List args) { var params = SSHNPPartialParams.empty(); - var parsedArgs = _createArgParser(withDefaults: false).parse(args); + var parsedArgs = createArgParser(withDefaults: false).parse(args); if (parsedArgs.wasParsed('config-file')) { var configFileName = parsedArgs['config-file'] as String; params = SSHNPPartialParams.merge( params, - SSHNPPartialParams.fromConfig(configFileName), + SSHNPPartialParams.fromConfigFile(configFileName), ); } @@ -317,8 +248,8 @@ class SSHNPPartialParams { ); } - factory SSHNPPartialParams.fromConfig(String fileName) { - var args = _parseConfigFile(fileName); + factory SSHNPPartialParams.fromConfigFile(String fileName) { + var args = parseConfigFile(fileName); args['profile-name'] = configFileNameToProfileName(fileName); return SSHNPPartialParams.fromMap(args); } @@ -372,112 +303,4 @@ class SSHNPPartialParams { legacyDaemon: params2.legacyDaemon ?? params1.legacyDaemon, ); } - - static ArgParser _createArgParser({ - bool withConfig = true, - bool withDefaults = true, - bool withListDevices = true, - }) { - var parser = ArgParser(); - // Basic arguments - for (SSHNPArg arg in SSHNPArg.args) { - switch (arg.format) { - case ArgFormat.option: - parser.addOption( - arg.name, - abbr: arg.abbr, - mandatory: arg.mandatory, - defaultsTo: withDefaults ? arg.defaultsTo?.toString() : null, - help: arg.help, - ); - break; - case ArgFormat.multiOption: - parser.addMultiOption( - arg.name, - abbr: arg.abbr, - defaultsTo: withDefaults ? arg.defaultsTo as List? : null, - help: arg.help, - ); - break; - case ArgFormat.flag: - parser.addFlag( - arg.name, - abbr: arg.abbr, - defaultsTo: withDefaults ? arg.defaultsTo as bool? : null, - help: arg.help, - ); - break; - } - } - if (withConfig) { - parser.addOption( - 'config-file', - help: 'Read args from a config file\nMandatory args are not required if already supplied in the config file', - ); - } - if (withListDevices) { - parser.addFlag( - 'list-devices', - aliases: ['ls'], - negatable: false, - help: 'List available devices', - ); - } - return parser; - } - - static Map _parseConfigFile(String fileName) { - Map args = {}; - - File file = File(fileName); - - if (!file.existsSync()) { - throw Exception('Config file does not exist: $fileName'); - } - try { - List lines = file.readAsLinesSync(); - - for (String line in lines) { - if (line.startsWith('#')) continue; - - var parts = line.split('='); - if (parts.length != 2) continue; - - var key = parts[0].trim(); - var value = parts[1].trim(); - - SSHNPArg arg = SSHNPArg.fromBashName(key); - if (arg.name.isEmpty) continue; - - switch (arg.format) { - case ArgFormat.flag: - if (value.toLowerCase() == 'true') { - args[arg.name] = true; - } - continue; - case ArgFormat.multiOption: - var values = value.split(','); - args.putIfAbsent(arg.name, () => []); - for (String val in values) { - if (val.isEmpty) continue; - args[arg.name].add(val); - } - continue; - case ArgFormat.option: - if (value.isEmpty) continue; - if (arg.type == ArgType.integer) { - args[arg.name] = int.tryParse(value); - } else { - args[arg.name] = value; - } - continue; - } - } - return args; - } on FileSystemException { - throw Exception('Error reading config file: $fileName'); - } catch (e) { - throw Exception('Error parsing config file: $fileName'); - } - } } diff --git a/packages/sshnoports/lib/sshnp/utils.dart b/packages/sshnoports/lib/sshnp/utils.dart index d29adc67f..5964cc506 100644 --- a/packages/sshnoports/lib/sshnp/utils.dart +++ b/packages/sshnoports/lib/sshnp/utils.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:sshnoports/common/utils.dart'; import 'package:at_utils/at_logger.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:path/path.dart' as path; Future cleanUpAfterReverseSsh(SSHNP sshnp) async { if (!sshnp.initialized) { @@ -84,13 +83,3 @@ bool useDirectSsh(bool legacyDaemon, String host) { } } } - -String configFileNameToProfileName(String fileName) => path.basenameWithoutExtension(fileName).replaceAll('_', ' '); - -String profileNameToConfigFileName(String profileName, {String? directory, bool replaceSpaces = true}) { - var fileName = profileName.replaceAll(' ', '_'); - return path.join( - directory ?? getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), - '$fileName.env', - ); -} diff --git a/packages/sshnp_gui/lib/src/controllers/config_controller.dart b/packages/sshnp_gui/lib/src/controllers/config_controller.dart index 9902afd71..ce1e5d6c8 100644 --- a/packages/sshnp_gui/lib/src/controllers/config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/config_controller.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnoports/sshnp/config_manager.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; enum ConfigFileWriteState { create, update } @@ -12,12 +13,17 @@ final currentConfigController = AutoDisposeNotifierProvider>( +final configListController = AutoDisposeAsyncNotifierProvider>( ConfigListController.new, ); +final configManagerProvider = Provider((ref) { + return ConfigManager(AtClientManager.getInstance().atClient); +}); + /// A provider that exposes the [ConfigFamilyController] to the app. -final configFamilyController = AutoDisposeAsyncNotifierProviderFamily( +final configFamilyController = + AutoDisposeAsyncNotifierProviderFamily( ConfigFamilyController.new, ); @@ -45,10 +51,11 @@ class CurrentConfigController extends AutoDisposeNotifier { } /// Controller for the list of all profileNames for each config file -class ConfigListController extends AutoDisposeAsyncNotifier> { +class ConfigListController extends AutoDisposeAsyncNotifier> { @override - Future> build() async { - return (await SSHNPParams.listFiles()).toSet(); + Future> build() async { + await ref.read(configManagerProvider).initialized; + return (ref.read(configManagerProvider).managers.keys).toSet(); } Future refresh() async { @@ -61,41 +68,28 @@ class ConfigListController extends AutoDisposeAsyncNotifier> { } void remove(String profileName) { - state = AsyncData(state.value?.difference({profileName}) ?? {}); + state = AsyncData(state.value?.where((e) => e != profileName) ?? []); } } /// Controller for the family of [SSHNPParams] controllers -class ConfigFamilyController extends AutoDisposeFamilyAsyncNotifier { +class ConfigFamilyController extends AutoDisposeFamilyAsyncNotifier { @override - Future build(String arg) async { - return (await SSHNPParams.fileExists(arg)) - ? await SSHNPParams.fromFile(arg) - : SSHNPParams.merge( - SSHNPParams.empty(), - SSHNPPartialParams(clientAtSign: AtClientManager.getInstance().atClient.getCurrentAtSign()!), - ); - } - - Future refresh(String arg) async { - state = const AsyncLoading(); - state = await AsyncValue.guard(() => build(arg)); + Future build(String arg) { + return ref.read(configManagerProvider)[arg]; } - Future create(SSHNPParams params) async { - await params.toFile(); - state = AsyncValue.data(params); + Future createConfig(SSHNPParams params) async { + await state.value?.create(params); ref.read(configListController.notifier).add(params.profileName!); } - Future edit(SSHNPParams params) async { - await params.toFile(overwrite: true); - state = AsyncValue.data(params); + Future updateConfig(SSHNPParams params) async { + await state.value?.update(params); } - Future delete() async { - await state.value?.deleteFile(); - state = const AsyncError('File deleted', StackTrace.empty); - ref.read(configListController.notifier).remove(state.value!.profileName!); + Future deleteConfig() async { + await state.value?.delete(); + ref.read(configListController.notifier).remove(state.value!.profileName); } } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart index 47ef9e7a1..d52554d30 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart @@ -69,7 +69,7 @@ class DeleteAlertDialog extends ConsumerWidget { ), ElevatedButton( onPressed: () async { - await ref.read(configFamilyController(sshnpParams.profileName!).notifier).delete(); + await ref.read(configFamilyController(sshnpParams.profileName!).notifier).deleteConfig(); if (context.mounted) Navigator.of(context).pop(); }, style: Theme.of(context).elevatedButtonTheme.style!.copyWith( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart index 0e4266cc8..aae70be99 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart @@ -19,7 +19,7 @@ class _ProfileBarState extends ConsumerState { return controller.when( error: (error, stackTrace) => Container(), loading: () => const LinearProgressIndicator(), - data: (params) => Container( + data: (manager) => Container( decoration: BoxDecoration( border: Border( top: BorderSide( @@ -30,9 +30,9 @@ class _ProfileBarState extends ConsumerState { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(params.profileName ?? ''), + Text(manager.profileName), const ProfileBarStats(), - ProfileBarActions(params), + ProfileBarActions(manager.params), ], ), ), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index d14867262..b68658c47 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -40,14 +40,14 @@ class _ProfileFormState extends ConsumerState { SSHNPParams config = SSHNPParams.merge(oldConfig, newConfig); if (rename) { // delete old config file and write the new one - await ref.read(configFamilyController(oldConfig.profileName!).notifier).delete(); - await controller.create(config); + await ref.read(configFamilyController(oldConfig.profileName!).notifier).deleteConfig(); + await controller.createConfig(config); } else if (overwrite) { // overwrite the existing file - await controller.edit(config); + await controller.updateConfig(config); } else { // create new config file - await controller.create(config); + await controller.createConfig(config); } if (context.mounted) { ref.read(navigationRailController.notifier).setRoute(AppRoute.home); @@ -75,7 +75,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ CustomTextFormField( - initialValue: oldConfig.profileName ?? '', + initialValue: oldConfig.profileName, labelText: strings.profileName, onChanged: (value) { newConfig = SSHNPPartialParams.merge( @@ -87,7 +87,7 @@ class _ProfileFormState extends ConsumerState { ), gapW8, CustomTextFormField( - initialValue: oldConfig.device, + initialValue: oldConfig.params.device, labelText: strings.device, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -100,7 +100,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ CustomTextFormField( - initialValue: oldConfig.sshnpdAtSign ?? '', + initialValue: oldConfig.params.sshnpdAtSign ?? '', labelText: strings.sshnpdAtSign, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -110,7 +110,7 @@ class _ProfileFormState extends ConsumerState { ), gapW8, CustomTextFormField( - initialValue: oldConfig.host ?? '', + initialValue: oldConfig.params.host ?? '', labelText: strings.host, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -124,7 +124,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ CustomTextFormField( - initialValue: oldConfig.sendSshPublicKey, + initialValue: oldConfig.params.sendSshPublicKey, labelText: strings.sendSshPublicKey, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -138,7 +138,7 @@ class _ProfileFormState extends ConsumerState { Text(strings.rsa), gapW8, Switch( - value: newConfig.rsa ?? oldConfig.rsa, + value: newConfig.rsa ?? oldConfig.params.rsa, onChanged: (newValue) { setState(() { newConfig = SSHNPPartialParams.merge( @@ -155,7 +155,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ CustomTextFormField( - initialValue: oldConfig.remoteUsername ?? '', + initialValue: oldConfig.params.remoteUsername ?? '', labelText: strings.remoteUserName, onChanged: (value) { newConfig = SSHNPPartialParams.merge( @@ -165,7 +165,7 @@ class _ProfileFormState extends ConsumerState { }), gapW8, CustomTextFormField( - initialValue: oldConfig.port.toString(), + initialValue: oldConfig.params.port.toString(), labelText: strings.port, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -179,7 +179,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ CustomTextFormField( - initialValue: oldConfig.localPort.toString(), + initialValue: oldConfig.params.localPort.toString(), labelText: strings.localPort, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -188,7 +188,7 @@ class _ProfileFormState extends ConsumerState { ), gapW8, CustomTextFormField( - initialValue: oldConfig.localSshdPort.toString(), + initialValue: oldConfig.params.localSshdPort.toString(), labelText: strings.localSshdPort, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -199,7 +199,7 @@ class _ProfileFormState extends ConsumerState { ), gapH10, CustomTextFormField( - initialValue: oldConfig.localSshOptions.join(','), + initialValue: oldConfig.params.localSshOptions.join(','), hintText: strings.localSshOptionsHint, labelText: strings.localSshOptions, width: 192 * 2 + 10, @@ -212,7 +212,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ CustomTextFormField( - initialValue: oldConfig.atKeysFilePath, + initialValue: oldConfig.params.atKeysFilePath, labelText: strings.atKeysFilePath, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -221,7 +221,7 @@ class _ProfileFormState extends ConsumerState { ), gapW8, CustomTextFormField( - initialValue: oldConfig.rootDomain, + initialValue: oldConfig.params.rootDomain, labelText: strings.rootDomain, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -236,7 +236,7 @@ class _ProfileFormState extends ConsumerState { Text(strings.verbose), gapW8, Switch( - value: newConfig.verbose ?? oldConfig.verbose, + value: newConfig.verbose ?? oldConfig.params.verbose, onChanged: (newValue) { setState(() { newConfig = SSHNPPartialParams.merge( @@ -250,7 +250,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ ElevatedButton( - onPressed: () => onSubmit(oldConfig, newConfig), + onPressed: () => onSubmit(oldConfig.params, newConfig), child: Text(strings.submit), ), gapW8, diff --git a/packages/sshnp_gui/macos/Podfile.lock b/packages/sshnp_gui/macos/Podfile.lock index b1eda3d11..2d6c4cf07 100644 --- a/packages/sshnp_gui/macos/Podfile.lock +++ b/packages/sshnp_gui/macos/Podfile.lock @@ -87,4 +87,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.12.1 +COCOAPODS: 1.11.3 From 29bee9c00a35aa78d7b0dd6ec58e47d4607abe0f Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 12 Sep 2023 18:49:01 +0800 Subject: [PATCH 62/95] chore: revert configFamilyController --- .../src/controllers/config_controller.dart | 24 ++++++---------- .../widgets/profile_bar/profile_bar.dart | 6 ++-- .../widgets/profile_form/profile_form.dart | 28 +++++++++---------- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/packages/sshnp_gui/lib/src/controllers/config_controller.dart b/packages/sshnp_gui/lib/src/controllers/config_controller.dart index ce1e5d6c8..5693f90f4 100644 --- a/packages/sshnp_gui/lib/src/controllers/config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/config_controller.dart @@ -17,13 +17,8 @@ final configListController = AutoDisposeAsyncNotifierProvider((ref) { - return ConfigManager(AtClientManager.getInstance().atClient); -}); - /// A provider that exposes the [ConfigFamilyController] to the app. -final configFamilyController = - AutoDisposeAsyncNotifierProviderFamily( +final configFamilyController = AutoDisposeAsyncNotifierProviderFamily( ConfigFamilyController.new, ); @@ -54,8 +49,7 @@ class CurrentConfigController extends AutoDisposeNotifier { class ConfigListController extends AutoDisposeAsyncNotifier> { @override Future> build() async { - await ref.read(configManagerProvider).initialized; - return (ref.read(configManagerProvider).managers.keys).toSet(); + return []; // TODO } Future refresh() async { @@ -73,23 +67,23 @@ class ConfigListController extends AutoDisposeAsyncNotifier> { } /// Controller for the family of [SSHNPParams] controllers -class ConfigFamilyController extends AutoDisposeFamilyAsyncNotifier { +class ConfigFamilyController extends AutoDisposeFamilyAsyncNotifier { @override - Future build(String arg) { - return ref.read(configManagerProvider)[arg]; + Future build(String arg) async { + return SSHNPParams.empty(); // TODO } Future createConfig(SSHNPParams params) async { - await state.value?.create(params); + // TODO ref.read(configListController.notifier).add(params.profileName!); } Future updateConfig(SSHNPParams params) async { - await state.value?.update(params); + // TODO } Future deleteConfig() async { - await state.value?.delete(); - ref.read(configListController.notifier).remove(state.value!.profileName); + // TODO + ref.read(configListController.notifier).remove(state.value!.profileName!); } } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart index aae70be99..e479ec1bc 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart @@ -19,7 +19,7 @@ class _ProfileBarState extends ConsumerState { return controller.when( error: (error, stackTrace) => Container(), loading: () => const LinearProgressIndicator(), - data: (manager) => Container( + data: (profile) => Container( decoration: BoxDecoration( border: Border( top: BorderSide( @@ -30,9 +30,9 @@ class _ProfileBarState extends ConsumerState { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(manager.profileName), + Text(profile.profileName!), const ProfileBarStats(), - ProfileBarActions(manager.params), + ProfileBarActions(profile), ], ), ), diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index b68658c47..6787d27b7 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -87,7 +87,7 @@ class _ProfileFormState extends ConsumerState { ), gapW8, CustomTextFormField( - initialValue: oldConfig.params.device, + initialValue: oldConfig.device, labelText: strings.device, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -100,7 +100,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ CustomTextFormField( - initialValue: oldConfig.params.sshnpdAtSign ?? '', + initialValue: oldConfig.sshnpdAtSign ?? '', labelText: strings.sshnpdAtSign, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -110,7 +110,7 @@ class _ProfileFormState extends ConsumerState { ), gapW8, CustomTextFormField( - initialValue: oldConfig.params.host ?? '', + initialValue: oldConfig.host ?? '', labelText: strings.host, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -124,7 +124,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ CustomTextFormField( - initialValue: oldConfig.params.sendSshPublicKey, + initialValue: oldConfig.sendSshPublicKey, labelText: strings.sendSshPublicKey, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -138,7 +138,7 @@ class _ProfileFormState extends ConsumerState { Text(strings.rsa), gapW8, Switch( - value: newConfig.rsa ?? oldConfig.params.rsa, + value: newConfig.rsa ?? oldConfig.rsa, onChanged: (newValue) { setState(() { newConfig = SSHNPPartialParams.merge( @@ -155,7 +155,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ CustomTextFormField( - initialValue: oldConfig.params.remoteUsername ?? '', + initialValue: oldConfig.remoteUsername ?? '', labelText: strings.remoteUserName, onChanged: (value) { newConfig = SSHNPPartialParams.merge( @@ -165,7 +165,7 @@ class _ProfileFormState extends ConsumerState { }), gapW8, CustomTextFormField( - initialValue: oldConfig.params.port.toString(), + initialValue: oldConfig.port.toString(), labelText: strings.port, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -179,7 +179,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ CustomTextFormField( - initialValue: oldConfig.params.localPort.toString(), + initialValue: oldConfig.localPort.toString(), labelText: strings.localPort, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -188,7 +188,7 @@ class _ProfileFormState extends ConsumerState { ), gapW8, CustomTextFormField( - initialValue: oldConfig.params.localSshdPort.toString(), + initialValue: oldConfig.localSshdPort.toString(), labelText: strings.localSshdPort, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -199,7 +199,7 @@ class _ProfileFormState extends ConsumerState { ), gapH10, CustomTextFormField( - initialValue: oldConfig.params.localSshOptions.join(','), + initialValue: oldConfig.localSshOptions.join(','), hintText: strings.localSshOptionsHint, labelText: strings.localSshOptions, width: 192 * 2 + 10, @@ -212,7 +212,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ CustomTextFormField( - initialValue: oldConfig.params.atKeysFilePath, + initialValue: oldConfig.atKeysFilePath, labelText: strings.atKeysFilePath, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -221,7 +221,7 @@ class _ProfileFormState extends ConsumerState { ), gapW8, CustomTextFormField( - initialValue: oldConfig.params.rootDomain, + initialValue: oldConfig.rootDomain, labelText: strings.rootDomain, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, @@ -236,7 +236,7 @@ class _ProfileFormState extends ConsumerState { Text(strings.verbose), gapW8, Switch( - value: newConfig.verbose ?? oldConfig.params.verbose, + value: newConfig.verbose ?? oldConfig.verbose, onChanged: (newValue) { setState(() { newConfig = SSHNPPartialParams.merge( @@ -250,7 +250,7 @@ class _ProfileFormState extends ConsumerState { Row( children: [ ElevatedButton( - onPressed: () => onSubmit(oldConfig.params, newConfig), + onPressed: () => onSubmit(oldConfig, newConfig), child: Text(strings.submit), ), gapW8, From 4a1982061d6c183d02b4b7cab630429536b50c83 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 12 Sep 2023 19:20:47 +0800 Subject: [PATCH 63/95] feat: add config_repos to sshnoports core --- .../lib/sshnp/config_file_utils.dart | 172 ------------------ .../sshnoports/lib/sshnp/config_manager.dart | 159 ---------------- .../config_file_repository.dart | 151 +++++++++++++++ .../config_key_repository.dart | 48 +++++ .../config_source/config_file_source.dart | 55 ------ .../sshnp/config_source/config_source.dart | 25 --- .../config_source/config_sync_source.dart | 59 ------ packages/sshnoports/lib/sshnp/sshnp.dart | 2 +- .../sshnoports/lib/sshnp/sshnp_params.dart | 13 +- .../src/controllers/config_controller.dart | 19 +- .../widgets/profile_form/profile_form.dart | 8 +- 11 files changed, 217 insertions(+), 494 deletions(-) delete mode 100644 packages/sshnoports/lib/sshnp/config_file_utils.dart delete mode 100644 packages/sshnoports/lib/sshnp/config_manager.dart create mode 100644 packages/sshnoports/lib/sshnp/config_repository/config_file_repository.dart create mode 100644 packages/sshnoports/lib/sshnp/config_repository/config_key_repository.dart delete mode 100644 packages/sshnoports/lib/sshnp/config_source/config_file_source.dart delete mode 100644 packages/sshnoports/lib/sshnp/config_source/config_source.dart delete mode 100644 packages/sshnoports/lib/sshnp/config_source/config_sync_source.dart diff --git a/packages/sshnoports/lib/sshnp/config_file_utils.dart b/packages/sshnoports/lib/sshnp/config_file_utils.dart deleted file mode 100644 index aa9be83bc..000000000 --- a/packages/sshnoports/lib/sshnp/config_file_utils.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'dart:io'; - -import 'package:at_client/at_client.dart'; -import 'package:sshnoports/common/utils.dart'; -import 'package:sshnoports/sshnp/config_manager.dart'; -import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnoports/sshnp/sshnp_arg.dart'; -import 'package:path/path.dart' as path; - -String configFileNameToProfileName(String fileName, {bool replaceSpaces = true}) { - var profileName = path.basenameWithoutExtension(fileName); - if (replaceSpaces) profileName = profileName.replaceAll('_', ' '); - return profileName; -} - -String profileNameToConfigFileName(String profileName, {String? directory, bool replaceSpaces = true}) { - var fileName = profileName; - if (replaceSpaces) fileName = fileName.replaceAll(' ', '_'); - return path.join( - directory ?? getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), - '$fileName.env', - ); -} - -const String _keyPrefix = 'profile_'; - -String atKeyToProfileName(AtKey atKey, {bool replaceSpaces = true}) { - var profileName = atKey.key!.split('.').first; - print('r1: $profileName'); - profileName = profileName.replaceFirst(_keyPrefix, ''); - print('r2: $profileName'); - if (replaceSpaces) profileName = profileName.replaceAll('_', ' '); - print('r3: $profileName'); - return profileName; -} - -AtKey profileNameToAtKey(String profileName, {String sharedBy = '', bool replaceSpaces = true}) { - if (replaceSpaces) profileName = profileName.replaceAll(' ', '_'); - return AtKey.self( - '$_keyPrefix$profileName', - namespace: ConfigManager.namespace, - sharedBy: sharedBy, - ).build(); -} - -Future createConfigDirectory({String? directory}) async { - directory ??= getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!); - var dir = Directory(directory); - if (!await dir.exists()) { - await dir.create(recursive: true); - } - return dir; -} - -Future> listProfilesFromDirectory({String? directory}) async { - var profileNames = {}; - - var homeDirectory = getHomeDirectory(throwIfNull: true)!; - directory ??= getDefaultSshnpConfigDirectory(homeDirectory); - var files = Directory(directory).list(); - - await files.forEach((file) { - if (file is! File) return; - if (path.extension(file.path) != '.env') return; - if (path.basenameWithoutExtension(file.path).isEmpty) return; // ignore '.env' file - empty profileName - profileNames.add(configFileNameToProfileName(file.path)); - }); - return profileNames; -} - -Map parseConfigFile(String fileName) { - Map args = {}; - - if (path.normalize(fileName).contains('/') || path.normalize(fileName).contains(r'\')) { - fileName = path.normalize(path.absolute(fileName)); - } else { - fileName = - path.normalize(path.absolute(getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), fileName)); - } - - File file = File(fileName); - - if (!file.existsSync()) { - throw Exception('Config file does not exist: $fileName'); - } - try { - List lines = file.readAsLinesSync(); - - for (String line in lines) { - if (line.startsWith('#')) continue; - - var parts = line.split('='); - if (parts.length != 2) continue; - - var key = parts[0].trim(); - var value = parts[1].trim(); - - SSHNPArg arg = SSHNPArg.fromBashName(key); - if (arg.name.isEmpty) continue; - - switch (arg.format) { - case ArgFormat.flag: - if (value.toLowerCase() == 'true') { - args[arg.name] = true; - } - continue; - case ArgFormat.multiOption: - var values = value.split(','); - args.putIfAbsent(arg.name, () => []); - for (String val in values) { - if (val.isEmpty) continue; - args[arg.name].add(val); - } - continue; - case ArgFormat.option: - if (value.isEmpty) continue; - if (arg.type == ArgType.integer) { - args[arg.name] = int.tryParse(value); - } else { - args[arg.name] = value; - } - continue; - } - } - return args; - } on FileSystemException { - throw Exception('Error reading config file: $fileName'); - } catch (e) { - throw Exception('Error parsing config file: $fileName'); - } -} - -Future sshnpParamsFromFile(String profileName, {String? directory}) async { - var fileName = profileNameToConfigFileName(profileName, directory: directory); - return SSHNPParams.fromConfigFile(fileName); -} - -Future sshnpParamsToFile(SSHNPParams params, {String? directory, bool overwrite = false}) async { - if (params.profileName == null || params.profileName!.isEmpty) { - throw Exception('profileName is null or empty'); - } - - var fileName = profileNameToConfigFileName(params.profileName!, directory: directory); - var file = File(fileName); - - var exists = await file.exists(); - - if (exists && !overwrite) { - throw Exception('Failed to write config file: ${file.path} already exists'); - } - - // FileMode.write will create the file if it does not exist - // and overwrite existing files if it does exist - return file.writeAsString(params.toConfig(), mode: FileMode.write); -} - -Future deleteFile(SSHNPParams params, {String? directory}) async { - if (params.profileName == null || params.profileName!.isEmpty) { - throw Exception('profileName is null or empty'); - } - - var fileName = profileNameToConfigFileName(params.profileName!, directory: directory); - var file = File(fileName); - - var exists = await file.exists(); - - if (!exists) { - throw Exception('Cannot delete ${file.path}, file does not exist'); - } - - return file.delete(); -} diff --git a/packages/sshnoports/lib/sshnp/config_manager.dart b/packages/sshnoports/lib/sshnp/config_manager.dart deleted file mode 100644 index 66d8af432..000000000 --- a/packages/sshnoports/lib/sshnp/config_manager.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; - -import 'package:at_client/at_client.dart'; -import 'package:sshnoports/sshnp/config_file_utils.dart'; -import 'package:sshnoports/sshnp/config_source/config_source.dart'; -import 'package:sshnoports/sshnp/sshnp.dart'; -import 'package:sshnoports/sshnpd/sshnpd.dart'; - -class ConfigManager { - static const namespace = 'profiles.${SSHNPD.namespace}'; - - final Map _configFamilyManagers = {}; - Map get managers => _configFamilyManagers; - - final Completer _initialized = Completer(); - Future get initialized => _initialized.future; - - final AtClient _atClient; - - ConfigManager(this._atClient, {bool useRemoteConfig = true, bool useLocalConfig = true}) { - _init(useRemoteConfig: useRemoteConfig, useLocalConfig: useLocalConfig); - } - - Future _init({bool useRemoteConfig = true, bool useLocalConfig = true}) async { - await Future.wait([ - if (useLocalConfig) _loadFiles(), - if (useRemoteConfig) _loadKeys(), - ]); - print('loaded config'); - await Future.wait(managers.values.map((manager) { - return Future.wait([ - manager._init(), - // manager.syncToRemote(_atClient), - // manager.syncToLocal(), - // TOdo sync based on which is latest / more correct - ]); - })); - print('synced config'); - _initialized.complete(true); - } - - Future _loadFiles() async { - await createConfigDirectory(); - var profiles = await listProfilesFromDirectory(); - for (var fileName in profiles) { - var profileName = configFileNameToProfileName(fileName); - if (!_configFamilyManagers.containsKey(profileName)) { - _configFamilyManagers[profileName] = ConfigFamilyManager(profileName); - } - _configFamilyManagers[profileName]!.sources.add(ConfigSource.file(profileName)); - } - } - - Future _loadKeys() async { - var keys = await _atClient.getAtKeys(regex: namespace); - for (var atKey in keys) { - var profileName = atKeyToProfileName(atKey); - print('load: $atKey, $profileName'); - if (!_configFamilyManagers.containsKey(profileName)) { - _configFamilyManagers[profileName] = ConfigFamilyManager(profileName); - } - _configFamilyManagers[profileName]!.sources.add(ConfigSource.sync(profileName, _atClient)); - } - } - - Future operator [](String key) async { - _configFamilyManagers[key] ??= ConfigFamilyManager(key); - await _configFamilyManagers[key]!._init(); - return _configFamilyManagers[key]!; - } -} - -class ConfigFamilyManager { - final String profileName; - final List sources = []; - - ConfigFamilyManager(this.profileName); - - final Completer initialized = Completer(); - bool _initCalled = false; - - late SSHNPParams _params; - SSHNPParams get params => _params; - - Future _init() async { - print('called init for $profileName'); - if (_initCalled) return; - _initCalled = true; - - await Future.wait(sources.map((s) => s.read())); - print('done read for $profileName'); - var latestModified = DateTime(0); - - ConfigSource? latestSource; - for (var source in sources) { - var modified = await source.getLastModified(refresh: false); - if (modified.isAfter(latestModified)) { - latestModified = modified; - latestSource = source; - } - } - print('done getLastestSource for $profileName'); - - _params = latestSource?.params ?? SSHNPParams.empty(); - initialized.complete(true); - } - - Future create(SSHNPParams params) async { - await initialized.future; - _params = params; - await Future.wait(sources.map((s) => s.create(params))); - } - - Future update(SSHNPParams params) async { - await initialized.future; - _params = params; - await Future.wait(sources.map((s) => s.update(params))); - } - - Future delete() async { - if (!initialized.isCompleted) { - await _init(); - } - await Future.wait(sources.map((s) => s.delete())); - } - - Future syncToRemote(AtClient atClient) async { - print('starting sync'); - await initialized.future; - print('init sync local'); - var remotes = sources.whereType(); - if (remotes.isNotEmpty) { - await Future.wait(remotes.map((s) => s.update(params))); - print('done sync refresh'); - return; - } - var source = ConfigSource.sync(profileName, atClient); - await source.create(params); - print('done sync'); - sources.add(source); - } - - Future syncToLocal() async { - print('starting sync local'); - await initialized.future; - print('init sync local'); - var locals = sources.whereType(); - if (locals.isNotEmpty) { - await Future.wait(locals.map((s) => s.update(params))); - print('done sync refresh local'); - return; - } - print('comp: $profileName, ${params.toJson()}'); - var source = ConfigSource.file(profileName); - await source.create(params); - print('done sync local'); - sources.add(source); - } -} diff --git a/packages/sshnoports/lib/sshnp/config_repository/config_file_repository.dart b/packages/sshnoports/lib/sshnp/config_repository/config_file_repository.dart new file mode 100644 index 000000000..1dfa43b54 --- /dev/null +++ b/packages/sshnoports/lib/sshnp/config_repository/config_file_repository.dart @@ -0,0 +1,151 @@ +import 'dart:io'; + +import 'package:sshnoports/common/utils.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnoports/sshnp/sshnp_arg.dart'; +import 'package:path/path.dart' as path; + +class ConfigFileRepository { + static String toProfileName(String fileName, {bool replaceSpaces = true}) { + var profileName = path.basenameWithoutExtension(fileName); + if (replaceSpaces) profileName = profileName.replaceAll('_', ' '); + return profileName; + } + + static String fromProfileName(String profileName, {String? directory, bool replaceSpaces = true}) { + var fileName = profileName; + if (replaceSpaces) fileName = fileName.replaceAll(' ', '_'); + return path.join( + directory ?? getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), + '$fileName.env', + ); + } + + static Future createConfigDirectory({String? directory}) async { + directory ??= getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!); + var dir = Directory(directory); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + return dir; + } + + static Future> listProfiles({String? directory}) async { + var profileNames = {}; + + var homeDirectory = getHomeDirectory(throwIfNull: true)!; + directory ??= getDefaultSshnpConfigDirectory(homeDirectory); + var files = Directory(directory).list(); + + await files.forEach((file) { + if (file is! File) return; + if (path.extension(file.path) != '.env') return; + if (path.basenameWithoutExtension(file.path).isEmpty) return; // ignore '.env' file - empty profileName + profileNames.add(toProfileName(file.path)); + }); + return profileNames; + } + + static Future getParams(String profileName, {String? directory}) async { + var fileName = fromProfileName(profileName, directory: directory); + return SSHNPParams.fromFile(fileName); + } + + static Future putParams(SSHNPParams params, {String? directory, bool overwrite = false}) async { + if (params.profileName == null || params.profileName!.isEmpty) { + throw Exception('profileName is null or empty'); + } + + var fileName = fromProfileName(params.profileName!, directory: directory); + var file = File(fileName); + + var exists = await file.exists(); + + if (exists && !overwrite) { + throw Exception('Failed to write config file: ${file.path} already exists'); + } + + // FileMode.write will create the file if it does not exist + // and overwrite existing files if it does exist + return file.writeAsString(params.toConfig(), mode: FileMode.write); + } + + static Future deleteParams(SSHNPParams params, {String? directory}) async { + if (params.profileName == null || params.profileName!.isEmpty) { + throw Exception('profileName is null or empty'); + } + + var fileName = fromProfileName(params.profileName!, directory: directory); + var file = File(fileName); + + var exists = await file.exists(); + + if (!exists) { + throw Exception('Cannot delete ${file.path}, file does not exist'); + } + + return file.delete(); + } + + static Map parseConfigFile(String fileName) { + Map args = {}; + + if (path.normalize(fileName).contains('/') || path.normalize(fileName).contains(r'\')) { + fileName = path.normalize(path.absolute(fileName)); + } else { + fileName = + path.normalize(path.absolute(getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), fileName)); + } + + File file = File(fileName); + + if (!file.existsSync()) { + throw Exception('Config file does not exist: $fileName'); + } + try { + List lines = file.readAsLinesSync(); + + for (String line in lines) { + if (line.startsWith('#')) continue; + + var parts = line.split('='); + if (parts.length != 2) continue; + + var key = parts[0].trim(); + var value = parts[1].trim(); + + SSHNPArg arg = SSHNPArg.fromBashName(key); + if (arg.name.isEmpty) continue; + + switch (arg.format) { + case ArgFormat.flag: + if (value.toLowerCase() == 'true') { + args[arg.name] = true; + } + continue; + case ArgFormat.multiOption: + var values = value.split(','); + args.putIfAbsent(arg.name, () => []); + for (String val in values) { + if (val.isEmpty) continue; + args[arg.name].add(val); + } + continue; + case ArgFormat.option: + if (value.isEmpty) continue; + if (arg.type == ArgType.integer) { + args[arg.name] = int.tryParse(value); + } else { + args[arg.name] = value; + } + continue; + } + } + return args; + } on FileSystemException { + throw Exception('Error reading config file: $fileName'); + } catch (e) { + throw Exception('Error parsing config file: $fileName'); + } + } +} diff --git a/packages/sshnoports/lib/sshnp/config_repository/config_key_repository.dart b/packages/sshnoports/lib/sshnp/config_repository/config_key_repository.dart new file mode 100644 index 000000000..8c5cc53df --- /dev/null +++ b/packages/sshnoports/lib/sshnp/config_repository/config_key_repository.dart @@ -0,0 +1,48 @@ +import 'package:at_client/at_client.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnoports/sshnpd/sshnpd.dart'; + +class ConfigKeyRepository { + static const String _keyPrefix = 'profile_'; + static const String _configNamespace = 'profiles.${SSHNPD.namespace}'; + + static String toProfileName(AtKey atKey, {bool replaceSpaces = true}) { + var profileName = atKey.key!.split('.').first; + profileName = profileName.replaceFirst(_keyPrefix, ''); + if (replaceSpaces) profileName = profileName.replaceAll('_', ' '); + return profileName; + } + + static AtKey fromProfileName(String profileName, {String sharedBy = '', bool replaceSpaces = true}) { + if (replaceSpaces) profileName = profileName.replaceAll(' ', '_'); + return AtKey.self( + '$_keyPrefix$profileName', + namespace: _configNamespace, + sharedBy: sharedBy, + ).build(); + } + + static Future> listProfiles(AtClient atClient) async { + var keys = await atClient.getAtKeys(regex: namespace); + return keys.map((e) => toProfileName(e)); + } + + static Future getParams(String profileName, + {required AtClient atClient, GetRequestOptions? options}) async { + AtKey key = fromProfileName(profileName); + AtValue value = await atClient.get(key, getRequestOptions: options); + if (value.value == null) return SSHNPParams.empty(); + return SSHNPParams.fromJson(value.value!); + } + + static Future putParams(SSHNPParams params, {required AtClient atClient, PutRequestOptions? options}) async { + AtKey key = fromProfileName(params.profileName!); + await atClient.put(key, params.toJson(), putRequestOptions: options); + } + + static Future deleteParams(SSHNPParams params, + {required AtClient atClient, DeleteRequestOptions? options}) async { + AtKey key = fromProfileName(params.profileName!); + await atClient.delete(key, deleteRequestOptions: options); + } +} diff --git a/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart b/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart deleted file mode 100644 index 280b3fca7..000000000 --- a/packages/sshnoports/lib/sshnp/config_source/config_file_source.dart +++ /dev/null @@ -1,55 +0,0 @@ -part of 'config_source.dart'; - -/// [ConfigSource] covariant from a [File] -class ConfigFileSource implements ConfigSource { - late final String profileName; - late final String? directory; - late final String? fileName; - late final File file; - - SSHNPParams? _params; - - @override - SSHNPParams get params => _params ?? SSHNPParams.empty(); - - ConfigFileSource(this.profileName, {this.directory, this.fileName}) - : file = File( - profileNameToConfigFileName( - fileName ?? profileName, - directory: directory, - replaceSpaces: (fileName == null), // only replace spaces for [profileName] not [fileName] - ), - ); - - @override - DateTime getLastModified({bool refresh = true}) => file.lastModifiedSync(); - - @override - Future create(SSHNPParams params) async { - if (params.profileName != profileName) { - throw ArgumentError.value(params.profileName, 'params.profileName', 'must be $profileName'); - } - await sshnpParamsToFile(params, directory: directory); - } - - @override - Future read() async { - try { - var params = SSHNPParams.fromConfigFile(file.path); - _params = params; - } catch (e) { - _params = null; - } - return params; - } - - @override - Future update(SSHNPParams params) async { - await sshnpParamsToFile(params, directory: directory, overwrite: true); - } - - @override - Future delete() async { - await file.delete(); - } -} diff --git a/packages/sshnoports/lib/sshnp/config_source/config_source.dart b/packages/sshnoports/lib/sshnp/config_source/config_source.dart deleted file mode 100644 index 09d918b28..000000000 --- a/packages/sshnoports/lib/sshnp/config_source/config_source.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:at_client/at_client.dart'; -import 'package:sshnoports/sshnp/config_file_utils.dart'; -import 'package:sshnoports/sshnp/sshnp.dart'; - -part 'config_sync_source.dart'; -part 'config_file_source.dart'; - -/// Generic Type -abstract class ConfigSource { - SSHNPParams get params; - - FutureOr getLastModified({bool refresh = true}); - Future create(SSHNPParams params); - Future read(); - Future update(SSHNPParams params); - Future delete(); - - factory ConfigSource.file(String profileName, {String? directory, String? fileName}) => - ConfigFileSource(profileName, directory: directory, fileName: fileName); - - factory ConfigSource.sync(String profileName, AtClient atClient) => ConfigSyncSource(profileName, atClient); -} diff --git a/packages/sshnoports/lib/sshnp/config_source/config_sync_source.dart b/packages/sshnoports/lib/sshnp/config_source/config_sync_source.dart deleted file mode 100644 index fe003dc87..000000000 --- a/packages/sshnoports/lib/sshnp/config_source/config_sync_source.dart +++ /dev/null @@ -1,59 +0,0 @@ -part of 'config_source.dart'; - -/// [ConfigSource] covariant from an [AtKey] -class ConfigSyncSource implements ConfigSource { - late final AtKey atKey; - late final AtClient atClient; - - SSHNPParams? _params; - - @override - SSHNPParams get params => _params ?? SSHNPParams.empty(); - - ConfigSyncSource._(this.atKey, this.atClient); - - factory ConfigSyncSource(String profileName, AtClient atClient) { - AtKey atKey = profileNameToAtKey(profileName, sharedBy: atClient.getCurrentAtSign()!); - print('sync src: $profileName, $atKey'); - return ConfigSyncSource._(atKey, atClient); - } - - void _updateTimestamp() { - atKey.metadata = Metadata()..updatedAt = DateTime.now(); - } - - @override - Future getLastModified({bool refresh = true}) async { - if (refresh) await atClient.get(atKey); - return atKey.metadata?.updatedAt ?? DateTime(0); - } - - @override - Future create(SSHNPParams params) => update(params); - - @override - Future read() async { - print('called read for $atKey'); - var atValue = await atClient.get(atKey); - print('done read'); - try { - print("json: ${atValue.value}"); - _params = SSHNPParams.fromJson(atValue.value!); - } catch (e) { - _params = SSHNPParams.empty(); - } - - return params; - } - - @override - Future update(SSHNPParams params) { - _updateTimestamp(); - return atClient.put(atKey, params.toJson()); - } - - @override - Future delete() { - return atClient.delete(atKey); - } -} diff --git a/packages/sshnoports/lib/sshnp/sshnp.dart b/packages/sshnoports/lib/sshnp/sshnp.dart index b529ef99c..094583096 100644 --- a/packages/sshnoports/lib/sshnp/sshnp.dart +++ b/packages/sshnoports/lib/sshnp/sshnp.dart @@ -13,7 +13,7 @@ import 'package:path/path.dart' as path; import 'package:sshnoports/common/create_at_client_cli.dart'; import 'package:sshnoports/common/supported_ssh_clients.dart'; import 'package:sshnoports/common/utils.dart'; -import 'package:sshnoports/sshnp/config_file_utils.dart'; +import 'package:sshnoports/sshnp/config_repository/config_file_repository.dart'; import 'package:sshnoports/sshnp/sshnp_arg.dart'; import 'package:sshnoports/sshnp/utils.dart'; import 'package:sshnoports/sshnpd/sshnpd.dart'; diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index fc56aa987..b8c8ca9ab 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -62,14 +62,15 @@ class SSHNPParams { factory SSHNPParams.empty() { return SSHNPParams( + profileName: '', clientAtSign: '', sshnpdAtSign: '', host: '', ); } - factory SSHNPParams.fromConfigFile(String fileName) { - return SSHNPParams.fromPartial(SSHNPPartialParams.fromConfigFile(fileName)); + factory SSHNPParams.fromFile(String fileName) { + return SSHNPParams.fromPartial(SSHNPPartialParams.fromFile(fileName)); } factory SSHNPParams.fromJson(String json) => SSHNPParams.fromPartial(SSHNPPartialParams.fromJson(json)); @@ -232,7 +233,7 @@ class SSHNPPartialParams { var configFileName = parsedArgs['config-file'] as String; params = SSHNPPartialParams.merge( params, - SSHNPPartialParams.fromConfigFile(configFileName), + SSHNPPartialParams.fromFile(configFileName), ); } @@ -248,9 +249,9 @@ class SSHNPPartialParams { ); } - factory SSHNPPartialParams.fromConfigFile(String fileName) { - var args = parseConfigFile(fileName); - args['profile-name'] = configFileNameToProfileName(fileName); + factory SSHNPPartialParams.fromFile(String fileName) { + var args = ConfigFileRepository.parseConfigFile(fileName); + args['profile-name'] = ConfigFileRepository.toProfileName(fileName); return SSHNPPartialParams.fromMap(args); } diff --git a/packages/sshnp_gui/lib/src/controllers/config_controller.dart b/packages/sshnp_gui/lib/src/controllers/config_controller.dart index 5693f90f4..5bb939ea4 100644 --- a/packages/sshnp_gui/lib/src/controllers/config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/config_controller.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; +import 'package:at_client_mobile/at_client_mobile.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sshnoports/sshnp/config_manager.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnoports/sshnp/config_repository/config_key_repository.dart'; enum ConfigFileWriteState { create, update } @@ -49,7 +49,8 @@ class CurrentConfigController extends AutoDisposeNotifier { class ConfigListController extends AutoDisposeAsyncNotifier> { @override Future> build() async { - return []; // TODO + AtClient atClient = AtClientManager.getInstance().atClient; + return ConfigKeyRepository.listProfiles(atClient); } Future refresh() async { @@ -70,20 +71,16 @@ class ConfigListController extends AutoDisposeAsyncNotifier> { class ConfigFamilyController extends AutoDisposeFamilyAsyncNotifier { @override Future build(String arg) async { - return SSHNPParams.empty(); // TODO + return ConfigKeyRepository.getParams(arg, atClient: AtClientManager.getInstance().atClient); } - Future createConfig(SSHNPParams params) async { - // TODO + Future putConfig(SSHNPParams params) async { + await ConfigKeyRepository.putParams(params, atClient: AtClientManager.getInstance().atClient); ref.read(configListController.notifier).add(params.profileName!); } - Future updateConfig(SSHNPParams params) async { - // TODO - } - Future deleteConfig() async { - // TODO + await ConfigKeyRepository.deleteParams(state.value!, atClient: AtClientManager.getInstance().atClient); ref.read(configListController.notifier).remove(state.value!.profileName!); } } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index 6787d27b7..f99062e3e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -31,7 +31,6 @@ class _ProfileFormState extends ConsumerState { if (_formkey.currentState!.validate()) { _formkey.currentState!.save(); final controller = ref.read(configFamilyController(newConfig.profileName ?? oldConfig.profileName!).notifier); - bool overwrite = currentProfile.configFileWriteState == ConfigFileWriteState.update; bool rename = newConfig.profileName.isNotNull && newConfig.profileName!.isNotEmpty && oldConfig.profileName.isNotNull && @@ -41,13 +40,10 @@ class _ProfileFormState extends ConsumerState { if (rename) { // delete old config file and write the new one await ref.read(configFamilyController(oldConfig.profileName!).notifier).deleteConfig(); - await controller.createConfig(config); - } else if (overwrite) { - // overwrite the existing file - await controller.updateConfig(config); + await controller.putConfig(config); } else { // create new config file - await controller.createConfig(config); + await controller.putConfig(config); } if (context.mounted) { ref.read(navigationRailController.notifier).setRoute(AppRoute.home); From 6ce836a303fcbfc57b209993d86181ea1c8c27e8 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 12 Sep 2023 20:07:38 +0800 Subject: [PATCH 64/95] feat: allow deletion of corrupted configs --- .../config_key_repository.dart | 9 +- packages/sshnoports/lib/sshnp/sshnp.dart | 3 +- packages/sshnoports/lib/sshnp/sshnp_arg.dart | 109 +++++++----- packages/sshnoports/lib/sshnp/sshnp_impl.dart | 1 + .../sshnoports/lib/sshnp/sshnp_params.dart | 166 ++++-------------- packages/sshnp_gui/lib/l10n/app_en.arb | 1 + .../src/controllers/config_controller.dart | 4 +- .../src/presentation/screens/home_screen.dart | 2 +- .../profile_delete_action.dart | 13 +- .../widgets/profile_bar/profile_bar.dart | 30 +++- .../profile_bar/profile_bar_actions.dart | 2 +- 11 files changed, 144 insertions(+), 196 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/config_repository/config_key_repository.dart b/packages/sshnoports/lib/sshnp/config_repository/config_key_repository.dart index 8c5cc53df..ba52b0066 100644 --- a/packages/sshnoports/lib/sshnp/config_repository/config_key_repository.dart +++ b/packages/sshnoports/lib/sshnp/config_repository/config_key_repository.dart @@ -23,13 +23,14 @@ class ConfigKeyRepository { } static Future> listProfiles(AtClient atClient) async { - var keys = await atClient.getAtKeys(regex: namespace); + var keys = await atClient.getAtKeys(regex: _configNamespace); return keys.map((e) => toProfileName(e)); } static Future getParams(String profileName, {required AtClient atClient, GetRequestOptions? options}) async { AtKey key = fromProfileName(profileName); + key.sharedBy = atClient.getCurrentAtSign()!; AtValue value = await atClient.get(key, getRequestOptions: options); if (value.value == null) return SSHNPParams.empty(); return SSHNPParams.fromJson(value.value!); @@ -37,12 +38,14 @@ class ConfigKeyRepository { static Future putParams(SSHNPParams params, {required AtClient atClient, PutRequestOptions? options}) async { AtKey key = fromProfileName(params.profileName!); + key.sharedBy = atClient.getCurrentAtSign()!; await atClient.put(key, params.toJson(), putRequestOptions: options); } - static Future deleteParams(SSHNPParams params, + static Future deleteParams(String profileName, {required AtClient atClient, DeleteRequestOptions? options}) async { - AtKey key = fromProfileName(params.profileName!); + AtKey key = fromProfileName(profileName); + key.sharedBy = atClient.getCurrentAtSign()!; await atClient.delete(key, deleteRequestOptions: options); } } diff --git a/packages/sshnoports/lib/sshnp/sshnp.dart b/packages/sshnoports/lib/sshnp/sshnp.dart index 597d7a2f3..5ea305fc7 100644 --- a/packages/sshnoports/lib/sshnp/sshnp.dart +++ b/packages/sshnoports/lib/sshnp/sshnp.dart @@ -269,6 +269,5 @@ abstract class SSHNP { /// Returns two Iterable: /// - Iterable of atSigns of sshnpd that responded /// - Iterable of atSigns of sshnpd that did not respond - Future<(Iterable, Iterable, Map)> - listDevices(); + Future<(Iterable, Iterable, Map)> listDevices(); } diff --git a/packages/sshnoports/lib/sshnp/sshnp_arg.dart b/packages/sshnoports/lib/sshnp/sshnp_arg.dart index 7fd61541a..6c566f9d8 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_arg.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_arg.dart @@ -25,6 +25,9 @@ class SSHNPArg { final dynamic defaultsTo; final ArgType type; final Iterable? allowed; + final bool commandLineOnly; + final List? aliases; + final bool negatable; const SSHNPArg({ required this.name, @@ -35,6 +38,9 @@ class SSHNPArg { this.defaultsTo, this.type = ArgType.string, this.allowed, + this.commandLineOnly = false, + this.aliases, + this.negatable = true, }); String get bashName => name.replaceAll('-', '_').toUpperCase(); @@ -58,87 +64,88 @@ class SSHNPArg { } static List args = [ - SSHNPArg( + const SSHNPArg( name: 'key-file', abbr: 'k', help: 'Sending atSign\'s atKeys file if not in ~/.atsign/keys/', ), - SSHNPArg( + const SSHNPArg( name: 'from', abbr: 'f', help: 'Sending (a.k.a. client) atSign', mandatory: true, ), - SSHNPArg( + const SSHNPArg( name: 'to', abbr: 't', help: 'Receiving device atSign', mandatory: true, ), - SSHNPArg( + const SSHNPArg( name: 'device', abbr: 'd', help: 'Receiving device name', defaultsTo: SSHNP.defaultDevice, ), - SSHNPArg( + const SSHNPArg( name: 'host', abbr: 'h', help: 'atSign of sshrvd daemon or FQDN/IP address to connect back to', mandatory: true, ), - SSHNPArg( + const SSHNPArg( name: 'port', abbr: 'p', help: 'TCP port to connect back to (only required if --host specified a FQDN/IP)', defaultsTo: SSHNP.defaultPort, type: ArgType.integer, ), - SSHNPArg( - name: 'local-port', - abbr: 'l', - help: 'Reverse ssh port to listen on, on your local machine, by sshnp default finds a spare port', - defaultsTo: SSHNP.defaultLocalPort, - type: ArgType.integer), - SSHNPArg( + const SSHNPArg( + name: 'local-port', + abbr: 'l', + help: 'Reverse ssh port to listen on, on your local machine, by sshnp default finds a spare port', + defaultsTo: SSHNP.defaultLocalPort, + type: ArgType.integer, + ), + const SSHNPArg( name: 'ssh-public-key', abbr: 's', help: 'Public key file from ~/.ssh to be appended to authorized_hosts on the remote device', defaultsTo: SSHNP.defaultSendSshPublicKey, ), - SSHNPArg( + const SSHNPArg( name: 'local-ssh-options', abbr: 'o', help: 'Add these commands to the local ssh command', format: ArgFormat.multiOption, ), - SSHNPArg( + const SSHNPArg( name: 'verbose', abbr: 'v', defaultsTo: defaultVerbose, help: 'More logging', format: ArgFormat.flag, ), - SSHNPArg( + const SSHNPArg( name: 'rsa', abbr: 'r', defaultsTo: defaultRsa, help: 'Use RSA 4096 keys rather than the default ED25519 keys', format: ArgFormat.flag, ), - SSHNPArg( + const SSHNPArg( name: 'remote-user-name', abbr: 'u', help: 'username to use in the ssh session on the remote host', ), - SSHNPArg( + const SSHNPArg( name: 'root-domain', help: 'atDirectory domain', defaultsTo: defaultRootDomain, mandatory: false, format: ArgFormat.option, ), - SSHNPArg( + const SSHNPArg( name: 'local-sshd-port', help: 'port on which sshd is listening locally on the client host', defaultsTo: defaultLocalSshdPort, @@ -147,13 +154,13 @@ class SSHNPArg { format: ArgFormat.option, type: ArgType.integer, ), - SSHNPArg( + const SSHNPArg( name: 'legacy-daemon', defaultsTo: SSHNP.defaultLegacyDaemon, help: 'Request is to a legacy (< 3.5.0) noports daemon', format: ArgFormat.flag, ), - SSHNPArg( + const SSHNPArg( name: 'remote-sshd-port', help: 'port on which sshd is listening locally on the device host', defaultsTo: defaultRemoteSshdPort, @@ -161,29 +168,44 @@ class SSHNPArg { format: ArgFormat.option, type: ArgType.integer, ), - SSHNPArg( + const SSHNPArg( name: 'idle-timeout', - help: - 'number of seconds after which inactive ssh connections will be closed', + help: 'number of seconds after which inactive ssh connections will be closed', defaultsTo: defaultIdleTimeout, mandatory: false, format: ArgFormat.option, type: ArgType.integer, + commandLineOnly: true, ), SSHNPArg( - name: 'ssh-client', - help: 'What to use for outbound ssh connections', - defaultsTo: SupportedSshClient.hostSsh.cliArg, - mandatory: false, - format: ArgFormat.option, - type: ArgType.string, - allowed: SupportedSshClient.values.map((c) => c.cliArg).toList()), - SSHNPArg( + name: 'ssh-client', + help: 'What to use for outbound ssh connections', + defaultsTo: SupportedSshClient.hostSsh.cliArg, + mandatory: false, + format: ArgFormat.option, + type: ArgType.string, + allowed: SupportedSshClient.values.map((c) => c.cliArg).toList(), + commandLineOnly: true, + ), + const SSHNPArg( name: 'add-forwards-to-tunnel', defaultsTo: false, help: 'When true, any local forwarding directives provided in' '--local-ssh-options will be added to the initial tunnel ssh request', format: ArgFormat.flag, + commandLineOnly: true, + ), + const SSHNPArg( + name: 'config-file', + help: 'Read args from a config file\nMandatory args are not required if already supplied in the config file', + commandLineOnly: true, + ), + const SSHNPArg( + name: 'list-devices', + aliases: ['ls'], + negatable: false, + help: 'List available devices', + commandLineOnly: true, ), ]; @@ -194,13 +216,15 @@ class SSHNPArg { } ArgParser createArgParser({ - bool withConfig = true, + bool isCommandLine = true, bool withDefaults = true, - bool withListDevices = true, }) { var parser = ArgParser(); // Basic arguments for (SSHNPArg arg in SSHNPArg.args) { + if (arg.commandLineOnly && !isCommandLine) { + continue; + } switch (arg.format) { case ArgFormat.option: parser.addOption( @@ -209,6 +233,8 @@ ArgParser createArgParser({ mandatory: arg.mandatory, defaultsTo: withDefaults ? arg.defaultsTo?.toString() : null, help: arg.help, + allowed: arg.allowed, + aliases: arg.aliases ?? const [], ); break; case ArgFormat.multiOption: @@ -225,23 +251,10 @@ ArgParser createArgParser({ abbr: arg.abbr, defaultsTo: withDefaults ? arg.defaultsTo as bool? : null, help: arg.help, + negatable: arg.negatable, ); break; } } - if (withConfig) { - parser.addOption( - 'config-file', - help: 'Read args from a config file\nMandatory args are not required if already supplied in the config file', - ); - } - if (withListDevices) { - parser.addFlag( - 'list-devices', - aliases: ['ls'], - negatable: false, - help: 'List available devices', - ); - } return parser; } diff --git a/packages/sshnoports/lib/sshnp/sshnp_impl.dart b/packages/sshnoports/lib/sshnp/sshnp_impl.dart index 2c1a76790..04a49c673 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_impl.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_impl.dart @@ -554,6 +554,7 @@ class SSHNPImpl implements SSHNP { }, onDone: () { counter = 0; }); + return null; } // Start local forwarding to the remote sshd diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index 6ef0df891..a38e89acb 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -79,6 +79,35 @@ class SSHNPParams { ); } + /// Merge an SSHNPPartialParams objects into an SSHNPParams + /// Params in params2 take precedence over params1 + factory SSHNPParams.merge(SSHNPParams params1, [SSHNPPartialParams? params2]) { + params2 ??= SSHNPPartialParams.empty(); + return SSHNPParams( + profileName: params2.profileName ?? params1.profileName, + clientAtSign: params2.clientAtSign ?? params1.clientAtSign, + sshnpdAtSign: params2.sshnpdAtSign ?? params1.sshnpdAtSign, + host: params2.host ?? params1.host, + device: params2.device ?? params1.device, + port: params2.port ?? params1.port, + localPort: params2.localPort ?? params1.localPort, + atKeysFilePath: params2.atKeysFilePath ?? params1.atKeysFilePath, + sendSshPublicKey: params2.sendSshPublicKey ?? params1.sendSshPublicKey, + localSshOptions: params2.localSshOptions ?? params1.localSshOptions, + rsa: params2.rsa ?? params1.rsa, + remoteUsername: params2.remoteUsername ?? params1.remoteUsername, + verbose: params2.verbose ?? params1.verbose, + rootDomain: params2.rootDomain ?? params1.rootDomain, + localSshdPort: params2.localSshdPort ?? params1.localSshdPort, + listDevices: params2.listDevices ?? params1.listDevices, + legacyDaemon: params2.legacyDaemon ?? params1.legacyDaemon, + remoteSshdPort: params2.remoteSshdPort ?? params1.remoteSshdPort, + idleTimeout: params2.idleTimeout ?? params1.idleTimeout, + sshClient: params2.sshClient ?? params1.sshClient, + addForwardsToTunnel: params2.addForwardsToTunnel ?? params1.addForwardsToTunnel, + ); + } + factory SSHNPParams.fromFile(String fileName) { return SSHNPParams.fromPartial(SSHNPPartialParams.fromFile(fileName)); } @@ -103,14 +132,14 @@ class SSHNPParams { port: partial.port ?? SSHNP.defaultPort, localPort: partial.localPort ?? SSHNP.defaultLocalPort, sendSshPublicKey: partial.sendSshPublicKey ?? SSHNP.defaultSendSshPublicKey, - localSshOptions: partial.localSshOptions, + localSshOptions: partial.localSshOptions ?? SSHNP.defaultLocalSshOptions, rsa: partial.rsa ?? defaults.defaultRsa, verbose: partial.verbose ?? defaults.defaultVerbose, remoteUsername: partial.remoteUsername, atKeysFilePath: partial.atKeysFilePath, rootDomain: partial.rootDomain ?? defaults.defaultRootDomain, localSshdPort: partial.localSshdPort ?? defaults.defaultLocalSshdPort, - listDevices: partial.listDevices, + listDevices: partial.listDevices ?? SSHNP.defaultListDevices, legacyDaemon: partial.legacyDaemon ?? SSHNP.defaultLegacyDaemon, remoteSshdPort: partial.remoteSshdPort ?? defaults.defaultRemoteSshdPort, idleTimeout: partial.idleTimeout ?? defaults.defaultIdleTimeout, @@ -123,73 +152,6 @@ class SSHNPParams { return SSHNPParams.fromPartial(SSHNPPartialParams.fromConfig(fileName)); } - static Future> getConfigFilesFromDirectory([String? directory]) async { - var params = []; - - var homeDirectory = getHomeDirectory(throwIfNull: true)!; - directory ??= getDefaultSshnpConfigDirectory(homeDirectory); - var files = Directory(directory).list(); - - await files.forEach((file) { - if (file is! File) return; - if (path.extension(file.path) != '.env') return; - try { - var p = SSHNPParams.fromConfigFile(file.path); - - params.add(p); - } catch (e) { - print('Error reading config file: ${file.path}'); - print(e); - } - }); - - return params; - } - - Future toFile({String? directory, bool overwrite = false}) async { - if (profileName == null || profileName!.isEmpty) { - throw Exception('profileName is null or empty'); - } - - var fileName = profileName!.replaceAll(' ', '_'); - - var file = File(path.join( - directory ?? getDefaultSshnpConfigDirectory(homeDirectory), - '$fileName.env', - )); - - var exists = await file.exists(); - - if (exists && !overwrite) { - throw Exception('Failed to write config file: ${file.path} already exists'); - } - - // FileMode.write will create the file if it does not exist - // and overwrite existing files if it does exist - return file.writeAsString(toConfig(), mode: FileMode.write); - } - - Future deleteFile({String? directory, bool overwrite = false}) async { - if (profileName == null || profileName!.isEmpty) { - throw Exception('profileName is null or empty'); - } - - var fileName = profileName!.replaceAll(' ', '_'); - - var file = File(path.join( - directory ?? getDefaultSshnpConfigDirectory(homeDirectory), - '$fileName.env', - )); - - var exists = await file.exists(); - - if (!exists) { - throw Exception('Cannot delete ${file.path}, file does not exist'); - } - - return file.delete(); - } - Map toArgs() { return { 'profile-name': profileName, @@ -297,7 +259,6 @@ class SSHNPPartialParams { /// Merge two SSHNPPartialParams objects together /// Params in params2 take precedence over params1 - /// - localSshOptions are concatenated together as (params1 + params2) factory SSHNPPartialParams.merge(SSHNPPartialParams params1, [SSHNPPartialParams? params2]) { params2 ??= SSHNPPartialParams.empty(); return SSHNPPartialParams( @@ -316,7 +277,7 @@ class SSHNPPartialParams { verbose: params2.verbose ?? params1.verbose, rootDomain: params2.rootDomain ?? params1.rootDomain, localSshdPort: params2.localSshdPort ?? params1.localSshdPort, - listDevices: params2.listDevices || params1.listDevices, + listDevices: params2.listDevices ?? params1.listDevices, legacyDaemon: params2.legacyDaemon ?? params1.legacyDaemon, remoteSshdPort: params2.remoteSshdPort ?? params1.remoteSshdPort, idleTimeout: params2.idleTimeout ?? params1.idleTimeout, @@ -344,13 +305,13 @@ class SSHNPPartialParams { localPort: args['local-port'], atKeysFilePath: args['key-file'], sendSshPublicKey: args['ssh-public-key'], - localSshOptions: args['local-ssh-options'] ?? SSHNP.defaultLocalSshOptions, + localSshOptions: args['local-ssh-options'], rsa: args['rsa'], remoteUsername: args['remote-user-name'], verbose: args['verbose'], rootDomain: args['root-domain'], localSshdPort: args['local-sshd-port'], - listDevices: args['list-devices'] ?? SSHNP.defaultListDevices, + listDevices: args['list-devices'], legacyDaemon: args['legacy-daemon'], remoteSshdPort: args['remote-sshd-port'], idleTimeout: args['idle-timeout'], @@ -370,7 +331,7 @@ class SSHNPPartialParams { factory SSHNPPartialParams.fromArgs(List args) { var params = SSHNPPartialParams.empty(); - var parsedArgs = _createArgParser(withDefaults: false).parse(args); + var parsedArgs = createArgParser(withDefaults: false).parse(args); if (parsedArgs.wasParsed('config-file')) { var configFileName = parsedArgs['config-file'] as String; @@ -388,65 +349,10 @@ class SSHNPPartialParams { return SSHNPPartialParams.merge( params, - SSHNPPartialParams.fromArgMap(parsedArgsMap), + SSHNPPartialParams.fromMap(parsedArgsMap), ); } - static ArgParser _createArgParser({ - bool withConfig = true, - bool withDefaults = true, - bool withListDevices = true, - }) { - var parser = ArgParser(); - // Basic arguments - for (SSHNPArg arg in SSHNPArg.args) { - switch (arg.format) { - case ArgFormat.option: - parser.addOption( - arg.name, - abbr: arg.abbr, - mandatory: arg.mandatory, - defaultsTo: withDefaults ? arg.defaultsTo?.toString() : null, - allowed: arg.allowed, - help: arg.help, - ); - break; - case ArgFormat.multiOption: - parser.addMultiOption( - arg.name, - abbr: arg.abbr, - defaultsTo: withDefaults ? arg.defaultsTo as List? : null, - allowed: arg.allowed, - help: arg.help, - ); - break; - case ArgFormat.flag: - parser.addFlag( - arg.name, - abbr: arg.abbr, - defaultsTo: withDefaults ? arg.defaultsTo as bool? : null, - help: arg.help, - ); - break; - } - } - if (withConfig) { - parser.addOption( - 'config-file', - help: 'Read args from a config file\nMandatory args are not required if already supplied in the config file', - ); - } - if (withListDevices) { - parser.addFlag( - 'list-devices', - aliases: ['ls'], - negatable: false, - help: 'List available devices', - ); - } - return parser; - } - static Map _parseConfigFile(String fileName) { Map args = {}; diff --git a/packages/sshnp_gui/lib/l10n/app_en.arb b/packages/sshnp_gui/lib/l10n/app_en.arb index 96496efd8..7bfdf058f 100644 --- a/packages/sshnp_gui/lib/l10n/app_en.arb +++ b/packages/sshnp_gui/lib/l10n/app_en.arb @@ -12,6 +12,7 @@ "connect" : "Connect", "contactUs" : "Contact Us", "copiedToClipboard": "Copied to Clipboard", + "corruptedProfile": "Status: profile is corrupted", "currentConnections" : "Current Connections", "deleteButton" : "Delete", "dest" : "Dest.", diff --git a/packages/sshnp_gui/lib/src/controllers/config_controller.dart b/packages/sshnp_gui/lib/src/controllers/config_controller.dart index 5bb939ea4..f32f40f57 100644 --- a/packages/sshnp_gui/lib/src/controllers/config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/config_controller.dart @@ -80,7 +80,7 @@ class ConfigFamilyController extends AutoDisposeFamilyAsyncNotifier deleteConfig() async { - await ConfigKeyRepository.deleteParams(state.value!, atClient: AtClientManager.getInstance().atClient); - ref.read(configListController.notifier).remove(state.value!.profileName!); + await ConfigKeyRepository.deleteParams(arg, atClient: AtClientManager.getInstance().atClient); + ref.read(configListController.notifier).remove(arg); } } diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index 3ed1c85c7..4676f889e 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -23,7 +24,6 @@ class _HomeScreenState extends ConsumerState { final strings = AppLocalizations.of(context)!; final profileNames = ref.watch(configListController); - return Scaffold( body: SafeArea( child: Row( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart index d52554d30..6ec6b666a 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/config_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; import 'package:sshnp_gui/src/utility/sizes.dart'; class ProfileDeleteAction extends StatelessWidget { - final SSHNPParams params; - const ProfileDeleteAction(this.params, {Key? key}) : super(key: key); + final String profileName; + const ProfileDeleteAction(this.profileName, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -17,7 +16,7 @@ class ProfileDeleteAction extends StatelessWidget { showDialog( context: context, barrierDismissible: false, - builder: (_) => DeleteAlertDialog(sshnpParams: params), + builder: (_) => DeleteAlertDialog(profileName: profileName), ); }, icon: const Icon(Icons.delete_forever), @@ -26,8 +25,8 @@ class ProfileDeleteAction extends StatelessWidget { } class DeleteAlertDialog extends ConsumerWidget { - const DeleteAlertDialog({required this.sshnpParams, super.key}); - final SSHNPParams sshnpParams; + const DeleteAlertDialog({required this.profileName, super.key}); + final String profileName; @override Widget build( @@ -69,7 +68,7 @@ class DeleteAlertDialog extends ConsumerWidget { ), ElevatedButton( onPressed: () async { - await ref.read(configFamilyController(sshnpParams.profileName!).notifier).deleteConfig(); + await ref.read(configFamilyController(profileName).notifier).deleteConfig(); if (context.mounted) Navigator.of(context).pop(); }, style: Theme.of(context).elevatedButtonTheme.style!.copyWith( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart index e479ec1bc..d652ac5b4 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart @@ -1,6 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:sshnp_gui/src/controllers/config_controller.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_actions.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; + import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar_actions.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_bar/profile_bar_stats.dart'; @@ -15,10 +20,29 @@ class ProfileBar extends ConsumerStatefulWidget { class _ProfileBarState extends ConsumerState { @override Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; final controller = ref.watch(configFamilyController(widget.profileName)); return controller.when( - error: (error, stackTrace) => Container(), loading: () => const LinearProgressIndicator(), + error: (error, stackTrace) => Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: Theme.of(context).dividerColor, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(widget.profileName), + gapW8, + Expanded(child: Container()), + Text(strings.corruptedProfile), + ProfileDeleteAction(widget.profileName), + ], + ), + ), data: (profile) => Container( decoration: BoxDecoration( border: Border( @@ -30,7 +54,9 @@ class _ProfileBarState extends ConsumerState { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(profile.profileName!), + Text(widget.profileName), + gapW8, + Expanded(child: Container()), const ProfileBarStats(), ProfileBarActions(profile), ], diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart index 556165fbc..b886d7b1f 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart @@ -13,7 +13,7 @@ class ProfileBarActions extends StatelessWidget { // ProfileRunAction(params), ProfileTerminalAction(params), ProfileEditAction(params), - ProfileDeleteAction(params), + ProfileDeleteAction(params.profileName!), ], ); } From 4327a4c034b9cb6587cc102a1ea7d69cce04dbc2 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 12 Sep 2023 23:19:38 +0800 Subject: [PATCH 65/95] feat: implement profile menu button --- .../sshnoports/lib/sshnp/sshnp_params.dart | 3 +- packages/sshnp_gui/lib/l10n/app_en.arb | 2 + .../src/controllers/config_controller.dart | 1 + ...ion.dart => profile_action_callbacks.dart} | 28 +++---- .../profile_actions/profile_actions.dart | 1 - .../profile_delete_action.dart | 81 ++----------------- .../profile_delete_dialog.dart | 68 ++++++++++++++++ .../profile_actions/profile_menu_button.dart | 54 +++++++++++++ .../profile_actions/profile_run_action.dart | 2 +- .../profile_terminal_action.dart | 2 +- ...ction_button.dart => profile_widgets.dart} | 3 + .../widgets/profile_bar/profile_bar.dart | 37 ++++----- .../profile_bar/profile_bar_actions.dart | 4 +- 13 files changed, 167 insertions(+), 119 deletions(-) rename packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/{profile_edit_action.dart => profile_action_callbacks.dart} (52%) create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_dialog.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_menu_button.dart rename packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/{profile_action_button.dart => profile_widgets.dart} (88%) diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index a38e89acb..57ac592eb 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -295,6 +295,7 @@ class SSHNPPartialParams { factory SSHNPPartialParams.fromJson(String json) => SSHNPPartialParams.fromMap(jsonDecode(json)); factory SSHNPPartialParams.fromMap(Map args) { + print(args['local-ssh-options']); return SSHNPPartialParams( profileName: args['profile-name'], clientAtSign: args['from'], @@ -305,7 +306,7 @@ class SSHNPPartialParams { localPort: args['local-port'], atKeysFilePath: args['key-file'], sendSshPublicKey: args['ssh-public-key'], - localSshOptions: args['local-ssh-options'], + localSshOptions: List.from(args['local-ssh-options'] ?? []), rsa: args['rsa'], remoteUsername: args['remote-user-name'], verbose: args['verbose'], diff --git a/packages/sshnp_gui/lib/l10n/app_en.arb b/packages/sshnp_gui/lib/l10n/app_en.arb index 7bfdf058f..2c94ec3f3 100644 --- a/packages/sshnp_gui/lib/l10n/app_en.arb +++ b/packages/sshnp_gui/lib/l10n/app_en.arb @@ -15,10 +15,12 @@ "corruptedProfile": "Status: profile is corrupted", "currentConnections" : "Current Connections", "deleteButton" : "Delete", + "delete" : "Delete", "dest" : "Dest.", "destination" : "Destination", "device" : "Device Name", "deviceHint" : "The device name of the sshnpd we wish to communicate with", + "edit": "Edit", "error" : "Error", "failed": "Failed", "faq" : "FAQ", diff --git a/packages/sshnp_gui/lib/src/controllers/config_controller.dart b/packages/sshnp_gui/lib/src/controllers/config_controller.dart index f32f40f57..df9ac8351 100644 --- a/packages/sshnp_gui/lib/src/controllers/config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/config_controller.dart @@ -71,6 +71,7 @@ class ConfigListController extends AutoDisposeAsyncNotifier> { class ConfigFamilyController extends AutoDisposeFamilyAsyncNotifier { @override Future build(String arg) async { + if (arg.isEmpty) return SSHNPParams.empty(); return ConfigKeyRepository.getParams(arg, atClient: AtClientManager.getInstance().atClient); } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_callbacks.dart similarity index 52% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_callbacks.dart index b57fb6ed5..8d7e8acf7 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_edit_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_callbacks.dart @@ -1,26 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; - -import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/controllers/config_controller.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_delete_dialog.dart'; -class ProfileEditAction extends ConsumerStatefulWidget { - final SSHNPParams params; - const ProfileEditAction(this.params, {Key? key}) : super(key: key); - - @override - ConsumerState createState() => _ProfileEditActionState(); -} - -class _ProfileEditActionState extends ConsumerState { - void onPressed() { +class ProfileActionCallbacks { + static void edit(WidgetRef ref, BuildContext context, String profileName) { // Change value to update to trigger the update functionality on the new connection form. ref.watch(currentConfigController.notifier).setState( CurrentConfigState( - profileName: widget.params.profileName!, + profileName: profileName, configFileWriteState: ConfigFileWriteState.update, ), ); @@ -29,11 +19,11 @@ class _ProfileEditActionState extends ConsumerState { ); } - @override - Widget build(BuildContext context) { - return ProfileActionButton( - onPressed: onPressed, - icon: const Icon(Icons.edit), + static void delete(BuildContext context, String profileName) async { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => ProfileDeleteDialog(profileName: profileName), ); } } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_actions.dart index 60a819c1b..9b8fd8a92 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_actions.dart @@ -1,4 +1,3 @@ export 'profile_delete_action.dart'; -export 'profile_edit_action.dart'; export 'profile_run_action.dart'; export 'profile_terminal_action.dart'; \ No newline at end of file diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart index 6ec6b666a..df14a071e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart @@ -1,88 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sshnp_gui/src/controllers/config_controller.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; -import 'package:sshnp_gui/src/utility/sizes.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_callbacks.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_widgets.dart'; class ProfileDeleteAction extends StatelessWidget { final String profileName; - const ProfileDeleteAction(this.profileName, {Key? key}) : super(key: key); + final bool menuItem; + const ProfileDeleteAction(this.profileName, {this.menuItem = false, Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ProfileActionButton( - onPressed: () async { - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => DeleteAlertDialog(profileName: profileName), - ); - }, + onPressed: () => ProfileActionCallbacks.delete(context, profileName), icon: const Icon(Icons.delete_forever), ); } } - -class DeleteAlertDialog extends ConsumerWidget { - const DeleteAlertDialog({required this.profileName, super.key}); - final String profileName; - - @override - Widget build( - BuildContext context, - WidgetRef ref, - ) { - final strings = AppLocalizations.of(context)!; - - return Padding( - padding: const EdgeInsets.only(left: 0), - child: Center( - child: AlertDialog( - title: Center(child: Text(strings.warning)), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(strings.warningMessage), - gapH12, - Text.rich( - TextSpan( - children: [ - TextSpan( - text: strings.note, - style: Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w700), - ), - TextSpan( - text: strings.noteMessage, - ), - ], - ), - ) - ], - ), - actions: [ - OutlinedButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(strings.cancelButton, - style: Theme.of(context).textTheme.bodyLarge!.copyWith(decoration: TextDecoration.underline)), - ), - ElevatedButton( - onPressed: () async { - await ref.read(configFamilyController(profileName).notifier).deleteConfig(); - if (context.mounted) Navigator.of(context).pop(); - }, - style: Theme.of(context).elevatedButtonTheme.style!.copyWith( - backgroundColor: MaterialStateProperty.all(Colors.black), - ), - child: Text( - strings.deleteButton, - style: - Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w700, color: Colors.white), - ), - ) - ], - ), - ), - ); - } -} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_dialog.dart new file mode 100644 index 000000000..ec115ceb5 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_dialog.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnp_gui/src/controllers/config_controller.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ProfileDeleteDialog extends ConsumerWidget { + const ProfileDeleteDialog({required this.profileName, super.key}); + final String profileName; + + @override + Widget build( + BuildContext context, + WidgetRef ref, + ) { + final strings = AppLocalizations.of(context)!; + + return Padding( + padding: const EdgeInsets.only(left: 0), + child: Center( + child: AlertDialog( + title: Center(child: Text(strings.warning)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(strings.warningMessage), + gapH12, + Text.rich( + TextSpan( + children: [ + TextSpan( + text: strings.note, + style: Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w700), + ), + TextSpan( + text: strings.noteMessage, + ), + ], + ), + ) + ], + ), + actions: [ + OutlinedButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(strings.cancelButton, + style: Theme.of(context).textTheme.bodyLarge!.copyWith(decoration: TextDecoration.underline)), + ), + ElevatedButton( + onPressed: () async { + await ref.read(configFamilyController(profileName).notifier).deleteConfig(); + if (context.mounted) Navigator.of(context).pop(); + }, + style: Theme.of(context).elevatedButtonTheme.style!.copyWith( + backgroundColor: MaterialStateProperty.all(Colors.black), + ), + child: Text( + strings.deleteButton, + style: + Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w700, color: Colors.white), + ), + ) + ], + ), + ), + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_menu_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_menu_button.dart new file mode 100644 index 000000000..f7b498ac5 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_menu_button.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:sshnp_gui/src/controllers/config_controller.dart'; +import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; + +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_callbacks.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; + +class ProfileMenuButton extends ConsumerStatefulWidget { + final String profileName; + const ProfileMenuButton(this.profileName, {Key? key}) : super(key: key); + + @override + ConsumerState createState() => _ProfileMenuBarState(); +} + +class _ProfileMenuBarState extends ConsumerState { + @override + Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; + return PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: ProfileMenuItem(const Icon(Icons.edit), strings.edit), + onTap: () => ProfileActionCallbacks.edit(ref, context, widget.profileName), + ), + PopupMenuItem( + child: const ProfileMenuItem(Icon(Icons.delete_forever), 'Delete'), + onTap: () => ProfileActionCallbacks.delete(context, widget.profileName), + ), + ], + padding: EdgeInsets.zero, + ); + } +} + +class ProfileMenuItem extends StatelessWidget { + final Widget icon; + final String text; + const ProfileMenuItem(this.icon, this.text, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + icon, + gapW12, + Text(text), + ], + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index 62b72bf52..fa816d67b 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -5,7 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; import 'package:sshnp_gui/src/controllers/background_session_controller.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_widgets.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; class ProfileRunAction extends ConsumerStatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart index dcfa84bb2..6fb6c7968 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart @@ -6,7 +6,7 @@ import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; import 'package:sshnp_gui/src/controllers/navigation_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_widgets.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_widgets.dart similarity index 88% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_button.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_widgets.dart index fcac5bae0..15361df8f 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_button.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_widgets.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; class ProfileActionButton extends StatelessWidget { final void Function() onPressed; @@ -17,3 +18,5 @@ class ProfileActionButton extends StatelessWidget { ); } } + + diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart index d652ac5b4..35bc7085e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:sshnp_gui/src/controllers/config_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_actions.dart'; @@ -24,25 +23,27 @@ class _ProfileBarState extends ConsumerState { final controller = ref.watch(configFamilyController(widget.profileName)); return controller.when( loading: () => const LinearProgressIndicator(), - error: (error, stackTrace) => Container( - decoration: BoxDecoration( - border: Border( - top: BorderSide( - color: Theme.of(context).dividerColor, + error: (error, stackTrace) { + return Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: Theme.of(context).dividerColor, + ), ), ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(widget.profileName), - gapW8, - Expanded(child: Container()), - Text(strings.corruptedProfile), - ProfileDeleteAction(widget.profileName), - ], - ), - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(widget.profileName), + gapW8, + Expanded(child: Container()), + Text(strings.corruptedProfile), + ProfileDeleteAction(widget.profileName), + ], + ), + ); + }, data: (profile) => Container( decoration: BoxDecoration( border: Border( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart index b886d7b1f..f41cc86c9 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_actions.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_menu_button.dart'; class ProfileBarActions extends StatelessWidget { final SSHNPParams params; @@ -12,8 +13,7 @@ class ProfileBarActions extends StatelessWidget { children: [ // ProfileRunAction(params), ProfileTerminalAction(params), - ProfileEditAction(params), - ProfileDeleteAction(params.profileName!), + ProfileMenuButton(params.profileName!), ], ); } From 98df3dc2a0d5fe7a923069a93f8138201bcccc3e Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Tue, 12 Sep 2023 23:20:37 +0800 Subject: [PATCH 66/95] chore: rename profile action button file --- .../{profile_widgets.dart => profile_action_button.dart} | 1 - .../widgets/profile_actions/profile_delete_action.dart | 2 +- .../widgets/profile_actions/profile_menu_button.dart | 4 ---- .../widgets/profile_actions/profile_run_action.dart | 2 +- .../widgets/profile_actions/profile_terminal_action.dart | 2 +- 5 files changed, 3 insertions(+), 8 deletions(-) rename packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/{profile_widgets.dart => profile_action_button.dart} (88%) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_widgets.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_button.dart similarity index 88% rename from packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_widgets.dart rename to packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_button.dart index 15361df8f..ff8f1e463 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_widgets.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_button.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:sshnp_gui/src/utility/sizes.dart'; class ProfileActionButton extends StatelessWidget { final void Function() onPressed; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart index df14a071e..3615577aa 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_action.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_callbacks.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_widgets.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; class ProfileDeleteAction extends StatelessWidget { final String profileName; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_menu_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_menu_button.dart index f7b498ac5..abb030a9d 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_menu_button.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_menu_button.dart @@ -1,9 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:sshnp_gui/src/controllers/config_controller.dart'; -import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; - import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_callbacks.dart'; import 'package:sshnp_gui/src/utility/sizes.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index fa816d67b..62b72bf52 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -5,7 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; import 'package:sshnp_gui/src/controllers/background_session_controller.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_widgets.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; class ProfileRunAction extends ConsumerStatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart index 6fb6c7968..dcfa84bb2 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart @@ -6,7 +6,7 @@ import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; import 'package:sshnp_gui/src/controllers/navigation_rail_controller.dart'; import 'package:sshnp_gui/src/controllers/terminal_session_controller.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_widgets.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_button.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; From 2118f4d8fdf94b784e2491431652128437556fce Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Wed, 13 Sep 2023 22:33:00 +0800 Subject: [PATCH 67/95] fix: Add macOS UTI specification for atKeys files --- packages/sshnp_gui/macos/Runner/Info.plist | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/sshnp_gui/macos/Runner/Info.plist b/packages/sshnp_gui/macos/Runner/Info.plist index 4789daa6a..96349e965 100644 --- a/packages/sshnp_gui/macos/Runner/Info.plist +++ b/packages/sshnp_gui/macos/Runner/Info.plist @@ -28,5 +28,27 @@ MainMenu NSPrincipalClass NSApplication + UTImportedTypeDeclarations + + + UTTypeIdentifier + com.atsign.atkeys + UTTypeConformsTo + + public.json + + UTTypeDescription + Atsign Cryptographic Key File + UTTypeTagSpecification + + public.filename-extension + + atkeys + + + UTTypeReferenceURL + https://github.com/atsign-foundation/at_protocol + + From f8bea7cc30dd7ae8624785566066ffa95ca9802c Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Wed, 13 Sep 2023 22:49:44 +0800 Subject: [PATCH 68/95] fix: ensure config files are properly formed when putting them --- .../src/controllers/config_controller.dart | 21 ++++++++++++++++--- packages/sshnp_gui/pubspec.lock | 2 +- packages/sshnp_gui/pubspec.yaml | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/sshnp_gui/lib/src/controllers/config_controller.dart b/packages/sshnp_gui/lib/src/controllers/config_controller.dart index df9ac8351..25a708722 100644 --- a/packages/sshnp_gui/lib/src/controllers/config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/config_controller.dart @@ -71,12 +71,27 @@ class ConfigListController extends AutoDisposeAsyncNotifier> { class ConfigFamilyController extends AutoDisposeFamilyAsyncNotifier { @override Future build(String arg) async { - if (arg.isEmpty) return SSHNPParams.empty(); - return ConfigKeyRepository.getParams(arg, atClient: AtClientManager.getInstance().atClient); + AtClient atClient = AtClientManager.getInstance().atClient; + if (arg.isEmpty) { + return SSHNPParams.merge( + SSHNPParams.empty(), + SSHNPPartialParams()..clientAtSign = atClient.getCurrentAtSign()!, + ); + } + return ConfigKeyRepository.getParams(arg, atClient: atClient); } Future putConfig(SSHNPParams params) async { - await ConfigKeyRepository.putParams(params, atClient: AtClientManager.getInstance().atClient); + AtClient atClient = AtClientManager.getInstance().atClient; + if (params.clientAtSign != atClient.getCurrentAtSign()) { + params = SSHNPParams.merge( + params, + SSHNPPartialParams( + clientAtSign: atClient.getCurrentAtSign(), + ), + ); + } + await ConfigKeyRepository.putParams(params, atClient: atClient); ref.read(configListController.notifier).add(params.profileName!); } diff --git a/packages/sshnp_gui/pubspec.lock b/packages/sshnp_gui/pubspec.lock index c383b5a35..7c2ba51bb 100644 --- a/packages/sshnp_gui/pubspec.lock +++ b/packages/sshnp_gui/pubspec.lock @@ -1168,7 +1168,7 @@ packages: path: "../sshnoports" relative: true source: path - version: "3.4.1" + version: "4.0.0-rc.3" stack_trace: dependency: transitive description: diff --git a/packages/sshnp_gui/pubspec.yaml b/packages/sshnp_gui/pubspec.yaml index ccb6d4b22..e77cdebe1 100644 --- a/packages/sshnp_gui/pubspec.yaml +++ b/packages/sshnp_gui/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: at_common_flutter: ^2.0.12 at_contact: ^3.0.7 at_contacts_flutter: ^4.0.5 - at_onboarding_flutter: ^6.1.0 + at_onboarding_flutter: ^6.1.3 at_utils: ^3.0.11 biometric_storage: ^5.0.0+4 flutter: From 505573e435ad3c4f6731f5ed071e22bf89785665 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 09:28:40 +0800 Subject: [PATCH 69/95] fix: use ConfigFileRepository helper function --- .../sshnoports/lib/sshnp/sshnp_params.dart | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index ceb49243f..ac1a111ff 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -68,7 +68,8 @@ class SSHNPParams { // Use default atKeysFilePath if not provided - this.atKeysFilePath = atKeysFilePath ?? getDefaultAtKeysFilePath(homeDirectory, clientAtSign); + this.atKeysFilePath = + atKeysFilePath ?? getDefaultAtKeysFilePath(homeDirectory, clientAtSign); this.sshClient = sshClient ?? SSHNP.defaultSshClient.cliArg; } @@ -84,7 +85,8 @@ class SSHNPParams { /// Merge an SSHNPPartialParams objects into an SSHNPParams /// Params in params2 take precedence over params1 - factory SSHNPParams.merge(SSHNPParams params1, [SSHNPPartialParams? params2]) { + factory SSHNPParams.merge(SSHNPParams params1, + [SSHNPPartialParams? params2]) { params2 ??= SSHNPPartialParams.empty(); return SSHNPParams( profileName: params2.profileName ?? params1.profileName, @@ -107,7 +109,8 @@ class SSHNPParams { remoteSshdPort: params2.remoteSshdPort ?? params1.remoteSshdPort, idleTimeout: params2.idleTimeout ?? params1.idleTimeout, sshClient: params2.sshClient ?? params1.sshClient, - addForwardsToTunnel: params2.addForwardsToTunnel ?? params1.addForwardsToTunnel, + addForwardsToTunnel: + params2.addForwardsToTunnel ?? params1.addForwardsToTunnel, ); } @@ -115,7 +118,8 @@ class SSHNPParams { return SSHNPParams.fromPartial(SSHNPPartialParams.fromFile(fileName)); } - factory SSHNPParams.fromJson(String json) => SSHNPParams.fromPartial(SSHNPPartialParams.fromJson(json)); + factory SSHNPParams.fromJson(String json) => + SSHNPParams.fromPartial(SSHNPPartialParams.fromJson(json)); factory SSHNPParams.fromPartial(SSHNPPartialParams partial) { AtSignLogger logger = AtSignLogger(' SSHNPParams '); @@ -263,7 +267,8 @@ class SSHNPPartialParams { /// Merge two SSHNPPartialParams objects together /// Params in params2 take precedence over params1 - factory SSHNPPartialParams.merge(SSHNPPartialParams params1, [SSHNPPartialParams? params2]) { + factory SSHNPPartialParams.merge(SSHNPPartialParams params1, + [SSHNPPartialParams? params2]) { params2 ??= SSHNPPartialParams.empty(); return SSHNPPartialParams( profileName: params2.profileName ?? params1.profileName, @@ -286,7 +291,8 @@ class SSHNPPartialParams { remoteSshdPort: params2.remoteSshdPort ?? params1.remoteSshdPort, idleTimeout: params2.idleTimeout ?? params1.idleTimeout, sshClient: params2.sshClient ?? params1.sshClient, - addForwardsToTunnel: params2.addForwardsToTunnel ?? params1.addForwardsToTunnel, + addForwardsToTunnel: + params2.addForwardsToTunnel ?? params1.addForwardsToTunnel, ); } @@ -296,7 +302,8 @@ class SSHNPPartialParams { return SSHNPPartialParams.fromMap(args); } - factory SSHNPPartialParams.fromJson(String json) => SSHNPPartialParams.fromMap(jsonDecode(json)); + factory SSHNPPartialParams.fromJson(String json) => + SSHNPPartialParams.fromMap(jsonDecode(json)); factory SSHNPPartialParams.fromMap(Map args) { print(args['local-ssh-options']); @@ -327,7 +334,7 @@ class SSHNPPartialParams { factory SSHNPPartialParams.fromConfig(String fileName) { var args = _parseConfigFile(fileName); - args['profile-name'] = path.basenameWithoutExtension(fileName).replaceAll('_', ' '); + args['profile-name'] = ConfigFileRepository.toProfileName(fileName); return SSHNPPartialParams.fromMap(args); } @@ -349,7 +356,9 @@ class SSHNPPartialParams { // THIS IS A WORKAROUND IN ORDER TO BE TYPE SAFE IN SSHNPPartialParams.fromArgMap Map parsedArgsMap = { for (var e in parsedArgs.options) - e: SSHNPArg.fromName(e).type == ArgType.integer ? int.tryParse(parsedArgs[e]) : parsedArgs[e] + e: SSHNPArg.fromName(e).type == ArgType.integer + ? int.tryParse(parsedArgs[e]) + : parsedArgs[e] }; return SSHNPPartialParams.merge( From d55ef7815eb0820648dccc4e2402a602d7f350e6 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 09:33:03 +0800 Subject: [PATCH 70/95] fix: set clientAtSign within the constructor for SSHNPPartialParams --- packages/sshnp_gui/lib/src/controllers/config_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sshnp_gui/lib/src/controllers/config_controller.dart b/packages/sshnp_gui/lib/src/controllers/config_controller.dart index 25a708722..9f6a24247 100644 --- a/packages/sshnp_gui/lib/src/controllers/config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/config_controller.dart @@ -75,7 +75,7 @@ class ConfigFamilyController extends AutoDisposeFamilyAsyncNotifier Date: Thu, 14 Sep 2023 09:55:32 +0800 Subject: [PATCH 71/95] feat: implement idleTimeout and addForwardsToTunnel for Run action --- .../profile_actions/profile_run_action.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index 62b72bf52..a751bd336 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -34,17 +34,22 @@ class _ProfileRunActionState extends ConsumerState { } try { - sshnp = await SSHNP.fromParams( + SSHNPParams params = SSHNPParams.merge( widget.params, + SSHNPPartialParams( + idleTimeout: 60, + addForwardsToTunnel: true, + ), + ); + + sshnp = await SSHNP.fromParams( + params, atClient: AtClientManager.getInstance().atClient, sshrvGenerator: SSHRV.pureDart, ); - // TODO set --single-session, --timeout - await sshnp!.init(); final sshnpResult = await sshnp!.run(); - // TODO throw away bad results } catch (e) { if (mounted) { CustomSnackBar.error(content: e.toString()); @@ -57,7 +62,7 @@ class _ProfileRunActionState extends ConsumerState { } Future onStop() async { - // need to implement SSHNP.stop + // TODO need to implement SSHNP.stop } static const Map _iconMap = { From 221dcb497db4156addb53ebe0eb23b595aa77981 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 11:14:03 +0800 Subject: [PATCH 72/95] feat: add export option --- .../config_file_repository.dart | 7 ++- packages/sshnp_gui/lib/l10n/app_en.arb | 3 +- .../profile_action_callbacks.dart | 43 +++++++++++++++++++ .../profile_actions/profile_menu_button.dart | 4 ++ packages/sshnp_gui/pubspec.yaml | 1 + 5 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/config_repository/config_file_repository.dart b/packages/sshnoports/lib/sshnp/config_repository/config_file_repository.dart index 1dfa43b54..3822d2982 100644 --- a/packages/sshnoports/lib/sshnp/config_repository/config_file_repository.dart +++ b/packages/sshnoports/lib/sshnp/config_repository/config_file_repository.dart @@ -12,12 +12,15 @@ class ConfigFileRepository { return profileName; } - static String fromProfileName(String profileName, {String? directory, bool replaceSpaces = true}) { + static String fromProfileName(String profileName, + {String? directory, bool replaceSpaces = true, bool basenameOnly = false}) { var fileName = profileName; if (replaceSpaces) fileName = fileName.replaceAll(' ', '_'); + final basename = '$fileName.env'; + if (basenameOnly) return basename; return path.join( directory ?? getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), - '$fileName.env', + basename, ); } diff --git a/packages/sshnp_gui/lib/l10n/app_en.arb b/packages/sshnp_gui/lib/l10n/app_en.arb index 2c94ec3f3..fe2806fe2 100644 --- a/packages/sshnp_gui/lib/l10n/app_en.arb +++ b/packages/sshnp_gui/lib/l10n/app_en.arb @@ -20,7 +20,8 @@ "destination" : "Destination", "device" : "Device Name", "deviceHint" : "The device name of the sshnpd we wish to communicate with", - "edit": "Edit", + "edit" : "Edit", + "export" : "Export", "error" : "Error", "failed": "Failed", "faq" : "FAQ", diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_callbacks.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_callbacks.dart index 8d7e8acf7..3bca032f0 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_callbacks.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_callbacks.dart @@ -1,9 +1,17 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:sshnoports/common/utils.dart'; +import 'package:sshnoports/sshnp/config_repository/config_file_repository.dart'; import 'package:sshnp_gui/src/controllers/config_controller.dart'; import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_delete_dialog.dart'; +import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; +import 'package:path/path.dart' as path; class ProfileActionCallbacks { static void edit(WidgetRef ref, BuildContext context, String profileName) { @@ -26,4 +34,39 @@ class ProfileActionCallbacks { builder: (_) => ProfileDeleteDialog(profileName: profileName), ); } + + static Future export(WidgetRef ref, BuildContext context, String profileName) async { + if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) { + return _exportDesktop(ref, context, profileName); + } + CustomSnackBar.error(content: 'Unable to export profile:\nUnsupported platform'); + } + + static Future _exportDesktop(WidgetRef ref, BuildContext context, String profileName) async { + try { + final suggestedName = ConfigFileRepository.fromProfileName(profileName, basenameOnly: true); + final initialDirectory = getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!); + const String mimeType = 'text/plain'; + + final FileSaveLocation? saveLocation = await getSaveLocation( + suggestedName: suggestedName, + initialDirectory: initialDirectory, + acceptedTypeGroups: [ + const XTypeGroup(extensions: ['env']), + ], + ); + if (saveLocation == null) return; + final params = ref.read(configFamilyController(profileName)); + final fileData = Uint8List.fromList(params.requireValue.toConfig().codeUnits); + final XFile textFile = XFile.fromData( + fileData, + mimeType: mimeType, + name: path.basename(saveLocation.path), + ); + + await textFile.saveTo(saveLocation.path); + } catch (e) { + CustomSnackBar.error(content: 'Unable to export profile:\n${e.toString()}'); + } + } } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_menu_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_menu_button.dart index abb030a9d..ce28e53bf 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_menu_button.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_menu_button.dart @@ -18,6 +18,10 @@ class _ProfileMenuBarState extends ConsumerState { final strings = AppLocalizations.of(context)!; return PopupMenuButton( itemBuilder: (context) => [ + PopupMenuItem( + child: ProfileMenuItem(const Icon(Icons.download), strings.export), + onTap: () => ProfileActionCallbacks.export(ref, context, widget.profileName), + ), PopupMenuItem( child: ProfileMenuItem(const Icon(Icons.edit), strings.edit), onTap: () => ProfileActionCallbacks.edit(ref, context, widget.profileName), diff --git a/packages/sshnp_gui/pubspec.yaml b/packages/sshnp_gui/pubspec.yaml index e77cdebe1..a6e1930a2 100644 --- a/packages/sshnp_gui/pubspec.yaml +++ b/packages/sshnp_gui/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: at_onboarding_flutter: ^6.1.3 at_utils: ^3.0.11 biometric_storage: ^5.0.0+4 + file_selector: ^1.0.1 flutter: sdk: flutter flutter_dotenv: ^5.0.2 From 1ba1152e004474f2f3515c228c0b002bb2a8cbe2 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 12:16:32 +0800 Subject: [PATCH 73/95] feat: add import profile feature --- .../config_file_repository.dart | 22 ++-- .../sshnoports/lib/sshnp/sshnp_params.dart | 104 ++++-------------- packages/sshnp_gui/lib/l10n/app_en.arb | 2 + .../src/controllers/config_controller.dart | 2 +- .../home_screen_action_callbacks.dart | 47 ++++++++ .../home_screen_actions.dart | 3 +- .../home_screen_import_dialog.dart | 42 +++++++ .../home_screen_menu_button.dart | 46 ++++++++ .../profile_action_callbacks.dart | 8 +- .../sshnp_gui/lib/src/utility/constants.dart | 9 ++ packages/sshnp_gui/macos/Runner/Info.plist | 19 ++++ packages/sshnp_gui/pubspec.lock | 2 +- packages/sshnp_gui/pubspec.yaml | 2 +- 13 files changed, 202 insertions(+), 106 deletions(-) create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_action_callbacks.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_import_dialog.dart create mode 100644 packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_menu_button.dart diff --git a/packages/sshnoports/lib/sshnp/config_repository/config_file_repository.dart b/packages/sshnoports/lib/sshnp/config_repository/config_file_repository.dart index 3822d2982..1a9f3aa3d 100644 --- a/packages/sshnoports/lib/sshnp/config_repository/config_file_repository.dart +++ b/packages/sshnoports/lib/sshnp/config_repository/config_file_repository.dart @@ -91,15 +91,6 @@ class ConfigFileRepository { } static Map parseConfigFile(String fileName) { - Map args = {}; - - if (path.normalize(fileName).contains('/') || path.normalize(fileName).contains(r'\')) { - fileName = path.normalize(path.absolute(fileName)); - } else { - fileName = - path.normalize(path.absolute(getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!), fileName)); - } - File file = File(fileName); if (!file.existsSync()) { @@ -107,7 +98,16 @@ class ConfigFileRepository { } try { List lines = file.readAsLinesSync(); + return parseConfigFileContents(lines); + } on FileSystemException { + throw Exception('Error reading config file: $fileName'); + } + } + + static Map parseConfigFileContents(List lines) { + Map args = {}; + try { for (String line in lines) { if (line.startsWith('#')) continue; @@ -145,10 +145,8 @@ class ConfigFileRepository { } } return args; - } on FileSystemException { - throw Exception('Error reading config file: $fileName'); } catch (e) { - throw Exception('Error parsing config file: $fileName'); + throw Exception('Error parsing config file'); } } } diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index ac1a111ff..3fc2fbc00 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -33,8 +33,7 @@ class SSHNPParams { late final String sshClient; /// Special Arguments - final String? - profileName; // automatically populated with the filename if from a configFile + final String? profileName; // automatically populated with the filename if from a configFile final bool listDevices; SSHNPParams({ @@ -68,8 +67,7 @@ class SSHNPParams { // Use default atKeysFilePath if not provided - this.atKeysFilePath = - atKeysFilePath ?? getDefaultAtKeysFilePath(homeDirectory, clientAtSign); + this.atKeysFilePath = atKeysFilePath ?? getDefaultAtKeysFilePath(homeDirectory, clientAtSign); this.sshClient = sshClient ?? SSHNP.defaultSshClient.cliArg; } @@ -85,8 +83,7 @@ class SSHNPParams { /// Merge an SSHNPPartialParams objects into an SSHNPParams /// Params in params2 take precedence over params1 - factory SSHNPParams.merge(SSHNPParams params1, - [SSHNPPartialParams? params2]) { + factory SSHNPParams.merge(SSHNPParams params1, [SSHNPPartialParams? params2]) { params2 ??= SSHNPPartialParams.empty(); return SSHNPParams( profileName: params2.profileName ?? params1.profileName, @@ -109,8 +106,7 @@ class SSHNPParams { remoteSshdPort: params2.remoteSshdPort ?? params1.remoteSshdPort, idleTimeout: params2.idleTimeout ?? params1.idleTimeout, sshClient: params2.sshClient ?? params1.sshClient, - addForwardsToTunnel: - params2.addForwardsToTunnel ?? params1.addForwardsToTunnel, + addForwardsToTunnel: params2.addForwardsToTunnel ?? params1.addForwardsToTunnel, ); } @@ -118,8 +114,7 @@ class SSHNPParams { return SSHNPParams.fromPartial(SSHNPPartialParams.fromFile(fileName)); } - factory SSHNPParams.fromJson(String json) => - SSHNPParams.fromPartial(SSHNPPartialParams.fromJson(json)); + factory SSHNPParams.fromJson(String json) => SSHNPParams.fromPartial(SSHNPPartialParams.fromJson(json)); factory SSHNPParams.fromPartial(SSHNPPartialParams partial) { AtSignLogger logger = AtSignLogger(' SSHNPParams '); @@ -138,8 +133,7 @@ class SSHNPParams { device: partial.device ?? SSHNP.defaultDevice, port: partial.port ?? SSHNP.defaultPort, localPort: partial.localPort ?? SSHNP.defaultLocalPort, - sendSshPublicKey: - partial.sendSshPublicKey ?? SSHNP.defaultSendSshPublicKey, + sendSshPublicKey: partial.sendSshPublicKey ?? SSHNP.defaultSendSshPublicKey, localSshOptions: partial.localSshOptions ?? SSHNP.defaultLocalSshOptions, rsa: partial.rsa ?? defaults.defaultRsa, verbose: partial.verbose ?? defaults.defaultVerbose, @@ -156,8 +150,8 @@ class SSHNPParams { ); } - factory SSHNPParams.fromConfigFile(String fileName) { - return SSHNPParams.fromPartial(SSHNPPartialParams.fromConfig(fileName)); + factory SSHNPParams.fromConfig(String profileName, List lines) { + return SSHNPParams.fromPartial(SSHNPPartialParams.fromConfig(profileName, lines)); } Map toArgs() { @@ -267,8 +261,7 @@ class SSHNPPartialParams { /// Merge two SSHNPPartialParams objects together /// Params in params2 take precedence over params1 - factory SSHNPPartialParams.merge(SSHNPPartialParams params1, - [SSHNPPartialParams? params2]) { + factory SSHNPPartialParams.merge(SSHNPPartialParams params1, [SSHNPPartialParams? params2]) { params2 ??= SSHNPPartialParams.empty(); return SSHNPPartialParams( profileName: params2.profileName ?? params1.profileName, @@ -291,8 +284,7 @@ class SSHNPPartialParams { remoteSshdPort: params2.remoteSshdPort ?? params1.remoteSshdPort, idleTimeout: params2.idleTimeout ?? params1.idleTimeout, sshClient: params2.sshClient ?? params1.sshClient, - addForwardsToTunnel: - params2.addForwardsToTunnel ?? params1.addForwardsToTunnel, + addForwardsToTunnel: params2.addForwardsToTunnel ?? params1.addForwardsToTunnel, ); } @@ -302,8 +294,13 @@ class SSHNPPartialParams { return SSHNPPartialParams.fromMap(args); } - factory SSHNPPartialParams.fromJson(String json) => - SSHNPPartialParams.fromMap(jsonDecode(json)); + factory SSHNPPartialParams.fromConfig(String profileName, List lines) { + var args = ConfigFileRepository.parseConfigFileContents(lines); + args['profile-name'] = profileName; + return SSHNPPartialParams.fromMap(args); + } + + factory SSHNPPartialParams.fromJson(String json) => SSHNPPartialParams.fromMap(jsonDecode(json)); factory SSHNPPartialParams.fromMap(Map args) { print(args['local-ssh-options']); @@ -332,12 +329,6 @@ class SSHNPPartialParams { ); } - factory SSHNPPartialParams.fromConfig(String fileName) { - var args = _parseConfigFile(fileName); - args['profile-name'] = ConfigFileRepository.toProfileName(fileName); - return SSHNPPartialParams.fromMap(args); - } - /// Parses args from command line /// first merges from a config file if provided via --config-file factory SSHNPPartialParams.fromArgs(List args) { @@ -349,16 +340,14 @@ class SSHNPPartialParams { var configFileName = parsedArgs['config-file'] as String; params = SSHNPPartialParams.merge( params, - SSHNPPartialParams.fromConfig(configFileName), + SSHNPPartialParams.fromFile(configFileName), ); } // THIS IS A WORKAROUND IN ORDER TO BE TYPE SAFE IN SSHNPPartialParams.fromArgMap Map parsedArgsMap = { for (var e in parsedArgs.options) - e: SSHNPArg.fromName(e).type == ArgType.integer - ? int.tryParse(parsedArgs[e]) - : parsedArgs[e] + e: SSHNPArg.fromName(e).type == ArgType.integer ? int.tryParse(parsedArgs[e]) : parsedArgs[e] }; return SSHNPPartialParams.merge( @@ -366,59 +355,4 @@ class SSHNPPartialParams { SSHNPPartialParams.fromMap(parsedArgsMap), ); } - - static Map _parseConfigFile(String fileName) { - Map args = {}; - - File file = File(fileName); - - if (!file.existsSync()) { - throw Exception('Config file does not exist: $fileName'); - } - try { - List lines = file.readAsLinesSync(); - - for (String line in lines) { - if (line.startsWith('#')) continue; - - var parts = line.split('='); - if (parts.length != 2) continue; - - var key = parts[0].trim(); - var value = parts[1].trim(); - - SSHNPArg arg = SSHNPArg.fromBashName(key); - if (arg.name.isEmpty) continue; - - switch (arg.format) { - case ArgFormat.flag: - if (value.toLowerCase() == 'true') { - args[arg.name] = true; - } - continue; - case ArgFormat.multiOption: - var values = value.split(','); - args.putIfAbsent(arg.name, () => []); - for (String val in values) { - if (val.isEmpty) continue; - args[arg.name].add(val); - } - continue; - case ArgFormat.option: - if (value.isEmpty) continue; - if (arg.type == ArgType.integer) { - args[arg.name] = int.tryParse(value); - } else { - args[arg.name] = value; - } - continue; - } - } - return args; - } on FileSystemException { - throw Exception('Error reading config file: $fileName'); - } catch (e) { - throw Exception('Error parsing config file: $fileName'); - } - } } diff --git a/packages/sshnp_gui/lib/l10n/app_en.arb b/packages/sshnp_gui/lib/l10n/app_en.arb index fe2806fe2..85999dda4 100644 --- a/packages/sshnp_gui/lib/l10n/app_en.arb +++ b/packages/sshnp_gui/lib/l10n/app_en.arb @@ -31,6 +31,8 @@ "host" : "Host", "host" : "Host", "hostSelection" : "Host Selection", + "import" : "Import", + "importProfile" : "Import Profile", "keyFile" : "Key File", "listDevices" : "List Devices", "localPort": "Local Port", diff --git a/packages/sshnp_gui/lib/src/controllers/config_controller.dart b/packages/sshnp_gui/lib/src/controllers/config_controller.dart index 9f6a24247..fdfd12a39 100644 --- a/packages/sshnp_gui/lib/src/controllers/config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/config_controller.dart @@ -50,7 +50,7 @@ class ConfigListController extends AutoDisposeAsyncNotifier> { @override Future> build() async { AtClient atClient = AtClientManager.getInstance().atClient; - return ConfigKeyRepository.listProfiles(atClient); + return Set.from(await ConfigKeyRepository.listProfiles(atClient)); } Future refresh() async { diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_action_callbacks.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_action_callbacks.dart new file mode 100644 index 000000000..50cc7721e --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_action_callbacks.dart @@ -0,0 +1,47 @@ +import 'dart:io'; + +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sshnoports/sshnp/config_repository/config_file_repository.dart'; +import 'package:sshnoports/sshnp/sshnp.dart'; +import 'package:sshnp_gui/src/controllers/config_controller.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_import_dialog.dart'; +import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; +import 'package:sshnp_gui/src/utility/constants.dart'; + +class HomeScreenActionCallbacks { + static Future import(WidgetRef ref, BuildContext context) async { + if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) { + return _importDesktop(ref, context); + } + CustomSnackBar.error(content: 'Unable to import profile:\nUnsupported platform'); + } + + static Future _importDesktop(WidgetRef ref, BuildContext context) async { + try { + final XFile? file = await openFile(acceptedTypeGroups: [dotEnvTypeGroup]); + if (file == null) return; + if (context.mounted) { + String initialName = ConfigFileRepository.toProfileName(file.path); + String? profileName = await _getProfileNameFromUser(context, initialName: initialName); + if (profileName == null) return; + if (profileName.isEmpty) profileName = initialName; + final lines = (await file.readAsString()).split('\n'); + ref.read(configFamilyController(profileName).notifier).putConfig(SSHNPParams.fromConfig(profileName, lines)); + } + } catch (e) { + CustomSnackBar.error(content: 'Unable to import profile:\n${e.toString()}'); + } + } + + static Future _getProfileNameFromUser(BuildContext context, {String? initialName}) async { + String? profileName; + setProfileName(String? p) => profileName = p; + await showDialog( + context: context, + builder: (_) => HomeScreenImportDialog(setProfileName, initialName: initialName), + ); + return profileName; + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_actions.dart index 2c9a92840..862e53412 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_actions.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_menu_button.dart'; import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/new_profile_action.dart'; class HomeScreenActions extends StatelessWidget { @@ -7,7 +8,7 @@ class HomeScreenActions extends StatelessWidget { @override Widget build(BuildContext context) { return const Row( - children: [NewProfileAction()], + children: [NewProfileAction(), HomeScreenMenuButton()], ); } } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_import_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_import_dialog.dart new file mode 100644 index 000000000..287f1c8f5 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_import_dialog.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class HomeScreenImportDialog extends StatelessWidget { + final void Function(String?) setValue; + final String? initialName; + const HomeScreenImportDialog(this.setValue, {this.initialName, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final TextEditingController controller = TextEditingController(text: initialName); + final strings = AppLocalizations.of(context)!; + + return AlertDialog( + title: Text(strings.importProfile), + content: TextField( + controller: controller, + decoration: InputDecoration(hintText: strings.profileName), + ), + actions: [ + OutlinedButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(strings.cancelButton, + style: Theme.of(context).textTheme.bodyLarge!.copyWith(decoration: TextDecoration.underline)), + ), + ElevatedButton( + onPressed: () async { + setValue(controller.text); + if (context.mounted) Navigator.of(context).pop(); + }, + style: Theme.of(context).elevatedButtonTheme.style!.copyWith( + backgroundColor: MaterialStateProperty.all(Colors.black), + ), + child: Text( + strings.submit, + style: Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w700, color: Colors.white), + ), + ) + ], + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_menu_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_menu_button.dart new file mode 100644 index 000000000..1fd56d2b4 --- /dev/null +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_menu_button.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_action_callbacks.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_callbacks.dart'; +import 'package:sshnp_gui/src/utility/sizes.dart'; + +class HomeScreenMenuButton extends ConsumerStatefulWidget { + const HomeScreenMenuButton({Key? key}) : super(key: key); + + @override + ConsumerState createState() => _ProfileMenuBarState(); +} + +class _ProfileMenuBarState extends ConsumerState { + @override + Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; + return PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: ProfileMenuItem(const Icon(Icons.upload), strings.import), + onTap: () => HomeScreenActionCallbacks.import(ref, context), + ), + ], + padding: EdgeInsets.zero, + ); + } +} + +class ProfileMenuItem extends StatelessWidget { + final Widget icon; + final String text; + const ProfileMenuItem(this.icon, this.text, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + icon, + gapW12, + Text(text), + ], + ); + } +} diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_callbacks.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_callbacks.dart index 3bca032f0..bfeac0c08 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_callbacks.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_action_callbacks.dart @@ -12,6 +12,7 @@ import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_delete_dialog.dart'; import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; import 'package:path/path.dart' as path; +import 'package:sshnp_gui/src/utility/constants.dart'; class ProfileActionCallbacks { static void edit(WidgetRef ref, BuildContext context, String profileName) { @@ -46,21 +47,18 @@ class ProfileActionCallbacks { try { final suggestedName = ConfigFileRepository.fromProfileName(profileName, basenameOnly: true); final initialDirectory = getDefaultSshnpConfigDirectory(getHomeDirectory(throwIfNull: true)!); - const String mimeType = 'text/plain'; final FileSaveLocation? saveLocation = await getSaveLocation( suggestedName: suggestedName, initialDirectory: initialDirectory, - acceptedTypeGroups: [ - const XTypeGroup(extensions: ['env']), - ], + acceptedTypeGroups: [dotEnvTypeGroup], ); if (saveLocation == null) return; final params = ref.read(configFamilyController(profileName)); final fileData = Uint8List.fromList(params.requireValue.toConfig().codeUnits); final XFile textFile = XFile.fromData( fileData, - mimeType: mimeType, + mimeType: dotEnvMimeType, name: path.basename(saveLocation.path), ); diff --git a/packages/sshnp_gui/lib/src/utility/constants.dart b/packages/sshnp_gui/lib/src/utility/constants.dart index 353ebc8cd..e61791d2e 100644 --- a/packages/sshnp_gui/lib/src/utility/constants.dart +++ b/packages/sshnp_gui/lib/src/utility/constants.dart @@ -1,3 +1,4 @@ +import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; const kPrimaryColor = Color(0xFFF05E3E); @@ -6,3 +7,11 @@ const kBackGroundColorDark = Color(0xFF222222); const kEmptyFieldValidationError = 'Field Cannot be left blank'; const kAtsignFieldValidationError = 'Field must start with @'; + +const String dotEnvMimeType = 'text/plain'; +const XTypeGroup dotEnvTypeGroup = XTypeGroup( + label: 'dotenv', + extensions: ['env'], + mimeTypes: [dotEnvMimeType], + uniformTypeIdentifiers: ['com.atsign.sshnp-config'], +); diff --git a/packages/sshnp_gui/macos/Runner/Info.plist b/packages/sshnp_gui/macos/Runner/Info.plist index 96349e965..8e0ae8e19 100644 --- a/packages/sshnp_gui/macos/Runner/Info.plist +++ b/packages/sshnp_gui/macos/Runner/Info.plist @@ -49,6 +49,25 @@ UTTypeReferenceURL https://github.com/atsign-foundation/at_protocol + + UTTypeIdentifier + com.atsign.sshnp-config + UTTypeConformsTo + + public.plain-text + + UTTypeDescription + Dotenv File + UTTypeTagSpecification + + public.filename-extension + + env + + + UTTypeReferenceURL + https://12factor.net/ + diff --git a/packages/sshnp_gui/pubspec.lock b/packages/sshnp_gui/pubspec.lock index 7c2ba51bb..f7caea49c 100644 --- a/packages/sshnp_gui/pubspec.lock +++ b/packages/sshnp_gui/pubspec.lock @@ -410,7 +410,7 @@ packages: source: hosted version: "5.3.3" file_selector: - dependency: transitive + dependency: "direct main" description: name: file_selector sha256: "1d2fde93dddf634a9c3c0faa748169d7ac0d83757135555707e52f02c017ad4f" diff --git a/packages/sshnp_gui/pubspec.yaml b/packages/sshnp_gui/pubspec.yaml index a6e1930a2..96058d46c 100644 --- a/packages/sshnp_gui/pubspec.yaml +++ b/packages/sshnp_gui/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: at_onboarding_flutter: ^6.1.3 at_utils: ^3.0.11 biometric_storage: ^5.0.0+4 - file_selector: ^1.0.1 + file_selector: ^0.9.5 flutter: sdk: flutter flutter_dotenv: ^5.0.2 From 7e435ff23a75e8b7d5055968dbb01fd31f7ca375 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 14:15:27 +0800 Subject: [PATCH 74/95] feat: add background ssh session --- packages/sshnoports/lib/sshnp/sshnp_impl.dart | 149 ++++++------------ .../sshnoports/lib/sshnp/sshnp_result.dart | 7 + .../background_session_controller.dart | 21 +-- .../home_screen_menu_button.dart | 1 - .../profile_actions/profile_actions.dart | 6 +- .../profile_actions/profile_run_action.dart | 55 ++++--- .../profile_bar/profile_bar_actions.dart | 3 +- packages/sshnp_gui/pubspec.lock | 29 +--- packages/sshnp_gui/pubspec.yaml | 5 + 9 files changed, 119 insertions(+), 157 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_impl.dart b/packages/sshnoports/lib/sshnp/sshnp_impl.dart index 04a49c673..1c92555ed 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_impl.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_impl.dart @@ -206,8 +206,7 @@ class SSHNPImpl implements SSHNP { // search for a public key file called 'false' if (sendSshPublicKey == 'false' || sendSshPublicKey.isEmpty) { publicKeyFileName = ''; - } else if (path.normalize(sendSshPublicKey).contains('/') || - path.normalize(sendSshPublicKey).contains(r'\')) { + } else if (path.normalize(sendSshPublicKey).contains('/') || path.normalize(sendSshPublicKey).contains(r'\')) { publicKeyFileName = path.normalize(path.absolute(sendSshPublicKey)); } else { publicKeyFileName = path.normalize('$sshHomeDirectory$sendSshPublicKey'); @@ -239,21 +238,18 @@ class SSHNPImpl implements SSHNP { if (atClient != null) { if (p.clientAtSign != atClient.getCurrentAtSign()) { - throw ArgumentError( - 'Option from must match the current atSign of the AtClient'); + throw ArgumentError('Option from must match the current atSign of the AtClient'); } } else { // Check atKeyFile selected exists if (!await fileExists(p.atKeysFilePath)) { - throw ArgumentError( - '\nUnable to find .atKeys file : ${p.atKeysFilePath}'); + throw ArgumentError('\nUnable to find .atKeys file : ${p.atKeysFilePath}'); } } // Check to see if the port number is in range for TCP ports if (p.localSshdPort > 65535 || p.localSshdPort < 1) { - throw ArgumentError( - '\nInvalid port number for sshd (1-65535) : ${p.localSshdPort}'); + throw ArgumentError('\nInvalid port number for sshd (1-65535) : ${p.localSshdPort}'); } String sessionId = Uuid().v4(); @@ -292,8 +288,7 @@ class SSHNPImpl implements SSHNP { legacyDaemon: p.legacyDaemon, remoteSshdPort: p.remoteSshdPort, idleTimeout: p.idleTimeout, - sshClient: SupportedSshClient.values - .firstWhere((c) => c.cliArg == p.sshClient), + sshClient: SupportedSshClient.values.firstWhere((c) => c.cliArg == p.sshClient), addForwardsToTunnel: p.addForwardsToTunnel, ); if (p.verbose) { @@ -343,8 +338,7 @@ class SSHNPImpl implements SSHNP { throw ('\n Unable to find ssh public key file : $publicKeyFileName'); } - if (publicKeyFileName.isNotEmpty && - !File(publicKeyFileName.replaceAll('.pub', '')).existsSync()) { + if (publicKeyFileName.isNotEmpty && !File(publicKeyFileName.replaceAll('.pub', '')).existsSync()) { throw ('\n Unable to find matching ssh private key for public key : $publicKeyFileName'); } @@ -352,8 +346,7 @@ class SSHNPImpl implements SSHNP { // find a spare local port if (localPort == 0) { - ServerSocket serverSocket = - await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + ServerSocket serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); localPort = serverSocket.port; await serverSocket.close(); } @@ -370,17 +363,12 @@ class SSHNPImpl implements SSHNP { // 2) if legacy then we share the private key via its own notification if (!direct) { var (String ephemeralPublicKey, String ephemeralPrivateKey) = - await generateSshKeys( - rsa: rsa, - sessionId: sessionId, - sshHomeDirectory: sshHomeDirectory); + await generateSshKeys(rsa: rsa, sessionId: sessionId, sshHomeDirectory: sshHomeDirectory); sshPublicKey = ephemeralPublicKey; sshPrivateKey = ephemeralPrivateKey; await addEphemeralKeyToAuthorizedKeys( - sshPublicKey: sshPublicKey, - localSshdPort: localSshdPort, - sessionId: sessionId); + sshPublicKey: sshPublicKey, localSshdPort: localSshdPort, sessionId: sessionId); if (legacyDaemon) { await sharePrivateKeyWithSshnpd(); @@ -418,8 +406,7 @@ class SSHNPImpl implements SSHNP { // tunnel is being managed by the SSHNP instance. In that case, // _doneCompleter.complete() is called once the tunnel determines // that there are no more active connections. - logger.info( - 'Requesting daemon to set up socket tunnel for direct ssh session'); + logger.info('Requesting daemon to set up socket tunnel for direct ssh session'); res = await startDirectSsh(); } else { logger.info('Requesting daemon to start reverse ssh session'); @@ -442,12 +429,7 @@ class SSHNPImpl implements SSHNP { ..metadata = (Metadata() ..ttr = -1 ..ttl = 10000), - signAndWrapAndJsonEncode(atClient, { - 'direct': true, - 'sessionId': sessionId, - 'host': host, - 'port': port - }), + signAndWrapAndJsonEncode(atClient, {'direct': true, 'sessionId': sessionId, 'host': host, 'port': port}), sessionId: sessionId); bool acked = await waitForDaemonResponse(); @@ -467,20 +449,20 @@ class SSHNPImpl implements SSHNP { try { bool success = false; String? errorMessage; - + Process? process; + SSHClient? client; switch (sshClient) { case SupportedSshClient.hostSsh: - (success, errorMessage) = await directSshViaExec(); + (success, errorMessage, process) = await directSshViaExec(); _doneCompleter.complete(); break; case SupportedSshClient.pureDart: - (success, errorMessage) = await directSshViaSSHClient(); + (success, errorMessage, client) = await directSshViaSSHClient(); break; } if (!success) { - errorMessage ??= - 'Failed to start ssh tunnel and / or forward local port $localPort'; + errorMessage ??= 'Failed to start ssh tunnel and / or forward local port $localPort'; return SSHNPFailed(errorMessage); } // All good - write the ssh command to stdout @@ -489,18 +471,20 @@ class SSHNPImpl implements SSHNP { remoteUsername: remoteUsername, host: 'localhost', privateKeyFileName: publicKeyFileName.replaceAll('.pub', ''), + sshProcess: process, + sshClient: client, ); } catch (e, s) { return SSHNPFailed('SSH Client failure : $e', e, s); } } - Future<(bool, String?)> directSshViaSSHClient() async { + Future<(bool, String?, SSHClient?)> directSshViaSSHClient() async { late final SSHSocket socket; try { socket = await SSHSocket.connect(host, _sshrvdPort); } catch (e) { - return (false, 'Failed to open socket to $host:$port : $e'); + return (false, 'Failed to open socket to $host:$port : $e', null); } late final SSHClient client; @@ -513,24 +497,19 @@ class SSHNPImpl implements SSHNP { ], keepAliveInterval: Duration(seconds: 15)); } catch (e) { - return ( - false, - 'Failed to create SSHClient for $username@$host:$port : $e' - ); + return (false, 'Failed to create SSHClient for $username@$host:$port : $e', null); } try { await client.authenticated; } catch (e) { - return (false, 'Failed to authenticate as $username@$host:$port : $e'); + return (false, 'Failed to authenticate as $username@$host:$port : $e', null); } int counter = 0; Future startForwarding( - {required int fLocalPort, - required String fRemoteHost, - required int fRemotePort}) async { + {required int fLocalPort, required String fRemoteHost, required int fRemotePort}) async { logger.info('Starting port forwarding' ' from port $fLocalPort on localhost' ' to $fRemoteHost:$fRemotePort on remote side'); @@ -554,14 +533,10 @@ class SSHNPImpl implements SSHNP { }, onDone: () { counter = 0; }); - return null; } // Start local forwarding to the remote sshd - await startForwarding( - fLocalPort: localPort, - fRemoteHost: 'localhost', - fRemotePort: remoteSshdPort); + await startForwarding(fLocalPort: localPort, fRemoteHost: 'localhost', fRemotePort: remoteSshdPort); if (addForwardsToTunnel) { var optionsSplitBySpace = localSshOptions.join(' ').split(' '); @@ -588,18 +563,13 @@ class SSHNPImpl implements SSHNP { int? fLocalPort = int.tryParse(args[0]); String fRemoteHost = args[1]; int? fRemotePort = int.tryParse(args[2]); - if (fLocalPort == null || - fRemoteHost.isEmpty || - fRemotePort == null) { + if (fLocalPort == null || fRemoteHost.isEmpty || fRemotePort == null) { logger.warning('localSshOptions has -L with bad args $argString'); continue; } // Start the forwarding - await startForwarding( - fLocalPort: fLocalPort, - fRemoteHost: fRemoteHost, - fRemotePort: fRemotePort); + await startForwarding(fLocalPort: fLocalPort, fRemoteHost: fRemoteHost, fRemotePort: fRemotePort); } } } @@ -618,10 +588,10 @@ class SSHNPImpl implements SSHNP { } }); - return (true, null); + return (true, null, client); } - Future<(bool, String?)> directSshViaExec() async { + Future<(bool, String?, Process?)> directSshViaExec() async { // If using exec then we can assume we're on something unix-y // So we can write the ephemeralPrivateKey to a tmp file, // set its permissions appropriately, and remove it after we've @@ -629,8 +599,7 @@ class SSHNPImpl implements SSHNP { var tmpFileName = '/tmp/ephemeral_$sessionId'; File tmpFile = File(tmpFileName); await tmpFile.create(recursive: true); - await tmpFile.writeAsString(ephemeralPrivateKey, - mode: FileMode.write, flush: true); + await tmpFile.writeAsString(ephemeralPrivateKey, mode: FileMode.write, flush: true); await Process.run('chmod', ['go-rwx', tmpFileName]); String argsString = '$remoteUsername@$host' @@ -660,8 +629,9 @@ class SSHNPImpl implements SSHNP { late int sshExitCode; final soutBuf = StringBuffer(); final serrBuf = StringBuffer(); + Process? process; try { - Process process = await Process.start('/usr/bin/ssh', args); + process = await Process.start('/usr/bin/ssh', args); process.stdout.listen((List l) { var s = utf8.decode(l); soutBuf.write(s); @@ -683,27 +653,24 @@ class SSHNPImpl implements SSHNP { String? errorMessage; if (sshExitCode != 0) { if (sshExitCode == 6464) { - logger.shout( - '$sessionId | Command timed out: /usr/bin/ssh ${args.join(' ')}'); + logger.shout('$sessionId | Command timed out: /usr/bin/ssh ${args.join(' ')}'); errorMessage = 'Failed to establish connection - timed out'; } else { logger.shout('$sessionId | Exit code $sshExitCode from' ' /usr/bin/ssh ${args.join(' ')}'); - errorMessage = - 'Failed to establish connection - exit code $sshExitCode'; + errorMessage = 'Failed to establish connection - exit code $sshExitCode'; } } - return (sshExitCode == 0, errorMessage); + return (sshExitCode == 0, errorMessage, process); } /// Identical to [legacyStartReverseSsh] except for the request notification Future startReverseSsh() async { // Connect to rendezvous point using background process. // sshnp (this program) can then exit without issue. - SSHRV sshrv = - sshrvGenerator(host, _sshrvdPort, localSshdPort: localSshdPort); - unawaited(sshrv.run()); + SSHRV sshrv = sshrvGenerator(host, _sshrvdPort, localSshdPort: localSshdPort); + Future sshrvResult = sshrv.run(); // send request to the daemon via notification await _notify( @@ -729,8 +696,7 @@ class SSHNPImpl implements SSHNP { bool acked = await waitForDaemonResponse(); await cleanUpAfterReverseSsh(this); if (!acked) { - return SSHNPFailed( - 'sshnp connection timeout: waiting for daemon response'); + return SSHNPFailed('sshnp connection timeout: waiting for daemon response'); } if (sshnpdAckErrors) { @@ -742,15 +708,15 @@ class SSHNPImpl implements SSHNP { remoteUsername: remoteUsername, host: 'localhost', privateKeyFileName: publicKeyFileName.replaceAll('.pub', ''), + sshrvResult: sshrvResult, ); } Future legacyStartReverseSsh() async { // Connect to rendezvous point using background process. // sshnp (this program) can then exit without issue. - SSHRV sshrv = - sshrvGenerator(host, _sshrvdPort, localSshdPort: localSshdPort); - unawaited(sshrv.run()); + SSHRV sshrv = sshrvGenerator(host, _sshrvdPort, localSshdPort: localSshdPort); + Future sshrvResult = sshrv.run(); // send request to the daemon via notification await _notify( @@ -780,6 +746,7 @@ class SSHNPImpl implements SSHNP { remoteUsername: remoteUsername, host: 'localhost', privateKeyFileName: publicKeyFileName.replaceAll('.pub', ''), + sshrvResult: sshrvResult, ); } @@ -812,8 +779,7 @@ class SSHNPImpl implements SSHNP { assertValidValue(daemonResponse, 'sessionId', String); assertValidValue(daemonResponse, 'ephemeralPrivateKey', String); } catch (e) { - logger.warning( - 'Failed to extract parameters from notification value "${notification.value}" with error : $e'); + logger.warning('Failed to extract parameters from notification value "${notification.value}" with error : $e'); sshnpdAck = true; sshnpdAckErrors = true; return; @@ -850,8 +816,7 @@ class SSHNPImpl implements SSHNP { /// @human:username.device.sshnp@daemon /// Is not called if remoteUserName was set via constructor Future fetchRemoteUserName() async { - AtKey userNameRecordID = - AtKey.fromString('$clientAtSign:username.$namespace$sshnpdAtSign'); + AtKey userNameRecordID = AtKey.fromString('$clientAtSign:username.$namespace$sshnpdAtSign'); try { remoteUsername = (await atClient.get(userNameRecordID)).value as String; } catch (e) { @@ -878,8 +843,7 @@ class SSHNPImpl implements SSHNP { ..ttl = 10000); await _notify(sendOurPublicKeyToSshnpd, toSshPublicKey); } catch (e) { - stderr.writeln( - "Error opening or validating public key file or sending to remote atSign: $e"); + stderr.writeln("Error opening or validating public key file or sending to remote atSign: $e"); await cleanUpAfterReverseSsh(this); rethrow; } @@ -899,8 +863,7 @@ class SSHNPImpl implements SSHNP { Future getHostAndPortFromSshrvd() async { atClient.notificationService - .subscribe( - regex: '$sessionId.${SSHRVD.namespace}@', shouldDecrypt: true) + .subscribe(regex: '$sessionId.${SSHRVD.namespace}@', shouldDecrypt: true) .listen((notification) async { String ipPorts = notification.value.toString(); List results = ipPorts.split(','); @@ -935,10 +898,7 @@ class SSHNPImpl implements SSHNP { } Future> _getAtKeysRemote( - {String? regex, - String? sharedBy, - String? sharedWith, - bool showHiddenKeys = false}) async { + {String? regex, String? sharedBy, String? sharedWith, bool showHiddenKeys = false}) async { var builder = ScanVerbBuilder() ..sharedWith = sharedWith ..sharedBy = sharedBy @@ -955,8 +915,7 @@ class SSHNPImpl implements SSHNP { } on InvalidSyntaxException { logger.severe('$key is not a well-formed key'); } on Exception catch (e) { - logger.severe( - 'Exception occurred: ${e.toString()}. Unable to form key $key'); + logger.severe('Exception occurred: ${e.toString()}. Unable to form key $key'); } }).toList(); } @@ -965,13 +924,11 @@ class SSHNPImpl implements SSHNP { } @override - Future<(Iterable, Iterable, Map)> - listDevices() async { + Future<(Iterable, Iterable, Map)> listDevices() async { // get all the keys device_info.*.sshnpd var scanRegex = 'device_info\\.$asciiMatcher\\.${SSHNPD.namespace}'; - var atKeys = - await _getAtKeysRemote(regex: scanRegex, sharedBy: sshnpdAtSign); + var atKeys = await _getAtKeysRemote(regex: scanRegex, sharedBy: sshnpdAtSign); var devices = {}; var heartbeats = {}; @@ -1035,11 +992,9 @@ class SSHNPImpl implements SSHNP { } /// This function sends a notification given an atKey and value - Future _notify(AtKey atKey, String value, - {String sessionId = ""}) async { - await atClient.notificationService - .notify(NotificationParams.forUpdate(atKey, value: value), - onSuccess: (notification) { + Future _notify(AtKey atKey, String value, {String sessionId = ""}) async { + await atClient.notificationService.notify(NotificationParams.forUpdate(atKey, value: value), + onSuccess: (notification) { logger.info('SUCCESS:$notification for: $sessionId with value: $value'); }, onError: (notification) { logger.info('ERROR:$notification'); diff --git a/packages/sshnoports/lib/sshnp/sshnp_result.dart b/packages/sshnoports/lib/sshnp/sshnp_result.dart index 7a9392f72..30e12b40b 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_result.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_result.dart @@ -33,11 +33,18 @@ class SSHCommand implements SSHNPCommandResult { final List sshOptions; + Future? sshrvResult; + Process? sshProcess; + SSHClient? sshClient; + SSHCommand.base({ required this.localPort, required this.remoteUsername, required this.host, this.privateKeyFileName, + this.sshrvResult, + this.sshProcess, + this.sshClient, }) : sshOptions = (shouldIncludePrivateKey(privateKeyFileName) ? _optionsWithPrivateKey : []); static bool shouldIncludePrivateKey(String? privateKeyFileName) => diff --git a/packages/sshnp_gui/lib/src/controllers/background_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/background_session_controller.dart index 7b09aed9a..d2a848f6d 100644 --- a/packages/sshnp_gui/lib/src/controllers/background_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/background_session_controller.dart @@ -3,26 +3,19 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; enum BackgroundSessionStatus { stopped, loading, running } final backgroundSessionFamilyController = - NotifierProviderFamily( + NotifierProviderFamily( BackgroundSessionFamilyController.new, ); -class BackgroundSession { - final String profileName; - BackgroundSessionStatus status = BackgroundSessionStatus.stopped; - - BackgroundSession(this.profileName); -} - -class BackgroundSessionFamilyController extends FamilyNotifier { +class BackgroundSessionFamilyController extends FamilyNotifier { @override - BackgroundSession build(String arg) { - return BackgroundSession(arg); + BackgroundSessionStatus build(String arg) { + return BackgroundSessionStatus.stopped; } - BackgroundSessionStatus get status => state.status; - - void setStatus(BackgroundSessionStatus status) => state.status = status; + void setStatus(BackgroundSessionStatus status) { + state = status; + } void start() => setStatus(BackgroundSessionStatus.loading); void endStartUp() => setStatus(BackgroundSessionStatus.running); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_menu_button.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_menu_button.dart index 1fd56d2b4..2192dd363 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_menu_button.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_menu_button.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:sshnp_gui/src/presentation/widgets/home_screen_actions/home_screen_action_callbacks.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_action_callbacks.dart'; import 'package:sshnp_gui/src/utility/sizes.dart'; class HomeScreenMenuButton extends ConsumerStatefulWidget { diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_actions.dart index 9b8fd8a92..9a4e71ac6 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_actions.dart @@ -1,3 +1,7 @@ +export 'profile_action_button.dart'; +export 'profile_action_callbacks.dart'; export 'profile_delete_action.dart'; +export 'profile_delete_dialog.dart'; +export 'profile_menu_button.dart'; export 'profile_run_action.dart'; -export 'profile_terminal_action.dart'; \ No newline at end of file +export 'profile_terminal_action.dart'; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index a751bd336..c24f8b0ea 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -1,7 +1,10 @@ +import 'dart:io'; + import 'package:at_client_mobile/at_client_mobile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:socket_connector/socket_connector.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; import 'package:sshnp_gui/src/controllers/background_session_controller.dart'; @@ -18,6 +21,7 @@ class ProfileRunAction extends ConsumerStatefulWidget { class _ProfileRunActionState extends ConsumerState { SSHNP? sshnp; + SSHNPResult? sshnpResult; @override void initState() { @@ -25,14 +29,7 @@ class _ProfileRunActionState extends ConsumerState { } Future onStart() async { - if (mounted) { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) => const Center(child: CircularProgressIndicator()), - ); - } - + ref.read(backgroundSessionFamilyController(widget.params.profileName!).notifier).start(); try { SSHNPParams params = SSHNPParams.merge( widget.params, @@ -49,31 +46,49 @@ class _ProfileRunActionState extends ConsumerState { ); await sshnp!.init(); - final sshnpResult = await sshnp!.run(); + sshnpResult = await sshnp!.run(); + + if (sshnpResult is SSHNPFailed) { + throw sshnpResult!; + } } catch (e) { if (mounted) { CustomSnackBar.error(content: e.toString()); } } finally { - if (mounted) { - context.pop(); - } + ref.read(backgroundSessionFamilyController(widget.params.profileName!).notifier).endStartUp(); } } Future onStop() async { - // TODO need to implement SSHNP.stop + if (sshnpResult is SSHCommand) { + (sshnpResult as SSHCommand).sshProcess?.kill(); // DirectSSHViaExec + (sshnpResult as SSHCommand).sshClient?.close(); // DirectSSHViaClient + var sshrvResult = await (sshnpResult as SSHCommand).sshrvResult; + if (sshrvResult is Process) sshrvResult.kill(); // SSHRV via local binary + if (sshrvResult is SocketConnector) sshrvResult.close(); // SSHRV via pure dart + } + ref.read(backgroundSessionFamilyController(widget.params.profileName!).notifier).stop(); } - static const Map _iconMap = { - BackgroundSessionStatus.stopped: Icon(Icons.play_arrow), - BackgroundSessionStatus.loading: CircularProgressIndicator(), - BackgroundSessionStatus.running: Icon(Icons.stop), - }; + static Widget getIconFromStatus(BackgroundSessionStatus status, BuildContext context) { + switch (status) { + case BackgroundSessionStatus.stopped: + return const Icon(Icons.play_arrow); + case BackgroundSessionStatus.running: + return const Icon(Icons.stop); + case BackgroundSessionStatus.loading: + return SizedBox( + width: IconTheme.of(context).size, + height: IconTheme.of(context).size, + child: const CircularProgressIndicator(), + ); + } + } @override Widget build(BuildContext context) { - final status = ref.watch(backgroundSessionFamilyController(widget.params.profileName!)).status; + final status = ref.watch(backgroundSessionFamilyController(widget.params.profileName!)); return ProfileActionButton( onPressed: () async { switch (status) { @@ -87,7 +102,7 @@ class _ProfileRunActionState extends ConsumerState { break; } }, - icon: _iconMap[status]!, + icon: getIconFromStatus(status, context), ); } } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart index f41cc86c9..8d149ad34 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_actions.dart'; -import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_menu_button.dart'; class ProfileBarActions extends StatelessWidget { final SSHNPParams params; @@ -11,7 +10,7 @@ class ProfileBarActions extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - // ProfileRunAction(params), + ProfileRunAction(params), ProfileTerminalAction(params), ProfileMenuButton(params.profileName!), ], diff --git a/packages/sshnp_gui/pubspec.lock b/packages/sshnp_gui/pubspec.lock index 6aba4aa3e..120a23a2d 100644 --- a/packages/sshnp_gui/pubspec.lock +++ b/packages/sshnp_gui/pubspec.lock @@ -1021,22 +1021,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.0" - share_plus_web: - dependency: transitive - description: - name: share_plus_web - sha256: eaef05fa8548b372253e772837dd1fbe4ce3aca30ea330765c945d7d4f7c9935 - url: "https://pub.dev" - source: hosted - version: "3.1.0" - share_plus_windows: - dependency: transitive - description: - name: share_plus_windows - sha256: "3a21515ae7d46988d42130cd53294849e280a5de6ace24bae6912a1bffd757d4" - url: "https://pub.dev" - source: hosted - version: "3.0.1" shared_preferences: dependency: "direct main" description: @@ -1139,13 +1123,14 @@ packages: source: sdk version: "0.0.99" socket_connector: - dependency: transitive + dependency: "direct main" description: - name: socket_connector - sha256: f461da716d74eb6fda80efea98244ef467363d69eb6bb307c43722389e70d7b1 - url: "https://pub.dev" - source: hosted - version: "1.0.10" + path: "." + ref: main + resolved-ref: "2efb79d3c223a62e4886690afb8fa82b3cb8a662" + url: "https://github.com/xavierchanth/socket_connector/" + source: git + version: "1.0.11" source_map_stack_trace: dependency: transitive description: diff --git a/packages/sshnp_gui/pubspec.yaml b/packages/sshnp_gui/pubspec.yaml index 96058d46c..bdf0e63e8 100644 --- a/packages/sshnp_gui/pubspec.yaml +++ b/packages/sshnp_gui/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: path: ^1.8.3 path_provider: ^2.0.11 shared_preferences: ^2.2.0 + socket_connector: ^1.0.10 sshnoports: path: ../sshnoports/ url_launcher: ^6.1.14 @@ -47,6 +48,10 @@ dependency_overrides: intl: ^0.17.0-nullsafety.2 # image: ^3.1.3 zxing2: ^0.2.0 + socket_connector: + git: + url: https://github.com/xavierchanth/socket_connector/ + ref: main flutter: uses-material-design: true generate: true From 31f4a02bb42c72da075ef96ac0c07b30b205c7b3 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 14:18:01 +0800 Subject: [PATCH 75/95] chore: remove unused import --- .../presentation/widgets/profile_actions/profile_run_action.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index c24f8b0ea..d1566cf5e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:at_client_mobile/at_client_mobile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; import 'package:socket_connector/socket_connector.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshrv/sshrv.dart'; From d4d16dffa95ccd6648f0748c32e851323964cde5 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 14:41:42 +0800 Subject: [PATCH 76/95] chore: cleanup --- packages/sshnoports/lib/sshnp/sshnp_params.dart | 1 - .../widgets/profile_actions/profile_run_action.dart | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_params.dart b/packages/sshnoports/lib/sshnp/sshnp_params.dart index 3fc2fbc00..447e4713c 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_params.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_params.dart @@ -303,7 +303,6 @@ class SSHNPPartialParams { factory SSHNPPartialParams.fromJson(String json) => SSHNPPartialParams.fromMap(jsonDecode(json)); factory SSHNPPartialParams.fromMap(Map args) { - print(args['local-ssh-options']); return SSHNPPartialParams( profileName: args['profile-name'], clientAtSign: args['from'], diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index d1566cf5e..492b6c6a4 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -50,12 +50,13 @@ class _ProfileRunActionState extends ConsumerState { if (sshnpResult is SSHNPFailed) { throw sshnpResult!; } + ref.read(backgroundSessionFamilyController(widget.params.profileName!).notifier).endStartUp(); } catch (e) { + Future stop = onStop(); if (mounted) { CustomSnackBar.error(content: e.toString()); } - } finally { - ref.read(backgroundSessionFamilyController(widget.params.profileName!).notifier).endStartUp(); + await stop; } } From 8e3d87c9bc41ec78343aa37768e6b0c6fee8c7ae Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 15:18:48 +0800 Subject: [PATCH 77/95] chore: cleanup error messages --- packages/sshnoports/lib/common/utils.dart | 106 ++++++------------ packages/sshnoports/lib/sshnp/sshnp_impl.dart | 78 +++++++++---- 2 files changed, 89 insertions(+), 95 deletions(-) diff --git a/packages/sshnoports/lib/common/utils.dart b/packages/sshnoports/lib/common/utils.dart index 3ccd15b61..9c7786e58 100644 --- a/packages/sshnoports/lib/common/utils.dart +++ b/packages/sshnoports/lib/common/utils.dart @@ -56,8 +56,7 @@ bool checkNonAscii(String test) { String getDefaultAtKeysFilePath(String homeDirectory, String? atSign) { if (atSign == null) return ''; - return '$homeDirectory/.atsign/keys/${atSign}_key.atKeys' - .replaceAll('/', Platform.pathSeparator); + return '$homeDirectory/.atsign/keys/${atSign}_key.atKeys'.replaceAll('/', Platform.pathSeparator); } String getDefaultSshDirectory(String homeDirectory) { @@ -107,55 +106,33 @@ Future atSignIsActivated(final AtClient atClient, String atSign) async { void assertValidValue(Map m, String k, Type t) { var v = m[k]; if (v == null || v.runtimeType != t) { - throw ArgumentError( - 'Parameter $k should be a $t but is actually a ${v.runtimeType} with value $v'); + throw ArgumentError('Parameter $k should be a $t but is actually a ${v.runtimeType} with value $v'); } } Future<(String, String)> generateSshKeys( - {required bool rsa, - required String sessionId, - String? sshHomeDirectory}) async { - sshHomeDirectory ??= - getDefaultSshDirectory(getHomeDirectory(throwIfNull: true)!); + {required bool rsa, required String sessionId, String? sshHomeDirectory}) async { + sshHomeDirectory ??= getDefaultSshDirectory(getHomeDirectory(throwIfNull: true)!); if (!Directory(sshHomeDirectory).existsSync()) { Directory(sshHomeDirectory).createSync(); } if (rsa) { - await Process.run('ssh-keygen', - ['-t', 'rsa', '-b', '4096', '-f', '${sessionId}_sshnp', '-q', '-N', ''], + await Process.run('ssh-keygen', ['-t', 'rsa', '-b', '4096', '-f', '${sessionId}_sshnp', '-q', '-N', ''], workingDirectory: sshHomeDirectory); } else { - await Process.run( - 'ssh-keygen', - [ - '-t', - 'ed25519', - '-a', - '100', - '-f', - '${sessionId}_sshnp', - '-q', - '-N', - '' - ], + await Process.run('ssh-keygen', ['-t', 'ed25519', '-a', '100', '-f', '${sessionId}_sshnp', '-q', '-N', ''], workingDirectory: sshHomeDirectory); } - String sshPublicKey = - await File('$sshHomeDirectory${sessionId}_sshnp.pub').readAsString(); - String sshPrivateKey = - await File('$sshHomeDirectory${sessionId}_sshnp').readAsString(); + String sshPublicKey = await File('$sshHomeDirectory${sessionId}_sshnp.pub').readAsString(); + String sshPrivateKey = await File('$sshHomeDirectory${sessionId}_sshnp').readAsString(); return (sshPublicKey, sshPrivateKey); } Future addEphemeralKeyToAuthorizedKeys( - {required String sshPublicKey, - required int localSshdPort, - String sessionId = '', - String permissions = ''}) async { + {required String sshPublicKey, required int localSshdPort, String sessionId = '', String permissions = ''}) async { // Check to see if the ssh public key looks like one! if (!sshPublicKey.startsWith('ssh-')) { throw ('$sshPublicKey does not look like a public key'); @@ -163,8 +140,7 @@ Future addEphemeralKeyToAuthorizedKeys( String homeDirectory = getHomeDirectory(throwIfNull: true)!; - var sshHomeDirectory = - '$homeDirectory/.ssh/'.replaceAll('/', Platform.pathSeparator); + var sshHomeDirectory = '$homeDirectory/.ssh/'.replaceAll('/', Platform.pathSeparator); if (!Directory(sshHomeDirectory).existsSync()) { Directory(sshHomeDirectory).createSync(); } @@ -184,23 +160,22 @@ Future addEphemeralKeyToAuthorizedKeys( } // Set up a safe authorized_keys file, for the ssh tunnel await authKeys.writeAsString( - 'command="echo \\"ssh session complete\\";sleep 20"' - ',PermitOpen="localhost:$localSshdPort"' - '$permissions' - ' ' - '${sshPublicKey.trim()}' - ' ' - 'sshnp_ephemeral_$sessionId\n', - mode: FileMode.append); + 'command="echo \\"ssh session complete\\";sleep 20"' + ',PermitOpen="localhost:$localSshdPort"' + '$permissions' + ' ' + '${sshPublicKey.trim()}' + ' ' + 'sshnp_ephemeral_$sessionId\n', + mode: FileMode.append, + ); } } -Future removeEphemeralKeyFromAuthorizedKeys( - String sessionId, AtSignLogger logger, +Future removeEphemeralKeyFromAuthorizedKeys(String sessionId, AtSignLogger logger, {String? sshHomeDirectory}) async { try { - sshHomeDirectory ??= - getDefaultSshDirectory(getHomeDirectory(throwIfNull: true)!); + sshHomeDirectory ??= getDefaultSshDirectory(getHomeDirectory(throwIfNull: true)!); final File file = File('${sshHomeDirectory}authorized_keys'); logger.info('Removing ephemeral key for session $sessionId' ' from ${file.absolute.path}'); @@ -219,8 +194,7 @@ Future removeEphemeralKeyFromAuthorizedKeys( String signAndWrapAndJsonEncode(AtClient atClient, Map payload) { Map envelope = {'payload': payload}; - final AtSigningInput signingInput = AtSigningInput(jsonEncode(payload)) - ..signingMode = AtSigningMode.data; + final AtSigningInput signingInput = AtSigningInput(jsonEncode(payload))..signingMode = AtSigningMode.data; final AtSigningResult sr = atClient.atChops!.sign(signingInput); final String signature = sr.result.toString(); @@ -230,16 +204,14 @@ String signAndWrapAndJsonEncode(AtClient atClient, Map payload) { return jsonEncode(envelope); } -Future verifyEnvelopeSignature(AtClient atClient, String requestingAtsign, - AtSignLogger logger, Map envelope) async { +Future verifyEnvelopeSignature( + AtClient atClient, String requestingAtsign, AtSignLogger logger, Map envelope) async { final String signature = envelope['signature']; Map payload = envelope['payload']; final hashingAlgo = HashingAlgoType.values.byName(envelope['hashingAlgo']); final signingAlgo = SigningAlgoType.values.byName(envelope['signingAlgo']); - final pk = await getLocallyCachedPK(atClient, requestingAtsign, - useFileStorage: true); - AtSigningVerificationInput input = AtSigningVerificationInput( - jsonEncode(payload), base64Decode(signature), pk) + final pk = await getLocallyCachedPK(atClient, requestingAtsign, useFileStorage: true); + AtSigningVerificationInput input = AtSigningVerificationInput(jsonEncode(payload), base64Decode(signature), pk) ..signingMode = AtSigningMode.data ..signingAlgoType = signingAlgo ..hashingAlgoType = hashingAlgo; @@ -264,12 +236,10 @@ Future verifyEnvelopeSignature(AtClient atClient, String requestingAtsign, /// `~/.atsign/sshnp/cached_pks/alice` /// /// Note that for storage, the leading `@` in the atSign is stripped off. -Future getLocallyCachedPK(AtClient atClient, String atSign, - {bool useFileStorage = true}) async { +Future getLocallyCachedPK(AtClient atClient, String atSign, {bool useFileStorage = true}) async { atSign = AtUtils.fixAtSign(atSign); - String? cachedPK = - await _fetchFromLocalPKCache(atClient, atSign, useFileStorage); + String? cachedPK = await _fetchFromLocalPKCache(atClient, atSign, useFileStorage); if (cachedPK != null) { return cachedPK; } @@ -285,8 +255,7 @@ Future getLocallyCachedPK(AtClient atClient, String atSign, return av.value; } -Future _fetchFromLocalPKCache( - AtClient atClient, String atSign, bool useFileStorage) async { +Future _fetchFromLocalPKCache(AtClient atClient, String atSign, bool useFileStorage) async { String dontAtMe = atSign.substring(1); if (useFileStorage) { String fn = '${getHomeDirectory(throwIfNull: true)}' @@ -301,8 +270,7 @@ Future _fetchFromLocalPKCache( } else { late final AtValue av; try { - av = await atClient.get(AtKey.fromString( - 'local:$dontAtMe.cached_pks.sshnp@${atClient.getCurrentAtSign()!}')); + av = await atClient.get(AtKey.fromString('local:$dontAtMe.cached_pks.sshnp@${atClient.getCurrentAtSign()!}')); return av.value; } on AtKeyNotFoundException catch (_) { return null; @@ -310,15 +278,12 @@ Future _fetchFromLocalPKCache( } } -Future _storeToLocalPKCache( - String pk, AtClient atClient, String atSign, bool useFileStorage) async { +Future _storeToLocalPKCache(String pk, AtClient atClient, String atSign, bool useFileStorage) async { String dontAtMe = atSign.substring(1); if (useFileStorage) { String dirName = - '${getHomeDirectory(throwIfNull: true)}/.atsign/sshnp/cached_pks' - .replaceAll('/', Platform.pathSeparator); - String fileName = - '$dirName/$dontAtMe'.replaceAll('/', Platform.pathSeparator); + '${getHomeDirectory(throwIfNull: true)}/.atsign/sshnp/cached_pks'.replaceAll('/', Platform.pathSeparator); + String fileName = '$dirName/$dontAtMe'.replaceAll('/', Platform.pathSeparator); File f = File(fileName); if (!await f.exists()) { @@ -328,10 +293,7 @@ Future _storeToLocalPKCache( await f.writeAsString('$pk\n'); return true; } else { - await atClient.put( - AtKey.fromString( - 'local:$dontAtMe.cached_pks.sshnp@${atClient.getCurrentAtSign()!}'), - pk); + await atClient.put(AtKey.fromString('local:$dontAtMe.cached_pks.sshnp@${atClient.getCurrentAtSign()!}'), pk); return true; } } diff --git a/packages/sshnoports/lib/sshnp/sshnp_impl.dart b/packages/sshnoports/lib/sshnp/sshnp_impl.dart index 1c92555ed..d9f7fc764 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_impl.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_impl.dart @@ -198,7 +198,16 @@ class SSHNPImpl implements SSHNP { sshHomeDirectory = getDefaultSshDirectory(homeDirectory); if (!Directory(sshHomeDirectory).existsSync()) { - Directory(sshHomeDirectory).createSync(); + try { + Directory(sshHomeDirectory).createSync(); + } catch (e, s) { + throw SSHNPFailed( + 'Unable to create ssh home directory $sshHomeDirectory\n' + 'hint: try manually creating $sshHomeDirectory and re-running sshnp', + e, + s, + ); + } } // previously, the default value for sendSshPublicKey was 'false' instead of '' @@ -296,11 +305,14 @@ class SSHNPImpl implements SSHNP { } return sshnp; - } catch (e) { + } catch (e, s) { printVersion(); stdout.writeln(SSHNPPartialParams.parser.usage); stderr.writeln('\n$e'); - rethrow; + if (e is SSHNPFailed) { + rethrow; + } + throw SSHNPFailed('Unknown failure:\n$e', e, s); } } @@ -323,9 +335,12 @@ class SSHNPImpl implements SSHNP { // determine the ssh direction direct = useDirectSsh(legacyDaemon, host); - - if (!(await atSignIsActivated(atClient, sshnpdAtSign))) { - throw ('sshnpd atSign $sshnpdAtSign is not activated.'); + try { + if (!(await atSignIsActivated(atClient, sshnpdAtSign))) { + throw ('Device address $sshnpdAtSign is not activated.'); + } + } catch (e, s) { + throw SSHNPFailed('Device address $sshnpdAtSign does not exist or is not activated.', e, s); } logger.info('Subscribing to notifications on $sessionId.$namespace@'); @@ -335,20 +350,24 @@ class SSHNPImpl implements SSHNP { .listen(handleSshnpdResponses); if (publicKeyFileName.isNotEmpty && !File(publicKeyFileName).existsSync()) { - throw ('\n Unable to find ssh public key file : $publicKeyFileName'); + throw ('Unable to find ssh public key file : $publicKeyFileName'); } if (publicKeyFileName.isNotEmpty && !File(publicKeyFileName.replaceAll('.pub', '')).existsSync()) { - throw ('\n Unable to find matching ssh private key for public key : $publicKeyFileName'); + throw ('Unable to find matching ssh private key for public key : $publicKeyFileName'); } remoteUsername ?? await fetchRemoteUserName(); // find a spare local port if (localPort == 0) { - ServerSocket serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); - localPort = serverSocket.port; - await serverSocket.close(); + try { + ServerSocket serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + localPort = serverSocket.port; + await serverSocket.close(); + } catch (e, s) { + throw SSHNPFailed('Unable to find a spare local port', e, s); + } } await sharePublicKeyWithSshnpdIfRequired(); @@ -362,13 +381,22 @@ class SSHNPImpl implements SSHNP { // 1) generate some ephemeral keys for the daemon to use to ssh back to us // 2) if legacy then we share the private key via its own notification if (!direct) { - var (String ephemeralPublicKey, String ephemeralPrivateKey) = - await generateSshKeys(rsa: rsa, sessionId: sessionId, sshHomeDirectory: sshHomeDirectory); - sshPublicKey = ephemeralPublicKey; - sshPrivateKey = ephemeralPrivateKey; + try { + var (String ephemeralPublicKey, String ephemeralPrivateKey) = + await generateSshKeys(rsa: rsa, sessionId: sessionId, sshHomeDirectory: sshHomeDirectory); - await addEphemeralKeyToAuthorizedKeys( - sshPublicKey: sshPublicKey, localSshdPort: localSshdPort, sessionId: sessionId); + sshPublicKey = ephemeralPublicKey; + sshPrivateKey = ephemeralPrivateKey; + } catch (e, s) { + throw SSHNPFailed('Failed to generate ephemeral keypair', e, s); + } + + try { + await addEphemeralKeyToAuthorizedKeys( + sshPublicKey: sshPublicKey, localSshdPort: localSshdPort, sessionId: sessionId); + } catch (e, s) { + throw SSHNPFailed('Failed to add ephemeral key to authorized_keys', e, s); + } if (legacyDaemon) { await sharePrivateKeyWithSshnpd(); @@ -819,10 +847,14 @@ class SSHNPImpl implements SSHNP { AtKey userNameRecordID = AtKey.fromString('$clientAtSign:username.$namespace$sshnpdAtSign'); try { remoteUsername = (await atClient.get(userNameRecordID)).value as String; - } catch (e) { - stderr.writeln("Device \"$device\" unknown, or username not shared "); + } catch (e, s) { + stderr.writeln("Device \"$device\" unknown, or username not shared"); await cleanUpAfterReverseSsh(this); - rethrow; + throw SSHNPFailed( + "Device unknown, or username not shared\n" + "hint: make sure the device shares username or set remote username manually", + e, + s); } } @@ -842,10 +874,10 @@ class SSHNPImpl implements SSHNP { ..ttr = -1 ..ttl = 10000); await _notify(sendOurPublicKeyToSshnpd, toSshPublicKey); - } catch (e) { + } catch (e, s) { stderr.writeln("Error opening or validating public key file or sending to remote atSign: $e"); await cleanUpAfterReverseSsh(this); - rethrow; + throw SSHNPFailed('Error opening or validating public key file or sending to remote atSign', e, s); } } @@ -892,7 +924,7 @@ class SSHNPImpl implements SSHNP { if (counter == 100) { await cleanUpAfterReverseSsh(this); stderr.writeln('sshnp: connection timeout to sshrvd $host service'); - throw ('sshnp: connection timeout to sshrvd $host service'); + throw ('Connection timeout to sshrvd $host service\nhint: make sure host is valid and online'); } } } From a2f2554d3117fa6e4e1124e0302363e025b3c53c Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 16:14:34 +0800 Subject: [PATCH 78/95] chore: change 3.5.0 references to 3.4.0 --- packages/sshnoports/lib/sshnp/sshnp_arg.dart | 2 +- packages/sshnoports/lib/sshnpd/sshnpd_impl.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_arg.dart b/packages/sshnoports/lib/sshnp/sshnp_arg.dart index 6c566f9d8..5e280a0d2 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_arg.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_arg.dart @@ -157,7 +157,7 @@ class SSHNPArg { const SSHNPArg( name: 'legacy-daemon', defaultsTo: SSHNP.defaultLegacyDaemon, - help: 'Request is to a legacy (< 3.5.0) noports daemon', + help: 'Request is to a legacy (< 3.4.0) noports daemon', format: ArgFormat.flag, ), const SSHNPArg( diff --git a/packages/sshnoports/lib/sshnpd/sshnpd_impl.dart b/packages/sshnoports/lib/sshnpd/sshnpd_impl.dart index 5e2365a56..999aae258 100644 --- a/packages/sshnoports/lib/sshnpd/sshnpd_impl.dart +++ b/packages/sshnoports/lib/sshnpd/sshnpd_impl.dart @@ -250,7 +250,7 @@ class SSHNPDImpl implements SSHNPD { break; case 'ssh_request': - logger.info('>=3.5.0 request for ssh received from ${notification.from}' + logger.info('>=3.4.0 request for ssh received from ${notification.from}' ' ( $notification )'); _handleSshRequestNotification(notification); break; From d93aeab9cb906622563ad3d7da9d2218939efc6a Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 16:14:58 +0800 Subject: [PATCH 79/95] chore: add a hint to the SSHNPFailed for daemon timeouts --- packages/sshnoports/lib/sshnp/sshnp_impl.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_impl.dart b/packages/sshnoports/lib/sshnp/sshnp_impl.dart index d9f7fc764..a9f174dc9 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_impl.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_impl.dart @@ -462,7 +462,7 @@ class SSHNPImpl implements SSHNP { bool acked = await waitForDaemonResponse(); if (!acked) { - return SSHNPFailed('sshnp timed out: waiting for daemon response'); + return SSHNPFailed('sshnp timed out: waiting for daemon response\nhint: make sure the device is online'); } if (sshnpdAckErrors) { @@ -762,7 +762,7 @@ class SSHNPImpl implements SSHNP { bool acked = await waitForDaemonResponse(); await cleanUpAfterReverseSsh(this); if (!acked) { - return SSHNPFailed('sshnp timed out: waiting for daemon response'); + return SSHNPFailed('sshnp timed out: waiting for daemon response\nhint: make sure the device is online'); } if (sshnpdAckErrors) { From ac49b06474888d76a55c2a65a8f392956cc20128 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 17:07:14 +0800 Subject: [PATCH 80/95] feat: add localSshOptions to SSHCommand.base --- packages/sshnoports/lib/sshnp/sshnp_impl.dart | 3 +++ packages/sshnoports/lib/sshnp/sshnp_result.dart | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_impl.dart b/packages/sshnoports/lib/sshnp/sshnp_impl.dart index a9f174dc9..2d57bb86a 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_impl.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_impl.dart @@ -499,6 +499,7 @@ class SSHNPImpl implements SSHNP { remoteUsername: remoteUsername, host: 'localhost', privateKeyFileName: publicKeyFileName.replaceAll('.pub', ''), + localSshOptions: (addForwardsToTunnel) ? localSshOptions : null, sshProcess: process, sshClient: client, ); @@ -736,6 +737,7 @@ class SSHNPImpl implements SSHNP { remoteUsername: remoteUsername, host: 'localhost', privateKeyFileName: publicKeyFileName.replaceAll('.pub', ''), + localSshOptions: (addForwardsToTunnel) ? localSshOptions : null, sshrvResult: sshrvResult, ); } @@ -774,6 +776,7 @@ class SSHNPImpl implements SSHNP { remoteUsername: remoteUsername, host: 'localhost', privateKeyFileName: publicKeyFileName.replaceAll('.pub', ''), + localSshOptions: (addForwardsToTunnel) ? localSshOptions : null, sshrvResult: sshrvResult, ); } diff --git a/packages/sshnoports/lib/sshnp/sshnp_result.dart b/packages/sshnoports/lib/sshnp/sshnp_result.dart index 30e12b40b..51f48d97d 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_result.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_result.dart @@ -41,11 +41,15 @@ class SSHCommand implements SSHNPCommandResult { required this.localPort, required this.remoteUsername, required this.host, + List? localSshOptions, this.privateKeyFileName, this.sshrvResult, this.sshProcess, this.sshClient, - }) : sshOptions = (shouldIncludePrivateKey(privateKeyFileName) ? _optionsWithPrivateKey : []); + }) : sshOptions = [ + if (shouldIncludePrivateKey(privateKeyFileName)) ..._optionsWithPrivateKey, + ...(localSshOptions ?? []) + ]; static bool shouldIncludePrivateKey(String? privateKeyFileName) => privateKeyFileName != null && privateKeyFileName.isNotEmpty; From 387ddbf5a6b85cbc8a8bee98af8f120dc41f07d2 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 17:13:52 +0800 Subject: [PATCH 81/95] chore: set version to 4.0.0-rc.4 --- packages/sshnoports/lib/version.dart | 2 +- packages/sshnoports/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sshnoports/lib/version.dart b/packages/sshnoports/lib/version.dart index 39039f0fd..ec237c751 100644 --- a/packages/sshnoports/lib/version.dart +++ b/packages/sshnoports/lib/version.dart @@ -1,7 +1,7 @@ import 'dart:io'; // Note: if you update this version also update pubspec.yaml -const String version = "4.0.0-rc.3"; +const String version = "4.0.0-rc.4"; /// Print version number void printVersion() { diff --git a/packages/sshnoports/pubspec.yaml b/packages/sshnoports/pubspec.yaml index 6550b9c22..37077a4f9 100644 --- a/packages/sshnoports/pubspec.yaml +++ b/packages/sshnoports/pubspec.yaml @@ -3,7 +3,7 @@ description: Encrypted/Secure control plane for ssh/d or other commands in the f # NOTE: If you update the version number here, you # must also update it in version.dart -version: 4.0.0-rc.3 +version: 4.0.0-rc.4 homepage: https://docs.atsign.com/ From 5d2a4c6703baf24af92671db82054d6f03bbec60 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 18:48:03 +0800 Subject: [PATCH 82/95] fix: resize computation --- .../lib/src/controllers/terminal_session_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart index e9ab17296..47e2c51dc 100644 --- a/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/terminal_session_controller.dart @@ -143,7 +143,7 @@ class TerminalSessionFamilyController extends FamilyNotifier Date: Thu, 14 Sep 2023 18:55:27 +0800 Subject: [PATCH 83/95] feat: force legacy daemon to false --- .../widgets/profile_actions/profile_run_action.dart | 3 ++- .../widgets/profile_actions/profile_terminal_action.dart | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index 492b6c6a4..52afa8181 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -33,8 +33,9 @@ class _ProfileRunActionState extends ConsumerState { SSHNPParams params = SSHNPParams.merge( widget.params, SSHNPPartialParams( - idleTimeout: 60, + idleTimeout: 120, // 120 / 60 = 2 minutes addForwardsToTunnel: true, + legacyDaemon: false, ), ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart index dcfa84bb2..067409f2e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart @@ -29,8 +29,15 @@ class _ProfileTerminalActionState extends ConsumerState { } try { - final sshnp = await SSHNP.fromParams( + SSHNPParams params = SSHNPParams.merge( widget.params, + SSHNPPartialParams( + legacyDaemon: false, + ), + ); + + final sshnp = await SSHNP.fromParams( + params, atClient: AtClientManager.getInstance().atClient, sshrvGenerator: SSHRV.pureDart, ); From 0d4879a07ef0459ffcdc1aabb79c2c274f9cae40 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 19:19:24 +0800 Subject: [PATCH 84/95] feat: improve form consistency --- .../profile_form/custom_text_form_field.dart | 6 +- .../widgets/profile_form/profile_form.dart | 64 +++++++++++++------ 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/custom_text_form_field.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/custom_text_form_field.dart index 41b3244ef..407395329 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/custom_text_form_field.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/custom_text_form_field.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; class CustomTextFormField extends StatelessWidget { + static const defaultWidth = 192.0; + static const defaultHeight = 33.0; const CustomTextFormField({ super.key, required this.labelText, @@ -8,8 +10,8 @@ class CustomTextFormField extends StatelessWidget { this.validator, this.onChanged, this.hintText, - this.width = 192, - this.height = 33, + this.width = defaultWidth, + this.height = defaultHeight, }); final String labelText; diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index f99062e3e..039a5e4c2 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -69,6 +69,7 @@ class _ProfileFormState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTextFormField( initialValue: oldConfig.profileName, @@ -94,6 +95,7 @@ class _ProfileFormState extends ConsumerState { ), gapH10, Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTextFormField( initialValue: oldConfig.sshnpdAtSign ?? '', @@ -118,6 +120,7 @@ class _ProfileFormState extends ConsumerState { ), gapH10, Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTextFormField( initialValue: oldConfig.sendSshPublicKey, @@ -126,14 +129,16 @@ class _ProfileFormState extends ConsumerState { newConfig, SSHNPPartialParams(sendSshPublicKey: value), ), - validator: FormValidator.validateRequiredField, ), gapW8, - Row( - children: [ - Text(strings.rsa), - gapW8, - Switch( + SizedBox( + width: CustomTextFormField.defaultWidth, + height: CustomTextFormField.defaultHeight, + child: Row( + children: [ + Text(strings.rsa), + gapW8, + Switch( value: newConfig.rsa ?? oldConfig.rsa, onChanged: (newValue) { setState(() { @@ -142,13 +147,16 @@ class _ProfileFormState extends ConsumerState { SSHNPPartialParams(rsa: newValue), ); }); - }), - ], + }, + ), + ], + ), ), ], ), gapH10, Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTextFormField( initialValue: oldConfig.remoteUsername ?? '', @@ -173,6 +181,7 @@ class _ProfileFormState extends ConsumerState { ), gapH10, Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTextFormField( initialValue: oldConfig.localPort.toString(), @@ -198,7 +207,8 @@ class _ProfileFormState extends ConsumerState { initialValue: oldConfig.localSshOptions.join(','), hintText: strings.localSshOptionsHint, labelText: strings.localSshOptions, - width: 192 * 2 + 10, + //Double the width of the text field (+8 for the gapW8) + width: CustomTextFormField.defaultWidth * 2 + 8, onChanged: (value) => newConfig = SSHNPPartialParams.merge( newConfig, SSHNPPartialParams(localSshOptions: value.split(',')), @@ -206,6 +216,7 @@ class _ProfileFormState extends ConsumerState { ), gapH10, Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTextFormField( initialValue: oldConfig.atKeysFilePath, @@ -228,22 +239,33 @@ class _ProfileFormState extends ConsumerState { ), gapH10, Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(strings.verbose), - gapW8, - Switch( - value: newConfig.verbose ?? oldConfig.verbose, - onChanged: (newValue) { - setState(() { - newConfig = SSHNPPartialParams.merge( - newConfig, - SSHNPPartialParams(verbose: newValue), - ); - }); - }), + SizedBox( + width: CustomTextFormField.defaultWidth, + height: CustomTextFormField.defaultHeight, + child: Row( + children: [ + Text(strings.verbose), + gapW8, + Switch( + value: newConfig.verbose ?? oldConfig.verbose, + onChanged: (newValue) { + setState(() { + newConfig = SSHNPPartialParams.merge( + newConfig, + SSHNPPartialParams(verbose: newValue), + ); + }); + }, + ), + ], + ), + ), ], ), Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ ElevatedButton( onPressed: () => onSubmit(oldConfig, newConfig), From 651fef43196b8c69289c2220a3a313d11e0cf68a Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 19:46:31 +0800 Subject: [PATCH 85/95] fix: better handle failed profile editing --- .../src/controllers/config_controller.dart | 31 ++++++++++++++++--- .../profile_delete_dialog.dart | 2 +- .../widgets/profile_form/profile_form.dart | 8 ++--- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/sshnp_gui/lib/src/controllers/config_controller.dart b/packages/sshnp_gui/lib/src/controllers/config_controller.dart index fdfd12a39..403bbd3f3 100644 --- a/packages/sshnp_gui/lib/src/controllers/config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/config_controller.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'package:at_client_mobile/at_client_mobile.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sshnoports/sshnp/sshnp.dart'; import 'package:sshnoports/sshnp/config_repository/config_key_repository.dart'; +import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; enum ConfigFileWriteState { create, update } @@ -81,8 +83,12 @@ class ConfigFamilyController extends AutoDisposeFamilyAsyncNotifier putConfig(SSHNPParams params) async { + Future putConfig(SSHNPParams params, {String? oldProfileName, BuildContext? context}) async { AtClient atClient = AtClientManager.getInstance().atClient; + SSHNPParams oldParams = state.value ?? SSHNPParams.empty(); + if (oldProfileName != null) { + ref.read(configFamilyController(oldProfileName).notifier).deleteConfig(context: context); + } if (params.clientAtSign != atClient.getCurrentAtSign()) { params = SSHNPParams.merge( params, @@ -91,12 +97,27 @@ class ConfigFamilyController extends AutoDisposeFamilyAsyncNotifier deleteConfig() async { - await ConfigKeyRepository.deleteParams(arg, atClient: AtClientManager.getInstance().atClient); - ref.read(configListController.notifier).remove(arg); + Future deleteConfig({BuildContext? context}) async { + try { + await ConfigKeyRepository.deleteParams(arg, atClient: AtClientManager.getInstance().atClient); + ref.read(configListController.notifier).remove(arg); + state = AsyncValue.error('SSHNPParams has been disposed', StackTrace.current); + } catch (e) { + if (context?.mounted ?? false) { + CustomSnackBar.error(content: 'Failed to delete profile: $arg'); + } + } } } diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_dialog.dart index ec115ceb5..53f1539aa 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_delete_dialog.dart @@ -48,7 +48,7 @@ class ProfileDeleteDialog extends ConsumerWidget { ), ElevatedButton( onPressed: () async { - await ref.read(configFamilyController(profileName).notifier).deleteConfig(); + await ref.read(configFamilyController(profileName).notifier).deleteConfig(context: context); if (context.mounted) Navigator.of(context).pop(); }, style: Theme.of(context).elevatedButtonTheme.style!.copyWith( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index 039a5e4c2..8a3e81f4d 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -37,15 +37,15 @@ class _ProfileFormState extends ConsumerState { oldConfig.profileName!.isNotEmpty && newConfig.profileName != oldConfig.profileName; SSHNPParams config = SSHNPParams.merge(oldConfig, newConfig); + if (rename) { // delete old config file and write the new one - await ref.read(configFamilyController(oldConfig.profileName!).notifier).deleteConfig(); - await controller.putConfig(config); + await controller.putConfig(config, oldProfileName: oldConfig.profileName!, context: context); } else { // create new config file - await controller.putConfig(config); + await controller.putConfig(config, context: context); } - if (context.mounted) { + if (mounted) { ref.read(navigationRailController.notifier).setRoute(AppRoute.home); context.pushReplacementNamed(AppRoute.home.name); } From 1336e4a18dab3b60d8714780af98d10ecb73fe29 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 19:49:04 +0800 Subject: [PATCH 86/95] fix: note that legacy daemons are actually <4.0.0 not <3.4.0 --- packages/sshnoports/lib/sshnp/sshnp_arg.dart | 2 +- packages/sshnoports/lib/sshnpd/sshnpd_impl.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_arg.dart b/packages/sshnoports/lib/sshnp/sshnp_arg.dart index 5e280a0d2..e9ca42ea2 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_arg.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_arg.dart @@ -157,7 +157,7 @@ class SSHNPArg { const SSHNPArg( name: 'legacy-daemon', defaultsTo: SSHNP.defaultLegacyDaemon, - help: 'Request is to a legacy (< 3.4.0) noports daemon', + help: 'Request is to a legacy (< 4.0.0) noports daemon', format: ArgFormat.flag, ), const SSHNPArg( diff --git a/packages/sshnoports/lib/sshnpd/sshnpd_impl.dart b/packages/sshnoports/lib/sshnpd/sshnpd_impl.dart index 999aae258..698f5b4ca 100644 --- a/packages/sshnoports/lib/sshnpd/sshnpd_impl.dart +++ b/packages/sshnoports/lib/sshnpd/sshnpd_impl.dart @@ -240,7 +240,7 @@ class SSHNPDImpl implements SSHNPD { case 'sshd': logger.info( - '<3.4.0 request for (reverse) ssh received from ${notification.from}' + '<4.0.0 request for (reverse) ssh received from ${notification.from}' ' ( notification id : ${notification.id} )'); _handleLegacySshRequestNotification(notification); break; @@ -250,7 +250,7 @@ class SSHNPDImpl implements SSHNPD { break; case 'ssh_request': - logger.info('>=3.4.0 request for ssh received from ${notification.from}' + logger.info('>=4.0.0 request for ssh received from ${notification.from}' ' ( $notification )'); _handleSshRequestNotification(notification); break; From 2aac517eb7eedb729195cad2cbe5c62640bc6531 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 20:51:29 +0800 Subject: [PATCH 87/95] fix: normalize all paths --- .../lib/common/create_at_client_cli.dart | 16 ++++------ packages/sshnoports/lib/common/utils.dart | 29 +++++++++---------- packages/sshnoports/lib/sshnp/sshnp_impl.dart | 4 +-- packages/sshnoports/lib/sshnp/utils.dart | 13 +++------ 4 files changed, 26 insertions(+), 36 deletions(-) diff --git a/packages/sshnoports/lib/common/create_at_client_cli.dart b/packages/sshnoports/lib/common/create_at_client_cli.dart index eb749553a..3ac5df538 100644 --- a/packages/sshnoports/lib/common/create_at_client_cli.dart +++ b/packages/sshnoports/lib/common/create_at_client_cli.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:at_client/at_client.dart'; import 'package:at_onboarding_cli/at_onboarding_cli.dart'; import 'package:version/version.dart'; - +import 'package:path/path.dart' as path; import 'service_factories.dart'; Future createAtClientCli({ @@ -21,22 +21,18 @@ Future createAtClientCli({ pathBase += '$pathExtension${Platform.pathSeparator}'; } AtOnboardingPreference atOnboardingConfig = AtOnboardingPreference() - ..hiveStoragePath = - '$pathBase/storage'.replaceAll('/', Platform.pathSeparator) + ..hiveStoragePath = path.normalize('$pathBase/storage') ..namespace = namespace - ..downloadPath = '$homeDirectory/$subDirectory/files' - .replaceAll('/', Platform.pathSeparator) + ..downloadPath = path.normalize('$homeDirectory/$subDirectory/files') ..isLocalStoreRequired = true - ..commitLogPath = - '$pathBase/storage/commitLog'.replaceAll('/', Platform.pathSeparator) + ..commitLogPath = path.normalize('$pathBase/storage/commitLog') ..fetchOfflineNotifications = false ..atKeysFilePath = atKeysFilePath ..atProtocolEmitted = Version(2, 0, 0) ..rootDomain = rootDomain; - AtOnboardingService onboardingService = AtOnboardingServiceImpl( - atsign, atOnboardingConfig, - atServiceFactory: ServiceFactoryWithNoOpSyncService()); + AtOnboardingService onboardingService = + AtOnboardingServiceImpl(atsign, atOnboardingConfig, atServiceFactory: ServiceFactoryWithNoOpSyncService()); await onboardingService.authenticate(); diff --git a/packages/sshnoports/lib/common/utils.dart b/packages/sshnoports/lib/common/utils.dart index 9c7786e58..9e370e357 100644 --- a/packages/sshnoports/lib/common/utils.dart +++ b/packages/sshnoports/lib/common/utils.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:at_chops/at_chops.dart'; import 'package:at_client/at_client.dart'; import 'package:at_utils/at_utils.dart'; +import 'package:path/path.dart' as path; /// Get the home directory or null if unknown. String? getHomeDirectory({bool throwIfNull = false}) { @@ -56,19 +57,19 @@ bool checkNonAscii(String test) { String getDefaultAtKeysFilePath(String homeDirectory, String? atSign) { if (atSign == null) return ''; - return '$homeDirectory/.atsign/keys/${atSign}_key.atKeys'.replaceAll('/', Platform.pathSeparator); + return path.normalize('$homeDirectory/.atsign/keys/${atSign}_key.atKeys'); } String getDefaultSshDirectory(String homeDirectory) { - return '$homeDirectory/.ssh/'.replaceAll('/', Platform.pathSeparator); + return path.normalize('$homeDirectory/.ssh/'); } String getDefaultSshnpDirectory(String homeDirectory) { - return '$homeDirectory/.sshnp/'.replaceAll('/', Platform.pathSeparator); + return path.normalize('$homeDirectory/.sshnp/'); } String getDefaultSshnpConfigDirectory(String homeDirectory) { - return '$homeDirectory/.sshnp/config'.replaceAll('/', Platform.pathSeparator); + return path.normalize('$homeDirectory/.sshnp/config'); } /// Checks if the provided atSign's atServer has been properly activated with a public RSA key. @@ -125,8 +126,8 @@ Future<(String, String)> generateSshKeys( workingDirectory: sshHomeDirectory); } - String sshPublicKey = await File('$sshHomeDirectory${sessionId}_sshnp.pub').readAsString(); - String sshPrivateKey = await File('$sshHomeDirectory${sessionId}_sshnp').readAsString(); + String sshPublicKey = await File('$sshHomeDirectory/${sessionId}_sshnp.pub').readAsString(); + String sshPrivateKey = await File('$sshHomeDirectory/${sessionId}_sshnp').readAsString(); return (sshPublicKey, sshPrivateKey); } @@ -140,14 +141,14 @@ Future addEphemeralKeyToAuthorizedKeys( String homeDirectory = getHomeDirectory(throwIfNull: true)!; - var sshHomeDirectory = '$homeDirectory/.ssh/'.replaceAll('/', Platform.pathSeparator); + var sshHomeDirectory = path.normalize('$homeDirectory/.ssh'); if (!Directory(sshHomeDirectory).existsSync()) { Directory(sshHomeDirectory).createSync(); } // Check to see if the ssh Publickey is already in the authorized_keys file. // If not, then append it. - var authKeys = File('${sshHomeDirectory}authorized_keys'); + var authKeys = File(path.normalize('$sshHomeDirectory/authorized_keys')); var authKeysContent = await authKeys.readAsString(); if (!authKeysContent.endsWith('\n')) { @@ -176,7 +177,7 @@ Future removeEphemeralKeyFromAuthorizedKeys(String sessionId, AtSignLogger {String? sshHomeDirectory}) async { try { sshHomeDirectory ??= getDefaultSshDirectory(getHomeDirectory(throwIfNull: true)!); - final File file = File('${sshHomeDirectory}authorized_keys'); + final File file = File(path.normalize('$sshHomeDirectory/authorized_keys')); logger.info('Removing ephemeral key for session $sessionId' ' from ${file.absolute.path}'); // read into List of strings @@ -187,7 +188,7 @@ Future removeEphemeralKeyFromAuthorizedKeys(String sessionId, AtSignLogger await file.writeAsString(lines.join('\n')); await file.writeAsString('\n', mode: FileMode.writeOnlyAppend); } catch (e) { - logger.severe('Unable to tidy up ${sshHomeDirectory}authorized_keys'); + logger.severe('Unable to tidy up ${path.normalize('sshHomeDirectory/authorized_keys')}'); } } @@ -258,9 +259,7 @@ Future getLocallyCachedPK(AtClient atClient, String atSign, {bool useFil Future _fetchFromLocalPKCache(AtClient atClient, String atSign, bool useFileStorage) async { String dontAtMe = atSign.substring(1); if (useFileStorage) { - String fn = '${getHomeDirectory(throwIfNull: true)}' - '/.atsign/sshnp/cached_pks/$dontAtMe' - .replaceAll('/', Platform.pathSeparator); + String fn = path.normalize('${getHomeDirectory(throwIfNull: true)}/.atsign/sshnp/cached_pks/$dontAtMe'); File f = File(fn); if (await f.exists()) { return (await f.readAsString()).trim(); @@ -282,8 +281,8 @@ Future _storeToLocalPKCache(String pk, AtClient atClient, String atSign, b String dontAtMe = atSign.substring(1); if (useFileStorage) { String dirName = - '${getHomeDirectory(throwIfNull: true)}/.atsign/sshnp/cached_pks'.replaceAll('/', Platform.pathSeparator); - String fileName = '$dirName/$dontAtMe'.replaceAll('/', Platform.pathSeparator); + path.normalize('${getHomeDirectory(throwIfNull: true)}/.atsign/sshnp/cached_pks'); + String fileName = path.normalize('$dirName/$dontAtMe'); File f = File(fileName); if (!await f.exists()) { diff --git a/packages/sshnoports/lib/sshnp/sshnp_impl.dart b/packages/sshnoports/lib/sshnp/sshnp_impl.dart index 2d57bb86a..3799c5f37 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_impl.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_impl.dart @@ -218,7 +218,7 @@ class SSHNPImpl implements SSHNP { } else if (path.normalize(sendSshPublicKey).contains('/') || path.normalize(sendSshPublicKey).contains(r'\')) { publicKeyFileName = path.normalize(path.absolute(sendSshPublicKey)); } else { - publicKeyFileName = path.normalize('$sshHomeDirectory$sendSshPublicKey'); + publicKeyFileName = path.normalize('$sshHomeDirectory/$sendSshPublicKey'); } } @@ -625,7 +625,7 @@ class SSHNPImpl implements SSHNP { // So we can write the ephemeralPrivateKey to a tmp file, // set its permissions appropriately, and remove it after we've // executed the command - var tmpFileName = '/tmp/ephemeral_$sessionId'; + var tmpFileName = path.normalize('$sshHomeDirectory/tmp/ephemeral_$sessionId'); File tmpFile = File(tmpFileName); await tmpFile.create(recursive: true); await tmpFile.writeAsString(ephemeralPrivateKey, mode: FileMode.write, flush: true); diff --git a/packages/sshnoports/lib/sshnp/utils.dart b/packages/sshnoports/lib/sshnp/utils.dart index 22aab9586..b7d7894d1 100644 --- a/packages/sshnoports/lib/sshnp/utils.dart +++ b/packages/sshnoports/lib/sshnp/utils.dart @@ -17,17 +17,12 @@ Future cleanUpAfterReverseSsh(SSHNP sshnp) async { if (homeDirectory == null) { return; } - var sshHomeDirectory = "$homeDirectory/.ssh/"; - if (Platform.isWindows) { - sshHomeDirectory = r'$homeDirectory\.ssh\'; - } + var sshHomeDirectory = getDefaultSshDirectory(homeDirectory); sshnp.logger.info('Tidying up files'); // Delete the generated RSA keys and remove the entry from ~/.ssh/authorized_keys - await deleteFile('$sshHomeDirectory${sshnp.sessionId}_sshnp', sshnp.logger); - await deleteFile( - '$sshHomeDirectory${sshnp.sessionId}_sshnp.pub', sshnp.logger); - await removeEphemeralKeyFromAuthorizedKeys(sshnp.sessionId, sshnp.logger, - sshHomeDirectory: sshHomeDirectory); + await deleteFile('$sshHomeDirectory/${sshnp.sessionId}_sshnp', sshnp.logger); + await deleteFile('$sshHomeDirectory/${sshnp.sessionId}_sshnp.pub', sshnp.logger); + await removeEphemeralKeyFromAuthorizedKeys(sshnp.sessionId, sshnp.logger, sshHomeDirectory: sshHomeDirectory); } Future deleteFile(String fileName, AtSignLogger logger) async { From ee2da0e69c89b9b426ac148edd6e3774e925152e Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 21:06:47 +0800 Subject: [PATCH 88/95] format: dart format --- packages/sshnoports/lib/sshnp/sshnp_impl.dart | 155 ++++++++++++------ 1 file changed, 109 insertions(+), 46 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_impl.dart b/packages/sshnoports/lib/sshnp/sshnp_impl.dart index 3799c5f37..b94849e65 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_impl.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_impl.dart @@ -215,7 +215,8 @@ class SSHNPImpl implements SSHNP { // search for a public key file called 'false' if (sendSshPublicKey == 'false' || sendSshPublicKey.isEmpty) { publicKeyFileName = ''; - } else if (path.normalize(sendSshPublicKey).contains('/') || path.normalize(sendSshPublicKey).contains(r'\')) { + } else if (path.normalize(sendSshPublicKey).contains('/') || + path.normalize(sendSshPublicKey).contains(r'\')) { publicKeyFileName = path.normalize(path.absolute(sendSshPublicKey)); } else { publicKeyFileName = path.normalize('$sshHomeDirectory/$sendSshPublicKey'); @@ -247,18 +248,21 @@ class SSHNPImpl implements SSHNP { if (atClient != null) { if (p.clientAtSign != atClient.getCurrentAtSign()) { - throw ArgumentError('Option from must match the current atSign of the AtClient'); + throw ArgumentError( + 'Option from must match the current atSign of the AtClient'); } } else { // Check atKeyFile selected exists if (!await fileExists(p.atKeysFilePath)) { - throw ArgumentError('\nUnable to find .atKeys file : ${p.atKeysFilePath}'); + throw ArgumentError( + '\nUnable to find .atKeys file : ${p.atKeysFilePath}'); } } // Check to see if the port number is in range for TCP ports if (p.localSshdPort > 65535 || p.localSshdPort < 1) { - throw ArgumentError('\nInvalid port number for sshd (1-65535) : ${p.localSshdPort}'); + throw ArgumentError( + '\nInvalid port number for sshd (1-65535) : ${p.localSshdPort}'); } String sessionId = Uuid().v4(); @@ -297,7 +301,8 @@ class SSHNPImpl implements SSHNP { legacyDaemon: p.legacyDaemon, remoteSshdPort: p.remoteSshdPort, idleTimeout: p.idleTimeout, - sshClient: SupportedSshClient.values.firstWhere((c) => c.cliArg == p.sshClient), + sshClient: SupportedSshClient.values + .firstWhere((c) => c.cliArg == p.sshClient), addForwardsToTunnel: p.addForwardsToTunnel, ); if (p.verbose) { @@ -340,7 +345,10 @@ class SSHNPImpl implements SSHNP { throw ('Device address $sshnpdAtSign is not activated.'); } } catch (e, s) { - throw SSHNPFailed('Device address $sshnpdAtSign does not exist or is not activated.', e, s); + throw SSHNPFailed( + 'Device address $sshnpdAtSign does not exist or is not activated.', + e, + s); } logger.info('Subscribing to notifications on $sessionId.$namespace@'); @@ -353,7 +361,8 @@ class SSHNPImpl implements SSHNP { throw ('Unable to find ssh public key file : $publicKeyFileName'); } - if (publicKeyFileName.isNotEmpty && !File(publicKeyFileName.replaceAll('.pub', '')).existsSync()) { + if (publicKeyFileName.isNotEmpty && + !File(publicKeyFileName.replaceAll('.pub', '')).existsSync()) { throw ('Unable to find matching ssh private key for public key : $publicKeyFileName'); } @@ -362,7 +371,8 @@ class SSHNPImpl implements SSHNP { // find a spare local port if (localPort == 0) { try { - ServerSocket serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + ServerSocket serverSocket = + await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); localPort = serverSocket.port; await serverSocket.close(); } catch (e, s) { @@ -383,7 +393,10 @@ class SSHNPImpl implements SSHNP { if (!direct) { try { var (String ephemeralPublicKey, String ephemeralPrivateKey) = - await generateSshKeys(rsa: rsa, sessionId: sessionId, sshHomeDirectory: sshHomeDirectory); + await generateSshKeys( + rsa: rsa, + sessionId: sessionId, + sshHomeDirectory: sshHomeDirectory); sshPublicKey = ephemeralPublicKey; sshPrivateKey = ephemeralPrivateKey; @@ -393,9 +406,12 @@ class SSHNPImpl implements SSHNP { try { await addEphemeralKeyToAuthorizedKeys( - sshPublicKey: sshPublicKey, localSshdPort: localSshdPort, sessionId: sessionId); + sshPublicKey: sshPublicKey, + localSshdPort: localSshdPort, + sessionId: sessionId); } catch (e, s) { - throw SSHNPFailed('Failed to add ephemeral key to authorized_keys', e, s); + throw SSHNPFailed( + 'Failed to add ephemeral key to authorized_keys', e, s); } if (legacyDaemon) { @@ -434,7 +450,8 @@ class SSHNPImpl implements SSHNP { // tunnel is being managed by the SSHNP instance. In that case, // _doneCompleter.complete() is called once the tunnel determines // that there are no more active connections. - logger.info('Requesting daemon to set up socket tunnel for direct ssh session'); + logger.info( + 'Requesting daemon to set up socket tunnel for direct ssh session'); res = await startDirectSsh(); } else { logger.info('Requesting daemon to start reverse ssh session'); @@ -457,12 +474,18 @@ class SSHNPImpl implements SSHNP { ..metadata = (Metadata() ..ttr = -1 ..ttl = 10000), - signAndWrapAndJsonEncode(atClient, {'direct': true, 'sessionId': sessionId, 'host': host, 'port': port}), + signAndWrapAndJsonEncode(atClient, { + 'direct': true, + 'sessionId': sessionId, + 'host': host, + 'port': port + }), sessionId: sessionId); bool acked = await waitForDaemonResponse(); if (!acked) { - return SSHNPFailed('sshnp timed out: waiting for daemon response\nhint: make sure the device is online'); + return SSHNPFailed( + 'sshnp timed out: waiting for daemon response\nhint: make sure the device is online'); } if (sshnpdAckErrors) { @@ -490,7 +513,8 @@ class SSHNPImpl implements SSHNP { } if (!success) { - errorMessage ??= 'Failed to start ssh tunnel and / or forward local port $localPort'; + errorMessage ??= + 'Failed to start ssh tunnel and / or forward local port $localPort'; return SSHNPFailed(errorMessage); } // All good - write the ssh command to stdout @@ -526,19 +550,29 @@ class SSHNPImpl implements SSHNP { ], keepAliveInterval: Duration(seconds: 15)); } catch (e) { - return (false, 'Failed to create SSHClient for $username@$host:$port : $e', null); + return ( + false, + 'Failed to create SSHClient for $username@$host:$port : $e', + null + ); } try { await client.authenticated; } catch (e) { - return (false, 'Failed to authenticate as $username@$host:$port : $e', null); + return ( + false, + 'Failed to authenticate as $username@$host:$port : $e', + null + ); } int counter = 0; Future startForwarding( - {required int fLocalPort, required String fRemoteHost, required int fRemotePort}) async { + {required int fLocalPort, + required String fRemoteHost, + required int fRemotePort}) async { logger.info('Starting port forwarding' ' from port $fLocalPort on localhost' ' to $fRemoteHost:$fRemotePort on remote side'); @@ -565,7 +599,10 @@ class SSHNPImpl implements SSHNP { } // Start local forwarding to the remote sshd - await startForwarding(fLocalPort: localPort, fRemoteHost: 'localhost', fRemotePort: remoteSshdPort); + await startForwarding( + fLocalPort: localPort, + fRemoteHost: 'localhost', + fRemotePort: remoteSshdPort); if (addForwardsToTunnel) { var optionsSplitBySpace = localSshOptions.join(' ').split(' '); @@ -592,13 +629,18 @@ class SSHNPImpl implements SSHNP { int? fLocalPort = int.tryParse(args[0]); String fRemoteHost = args[1]; int? fRemotePort = int.tryParse(args[2]); - if (fLocalPort == null || fRemoteHost.isEmpty || fRemotePort == null) { + if (fLocalPort == null || + fRemoteHost.isEmpty || + fRemotePort == null) { logger.warning('localSshOptions has -L with bad args $argString'); continue; } // Start the forwarding - await startForwarding(fLocalPort: fLocalPort, fRemoteHost: fRemoteHost, fRemotePort: fRemotePort); + await startForwarding( + fLocalPort: fLocalPort, + fRemoteHost: fRemoteHost, + fRemotePort: fRemotePort); } } } @@ -625,10 +667,12 @@ class SSHNPImpl implements SSHNP { // So we can write the ephemeralPrivateKey to a tmp file, // set its permissions appropriately, and remove it after we've // executed the command - var tmpFileName = path.normalize('$sshHomeDirectory/tmp/ephemeral_$sessionId'); + var tmpFileName = + path.normalize('$sshHomeDirectory/tmp/ephemeral_$sessionId'); File tmpFile = File(tmpFileName); await tmpFile.create(recursive: true); - await tmpFile.writeAsString(ephemeralPrivateKey, mode: FileMode.write, flush: true); + await tmpFile.writeAsString(ephemeralPrivateKey, + mode: FileMode.write, flush: true); await Process.run('chmod', ['go-rwx', tmpFileName]); String argsString = '$remoteUsername@$host' @@ -660,14 +704,12 @@ class SSHNPImpl implements SSHNP { final serrBuf = StringBuffer(); Process? process; try { - process = await Process.start('/usr/bin/ssh', args); - process.stdout.listen((List l) { - var s = utf8.decode(l); + process = await Process.start('ssh', args); + process.stdout.transform(Utf8Decoder()).listen((String s) { soutBuf.write(s); logger.info('$sessionId | sshStdOut | $s'); }, onError: (e) {}); - process.stderr.listen((List l) { - var s = utf8.decode(l); + process.stderr.transform(Utf8Decoder()).listen((String s) { serrBuf.write(s); logger.info('$sessionId | sshStdErr | $s'); }, onError: (e) {}); @@ -682,12 +724,14 @@ class SSHNPImpl implements SSHNP { String? errorMessage; if (sshExitCode != 0) { if (sshExitCode == 6464) { - logger.shout('$sessionId | Command timed out: /usr/bin/ssh ${args.join(' ')}'); + logger.shout( + '$sessionId | Command timed out: /usr/bin/ssh ${args.join(' ')}'); errorMessage = 'Failed to establish connection - timed out'; } else { logger.shout('$sessionId | Exit code $sshExitCode from' ' /usr/bin/ssh ${args.join(' ')}'); - errorMessage = 'Failed to establish connection - exit code $sshExitCode'; + errorMessage = + 'Failed to establish connection - exit code $sshExitCode'; } } @@ -698,7 +742,8 @@ class SSHNPImpl implements SSHNP { Future startReverseSsh() async { // Connect to rendezvous point using background process. // sshnp (this program) can then exit without issue. - SSHRV sshrv = sshrvGenerator(host, _sshrvdPort, localSshdPort: localSshdPort); + SSHRV sshrv = + sshrvGenerator(host, _sshrvdPort, localSshdPort: localSshdPort); Future sshrvResult = sshrv.run(); // send request to the daemon via notification @@ -725,7 +770,8 @@ class SSHNPImpl implements SSHNP { bool acked = await waitForDaemonResponse(); await cleanUpAfterReverseSsh(this); if (!acked) { - return SSHNPFailed('sshnp connection timeout: waiting for daemon response'); + return SSHNPFailed( + 'sshnp connection timeout: waiting for daemon response'); } if (sshnpdAckErrors) { @@ -745,7 +791,8 @@ class SSHNPImpl implements SSHNP { Future legacyStartReverseSsh() async { // Connect to rendezvous point using background process. // sshnp (this program) can then exit without issue. - SSHRV sshrv = sshrvGenerator(host, _sshrvdPort, localSshdPort: localSshdPort); + SSHRV sshrv = + sshrvGenerator(host, _sshrvdPort, localSshdPort: localSshdPort); Future sshrvResult = sshrv.run(); // send request to the daemon via notification @@ -764,7 +811,8 @@ class SSHNPImpl implements SSHNP { bool acked = await waitForDaemonResponse(); await cleanUpAfterReverseSsh(this); if (!acked) { - return SSHNPFailed('sshnp timed out: waiting for daemon response\nhint: make sure the device is online'); + return SSHNPFailed( + 'sshnp timed out: waiting for daemon response\nhint: make sure the device is online'); } if (sshnpdAckErrors) { @@ -810,7 +858,8 @@ class SSHNPImpl implements SSHNP { assertValidValue(daemonResponse, 'sessionId', String); assertValidValue(daemonResponse, 'ephemeralPrivateKey', String); } catch (e) { - logger.warning('Failed to extract parameters from notification value "${notification.value}" with error : $e'); + logger.warning( + 'Failed to extract parameters from notification value "${notification.value}" with error : $e'); sshnpdAck = true; sshnpdAckErrors = true; return; @@ -847,7 +896,8 @@ class SSHNPImpl implements SSHNP { /// @human:username.device.sshnp@daemon /// Is not called if remoteUserName was set via constructor Future fetchRemoteUserName() async { - AtKey userNameRecordID = AtKey.fromString('$clientAtSign:username.$namespace$sshnpdAtSign'); + AtKey userNameRecordID = + AtKey.fromString('$clientAtSign:username.$namespace$sshnpdAtSign'); try { remoteUsername = (await atClient.get(userNameRecordID)).value as String; } catch (e, s) { @@ -878,9 +928,13 @@ class SSHNPImpl implements SSHNP { ..ttl = 10000); await _notify(sendOurPublicKeyToSshnpd, toSshPublicKey); } catch (e, s) { - stderr.writeln("Error opening or validating public key file or sending to remote atSign: $e"); + stderr.writeln( + "Error opening or validating public key file or sending to remote atSign: $e"); await cleanUpAfterReverseSsh(this); - throw SSHNPFailed('Error opening or validating public key file or sending to remote atSign', e, s); + throw SSHNPFailed( + 'Error opening or validating public key file or sending to remote atSign', + e, + s); } } @@ -898,7 +952,8 @@ class SSHNPImpl implements SSHNP { Future getHostAndPortFromSshrvd() async { atClient.notificationService - .subscribe(regex: '$sessionId.${SSHRVD.namespace}@', shouldDecrypt: true) + .subscribe( + regex: '$sessionId.${SSHRVD.namespace}@', shouldDecrypt: true) .listen((notification) async { String ipPorts = notification.value.toString(); List results = ipPorts.split(','); @@ -933,7 +988,10 @@ class SSHNPImpl implements SSHNP { } Future> _getAtKeysRemote( - {String? regex, String? sharedBy, String? sharedWith, bool showHiddenKeys = false}) async { + {String? regex, + String? sharedBy, + String? sharedWith, + bool showHiddenKeys = false}) async { var builder = ScanVerbBuilder() ..sharedWith = sharedWith ..sharedBy = sharedBy @@ -950,7 +1008,8 @@ class SSHNPImpl implements SSHNP { } on InvalidSyntaxException { logger.severe('$key is not a well-formed key'); } on Exception catch (e) { - logger.severe('Exception occurred: ${e.toString()}. Unable to form key $key'); + logger.severe( + 'Exception occurred: ${e.toString()}. Unable to form key $key'); } }).toList(); } @@ -959,11 +1018,13 @@ class SSHNPImpl implements SSHNP { } @override - Future<(Iterable, Iterable, Map)> listDevices() async { + Future<(Iterable, Iterable, Map)> + listDevices() async { // get all the keys device_info.*.sshnpd var scanRegex = 'device_info\\.$asciiMatcher\\.${SSHNPD.namespace}'; - var atKeys = await _getAtKeysRemote(regex: scanRegex, sharedBy: sshnpdAtSign); + var atKeys = + await _getAtKeysRemote(regex: scanRegex, sharedBy: sshnpdAtSign); var devices = {}; var heartbeats = {}; @@ -1027,9 +1088,11 @@ class SSHNPImpl implements SSHNP { } /// This function sends a notification given an atKey and value - Future _notify(AtKey atKey, String value, {String sessionId = ""}) async { - await atClient.notificationService.notify(NotificationParams.forUpdate(atKey, value: value), - onSuccess: (notification) { + Future _notify(AtKey atKey, String value, + {String sessionId = ""}) async { + await atClient.notificationService + .notify(NotificationParams.forUpdate(atKey, value: value), + onSuccess: (notification) { logger.info('SUCCESS:$notification for: $sessionId with value: $value'); }, onError: (notification) { logger.info('ERROR:$notification'); From 6890368a619c62c50d046c917158f6fe0d1d881e Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 21:37:48 +0800 Subject: [PATCH 89/95] feat: use pure-dart clients --- .../profile_actions/profile_run_action.dart | 1 + .../profile_actions/profile_terminal_action.dart | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index 52afa8181..0a2da11af 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -36,6 +36,7 @@ class _ProfileRunActionState extends ConsumerState { idleTimeout: 120, // 120 / 60 = 2 minutes addForwardsToTunnel: true, legacyDaemon: false, + sshClient: 'pure-dart', ), ); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart index 067409f2e..600830cc5 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart @@ -15,7 +15,8 @@ class ProfileTerminalAction extends ConsumerStatefulWidget { const ProfileTerminalAction(this.params, {Key? key}) : super(key: key); @override - ConsumerState createState() => _ProfileTerminalActionState(); + ConsumerState createState() => + _ProfileTerminalActionState(); } class _ProfileTerminalActionState extends ConsumerState { @@ -24,7 +25,8 @@ class _ProfileTerminalActionState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: (BuildContext context) => const Center(child: CircularProgressIndicator()), + builder: (BuildContext context) => + const Center(child: CircularProgressIndicator()), ); } @@ -33,6 +35,7 @@ class _ProfileTerminalActionState extends ConsumerState { widget.params, SSHNPPartialParams( legacyDaemon: false, + sshClient: 'pure-dart', ), ); @@ -49,14 +52,17 @@ class _ProfileTerminalActionState extends ConsumerState { } /// Issue a new session id - final sessionId = ref.watch(terminalSessionController.notifier).createSession(); + final sessionId = + ref.watch(terminalSessionController.notifier).createSession(); /// Create the session controller for the new session id - final sessionController = ref.watch(terminalSessionFamilyController(sessionId).notifier); + final sessionController = + ref.watch(terminalSessionFamilyController(sessionId).notifier); if (result is SSHNPCommandResult) { /// Set the command for the new session - sessionController.setProcess(command: result.command, args: result.args); + sessionController.setProcess( + command: result.command, args: result.args); sessionController.issueDisplayName(widget.params.profileName!); ref.read(navigationRailController.notifier).setRoute(AppRoute.terminal); if (mounted) { From f5df86c52d9e1f59664f15372f92dcb2ff8ac83a Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 21:37:59 +0800 Subject: [PATCH 90/95] format: dart format --- packages/sshnoports/lib/common/utils.dart | 90 +++++++++++++------ packages/sshnoports/lib/sshnp/sshnp_impl.dart | 18 ++-- 2 files changed, 74 insertions(+), 34 deletions(-) diff --git a/packages/sshnoports/lib/common/utils.dart b/packages/sshnoports/lib/common/utils.dart index 9e370e357..ac8c36a76 100644 --- a/packages/sshnoports/lib/common/utils.dart +++ b/packages/sshnoports/lib/common/utils.dart @@ -107,41 +107,63 @@ Future atSignIsActivated(final AtClient atClient, String atSign) async { void assertValidValue(Map m, String k, Type t) { var v = m[k]; if (v == null || v.runtimeType != t) { - throw ArgumentError('Parameter $k should be a $t but is actually a ${v.runtimeType} with value $v'); + throw ArgumentError( + 'Parameter $k should be a $t but is actually a ${v.runtimeType} with value $v'); } } Future<(String, String)> generateSshKeys( - {required bool rsa, required String sessionId, String? sshHomeDirectory}) async { - sshHomeDirectory ??= getDefaultSshDirectory(getHomeDirectory(throwIfNull: true)!); + {required bool rsa, + required String sessionId, + String? sshHomeDirectory}) async { + sshHomeDirectory ??= + getDefaultSshDirectory(getHomeDirectory(throwIfNull: true)!); if (!Directory(sshHomeDirectory).existsSync()) { Directory(sshHomeDirectory).createSync(); } if (rsa) { - await Process.run('ssh-keygen', ['-t', 'rsa', '-b', '4096', '-f', '${sessionId}_sshnp', '-q', '-N', ''], + await Process.run('ssh-keygen', + ['-t', 'rsa', '-b', '4096', '-f', '${sessionId}_sshnp', '-q', '-N', ''], workingDirectory: sshHomeDirectory); } else { - await Process.run('ssh-keygen', ['-t', 'ed25519', '-a', '100', '-f', '${sessionId}_sshnp', '-q', '-N', ''], + await Process.run( + 'ssh-keygen', + [ + '-t', + 'ed25519', + '-a', + '100', + '-f', + '${sessionId}_sshnp', + '-q', + '-N', + '' + ], workingDirectory: sshHomeDirectory); } - String sshPublicKey = await File('$sshHomeDirectory/${sessionId}_sshnp.pub').readAsString(); - String sshPrivateKey = await File('$sshHomeDirectory/${sessionId}_sshnp').readAsString(); + String sshPublicKey = + await File('$sshHomeDirectory/${sessionId}_sshnp.pub').readAsString(); + String sshPrivateKey = + await File('$sshHomeDirectory/${sessionId}_sshnp').readAsString(); return (sshPublicKey, sshPrivateKey); } Future addEphemeralKeyToAuthorizedKeys( - {required String sshPublicKey, required int localSshdPort, String sessionId = '', String permissions = ''}) async { + {required String sshPublicKey, + required int localSshdPort, + String sessionId = '', + String permissions = ''}) async { // Check to see if the ssh public key looks like one! if (!sshPublicKey.startsWith('ssh-')) { throw ('$sshPublicKey does not look like a public key'); } String homeDirectory = getHomeDirectory(throwIfNull: true)!; + var sshHomeDirectory = getDefaultSshDirectory(homeDirectory); - var sshHomeDirectory = path.normalize('$homeDirectory/.ssh'); if (!Directory(sshHomeDirectory).existsSync()) { Directory(sshHomeDirectory).createSync(); } @@ -169,14 +191,17 @@ Future addEphemeralKeyToAuthorizedKeys( ' ' 'sshnp_ephemeral_$sessionId\n', mode: FileMode.append, + flush: true, ); } } -Future removeEphemeralKeyFromAuthorizedKeys(String sessionId, AtSignLogger logger, +Future removeEphemeralKeyFromAuthorizedKeys( + String sessionId, AtSignLogger logger, {String? sshHomeDirectory}) async { try { - sshHomeDirectory ??= getDefaultSshDirectory(getHomeDirectory(throwIfNull: true)!); + sshHomeDirectory ??= + getDefaultSshDirectory(getHomeDirectory(throwIfNull: true)!); final File file = File(path.normalize('$sshHomeDirectory/authorized_keys')); logger.info('Removing ephemeral key for session $sessionId' ' from ${file.absolute.path}'); @@ -188,14 +213,16 @@ Future removeEphemeralKeyFromAuthorizedKeys(String sessionId, AtSignLogger await file.writeAsString(lines.join('\n')); await file.writeAsString('\n', mode: FileMode.writeOnlyAppend); } catch (e) { - logger.severe('Unable to tidy up ${path.normalize('sshHomeDirectory/authorized_keys')}'); + logger.severe( + 'Unable to tidy up ${path.normalize('$sshHomeDirectory/authorized_keys')}'); } } String signAndWrapAndJsonEncode(AtClient atClient, Map payload) { Map envelope = {'payload': payload}; - final AtSigningInput signingInput = AtSigningInput(jsonEncode(payload))..signingMode = AtSigningMode.data; + final AtSigningInput signingInput = AtSigningInput(jsonEncode(payload)) + ..signingMode = AtSigningMode.data; final AtSigningResult sr = atClient.atChops!.sign(signingInput); final String signature = sr.result.toString(); @@ -205,14 +232,16 @@ String signAndWrapAndJsonEncode(AtClient atClient, Map payload) { return jsonEncode(envelope); } -Future verifyEnvelopeSignature( - AtClient atClient, String requestingAtsign, AtSignLogger logger, Map envelope) async { +Future verifyEnvelopeSignature(AtClient atClient, String requestingAtsign, + AtSignLogger logger, Map envelope) async { final String signature = envelope['signature']; Map payload = envelope['payload']; final hashingAlgo = HashingAlgoType.values.byName(envelope['hashingAlgo']); final signingAlgo = SigningAlgoType.values.byName(envelope['signingAlgo']); - final pk = await getLocallyCachedPK(atClient, requestingAtsign, useFileStorage: true); - AtSigningVerificationInput input = AtSigningVerificationInput(jsonEncode(payload), base64Decode(signature), pk) + final pk = await getLocallyCachedPK(atClient, requestingAtsign, + useFileStorage: true); + AtSigningVerificationInput input = AtSigningVerificationInput( + jsonEncode(payload), base64Decode(signature), pk) ..signingMode = AtSigningMode.data ..signingAlgoType = signingAlgo ..hashingAlgoType = hashingAlgo; @@ -237,10 +266,12 @@ Future verifyEnvelopeSignature( /// `~/.atsign/sshnp/cached_pks/alice` /// /// Note that for storage, the leading `@` in the atSign is stripped off. -Future getLocallyCachedPK(AtClient atClient, String atSign, {bool useFileStorage = true}) async { +Future getLocallyCachedPK(AtClient atClient, String atSign, + {bool useFileStorage = true}) async { atSign = AtUtils.fixAtSign(atSign); - String? cachedPK = await _fetchFromLocalPKCache(atClient, atSign, useFileStorage); + String? cachedPK = + await _fetchFromLocalPKCache(atClient, atSign, useFileStorage); if (cachedPK != null) { return cachedPK; } @@ -256,10 +287,12 @@ Future getLocallyCachedPK(AtClient atClient, String atSign, {bool useFil return av.value; } -Future _fetchFromLocalPKCache(AtClient atClient, String atSign, bool useFileStorage) async { +Future _fetchFromLocalPKCache( + AtClient atClient, String atSign, bool useFileStorage) async { String dontAtMe = atSign.substring(1); if (useFileStorage) { - String fn = path.normalize('${getHomeDirectory(throwIfNull: true)}/.atsign/sshnp/cached_pks/$dontAtMe'); + String fn = path.normalize( + '${getHomeDirectory(throwIfNull: true)}/.atsign/sshnp/cached_pks/$dontAtMe'); File f = File(fn); if (await f.exists()) { return (await f.readAsString()).trim(); @@ -269,7 +302,8 @@ Future _fetchFromLocalPKCache(AtClient atClient, String atSign, bool us } else { late final AtValue av; try { - av = await atClient.get(AtKey.fromString('local:$dontAtMe.cached_pks.sshnp@${atClient.getCurrentAtSign()!}')); + av = await atClient.get(AtKey.fromString( + 'local:$dontAtMe.cached_pks.sshnp@${atClient.getCurrentAtSign()!}')); return av.value; } on AtKeyNotFoundException catch (_) { return null; @@ -277,11 +311,12 @@ Future _fetchFromLocalPKCache(AtClient atClient, String atSign, bool us } } -Future _storeToLocalPKCache(String pk, AtClient atClient, String atSign, bool useFileStorage) async { +Future _storeToLocalPKCache( + String pk, AtClient atClient, String atSign, bool useFileStorage) async { String dontAtMe = atSign.substring(1); if (useFileStorage) { - String dirName = - path.normalize('${getHomeDirectory(throwIfNull: true)}/.atsign/sshnp/cached_pks'); + String dirName = path.normalize( + '${getHomeDirectory(throwIfNull: true)}/.atsign/sshnp/cached_pks'); String fileName = path.normalize('$dirName/$dontAtMe'); File f = File(fileName); @@ -292,7 +327,10 @@ Future _storeToLocalPKCache(String pk, AtClient atClient, String atSign, b await f.writeAsString('$pk\n'); return true; } else { - await atClient.put(AtKey.fromString('local:$dontAtMe.cached_pks.sshnp@${atClient.getCurrentAtSign()!}'), pk); + await atClient.put( + AtKey.fromString( + 'local:$dontAtMe.cached_pks.sshnp@${atClient.getCurrentAtSign()!}'), + pk); return true; } } diff --git a/packages/sshnoports/lib/sshnp/sshnp_impl.dart b/packages/sshnoports/lib/sshnp/sshnp_impl.dart index b94849e65..fb555186d 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_impl.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_impl.dart @@ -542,13 +542,15 @@ class SSHNPImpl implements SSHNP { late final SSHClient client; try { - client = SSHClient(socket, - username: remoteUsername!, - identities: [ - // A single private key file may contain multiple keys. - ...SSHKeyPair.fromPem(ephemeralPrivateKey) - ], - keepAliveInterval: Duration(seconds: 15)); + client = SSHClient( + socket, + username: remoteUsername!, + identities: [ + // A single private key file may contain multiple keys. + ...SSHKeyPair.fromPem(ephemeralPrivateKey) + ], + keepAliveInterval: Duration(seconds: 15), + ); } catch (e) { return ( false, @@ -704,7 +706,7 @@ class SSHNPImpl implements SSHNP { final serrBuf = StringBuffer(); Process? process; try { - process = await Process.start('ssh', args); + process = await Process.start('/usr/bin/ssh', args); process.stdout.transform(Utf8Decoder()).listen((String s) { soutBuf.write(s); logger.info('$sessionId | sshStdOut | $s'); From e8359fe50619f036d7c906ad204adb5f050611fc Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 21:55:05 +0800 Subject: [PATCH 91/95] fix: flip addForwardsToTunnel --- packages/sshnoports/lib/sshnp/sshnp_impl.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sshnoports/lib/sshnp/sshnp_impl.dart b/packages/sshnoports/lib/sshnp/sshnp_impl.dart index fb555186d..37b86dc30 100644 --- a/packages/sshnoports/lib/sshnp/sshnp_impl.dart +++ b/packages/sshnoports/lib/sshnp/sshnp_impl.dart @@ -523,7 +523,7 @@ class SSHNPImpl implements SSHNP { remoteUsername: remoteUsername, host: 'localhost', privateKeyFileName: publicKeyFileName.replaceAll('.pub', ''), - localSshOptions: (addForwardsToTunnel) ? localSshOptions : null, + localSshOptions: (addForwardsToTunnel) ? null : localSshOptions, sshProcess: process, sshClient: client, ); @@ -826,7 +826,7 @@ class SSHNPImpl implements SSHNP { remoteUsername: remoteUsername, host: 'localhost', privateKeyFileName: publicKeyFileName.replaceAll('.pub', ''), - localSshOptions: (addForwardsToTunnel) ? localSshOptions : null, + localSshOptions: (addForwardsToTunnel) ? null : localSshOptions, sshrvResult: sshrvResult, ); } From 831babefba6407d75a3846a741622cd8bf17b988 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 22:31:06 +0800 Subject: [PATCH 92/95] chore: update pubspec.lock --- packages/sshnp_gui/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sshnp_gui/pubspec.lock b/packages/sshnp_gui/pubspec.lock index 120a23a2d..dc453797c 100644 --- a/packages/sshnp_gui/pubspec.lock +++ b/packages/sshnp_gui/pubspec.lock @@ -1169,7 +1169,7 @@ packages: path: "../sshnoports" relative: true source: path - version: "4.0.0-rc.3" + version: "4.0.0-rc.4" stack_trace: dependency: transitive description: From 016dd2cb7636062a49bba838de413f84b4800ffd Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 22:36:48 +0800 Subject: [PATCH 93/95] chore: increase version --- packages/sshnoports/lib/version.dart | 2 +- packages/sshnoports/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sshnoports/lib/version.dart b/packages/sshnoports/lib/version.dart index ec237c751..49030075a 100644 --- a/packages/sshnoports/lib/version.dart +++ b/packages/sshnoports/lib/version.dart @@ -1,7 +1,7 @@ import 'dart:io'; // Note: if you update this version also update pubspec.yaml -const String version = "4.0.0-rc.4"; +const String version = "4.0.0-rc.5"; /// Print version number void printVersion() { diff --git a/packages/sshnoports/pubspec.yaml b/packages/sshnoports/pubspec.yaml index 37077a4f9..132c95603 100644 --- a/packages/sshnoports/pubspec.yaml +++ b/packages/sshnoports/pubspec.yaml @@ -3,7 +3,7 @@ description: Encrypted/Secure control plane for ssh/d or other commands in the f # NOTE: If you update the version number here, you # must also update it in version.dart -version: 4.0.0-rc.4 +version: 4.0.0-rc.5 homepage: https://docs.atsign.com/ From aa0c5910c2bc85afbf91e00aa0434f9f8d069c6e Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 23:16:05 +0800 Subject: [PATCH 94/95] fix: use stricter form validation --- .../widgets/profile_form/profile_form.dart | 46 +++++++++++++------ .../sshnp_gui/lib/src/utility/constants.dart | 3 +- .../lib/src/utility/form_validator.dart | 22 ++++++--- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index 8a3e81f4d..a022ff733 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -30,7 +30,9 @@ class _ProfileFormState extends ConsumerState { void onSubmit(SSHNPParams oldConfig, SSHNPPartialParams newConfig) async { if (_formkey.currentState!.validate()) { _formkey.currentState!.save(); - final controller = ref.read(configFamilyController(newConfig.profileName ?? oldConfig.profileName!).notifier); + final controller = ref.read(configFamilyController( + newConfig.profileName ?? oldConfig.profileName!) + .notifier); bool rename = newConfig.profileName.isNotNull && newConfig.profileName!.isNotEmpty && oldConfig.profileName.isNotNull && @@ -40,7 +42,8 @@ class _ProfileFormState extends ConsumerState { if (rename) { // delete old config file and write the new one - await controller.putConfig(config, oldProfileName: oldConfig.profileName!, context: context); + await controller.putConfig(config, + oldProfileName: oldConfig.profileName!, context: context); } else { // create new config file await controller.putConfig(config, context: context); @@ -57,7 +60,8 @@ class _ProfileFormState extends ConsumerState { final strings = AppLocalizations.of(context)!; currentProfile = ref.watch(currentConfigController); - final asyncOldConfig = ref.watch(configFamilyController(currentProfile.profileName)); + final asyncOldConfig = + ref.watch(configFamilyController(currentProfile.profileName)); return asyncOldConfig.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text(error.toString())), @@ -80,13 +84,14 @@ class _ProfileFormState extends ConsumerState { SSHNPPartialParams(profileName: value), ); }, - validator: FormValidator.validateRequiredField, + validator: FormValidator.validateProfileNameField, ), gapW8, CustomTextFormField( initialValue: oldConfig.device, labelText: strings.device, - onChanged: (value) => newConfig = SSHNPPartialParams.merge( + onChanged: (value) => + newConfig = SSHNPPartialParams.merge( newConfig, SSHNPPartialParams(device: value), ), @@ -100,7 +105,8 @@ class _ProfileFormState extends ConsumerState { CustomTextFormField( initialValue: oldConfig.sshnpdAtSign ?? '', labelText: strings.sshnpdAtSign, - onChanged: (value) => newConfig = SSHNPPartialParams.merge( + onChanged: (value) => + newConfig = SSHNPPartialParams.merge( newConfig, SSHNPPartialParams(sshnpdAtSign: value), ), @@ -110,7 +116,8 @@ class _ProfileFormState extends ConsumerState { CustomTextFormField( initialValue: oldConfig.host ?? '', labelText: strings.host, - onChanged: (value) => newConfig = SSHNPPartialParams.merge( + onChanged: (value) => + newConfig = SSHNPPartialParams.merge( newConfig, SSHNPPartialParams(host: value), ), @@ -125,7 +132,8 @@ class _ProfileFormState extends ConsumerState { CustomTextFormField( initialValue: oldConfig.sendSshPublicKey, labelText: strings.sendSshPublicKey, - onChanged: (value) => newConfig = SSHNPPartialParams.merge( + onChanged: (value) => + newConfig = SSHNPPartialParams.merge( newConfig, SSHNPPartialParams(sendSshPublicKey: value), ), @@ -171,7 +179,8 @@ class _ProfileFormState extends ConsumerState { CustomTextFormField( initialValue: oldConfig.port.toString(), labelText: strings.port, - onChanged: (value) => newConfig = SSHNPPartialParams.merge( + onChanged: (value) => + newConfig = SSHNPPartialParams.merge( newConfig, SSHNPPartialParams(port: int.tryParse(value)), ), @@ -186,7 +195,8 @@ class _ProfileFormState extends ConsumerState { CustomTextFormField( initialValue: oldConfig.localPort.toString(), labelText: strings.localPort, - onChanged: (value) => newConfig = SSHNPPartialParams.merge( + onChanged: (value) => + newConfig = SSHNPPartialParams.merge( newConfig, SSHNPPartialParams(localPort: int.tryParse(value)), ), @@ -195,9 +205,11 @@ class _ProfileFormState extends ConsumerState { CustomTextFormField( initialValue: oldConfig.localSshdPort.toString(), labelText: strings.localSshdPort, - onChanged: (value) => newConfig = SSHNPPartialParams.merge( + onChanged: (value) => + newConfig = SSHNPPartialParams.merge( newConfig, - SSHNPPartialParams(localSshdPort: int.tryParse(value)), + SSHNPPartialParams( + localSshdPort: int.tryParse(value)), ), ), ], @@ -221,7 +233,8 @@ class _ProfileFormState extends ConsumerState { CustomTextFormField( initialValue: oldConfig.atKeysFilePath, labelText: strings.atKeysFilePath, - onChanged: (value) => newConfig = SSHNPPartialParams.merge( + onChanged: (value) => + newConfig = SSHNPPartialParams.merge( newConfig, SSHNPPartialParams(atKeysFilePath: value), ), @@ -230,7 +243,8 @@ class _ProfileFormState extends ConsumerState { CustomTextFormField( initialValue: oldConfig.rootDomain, labelText: strings.rootDomain, - onChanged: (value) => newConfig = SSHNPPartialParams.merge( + onChanged: (value) => + newConfig = SSHNPPartialParams.merge( newConfig, SSHNPPartialParams(rootDomain: value), ), @@ -274,7 +288,9 @@ class _ProfileFormState extends ConsumerState { gapW8, TextButton( onPressed: () { - ref.read(navigationRailController.notifier).setRoute(AppRoute.home); + ref + .read(navigationRailController.notifier) + .setRoute(AppRoute.home); context.pushReplacementNamed(AppRoute.home.name); }, child: Text(strings.cancel), diff --git a/packages/sshnp_gui/lib/src/utility/constants.dart b/packages/sshnp_gui/lib/src/utility/constants.dart index e61791d2e..cf5b2bba1 100644 --- a/packages/sshnp_gui/lib/src/utility/constants.dart +++ b/packages/sshnp_gui/lib/src/utility/constants.dart @@ -5,8 +5,9 @@ const kPrimaryColor = Color(0xFFF05E3E); // const kBackGroundColorDark = Color(0xFF242424); const kBackGroundColorDark = Color(0xFF222222); -const kEmptyFieldValidationError = 'Field Cannot be left blank'; +const kEmptyFieldValidationError = 'Field cannot be left blank'; const kAtsignFieldValidationError = 'Field must start with @'; +const kProfileNameFieldValidationError = 'Field cannot contain any of ".@:_"'; const String dotEnvMimeType = 'text/plain'; const XTypeGroup dotEnvTypeGroup = XTypeGroup( diff --git a/packages/sshnp_gui/lib/src/utility/form_validator.dart b/packages/sshnp_gui/lib/src/utility/form_validator.dart index 49700f57b..b8051c5eb 100644 --- a/packages/sshnp_gui/lib/src/utility/form_validator.dart +++ b/packages/sshnp_gui/lib/src/utility/form_validator.dart @@ -2,20 +2,28 @@ import 'package:sshnp_gui/src/utility/constants.dart'; class FormValidator { static String? validateRequiredField(String? value) { - if (value!.isEmpty) { + if (value?.isEmpty ?? true) { return kEmptyFieldValidationError; - } else { - return null; } + return null; } static String? validateAtsignField(String? value) { - if (value!.isEmpty) { + if (value?.isEmpty ?? true) { return kEmptyFieldValidationError; - } else if (!value.startsWith('@')) { + } else if (!value!.startsWith('@')) { return kAtsignFieldValidationError; - } else { - return null; } + return null; + } + + static String? validateProfileNameField(String? value) { + String invalidChars = '.@:_'; + if (value?.isEmpty ?? true) { + return kEmptyFieldValidationError; + } else if (value!.contains(RegExp('[$invalidChars]'))) { + return kProfileNameFieldValidationError; + } + return null; } } From 072adf959f45801d2a8179b7b71b83327b899171 Mon Sep 17 00:00:00 2001 From: xavierchanth Date: Thu, 14 Sep 2023 23:31:10 +0800 Subject: [PATCH 95/95] fix: profileName validator --- .../home_screen_import_dialog.dart | 53 ++++++++++++++----- .../sshnp_gui/lib/src/utility/constants.dart | 3 +- .../lib/src/utility/form_validator.dart | 4 +- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_import_dialog.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_import_dialog.dart index 287f1c8f5..956b3082d 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_import_dialog.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_import_dialog.dart @@ -1,39 +1,66 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:sshnp_gui/src/presentation/widgets/profile_form/custom_text_form_field.dart'; +import 'package:sshnp_gui/src/utility/form_validator.dart'; -class HomeScreenImportDialog extends StatelessWidget { +class HomeScreenImportDialog extends StatefulWidget { final void Function(String?) setValue; + final String? initialName; - const HomeScreenImportDialog(this.setValue, {this.initialName, Key? key}) : super(key: key); + const HomeScreenImportDialog(this.setValue, {this.initialName, Key? key}) + : super(key: key); + + @override + State createState() => _HomeScreenImportDialogState(); +} + +class _HomeScreenImportDialogState extends State { + final GlobalKey _formkey = GlobalKey(); + String? result; @override Widget build(BuildContext context) { - final TextEditingController controller = TextEditingController(text: initialName); final strings = AppLocalizations.of(context)!; - return AlertDialog( title: Text(strings.importProfile), - content: TextField( - controller: controller, - decoration: InputDecoration(hintText: strings.profileName), + content: Form( + key: _formkey, + child: CustomTextFormField( + initialValue: widget.initialName, + labelText: strings.profileName, + onChanged: (value) { + result = value; + }, + validator: FormValidator.validateProfileNameField, + ), ), actions: [ OutlinedButton( - onPressed: () => Navigator.of(context).pop(false), + onPressed: () { + Navigator.of(context).pop(false); + }, child: Text(strings.cancelButton, - style: Theme.of(context).textTheme.bodyLarge!.copyWith(decoration: TextDecoration.underline)), + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(decoration: TextDecoration.underline)), ), ElevatedButton( - onPressed: () async { - setValue(controller.text); - if (context.mounted) Navigator.of(context).pop(); + onPressed: () { + if (_formkey.currentState!.validate()) { + widget.setValue(result); + Navigator.of(context).pop(); + } }, style: Theme.of(context).elevatedButtonTheme.style!.copyWith( backgroundColor: MaterialStateProperty.all(Colors.black), ), child: Text( strings.submit, - style: Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w700, color: Colors.white), + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(fontWeight: FontWeight.w700, color: Colors.white), ), ) ], diff --git a/packages/sshnp_gui/lib/src/utility/constants.dart b/packages/sshnp_gui/lib/src/utility/constants.dart index cf5b2bba1..dd2457eef 100644 --- a/packages/sshnp_gui/lib/src/utility/constants.dart +++ b/packages/sshnp_gui/lib/src/utility/constants.dart @@ -7,7 +7,8 @@ const kBackGroundColorDark = Color(0xFF222222); const kEmptyFieldValidationError = 'Field cannot be left blank'; const kAtsignFieldValidationError = 'Field must start with @'; -const kProfileNameFieldValidationError = 'Field cannot contain any of ".@:_"'; +const kProfileNameFieldValidationError = + 'Field must only use alphanumeric characters and spaces'; const String dotEnvMimeType = 'text/plain'; const XTypeGroup dotEnvTypeGroup = XTypeGroup( diff --git a/packages/sshnp_gui/lib/src/utility/form_validator.dart b/packages/sshnp_gui/lib/src/utility/form_validator.dart index b8051c5eb..2e654333d 100644 --- a/packages/sshnp_gui/lib/src/utility/form_validator.dart +++ b/packages/sshnp_gui/lib/src/utility/form_validator.dart @@ -18,10 +18,10 @@ class FormValidator { } static String? validateProfileNameField(String? value) { - String invalidChars = '.@:_'; + String invalid = '[^a-zA-Z0-9 ]'; if (value?.isEmpty ?? true) { return kEmptyFieldValidationError; - } else if (value!.contains(RegExp('[$invalidChars]'))) { + } else if (value!.contains(RegExp(invalid))) { return kProfileNameFieldValidationError; } return null;