library debug_console; import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:star_lock/mine/about/debug/controller.dart'; import 'package:star_lock/mine/about/debug/log.dart'; import 'package:star_lock/mine/about/debug/tile.dart'; /// # Debug Console /// /// A console for debugging Flutter apps, and displaying console messages on the widget. /// /// Check the console for prints and errors, while you're testing it, all within your app. Make your own logging or watch for console prints. /// /// ## Features /// /// * Log your messages /// * Display console messages and errors /// * Use different levels for better emphasis /// * Filter the logs /// * Add extra actions to execute from the Debug Console menu /// * Check StackTrace of errors class DebugConsole extends StatefulWidget { final DebugConsoleController controller; final List> actions; final bool expandStackTrace; final String? savePath; /// # Debug Console /// /// A console for debugging Flutter apps, and displaying console messages on the widget. /// /// Check the console for prints and errors, while you're testing it, all within your app. Make your own logging or watch for console prints. /// /// ## Features /// /// * Log your messages /// * Display console messages and errors /// * Use different levels for better emphasis /// * Filter the logs /// * Add extra actions to execute from the Debug Console menu /// * Check StackTrace of errors DebugConsole({ key, DebugConsoleController? controller, this.actions = const [], this.expandStackTrace = false, this.savePath, }) : controller = controller ?? DebugConsole.instance; @override State createState() => _DebugConsoleState(); static DebugConsoleController? _instance; static DebugConsoleController get instance { _instance ??= DebugConsoleController( logs: DebugConsoleLog.fromFile(DebugConsole.loadPath)); return _instance!; } static String loadPath = 'debug_console.log'; /// Adds a log to the root controller, attached with a message, level, timestamp and stack trace. /// /// The default level is `DebugConsoleLogLevel.normal`. /// /// Same as: /// ```dart /// DebugConsole.instance.log( ... ); /// ``` static void log( Object? message, { DebugConsoleLevel level = DebugConsoleLevel.normal, DateTime? timestamp, StackTrace? stackTrace, }) => instance.log( message, level: level, timestamp: timestamp, stackTrace: stackTrace, ); /// Clears the logs of the root controller. /// /// Same as: /// ```dart /// DebugConsole.instance.clear(); /// ``` static void clear() => instance.clear(); /// Adds a log to the root controller, with the level `DebugConsoleLevel.info`. /// /// Same as: /// ```dart /// DebugConsole.log(message, level: DebugConsoleLevel.info, ... ); /// ``` static void info( Object? message, { DateTime? timestamp, StackTrace? stackTrace, bool isErr = false, }) => log( message, level: isErr ? DebugConsoleLevel.error : DebugConsoleLevel.info, timestamp: timestamp, stackTrace: stackTrace, ); /// Adds a log to the root controller, with the level `DebugConsoleLevel.warning`. /// /// Same as: /// ```dart /// DebugConsole.log(message, level: DebugConsoleLevel.warning, ... ); /// ``` static void warning( Object? message, { DateTime? timestamp, StackTrace? stackTrace, }) => log( message, level: DebugConsoleLevel.warning, timestamp: timestamp, stackTrace: stackTrace, ); /// Adds a log to the root controller, with the level `DebugConsoleLevel.error`. /// /// Same as: /// ```dart /// DebugConsole.log(message, level: DebugConsoleLevel.error, ... ); /// ``` static void error( Object? message, { DateTime? timestamp, StackTrace? stackTrace, }) => log( message, level: DebugConsoleLevel.error, timestamp: timestamp, stackTrace: stackTrace, ); /// Adds a log to the root controller, with the level `DebugConsoleLevel.fatal`. /// /// Same as: /// ```dart /// DebugConsole.log(message, level: DebugConsoleLevel.fatal, ... ); /// ``` static void fatal( Object? message, { DateTime? timestamp, StackTrace? stackTrace, }) => log( message, level: DebugConsoleLevel.fatal, timestamp: timestamp, stackTrace: stackTrace, ); /// Adds a log to the root controller, with the level `DebugConsoleLevel.debug`. /// /// Same as: /// ```dart /// DebugConsole.log(message, level: DebugConsoleLevel.debug, ... ); /// ``` static void debug( Object? message, { DateTime? timestamp, StackTrace? stackTrace, }) => log( message, level: DebugConsoleLevel.debug, timestamp: timestamp, stackTrace: stackTrace, ); /// Listen for prints and errors, to catch all messages in your app. /// /// Everything inside that function will be automatically logged. /// /// ```dart /// DebugConsole.listen(() { /// runApp(const MyApp()); /// }); /// ``` /// /// * A controller can be given, instead of logging to the root. static void listen(void Function() body, {DebugConsoleController? controller}) { controller ??= DebugConsole.instance; runZoned(body, zoneSpecification: ZoneSpecification( handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, Object error, StackTrace stackTrace) { controller! .log(error, level: DebugConsoleLevel.error, stackTrace: stackTrace); parent.handleUncaughtError(zone, error, stackTrace); }, )); } } class _DebugConsoleState extends State { StreamSubscription>? subscription; List logs = []; bool expandStackTrace = false; bool save = true; String filter = ''; TextEditingController? textController; @override void initState() { super.initState(); textController = TextEditingController(); expandStackTrace = widget.expandStackTrace; logs = widget.controller.logs; subscription = widget.controller.stream.listen((logs) { if (widget.savePath != null && save) saveToFile(logs: logs); logs.sort((a, b) => a.timestamp.compareTo(b.timestamp)); logs = logs.reversed.toList(); setState(() => this.logs = logs); }); } @override void dispose() { subscription?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { final filteredLogs = filter.isEmpty ? logs : logs .where((log) => filter.split(',').any((filter) { filter = filter.trim(); return filter.isNotEmpty && log.message.toLowerCase().contains(filter.toLowerCase()); })) .toList(); return Scaffold( appBar: AppBar( title: const Text('日志记录'), actions: [ PopupMenuButton( icon: const Icon(Icons.more_vert), itemBuilder: (context) => [ ...widget.actions, if (widget.actions.isNotEmpty) const PopupMenuDivider(), PopupMenuItem( child: StatefulBuilder( builder: (context, setCheckboxState) => Row( children: [ const Expanded(child: Text('暂停日志记录')), Checkbox( value: subscription!.isPaused, onChanged: (value) => setCheckboxState(() => toggleLogging(!value!)), ), ], ), ), onTap: () => toggleLogging(), ), PopupMenuItem( child: StatefulBuilder( builder: (context, setCheckboxState) => Row( children: [ const Expanded(child: Text('展开 StackTrace')), Checkbox( value: expandStackTrace, onChanged: (value) => setCheckboxState( () => setState(() => expandStackTrace = value!)), ), ], ), ), onTap: () => setState(() => expandStackTrace = !expandStackTrace), ), if (widget.savePath != null) PopupMenuItem( child: StatefulBuilder( builder: (context, setCheckboxState) => Row( children: [ const Expanded(child: Text('保存')), Checkbox( value: save, onChanged: (value) { if (value!) saveToFile(); setCheckboxState( () => setState(() => save = value)); }, ), ], ), ), onTap: () { if (!save) saveToFile(); setState(() => save = !save); }, ), PopupMenuItem( onTap: () => widget.controller.clear(), child: const Text('清除日志'), ), PopupMenuItem( onTap: () { // widget.controller.showMore(context); }, child: const Text('更多'), ), ], ) ], ), body: Stack( children: [ logs.isEmpty ? const Padding( padding: EdgeInsets.only(bottom: 50), child: Center(child: Text('没有日志')), ) : ListView.builder( padding: const EdgeInsets.only(bottom: 75), reverse: true, itemCount: filteredLogs.length, itemBuilder: (context, index) { final log = filteredLogs[index]; return DebugConsoleTile(log, key: ValueKey(log.timestamp), expanded: expandStackTrace); }, ), Positioned( bottom: 0, left: 0, right: 0, child: Container( margin: const EdgeInsets.all(15), child: Row( children: [ Expanded( child: TextField( controller: textController, decoration: InputDecoration( filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric(horizontal: 15), hintText: 'Filter logs', border: OutlineInputBorder( borderRadius: BorderRadius.circular(30), ), ), onChanged: (value) => setState(() => filter = value), ), ), const SizedBox(width: 5), IconButton( icon: const Icon(Icons.close), onPressed: () { textController!.clear(); setState(() => filter = ''); }, ), const SizedBox(width: 5), FloatingActionButton( tooltip: subscription!.isPaused ? 'Resume logging' : 'Pause logging', onPressed: () => toggleLogging(), child: Icon(subscription!.isPaused ? Icons.play_arrow : Icons.pause), ), ], ), ), ), ], ), ); } void saveToFile({List? logs, String? path}) { path ??= widget.savePath; if (path == null) return; logs ??= this.logs; logs.sort((a, b) => a.timestamp.compareTo(b.timestamp)); logs = logs.reversed.toList(); final file = File(path); if (logs.isEmpty) { file.delete(); } else { file.writeAsString( logs.map((log) => log.toString()).join('\n'), ); } } void toggleLogging([bool? paused]) { paused ??= subscription!.isPaused; setState(() { if (paused!) { subscription!.resume(); } else { subscription!.pause(); } }); } }