import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_file_dialog/flutter_file_dialog.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import '../controls/padding.dart'; import '../controls/responsive_max_width.dart'; import '../controls/standard_appbar.dart'; import '../services/log_service.dart'; import '../services/setting_service.dart'; import '../utils/clipboard_utils.dart'; import '../utils/dateutils.dart'; import '../utils/json_printer.dart'; import '../utils/logrecord_extensions.dart'; import '../utils/snackbar_builder.dart'; final _logger = Logger('LogViewerScreen'); class LogViewerScreen extends StatefulWidget { const LogViewerScreen({super.key}); @override State createState() => _LogViewerScreenState(); } class _LogViewerScreenState extends State { var filterText = ''; var filterByText = false; var filterByModule = false; var filterModuleName = ''; var attemptingWrite = false; Future _writeEventsLog(List events) async { if (attemptingWrite) { return; } attemptingWrite = true; try { final json = PrettyJsonEncoder().convert(events.map((e) => e.toJson()).toList()); final filename = 'EventsLog_${DateTime.now().toFileNameString()}.json'; if (Platform.isAndroid || Platform.isIOS) { final params = SaveFileDialogParams( data: Uint8List.fromList(json.codeUnits), fileName: filename, ); await FlutterFileDialog.saveFile(params: params); if (mounted) { buildSnackbar(context, 'Wrote Events Log to: $filename'); } } else { final appsDir = await getApplicationDocumentsDirectory(); final location = await FilePicker.platform.saveFile( dialogTitle: 'Save Events Log', fileName: filename, initialDirectory: appsDir.path, ); if (location != null) { await File(location).writeAsString(json); if (mounted) { buildSnackbar(context, 'Wrote Events Log to: $location'); } } } } catch (e) { _logger.severe('Error attempting to write out log: $e'); if (mounted) { buildSnackbar(context, 'Error attempting to write out log: $e'); } } attemptingWrite = false; } @override Widget build(BuildContext context) { final settings = context.watch(); final logService = context.watch(); final events = logService.events; return Scaffold( appBar: StandardAppBar.build(context, 'Log Viewer'), body: Center( child: Padding( padding: const EdgeInsets.all(8.0), child: ResponsiveMaxWidth( child: Column( children: [ buildLogPanel(settings), buildModuleFilter(settings, events), buildTextSearchPanel(), const VerticalPadding(), buildLogList(settings.logLevel, events), ], ), ), ), ), floatingActionButton: FloatingActionButton.small( onPressed: () => _writeEventsLog(events), child: const Icon(Icons.save), ), ); } Widget buildLogPanel(SettingsService settings) { return ListTile( title: const Text('Log Level'), trailing: DropdownButton( value: settings.logLevel, items: Level.LEVELS .map((c) => DropdownMenuItem(value: c, child: Text(c.name))) .toList(), onChanged: (value) { settings.logLevel = value ?? Level.OFF; }), ); } Widget buildModuleFilter(SettingsService settings, List events) { final modules = events.map((e) => e.loggerName).toSet().toList(); modules.sort(); modules.add(''); return ListTile( leading: Checkbox( value: filterByModule, onChanged: (bool? value) { setState(() { filterByModule = value ?? false; }); }, ), title: filterByModule ? null : const Text('Filter by module'), trailing: !filterByModule ? null : DropdownButton( value: filterModuleName, items: modules .map((c) => DropdownMenuItem(value: c, child: Text(c))) .toList(), onChanged: (value) { setState(() { filterModuleName = value ?? ''; }); }), ); } Widget buildTextSearchPanel() { return ListTile( leading: Checkbox( value: filterByText, onChanged: (value) { setState(() { filterByText = value ?? false; }); }, ), title: TextField( enabled: filterByText, onChanged: (value) { setState(() { filterText = value.toLowerCase(); }); }, decoration: InputDecoration( labelText: 'Filter by text', alignLabelWithHint: true, border: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).highlightColor, ), borderRadius: BorderRadius.circular(5.0), ), ), ), ); } Widget buildLogList(Level logLevel, List allEvents) { final events = allEvents.where((e) { final levelFilterPasses = e.level >= logLevel; final passesTextFilter = filterByText ? e.message.toLowerCase().contains(filterText.toLowerCase()) : true; final passesModuleFilter = filterByModule ? filterModuleName.isEmpty || e.loggerName == filterModuleName : true; return levelFilterPasses && passesTextFilter && passesModuleFilter; }).toList(); return Expanded( child: ListView.separated( itemBuilder: (context, index) { final event = events[index]; return ListTile( onLongPress: () { copyToClipboard( context: context, text: jsonEncode(event.toJson()), ); }, titleAlignment: ListTileTitleAlignment.titleHeight, leading: Text(event.level.toString()), title: Text('${event.loggerName} at ${event.time.toIso8601String()}'), subtitle: Text( event.message, softWrap: true, ), ); }, separatorBuilder: (_, __) => const Divider(), itemCount: events.length, ), ); } }