Skip to content

Commit bfe923b

Browse files
authored
Feat: Share and Save logs files (#412)
* feat: add logger service with memory buffer and Riverpod provider * fix : update config with logger constants * feat : connect logs screen to logger service with real-time display * feat: sync logging toggle with MemoryLogOutput * refactor: migrate files to use logger singleton * docs: updating docs * Migrate NostrService to use logger singleton * Migrate mostro_storage to use logger singleton * Update docs for Phase 3 completion * Enable isolate log receiver for background services * Update docs for Phase 4 completion * Add bottom padding to logs list to prevent overlap with system buttons * Localize relative time strings using timeAgoWithLocale extension * Increase bottom padding in logs list to prevent overlap with system navigation * Fix typo in LOGGING_IMPLEMENTATION.md * Configure logger with IsolateLogOutput in mobile background isolate * Print background isolate logs to console only in debug mode * Add fallback logging for errors before logger initialization * Add logger export service for logs save and share Create LoggerExportService with methods to: - Generate timestamped filenames (mostro_logs_YYYY-MM-DD_HH-MM-SS.txt) - Convert LogEntry list to formatted text with metadata header - Export logs using FilePicker.saveFile() for Android 13+ SAF compatibility - Export logs to temp directory for sharing - Share logs via native share sheet using Share.shareXFiles() * Add hamburger menu widget for logs screen Create LogsActionsMenu with PopupMenuButton containing three actions: - Save: Export logs to user-selected location via FilePicker - Share: Share logs via native system share sheet - Clear: Delete all logs with confirmation dialog Features: - Uses HeroIcons for consistent iconography - Menu items disabled when no logs available - Proper error handling with SnackBar feedback - Success message without showing file path for cleaner UX * Integrate hamburger menu into logs screen Replace individual IconButtons with LogsActionsMenu in AppBar. Remove methods moved to menu widget. Clean up lifecycle observer. Simplified scope: manual save only when user explicitly requests it. * Remove unused logExportPath from Settings model Clean up Settings model by removing logExportPath field and related code. Simplified implementation: no default folder configuration. * Add translations for logs export functionality Add translations in all languages (EN, ES, IT) for: - saveLogs, logsExportSuccess, logsExportError - shareLogsError, shareLogs - exportSettings, close Simplified success message without showing file path. * Update documentation for Phase 5 completion Mark Phase 5 as completed with implemented features: - Manual export via hamburger menu - Save logs to user-selected location using FilePicker - Share logs via native system share sheet - Generate timestamped .txt files - Clear logs with confirmation dialog - Simplified scope: manual save only, no auto-save or default folder * Fix: code rabbit suggestions Refactors log export and sharing to utilize localized strings for file content, subject, and text. This change ensures a consistent user experience across different languages when exporting and sharing logs by using the app's localization system. It also includes a title to the logs file, generated timestamp and the total number of logs written.
1 parent e64d8df commit bfe923b

File tree

8 files changed

+348
-57
lines changed

8 files changed

+348
-57
lines changed

docs/LOGGING_IMPLEMENTATION.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ Implementation of a comprehensive logging system for MostroP2P mobile app with i
3434
- Desktop background service with isolate logging
3535
- Isolate log receiver initialized in main.dart
3636

37-
### Phase 5: File Export & Persistence
38-
- Auto-save to storage
39-
- Restore on app restart
40-
- Generate .txt files
37+
### Phase 5: File Export & Persistence (Completed)
38+
- Manual export via hamburger menu
39+
- Save logs to user-selected location using FilePicker
40+
- Share logs via native system share sheet
4141
- Folder picker and permissions
42+
- Clear logs with confirmation dialog
4243

4344
### Phase 6: UI Enhancements
4445
- Recording indicator widget
@@ -206,6 +207,6 @@ void backgroundMain(SendPort sendPort) async {
206207

207208
---
208209

209-
**Version**: 5
210-
**Status**: Phase 4 - Completed
210+
**Version**: 6
211+
**Status**: Phase 5 - Completed
211212
**Last Updated**: 2026-01-12

lib/features/logs/screens/logs_screen.dart

Lines changed: 8 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import 'package:logger/logger.dart';
44
import 'package:mostro_mobile/core/app_theme.dart';
55
import 'package:mostro_mobile/core/config.dart';
66
import 'package:mostro_mobile/features/logs/logs_provider.dart';
7+
import 'package:mostro_mobile/features/logs/widgets/logs_actions_menu.dart';
78
import 'package:mostro_mobile/features/settings/settings_provider.dart';
89
import 'package:mostro_mobile/generated/l10n.dart';
910
import 'package:mostro_mobile/services/logger_service.dart';
1011
import 'package:mostro_mobile/shared/utils/datetime_extensions_utils.dart';
11-
import 'package:mostro_mobile/shared/utils/snack_bar_helper.dart';
1212

1313
class LogsScreen extends ConsumerStatefulWidget {
1414
const LogsScreen({super.key});
@@ -17,7 +17,7 @@ class LogsScreen extends ConsumerStatefulWidget {
1717
ConsumerState<LogsScreen> createState() => _LogsScreenState();
1818
}
1919

20-
class _LogsScreenState extends ConsumerState<LogsScreen> {
20+
class _LogsScreenState extends ConsumerState<LogsScreen> with WidgetsBindingObserver {
2121
String? _selectedLevel;
2222
String _searchQuery = '';
2323
final TextEditingController _searchController = TextEditingController();
@@ -28,6 +28,7 @@ class _LogsScreenState extends ConsumerState<LogsScreen> {
2828
void initState() {
2929
super.initState();
3030
_scrollController.addListener(_onScroll);
31+
WidgetsBinding.instance.addObserver(this);
3132
}
3233

3334
void _onScroll() {
@@ -50,11 +51,15 @@ class _LogsScreenState extends ConsumerState<LogsScreen> {
5051

5152
@override
5253
void dispose() {
54+
WidgetsBinding.instance.removeObserver(this);
5355
_scrollController.dispose();
5456
_searchController.dispose();
5557
super.dispose();
5658
}
5759

60+
@override
61+
void didChangeAppLifecycleState(AppLifecycleState state) {}
62+
5863
Future<void> _toggleLogging(bool value) async {
5964
if (value) {
6065
await _showPerformanceWarning();
@@ -110,48 +115,6 @@ class _LogsScreenState extends ConsumerState<LogsScreen> {
110115
}
111116
}
112117

113-
Future<void> _showClearConfirmation() async {
114-
final confirmed = await showDialog<bool>(
115-
context: context,
116-
builder: (context) => AlertDialog(
117-
backgroundColor: AppTheme.backgroundCard,
118-
title: Text(
119-
S.of(context)!.clearLogs,
120-
style: TextStyle(color: AppTheme.textPrimary),
121-
),
122-
content: Text(
123-
S.of(context)!.clearLogsConfirmation,
124-
style: TextStyle(color: AppTheme.textSecondary),
125-
),
126-
actions: [
127-
TextButton(
128-
onPressed: () => Navigator.of(context).pop(false),
129-
child: Text(S.of(context)!.cancel),
130-
),
131-
ElevatedButton(
132-
onPressed: () => Navigator.of(context).pop(true),
133-
style: ElevatedButton.styleFrom(
134-
backgroundColor: AppTheme.statusError,
135-
),
136-
child: Text(S.of(context)!.clear),
137-
),
138-
],
139-
),
140-
);
141-
142-
if (confirmed == true && mounted) {
143-
ref.read(logsProvider.notifier).clearLogs();
144-
WidgetsBinding.instance.addPostFrameCallback((_) {
145-
if (mounted) {
146-
SnackBarHelper.showTopSnackBar(
147-
context,
148-
S.of(context)!.logsCleared,
149-
);
150-
}
151-
});
152-
}
153-
}
154-
155118
@override
156119
Widget build(BuildContext context) {
157120
final settings = ref.watch(settingsProvider);
@@ -180,11 +143,7 @@ class _LogsScreenState extends ConsumerState<LogsScreen> {
180143
),
181144
iconTheme: const IconThemeData(color: AppTheme.textPrimary),
182145
actions: [
183-
IconButton(
184-
icon: const Icon(Icons.delete_outline),
185-
onPressed: logs.isEmpty ? null : _showClearConfirmation,
186-
tooltip: S.of(context)!.clearLogs,
187-
),
146+
LogsActionsMenu(),
188147
],
189148
),
190149
body: SafeArea(
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
import 'package:heroicons/heroicons.dart';
4+
import 'package:logger/logger.dart';
5+
import 'package:mostro_mobile/core/app_theme.dart';
6+
import 'package:mostro_mobile/features/logs/logs_provider.dart';
7+
import 'package:mostro_mobile/generated/l10n.dart';
8+
import 'package:mostro_mobile/services/logger_export_service.dart';
9+
import 'package:mostro_mobile/services/logger_service.dart';
10+
11+
class LogsActionsMenu extends ConsumerWidget {
12+
final _logger = Logger();
13+
14+
LogsActionsMenu({super.key});
15+
16+
@override
17+
Widget build(BuildContext context, WidgetRef ref) {
18+
final logs = ref.watch(logsProvider);
19+
final hasLogs = logs.isNotEmpty;
20+
21+
return PopupMenuButton<String>(
22+
icon: const HeroIcon(
23+
HeroIcons.ellipsisVertical,
24+
style: HeroIconStyle.outline,
25+
color: AppTheme.cream1,
26+
size: 24,
27+
),
28+
color: AppTheme.backgroundDark,
29+
onSelected: (value) => _handleMenuAction(context, ref, value, logs),
30+
itemBuilder: (context) => [
31+
_buildMenuItem(
32+
'save',
33+
HeroIcons.arrowDownTray,
34+
S.of(context)!.saveLogs,
35+
hasLogs ? AppTheme.cream1 : AppTheme.textSecondary,
36+
enabled: hasLogs,
37+
),
38+
_buildMenuItem(
39+
'share',
40+
HeroIcons.share,
41+
S.of(context)!.shareLogs,
42+
hasLogs ? AppTheme.cream1 : AppTheme.textSecondary,
43+
enabled: hasLogs,
44+
),
45+
_buildMenuItem(
46+
'clear',
47+
HeroIcons.trash,
48+
S.of(context)!.clearLogs,
49+
hasLogs ? AppTheme.statusError : AppTheme.textSecondary,
50+
enabled: hasLogs,
51+
),
52+
],
53+
);
54+
}
55+
56+
PopupMenuItem<String> _buildMenuItem(
57+
String value,
58+
HeroIcons icon,
59+
String label,
60+
Color color, {
61+
bool enabled = true,
62+
}) {
63+
return PopupMenuItem(
64+
value: value,
65+
enabled: enabled,
66+
child: Row(
67+
children: [
68+
HeroIcon(
69+
icon,
70+
style: HeroIconStyle.outline,
71+
size: 20,
72+
color: color,
73+
),
74+
const SizedBox(width: 12),
75+
Text(
76+
label,
77+
style: TextStyle(
78+
color: enabled ? AppTheme.textPrimary : AppTheme.textSecondary,
79+
),
80+
),
81+
],
82+
),
83+
);
84+
}
85+
86+
Future<void> _handleMenuAction(
87+
BuildContext context,
88+
WidgetRef ref,
89+
String action,
90+
List<LogEntry> logs,
91+
) async {
92+
switch (action) {
93+
case 'save':
94+
await _saveLogsToFolder(context, ref, logs);
95+
break;
96+
case 'share':
97+
await _shareLogsFile(context, logs);
98+
break;
99+
case 'clear':
100+
await _showClearConfirmation(context, ref);
101+
break;
102+
}
103+
}
104+
105+
Future<void> _saveLogsToFolder(
106+
BuildContext context,
107+
WidgetRef ref,
108+
List<LogEntry> logs,
109+
) async {
110+
final localizations = S.of(context)!;
111+
final strings = LogExportStrings(
112+
headerTitle: localizations.logsHeaderTitle,
113+
generatedLabel: localizations.logsGeneratedLabel,
114+
totalLabel: localizations.logsTotalLabel,
115+
emptyMessage: localizations.noLogsAvailable,
116+
);
117+
118+
try {
119+
final filePath = await LoggerExportService.exportLogsToFolder(logs, strings);
120+
121+
if (filePath != null && context.mounted) {
122+
ScaffoldMessenger.of(context).showSnackBar(
123+
SnackBar(
124+
content: Text(localizations.logsExportSuccess),
125+
backgroundColor: AppTheme.statusSuccess,
126+
),
127+
);
128+
}
129+
} catch (e, stackTrace) {
130+
_logger.e('Error exporting logs', error: e, stackTrace: stackTrace);
131+
if (context.mounted) {
132+
ScaffoldMessenger.of(context).showSnackBar(
133+
SnackBar(
134+
content: Text(localizations.logsExportError),
135+
backgroundColor: AppTheme.statusError,
136+
),
137+
);
138+
}
139+
}
140+
}
141+
142+
Future<void> _shareLogsFile(BuildContext context, List<LogEntry> logs) async {
143+
final localizations = S.of(context)!;
144+
final strings = LogExportStrings(
145+
headerTitle: localizations.logsHeaderTitle,
146+
generatedLabel: localizations.logsGeneratedLabel,
147+
totalLabel: localizations.logsTotalLabel,
148+
emptyMessage: localizations.noLogsAvailable,
149+
);
150+
151+
try {
152+
final file = await LoggerExportService.exportLogsForSharing(logs, strings);
153+
await LoggerExportService.shareLogs(
154+
file,
155+
subject: localizations.logsShareSubject,
156+
text: localizations.logsShareText,
157+
);
158+
} catch (e, stackTrace) {
159+
_logger.e('Error sharing logs', error: e, stackTrace: stackTrace);
160+
if (context.mounted) {
161+
ScaffoldMessenger.of(context).showSnackBar(
162+
SnackBar(
163+
content: Text(localizations.shareLogsError),
164+
backgroundColor: AppTheme.statusError,
165+
),
166+
);
167+
}
168+
}
169+
}
170+
171+
Future<void> _showClearConfirmation(BuildContext context, WidgetRef ref) async {
172+
final confirmed = await showDialog<bool>(
173+
context: context,
174+
builder: (context) => AlertDialog(
175+
backgroundColor: AppTheme.backgroundDark,
176+
title: Text(
177+
S.of(context)!.clearLogs,
178+
style: const TextStyle(color: AppTheme.textPrimary),
179+
),
180+
content: Text(
181+
S.of(context)!.clearLogsConfirmation,
182+
style: const TextStyle(color: AppTheme.textSecondary),
183+
),
184+
actions: [
185+
TextButton(
186+
onPressed: () => Navigator.of(context).pop(false),
187+
child: Text(
188+
S.of(context)!.cancel,
189+
style: const TextStyle(color: AppTheme.textSecondary),
190+
),
191+
),
192+
TextButton(
193+
onPressed: () => Navigator.of(context).pop(true),
194+
child: Text(
195+
S.of(context)!.clear,
196+
style: const TextStyle(color: AppTheme.statusError),
197+
),
198+
),
199+
],
200+
),
201+
);
202+
203+
if (confirmed == true) {
204+
ref.read(logsProvider.notifier).clearLogs();
205+
}
206+
}
207+
}

lib/features/settings/settings.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ class Settings {
55
final String? defaultFiatCode;
66
final String? selectedLanguage; // null means use system locale
77
final String? defaultLightningAddress;
8-
final List<String> blacklistedRelays; // Relays blocked by user from auto-sync
9-
final List<Map<String, dynamic>> userRelays; // User-added relays with metadata
8+
final List<String> blacklistedRelays;
9+
final List<Map<String, dynamic>> userRelays;
1010
final bool isLoggingEnabled;
1111
// Push notification settings
1212
final bool pushNotificationsEnabled;

lib/l10n/intl_en.arb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,16 @@
13001300
"devTools": "Dev Tools",
13011301
"devToolsWarning": "For debugging and troubleshooting only",
13021302
"viewAndExportLogs": "View and export application logs",
1303+
"saveLogs": "Save Logs",
1304+
"logsExportSuccess": "Logs exported successfully",
1305+
"logsExportError": "Failed to export logs",
1306+
"shareLogsError": "Failed to share logs",
1307+
"exportSettings": "Export Settings",
1308+
"logsHeaderTitle": "Mostro P2P Application Logs",
1309+
"logsGeneratedLabel": "Generated",
1310+
"logsTotalLabel": "Total logs",
1311+
"logsShareSubject": "Mostro P2P Logs",
1312+
"logsShareText": "Application logs from Mostro P2P",
13031313
"pushNotifications": "Push Notifications",
13041314
"pushNotificationsDescription": "Receive notifications when there are updates to your trades, even when the app is closed.",
13051315
"pushNotificationsNotSupported": "Push notifications are not supported on this platform.",

lib/l10n/intl_es.arb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,6 +1276,16 @@
12761276
"devTools": "Herramientas de Desarrollo",
12771277
"devToolsWarning": "Solo para depuración y solución de problemas",
12781278
"viewAndExportLogs": "Ver y exportar registros de la aplicación",
1279+
"saveLogs": "Guardar Registros",
1280+
"logsExportSuccess": "Registros exportados exitosamente",
1281+
"logsExportError": "Error al exportar registros",
1282+
"shareLogsError": "Error al compartir registros",
1283+
"exportSettings": "Configuración de Exportación",
1284+
"logsHeaderTitle": "Registros de Aplicación Mostro P2P",
1285+
"logsGeneratedLabel": "Generado",
1286+
"logsTotalLabel": "Total de registros",
1287+
"logsShareSubject": "Registros Mostro P2P",
1288+
"logsShareText": "Registros de aplicación de Mostro P2P",
12791289
"pushNotifications": "Notificaciones Push",
12801290
"pushNotificationsDescription": "Recibe notificaciones cuando haya actualizaciones en tus operaciones, incluso cuando la app está cerrada.",
12811291
"pushNotificationsNotSupported": "Las notificaciones push no están soportadas en esta plataforma.",

0 commit comments

Comments
 (0)