Sfoglia il codice sorgente

Share assets from mobile to other apps (#435)

* Share unique assets

* Style share preparing dialog

* Share assets from multiselect

* Fix i18n

* Use navigator like in delete dialog

* Center bottom-bar buttons
Matthias Rupp 3 anni fa
parent
commit
e57e279fe1

+ 3 - 1
mobile/assets/i18n/en-US.json

@@ -110,5 +110,7 @@
   "album_thumbnail_card_shared": " · Shared",
   "library_page_albums": "Albums",
   "library_page_new_album": "New album",
-  "create_album_page_untitled": "Untitled"
+  "create_album_page_untitled": "Untitled",
+  "share_dialog_preparing": "Preparing...",
+  "control_bottom_app_bar_share": "Share"
 }

+ 20 - 2
mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart

@@ -1,15 +1,19 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
 import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
+import 'package:immich_mobile/shared/services/share.service.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
+import 'package:immich_mobile/shared/ui/share_dialog.dart';
 import 'package:openapi/api.dart';
 
 class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
   final ImageViewerService _imageViewerService;
+  final ShareService _shareService;
 
-  ImageViewerStateNotifier(this._imageViewerService)
+  ImageViewerStateNotifier(this._imageViewerService, this._shareService)
       : super(
           ImageViewerPageState(
             downloadAssetStatus: DownloadAssetStatus.idle,
@@ -42,9 +46,23 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
 
     state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
   }
+
+  void shareAsset(AssetResponseDto asset, BuildContext context) async {
+    showDialog(
+      context: context,
+      builder: (BuildContext buildContext) {
+        _shareService
+            .shareAsset(asset)
+            .then((_) => Navigator.of(buildContext).pop());
+        return const ShareDialog();
+      },
+      barrierDismissible: false,
+    );
+  }
 }
 
 final imageViewerStateProvider =
     StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
-  ((ref) => ImageViewerStateNotifier(ref.watch(imageViewerServiceProvider))),
+  ((ref) => ImageViewerStateNotifier(
+      ref.watch(imageViewerServiceProvider), ref.watch(shareServiceProvider))),
 );

+ 10 - 0
mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart

@@ -11,12 +11,14 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
     required this.asset,
     required this.onMoreInfoPressed,
     required this.onDownloadPressed,
+    required this.onSharePressed,
     this.loading = false
   }) : super(key: key);
 
   final AssetResponseDto asset;
   final Function onMoreInfoPressed;
   final Function onDownloadPressed;
+  final Function onSharePressed;
   final bool loading;
 
   @override
@@ -63,6 +65,14 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
               ? const Icon(Icons.favorite_rounded)
               : const Icon(Icons.favorite_border_rounded),
         ),
