relatica/lib/screens/logviewer_screen.dart
2023-11-27 14:52:16 -06:00

224 lines
6.7 KiB
Dart

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<LogViewerScreen> createState() => _LogViewerScreenState();
}
class _LogViewerScreenState extends State<LogViewerScreen> {
var filterText = '';
var filterByText = false;
var filterByModule = false;
var filterModuleName = '';
var attemptingWrite = false;
Future<void> _writeEventsLog(List<LogRecord> 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<SettingsService>();
final logService = context.watch<LogService>();
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<Level>(
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<LogRecord> 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<String>(
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<LogRecord> 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,
),
);
}
}