import 'package:flutter/material.dart'; import 'package:multiselect/multiselect.dart'; enum LogType { error, warning, info, debug, trace, } extension LogTypeExtension on LogType { static LogType parse(String logType) { final logTypeS = logType.toLowerCase().trim(); switch (logTypeS) { case 'error': return LogType.error; case 'warning': return LogType.warning; case 'info': return LogType.info; case 'debug': return LogType.debug; case 'trace': return LogType.trace; default: // there's no particular reason why this should be the default // but hopefully this line is never needed return LogType.error; } } String name() { switch (this) { case LogType.error: return "Error"; case LogType.warning: return "Warning"; case LogType.info: return "Info"; case LogType.debug: return "Debug"; case LogType.trace: return "Trace"; } } } class LogEntry { final bool scriptLog; final LogType logType; final String? file; final int? lineNumber; final String message; final DateTime time; LogEntry( this.scriptLog, this.logType, this.file, this.lineNumber, this.message, ) : this.time = DateTime.now(); } class _LogFilter extends StatelessWidget { const _LogFilter(this.label, this.onChanged, this.selectedValues); final String label; final Function(List) onChanged; final List selectedValues; @override Widget build(BuildContext context) { return Row( children: [ Text( this.label, style: const TextStyle(fontSize: 16), ), const SizedBox(width: 10), SizedBox( width: 200, height: 40, child: DropDownMultiSelect( options: LogType.values.map((e) => e.name()).toList(), selectedValues: this.selectedValues.map((e) => e.name()).toList(), whenEmpty: "Filter Script Logs", isDense: true, onChanged: this.onChanged, ), ), ], ); } } class _LogLine extends StatelessWidget { const _LogLine(this.entry); final LogEntry entry; @override Widget build(BuildContext context) { Map icons = { LogType.error: Icon( Icons.error, color: Colors.red[300], size: 24, ), LogType.warning: Icon( Icons.warning, color: Colors.yellow[300], size: 24, ), LogType.info: Icon( Icons.info, color: Colors.blue[300], size: 24, ), LogType.debug: Icon( Icons.bug_report, color: Colors.green[300], size: 24, ), LogType.trace: Icon( Icons.message, color: Colors.grey[300], size: 24, ), }; return Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: DecoratedBox( decoration: BoxDecoration( color: Colors.purple.shade900.withAlpha(128), border: Border.all(color: Colors.purple.shade800, width: 3), borderRadius: const BorderRadius.all(Radius.circular(4)), ), child: Padding( padding: const EdgeInsets.all(6), child: Row( children: [ icons[this.entry.logType]!, const SizedBox(width: 5), SizedBox(width: 70, child: Text(this.entry.logType.name())), SizedBox(width: 54, child: Text(this.entry.scriptLog ? 'Script' : 'Runtime')), const SizedBox(width: 20), Expanded( child: Text( this.entry.message, style: const TextStyle(fontFamily: 'Consolas'), ), ), const SizedBox(width: 20), Text("${this.entry.file}:${this.entry.lineNumber}"), const SizedBox(width: 10), Text( "${this.entry.time.hour.toString().padLeft(2, '0')}:${this.entry.time.minute.toString().padLeft(2, '0')}") ], ), ), ), ); } } class LogPage extends StatefulWidget { const LogPage(this.logEntries, {super.key}); final List logEntries; @override State createState() => _LogPageState(); } class _LogPageState extends State { Set runtimeLogsFilter = {LogType.error, LogType.warning}; Set scriptLogsFilter = Set.from(LogType.values); String searchFilter = ''; Function(List) _generateOnFilter(Set filter) { return (values) { setState(() { filter.clear(); filter.addAll(values.map((e) => LogTypeExtension.parse(e))); }); }; } @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(20), child: Column(children: [ Row(children: [ Expanded( child: SizedBox( height: 40, child: TextField( autocorrect: false, onChanged: (search) => setState(() => this.searchFilter = search), decoration: const InputDecoration(border: OutlineInputBorder(), labelText: 'Search'), ), ), ), const SizedBox(width: 80), _LogFilter( "Runtime Logs Filter:", _generateOnFilter(this.runtimeLogsFilter), runtimeLogsFilter.toList(), ), const SizedBox(width: 80), _LogFilter( "Script Logs Filter:", _generateOnFilter(this.scriptLogsFilter), scriptLogsFilter.toList(), ), ]), Expanded( child: SingleChildScrollView( reverse: true, child: Column( children: this .widget .logEntries .where((e) => ((e.scriptLog && this.scriptLogsFilter.contains(e.logType)) || (!e.scriptLog && this.runtimeLogsFilter.contains(e.logType))) && (this.searchFilter.isEmpty || e.message.contains(this.searchFilter))) .map((e) => _LogLine(e)) .toList()), ), ), ]), ), ); } }