Commit dac6a64

derdilla <82763757+derdilla@users.noreply.github.com>
2025-07-11 16:46:03
Add share button to export page (#572)
1 parent 69be7d9
app/lib/data_util/entry_context.dart
@@ -58,7 +58,7 @@ extension EntryUtils on BuildContext {
 
         log.info(read<IntervalStoreManager>());
         if (mounted && exportSettings.exportAfterEveryEntry) {
-          performExport(this);
+          performExport(this, false);
         }
       }
     } on ProviderNotFoundException {
app/lib/features/export_import/export_button.dart
@@ -22,25 +22,33 @@ import 'package:logging/logging.dart';
 import 'package:path/path.dart';
 import 'package:persistent_user_dir_access_android/persistent_user_dir_access_android.dart';
 import 'package:provider/provider.dart';
+import 'package:share_plus/share_plus.dart';
 import 'package:sqflite/sqflite.dart';
 
 /// Text button to export entries like configured in the context.
 class ExportButton extends StatelessWidget {
   /// Create a text button to export entries like configured in the context.
-  const ExportButton({super.key});
+  const ExportButton({
+    super.key,
+    required this.share,
+  });
+
+  /// Whether to use the device sharing feature instead of the saving feature
+  /// for export.
+  final bool share;
 
   @override
   Widget build(BuildContext context) => TextButton.icon(
-    label: Text(AppLocalizations.of(context)!.export),
-    icon: Icon(Icons.file_download_outlined),
-    onPressed: () => performExport(context),
+    label: Text(share ? AppLocalizations.of(context)!.btnShare : AppLocalizations.of(context)!.export),
+    icon: Icon(share ? Icons.share : Icons.file_download_outlined),
+    onPressed: () => performExport(context, share),
   );
 }
 
 Logger _logger = Logger('BPM[export_button]');
 
 /// Perform a full export according to the configuration in [context].
-void performExport(BuildContext context) async { // TODO: extract
+void performExport(BuildContext context, bool share) async { // TODO: extract
   _logger.finer('performExport - mounted=${context.mounted}');
   final localizations = AppLocalizations.of(context);
   final exportSettings = Provider.of<ExportSettings>(context, listen: false);
@@ -51,7 +59,7 @@ void performExport(BuildContext context) async { // TODO: extract
       final path = join(await getDatabasesPath(), 'bp.db');
       final data = await File(path).readAsBytes();
 
-      if (context.mounted) await _exportData(context, data, '$filename.db', 'application/vnd.sqlite3');
+      if (context.mounted) await _exportData(context, data, '$filename.db', 'application/vnd.sqlite3', share);
       break;
     case ExportFormat.csv:
       final csvSettings = Provider.of<CsvExportSettings>(context, listen: false);
@@ -70,7 +78,7 @@ void performExport(BuildContext context) async { // TODO: extract
       final data = Uint8List.fromList(utf8.encode(csvString));
       if (context.mounted) {
         _logger.finer('performExport - Calling _exportData');
-        await _exportData(context, data, '$filename.csv', 'text/csv');
+        await _exportData(context, data, '$filename.csv', 'text/csv', share);
       } else  {
         _logger.warning('performExport - No longer mounted: stopping export');
       }
@@ -83,7 +91,7 @@ void performExport(BuildContext context) async { // TODO: extract
           Provider.of<ExportColumnsManager>(context, listen: false),
       );
       final pdf = await pdfConverter.create(await _getEntries(context));
-      if (context.mounted) await _exportData(context, pdf, '$filename.pdf', 'text/pdf');
+      if (context.mounted) await _exportData(context, pdf, '$filename.pdf', 'text/pdf', share);
   }
 }
 
@@ -122,7 +130,17 @@ Future<List<(DateTime, BloodPressureRecord, Note, List<MedicineIntake>, Weight?)
 }
 
 /// Save to default export path or share by providing binary data.
-Future<void> _exportData(BuildContext context, Uint8List data, String fullFileName, String mimeType) async {
+Future<void> _exportData(BuildContext context, Uint8List data, String fullFileName, String mimeType, bool share) async {
+  if (share) {
+    _logger.fine('_exportData - Saving file using SharePlus');
+    final result = await SharePlus.instance.share(ShareParams(
+      title: AppLocalizations.of(context)!.bloodPressure,
+      files: [XFile.fromData(data, name: fullFileName, mimeType: mimeType)]
+    ));
+    log.info('_exportData - Shared data with result: $result');
+    return;
+  }
+
   final settings = Provider.of<ExportSettings>(context, listen: false);
   if (settings.defaultExportDir.isEmpty || !Platform.isAndroid) {
     _logger.fine('_exportData - Saving file using FilePicker');
app/lib/features/settings/export_import_screen.dart
@@ -181,9 +181,10 @@ class ExportImportScreen extends StatelessWidget {
             ],
           ),
         ),),
-      persistentFooterButtons: [
-        const ExportButton(),
-        const ImportButton(),
+      persistentFooterButtons: const [
+        ExportButton(share: true),
+        ExportButton(share: false),
+        ImportButton(),
       ],
     );
   }
app/lib/features/settings/version_screen.dart
@@ -1,11 +1,8 @@
 import 'package:blood_pressure_app/data_util/consistent_future_builder.dart';
-import 'package:blood_pressure_app/features/settings/tiles/titled_column.dart';
 import 'package:blood_pressure_app/logging.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:blood_pressure_app/l10n/app_localizations.dart';
-import 'package:flutter_blue_plus/flutter_blue_plus.dart';
-import 'package:logging/logging.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 
 /// Screen that shows app version and debug options.
app/lib/l10n/app_en.arb
@@ -566,5 +566,7 @@
     }
   },
   "dontShowAgain": "Don''t show again",
