import 'dart:math'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/providers/providers.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/models.dart'; import '../widgets/widgets.dart'; class LogsFragment extends ConsumerStatefulWidget { const LogsFragment({super.key}); @override ConsumerState createState() => _LogsFragmentState(); } class _LogsFragmentState extends ConsumerState with PageMixin { final _logsStateNotifier = ValueNotifier( LogsState(loading: true), ); late ScrollController _scrollController; double _currentMaxWidth = 0; final _tag = CacheTag.rules; bool _isLoad = false; List _logs = []; @override void initState() { super.initState(); final position = globalState.cacheScrollPosition[_tag] ?? -1; _scrollController = ScrollController( initialScrollOffset: position > 0 ? position : double.maxFinite, ); _logs = globalState.appState.logs.list; _logsStateNotifier.value = _logsStateNotifier.value.copyWith( logs: _logs, ); ref.listenManual( isCurrentPageProvider( PageLabel.logs, handler: (pageLabel, viewMode) => pageLabel == PageLabel.tools && viewMode == ViewMode.mobile, ), (prev, next) { if (prev != next && next == true) { initPageState(); } }, fireImmediately: true, ); ref.listenManual( logsProvider.select((state) => state.list), (prev, next) { if (prev != next) { final isEquality = logListEquality.equals(prev, next); if (!isEquality) { _logs = next; updateLogsThrottler(); } } }, ); } @override List get actions => [ IconButton( onPressed: () { _handleExport(); }, icon: const Icon( Icons.file_download_outlined, ), ), ]; @override get onSearch => (value) { _logsStateNotifier.value = _logsStateNotifier.value.copyWith( query: value, ); }; @override get onKeywordsUpdate => (keywords) { _logsStateNotifier.value = _logsStateNotifier.value.copyWith(keywords: keywords); }; @override void dispose() { _logsStateNotifier.dispose(); _scrollController.dispose(); super.dispose(); } _handleExport() async { final commonScaffoldState = context.commonScaffoldState; final res = await commonScaffoldState?.loadingRun( () async { return await globalState.appController.exportLogs(); }, title: appLocalizations.exportLogs, ); if (res != true) return; globalState.showMessage( title: appLocalizations.tip, message: TextSpan(text: appLocalizations.exportSuccess), ); } double _getItemHeight(Log log) { final measure = globalState.measure; final bodySmallHeight = measure.bodySmallHeight; final bodyMediumHeight = measure.bodyMediumHeight; final height = globalState.measure .computeTextSize( Text( log.payload, style: context.textTheme.bodyLarge, ), maxWidth: _currentMaxWidth, ) .height; return height + bodySmallHeight + 8 + bodyMediumHeight + 40 + 8; } updateLogsThrottler() { throttler.call("logs", () { final isEquality = logListEquality.equals( _logs, _logsStateNotifier.value.logs, ); if (isEquality) { return; } WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _logsStateNotifier.value = _logsStateNotifier.value.copyWith( logs: _logs, ); } }); }, duration: commonDuration); } _preLoad() { if (_isLoad == true) { return; } _isLoad = true; WidgetsBinding.instance.addPostFrameCallback((_) async { if (!mounted) { return; } final isMobileView = ref.read(isMobileViewProvider); if (isMobileView) { await Future.delayed(Duration(milliseconds: 300)); } final parts = _logs.batch(10); globalState.cacheHeightMap[_tag] ??= FixedMap( _logs.length, ); for (int i = 0; i < parts.length; i++) { final part = parts[i]; await Future( () { for (final log in part) { globalState.cacheHeightMap[_tag]?.updateCacheValue( log.payload, () => _getItemHeight(log), ); } }, ); } _logsStateNotifier.value = _logsStateNotifier.value.copyWith( loading: false, ); }); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (_, constraints) { _currentMaxWidth = constraints.maxWidth - 40; return ValueListenableBuilder( valueListenable: _logsStateNotifier, builder: (_, state, __) { _preLoad(); final logs = state.list; final items = logs .map( (log) => LogItem( key: Key(log.dateTime), log: log, onClick: (value) { context.commonScaffoldState?.addKeyword(value); }, ), ) .separated( const Divider( height: 0, ), ) .toList(); final content = logs.isEmpty ? NullStatus( label: appLocalizations.nullLogsDesc, ) : Align( alignment: Alignment.topCenter, child: CommonScrollBar( controller: _scrollController, child: ScrollToEndBox( controller: _scrollController, tag: _tag, dataSource: logs, child: CacheItemExtentListView( tag: _tag, reverse: true, shrinkWrap: true, physics: NextClampingScrollPhysics(), controller: _scrollController, itemBuilder: (_, index) { return items[index]; }, itemExtentBuilder: (index) { if (index.isOdd) { return 0; } return _getItemHeight(logs[index ~/ 2]); }, itemCount: items.length, keyBuilder: (int index) { if (index.isOdd) { return "divider"; } return logs[index ~/ 2].payload; }, ), ), ), ); return FadeBox( child: state.loading ? Center( child: CircularProgressIndicator(), ) : content, ); }, ); }, ); } } class LogItem extends StatelessWidget { final Log log; final Function(String)? onClick; const LogItem({ super.key, required this.log, this.onClick, }); @override Widget build(BuildContext context) { return ListItem( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 4, ), title: SelectableText( log.payload, style: context.textTheme.bodyLarge, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SelectableText( log.dateTime, style: context.textTheme.bodySmall?.copyWith( color: context.colorScheme.primary, ), ), const SizedBox( height: 8, ), Container( alignment: Alignment.centerLeft, child: CommonChip( onPressed: () { if (onClick == null) return; onClick!(log.logLevel.name); }, label: log.logLevel.name, ), ), ], ), ); } }