import 'dart:io'; import 'package:logger/logger.dart'; import 'package:intl/intl.dart'; import 'package:permission_handler/permission_handler.dart'; /// Global logger instance late final Logger appLogger; /// Log file output handler late final FileLogOutput fileLogOutput; /// Initialize logging (call once in `main()`) Future initLogging() async { await requestStoragePermission(); fileLogOutput = FileLogOutput(); appLogger = Logger( printer: PrettyPrinter( methodCount: 0, printTime: true, colors: true, printEmojis: true, ), output: MultiOutput([ ConsoleOutput(), // ✅ Console will use the top-level PrettyPrinter fileLogOutput, // ✅ File will still use the SimpleFileLogPrinter ]), level: Level.debug, ); } /// Request storage permission (for Android 11+) Future requestStoragePermission() async { final status = await Permission.manageExternalStorage.status; if (!status.isGranted) { await Permission.manageExternalStorage.request(); } } /// Safe logger wrapper void logSafe( String message, { LogLevel level = LogLevel.info, dynamic error, StackTrace? stackTrace, bool sensitive = false, }) { if (sensitive) return; switch (level) { case LogLevel.debug: appLogger.d(message, error: error, stackTrace: stackTrace); break; case LogLevel.warning: appLogger.w(message, error: error, stackTrace: stackTrace); break; case LogLevel.error: appLogger.e(message, error: error, stackTrace: stackTrace); break; case LogLevel.verbose: appLogger.v(message, error: error, stackTrace: stackTrace); break; default: appLogger.i(message, error: error, stackTrace: stackTrace); } } /// Custom log output that writes to a local `.txt` file class FileLogOutput extends LogOutput { File? _logFile; /// Initialize log file in Downloads/marco_logs/log_YYYY-MM-DD.txt Future _init() async { if (_logFile != null) return; final directory = Directory('/storage/emulated/0/Download/marco_logs'); if (!await directory.exists()) { await directory.create(recursive: true); } final date = DateFormat('yyyy-MM-dd').format(DateTime.now()); final filePath = '${directory.path}/log_$date.txt'; _logFile = File(filePath); if (!await _logFile!.exists()) { await _logFile!.create(); } await _cleanOldLogs(directory); } @override void output(OutputEvent event) async { await _init(); if (event.lines.isEmpty) return; final logMessage = event.lines.join('\n') + '\n'; await _logFile!.writeAsString( logMessage, mode: FileMode.append, flush: true, ); } Future getLogFilePath() async { await _init(); return _logFile!.path; } Future clearLogs() async { await _init(); await _logFile!.writeAsString(''); } Future readLogs() async { await _init(); return _logFile!.readAsString(); } /// Delete logs older than 3 days Future _cleanOldLogs(Directory directory) async { final files = directory.listSync(); final now = DateTime.now(); for (var file in files) { if (file is File && file.path.endsWith('.txt')) { final stat = await file.stat(); if (now.difference(stat.modified).inDays > 3) { await file.delete(); } } } } } /// A simple, readable log printer for file output class SimpleFileLogPrinter extends LogPrinter { @override List log(LogEvent event) { final message = event.message.toString(); if (message.contains('[SENSITIVE]')) return []; final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()); final level = event.level.name.toUpperCase(); final error = event.error != null ? ' | ERROR: ${event.error}' : ''; final stack = event.stackTrace != null ? '\nSTACKTRACE:\n${event.stackTrace}' : ''; return ['[$timestamp] [$level] $message$error$stack']; } } /// Optional log level enum for better type safety enum LogLevel { debug, info, warning, error, verbose }