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<String>) onChanged;
final List<LogType> 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<LogType, Widget> 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<LogEntry> logEntries;
@override
State<StatefulWidget> createState() => _LogPageState();
}
class _LogPageState extends State<LogPage> {
Set<LogType> runtimeLogsFilter = {LogType.error, LogType.warning};
Set<LogType> scriptLogsFilter = Set.from(LogType.values);
String searchFilter = '';
Function(List<String>) _generateOnFilter(Set<LogType> 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()),
),
),
]),
),
);
}
}
|