import 'dart:io'; import 'package:flutter/material.dart'; import 'package:async/async.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/logs/log_tile.dart'; import 'package:adguard_home_manager/screens/logs/details/log_details_screen.dart'; import 'package:adguard_home_manager/services/api_client.dart'; import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:adguard_home_manager/models/logs.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart'; class LogsListClient extends StatefulWidget { final String ip; final String? name; final ServersProvider serversProvider; final AppConfigProvider appConfigProvider; final bool splitView; const LogsListClient({ super.key, required this.ip, this.name, required this.serversProvider, required this.appConfigProvider, required this.splitView, }); @override State createState() => _LogsListClientState(); } class _LogsListClientState extends State { late ScrollController scrollController; bool isLoadingMore = false; int logsQuantity = 100; int offset = 0; int loadStatus = 0; LogsData? logsData; String previousIp = ""; bool showDivider = true; CancelableOperation? cancelableRequest; Future fetchLogs({ int? inOffset, bool? loadingMore, String? responseStatus, String? searchText, }) async { final serversProvider = Provider.of(context, listen: false); int offst = inOffset ?? offset; if (loadingMore != null && loadingMore == true) { setState(() => isLoadingMore = true); } if (cancelableRequest != null) cancelableRequest!.cancel(); cancelableRequest = CancelableOperation.fromFuture( serversProvider.apiClient2!.getLogs( count: logsQuantity, offset: offst, search: '"${widget.ip}"' ) ); final result = await cancelableRequest?.value as ApiResponse; if (!mounted) return; if (loadingMore != null && loadingMore == true && mounted) { setState(() => isLoadingMore = false); } if (result.successful == true) { setState(() => offset = inOffset != null ? inOffset+logsQuantity : offset+logsQuantity); if (loadingMore != null && loadingMore == true && logsData != null) { LogsData newLogsData = result.content; newLogsData.data = [...logsData!.data, ...result.content.data]; setState(() => logsData = newLogsData); } else { LogsData newLogsData = result.content; setState(() => logsData = newLogsData); } setState(() => loadStatus = 1); } else { setState(() => loadStatus = 2); } } void scrollListener() { if (scrollController.position.extentAfter < 500 && isLoadingMore == false) { fetchLogs(loadingMore: true); } if (scrollController.position.pixels > 0) { setState(() => showDivider = false); } else { setState(() => showDivider = true); } } @override void initState() { scrollController = ScrollController()..addListener(scrollListener); fetchLogs(inOffset: 0); setState(() => previousIp = widget.ip); super.initState(); } @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; if (widget.ip != previousIp) { setState(() => loadStatus = 0); fetchLogs(inOffset: 0); setState(() => previousIp = widget.ip); } return Material( child: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar.large( pinned: true, floating: true, centerTitle: false, forceElevated: innerBoxIsScrolled, title: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocalizations.of(context)!.client, style: const TextStyle( fontSize: 24 ), ), const SizedBox(height: 4), Text( widget.name != null && widget.name != '' ? widget.name! : widget.ip, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.secondary ), ) ], ), ), surfaceTintColor: isDesktop(MediaQuery.of(context).size.width) ? Colors.transparent : null, actions: [ if (!(Platform.isAndroid || Platform.isIOS)) ...[ IconButton( onPressed: fetchLogs, icon: const Icon(Icons.refresh_rounded), tooltip: AppLocalizations.of(context)!.refresh, ), const SizedBox(width: 8) ] ], ) ) ], body: SafeArea( top: false, bottom: false, child: Builder( builder: (context) => RefreshIndicator( onRefresh: fetchLogs, displacement: 95, child: CustomScrollView( slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), if (loadStatus == 0) SliverFillRemaining( child: SizedBox( width: double.maxFinite, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ const CircularProgressIndicator(), const SizedBox(height: 30), Text( AppLocalizations.of(context)!.loadingLogs, textAlign: TextAlign.center, style: TextStyle( fontSize: 22, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ) ], ), ) ), if (loadStatus == 1 && logsData!.data.isNotEmpty) SliverList.builder( itemCount: isLoadingMore == true ? logsData!.data.length+1 : logsData!.data.length, itemBuilder: (context, index) { if (isLoadingMore == true && index == logsData!.data.length) { return const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Center( child: CircularProgressIndicator(), ), ); } else { return LogTile( log: logsData!.data[index], index: index, length: logsData!.data.length, useAlwaysNormalTile: true, onLogTap: (log) => { if (width > 700) { showDialog( context: context, builder: (context) => LogDetailsScreen( log: log, dialog: true, twoColumns: widget.splitView, ) ) } else { Navigator.of(context).push( MaterialPageRoute( builder: (context) => LogDetailsScreen( log: log, dialog: false, twoColumns: widget.splitView, ) ) ) } }, twoColumns: widget.splitView, ); } } ), if (loadStatus == 1 && logsData!.data.isEmpty) SliverFillRemaining( child: Center( child: Text( AppLocalizations.of(context)!.noLogsDisplay, textAlign: TextAlign.center, style: TextStyle( fontSize: 22, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), ), if (loadStatus == 2) SliverFillRemaining( child: SizedBox( width: double.maxFinite, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon( Icons.error, color: Colors.red, size: 50, ), const SizedBox(height: 30), Text( AppLocalizations.of(context)!.logsNotLoaded, textAlign: TextAlign.center, style: TextStyle( fontSize: 22, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ) ], ), ), ) ], ), ), ) ) ), ); } }