You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	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
This commit is contained in:
		| @@ -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" | ||||
| } | ||||
|   | ||||
| @@ -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))), | ||||
| ); | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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))), | ||||
| ); | ||||
|   | ||||
| @@ -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
									
								
								mobile/lib/shared/services/share.service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								mobile/lib/shared/services/share.service.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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
									
								
								mobile/lib/shared/ui/share_dialog.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								mobile/lib/shared/ui/share_dialog.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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(), | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user