diff --git a/lib/services/app_logger.dart b/lib/services/app_logger.dart new file mode 100644 index 0000000..510873a --- /dev/null +++ b/lib/services/app_logger.dart @@ -0,0 +1,239 @@ +// 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 = false; + + /// 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(); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index f68ffa0..a8b9936 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,8 @@ dependencies: firebase_auth: ^5.5.0 cloud_firestore: ^5.6.4 firebase_storage: ^12.4.3 + firebase_crashlytics: ^5.2.4 + firebase_analytics: ^12.4.3 google_sign_in: ^6.2.2 flutter_secure_storage: ^9.2.4 googleapis: ^13.2.0