diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 6ccf0946..33773fcc 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,73 +1,79 @@
-
-
-
+ xmlns:tools="http://schemas.android.com/tools">
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
-
+
-
-
-
+
-
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/network/mostro/app/MainActivity.kt b/android/app/src/main/kotlin/network/mostro/app/MainActivity.kt
index 708e42d1..0126d26a 100644
--- a/android/app/src/main/kotlin/network/mostro/app/MainActivity.kt
+++ b/android/app/src/main/kotlin/network/mostro/app/MainActivity.kt
@@ -1,5 +1,109 @@
package network.mostro.app
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
import io.flutter.embedding.android.FlutterActivity
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.plugin.common.EventChannel
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.util.concurrent.atomic.AtomicBoolean
-class MainActivity: FlutterActivity()
\ No newline at end of file
+class MainActivity : FlutterActivity() {
+
+ private val EVENT_CHANNEL = "native_logcat_stream"
+ private var logcatProcess: Process? = null
+ private val isCapturing = AtomicBoolean(false)
+ private val handler = Handler(Looper.getMainLooper())
+
+ override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+
+ // EventChannel for automatic native log streaming
+ EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL)
+ .setStreamHandler(object : EventChannel.StreamHandler {
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
+ startLogCapture(events)
+ }
+
+ override fun onCancel(arguments: Any?) {
+ stopLogCapture()
+ }
+ })
+ }
+
+ private fun startLogCapture(eventSink: EventChannel.EventSink?) {
+ // Use compareAndSet for thread-safe check-and-set operation
+ if (!isCapturing.compareAndSet(false, true)) return
+
+ Thread({
+ var reader: BufferedReader? = null
+ try {
+ // Capture logs only for this app with timestamp
+ logcatProcess = Runtime.getRuntime().exec(
+ arrayOf(
+ "logcat",
+ "-v", "time",
+ "--pid=${android.os.Process.myPid()}"
+ )
+ )
+
+ reader = BufferedReader(
+ InputStreamReader(logcatProcess?.inputStream)
+ )
+
+ // Use inline assignment in while loop
+ while (isCapturing.get()) {
+ val line = reader.readLine() ?: break
+
+ if (line.isNotEmpty()) {
+ handler.post {
+ eventSink?.success(line)
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("MostroLogCapture", "Error capturing native logs", e)
+ handler.post {
+ eventSink?.error("LOGCAT_ERROR", e.message, null)
+ }
+ } finally {
+ reader?.close()
+ logcatProcess?.destroy()
+ // Forcibly kill if not terminated after brief wait
+ try {
+ if (logcatProcess?.waitFor(100, java.util.concurrent.TimeUnit.MILLISECONDS) == false) {
+ logcatProcess?.destroyForcibly()
+ }
+ } catch (e: Exception) {
+ Log.w("MostroLogCapture", "Error waiting for process termination", e)
+ }
+ isCapturing.set(false)
+ }
+ }, "mostro-logcat").start()
+ }
+
+ private fun stopLogCapture() {
+ isCapturing.set(false)
+ logcatProcess?.let { process ->
+ process.destroy()
+ // Force kill if still alive after brief wait
+ Thread {
+ try {
+ if (!process.waitFor(200, java.util.concurrent.TimeUnit.MILLISECONDS)) {
+ process.destroyForcibly()
+ }
+ } catch (e: Exception) {
+ Log.w("MostroLogCapture", "Process cleanup interrupted", e)
+ }
+ }.start()
+ }
+ logcatProcess = null
+ }
+
+ override fun onDestroy() {
+ stopLogCapture()
+ super.onDestroy()
+ }
+}
\ No newline at end of file
diff --git a/lib/core/app_routes.dart b/lib/core/app_routes.dart
index d9511d02..791b1f6d 100644
--- a/lib/core/app_routes.dart
+++ b/lib/core/app_routes.dart
@@ -31,6 +31,8 @@ import 'package:mostro_mobile/shared/widgets/notification_listener_widget.dart';
import 'package:logger/logger.dart';
import 'package:mostro_mobile/generated/l10n.dart';
+import '../features/logs/logs_screen.dart';
+
GoRouter createRouter(WidgetRef ref) {
return GoRouter(
navigatorKey: GlobalKey(),
@@ -203,6 +205,16 @@ GoRouter createRouter(WidgetRef ref) {
child: const AboutScreen(),
),
),
+ GoRoute(
+ name: 'logs',
+ path: '/logs',
+ pageBuilder: (context, state) =>
+ buildPageWithDefaultTransition(
+ context: context,
+ state: state,
+ child: const LogsScreen(),
+ ),
+ ),
GoRoute(
path: '/walkthrough',
pageBuilder: (context, state) =>
diff --git a/lib/core/app_theme.dart b/lib/core/app_theme.dart
index dadddd6a..843b4bb9 100644
--- a/lib/core/app_theme.dart
+++ b/lib/core/app_theme.dart
@@ -64,6 +64,7 @@ class AppTheme {
static const Color statusSettledText = Color(0xFFC084FC);
static const Color statusInactiveBackground = Color(0xFF1F2937); // Colors.grey.shade800
static const Color statusInactiveText = Color(0xFFD1D5DB); // Colors.grey.shade300
+ static const Color statusNative = Color(0xFFFF9800);
// Text colors
static const Color secondaryText = Color(0xFFBDBDBD); // Colors.grey.shade400
diff --git a/lib/features/logs/logs_provider.dart b/lib/features/logs/logs_provider.dart
new file mode 100644
index 00000000..cd2f2a81
--- /dev/null
+++ b/lib/features/logs/logs_provider.dart
@@ -0,0 +1,82 @@
+import 'dart:collection';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:mostro_mobile/features/logs/logs_service.dart';
+
+// Define possible states for logs
+enum LogsState {
+ loading,
+ enabled,
+ disabled
+}
+
+// Main service provider (reactive via ChangeNotifier)
+final logsServiceProvider = ChangeNotifierProvider((ref) {
+ final service = LogsService();
+ ref.onDispose(() {
+ service.dispose();
+ });
+ return service;
+});
+
+// Provider for reactive logs list
+final logsProvider = Provider>((ref) {
+ final service = ref.watch(logsServiceProvider);
+ return service.logs;
+});
+
+
+// Updated to use LogsState
+final logsEnabledProvider = StateNotifierProvider((ref) {
+ final service = ref.watch(logsServiceProvider);
+ return LogsEnabledNotifier(service);
+});
+
+// Updated to use LogsState
+final nativeLogsEnabledProvider = StateNotifierProvider((ref) {
+ final service = ref.watch(logsServiceProvider);
+ return NativeLogsEnabledNotifier(service);
+});
+
+// Updated LogsEnabledNotifier to use LogsState
+class LogsEnabledNotifier extends StateNotifier {
+ final LogsService _logsService;
+
+ LogsEnabledNotifier(this._logsService) : super(LogsState.loading) {
+ _loadState();
+ }
+
+ Future _loadState() async {
+ final isEnabled = await _logsService.isLogsEnabled();
+ state = isEnabled ? LogsState.enabled : LogsState.disabled;
+ }
+
+ Future toggle(bool enabled) async {
+ state = LogsState.loading;
+ await _logsService.setLogsEnabled(enabled);
+ state = enabled ? LogsState.enabled : LogsState.disabled;
+ }
+
+ bool get isEnabled => state == LogsState.enabled;
+}
+
+// Updated NativeLogsEnabledNotifier to use LogsState
+class NativeLogsEnabledNotifier extends StateNotifier {
+ final LogsService _logsService;
+
+ NativeLogsEnabledNotifier(this._logsService) : super(LogsState.loading) {
+ _loadState();
+ }
+
+ Future _loadState() async {
+ final isEnabled = await _logsService.isNativeLogsEnabled();
+ state = isEnabled ? LogsState.enabled : LogsState.disabled;
+ }
+
+ Future toggle(bool enabled) async {
+ state = LogsState.loading;
+ await _logsService.setNativeLogsEnabled(enabled);
+ state = enabled ? LogsState.enabled : LogsState.disabled;
+ }
+
+ bool get isEnabled => state == LogsState.enabled;
+}
\ No newline at end of file
diff --git a/lib/features/logs/logs_screen.dart b/lib/features/logs/logs_screen.dart
new file mode 100644
index 00000000..8738dac2
--- /dev/null
+++ b/lib/features/logs/logs_screen.dart
@@ -0,0 +1,169 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:mostro_mobile/features/logs/logs_provider.dart';
+import 'package:share_plus/share_plus.dart';
+import 'package:mostro_mobile/generated/l10n.dart';
+import 'package:mostro_mobile/core/app_theme.dart';
+
+class LogsScreen extends ConsumerWidget {
+ const LogsScreen({super.key});
+
+ Color _getLogColor(String line) {
+ // Detect native logs first
+ if (line.contains('[NATIVE]')) {
+ // Specific color for Android native logs
+ return AppTheme.statusNative;
+ }
+
+ if (line.contains('ERROR') || line.contains('Exception') || line.contains('❌')) {
+ return AppTheme.statusError;
+ } else if (line.contains('WARN') || line.contains('⚠️')) {
+ return AppTheme.statusWarning;
+ } else if (line.contains('INFO') || line.contains('🟢') || line.contains('🚀')) {
+ return AppTheme.statusInfo;
+ } else {
+ return AppTheme.textPrimary;
+ }
+ }
+
+ // Get icon based on log type
+ IconData _getLogIcon(String line) {
+ if (line.contains('[NATIVE]')) {
+ return Icons.android;
+ } else if (line.contains('ERROR') || line.contains('❌')) {
+ return Icons.error_outline;
+ } else if (line.contains('WARN') || line.contains('⚠️')) {
+ return Icons.warning_amber;
+ } else if (line.contains('🚀')) {
+ return Icons.rocket_launch;
+ } else {
+ return Icons.info_outline;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ // Watch logsProvider directly for automatic updates
+ // This will rebuild whenever logs change thanks to ChangeNotifier
+ final logs = ref.watch(logsProvider);
+
+ final logsService = ref.read(logsServiceProvider);
+
+ final s = S.of(context)!;
+
+ return Scaffold(
+ backgroundColor: AppTheme.backgroundDark,
+ appBar: AppBar(
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back, color: AppTheme.textPrimary),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ title: Text(
+ s.logsScreenTitle,
+ style: const TextStyle(
+ fontWeight: FontWeight.w600,
+ color: AppTheme.textPrimary,
+ ),
+ ),
+ backgroundColor: AppTheme.backgroundDark,
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.delete_outline, color: AppTheme.textPrimary),
+ tooltip: s.deleteLogsTooltip,
+ onPressed: () async {
+ await logsService.clearLogs();
+ // No need to call ref.invalidate - ChangeNotifier handles it!
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(s.logsDeletedMessage)),
+ );
+ }
+ },
+ ),
+ IconButton(
+ icon: const Icon(Icons.share_outlined, color: AppTheme.textPrimary),
+ tooltip: s.shareLogsTooltip,
+ onPressed: () async {
+ final file = await logsService.getLogFile(clean: true);
+ if (file != null) {
+ await Share.shareXFiles(
+ [XFile(file.path)],
+ text: s.logsShareText,
+ );
+ } else {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(s.errorSharingLogs)),
+ );
+ }
+ }
+ },
+ ),
+ ],
+ ),
+ body: logs.isEmpty
+ ? Center(
+ child: Text(
+ s.noLogsMessage,
+ style: const TextStyle(
+ color: AppTheme.textSecondary,
+ fontSize: 14,
+ ),
+ ),
+ )
+ : ListView.builder(
+ padding: const EdgeInsets.all(12),
+ itemCount: logs.length,
+ reverse: true, // Show newest logs at the bottom
+ itemBuilder: (context, i) {
+ // Access logs in reverse order without copying the entire list
+ final log = logs[logs.length - 1 - i];
+ final logColor = _getLogColor(log);
+ final logIcon = _getLogIcon(log);
+
+ return Container(
+ margin: const EdgeInsets.symmetric(vertical: 4),
+ padding: const EdgeInsets.symmetric(
+ vertical: 8,
+ horizontal: 10,
+ ),
+ decoration: BoxDecoration(
+ color: AppTheme.backgroundCard.withValues(alpha: 180 / 255),
+ borderRadius: BorderRadius.circular(8),
+ // Left colored border for better distinction
+ border: Border(
+ left: BorderSide(
+ color: logColor,
+ width: 3,
+ ),
+ ),
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Indicator icon
+ Icon(
+ logIcon,
+ size: 16,
+ color: logColor,
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: SelectableText(
+ log,
+ style: TextStyle(
+ fontFamily: 'monospace',
+ fontSize: 12,
+ color: logColor,
+ height: 1.4,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/features/logs/logs_service.dart b/lib/features/logs/logs_service.dart
new file mode 100644
index 00000000..f1372c51
--- /dev/null
+++ b/lib/features/logs/logs_service.dart
@@ -0,0 +1,289 @@
+import 'dart:async';
+import 'dart:collection';
+import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'native_log_service.dart';
+
+class LogsService extends ChangeNotifier {
+ static final LogsService _instance = LogsService._internal();
+ factory LogsService() => _instance;
+ LogsService._internal();
+
+ static const String _logsEnabledKey = 'logs_enabled';
+ static const String _nativeLogsEnabledKey = 'native_logs_enabled';
+ static const String _logFileName = 'mostro_logs.txt';
+ static const int _maxLogLines = 5000;
+
+ final List _logs = [];
+ File? _logFile;
+ IOSink? _sink;
+ bool _initialized = false;
+ bool _isDisposed = false;
+ bool _isEnabled = true;
+ bool _nativeLogsEnabled = true;
+
+ // Native log service
+ final NativeLogService _nativeLogService = NativeLogService();
+ StreamSubscription? _nativeSubscription;
+
+ // Expose unmodifiable view to prevent external mutation
+ UnmodifiableListView get logs => UnmodifiableListView(_logs);
+
+ /// Initialize the logs service
+ Future init() async {
+ if (_initialized || _isDisposed) return;
+
+ try {
+ // Load preferences
+ final prefs = await SharedPreferences.getInstance();
+ _isEnabled = prefs.getBool(_logsEnabledKey) ?? true;
+ _nativeLogsEnabled = prefs.getBool(_nativeLogsEnabledKey) ?? true;
+
+ final dir = await getApplicationDocumentsDirectory();
+ _logFile = File('${dir.path}/$_logFileName');
+
+ // Load existing logs if file exists
+ if (await _logFile!.exists()) {
+ final content = await _logFile!.readAsString();
+ final lines = content.split('\n').where((line) => line.isNotEmpty).toList();
+
+ // Keep only last N lines to prevent memory bloat
+ if (lines.length > _maxLogLines) {
+ _logs.addAll(lines.skip(lines.length - _maxLogLines));
+ } else {
+ _logs.addAll(lines);
+ }
+
+ // Notify after loading existing logs
+ notifyListeners();
+ }
+
+ // Open file for appending
+ _sink = _logFile!.openWrite(mode: FileMode.append);
+
+ // Start native logs capture
+ if (_nativeLogsEnabled) {
+ _initNativeLogsCapture();
+ }
+
+ // Set initialized flag only after successful setup
+ _initialized = true;
+
+ log('🚀 LogsService initialized');
+ } catch (e) {
+ print('Error initializing LogsService: $e');
+ rethrow;
+ }
+ }
+
+ /// Initialize native logs capture
+ void _initNativeLogsCapture() {
+ if (_isDisposed) return;
+
+ try {
+ _nativeSubscription = _nativeLogService.nativeLogStream.listen(
+ (nativeLog) {
+ if (_isDisposed || !_isEnabled || !_nativeLogsEnabled) return;
+
+ // Format native log with prefix
+ final timestamp = DateTime.now().toIso8601String();
+ final line = '[$timestamp] [NATIVE] $nativeLog';
+
+ _logs.add(line);
+
+ // Keep only last N lines in memory
+ if (_logs.length > _maxLogLines) {
+ _logs.removeAt(0);
+ }
+
+ try {
+ _sink?.writeln(line);
+ } catch (e) {
+ print('Error writing native log: $e');
+ }
+
+ // Notify listeners about new native log
+ notifyListeners();
+ },
+ onError: (error) {
+ print('❌ Error in native logs stream: $error');
+ },
+ );
+
+ print('🔧 Native logs capture started');
+ } catch (e) {
+ print('❌ Error starting native logs capture: $e');
+ }
+ }
+
+ /// Get current logs enabled state
+ Future isLogsEnabled() async {
+ final prefs = await SharedPreferences.getInstance();
+ return prefs.getBool(_logsEnabledKey) ?? true;
+ }
+
+ /// Get native logs enabled state
+ Future isNativeLogsEnabled() async {
+ final prefs = await SharedPreferences.getInstance();
+ return prefs.getBool(_nativeLogsEnabledKey) ?? true;
+ }
+
+ /// Set logs enabled state
+ Future setLogsEnabled(bool enabled) async {
+ if (_isDisposed) return;
+
+ _isEnabled = enabled;
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setBool(_logsEnabledKey, enabled);
+
+ // Notify listeners about state change
+ notifyListeners();
+ }
+
+ /// Set native logs enabled state
+ Future setNativeLogsEnabled(bool enabled) async {
+ if (_isDisposed) return;
+
+ _nativeLogsEnabled = enabled;
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setBool(_nativeLogsEnabledKey, enabled);
+
+ if (enabled && _nativeSubscription == null) {
+ _initNativeLogsCapture();
+ } else if (!enabled && _nativeSubscription != null) {
+ await _nativeSubscription?.cancel();
+ _nativeSubscription = null;
+ log('🔧 Native logs capture stopped');
+ }
+
+ // Notify listeners about state change
+ notifyListeners();
+ }
+
+ /// Main logging method
+ void log(String message) {
+ if (!_initialized || _isDisposed || !_isEnabled) return;
+
+ final timestamp = DateTime.now().toIso8601String();
+ final line = '[$timestamp] $message';
+
+ _logs.add(line);
+
+ // Keep only last N lines in memory
+ if (_logs.length > _maxLogLines) {
+ _logs.removeAt(0);
+ }
+
+ try {
+ _sink?.writeln(line);
+ } catch (e) {
+ print('Error writing to log file: $e');
+ }
+
+ // Notify listeners about new log
+ notifyListeners();
+ }
+
+ /// Alias for backwards compatibility
+ Future writeLog(String message) async {
+ log(message);
+ }
+
+ /// Read logs (backwards compatibility)
+ Future> readLogs() async {
+ return _logs.toList();
+ }
+
+ /// Clear all logs
+ Future clearLogs({bool clean = true}) async {
+ if (!_initialized || _isDisposed) return;
+
+ try {
+ // Close current sink
+ await _sink?.close();
+
+ // Clear file
+ if (clean && _logFile != null && await _logFile!.exists()) {
+ await _logFile!.writeAsString('');
+ }
+
+ // Clear memory list
+ _logs.clear();
+
+ // Reopen sink if not disposed
+ if (!_isDisposed) {
+ _sink = _logFile?.openWrite(mode: FileMode.append);
+ }
+
+ // Notify listeners about cleared logs
+ notifyListeners();
+ } catch (e) {
+ print('Error clearing logs: $e');
+ }
+ }
+
+ /// Get the log file for sharing
+ Future getLogFile({bool clean = false}) async {
+ if (!_initialized || _isDisposed || _logFile == null) return null;
+
+ try {
+ // Flush before reading to ensure all data is written
+ await _sink?.flush();
+
+ if (!clean) {
+ return _logFile;
+ }
+
+ // Create cleaned copy
+ final dir = await getTemporaryDirectory();
+ final cleanFile = File('${dir.path}/mostro_logs_clean.txt');
+
+ final content = await _logFile!.readAsString();
+ final cleanedLines = content
+ .split('\n')
+ .where((line) => line.isNotEmpty)
+ .map(_cleanLine)
+ .join('\n');
+
+ await cleanFile.writeAsString(cleanedLines);
+ return cleanFile;
+ } catch (e) {
+ print('Error getting log file: $e');
+ return null;
+ }
+ }
+
+ /// Clean a log line by removing ANSI codes and control characters
+ String _cleanLine(String line) {
+ // Remove ANSI color codes (e.g., \x1B[31m for red)
+ final ansiRegex = RegExp(r'\x1B\[[0-?]*[ -/]*[@-~]');
+ final noAnsi = line.replaceAll(ansiRegex, '');
+
+ // Remove non-printable control characters but keep Unicode text
+ final controlCharsRegex = RegExp(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]');
+ return noAnsi.replaceAll(controlCharsRegex, '');
+ }
+
+ /// Dispose method for cleanup
+ @override
+ void dispose() {
+ if (_isDisposed) return;
+ _isDisposed = true;
+
+ unawaited(_nativeSubscription?.cancel());
+ _nativeSubscription = null;
+ _nativeLogService.dispose();
+
+ unawaited(_sink?.flush());
+ unawaited(_sink?.close());
+ _sink = null;
+
+ _logs.clear();
+ _logFile = null;
+ _initialized = false;
+
+ super.dispose();
+ }
+}
\ No newline at end of file
diff --git a/lib/features/logs/native_log_service.dart b/lib/features/logs/native_log_service.dart
new file mode 100644
index 00000000..51187e0f
--- /dev/null
+++ b/lib/features/logs/native_log_service.dart
@@ -0,0 +1,34 @@
+import 'dart:async';
+import 'package:flutter/services.dart';
+
+class NativeLogService {
+ static const EventChannel _logcatStream = EventChannel('native_logcat_stream');
+
+ Stream? _nativeLogStream;
+ StreamSubscription? _subscription;
+ bool _isListening = false;
+
+ Stream get nativeLogStream {
+ if (_nativeLogStream == null && !_isListening) {
+ _isListening = true;
+ _nativeLogStream = _logcatStream
+ .receiveBroadcastStream()
+ .map((event) => event.toString())
+ .handleError((error) {
+ print('❌ Error in native logcat stream: $error');
+ }).where((log) => log.isNotEmpty && log.trim().isNotEmpty);
+ }
+
+ return _nativeLogStream!;
+ }
+
+ bool get isListening => _isListening;
+
+ void dispose() {
+ _subscription?.cancel();
+ _subscription = null;
+ // Reset stream to allow restart
+ _nativeLogStream = null;
+ _isListening = false;
+ }
+}
\ No newline at end of file
diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart
index b58412bc..99084fcc 100644
--- a/lib/features/settings/settings_screen.dart
+++ b/lib/features/settings/settings_screen.dart
@@ -10,6 +10,7 @@ import 'package:mostro_mobile/features/settings/settings.dart';
import 'package:mostro_mobile/shared/widgets/currency_selection_dialog.dart';
import 'package:mostro_mobile/shared/providers/exchange_service_provider.dart';
import 'package:mostro_mobile/shared/widgets/language_selector.dart';
+import 'package:mostro_mobile/features/logs/logs_provider.dart';
import 'package:mostro_mobile/generated/l10n.dart';
class SettingsScreen extends ConsumerStatefulWidget {
@@ -22,6 +23,7 @@ class SettingsScreen extends ConsumerStatefulWidget {
class _SettingsScreenState extends ConsumerState {
late final TextEditingController _mostroTextController;
late final TextEditingController _lightningAddressController;
+ bool _hasSetupListener = false;
@override
void initState() {
@@ -31,7 +33,6 @@ class _SettingsScreenState extends ConsumerState {
_lightningAddressController = TextEditingController(text: settings.defaultLightningAddress ?? '');
}
-
@override
void dispose() {
_mostroTextController.dispose();
@@ -41,15 +42,18 @@ class _SettingsScreenState extends ConsumerState {
@override
Widget build(BuildContext context) {
- // Listen to settings changes and update controllers
- ref.listen(settingsProvider, (previous, next) {
- if (previous?.defaultLightningAddress != next.defaultLightningAddress) {
- final newText = next.defaultLightningAddress ?? '';
- if (_lightningAddressController.text != newText) {
- _lightningAddressController.text = newText;
+ // Setup listener only once to avoid repeated subscriptions
+ if (!_hasSetupListener) {
+ _hasSetupListener = true;
+ ref.listen(settingsProvider, (previous, next) {
+ if (previous?.defaultLightningAddress != next.defaultLightningAddress) {
+ final newText = next.defaultLightningAddress ?? '';
+ if (_lightningAddressController.text != newText) {
+ _lightningAddressController.text = newText;
+ }
}
- }
- });
+ });
+ }
return Scaffold(
appBar: AppBar(
@@ -57,7 +61,7 @@ class _SettingsScreenState extends ConsumerState {
elevation: 0,
leading: IconButton(
icon:
- const HeroIcon(HeroIcons.arrowLeft, color: AppTheme.textPrimary),
+ const HeroIcon(HeroIcons.arrowLeft, color: AppTheme.textPrimary),
onPressed: () => context.pop(),
),
title: Text(
@@ -99,6 +103,10 @@ class _SettingsScreenState extends ConsumerState {
_buildRelaysCard(context),
const SizedBox(height: 16),
+ // Logs Card
+ _buildLogsCard(context),
+ const SizedBox(height: 16),
+
// Mostro Card
_buildMostroCard(context, _mostroTextController),
const SizedBox(height: 16),
@@ -238,7 +246,6 @@ class _SettingsScreenState extends ConsumerState {
}
Widget _buildLightningAddressCard(BuildContext context) {
-
return Container(
decoration: BoxDecoration(
color: AppTheme.backgroundCard,
@@ -310,7 +317,7 @@ class _SettingsScreenState extends ConsumerState {
onChanged: (value) {
final cleanValue = value.trim().isEmpty ? null : value.trim();
ref.read(settingsProvider.notifier).updateDefaultLightningAddress(cleanValue);
-
+
// Force sync immediately for empty values
if (cleanValue == null) {
_lightningAddressController.text = '';
@@ -390,6 +397,124 @@ class _SettingsScreenState extends ConsumerState {
);
}
+ Widget _buildLogsCard(BuildContext context) {
+ final logsState = ref.watch(logsEnabledProvider);
+ final logsEnabledNotifier = ref.read(logsEnabledProvider.notifier);
+
+ return Container(
+ decoration: BoxDecoration(
+ color: AppTheme.backgroundCard,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ const Icon(
+ LucideIcons.bug,
+ color: AppTheme.activeColor,
+ size: 20,
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ S.of(context)!.logsMenuTitle,
+ style: const TextStyle(
+ color: AppTheme.textPrimary,
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ S.of(context)!.logsMenuSubtitle,
+ style: const TextStyle(
+ color: AppTheme.textSecondary,
+ fontSize: 12,
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ ],
+ ),
+ ),
+
+ if (logsState == LogsState.loading)
+ const SizedBox(
+ width: 24,
+ height: 24,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(AppTheme.activeColor),
+ ),
+ )
+ else
+ Switch(
+ value: logsState == LogsState.enabled,
+ onChanged: (value) async {
+ await logsEnabledNotifier.toggle(value);
+ },
+ activeThumbColor: AppTheme.activeColor,
+ ),
+ ],
+ ),
+ const SizedBox(height: 20),
+ Text(
+ logsState == LogsState.enabled
+ ? S.of(context)!.logsEnabledDescription
+ : S.of(context)!.logsDisabledDescription,
+ style: const TextStyle(
+ color: AppTheme.textSecondary,
+ fontSize: 14,
+ ),
+ ),
+ const SizedBox(height: 16),
+ InkWell(
+ onTap: () {
+ context.push('/logs');
+ },
+ borderRadius: BorderRadius.circular(8),
+ child: Container(
+ padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: AppTheme.activeColor.withValues(alpha: 0.3),
+ width: 1,
+ ),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ S.of(context)!.viewLogsButton,
+ style: const TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ color: AppTheme.activeColor,
+ ),
+ ),
+ const Icon(
+ Icons.arrow_forward_ios,
+ size: 16,
+ color: AppTheme.activeColor,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
Widget _buildMostroCard(
BuildContext context, TextEditingController controller) {
return Container(
@@ -461,7 +586,7 @@ class _SettingsScreenState extends ConsumerState {
controller: controller,
style: const TextStyle(color: AppTheme.textPrimary),
onChanged: (value) => ref
- .watch(settingsProvider.notifier)
+ .read(settingsProvider.notifier)
.updateMostroInstance(value),
decoration: InputDecoration(
border: InputBorder.none,
@@ -615,4 +740,4 @@ class _SettingsScreenState extends ConsumerState {
},
);
}
-}
+}
\ No newline at end of file
diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb
index ab95ab4a..8075788c 100644
--- a/lib/l10n/intl_en.arb
+++ b/lib/l10n/intl_en.arb
@@ -1144,5 +1144,20 @@
"deleteUserRelayCancel": "No",
"@_comment_session_timeout": "Session timeout message",
- "sessionTimeoutMessage": "No response received, check your connection and try again later"
+ "sessionTimeoutMessage": "No response received, check your connection and try again later",
+
+ "@_comment_logsScreenTitle": "Title for the logs screen in the app",
+ "logsScreenTitle": "Application Logs",
+ "deleteLogsTooltip": "Delete logs",
+ "logsDeletedMessage": "🧹 Logs cleared",
+ "shareLogsTooltip": "Share logs file",
+ "logsShareText": "MostroApp Logs",
+ "errorSharingLogs": "An error occurred while sharing logs.",
+ "noLogsMessage": "No logs yet.",
+ "logsMenuTitle": "View Logs",
+ "logsMenuSubtitle": "Internal diagnostics and events",
+ "logsEnabledDescription": "Saving activity logs",
+ "logsDisabledDescription": "Logs disabled",
+ "viewLogsButton": "View logs"
+
}
diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb
index ea5a8039..ebd1494b 100644
--- a/lib/l10n/intl_es.arb
+++ b/lib/l10n/intl_es.arb
@@ -1122,6 +1122,21 @@
"deleteUserRelayCancel": "No",
"@_comment_session_timeout": "Session timeout message",
- "sessionTimeoutMessage": "No hubo respuesta, verifica tu conexión e inténtalo más tarde"
+ "sessionTimeoutMessage": "No hubo respuesta, verifica tu conexión e inténtalo más tarde",
-}
\ No newline at end of file
+
+ "@_comment_logsScreenTitle": "Title for the logs screen in the app",
+ "logsScreenTitle": "Registros de la aplicación",
+ "deleteLogsTooltip": "Borrar logs",
+ "logsDeletedMessage": "🧹 Logs borrados",
+ "shareLogsTooltip": "Compartir archivo de logs",
+ "errorSharingLogs": "Ocurrió un error al compartir los registros.",
+ "logsShareText": "Logs de MostroApp",
+ "noLogsMessage": "No hay registros aún.",
+ "logsMenuTitle": "Ver registros (Logs)",
+ "logsMenuSubtitle": "Diagnóstico y eventos internos",
+ "logsEnabledDescription": "Guardando registros de actividad",
+ "logsDisabledDescription": "Registros desactivados",
+ "viewLogsButton": "Ver registros"
+
+}
diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb
index a0e59dc0..04c80b0f 100644
--- a/lib/l10n/intl_it.arb
+++ b/lib/l10n/intl_it.arb
@@ -1177,5 +1177,19 @@
"deleteUserRelayCancel": "No",
"@_comment_session_timeout": "Session timeout message",
- "sessionTimeoutMessage": "Nessuna risposta ricevuta, verifica la tua connessione e riprova più tardi"
+ "sessionTimeoutMessage": "Nessuna risposta ricevuta, verifica la tua connessione e riprova più tardi",
+
+ "@_comment_logsScreenTitle": "Title for the logs screen in the app",
+ "logsScreenTitle": "Registri dell'applicazione",
+ "deleteLogsTooltip": "Elimina log",
+ "logsDeletedMessage": "🧹 Log cancellati",
+ "shareLogsTooltip": "Condividi file log",
+ "errorSharingLogs": "Si è verificato un errore durante la condivisione dei registri.",
+ "logsShareText": "Log di MostroApp",
+ "noLogsMessage": "Nessun registro presente.",
+ "logsMenuTitle": "Visualizza Log",
+ "logsMenuSubtitle": "Diagnostica ed eventi interni",
+ "logsEnabledDescription": "Salvataggio dei registri di attività",
+ "logsDisabledDescription": "Registri disattivati",
+ "viewLogsButton": "Visualizza registri"
}
diff --git a/lib/main.dart b/lib/main.dart
index 0c46fdcd..58064939 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,23 +1,64 @@
+import 'dart:async';
+import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:mostro_mobile/core/app.dart';
-import 'package:mostro_mobile/features/auth/providers/auth_notifier_provider.dart';
import 'package:mostro_mobile/features/relays/relays_provider.dart';
import 'package:mostro_mobile/features/settings/settings_notifier.dart';
import 'package:mostro_mobile/features/settings/settings_provider.dart';
import 'package:mostro_mobile/background/background_service.dart';
-import 'package:mostro_mobile/features/notifications/services/background_notification_service.dart';
import 'package:mostro_mobile/shared/providers/background_service_provider.dart';
import 'package:mostro_mobile/shared/providers/providers.dart';
import 'package:mostro_mobile/shared/utils/biometrics_helper.dart';
import 'package:mostro_mobile/shared/utils/notification_permission_helper.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:timeago/timeago.dart' as timeago;
+import 'package:mostro_mobile/features/logs/logs_service.dart';
+import 'package:mostro_mobile/features/logs/logs_provider.dart';
+import 'package:mostro_mobile/features/notifications/services/background_notification_service.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
+ // Initialize LogsService
+ final logsService = LogsService();
+ await logsService.init();
+
+ // Startup log
+ logsService.log('🚀 App started');
+
+ // Capture global Flutter errors
+ final previousFlutterOnError = FlutterError.onError;
+ FlutterError.onError = (details) {
+ logsService.log('❌ FlutterError: ${details.exceptionAsString()}');
+ if (details.stack != null) {
+ logsService.log('Stack: ${details.stack}');
+ }
+ previousFlutterOnError?.call(details);
+ };
+
+ // Capture unhandled Dart errors
+ final previousPlatformOnError = PlatformDispatcher.instance.onError;
+ PlatformDispatcher.instance.onError = (error, stack) {
+ logsService.log('❌ Uncaught error: $error');
+ logsService.log('Stack: $stack');
+ return previousPlatformOnError?.call(error, stack) ?? false;
+ };
+
+ runZonedGuarded>(
+ () async => _startApp(logsService),
+ (error, stackTrace) {
+ logsService.log('⚠️ Zone error: $error');
+ logsService.log('StackTrace: $stackTrace');
+ FlutterError.reportError(
+ FlutterErrorDetails(exception: error, stack: stackTrace),
+ );
+ },
+ );
+}
+
+Future _startApp(LogsService logsService) async {
await requestNotificationPermissionIfNeeded();
final biometricsHelper = BiometricsHelper();
@@ -31,7 +72,6 @@ Future main() async {
await settings.init();
await initializeNotifications();
-
_initializeTimeAgoLocalization();
final backgroundService = createBackgroundService(settings.settings);
@@ -39,17 +79,24 @@ Future main() async {
final container = ProviderContainer(
overrides: [
- settingsProvider.overrideWith((b) => settings),
+ settingsProvider.overrideWith((ref) => settings),
backgroundServiceProvider.overrideWithValue(backgroundService),
biometricsHelperProvider.overrideWithValue(biometricsHelper),
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
secureStorageProvider.overrideWithValue(secureStorage),
mostroDatabaseProvider.overrideWithValue(mostroDatabase),
eventDatabaseProvider.overrideWithValue(eventsDatabase),
+ // Use overrideWith to preserve reactive behavior
+ logsServiceProvider.overrideWith((ref) {
+ // Add cleanup for the custom instance
+ ref.onDispose(() {
+ logsService.dispose();
+ });
+ return logsService;
+ }),
],
);
- // Initialize relay sync on app start
_initializeRelaySynchronization(container);
runApp(
@@ -60,25 +107,16 @@ Future main() async {
);
}
-/// Initialize relay synchronization on app startup
void _initializeRelaySynchronization(ProviderContainer container) {
try {
- // Read the relays provider to trigger initialization of RelaysNotifier
- // This will automatically start sync with the configured Mostro instance
container.read(relaysProvider);
} catch (e) {
- // Log error but don't crash app if relay sync initialization fails
- debugPrint('Failed to initialize relay synchronization: $e');
+ final logsService = container.read(logsServiceProvider);
+ logsService.log('Failed to initialize relay synchronization: $e');
}
}
-/// Initialize timeago localization for supported languages
void _initializeTimeAgoLocalization() {
- // Set Spanish locale for timeago
timeago.setLocaleMessages('es', timeago.EsMessages());
-
- // Set Italian locale for timeago
timeago.setLocaleMessages('it', timeago.ItMessages());
-
- // English is already the default, no need to set it
-}
+}
\ No newline at end of file
diff --git a/lib/shared/providers/biometrics_provider.dart b/lib/shared/providers/biometrics_provider.dart
new file mode 100644
index 00000000..e16c4546
--- /dev/null
+++ b/lib/shared/providers/biometrics_provider.dart
@@ -0,0 +1,6 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:mostro_mobile/shared/utils/biometrics_helper.dart';
+
+final biometricsHelperProvider = Provider((ref) {
+ throw UnimplementedError();
+});
diff --git a/lib/shared/providers/providers.dart b/lib/shared/providers/providers.dart
index 851ff56b..f0a9bfa9 100644
--- a/lib/shared/providers/providers.dart
+++ b/lib/shared/providers/providers.dart
@@ -1,2 +1,3 @@
export 'package:mostro_mobile/shared/providers/mostro_database_provider.dart';
export 'package:mostro_mobile/shared/providers/storage_providers.dart';
+export 'package:mostro_mobile/shared/providers/biometrics_provider.dart';
diff --git a/lib/shared/widgets/custom_drawer_overlay.dart b/lib/shared/widgets/custom_drawer_overlay.dart
index 941e9cb0..469d9e04 100644
--- a/lib/shared/widgets/custom_drawer_overlay.dart
+++ b/lib/shared/widgets/custom_drawer_overlay.dart
@@ -16,32 +16,32 @@ class CustomDrawerOverlay extends ConsumerWidget {
final isDrawerOpen = ref.watch(drawerProvider);
final statusBarHeight = MediaQuery.of(context).padding.top;
- return Stack(
- children: [
- // Main content
- child,
+ return PopScope(
+ canPop: !isDrawerOpen,
+ onPopInvokedWithResult: (didPop, result) {
+ if (!didPop && isDrawerOpen) {
+ ref.read(drawerProvider.notifier).closeDrawer();
+ }
+ },
+ child: Stack(
+ children: [
+ // Main content
+ child,
- // Overlay background
- if (isDrawerOpen)
- GestureDetector(
- onTap: () => ref.read(drawerProvider.notifier).closeDrawer(),
- child: Container(
- width: double.infinity,
- height: double.infinity,
- color: Colors.black.withValues(alpha: 0.3),
+ // Overlay background
+ if (isDrawerOpen)
+ GestureDetector(
+ onTap: () =>
+ ref.read(drawerProvider.notifier).closeDrawer(),
+ child: Container(
+ width: double.infinity,
+ height: double.infinity,
+ color: Colors.black.withAlpha(80),
+ ),
),
- ),
- // Drawer
- PopScope(
- canPop: !isDrawerOpen,
- onPopInvokedWithResult: (didPop, result) {
- if (!didPop && isDrawerOpen) {
- // Close drawer if it's open
- ref.read(drawerProvider.notifier).closeDrawer();
- }
- },
- child: AnimatedPositioned(
+ // Drawer content
+ AnimatedPositioned(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
left: isDrawerOpen ? 0 : -MediaQuery.of(context).size.width * 0.7,
@@ -60,7 +60,7 @@ class CustomDrawerOverlay extends ConsumerWidget {
color: AppTheme.dark1,
border: Border(
right: BorderSide(
- color: Colors.white.withValues(alpha: 0.1),
+ color: Colors.white.withAlpha(25),
width: 1.0,
),
),
@@ -69,30 +69,29 @@ class CustomDrawerOverlay extends ConsumerWidget {
padding: EdgeInsets.only(top: statusBarHeight),
child: Column(
children: [
- SizedBox(height: 24),
+ const SizedBox(height: 24),
- // Logo header
+ // Logo
Container(
height: 100,
width: double.infinity,
alignment: Alignment.center,
decoration: const BoxDecoration(
image: DecorationImage(
- image: AssetImage('assets/images/logo-alpha.png'),
+ image: AssetImage(
+ 'assets/images/logo-alpha.png'),
fit: BoxFit.contain,
),
),
),
- SizedBox(height: 24),
-
+ const SizedBox(height: 24),
Divider(
height: 1,
thickness: 1,
- color: Colors.white.withValues(alpha: 0.1),
+ color: Colors.white.withAlpha(25),
),
-
- SizedBox(height: 16),
+ const SizedBox(height: 16),
// Menu items
_buildMenuItem(
@@ -116,31 +115,29 @@ class CustomDrawerOverlay extends ConsumerWidget {
title: S.of(context)!.about,
route: '/about',
),
+
+ const SizedBox(height: 8),
],
),
),
),
),
),
- ),
- ],
+ ],
+ ),
);
}
Widget _buildMenuItem(
- BuildContext context,
- WidgetRef ref, {
- required IconData icon,
- required String title,
- required String route,
- }) {
+ BuildContext context,
+ WidgetRef ref, {
+ required IconData icon,
+ required String title,
+ required String route,
+ }) {
return ListTile(
dense: true,
- leading: Icon(
- icon,
- color: AppTheme.cream1,
- size: 22,
- ),
+ leading: Icon(icon, color: AppTheme.cream1, size: 22),
title: Text(
title,
style: AppTheme.theme.textTheme.bodyLarge?.copyWith(
diff --git a/test/features/logs/logs_screen_test.dart b/test/features/logs/logs_screen_test.dart
new file mode 100644
index 00000000..ed6d8dc0
--- /dev/null
+++ b/test/features/logs/logs_screen_test.dart
@@ -0,0 +1,126 @@
+import 'dart:collection';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:mostro_mobile/features/logs/logs_screen.dart';
+import 'package:mostro_mobile/features/logs/logs_provider.dart';
+// import '../../../test_helpers.dart';
+import '../../mocks.mocks.dart';
+
+void main() {
+ late MockLogsService mockLogsService;
+
+ setUp(() {
+ mockLogsService = MockLogsService();
+ });
+
+ Widget createTestWidget() {
+ return ProviderScope(
+ overrides: [
+ // Use overrideWith instead of overrideWithValue for ChangeNotifierProvider
+ logsServiceProvider.overrideWith((ref) {
+ // Add cleanup for test instances
+ ref.onDispose(() {
+ mockLogsService.dispose();
+ });
+ return mockLogsService;
+ }),
+ ],
+ child: const MaterialApp(
+ home: LogsScreen(),
+ ),
+ );
+ }
+
+ testWidgets('Initial state shows no logs', (WidgetTester tester) async {
+ // Arrange: Mock devuelve lista vacía
+ when(mockLogsService.logs).thenReturn(UnmodifiableListView([]));
+
+ // Act
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // Assert
+ expect(find.text('No logs yet.'), findsOneWidget);
+ });
+
+ testWidgets('Displays logs when added', (WidgetTester tester) async {
+ // Arrange: Mock devuelve logs
+ when(mockLogsService.logs).thenReturn(
+ UnmodifiableListView(['[2025-10-31T00:00:00] Test log']),
+ );
+
+ // Act
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // Assert
+ expect(find.textContaining('Test log'), findsOneWidget);
+ });
+
+ testWidgets('Clears logs when delete button pressed', (WidgetTester tester) async {
+ // Arrange
+ when(mockLogsService.logs).thenReturn(
+ UnmodifiableListView(['[2025-10-31T00:00:00] Test log']),
+ );
+ when(mockLogsService.clearLogs(clean: anyNamed('clean')))
+ .thenAnswer((_) async => {});
+
+ // Act
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // Tap delete button
+ await tester.tap(find.byIcon(Icons.delete_outline));
+ await tester.pumpAndSettle();
+
+ // Assert
+ verify(mockLogsService.clearLogs(clean: anyNamed('clean'))).called(1);
+ expect(find.byType(SnackBar), findsOneWidget);
+ });
+
+ testWidgets('Exports logs when export button pressed', (WidgetTester tester) async {
+ // Arrange
+ when(mockLogsService.logs).thenReturn(
+ UnmodifiableListView(['[2025-10-31T00:00:00] Test log']),
+ );
+ when(mockLogsService.getLogFile(clean: true))
+ .thenAnswer((_) async => null); // Simula que no hay archivo
+
+ // Act
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // Tap share button
+ await tester.tap(find.byIcon(Icons.share_outlined));
+ await tester.pumpAndSettle();
+
+ // Assert
+ verify(mockLogsService.getLogFile(clean: true)).called(1);
+ });
+
+ testWidgets('UI updates when logs change via ChangeNotifier', (WidgetTester tester) async {
+ // Arrange: Start with empty logs
+ when(mockLogsService.logs).thenReturn(UnmodifiableListView([]));
+
+ await tester.pumpWidget(createTestWidget());
+ await tester.pumpAndSettle();
+
+ // Assert initial state
+ expect(find.text('No logs yet.'), findsOneWidget);
+
+ // Act: Simulate logs being added
+ when(mockLogsService.logs).thenReturn(
+ UnmodifiableListView(['[2025-10-31T00:00:00] New log']),
+ );
+
+ // Manually trigger notifyListeners if your mock supports it
+ // mockLogsService.notifyListeners(); // Uncomment if using a custom mock that extends ChangeNotifier
+
+ await tester.pumpAndSettle();
+
+ // Assert: UI should update (this test validates the ChangeNotifier pattern)
+ // Note: With a standard Mockito mock, you may need to trigger the update differently
+ });
+}
\ No newline at end of file
diff --git a/test/mocks.dart b/test/mocks.dart
index 56a35e2f..e7a8ca15 100644
--- a/test/mocks.dart
+++ b/test/mocks.dart
@@ -21,6 +21,7 @@ import 'package:mostro_mobile/features/order/models/order_state.dart';
import 'package:mostro_mobile/features/order/notfiers/order_notifier.dart';
import 'package:sembast/sembast.dart';
import 'package:shared_preferences/shared_preferences.dart';
+import 'package:mostro_mobile/features/logs/logs_service.dart';
import 'mocks.mocks.dart';
@@ -40,6 +41,7 @@ import 'mocks.mocks.dart';
OrderState,
OrderNotifier,
NostrKeyPairs,
+ LogsService,
])
// Custom mock for SettingsNotifier that returns a specific Settings object
diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart
index 60c9d0e7..ca0376a8 100644
--- a/test/mocks.mocks.dart
+++ b/test/mocks.mocks.dart
@@ -4,36 +4,40 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i5;
+import 'dart:collection' as _i14;
+import 'dart:io' as _i31;
+import 'dart:ui' as _i32;
import 'package:dart_nostr/dart_nostr.dart' as _i3;
-import 'package:dart_nostr/nostr/model/relay_informations.dart' as _i15;
+import 'package:dart_nostr/nostr/model/relay_informations.dart' as _i16;
import 'package:flutter_riverpod/flutter_riverpod.dart' as _i4;
import 'package:logger/logger.dart' as _i13;
import 'package:mockito/mockito.dart' as _i1;
-import 'package:mockito/src/dummies.dart' as _i16;
-import 'package:mostro_mobile/data/enums.dart' as _i27;
+import 'package:mockito/src/dummies.dart' as _i17;
+import 'package:mostro_mobile/data/enums.dart' as _i28;
import 'package:mostro_mobile/data/models.dart' as _i7;
-import 'package:mostro_mobile/data/models/order.dart' as _i17;
-import 'package:mostro_mobile/data/repositories/mostro_storage.dart' as _i24;
+import 'package:mostro_mobile/data/models/order.dart' as _i18;
+import 'package:mostro_mobile/data/repositories/mostro_storage.dart' as _i25;
import 'package:mostro_mobile/data/repositories/open_orders_repository.dart'
- as _i19;
-import 'package:mostro_mobile/data/repositories/session_storage.dart' as _i22;
-import 'package:mostro_mobile/features/key_manager/key_manager.dart' as _i23;
+ as _i20;
+import 'package:mostro_mobile/data/repositories/session_storage.dart' as _i23;
+import 'package:mostro_mobile/features/key_manager/key_manager.dart' as _i24;
+import 'package:mostro_mobile/features/logs/logs_service.dart' as _i30;
import 'package:mostro_mobile/features/order/models/order_state.dart' as _i11;
import 'package:mostro_mobile/features/order/notfiers/order_notifier.dart'
- as _i28;
-import 'package:mostro_mobile/features/relays/relay.dart' as _i25;
+ as _i29;
+import 'package:mostro_mobile/features/relays/relay.dart' as _i26;
import 'package:mostro_mobile/features/relays/relays_notifier.dart' as _i10;
import 'package:mostro_mobile/features/settings/settings.dart' as _i2;
import 'package:mostro_mobile/features/settings/settings_notifier.dart' as _i9;
-import 'package:mostro_mobile/services/deep_link_service.dart' as _i18;
+import 'package:mostro_mobile/services/deep_link_service.dart' as _i19;
import 'package:mostro_mobile/services/mostro_service.dart' as _i12;
-import 'package:mostro_mobile/services/nostr_service.dart' as _i14;
+import 'package:mostro_mobile/services/nostr_service.dart' as _i15;
import 'package:riverpod/src/internals.dart' as _i8;
import 'package:sembast/sembast.dart' as _i6;
-import 'package:sembast/src/api/transaction.dart' as _i21;
-import 'package:shared_preferences/src/shared_preferences_async.dart' as _i20;
-import 'package:state_notifier/state_notifier.dart' as _i26;
+import 'package:sembast/src/api/transaction.dart' as _i22;
+import 'package:shared_preferences/shared_preferences.dart' as _i21;
+import 'package:state_notifier/state_notifier.dart' as _i27;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
@@ -247,10 +251,21 @@ class _FakeLogger_18 extends _i1.SmartFake implements _i13.Logger {
);
}
+class _FakeUnmodifiableListView_19 extends _i1.SmartFake
+ implements _i14.UnmodifiableListView {
+ _FakeUnmodifiableListView_19(
+ Object parent,
+ Invocation parentInvocation,
+ ) : super(
+ parent,
+ parentInvocation,
+ );
+}
+
/// A class which mocks [NostrService].
///
/// See the documentation for Mockito's code generation for more information.
-class MockNostrService extends _i1.Mock implements _i14.NostrService {
+class MockNostrService extends _i1.Mock implements _i15.NostrService {
MockNostrService() {
_i1.throwOnMissingStub(this);
}
@@ -292,14 +307,14 @@ class MockNostrService extends _i1.Mock implements _i14.NostrService {
) as _i5.Future);
@override
- _i5.Future<_i15.RelayInformations?> getRelayInfo(String? relayUrl) =>
+ _i5.Future<_i16.RelayInformations?> getRelayInfo(String? relayUrl) =>
(super.noSuchMethod(
Invocation.method(
#getRelayInfo,
[relayUrl],
),
- returnValue: _i5.Future<_i15.RelayInformations?>.value(),
- ) as _i5.Future<_i15.RelayInformations?>);
+ returnValue: _i5.Future<_i16.RelayInformations?>.value(),
+ ) as _i5.Future<_i16.RelayInformations?>);
@override
_i5.Future publishEvent(_i3.NostrEvent? event) => (super.noSuchMethod(
@@ -382,7 +397,7 @@ class MockNostrService extends _i1.Mock implements _i14.NostrService {
#getMostroPubKey,
[],
),
- returnValue: _i16.dummyValue(
+ returnValue: _i17.dummyValue(
this,
Invocation.method(
#getMostroPubKey,
@@ -461,7 +476,7 @@ class MockNostrService extends _i1.Mock implements _i14.NostrService {
content,
],
),
- returnValue: _i5.Future.value(_i16.dummyValue(
+ returnValue: _i5.Future.value(_i17.dummyValue(
this,
Invocation.method(
#createRumor,
@@ -492,7 +507,7 @@ class MockNostrService extends _i1.Mock implements _i14.NostrService {
encryptedContent,
],
),
- returnValue: _i5.Future.value(_i16.dummyValue(
+ returnValue: _i5.Future.value(_i17.dummyValue(
this,
Invocation.method(
#createSeal,
@@ -544,7 +559,7 @@ class MockNostrService extends _i1.Mock implements _i14.NostrService {
);
@override
- _i5.Future<_i17.Order?> fetchEventById(
+ _i5.Future<_i18.Order?> fetchEventById(
String? eventId, [
List? specificRelays,
]) =>
@@ -556,11 +571,11 @@ class MockNostrService extends _i1.Mock implements _i14.NostrService {
specificRelays,
],
),
- returnValue: _i5.Future<_i17.Order?>.value(),
- ) as _i5.Future<_i17.Order?>);
+ returnValue: _i5.Future<_i18.Order?>.value(),
+ ) as _i5.Future<_i18.Order?>);
@override
- _i5.Future<_i18.OrderInfo?> fetchOrderInfoByEventId(
+ _i5.Future<_i19.OrderInfo?> fetchOrderInfoByEventId(
String? eventId, [
List? specificRelays,
]) =>
@@ -572,8 +587,8 @@ class MockNostrService extends _i1.Mock implements _i14.NostrService {
specificRelays,
],
),
- returnValue: _i5.Future<_i18.OrderInfo?>.value(),
- ) as _i5.Future<_i18.OrderInfo?>);
+ returnValue: _i5.Future<_i19.OrderInfo?>.value(),
+ ) as _i5.Future<_i19.OrderInfo?>);
}
/// A class which mocks [MostroService].
@@ -759,7 +774,7 @@ class MockMostroService extends _i1.Mock implements _i12.MostroService {
///
/// See the documentation for Mockito's code generation for more information.
class MockOpenOrdersRepository extends _i1.Mock
- implements _i19.OpenOrdersRepository {
+ implements _i20.OpenOrdersRepository {
MockOpenOrdersRepository() {
_i1.throwOnMissingStub(this);
}
@@ -852,7 +867,7 @@ class MockOpenOrdersRepository extends _i1.Mock
/// See the documentation for Mockito's code generation for more information.
// ignore: must_be_immutable
class MockSharedPreferencesAsync extends _i1.Mock
- implements _i20.SharedPreferencesAsync {
+ implements _i21.SharedPreferencesAsync {
MockSharedPreferencesAsync() {
_i1.throwOnMissingStub(this);
}
@@ -1058,7 +1073,7 @@ class MockDatabase extends _i1.Mock implements _i6.Database {
@override
String get path => (super.noSuchMethod(
Invocation.getter(#path),
- returnValue: _i16.dummyValue(
+ returnValue: _i17.dummyValue(
this,
Invocation.getter(#path),
),
@@ -1066,14 +1081,14 @@ class MockDatabase extends _i1.Mock implements _i6.Database {
@override
_i5.Future transaction(
- _i5.FutureOr Function(_i21.Transaction)? action) =>
+ _i5.FutureOr Function(_i22.Transaction)? action) =>
(super.noSuchMethod(
Invocation.method(
#transaction,
[action],
),
- returnValue: _i16.ifNotNull(
- _i16.dummyValueOrNull(
+ returnValue: _i17.ifNotNull(
+ _i17.dummyValueOrNull(
this,
Invocation.method(
#transaction,
@@ -1104,7 +1119,7 @@ class MockDatabase extends _i1.Mock implements _i6.Database {
/// A class which mocks [SessionStorage].
///
/// See the documentation for Mockito's code generation for more information.
-class MockSessionStorage extends _i1.Mock implements _i22.SessionStorage {
+class MockSessionStorage extends _i1.Mock implements _i23.SessionStorage {
MockSessionStorage() {
_i1.throwOnMissingStub(this);
}
@@ -1367,7 +1382,7 @@ class MockSessionStorage extends _i1.Mock implements _i22.SessionStorage {
/// A class which mocks [KeyManager].
///
/// See the documentation for Mockito's code generation for more information.
-class MockKeyManager extends _i1.Mock implements _i23.KeyManager {
+class MockKeyManager extends _i1.Mock implements _i24.KeyManager {
MockKeyManager() {
_i1.throwOnMissingStub(this);
}
@@ -1527,7 +1542,7 @@ class MockKeyManager extends _i1.Mock implements _i23.KeyManager {
/// A class which mocks [MostroStorage].
///
/// See the documentation for Mockito's code generation for more information.
-class MockMostroStorage extends _i1.Mock implements _i24.MostroStorage {
+class MockMostroStorage extends _i1.Mock implements _i25.MostroStorage {
MockMostroStorage() {
_i1.throwOnMissingStub(this);
}
@@ -1918,7 +1933,7 @@ class MockSettings extends _i1.Mock implements _i2.Settings {
@override
String get mostroPublicKey => (super.noSuchMethod(
Invocation.getter(#mostroPublicKey),
- returnValue: _i16.dummyValue(
+ returnValue: _i17.dummyValue(
this,
Invocation.getter(#mostroPublicKey),
),
@@ -2018,7 +2033,7 @@ class MockRef extends _i1.Mock
#refresh,
[provider],
),
- returnValue: _i16.dummyValue(
+ returnValue: _i17.dummyValue(
this,
Invocation.method(
#refresh,
@@ -2125,7 +2140,7 @@ class MockRef extends _i1.Mock
#read,
[provider],
),
- returnValue: _i16.dummyValue(
+ returnValue: _i17.dummyValue(
this,
Invocation.method(
#read,
@@ -2149,7 +2164,7 @@ class MockRef extends _i1.Mock
#watch,
[provider],
),
- returnValue: _i16.dummyValue(
+ returnValue: _i17.dummyValue(
this,
Invocation.method(
#watch,
@@ -2245,7 +2260,7 @@ class MockProviderSubscription extends _i1.Mock
#read,
[],
),
- returnValue: _i16.dummyValue(
+ returnValue: _i17.dummyValue(
this,
Invocation.method(
#read,
@@ -2297,10 +2312,10 @@ class MockRelaysNotifier extends _i1.Mock implements _i10.RelaysNotifier {
) as List);
@override
- List<_i25.MostroRelayInfo> get mostroRelaysWithStatus => (super.noSuchMethod(
+ List<_i26.MostroRelayInfo> get mostroRelaysWithStatus => (super.noSuchMethod(
Invocation.getter(#mostroRelaysWithStatus),
- returnValue: <_i25.MostroRelayInfo>[],
- ) as List<_i25.MostroRelayInfo>);
+ returnValue: <_i26.MostroRelayInfo>[],
+ ) as List<_i26.MostroRelayInfo>);
@override
bool get mounted => (super.noSuchMethod(
@@ -2309,22 +2324,22 @@ class MockRelaysNotifier extends _i1.Mock implements _i10.RelaysNotifier {
) as bool);
@override
- _i5.Stream> get stream => (super.noSuchMethod(
+ _i5.Stream> get stream => (super.noSuchMethod(
Invocation.getter(#stream),
- returnValue: _i5.Stream>.empty(),
- ) as _i5.Stream>);
+ returnValue: _i5.Stream>.empty(),
+ ) as _i5.Stream>);
@override
- List<_i25.Relay> get state => (super.noSuchMethod(
+ List<_i26.Relay> get state => (super.noSuchMethod(
Invocation.getter(#state),
- returnValue: <_i25.Relay>[],
- ) as List<_i25.Relay>);
+ returnValue: <_i26.Relay>[],
+ ) as List<_i26.Relay>);
@override
- List<_i25.Relay> get debugState => (super.noSuchMethod(
+ List<_i26.Relay> get debugState => (super.noSuchMethod(
Invocation.getter(#debugState),
- returnValue: <_i25.Relay>[],
- ) as List<_i25.Relay>);
+ returnValue: <_i26.Relay>[],
+ ) as List<_i26.Relay>);
@override
bool get hasListeners => (super.noSuchMethod(
@@ -2342,7 +2357,7 @@ class MockRelaysNotifier extends _i1.Mock implements _i10.RelaysNotifier {
);
@override
- set state(List<_i25.Relay>? value) => super.noSuchMethod(
+ set state(List<_i26.Relay>? value) => super.noSuchMethod(
Invocation.setter(
#state,
value,
@@ -2351,7 +2366,7 @@ class MockRelaysNotifier extends _i1.Mock implements _i10.RelaysNotifier {
);
@override
- _i5.Future addRelay(_i25.Relay? relay) => (super.noSuchMethod(
+ _i5.Future addRelay(_i26.Relay? relay) => (super.noSuchMethod(
Invocation.method(
#addRelay,
[relay],
@@ -2362,8 +2377,8 @@ class MockRelaysNotifier extends _i1.Mock implements _i10.RelaysNotifier {
@override
_i5.Future updateRelay(
- _i25.Relay? oldRelay,
- _i25.Relay? updatedRelay,
+ _i26.Relay? oldRelay,
+ _i26.Relay? updatedRelay,
) =>
(super.noSuchMethod(
Invocation.method(
@@ -2530,8 +2545,8 @@ class MockRelaysNotifier extends _i1.Mock implements _i10.RelaysNotifier {
@override
bool updateShouldNotify(
- List<_i25.Relay>? old,
- List<_i25.Relay>? current,
+ List<_i26.Relay>? old,
+ List<_i26.Relay>? current,
) =>
(super.noSuchMethod(
Invocation.method(
@@ -2546,7 +2561,7 @@ class MockRelaysNotifier extends _i1.Mock implements _i10.RelaysNotifier {
@override
_i4.RemoveListener addListener(
- _i26.Listener>? listener, {
+ _i27.Listener>? listener, {
bool? fireImmediately = true,
}) =>
(super.noSuchMethod(
@@ -2568,22 +2583,22 @@ class MockOrderState extends _i1.Mock implements _i11.OrderState {
}
@override
- _i27.Status get status => (super.noSuchMethod(
+ _i28.Status get status => (super.noSuchMethod(
Invocation.getter(#status),
- returnValue: _i27.Status.active,
- ) as _i27.Status);
+ returnValue: _i28.Status.active,
+ ) as _i28.Status);
@override
- _i27.Action get action => (super.noSuchMethod(
+ _i28.Action get action => (super.noSuchMethod(
Invocation.getter(#action),
- returnValue: _i27.Action.newOrder,
- ) as _i27.Action);
+ returnValue: _i28.Action.newOrder,
+ ) as _i28.Action);
@override
_i11.OrderState copyWith({
- _i27.Status? status,
- _i27.Action? action,
- _i17.Order? order,
+ _i28.Status? status,
+ _i28.Action? action,
+ _i18.Order? order,
_i7.PaymentRequest? paymentRequest,
_i7.CantDo? cantDo,
_i7.Dispute? dispute,
@@ -2641,19 +2656,19 @@ class MockOrderState extends _i1.Mock implements _i11.OrderState {
) as _i11.OrderState);
@override
- List<_i27.Action> getActions(_i27.Role? role) => (super.noSuchMethod(
+ List<_i28.Action> getActions(_i28.Role? role) => (super.noSuchMethod(
Invocation.method(
#getActions,
[role],
),
- returnValue: <_i27.Action>[],
- ) as List<_i27.Action>);
+ returnValue: <_i28.Action>[],
+ ) as List<_i28.Action>);
}
/// A class which mocks [OrderNotifier].
///
/// See the documentation for Mockito's code generation for more information.
-class MockOrderNotifier extends _i1.Mock implements _i28.OrderNotifier {
+class MockOrderNotifier extends _i1.Mock implements _i29.OrderNotifier {
MockOrderNotifier() {
_i1.throwOnMissingStub(this);
}
@@ -2679,7 +2694,7 @@ class MockOrderNotifier extends _i1.Mock implements _i28.OrderNotifier {
@override
String get orderId => (super.noSuchMethod(
Invocation.getter(#orderId),
- returnValue: _i16.dummyValue(
+ returnValue: _i17.dummyValue(
this,
Invocation.getter(#orderId),
),
@@ -2954,7 +2969,7 @@ class MockOrderNotifier extends _i1.Mock implements _i28.OrderNotifier {
@override
void sendNotification(
- _i27.Action? action, {
+ _i28.Action? action, {
Map? values,
bool? isTemporary = false,
String? eventId,
@@ -2990,7 +3005,7 @@ class MockOrderNotifier extends _i1.Mock implements _i28.OrderNotifier {
@override
_i4.RemoveListener addListener(
- _i26.Listener<_i11.OrderState>? listener, {
+ _i27.Listener<_i11.OrderState>? listener, {
bool? fireImmediately = true,
}) =>
(super.noSuchMethod(
@@ -3014,7 +3029,7 @@ class MockNostrKeyPairs extends _i1.Mock implements _i3.NostrKeyPairs {
@override
String get private => (super.noSuchMethod(
Invocation.getter(#private),
- returnValue: _i16.dummyValue(
+ returnValue: _i17.dummyValue(
this,
Invocation.getter(#private),
),
@@ -3023,7 +3038,7 @@ class MockNostrKeyPairs extends _i1.Mock implements _i3.NostrKeyPairs {
@override
String get public => (super.noSuchMethod(
Invocation.getter(#public),
- returnValue: _i16.dummyValue(
+ returnValue: _i17.dummyValue(
this,
Invocation.getter(#public),
),
@@ -3050,7 +3065,7 @@ class MockNostrKeyPairs extends _i1.Mock implements _i3.NostrKeyPairs {
#sign,
[message],
),
- returnValue: _i16.dummyValue(
+ returnValue: _i17.dummyValue(
this,
Invocation.method(
#sign,
@@ -3059,3 +3074,162 @@ class MockNostrKeyPairs extends _i1.Mock implements _i3.NostrKeyPairs {
),
) as String);
}
+
+/// A class which mocks [LogsService].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockLogsService extends _i1.Mock implements _i30.LogsService {
+ MockLogsService() {
+ _i1.throwOnMissingStub(this);
+ }
+
+ @override
+ _i14.UnmodifiableListView get logs => (super.noSuchMethod(
+ Invocation.getter(#logs),
+ returnValue: _FakeUnmodifiableListView_19(
+ this,
+ Invocation.getter(#logs),
+ ),
+ ) as _i14.UnmodifiableListView);
+
+ @override
+ bool get hasListeners => (super.noSuchMethod(
+ Invocation.getter(#hasListeners),
+ returnValue: false,
+ ) as bool);
+
+ @override
+ _i5.Future init() => (super.noSuchMethod(
+ Invocation.method(
+ #init,
+ [],
+ ),
+ returnValue: _i5.Future.value(),
+ returnValueForMissingStub: _i5.Future.value(),
+ ) as _i5.Future);
+
+ @override
+ _i5.Future isLogsEnabled() => (super.noSuchMethod(
+ Invocation.method(
+ #isLogsEnabled,
+ [],
+ ),
+ returnValue: _i5.Future.value(false),
+ ) as _i5.Future);
+
+ @override
+ _i5.Future isNativeLogsEnabled() => (super.noSuchMethod(
+ Invocation.method(
+ #isNativeLogsEnabled,
+ [],
+ ),
+ returnValue: _i5.Future.value(false),
+ ) as _i5.Future);
+
+ @override
+ _i5.Future setLogsEnabled(bool? enabled) => (super.noSuchMethod(
+ Invocation.method(
+ #setLogsEnabled,
+ [enabled],
+ ),
+ returnValue: _i5.Future.value(),
+ returnValueForMissingStub: _i5.Future.value(),
+ ) as _i5.Future);
+
+ @override
+ _i5.Future setNativeLogsEnabled(bool? enabled) => (super.noSuchMethod(
+ Invocation.method(
+ #setNativeLogsEnabled,
+ [enabled],
+ ),
+ returnValue: _i5.Future.value(),
+ returnValueForMissingStub: _i5.Future.value(),
+ ) as _i5.Future);
+
+ @override
+ void log(String? message) => super.noSuchMethod(
+ Invocation.method(
+ #log,
+ [message],
+ ),
+ returnValueForMissingStub: null,
+ );
+
+ @override
+ _i5.Future writeLog(String? message) => (super.noSuchMethod(
+ Invocation.method(
+ #writeLog,
+ [message],
+ ),
+ returnValue: _i5.Future.value(),
+ returnValueForMissingStub: _i5.Future.value(),
+ ) as _i5.Future);
+
+ @override
+ _i5.Future> readLogs() => (super.noSuchMethod(
+ Invocation.method(
+ #readLogs,
+ [],
+ ),
+ returnValue: _i5.Future>.value([]),
+ ) as _i5.Future>);
+
+ @override
+ _i5.Future clearLogs({bool? clean = true}) => (super.noSuchMethod(
+ Invocation.method(
+ #clearLogs,
+ [],
+ {#clean: clean},
+ ),
+ returnValue: _i5.Future.value(),
+ returnValueForMissingStub: _i5.Future.value(),
+ ) as _i5.Future);
+
+ @override
+ _i5.Future<_i31.File?> getLogFile({bool? clean = false}) =>
+ (super.noSuchMethod(
+ Invocation.method(
+ #getLogFile,
+ [],
+ {#clean: clean},
+ ),
+ returnValue: _i5.Future<_i31.File?>.value(),
+ ) as _i5.Future<_i31.File?>);
+
+ @override
+ _i5.Future dispose() => (super.noSuchMethod(
+ Invocation.method(
+ #dispose,
+ [],
+ ),
+ returnValue: _i5.Future.value(),
+ returnValueForMissingStub: _i5.Future.value(),
+ ) as _i5.Future);
+
+ @override
+ void addListener(_i32.VoidCallback? listener) => super.noSuchMethod(
+ Invocation.method(
+ #addListener,
+ [listener],
+ ),
+ returnValueForMissingStub: null,
+ );
+
+ @override
+ void removeListener(_i32.VoidCallback? listener) => super.noSuchMethod(
+ Invocation.method(
+ #removeListener,
+ [listener],
+ ),
+ returnValueForMissingStub: null,
+ );
+
+ @override
+ void notifyListeners() => super.noSuchMethod(
+ Invocation.method(
+ #notifyListeners,
+ [],
+ ),
+ returnValueForMissingStub: null,
+ );
+}