diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 39ec621..609c616 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -795,5 +795,7 @@ "clientIpCopied": "Client IP copied to the clipboard", "clientNameCopied": "Client name copied to the clipboard", "dnsServerAddressCopied": "DNS server address copied to the clipboard", - "select": "Select" + "select": "Select", + "liveLogs": "Live logs", + "hereWillAppearRealtimeLogs": "Here there will appear the logs on realtime." } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 399f429..231eeaf 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -795,5 +795,7 @@ "clientIpCopied": "Dirección IP del cliente copiada al portapapeles", "clientNameCopied": "Nombre del cliente copiado al portapapeles", "dnsServerAddressCopied": "Dirección del servidor DNS copiada al portapapeles", - "select": "Seleccionar" + "select": "Seleccionar", + "liveLogs": "Registros en directo", + "hereWillAppearRealtimeLogs": "Aquí aparecerán los registros en tiempo real." } \ No newline at end of file diff --git a/lib/providers/live_logs_provider.dart b/lib/providers/live_logs_provider.dart new file mode 100644 index 0000000..044dd57 --- /dev/null +++ b/lib/providers/live_logs_provider.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +import 'package:adguard_home_manager/models/logs.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; + +class LiveLogsProvider with ChangeNotifier { + ServersProvider? _serversProvider; + + update(ServersProvider? provider) { + _serversProvider = provider; + } + + bool _isDisposed = false; + + @override + void dispose() { + _isDisposed = true; + super.dispose(); + } + + List _logs = []; + + List get logs { + return _logs; + } + + DateTime? _lastTime; + + void startFetchLogs() { + _lastTime = DateTime.now(); + _fetchLogs(); + } + + void _fetchLogs() async { + if (_lastTime == null) return; + final result = await _serversProvider!.apiClient2!.getLogs( + count: 100 + ); + if (result.successful == false || result.content == null) return; + final valid = (result.content as LogsData).data.where((e) => e.time.isAfter(_lastTime!)); + _logs = [...valid, ..._logs]; + _lastTime = DateTime.now(); + notifyListeners(); + + await Future.delayed(const Duration(seconds: 2)); + if (_isDisposed == true) return; + _fetchLogs(); + } +} \ No newline at end of file diff --git a/lib/screens/logs/live/live_logs_screen.dart b/lib/screens/logs/live/live_logs_screen.dart new file mode 100644 index 0000000..7d20374 --- /dev/null +++ b/lib/screens/logs/live/live_logs_screen.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/logs/details/log_details_screen.dart'; +import 'package:adguard_home_manager/screens/logs/log_tile.dart'; + +import 'package:adguard_home_manager/providers/live_logs_provider.dart'; + +class LiveLogsScreen extends StatefulWidget { + const LiveLogsScreen({super.key}); + + @override + State createState() => _LiveLogsScreenState(); +} + +class _LiveLogsScreenState extends State { + @override + void initState() { + Provider.of(context, listen: false).startFetchLogs(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final liveLogsProvider = Provider.of(context); +print(liveLogsProvider.logs.length); + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + title: Text(AppLocalizations.of(context)!.liveLogs), + ) + ) + ], + body: SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (liveLogsProvider.logs.isEmpty) SliverFillRemaining( + child: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + AppLocalizations.of(context)!.hereWillAppearRealtimeLogs, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 24 + ), + ), + ), + ), + ), + if (liveLogsProvider.logs.isNotEmpty) SliverList.builder( + itemCount: liveLogsProvider.logs.length, + itemBuilder: (context, index) => LogTile( + log: liveLogsProvider.logs[index], + length: liveLogsProvider.logs.length, + index: index, + onLogTap: (log) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => LogDetailsScreen( + log: log, + dialog: false, + ) + ) + ); + }, + twoColumns: false + ), + ) + ], + ), + ) + ) + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/logs/logs_list_appbar.dart b/lib/screens/logs/logs_list_appbar.dart index 5014806..88c1c29 100644 --- a/lib/screens/logs/logs_list_appbar.dart +++ b/lib/screens/logs/logs_list_appbar.dart @@ -6,11 +6,14 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/screens/logs/live/live_logs_screen.dart'; import 'package:adguard_home_manager/screens/logs/filters/logs_filters_modal.dart'; import 'package:adguard_home_manager/config/globals.dart'; import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/providers/live_logs_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/models/applied_filters.dart'; import 'package:adguard_home_manager/providers/logs_provider.dart'; @@ -74,6 +77,22 @@ class LogsListAppBar extends StatelessWidget { ); } + void openLiveLogsScreen() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => MultiProvider( + providers: [ + ChangeNotifierProxyProvider( + create: (context) => LiveLogsProvider(), + update: (context, servers, logs) => logs!..update(servers), + ), + ], + child: const LiveLogsScreen() + ) + ) + ); + } + final Map translatedString = { "all": AppLocalizations.of(context)!.all, "filtered": AppLocalizations.of(context)!.filtered, @@ -104,10 +123,29 @@ class LogsListAppBar extends StatelessWidget { icon: const Icon(Icons.search_rounded), tooltip: AppLocalizations.of(context)!.search, ), - if (logsProvider.loadStatus == LoadStatus.loaded) IconButton( - onPressed: openFilersModal, - icon: const Icon(Icons.filter_list_rounded), - tooltip: AppLocalizations.of(context)!.filters, + if (logsProvider.loadStatus == LoadStatus.loaded) PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + onTap: openFilersModal, + child: Row( + children: [ + const Icon(Icons.filter_list_rounded), + const SizedBox(width: 10), + Text(AppLocalizations.of(context)!.filters) + ], + ) + ), + PopupMenuItem( + onTap: openLiveLogsScreen, + child: Row( + children: [ + const Icon(Icons.stream_rounded), + const SizedBox(width: 10), + Text(AppLocalizations.of(context)!.liveLogs) + ], + ) + ), + ], ), const SizedBox(width: 8), ], diff --git a/lib/services/api_client.dart b/lib/services/api_client.dart index 9bc283e..ce3e033 100644 --- a/lib/services/api_client.dart +++ b/lib/services/api_client.dart @@ -206,14 +206,14 @@ class ApiClientV2 { } Future getLogs({ - required int count, + int? count, int? offset, DateTime? olderThan, String? responseStatus, String? search }) async { final result = await HttpRequestClient.get( - urlPath: '/querylog?limit=$count${offset != null ? '&offset=$offset' : ''}${olderThan != null ? '&older_than=${olderThan.toIso8601String()}' : ''}${responseStatus != null ? '&response_status=$responseStatus' : ''}${search != null ? '&search=$search' : ''}', + urlPath: '/querylog?${count != null ? 'limit=$count' : ''}${offset != null ? '&offset=$offset' : ''}${olderThan != null ? '&older_than=${olderThan.toIso8601String()}' : ''}${responseStatus != null ? '&response_status=$responseStatus' : ''}${search != null ? '&search=$search' : ''}', server: server ); if (result.successful == true) {