Files
ignis_app/lib/screens/event_log_screen.dart
2026-04-23 20:02:59 +07:00

203 lines
6.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app/load_state.dart';
import '../providers/providers.dart';
import '../widgets/load_error_view.dart';
/// Экран просмотра лога событий.
class EventLogScreen extends ConsumerStatefulWidget {
const EventLogScreen({super.key});
@override
ConsumerState<EventLogScreen> createState() => _EventLogScreenState();
}
class _EventLogScreenState extends ConsumerState<EventLogScreen> {
int _limit = 100;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
await ref.read(eventLogProvider.notifier).load(limit: _limit);
}
@override
Widget build(BuildContext context) {
final eventsState = ref.watch(eventLogProvider);
final events = eventsState.data;
return Scaffold(
appBar: AppBar(
title: const Text('ЛОГ СОБЫТИЙ'),
actions: [
PopupMenuButton<int>(
icon: const Icon(Icons.filter_list),
tooltip: 'Количество записей',
onSelected: (v) {
_limit = v;
_load();
},
itemBuilder: (_) => [50, 100, 200, 500]
.map((n) => PopupMenuItem(value: n, child: Text('$n записей')))
.toList(),
),
],
),
body: _buildContent(eventsState, events),
);
}
Widget _buildContent(
LoadState<List<dynamic>> eventsState,
List<dynamic> events,
) {
if ((eventsState.isIdle || eventsState.isLoading) && events.isEmpty) {
return const Center(
child: CircularProgressIndicator(color: Colors.deepOrange),
);
}
if (eventsState.hasError && events.isEmpty) {
return LoadErrorView(
title: 'Не удалось загрузить лог событий',
message: eventsState.errorMessage,
icon: Icons.list_alt,
onRetry: _load,
);
}
if (events.isEmpty) {
return const Center(
child: Text('Нет событий', style: TextStyle(color: Colors.white54)),
);
}
final hasStatusHeader = eventsState.isLoading || eventsState.hasError;
final statusHeaderCount = hasStatusHeader ? 1 : 0;
return RefreshIndicator(
color: Colors.deepOrange,
onRefresh: _load,
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(8),
itemCount: events.length + statusHeaderCount,
itemBuilder: (context, index) {
if (hasStatusHeader && index == 0) {
if (eventsState.isLoading) {
return const Padding(
padding: EdgeInsets.only(bottom: 12),
child: LinearProgressIndicator(color: Colors.deepOrange),
);
}
return LoadErrorBanner(
title: 'Не удалось обновить лог событий',
message: eventsState.errorMessage,
onRetry: _load,
);
}
final eventIndex = index - statusHeaderCount;
final event = events[eventIndex];
return _EventRow(
event: event is Map ? Map<String, dynamic>.from(event) : {},
);
},
),
);
}
}
class _EventRow extends StatelessWidget {
final Map<String, dynamic> event;
const _EventRow({required this.event});
@override
Widget build(BuildContext context) {
final timestamp =
event['timestamp'] ?? event['time'] ?? event['created_at'] ?? '';
final action = event['action'] ?? event['command'] ?? event['type'] ?? '';
final targetId =
event['target_id'] ?? event['target'] ?? event['group_id'] ?? '';
final params = event['params'] ?? event['details'] ?? '';
final actor = event['actor'] ?? event['user'] ?? event['key_name'] ?? '';
final formattedTime = _formatTime(timestamp.toString());
return Card(
margin: const EdgeInsets.only(bottom: 4),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Время
SizedBox(
width: 80,
child: Text(
formattedTime,
style: const TextStyle(
fontSize: 11,
color: Colors.white38,
fontFamily: 'monospace',
),
),
),
const SizedBox(width: 8),
// Контент
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$action - $targetId',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
if (params.toString().isNotEmpty)
Text(
params.toString(),
style: const TextStyle(
fontSize: 11,
color: Colors.white38,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (actor.toString().isNotEmpty)
Text(
actor.toString(),
style: const TextStyle(
fontSize: 10,
color: Colors.white24,
),
),
],
),
),
],
),
),
);
}
String _formatTime(String iso) {
if (iso.isEmpty) return '';
try {
final d = DateTime.parse(iso);
String pad(int n) => n.toString().padLeft(2, '0');
return '${pad(d.day)}.${pad(d.month)} ${pad(d.hour)}:${pad(d.minute)}:${pad(d.second)}';
} catch (_) {
return iso;
}
}
}