From 3fad3812e117c6bc16b5007076803f498538e4c4 Mon Sep 17 00:00:00 2001 From: Mica White Date: Mon, 8 Dec 2025 19:54:36 -0500 Subject: First commit --- lib/logs.dart | 242 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100755 lib/logs.dart (limited to 'lib/logs.dart') diff --git a/lib/logs.dart b/lib/logs.dart new file mode 100755 index 0000000..78f43c2 --- /dev/null +++ b/lib/logs.dart @@ -0,0 +1,242 @@ +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()), + ), + ), + ]), + ), + ); + } +} -- cgit v1.2.3