diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 1a6cc9722..5e121daf6 100644 --- a/mobile/assets/i18n/en-US.json +++ b/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" } diff --git a/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart b/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart index 4bed6eb5f..c95b64f1d 100644 --- a/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart +++ b/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 { 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 { 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( - ((ref) => ImageViewerStateNotifier(ref.watch(imageViewerServiceProvider))), + ((ref) => ImageViewerStateNotifier( + ref.watch(imageViewerServiceProvider), ref.watch(shareServiceProvider))), ); diff --git a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart index 3c298568b..d3e22bbcf 100644 --- a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart +++ b/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, diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index c9d46e6c6..51535cf1a 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/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( diff --git a/mobile/lib/modules/home/providers/home_page_state.provider.dart b/mobile/lib/modules/home/providers/home_page_state.provider.dart index f81c7199a..974706d36 100644 --- a/mobile/lib/modules/home/providers/home_page_state.provider.dart +++ b/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 { - HomePageStateNotifier() + + final ShareService _shareService; + + HomePageStateNotifier(this._shareService) : super( HomePageState( isMultiSelectEnable: false, @@ -64,9 +71,22 @@ class HomePageStateNotifier extends StateNotifier { state = state.copyWith(selectedItems: currentList); } + + void shareAssets(List 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( - ((ref) => HomePageStateNotifier()), + ((ref) => HomePageStateNotifier(ref.watch(shareServiceProvider))), ); diff --git a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart index 87b08e231..8d655a0f5 100644 --- a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart +++ b/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: () { diff --git a/mobile/lib/shared/services/share.service.dart b/mobile/lib/shared/services/share.service.dart new file mode 100644 index 000000000..6ef4e097e --- /dev/null +++ b/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 shareAsset(AssetResponseDto asset) async { + await shareAssets([asset]); + } + + Future shareAssets(List 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)); + } + +} diff --git a/mobile/lib/shared/ui/share_dialog.dart b/mobile/lib/shared/ui/share_dialog.dart new file mode 100644 index 000000000..887dcd86f --- /dev/null +++ b/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(), + ) + ], + ), + ); + } +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 6a756d003..5e5131f3d 100644 --- a/mobile/pubspec.lock +++ b/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: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index e7d495a76..2b13c26bf 100644 --- a/mobile/pubspec.yaml +++ b/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