mirror of
https://github.com/immich-app/immich.git
synced 2025-06-04 22:24:26 -04: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:
parent
f43c58fc6d
commit
e57e279fe1
@ -110,5 +110,7 @@
|
|||||||
"album_thumbnail_card_shared": " · Shared",
|
"album_thumbnail_card_shared": " · Shared",
|
||||||
"library_page_albums": "Albums",
|
"library_page_albums": "Albums",
|
||||||
"library_page_new_album": "New album",
|
"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:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/models/image_viewer_page_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.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/immich_toast.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/share_dialog.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||||
final ImageViewerService _imageViewerService;
|
final ImageViewerService _imageViewerService;
|
||||||
|
final ShareService _shareService;
|
||||||
|
|
||||||
ImageViewerStateNotifier(this._imageViewerService)
|
ImageViewerStateNotifier(this._imageViewerService, this._shareService)
|
||||||
: super(
|
: super(
|
||||||
ImageViewerPageState(
|
ImageViewerPageState(
|
||||||
downloadAssetStatus: DownloadAssetStatus.idle,
|
downloadAssetStatus: DownloadAssetStatus.idle,
|
||||||
@ -42,9 +46,23 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
|||||||
|
|
||||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
|
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 =
|
final imageViewerStateProvider =
|
||||||
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
|
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.asset,
|
||||||
required this.onMoreInfoPressed,
|
required this.onMoreInfoPressed,
|
||||||
required this.onDownloadPressed,
|
required this.onDownloadPressed,
|
||||||
|
required this.onSharePressed,
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final AssetResponseDto asset;
|
final AssetResponseDto asset;
|
||||||
final Function onMoreInfoPressed;
|
final Function onMoreInfoPressed;
|
||||||
final Function onDownloadPressed;
|
final Function onDownloadPressed;
|
||||||
|
final Function onSharePressed;
|
||||||
final bool loading;
|
final bool loading;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -63,6 +65,14 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
|||||||
? const Icon(Icons.favorite_rounded)
|
? const Icon(Icons.favorite_rounded)
|
||||||
: const Icon(Icons.favorite_border_rounded),
|
: const Icon(Icons.favorite_border_rounded),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
iconSize: iconSize,
|
||||||
|
splashRadius: iconSize,
|
||||||
|
onPressed: () {
|
||||||
|
onSharePressed();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.share),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
|
@ -84,6 +84,10 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
ref
|
ref
|
||||||
.watch(imageViewerStateProvider.notifier)
|
.watch(imageViewerStateProvider.notifier)
|
||||||
.downloadAsset(assetList[indexOfAsset], context);
|
.downloadAsset(assetList[indexOfAsset], context);
|
||||||
|
}, onSharePressed: () {
|
||||||
|
ref
|
||||||
|
.watch(imageViewerStateProvider.notifier)
|
||||||
|
.shareAsset(assetList[indexOfAsset], context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/home_page_state.model.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';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
||||||
HomePageStateNotifier()
|
|
||||||
|
final ShareService _shareService;
|
||||||
|
|
||||||
|
HomePageStateNotifier(this._shareService)
|
||||||
: super(
|
: super(
|
||||||
HomePageState(
|
HomePageState(
|
||||||
isMultiSelectEnable: false,
|
isMultiSelectEnable: false,
|
||||||
@ -64,9 +71,22 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
|||||||
|
|
||||||
state = state.copyWith(selectedItems: currentList);
|
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 =
|
final homePageStateProvider =
|
||||||
StateNotifierProvider<HomePageStateNotifier, HomePageState>(
|
StateNotifierProvider<HomePageStateNotifier, HomePageState>(
|
||||||
((ref) => HomePageStateNotifier()),
|
((ref) => HomePageStateNotifier(ref.watch(shareServiceProvider))),
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/delete_diaglog.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);
|
const ControlBottomAppBar({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@ -25,7 +29,7 @@ class ControlBottomAppBar extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.delete_forever_rounded,
|
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,
|
width: 60,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
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"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.27.3"
|
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:
|
shared_preferences:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -41,6 +41,7 @@ dependencies:
|
|||||||
http: 0.13.4
|
http: 0.13.4
|
||||||
cancellation_token_http: ^1.1.0
|
cancellation_token_http: ^1.1.0
|
||||||
easy_localization: ^3.0.1
|
easy_localization: ^3.0.1
|
||||||
|
share_plus: ^4.0.10
|
||||||
flutter_displaymode: ^0.4.0
|
flutter_displaymode: ^0.4.0
|
||||||
|
|
||||||
path: ^1.8.1
|
path: ^1.8.1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user