extra form validation and connection list done

This commit is contained in:
RafayAhmad7548 2025-07-24 09:34:39 +05:00
parent bd8884787d
commit 789cea13a1
5 changed files with 281 additions and 74 deletions

View file

@ -1,13 +1,16 @@
import 'package:file_selector/file_selector.dart'; import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:fluxcloud/user.dart'; import 'package:fluxcloud/connection.dart';
class AddServerModal extends StatefulWidget { class AddServerModal extends StatefulWidget {
const AddServerModal({ const AddServerModal({
super.key, super.key, required this.updateConnectionList, this.initialState,
}); });
final Function updateConnectionList;
final Connection? initialState;
@override @override
State<AddServerModal> createState() => _AddServerModalState(); State<AddServerModal> createState() => _AddServerModalState();
} }
@ -21,9 +24,23 @@ class _AddServerModalState extends State<AddServerModal> {
bool _fileSelected = false; bool _fileSelected = false;
bool _showPassword = false; bool _showPassword = false;
bool _showPrivateKey = false;
bool _isPrivateKeyValid = false;
final Connection _connection = Connection(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return SingleChildScrollView(
@ -41,11 +58,12 @@ class _AddServerModalState extends State<AddServerModal> {
spacing: 16, spacing: 16,
children: [ children: [
Text( Text(
'Add Connection', widget.initialState == null ? 'Add Connection' : 'Edit Connection',
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: TextStyle(fontSize: 18), style: TextStyle(fontSize: 18),
), ),
TextFormField( TextFormField(
initialValue: widget.initialState?.host,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Host', labelText: 'Host',
), ),
@ -61,6 +79,7 @@ class _AddServerModalState extends State<AddServerModal> {
onSaved: (value) => _connection.host = value, onSaved: (value) => _connection.host = value,
), ),
TextFormField( TextFormField(
initialValue: widget.initialState?.port.toString(),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Port', labelText: 'Port',
@ -77,6 +96,7 @@ class _AddServerModalState extends State<AddServerModal> {
onSaved: (value) => _connection.port = int.parse(value!), onSaved: (value) => _connection.port = int.parse(value!),
), ),
TextFormField( TextFormField(
initialValue: widget.initialState?.username,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Username', labelText: 'Username',
), ),
@ -85,30 +105,69 @@ class _AddServerModalState extends State<AddServerModal> {
), ),
TextFormField( TextFormField(
controller: _privateKeyFileController, controller: _privateKeyFileController,
obscureText: !_showPrivateKey,
readOnly: true, readOnly: true,
onTap: () async { onTap: () async {
final XFile? result = await openFile(); final XFile? file = await openFile();
if (result != null) { if (file != null) {
setState(() => _fileSelected = true); const knownHeaders = [
_privateKeyFileController.text = result.name; '-----BEGIN OPENSSH PRIVATE KEY-----',
_connection.privateKeyFilePath = result.path; '-----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( decoration: InputDecoration(
labelText: 'Private Key File', 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(() { onPressed: () => setState(() {
_fileSelected = false; _fileSelected = false;
_privateKeyFileController.clear(); _privateKeyFileController.clear();
}), }),
icon: Icon(Icons.remove) icon: Icon(Icons.remove)
),
],
) : null ) : null
), ),
validator: (value) { validator: (value) {
// TODO: validate the file to see if it is valid private key
if (_privateKeyFileController.text.isEmpty && _passwordController.text.isEmpty) { if (_privateKeyFileController.text.isEmpty && _passwordController.text.isEmpty) {
return 'At least provide one of these'; return 'At least provide one of these';
} }
if (!_isPrivateKeyValid) {
return 'Invalid private key file';
}
return null; return null;
} }
), ),
@ -130,7 +189,18 @@ class _AddServerModalState extends State<AddServerModal> {
}, },
onSaved: (value) => _connection.password = value, 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( ElevatedButton(
onPressed: () async { onPressed: () async {
if (_formKey.currentState?.validate() == true) { if (_formKey.currentState?.validate() == true) {
@ -138,12 +208,13 @@ class _AddServerModalState extends State<AddServerModal> {
assert(_connection.assertComplete()); assert(_connection.assertComplete());
final storage = FlutterSecureStorage(); final storage = FlutterSecureStorage();
await storage.write(key: _connection.host!, value: _connection.toJson); await storage.write(key: _connection.host!, value: _connection.toJson);
widget.updateConnectionList();
if (context.mounted) { if (context.mounted) {
Navigator.pop(context); Navigator.pop(context);
} }
} }
}, },
child: Text('Add Connection') child: Text(widget.initialState == null ? 'Add Connection' : 'Save')
) )
], ],
) )

44
lib/connection.dart Normal file
View 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;
}
}

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluxcloud/add_server_modal.dart'; import 'package:fluxcloud/sftp_connection_list.dart';
void main() { void main() {
runApp(const MainApp()); runApp(const MainApp());
@ -23,26 +23,7 @@ class MainApp extends StatelessWidget {
), ),
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
color: Color(0x002EC1EB), color: Color(0x002EC1EB),
home: Scaffold( home: SftpConnectionList()
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!'),
),
),
); );
} }
} }

View 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())
);
}
}

View file

@ -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;
}
}