import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/models/models.dart'; import '../../../core/network/api_exception.dart'; import '../../../data/repositories/services_repository.dart'; const _composeTypes = ['docker compose', 'docker-compose', 'podman-compose']; /// Add/edit dialog for a [Service]'s core identity fields. Backup target /// configuration is deliberately out of scope here — that's a node/service /// backup-config concern owned by the Backups screen, not service CRUD. Future showServiceFormDialog(BuildContext context, {Service? existing}) { return showDialog( context: context, builder: (context) => _ServiceFormDialog(existing: existing), ); } class _ServiceFormDialog extends ConsumerStatefulWidget { const _ServiceFormDialog({this.existing}); final Service? existing; @override ConsumerState<_ServiceFormDialog> createState() => _ServiceFormDialogState(); } class _ServiceFormDialogState extends ConsumerState<_ServiceFormDialog> { final _formKey = GlobalKey(); late final _nameController = TextEditingController(text: widget.existing?.name); late final _composeFileController = TextEditingController(text: widget.existing?.composeFile); late final _backupFolderController = TextEditingController(text: widget.existing?.backupFolder); late String _composeType = widget.existing?.composeType?.isNotEmpty == true ? widget.existing!.composeType! : _composeTypes.first; late bool _stopOnBackup = widget.existing?.stopOnBackup ?? false; bool _saving = false; String? _error; @override void dispose() { _nameController.dispose(); _composeFileController.dispose(); _backupFolderController.dispose(); super.dispose(); } Future _save() async { if (!(_formKey.currentState?.validate() ?? false)) return; setState(() { _saving = true; _error = null; }); final existing = widget.existing; final draft = (existing ?? const Service(name: '', composeFile: '')).copyWith( name: _nameController.text.trim(), composeFile: _composeFileController.text.trim(), composeType: _composeType, backupFolder: _backupFolderController.text.trim(), stopOnBackup: _stopOnBackup, ); final repo = ref.read(servicesRepositoryProvider); if (repo == null) { setState(() { _saving = false; _error = 'No active connection.'; }); return; } try { if (existing == null) { await repo.add(draft); } else { await repo.update(existing.id, draft); } ref.invalidate(servicesListProvider); if (mounted) Navigator.of(context).pop(); } catch (e) { setState(() { _saving = false; _error = e is ApiException ? e.userMessage : e.toString(); }); } } @override Widget build(BuildContext context) { return AlertDialog( title: Text(widget.existing == null ? 'Add service' : 'Edit service'), content: Form( key: _formKey, child: SizedBox( width: 420, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: _nameController, decoration: const InputDecoration(labelText: 'Name'), validator: (v) => (v == null || v.trim().isEmpty) ? 'Required.' : null, ), const SizedBox(height: 14), TextFormField( controller: _composeFileController, decoration: const InputDecoration( labelText: 'Compose file', hintText: '/opt/compose/gitea/docker-compose.yml', ), validator: (v) => (v == null || v.trim().isEmpty) ? 'Required.' : null, ), const SizedBox(height: 14), DropdownButtonFormField( initialValue: _composeType, decoration: const InputDecoration(labelText: 'Compose type'), items: [ for (final type in _composeTypes) DropdownMenuItem(value: type, child: Text(type)), ], onChanged: (value) => setState(() => _composeType = value ?? _composeTypes.first), ), const SizedBox(height: 14), TextFormField( controller: _backupFolderController, decoration: const InputDecoration( labelText: 'Backup folder (optional)', hintText: '/opt/data/containers/gitea', ), ), const SizedBox(height: 8), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Stop container while backing up'), value: _stopOnBackup, onChanged: (value) => setState(() => _stopOnBackup = value), ), if (_error != null) ...[ const SizedBox(height: 8), Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)), ], ], ), ), ), actions: [ TextButton( onPressed: _saving ? null : () => Navigator.of(context).pop(), child: const Text('Cancel'), ), FilledButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Save'), ), ], ); } }