Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
6
lib/Peach/see/server/editor/ConnectServerEditor.dart
Normal file
6
lib/Peach/see/server/editor/ConnectServerEditor.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import '../../../about/internal/application/ServerProfile.dart';
|
||||
|
||||
abstract class ConnectServerEditor {
|
||||
/// The connection details were verified against the live server.
|
||||
void onConnectionVerified(ServerProfile server);
|
||||
}
|
||||
16
lib/Peach/see/server/editor/ServerEditor.dart
Normal file
16
lib/Peach/see/server/editor/ServerEditor.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import '../../../about/internal/application/ServerProfile.dart';
|
||||
import 'ServerEditorState.dart';
|
||||
|
||||
/// Edits a server's connection details only. Services are managed on the
|
||||
/// server detail screen.
|
||||
class ServerEditor extends StatefulWidget {
|
||||
/// null = register a new server.
|
||||
final ServerProfile? server;
|
||||
|
||||
const ServerEditor({super.key, this.server});
|
||||
|
||||
@override
|
||||
State<ServerEditor> createState() => ServerEditorState();
|
||||
}
|
||||
186
lib/Peach/see/server/editor/ServerEditorState.dart
Normal file
186
lib/Peach/see/server/editor/ServerEditorState.dart
Normal file
@@ -0,0 +1,186 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../../about/internal/application/ServerProfile.dart';
|
||||
import '../../../about/internal/application/TextType.dart';
|
||||
import '../../../designs/input/InputFields.dart';
|
||||
import '../../../designs/text/Text.dart';
|
||||
import '../../../utils/Colors.dart';
|
||||
import '../../../utils/Validators.dart';
|
||||
import 'ConnectServerEditor.dart';
|
||||
import 'ServerEditor.dart';
|
||||
import 'ViewServerEditor.dart';
|
||||
|
||||
class ServerEditorState extends State<ServerEditor>
|
||||
implements ConnectServerEditor {
|
||||
ViewServerEditor? _model;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final ServerProfile _server;
|
||||
bool _showPassword = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_server = widget.server?.copy() ?? ServerProfile();
|
||||
}
|
||||
|
||||
bool get _isNew => widget.server == null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<ViewServerEditor>.reactive(
|
||||
viewModelBuilder: () => ViewServerEditor(context, this),
|
||||
onViewModelReady: _onReady,
|
||||
builder: (context, viewModel, child) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: text(
|
||||
_isNew ? 'Register server' : 'Edit server',
|
||||
20,
|
||||
TextType.Bold,
|
||||
color: context.jp.text,
|
||||
),
|
||||
actions: [
|
||||
TextButton.icon(
|
||||
onPressed: _onTestConnection,
|
||||
icon: const Icon(Icons.wifi_tethering, size: 18),
|
||||
label: text('Test', 13, TextType.Bold, color: colorPrimary),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12, left: 8),
|
||||
child: FilledButton.icon(
|
||||
onPressed: _onSave,
|
||||
icon: const Icon(Icons.check, size: 18),
|
||||
label: text('Save', 14, TextType.Bold, color: colorWhite),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 40),
|
||||
children: [
|
||||
inputSection(context, 'Identity'),
|
||||
inputField(
|
||||
label: 'Server name',
|
||||
hint: 'prod-1',
|
||||
initial: _server.name,
|
||||
onSaved: _onSaveName,
|
||||
validator: requiredField,
|
||||
),
|
||||
inputSection(context, 'Connection'),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: inputField(
|
||||
label: 'Host / IP',
|
||||
hint: '10.0.0.5 or example.com',
|
||||
initial: _server.host,
|
||||
onSaved: _onSaveHost,
|
||||
validator: validHost,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: inputField(
|
||||
label: 'Port',
|
||||
initial: _server.port.toString(),
|
||||
keyboardType: TextInputType.number,
|
||||
onSaved: _onSavePort,
|
||||
validator: validPort,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
inputField(
|
||||
label: 'Username',
|
||||
hint: 'root / ubuntu',
|
||||
initial: _server.username,
|
||||
onSaved: _onSaveUsername,
|
||||
validator: requiredField,
|
||||
),
|
||||
inputField(
|
||||
label: 'Password',
|
||||
initial: _server.password,
|
||||
obscure: !_showPassword,
|
||||
onSaved: _onSavePassword,
|
||||
suffix: IconButton(
|
||||
icon: Icon(
|
||||
_showPassword ? Icons.visibility_off : Icons.visibility,
|
||||
),
|
||||
onPressed: _onTogglePassword,
|
||||
),
|
||||
),
|
||||
inputHint(
|
||||
context,
|
||||
Icons.lock,
|
||||
'Stored encrypted in your OS keystore '
|
||||
'(Keychain / Keystore / DPAPI / libsecret).',
|
||||
),
|
||||
inputSwitch(
|
||||
context: context,
|
||||
title: 'Use sudo for privileged commands',
|
||||
subtitle:
|
||||
'Sends the password to sudo -S for systemctl / mkdir / rm.',
|
||||
value: _server.useSudo,
|
||||
onChanged: _onToggleSudo,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onReady(ViewServerEditor model) {
|
||||
_model = model;
|
||||
// onViewModelReady fires mid-build; a synchronous setState from the
|
||||
// first load would dirty this State while it is still being built.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _initiate();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initiate() async {}
|
||||
|
||||
// ── named handlers ─────────────────────────────────────────────────────────
|
||||
|
||||
void _onSaveName(String v) => _server.name = v;
|
||||
|
||||
void _onSaveHost(String v) => _server.host = v;
|
||||
|
||||
void _onSavePort(String v) => _server.port = int.tryParse(v) ?? 22;
|
||||
|
||||
void _onSaveUsername(String v) => _server.username = v;
|
||||
|
||||
void _onSavePassword(String v) => _server.password = v;
|
||||
|
||||
void _onTogglePassword() => setState(() => _showPassword = !_showPassword);
|
||||
|
||||
void _onToggleSudo(bool v) => setState(() => _server.useSudo = v);
|
||||
|
||||
/// Commits the form and hands the profile back to whoever opened the editor.
|
||||
void _onSave() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
_formKey.currentState!.save();
|
||||
Navigator.of(context).pop(_server);
|
||||
}
|
||||
|
||||
/// Verifies the details as typed, without leaving the editor.
|
||||
void _onTestConnection() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
_formKey.currentState!.save();
|
||||
_model?.testConnection(_server);
|
||||
}
|
||||
|
||||
// ── ConnectServerEditor ────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
void onConnectionVerified(ServerProfile server) {
|
||||
// The success sheet raised by the ViewModel is the whole outcome; the form
|
||||
// keeps its values so the user can save straight away.
|
||||
}
|
||||
}
|
||||
32
lib/Peach/see/server/editor/ViewServerEditor.dart
Normal file
32
lib/Peach/see/server/editor/ViewServerEditor.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import '../../../about/internal/application/NotificationType.dart';
|
||||
import '../../../about/internal/application/ServerProfile.dart';
|
||||
import '../../parent/ParentViewModel.dart';
|
||||
import 'ConnectServerEditor.dart';
|
||||
|
||||
class ViewServerEditor extends ParentViewModel {
|
||||
ConnectServerEditor connection;
|
||||
|
||||
ViewServerEditor(super.context, this.connection);
|
||||
|
||||
/// Opens an SSH session with the details as entered, so the user finds out
|
||||
/// now — not at first deploy — whether they work.
|
||||
void testConnection(ServerProfile server) async {
|
||||
showLoading("Connecting to ${server.host} …");
|
||||
try {
|
||||
await getDataManager().connect(server);
|
||||
closeLoading();
|
||||
connection.onConnectionVerified(server);
|
||||
showApplicationNotification(
|
||||
NotificationType.success,
|
||||
"Connection verified",
|
||||
"Peach signed in to ${server.username}@${server.host}:${server.port} "
|
||||
"successfully.",
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
);
|
||||
} catch (error) {
|
||||
handleError(error, () => testConnection(server), () {}, "Retry");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user