+        IconButton(
+          iconSize: iconSize,
+          splashRadius: iconSize,
+          onPressed: () {
+            onSharePressed();
+          },
+          icon: const Icon(Icons.share),
+        ),
         IconButton(
           iconSize: iconSize,
           splashRadius: iconSize,

+ 4 - 0
mobile/lib/modules/asset_viewer/views/gallery_viewer.dart

@@ -84,6 +84,10 @@ class GalleryViewerPage extends HookConsumerWidget {
           ref
               .watch(imageViewerStateProvider.notifier)
               .downloadAsset(assetList[indexOfAsset], context);
+        }, onSharePressed: () {
+          ref
+              .watch(imageViewerStateProvider.notifier)
+              .shareAsset(assetList[indexOfAsset], context);
         },
       ),
       body: SafeArea(

+ 22 - 2
mobile/lib/modules/home/providers/home_page_state.provider.dart

@@ -1,9 +1,16 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
+import 'package:immich_mobile/shared/services/share.service.dart';
+import 'package:immich_mobile/shared/ui/share_dialog.dart';
 import 'package:openapi/api.dart';
 
 class HomePageStateNotifier extends StateNotifier<HomePageState> {
-  HomePageStateNotifier()
+
+  final ShareService _shareService;
+
+  HomePageStateNotifier(this._shareService)
       : super(
           HomePageState(
             isMultiSelectEnable: false,
@@ -64,9 +71,22 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
 
     state = state.copyWith(selectedItems: currentList);
   }
+
+  void shareAssets(List<AssetResponseDto> assets, BuildContext context) {
+    showDialog(
+      context: context,
+      builder: (BuildContext buildContext) {
+        _shareService
+            .shareAssets(assets)
+            .then((_) => Navigator.of(buildContext).pop());
+        return const ShareDialog();
+      },
+      barrierDismissible: false,
+    );
+  }
 }
 
 final homePageStateProvider =
     StateNotifierProvider<HomePageStateNotifier, HomePageState>(
-  ((ref) => HomePageStateNotifier()),
+  ((ref) => HomePageStateNotifier(ref.watch(shareServiceProvider))),
 );

+ 22 - 4
mobile/lib/modules/home/ui/control_bottom_app_bar.dart

@@ -1,12 +1,16 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
 
-class ControlBottomAppBar extends StatelessWidget {
+import '../../../shared/providers/asset.provider.dart';
+import '../providers/home_page_state.provider.dart';
+
+class ControlBottomAppBar extends ConsumerWidget {
   const ControlBottomAppBar({Key? key}) : super(key: key);
 
   @override
-  Widget build(BuildContext context) {
+  Widget build(BuildContext context, WidgetRef ref) {
     return Positioned(
       bottom: 0,
       left: 0,
@@ -25,7 +29,7 @@ class ControlBottomAppBar extends StatelessWidget {
             Padding(
               padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
               child: Row(
-                mainAxisAlignment: MainAxisAlignment.center,
+                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                 children: [
                   ControlBoxButton(
                     iconData: Icons.delete_forever_rounded,
@@ -39,6 +43,20 @@ class ControlBottomAppBar extends StatelessWidget {
                       );
                     },
                   ),
+                  ControlBoxButton(
+                    iconData: Icons.share,
+                    label: "control_bottom_app_bar_share".tr(),
+                    onPressed: () {
+                      final homePageState = ref.watch(homePageStateProvider);
+                      ref.watch(homePageStateProvider.notifier).shareAssets(
+                            homePageState.selectedItems.toList(),
+                            context,
+                          );
+                      ref
+                          .watch(homePageStateProvider.notifier)
+                          .disableMultiSelect();
+                    },
+                  ),
                 ],
               ),
             )
@@ -67,7 +85,7 @@ class ControlBoxButton extends StatelessWidget {
       width: 60,
       child: Column(
         mainAxisAlignment: MainAxisAlignment.start,
-        crossAxisAlignment: CrossAxisAlignment.start,
+        crossAxisAlignment: CrossAxisAlignment.center,
         children: [
           IconButton(
             onPressed: () {

+ 45 - 0
mobile/lib/shared/services/share.service.dart

@@ -0,0 +1,45 @@
+
+import 'dart:io';
+
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/shared/providers/api.provider.dart';
+import 'package:openapi/api.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:share_plus/share_plus.dart';
+import 'package:path/path.dart' as p;
+import 'api.service.dart';
+
+final shareServiceProvider =
+  Provider((ref) => ShareService(ref.watch(apiServiceProvider)));
+
+class ShareService {
+  final ApiService _apiService;
+
+  ShareService(this._apiService);
+
+  Future<void> shareAsset(AssetResponseDto asset) async {
+    await shareAssets([asset]);
+  }
+
+  Future<void> shareAssets(List<AssetResponseDto> assets) async {
+    final downloadedFilePaths = assets.map((asset) async {
+      final res = await _apiService.assetApi.downloadFileWithHttpInfo(
+        asset.deviceAssetId,
+        asset.deviceId,
+        isThumb: false,
+        isWeb: false,
+      );
+
+      final fileName = p.basename(asset.originalPath);
+
+      final tempDir = await getTemporaryDirectory();
+      final tempFile = await File('${tempDir.path}/$fileName').create();
+      tempFile.writeAsBytesSync(res.bodyBytes);
+
+      return tempFile.path;
+    });
+
+    Share.shareFiles(await Future.wait(downloadedFilePaths));
+  }
+
+}

+ 23 - 0
mobile/lib/shared/ui/share_dialog.dart

@@ -0,0 +1,23 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+
+class ShareDialog extends StatelessWidget {
+  const ShareDialog({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      content: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          const CircularProgressIndicator(),
+          Container(
+            margin: const EdgeInsets.only(top: 12),
+            child: const Text('share_dialog_preparing')
+                .tr(),
+          )
+        ],
+      ),
+    );
+  }
+}

+ 42 - 0
mobile/pubspec.lock

@@ -875,6 +875,48 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.27.3"
+  share_plus:
+    dependency: "direct main"
+    description:
+      name: share_plus
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.0.10"
+  share_plus_linux:
+    dependency: transitive
+    description:
+      name: share_plus_linux
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.0"
+  share_plus_macos:
+    dependency: transitive
+    description:
+      name: share_plus_macos
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  share_plus_platform_interface:
+    dependency: transitive
+    description:
+      name: share_plus_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.3"
+  share_plus_web:
+    dependency: transitive
+    description:
+      name: share_plus_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  share_plus_windows:
+    dependency: transitive
+    description:
+      name: share_plus_windows
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
   shared_preferences:
     dependency: transitive
     description:

+ 1 - 0
mobile/pubspec.yaml

@@ -41,6 +41,7 @@ dependencies:
   http: 0.13.4
   cancellation_token_http: ^1.1.0
   easy_localization: ^3.0.1
+  share_plus: ^4.0.10
   flutter_displaymode: ^0.4.0
 
   path: ^1.8.1