// Fejlesztési log service — fájlba írás terepi teszteléshez. // Az app külső tárhelyére ír, onnan könnyen elérhető. // // Használat: // AppLogger.i('startRecording', 'Track indítva: $name'); // AppLogger.w('_onPosition', 'Ugrás kiszűrve: ${dist}m'); // AppLogger.e('_onPosition', 'GPS hiba', error: e); // // Log fájl helye: /sdcard/Android/data/hu.app_dev.terepi_seged/files/logs/ // Elérhető: Android Studio Device Explorer, adb pull, vagy fájlkezelő app import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; enum _Level { info, warning, error } class AppLogger extends GetxService { static AppLogger get to => Get.find(); // ── Konfiguráció ────────────────────────────────────────────────── /// Csak fejlesztési módban logol (release build-ben kikapcsol) static const bool _enableInRelease = true; /// Max log fájl méret MB-ban — felette rotál static const int _maxSizeMb = 5; /// Max megőrzött log fájlok száma static const int _maxFiles = 5; // ── Belső állapot ───────────────────────────────────────────────── File? _logFile; IOSink? _sink; String? _logDir; static final _timeFmt = DateFormat('HH:mm:ss.SSS'); static final _dateFmt = DateFormat('yyyy-MM-dd'); static final _fileFmt = DateFormat('yyyy-MM-dd_HH-mm-ss'); final isEnabled = false.obs; final currentLogPath = ''.obs; // ── Lifecycle ───────────────────────────────────────────────────── @override Future onReady() async { super.onReady(); if (!kDebugMode && !_enableInRelease) return; try { await _initLogFile(); isEnabled.value = true; i('AppLogger', 'Log indítva — ${currentLogPath.value}'); } catch (e) { debugPrint('AppLogger init hiba: $e'); } } @override Future onClose() async { await _sink?.flush(); await _sink?.close(); super.onClose(); } // ── Publikus API ────────────────────────────────────────────────── /// Info szintű log static void i(String tag, String message) => _write(_Level.info, tag, message); /// Figyelmeztetés static void w(String tag, String message, {Object? error}) => _write(_Level.warning, tag, message, error: error); /// Hiba static void e(String tag, String message, {Object? error, StackTrace? stack}) => _write(_Level.error, tag, message, error: error, stack: stack); /// Szeparátor — jól látható elválasztó a logban static void separator(String label) { _write(_Level.info, '─────', '─── $label ───────────────────────'); } // ── Log fájl megosztása ─────────────────────────────────────────── Future shareLogs() async { await _sink?.flush(); final dir = _logDir; if (dir == null) return; final files = Directory(dir) .listSync() .whereType() .where((f) => f.path.endsWith('.log')) .toList() ..sort((a, b) => b.path.compareTo(a.path)); if (files.isEmpty) { Get.snackbar('Log', 'Nincs log fájl', snackPosition: SnackPosition.BOTTOM); return; } // Legutóbbi fájl megosztása await SharePlus.instance.share(ShareParams( files: [XFile(files.first.path, mimeType: 'text/plain')], subject: 'Terepi Segéd log — ${_dateFmt.format(DateTime.now())}', )); } /// Összes log fájl törlése Future clearLogs() async { await _sink?.flush(); await _sink?.close(); _sink = null; final dir = _logDir; if (dir == null) return; for (final f in Directory(dir).listSync().whereType()) { await f.delete().catchError((_) {}); } await _initLogFile(); i('AppLogger', 'Logok törölve, új fájl nyitva'); } // ── Belső ───────────────────────────────────────────────────────── static void _write( _Level level, String tag, String message, { Object? error, StackTrace? stack, }) { if (!kDebugMode && !_enableInRelease) return; // Konzolon is megjelenik final prefix = switch (level) { _Level.info => '📋', _Level.warning => '⚠️', _Level.error => '🔴', }; debugPrint('$prefix [$tag] $message' '${error != null ? ' | $error' : ''}'); // Fájlba írás try { AppLogger.to._writeLine(level, tag, message, error: error, stack: stack); } catch (_) {} } void _writeLine( _Level level, String tag, String message, { Object? error, StackTrace? stack, }) { final sink = _sink; if (sink == null) return; final time = _timeFmt.format(DateTime.now()); final lvl = switch (level) { _Level.info => 'I', _Level.warning => 'W', _Level.error => 'E', }; sink.writeln('$time $lvl [$tag] $message'); if (error != null) sink.writeln(' ↳ $error'); if (stack != null) { // Csak az első 5 sor a stack trace-ből final lines = stack.toString().split('\n').take(5); for (final l in lines) sink.writeln(' $l'); } // Méret ellenőrzés — ha szükséges, rotál _checkRotation(); } Future _initLogFile() async { final ext = await getExternalStorageDirectory(); final dir = Directory(p.join(ext!.path, 'logs')); await dir.create(recursive: true); _logDir = dir.path; final name = 'terepi_${_fileFmt.format(DateTime.now())}.log'; _logFile = File(p.join(dir.path, name)); _sink = _logFile!.openWrite(mode: FileMode.append); currentLogPath.value = _logFile!.path; // Fejléc _sink!.writeln('═' * 60); _sink!.writeln('Terepi Segéd — Log'); _sink!.writeln('Dátum: ${DateTime.now().toIso8601String()}'); _sink!.writeln('═' * 60); // Régi fájlok törlése await _pruneOldFiles(dir); } void _checkRotation() { final file = _logFile; if (file == null) return; if (!file.existsSync()) return; final sizeMb = file.lengthSync() / (1024 * 1024); if (sizeMb > _maxSizeMb) { _sink?.flush(); _sink?.close(); _initLogFile(); // új fájl } } Future _pruneOldFiles(Directory dir) async { final files = dir .listSync() .whereType() .where((f) => f.path.endsWith('.log')) .toList() ..sort((a, b) => a.path.compareTo(b.path)); while (files.length > _maxFiles) { await files.removeAt(0).delete(); } } }