From c16f31bf6a8e3340265b9817e6565f2a011155dd Mon Sep 17 00:00:00 2001 From: Strycher Date: Wed, 29 Jul 2026 02:21:43 -0400 Subject: [PATCH] fix(#433): log share = Save As on desktop, download on web The share helper only invoked the OS share sheet on mobile; on all desktop it opened the containing folder, which for a temp-dir capture dumped the user into TEMP amid unrelated files. Now: - Android/iOS: OS share sheet (unchanged). - Windows/macOS/Linux: native Save As dialog (file_selector) writing the file to a user-chosen location. - Web: browser download of the log text (no on-disk file on web). Adds file_selector; web download via a js_interop helper behind a conditional import. Folds in #427 (localized share strings). LogExport.shareFile keeps a compatible signature for the serial-capture screen (#430). Co-Authored-By: Claude Opus 4.8 --- lib/l10n/app_en.arb | 6 +- lib/l10n/app_localizations.dart | 24 +++-- lib/l10n/app_localizations_bg.dart | 11 ++- lib/l10n/app_localizations_de.dart | 11 ++- lib/l10n/app_localizations_en.dart | 11 ++- lib/l10n/app_localizations_es.dart | 11 ++- lib/l10n/app_localizations_fr.dart | 11 ++- lib/l10n/app_localizations_hu.dart | 11 ++- lib/l10n/app_localizations_it.dart | 11 ++- lib/l10n/app_localizations_ja.dart | 11 ++- lib/l10n/app_localizations_ko.dart | 11 ++- lib/l10n/app_localizations_nl.dart | 11 ++- lib/l10n/app_localizations_pl.dart | 11 ++- lib/l10n/app_localizations_pt.dart | 11 ++- lib/l10n/app_localizations_ru.dart | 11 ++- lib/l10n/app_localizations_sk.dart | 11 ++- lib/l10n/app_localizations_sl.dart | 11 ++- lib/l10n/app_localizations_sv.dart | 11 ++- lib/l10n/app_localizations_uk.dart | 11 ++- lib/l10n/app_localizations_zh.dart | 11 ++- lib/screens/app_debug_log_screen.dart | 10 +- lib/screens/ble_debug_log_screen.dart | 10 +- lib/utils/log_export.dart | 128 ++++++++++++++++++++------ lib/utils/log_web_download.dart | 4 + lib/utils/log_web_download_stub.dart | 3 + lib/utils/log_web_download_web.dart | 18 ++++ pubspec.lock | 64 +++++++++++++ pubspec.yaml | 1 + 28 files changed, 374 insertions(+), 92 deletions(-) create mode 100644 lib/utils/log_web_download.dart create mode 100644 lib/utils/log_web_download_stub.dart create mode 100644 lib/utils/log_web_download_web.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0f3346f..d174e03 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -757,9 +757,11 @@ "debugLog_copyLog": "Copy log", "debugLog_clearLog": "Clear log", "debugLog_shareLog": "Share logs", - "debugLog_openLogsFolder": "Open logs folder", + "debugLog_saveLog": "Save logs", + "debugLog_downloadLog": "Download logs", "debugLog_shareSubject": "Offband Meshcore logs", - "debugLog_fileLoggingUnavailable": "File logging is unavailable on this platform", + "debugLog_logSaved": "Log saved", + "debugLog_logUnavailable": "No log available to export", "debugLog_copied": "Debug log copied", "debugLog_bleCopied": "BLE log copied", "debugLog_noEntries": "No debug logs yet", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a68a1d6..4206a7c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2806,11 +2806,17 @@ abstract class AppLocalizations { /// **'Share logs'** String get debugLog_shareLog; - /// No description provided for @debugLog_openLogsFolder. + /// No description provided for @debugLog_saveLog. /// /// In en, this message translates to: - /// **'Open logs folder'** - String get debugLog_openLogsFolder; + /// **'Save logs'** + String get debugLog_saveLog; + + /// No description provided for @debugLog_downloadLog. + /// + /// In en, this message translates to: + /// **'Download logs'** + String get debugLog_downloadLog; /// No description provided for @debugLog_shareSubject. /// @@ -2818,11 +2824,17 @@ abstract class AppLocalizations { /// **'Offband Meshcore logs'** String get debugLog_shareSubject; - /// No description provided for @debugLog_fileLoggingUnavailable. + /// No description provided for @debugLog_logSaved. + /// + /// In en, this message translates to: + /// **'Log saved'** + String get debugLog_logSaved; + + /// No description provided for @debugLog_logUnavailable. /// /// In en, this message translates to: - /// **'File logging is unavailable on this platform'** - String get debugLog_fileLoggingUnavailable; + /// **'No log available to export'** + String get debugLog_logUnavailable; /// No description provided for @debugLog_copied. /// diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 31d8b5e..fa5afe3 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1524,14 +1524,19 @@ class AppLocalizationsBg extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Копирано лого за отстраняване на грешки'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b986164..12814e0 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1521,14 +1521,19 @@ class AppLocalizationsDe extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Debug-Protokoll kopiert'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4abac00..8e45963 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1493,14 +1493,19 @@ class AppLocalizationsEn extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Debug log copied'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 55f84c7..6cd42a4 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1520,14 +1520,19 @@ class AppLocalizationsEs extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Registro de depuración copiado'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 73622c9..a6c40f0 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1525,14 +1525,19 @@ class AppLocalizationsFr extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Journal de débogage copié'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 8551031..bb15161 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -1528,14 +1528,19 @@ class AppLocalizationsHu extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Hibajelentő napló másolva'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 164d149..e941e8d 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1522,14 +1522,19 @@ class AppLocalizationsIt extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Log di debug copiato'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index e972d36..cefdbe3 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1459,14 +1459,19 @@ class AppLocalizationsJa extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'デバッグログをコピー'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index dfcbd11..8e7f8d4 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1454,14 +1454,19 @@ class AppLocalizationsKo extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => '디버깅 로그 복사'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 3897574..122044d 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1510,14 +1510,19 @@ class AppLocalizationsNl extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Debuglog gekopieerd'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index ffec406..56fa7ed 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1532,14 +1532,19 @@ class AppLocalizationsPl extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Skopiowano dziennik debugowania'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index bd9ca9e..4437000 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1519,14 +1519,19 @@ class AppLocalizationsPt extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Log de depuração copiado'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index f488fd8..24ac3d3 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1520,14 +1520,19 @@ class AppLocalizationsRu extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Журнал отладки скопирован'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 2558f43..8e0b179 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1510,14 +1510,19 @@ class AppLocalizationsSk extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Záznam ladenia skopírovaný'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 9c93f2e..ed6cefb 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1508,14 +1508,19 @@ class AppLocalizationsSl extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Beležka kopirana.'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 33bf345..b25e4c8 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1502,14 +1502,19 @@ class AppLocalizationsSv extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Felsökningslogg kopierad'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index a4f45b1..5f09a20 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1514,14 +1514,19 @@ class AppLocalizationsUk extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => 'Журнал налагодження скопійовано'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 2f0bbd1..99e472c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1440,14 +1440,19 @@ class AppLocalizationsZh extends AppLocalizations { String get debugLog_shareLog => 'Share logs'; @override - String get debugLog_openLogsFolder => 'Open logs folder'; + String get debugLog_saveLog => 'Save logs'; + + @override + String get debugLog_downloadLog => 'Download logs'; @override String get debugLog_shareSubject => 'Offband Meshcore logs'; @override - String get debugLog_fileLoggingUnavailable => - 'File logging is unavailable on this platform'; + String get debugLog_logSaved => 'Log saved'; + + @override + String get debugLog_logUnavailable => 'No log available to export'; @override String get debugLog_copied => '调试日志已复制'; diff --git a/lib/screens/app_debug_log_screen.dart b/lib/screens/app_debug_log_screen.dart index a79df51..5a10920 100644 --- a/lib/screens/app_debug_log_screen.dart +++ b/lib/screens/app_debug_log_screen.dart @@ -26,7 +26,15 @@ class AppDebugLogScreen extends StatelessWidget { IconButton( tooltip: LogExport.tooltip(context), icon: Icon(LogExport.icon), - onPressed: () => LogExport.shareLogs(context), + onPressed: () => LogExport.shareLogs( + context, + webContent: () => entries + .map( + (entry) => + '[${entry.formattedTime}] [${entry.levelLabel}] [${entry.tag}] ${entry.message}', + ) + .join('\n'), + ), ), IconButton( tooltip: context.l10n.debugLog_copyLog, diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index 7964a67..99c89d0 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -37,7 +37,15 @@ class _BleDebugLogScreenState extends State { IconButton( tooltip: LogExport.tooltip(context), icon: Icon(LogExport.icon), - onPressed: () => LogExport.shareLogs(context), + onPressed: () => LogExport.shareLogs( + context, + webContent: () => entries + .map( + (entry) => + '${entry.description}\n${entry.hexPreview}\n', + ) + .join('\n'), + ), ), IconButton( tooltip: context.l10n.debugLog_copyLog, diff --git a/lib/utils/log_export.dart b/lib/utils/log_export.dart index 1d992ce..7bb0b15 100644 --- a/lib/utils/log_export.dart +++ b/lib/utils/log_export.dart @@ -1,54 +1,126 @@ +import 'dart:io' show File; + +import 'package:file_selector/file_selector.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../l10n/l10n.dart'; import '../services/file_log_service.dart'; +import 'log_web_download.dart'; -/// Shared log export for the App-log and BLE-log screens (#393, #97). +/// Shared log export for the App-log and BLE-log screens (#393, #97) and any +/// other on-disk log file (e.g. a serial capture). Per platform: +/// +/// - Android / iOS: OS share sheet ([SharePlus]) with the file. +/// - Windows / macOS / Linux: native "Save As" dialog ([getSaveLocation]), +/// writing the file to a location the user picks. +/// - Web: browser download of the log text (there is no on-disk file on web). /// -/// The on-disk log file ([FileLogService]) holds both the app log and BLE -/// frames as one combined stream, so either screen exports the same complete -/// file. Both screens use these helpers so their Share action can't drift. +/// Both log screens use these helpers so their Share action can't drift. #433 class LogExport { const LogExport._(); + static const _defaultFileName = 'offband-log.txt'; + static bool get _isMobile => defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS; - /// Platform-standard glyph: Android share (connected nodes), iOS square+arrow, - /// desktop reveals the saved file (folder). #396 - static IconData get icon => defaultTargetPlatform == TargetPlatform.android - ? Icons.share - : _isMobile - ? Icons.ios_share - : Icons.folder_open; - - static String tooltip(BuildContext context) => _isMobile - ? context.l10n.debugLog_shareLog - : context.l10n.debugLog_openLogsFolder; - - /// Flush the on-disk log and hand it to the OS share sheet (mobile), or open - /// the logs folder in the file manager (desktop). Shows a snackbar if file - /// logging is unavailable (e.g. web). - static Future shareLogs(BuildContext context) async { - final messenger = ScaffoldMessenger.of(context); - final unavailableMessage = context.l10n.debugLog_fileLoggingUnavailable; - final subject = context.l10n.debugLog_shareSubject; + /// Platform-standard action glyph: share (mobile), save (desktop), + /// download (web). + static IconData get icon { + if (kIsWeb) return Icons.download; + if (defaultTargetPlatform == TargetPlatform.android) return Icons.share; + if (_isMobile) return Icons.ios_share; + return Icons.save_alt; + } + + static String tooltip(BuildContext context) { + if (kIsWeb) return context.l10n.debugLog_downloadLog; + return _isMobile + ? context.l10n.debugLog_shareLog + : context.l10n.debugLog_saveLog; + } + + /// Export the combined on-disk app+BLE log (the App/BLE log screens). On web + /// there is no on-disk file, so [webContent] supplies the text to download. + static Future shareLogs( + BuildContext context, { + String Function()? webContent, + }) async { + if (kIsWeb) { + await _downloadOnWeb( + context, + content: webContent?.call() ?? '', + fileName: _defaultFileName, + ); + return; + } final file = await FileLogService.instance.flushAndGetActiveFile(); + if (!context.mounted) return; + await _exportFile( + context, + file: file, + fileName: _defaultFileName, + subject: context.l10n.debugLog_shareSubject, + ); + } + + /// Export an already-written file (e.g. a serial capture dump). Native only; + /// callers on web should use [shareLogs] with a `webContent` builder instead. + static Future shareFile( + BuildContext context, + File file, { + String? subject, + String? fileName, + }) async { + await _exportFile( + context, + file: file, + fileName: fileName ?? file.uri.pathSegments.last, + subject: subject ?? context.l10n.debugLog_shareSubject, + ); + } + + static Future _exportFile( + BuildContext context, { + required File? file, + required String fileName, + required String subject, + }) async { + final messenger = ScaffoldMessenger.of(context); + final unavailable = context.l10n.debugLog_logUnavailable; + final savedMessage = context.l10n.debugLog_logSaved; if (file == null) { - messenger.showSnackBar(SnackBar(content: Text(unavailableMessage))); + messenger.showSnackBar(SnackBar(content: Text(unavailable))); return; } if (_isMobile) { await SharePlus.instance.share( ShareParams(subject: subject, files: [XFile(file.path)]), ); - } else { - final dir = FileLogService.instance.logDir; - if (dir != null) await launchUrl(Uri.file(dir.path)); + return; + } + // Desktop: let the user pick where to save the file. + final location = await getSaveLocation(suggestedName: fileName); + if (location == null) return; // user cancelled + await file.copy(location.path); + messenger.showSnackBar(SnackBar(content: Text(savedMessage))); + } + + static Future _downloadOnWeb( + BuildContext context, { + required String content, + required String fileName, + }) async { + final messenger = ScaffoldMessenger.of(context); + if (content.isEmpty) { + messenger.showSnackBar( + SnackBar(content: Text(context.l10n.debugLog_logUnavailable)), + ); + return; } + downloadTextFile(fileName, content); } } diff --git a/lib/utils/log_web_download.dart b/lib/utils/log_web_download.dart new file mode 100644 index 0000000..c3c6b14 --- /dev/null +++ b/lib/utils/log_web_download.dart @@ -0,0 +1,4 @@ +// Trigger a browser download of `content` as `fileName`. Web only; the native +// stub is a no-op and is never reached (callers guard with `kIsWeb`). #433 +export 'log_web_download_stub.dart' + if (dart.library.js_interop) 'log_web_download_web.dart'; diff --git a/lib/utils/log_web_download_stub.dart b/lib/utils/log_web_download_stub.dart new file mode 100644 index 0000000..293e5b6 --- /dev/null +++ b/lib/utils/log_web_download_stub.dart @@ -0,0 +1,3 @@ +/// Native no-op: browser download only exists on web. Never called on native +/// (callers guard with `kIsWeb`). #433 +void downloadTextFile(String fileName, String content) {} diff --git a/lib/utils/log_web_download_web.dart b/lib/utils/log_web_download_web.dart new file mode 100644 index 0000000..8d0146a --- /dev/null +++ b/lib/utils/log_web_download_web.dart @@ -0,0 +1,18 @@ +import 'dart:js_interop'; + +import 'package:web/web.dart' as web; + +/// Trigger a browser download of [content] (UTF-8 text) as [fileName] by +/// creating an in-memory Blob and clicking a transient anchor. #433 +void downloadTextFile(String fileName, String content) { + final blob = web.Blob( + [content.toJS].toJS, + web.BlobPropertyBag(type: 'text/plain;charset=utf-8'), + ); + final url = web.URL.createObjectURL(blob); + final anchor = web.document.createElement('a') as web.HTMLAnchorElement + ..href = url + ..download = fileName; + anchor.click(); + web.URL.revokeObjectURL(url); +} diff --git a/pubspec.lock b/pubspec.lock index ace95e5..2add0a5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -345,6 +345,70 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector: + dependency: "direct main" + description: + name: file_selector + sha256: bd15e43e9268db636b53eeaca9f56324d1622af30e5c34d6e267649758c84d9a + url: "https://pub.dev" + source: hosted + version: "1.1.0" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: "1d45e9910f68c16eb0c74f0b10097ad81aed516ea28054c027137e8f7d75e840" + url: "https://pub.dev" + source: hosted + version: "0.5.2+9" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: e2ecf2885c121691ce13b60db3508f53c01f869fb6e8dc5c1cfa771e4c46aeca + url: "https://pub.dev" + source: hosted + version: "0.5.3+5" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_web: + dependency: transitive + description: + name: file_selector_web + sha256: "73181fbc5257776d8ecaa6a94ab3c8e920ad143b9132a6d984a9271dfc6928d3" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" fixnum: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 353137c..a889d27 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: yaml: ^3.1.2 # Parse importable device config profiles (#403) path_provider: ^2.1.6 share_plus: ^12.0.1 + file_selector: ^1.0.3 # Native Save As dialog for log export on desktop (#433) build_pipe: ^0.3.1 material_symbols_icons: ^4.2906.0 web: ^1.1.1