import 'dart:convert'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import '../core/app_controller.dart'; import '../core/failure.dart'; import '../l10n/strings.dart'; import '../models/server_profile.dart'; import '../services/local_files.dart'; import 'common.dart'; class ProfileEditor extends StatefulWidget { final AppController controller; final ServerProfile? profile; const ProfileEditor(this.controller, {this.profile, super.key}); @override State createState() => _ProfileEditorState(); } class _ProfileEditorState extends State { late final name = TextEditingController(text: widget.profile?.name); late final host = TextEditingController(text: widget.profile?.host); late final username = TextEditingController(text: widget.profile?.username); late final port = TextEditingController( text: '${widget.profile?.port ?? 22}', ); final password = TextEditingController(); late AuthMethod auth = widget.profile?.auth ?? AuthMethod.password; String? key; bool savePassword = false, clearPassword = false, busy = false; Future chooseKey() async { try { final picked = await widget.controller.withSystemDialog( () => FilePicker.pickFile(), ); if (picked == null || !mounted || !widget.controller.vault.unlocked) { return; } final text = utf8.decode( await readBounded(picked.readAsByteStream(), 1024 * 1024), ); if (!text.contains('PRIVATE KEY')) throw const Failure('invalidKey'); if (mounted) setState(() => key = text); } catch (e) { if (mounted) showFailure(context, e); } } Future save() async { setState(() => busy = true); try { final c = widget.controller; final profile = ServerProfile( id: widget.profile?.id, name: name.text, host: host.text, username: username.text, port: int.tryParse(port.text) ?? 0, auth: auth, ); if (auth == AuthMethod.privateKey && key == null && c.vault.secret(profile.id, 'privateKey') == null) { throw const Failure('invalidKey'); } await c.vault.save( profile, password: auth == AuthMethod.privateKey || clearPassword ? '' : savePassword ? password.text : null, privateKey: auth == AuthMethod.password ? '' : key, ); password.clear(); key = null; if (c.ssh.profile?.id == profile.id) c.ssh.disconnect(clear: true); c.changed(); if (mounted) Navigator.pop(context); } catch (e) { if (mounted) showFailure(context, e); } finally { if (mounted) setState(() => busy = false); } } @override void dispose() { for (final field in [name, host, username, port, password]) { field.dispose(); } key = null; super.dispose(); } @override Widget build(BuildContext context) { Widget field( TextEditingController controller, String label, { TextInputType? type, }) => Padding( padding: const EdgeInsets.only(bottom: 16), child: TextField( controller: controller, keyboardType: type, autocorrect: false, enableSuggestions: false, decoration: InputDecoration(labelText: context.tr(label)), ), ); return Scaffold( appBar: AppBar( title: Text( context.tr(widget.profile == null ? 'addServer' : 'editServer'), ), ), body: SafeArea( child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 600), child: ListView( padding: const EdgeInsets.all(20), children: [ field(name, 'name'), field(host, 'host', type: TextInputType.url), field(username, 'username'), field(port, 'port', type: TextInputType.number), DropdownButtonFormField( initialValue: auth, isExpanded: true, items: AuthMethod.values .map( (a) => DropdownMenuItem( value: a, child: Text(context.tr(a.name)), ), ) .toList(), onChanged: (a) => setState(() => auth = a!), ), const SizedBox(height: 16), if (auth == AuthMethod.privateKey) ...[ OutlinedButton.icon( onPressed: chooseKey, icon: const Icon(Icons.key), label: Text(context.tr('chooseKey')), ), if (key != null || (widget.profile != null && widget.controller.vault.secret( widget.profile!.id, 'privateKey', ) != null)) Text(context.tr('keyStored')), ] else ...[ CheckboxListTile( value: savePassword, onChanged: (v) => setState(() => savePassword = v!), title: Text(context.tr('savePassword')), ), if (savePassword) TextField( controller: password, obscureText: true, autocorrect: false, enableSuggestions: false, decoration: InputDecoration( labelText: context.tr('password'), ), ), if (widget.profile != null) CheckboxListTile( value: clearPassword, onChanged: (v) => setState(() => clearPassword = v!), title: Text(context.tr('clearPassword')), ), ], const SizedBox(height: 20), FilledButton( onPressed: busy ? null : save, child: Text(context.tr('save')), ), if (busy) const LinearProgressIndicator(), ], ), ), ), ), ); } }