import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import '../../../core/models/models.dart'; import '../../../core/models/severity_mapping.dart'; import '../../../core/network/api_exception.dart'; import '../../../core/theme/theme_x.dart'; import '../../../core/widgets/backup_target_form_dialog.dart'; import '../../../core/widgets/backup_targets_list.dart'; import '../../../core/widgets/mono_text.dart'; import '../../../core/widgets/status_pill.dart'; import '../../../data/repositories/services_repository.dart'; /// The scrollable body of the Services slide-over panel — everything below /// the title/close row that [SlideOverPanel] already renders. class ServiceDetailBody extends ConsumerWidget { const ServiceDetailBody({super.key, required this.service}); final Service service; Future _addTarget(BuildContext context, WidgetRef ref) { return showBackupTargetFormDialog( context, onSave: (draft) async { final repo = ref.read(servicesRepositoryProvider); if (repo == null) throw const ApiException.unknown('No active connection.'); await repo.addBackupTarget(service.id, draft); ref.invalidate(servicesListProvider); }, ); } Future _editTarget(BuildContext context, WidgetRef ref, BackupTarget target) { return showBackupTargetFormDialog( context, existing: target, onSave: (draft) async { final repo = ref.read(servicesRepositoryProvider); if (repo == null) throw const ApiException.unknown('No active connection.'); await repo.updateBackupTarget(service.id, target.id, draft); ref.invalidate(servicesListProvider); }, ); } Future _deleteTarget(BuildContext context, WidgetRef ref, BackupTarget target) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Remove backup target?'), content: Text('This removes "${target.name}" from this service\'s backup targets.'), actions: [ TextButton(onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel')), FilledButton( style: FilledButton.styleFrom(backgroundColor: context.status.critical), onPressed: () => Navigator.of(context).pop(true), child: const Text('Remove'), ), ], ), ); if (confirmed != true) return; final repo = ref.read(servicesRepositoryProvider); if (repo == null) return; try { await repo.deleteBackupTarget(service.id, target.id); ref.invalidate(servicesListProvider); } catch (e) { final message = e is ApiException ? e.userMessage : e.toString(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); } } } Future _runTarget(WidgetRef ref, BackupTarget target) async { final repo = ref.read(servicesRepositoryProvider); if (repo == null) throw const ApiException.unknown('No active connection.'); await repo.runBackupTarget(service.id, target.id); } Future _fetchProgress(WidgetRef ref, BackupTarget target) async { final repo = ref.read(servicesRepositoryProvider); if (repo == null) return const RunState(); return repo.getBackupTargetProgress(service.id, target.id); } @override Widget build(BuildContext context, WidgetRef ref) { final backup = service.backup; final targets = backup?.targets ?? const []; final history = (backup?.history ?? const []).reversed.take(5).toList(); final timeFormat = DateFormat('MM-dd HH:mm'); return ListView( padding: const EdgeInsets.fromLTRB(20, 12, 20, 20), children: [ _KvRow(label: 'Compose file', value: MonoText(service.composeFile)), _KvRow( label: 'Compose type', value: Text(service.composeType?.isNotEmpty == true ? service.composeType! : 'docker compose'), ), _KvRow( label: 'Backup folder', value: service.backupFolder?.isNotEmpty == true ? MonoText(service.backupFolder!) : Text('Not set', style: context.text.bodySmall), ), _KvRow(label: 'Stop on backup', value: Text(service.stopOnBackup ? 'Yes' : 'No')), const SizedBox(height: 18), Text('BACKUP TARGETS', style: context.text.titleSmall), const SizedBox(height: 4), BackupTargetsList( targets: targets, onAdd: () => _addTarget(context, ref), onEdit: (target) => _editTarget(context, ref, target), onDelete: (target) => _deleteTarget(context, ref, target), onRun: (target) => _runTarget(ref, target), fetchProgress: (target) => _fetchProgress(ref, target), onSettled: (target, state) => ref.invalidate(servicesListProvider), ), const SizedBox(height: 18), Text('RECENT BACKUPS', style: context.text.titleSmall), const SizedBox(height: 4), if (history.isEmpty) Padding( padding: const EdgeInsets.only(top: 6), child: Text('No backup history yet.', style: context.text.bodySmall), ) else for (final execution in history) Padding( padding: const EdgeInsets.symmetric(vertical: 7), child: Row( children: [ MonoText(timeFormat.format(execution.timestamp.toLocal()), small: true), const Spacer(), StatusPill( label: execution.success ? '${execution.durationSeconds}s' : 'failed', severity: severityForSuccess(execution.success), ), ], ), ), ], ); } } class _KvRow extends StatelessWidget { const _KvRow({required this.label, required this.value}); final String label; final Widget value; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration(border: Border(bottom: BorderSide(color: context.colors.outlineVariant))), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(width: 128, child: Text(label, style: context.text.bodySmall)), Expanded(child: value), ], ), ); } }