-  "@dontShowAgain": {}
+  "@dontShowAgain": {},
+  "btnShare": "SHARE",
+  "@btnShare": {}
 }
app/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -10,6 +10,8 @@ import bluetooth_low_energy_darwin
 import file_picker
 import flutter_blue_plus_darwin
 import package_info_plus
+import path_provider_foundation
+import share_plus
 import shared_preferences_foundation
 import sqflite_darwin
 import url_launcher_macos
@@ -20,6 +22,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
   FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
   FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
+  PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
+  SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
   UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
app/test/features/export_import/export_button_test.dart
@@ -0,0 +1,27 @@
+import 'package:blood_pressure_app/features/export_import/export_button.dart';
+import 'package:blood_pressure_app/l10n/app_localizations.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../../util.dart';
+
+void main() {
+  testWidgets('Shows share icon and text when sharing', (tester) async {
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+    await tester.pumpWidget(materialApp(ExportButton(share: true)));
+
+    expect(find.byIcon(Icons.file_download_outlined), findsNothing);
+    expect(find.byIcon(Icons.share), findsOneWidget);
+    expect(find.text(localizations.export), findsNothing);
+    expect(find.text(localizations.btnShare), findsOneWidget);
+  });
+  testWidgets('Shows download icon and export text when not sharing', (tester) async {
+    final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+    await tester.pumpWidget(materialApp(ExportButton(share: false)));
+
+    expect(find.byIcon(Icons.file_download_outlined), findsOneWidget);
+    expect(find.byIcon(Icons.share), findsNothing);
+    expect(find.text(localizations.export), findsOneWidget);
+    expect(find.text(localizations.btnShare), findsNothing);
+  });
+}
app/windows/flutter/generated_plugin_registrant.cc
@@ -7,11 +7,14 @@
 #include "generated_plugin_registrant.h"
 
 #include <bluetooth_low_energy_windows/bluetooth_low_energy_windows_plugin_c_api.h>
+#include <share_plus/share_plus_windows_plugin_c_api.h>
 #include <url_launcher_windows/url_launcher_windows.h>
 
 void RegisterPlugins(flutter::PluginRegistry* registry) {
   BluetoothLowEnergyWindowsPluginCApiRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("BluetoothLowEnergyWindowsPluginCApi"));
+  SharePlusWindowsPluginCApiRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
   UrlLauncherWindowsRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("UrlLauncherWindows"));
 }
app/windows/flutter/generated_plugins.cmake
@@ -4,6 +4,7 @@
 
 list(APPEND FLUTTER_PLUGIN_LIST
   bluetooth_low_energy_windows
+  share_plus
   url_launcher_windows
 )
 
app/pubspec.lock
@@ -755,6 +755,30 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.1.0"
+  path_provider:
+    dependency: transitive
+    description:
+      name: path_provider
+      sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.5"
+  path_provider_android:
+    dependency: transitive
+    description:
+      name: path_provider_android
+      sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.17"
+  path_provider_foundation:
+    dependency: transitive
+    description:
+      name: path_provider_foundation
+      sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.1"
   path_provider_linux:
     dependency: transitive
     description:
@@ -883,6 +907,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "0.28.0"
+  share_plus:
+    dependency: "direct main"
+    description:
+      name: share_plus
+      sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0
+      url: "https://pub.dev"
+    source: hosted
+    version: "11.0.0"
+  share_plus_platform_interface:
+    dependency: transitive
+    description:
+      name: share_plus_platform_interface
+      sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.0.0"
   shared_preferences:
     dependency: "direct main"
     description:
@@ -1008,6 +1048,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.10.1"
+  sprintf:
+    dependency: transitive
+    description:
+      name: sprintf
+      sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
   sqflite:
     dependency: "direct main"
     description:
@@ -1232,6 +1280,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.1.4"
+  uuid:
+    dependency: transitive
+    description:
+      name: uuid
+      sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.5.1"
   vector_math:
     dependency: transitive
     description:
@@ -1338,4 +1394,4 @@ packages:
     version: "3.1.3"
 sdks:
   dart: ">=3.7.0 <4.0.0"
-  flutter: ">=3.29.2"
+  flutter: ">=3.32.5"
app/pubspec.yaml
@@ -38,6 +38,7 @@ dependencies:
   app_settings: ^6.1.1
   logging: ^1.3.0
   persistent_user_dir_access_android: ^0.0.1
+  share_plus: ^11.0.0
 
   # desktop only
   sqflite_common_ffi: ^2.3.5