extra form validation and connection list done
This commit is contained in:
parent
bd8884787d
commit
789cea13a1
5 changed files with 281 additions and 74 deletions
|
@ -1,13 +1,16 @@
|
|||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:fluxcloud/user.dart';
|
||||
import 'package:fluxcloud/connection.dart';
|
||||
|
||||
class AddServerModal extends StatefulWidget {
|
||||
const AddServerModal({
|
||||
super.key,
|
||||
super.key, required this.updateConnectionList, this.initialState,
|
||||
});
|
||||
|
||||
final Function updateConnectionList;
|
||||
final Connection? initialState;
|
||||
|
||||
@override
|
||||
State<AddServerModal> createState() => _AddServerModalState();
|
||||
}
|
||||
|
@ -21,9 +24,23 @@ class _AddServerModalState extends State<AddServerModal> {
|
|||
|
||||
bool _fileSelected = false;
|
||||
bool _showPassword = false;
|
||||
bool _showPrivateKey = false;
|
||||
bool _isPrivateKeyValid = false;
|
||||
|
||||
final Connection _connection = Connection();
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.initialState != null) {
|
||||
_connection.isEncryptionEnabled = widget.initialState?.isEncryptionEnabled;
|
||||
_connection.isDefault = widget.initialState?.isDefault;
|
||||
_privateKeyFileController.text = widget.initialState?.privateKey ?? '';
|
||||
_passwordController.text = widget.initialState?.password ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
|
@ -41,11 +58,12 @@ class _AddServerModalState extends State<AddServerModal> {
|
|||
spacing: 16,
|
||||
children: [
|
||||
Text(
|
||||
'Add Connection',
|
||||
widget.initialState == null ? 'Add Connection' : 'Edit Connection',
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
TextFormField(
|
||||
initialValue: widget.initialState?.host,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Host',
|
||||
),
|
||||
|
@ -61,6 +79,7 @@ class _AddServerModalState extends State<AddServerModal> {
|
|||
onSaved: (value) => _connection.host = value,
|
||||
),
|
||||
TextFormField(
|
||||
initialValue: widget.initialState?.port.toString(),
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Port',
|
||||
|
@ -77,6 +96,7 @@ class _AddServerModalState extends State<AddServerModal> {
|
|||
onSaved: (value) => _connection.port = int.parse(value!),
|
||||
),
|
||||
TextFormField(
|
||||
initialValue: widget.initialState?.username,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Username',
|
||||
),
|
||||
|
@ -85,30 +105,69 @@ class _AddServerModalState extends State<AddServerModal> {
|
|||
),
|
||||
TextFormField(
|
||||
controller: _privateKeyFileController,
|
||||
obscureText: !_showPrivateKey,
|
||||
readOnly: true,
|
||||
onTap: () async {
|
||||
final XFile? result = await openFile();
|
||||
if (result != null) {
|
||||
setState(() => _fileSelected = true);
|
||||
_privateKeyFileController.text = result.name;
|
||||
_connection.privateKeyFilePath = result.path;
|
||||
final XFile? file = await openFile();
|
||||
if (file != null) {
|
||||
const knownHeaders = [
|
||||
'-----BEGIN OPENSSH PRIVATE KEY-----',
|
||||
'-----BEGIN RSA PRIVATE KEY-----',
|
||||
'-----BEGIN DSA PRIVATE KEY-----',
|
||||
'-----BEGIN EC PRIVATE KEY-----',
|
||||
'-----BEGIN PRIVATE KEY-----',
|
||||
];
|
||||
try {
|
||||
final privateKey = await file.readAsString();
|
||||
if (knownHeaders.any((h) => privateKey.startsWith(h))) {
|
||||
setState(() {
|
||||
_fileSelected = true;
|
||||
_showPrivateKey = false;
|
||||
_privateKeyFileController.text = privateKey;
|
||||
});
|
||||
_isPrivateKeyValid = true;
|
||||
_connection.privateKey = privateKey;
|
||||
}
|
||||
else {
|
||||
_isPrivateKeyValid = false;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
setState(() {
|
||||
_fileSelected = true;
|
||||
_showPrivateKey = true;
|
||||
_privateKeyFileController.text = 'Invalid private key file';
|
||||
});
|
||||
_isPrivateKeyValid = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Private Key File',
|
||||
suffixIcon: _fileSelected ? IconButton(
|
||||
suffixIcon: _fileSelected ? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => setState(() => _showPrivateKey = !_showPrivateKey),
|
||||
icon: _showPrivateKey ? Icon(Icons.visibility) : Icon(Icons.visibility_off)
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => setState(() {
|
||||
_fileSelected = false;
|
||||
_privateKeyFileController.clear();
|
||||
}),
|
||||
icon: Icon(Icons.remove)
|
||||
),
|
||||
],
|
||||
) : null
|
||||
),
|
||||
validator: (value) {
|
||||
// TODO: validate the file to see if it is valid private key
|
||||
if (_privateKeyFileController.text.isEmpty && _passwordController.text.isEmpty) {
|
||||
return 'At least provide one of these';
|
||||
}
|
||||
if (!_isPrivateKeyValid) {
|
||||
return 'Invalid private key file';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
),
|
||||
|
@ -130,7 +189,18 @@ class _AddServerModalState extends State<AddServerModal> {
|
|||
},
|
||||
onSaved: (value) => _connection.password = value,
|
||||
),
|
||||
|
||||
CheckboxListTile(
|
||||
title: Text('Enable Encryption'),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadiusGeometry.circular(10)),
|
||||
value: _connection.isEncryptionEnabled ?? true,
|
||||
onChanged: (value) => setState(() => _connection.isEncryptionEnabled = value),
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: Text('Set as Default Connection'),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadiusGeometry.circular(10)),
|
||||
value: _connection.isDefault ?? false,
|
||||
onChanged: (value) => setState(() => _connection.isDefault = value),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState?.validate() == true) {
|
||||
|
@ -138,12 +208,13 @@ class _AddServerModalState extends State<AddServerModal> {
|
|||
assert(_connection.assertComplete());
|
||||
final storage = FlutterSecureStorage();
|
||||
await storage.write(key: _connection.host!, value: _connection.toJson);
|
||||
widget.updateConnectionList();
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text('Add Connection')
|
||||
child: Text(widget.initialState == null ? 'Add Connection' : 'Save')
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
44
lib/connection.dart
Normal file
44
lib/connection.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
import 'dart:convert';
|
||||
|
||||
class Connection {
|
||||
String? host;
|
||||
int? port;
|
||||
String? username;
|
||||
String? privateKey;
|
||||
String? password;
|
||||
bool? isEncryptionEnabled;
|
||||
bool? isDefault;
|
||||
|
||||
Connection() : isEncryptionEnabled = true, isDefault = false;
|
||||
|
||||
Connection.fromJson(Map<String, dynamic> json) {
|
||||
host = json['host'];
|
||||
port = json['port'];
|
||||
username = json['username'];
|
||||
privateKey = json['privateKeyFilePath'];
|
||||
password = json['password'];
|
||||
isEncryptionEnabled = json['isEncryptionEnabled'];
|
||||
isDefault = json['isDefault'];
|
||||
}
|
||||
|
||||
String get toJson => jsonEncode({
|
||||
'host': host,
|
||||
'port': port,
|
||||
'username': username,
|
||||
'privateKeyFilePath': privateKey,
|
||||
'password': password,
|
||||
'isEncryptionEnabled': isEncryptionEnabled,
|
||||
'isDefault': isDefault
|
||||
});
|
||||
|
||||
|
||||
bool assertComplete() {
|
||||
assert(host != null);
|
||||
assert(port != null);
|
||||
assert(username != null);
|
||||
assert(privateKey != null || password != null);
|
||||
assert(isEncryptionEnabled != null);
|
||||
assert(isDefault != null);
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:fluxcloud/add_server_modal.dart';
|
||||
import 'package:fluxcloud/sftp_connection_list.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MainApp());
|
||||
|
@ -23,26 +23,7 @@ class MainApp extends StatelessWidget {
|
|||
),
|
||||
themeMode: ThemeMode.system,
|
||||
color: Color(0x002EC1EB),
|
||||
home: Scaffold(
|
||||
floatingActionButton: Builder(
|
||||
builder: (context) {
|
||||
return FloatingActionButton(
|
||||
child: Icon(Icons.add),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (context) => AddServerModal()
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
),
|
||||
body: Center(
|
||||
child: Text('nice World!'),
|
||||
),
|
||||
),
|
||||
home: SftpConnectionList()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
147
lib/sftp_connection_list.dart
Normal file
147
lib/sftp_connection_list.dart
Normal file
|
@ -0,0 +1,147 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:fluxcloud/add_server_modal.dart';
|
||||
import 'package:fluxcloud/connection.dart';
|
||||
|
||||
class SftpConnectionList extends StatefulWidget {
|
||||
const SftpConnectionList({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SftpConnectionList> createState() => _SftpConnectionListState();
|
||||
}
|
||||
|
||||
class _SftpConnectionListState extends State<SftpConnectionList> {
|
||||
|
||||
late List<Connection> _connections;
|
||||
bool _isConnectionsInit = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getConnections();
|
||||
}
|
||||
|
||||
Future<void> _getConnections() async {
|
||||
final storage = FlutterSecureStorage();
|
||||
final secureMap = await storage.readAll();
|
||||
setState(() {
|
||||
_connections = secureMap.values.map((json) => Connection.fromJson(jsonDecode(json))).toList();
|
||||
_isConnectionsInit = true;
|
||||
});
|
||||
}
|
||||
|
||||
String _getAuthString(int index) {
|
||||
String result = 'Auth: ';
|
||||
final hasKey = _connections[index].privateKey?.isNotEmpty ?? false;
|
||||
final hasPass = _connections[index].password?.isNotEmpty ?? false;
|
||||
|
||||
if (hasKey) result += 'Private Key';
|
||||
if (hasKey && hasPass) result += ', ';
|
||||
if (hasPass) result += 'Password';
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void _showBottomSheet(BuildContext context, Connection? initialState) {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (context) => AddServerModal(updateConnectionList: _getConnections, initialState: initialState,)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: Icon(Icons.add),
|
||||
onPressed: () => _showBottomSheet(context, null)
|
||||
),
|
||||
body: _isConnectionsInit ? RefreshIndicator(
|
||||
onRefresh: () => _getConnections(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: ListView.builder(
|
||||
itemCount: _connections.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Column(
|
||||
children: [
|
||||
Material(
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// TODO: connect to connection here
|
||||
},
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_connections[index].host!,
|
||||
style: TextStyle(fontSize: 20),
|
||||
),
|
||||
Text('Port: ${_connections[index].port}'),
|
||||
Text('Username: ${_connections[index].username}'),
|
||||
Text(_getAuthString(index)),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('Encryption: '),
|
||||
Icon(
|
||||
_connections[index].isEncryptionEnabled! ? Icons.check : Icons.close,
|
||||
size: 18
|
||||
)
|
||||
],
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('Default: '),
|
||||
Icon(
|
||||
_connections[index].isDefault! ? Icons.check : Icons.close,
|
||||
size: 18
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(child: SizedBox()),
|
||||
IconButton(
|
||||
onPressed: () => _showBottomSheet(context, _connections[index]),
|
||||
icon: Icon(Icons.edit)
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final storage = FlutterSecureStorage();
|
||||
storage.delete(key: _connections[index].host!);
|
||||
_getConnections();
|
||||
},
|
||||
icon: Icon(Icons.delete)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10,)
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
) : Center(child: CircularProgressIndicator())
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import 'dart:convert';
|
||||
|
||||
class Connection {
|
||||
String? host;
|
||||
int? port;
|
||||
String? username;
|
||||
String? privateKeyFilePath;
|
||||
String? password;
|
||||
|
||||
Connection();
|
||||
|
||||
Connection.fromJson(Map<String, dynamic> json) {
|
||||
host = json['host'];
|
||||
port = json['port'];
|
||||
username = json['username'];
|
||||
privateKeyFilePath = json['privateKeyFilePath)'];
|
||||
password = json['password'];
|
||||
}
|
||||
|
||||
String get toJson => jsonEncode({
|
||||
'host': host,
|
||||
'port': port,
|
||||
'username': username,
|
||||
'privateKeyFilePath': privateKeyFilePath,
|
||||
'password': password
|
||||
});
|
||||
|
||||
|
||||
bool assertComplete() {
|
||||
assert(host != null);
|
||||
assert(port != null);
|
||||
assert(username != null);
|
||||
assert(privateKeyFilePath != null || password != null);
|
||||
return true;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue