mirror of
https://github.com/immich-app/immich.git
synced 2026-08-02 17:09:26 -07:00
refactor: mobile share actions
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/toast.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
|
||||
final _stateProvider = Provider.family.autoDispose<List<BaseAsset>?, ActionSource>((ref, source) {
|
||||
final assets = ref.watch(assetsActionProvider(source));
|
||||
final shareable = assets.toList(growable: false);
|
||||
return shareable.isEmpty ? null : shareable;
|
||||
});
|
||||
|
||||
class ShareAction extends AssetActionBuilder {
|
||||
const ShareAction({required super.source});
|
||||
|
||||
@override
|
||||
ActionItem? create(BuildContext context, WidgetRef ref) {
|
||||
final assets = ref.watch(_stateProvider(source));
|
||||
if (assets == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return .new(
|
||||
icon: CurrentPlatform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
|
||||
label: context.t.share,
|
||||
onAction: () => _share(context, ref, assets, ref.read(appConfigProvider).share.fileType),
|
||||
onSecondaryAction: () => _promptQualityAndShare(context, ref, assets),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _promptQualityAndShare(BuildContext context, WidgetRef ref, List<BaseAsset> assets) async {
|
||||
// Only show preview option when at least one of the assets is not a video
|
||||
final showPreview = assets.any((asset) => !asset.isVideo);
|
||||
|
||||
final fileType = await showDialog<ShareAssetType>(
|
||||
context: context,
|
||||
builder: (_) => _ShareFileTypeDialog(showPreview: showPreview),
|
||||
useRootNavigator: false,
|
||||
);
|
||||
if (fileType == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _share(context, ref, assets, fileType);
|
||||
}
|
||||
|
||||
Future<void> _share(BuildContext context, WidgetRef ref, List<BaseAsset> assets, ShareAssetType fileType) async {
|
||||
final cancelCompleter = Completer<void>();
|
||||
final progress = ValueNotifier<double?>(null);
|
||||
final mediaRepository = ref.read(assetMediaRepositoryProvider);
|
||||
final toastService = ref.read(toastServiceProvider);
|
||||
final errorMessage = context.t.scaffold_body_error_occurred;
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
useRootNavigator: false,
|
||||
builder: (dialogContext) {
|
||||
void finish({required bool failed}) {
|
||||
if (cancelCompleter.isCompleted || !dialogContext.mounted) {
|
||||
return;
|
||||
}
|
||||
if (failed) {
|
||||
toastService.error(errorMessage);
|
||||
}
|
||||
dialogContext.pop();
|
||||
}
|
||||
|
||||
unawaited(
|
||||
mediaRepository
|
||||
.shareAssets(
|
||||
assets,
|
||||
context,
|
||||
fileType: fileType,
|
||||
cancelCompleter: cancelCompleter,
|
||||
onAssetDownloadProgress: (value) => progress.value = value,
|
||||
)
|
||||
.then<void>((count) => finish(failed: count == 0), onError: (_) => finish(failed: true)),
|
||||
);
|
||||
|
||||
return _SharePreparingDialog(progress: progress);
|
||||
},
|
||||
).then((_) {
|
||||
if (!cancelCompleter.isCompleted) {
|
||||
cancelCompleter.complete();
|
||||
}
|
||||
progress.dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _SharePreparingDialog extends StatelessWidget {
|
||||
final ValueNotifier<double?> progress;
|
||||
|
||||
const _SharePreparingDialog({required this.progress});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
Container(margin: const .only(bottom: 12), child: Text(context.t.share_dialog_preparing)),
|
||||
SizedBox(
|
||||
width: 240,
|
||||
child: ValueListenableBuilder<double?>(
|
||||
valueListenable: progress,
|
||||
builder: (context, value, _) {
|
||||
final percent = value == null ? null : (value * 100).clamp(0, 100);
|
||||
return Column(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
LinearProgressIndicator(value: value, minHeight: 8.0),
|
||||
if (percent != null)
|
||||
Container(margin: const .only(top: 8), child: Text('${percent.toStringAsFixed(0)}%')),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShareFileTypeDialog extends StatelessWidget {
|
||||
final bool showPreview;
|
||||
|
||||
const _ShareFileTypeDialog({this.showPreview = true});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(context.t.select_quality),
|
||||
contentPadding: const .symmetric(vertical: 8),
|
||||
content: Column(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.high_quality_rounded),
|
||||
title: Text(context.t.share_original),
|
||||
onTap: () => context.pop(ShareAssetType.original),
|
||||
),
|
||||
if (showPreview)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_size_select_large_rounded),
|
||||
title: Text(context.t.share_preview),
|
||||
onTap: () => context.pop(ShareAssetType.preview),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [TextButton(onPressed: () => context.pop(), child: Text(context.t.cancel))],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
final _stateProvider = Provider.family.autoDispose<List<String>?, ActionSource>((ref, source) {
|
||||
final assets = ref.watch(assetsActionProvider(source));
|
||||
final remoteIds = assets.remote().map((asset) => asset.id).toList(growable: false);
|
||||
return remoteIds.isEmpty ? null : remoteIds;
|
||||
});
|
||||
|
||||
class ShareLinkAction extends AssetActionBuilder {
|
||||
const ShareLinkAction({required super.source});
|
||||
|
||||
@override
|
||||
ActionItem? create(BuildContext context, WidgetRef ref) {
|
||||
final remoteIds = ref.watch(_stateProvider(source));
|
||||
if (remoteIds == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return .new(
|
||||
icon: Icons.link_rounded,
|
||||
label: context.t.share_link,
|
||||
onAction: () async => unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class _SharePreparingDialog extends StatelessWidget {
|
||||
final ValueNotifier<double?> progress;
|
||||
|
||||
const _SharePreparingDialog({required this.progress});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(margin: const EdgeInsets.only(bottom: 12), child: const Text('share_dialog_preparing').tr()),
|
||||
SizedBox(
|
||||
width: 240,
|
||||
child: ValueListenableBuilder<double?>(
|
||||
valueListenable: progress,
|
||||
builder: (context, value, _) {
|
||||
final percent = value == null ? null : (value * 100).clamp(0, 100);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
LinearProgressIndicator(value: value, minHeight: 8.0),
|
||||
if (percent != null)
|
||||
Container(margin: const EdgeInsets.only(top: 8), child: Text('${percent.toStringAsFixed(0)}%')),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShareFileTypeDialog extends StatelessWidget {
|
||||
final bool showPreview;
|
||||
|
||||
const _ShareFileTypeDialog({this.showPreview = true});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(context.t.select_quality),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.high_quality_rounded),
|
||||
title: Text(context.t.share_original),
|
||||
onTap: () => context.pop(ShareAssetType.original),
|
||||
),
|
||||
if (showPreview)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_size_select_large_rounded),
|
||||
title: Text(context.t.share_preview),
|
||||
onTap: () => context.pop(ShareAssetType.preview),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [TextButton(onPressed: () => context.pop(), child: Text(context.t.cancel))],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShareActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const ShareActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
Set<BaseAsset> _getSelectedAssets(WidgetRef ref) {
|
||||
return switch (source) {
|
||||
ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets,
|
||||
ActionSource.viewer => switch (ref.read(assetViewerProvider).currentAsset) {
|
||||
BaseAsset asset => {asset},
|
||||
null => const {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final fileType = ref.read(appConfigProvider).share.fileType;
|
||||
await _share(context, ref, fileType);
|
||||
}
|
||||
|
||||
void _onLongPress(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only show preview option when at least one of the assets is not a video
|
||||
// we cant share previews of videos
|
||||
final assets = _getSelectedAssets(ref);
|
||||
final showPreview = assets.isEmpty || assets.any((asset) => !asset.isVideo);
|
||||
|
||||
final fileType = await showDialog<ShareAssetType>(
|
||||
context: context,
|
||||
builder: (_) => _ShareFileTypeDialog(showPreview: showPreview),
|
||||
useRootNavigator: false,
|
||||
);
|
||||
|
||||
if (fileType == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _share(context, ref, fileType);
|
||||
}
|
||||
|
||||
Future<void> _share(BuildContext context, WidgetRef ref, ShareAssetType fileType) async {
|
||||
final cancelCompleter = Completer<void>();
|
||||
final progress = ValueNotifier<double?>(null);
|
||||
final preparingDialog = _SharePreparingDialog(progress: progress);
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext buildContext) {
|
||||
ref
|
||||
.read(actionProvider.notifier)
|
||||
.shareAssets(
|
||||
source,
|
||||
context,
|
||||
fileType: fileType,
|
||||
cancelCompleter: cancelCompleter,
|
||||
onAssetDownloadProgress: (value) => progress.value = value,
|
||||
)
|
||||
.then((ActionResult result) {
|
||||
if (cancelCompleter.isCompleted || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: context.t.scaffold_body_error_occurred,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
}
|
||||
|
||||
buildContext.pop();
|
||||
});
|
||||
|
||||
return preparingDialog;
|
||||
},
|
||||
barrierDismissible: false,
|
||||
useRootNavigator: false,
|
||||
).then((_) {
|
||||
if (!cancelCompleter.isCompleted) {
|
||||
cancelCompleter.complete();
|
||||
}
|
||||
progress.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
|
||||
label: context.t.share,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
onLongPressed: () => _onLongPress(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
|
||||
class ShareLinkActionButton extends ConsumerWidget {
|
||||
final ActionSource source;
|
||||
final bool iconOnly;
|
||||
final bool menuItem;
|
||||
|
||||
const ShareLinkActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
|
||||
|
||||
_onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(actionProvider.notifier).shareLink(source, context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.link_rounded,
|
||||
label: "share_link".t(context: context),
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import 'package:immich_mobile/presentation/actions/action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/delete.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/edit_asset.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/restore.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
@@ -45,7 +45,7 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
|
||||
final actions = <Widget>[
|
||||
..._actionColumnButtons(context, ref, const [RestoreAction(source: .viewer)]),
|
||||
const ShareActionButton(source: .viewer),
|
||||
..._actionColumnButtons(context, ref, const [ShareAction(source: .viewer)]),
|
||||
|
||||
if (!isInLockedView) ...[
|
||||
if (!isInTrash) ...[
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share_link.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/edit_location.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/delete.action.dart';
|
||||
@@ -12,8 +14,6 @@ import 'package:immich_mobile/presentation/actions/archive.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/stack.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
@@ -76,9 +76,9 @@ class _ArchiveBottomSheetState extends ConsumerState<ArchiveBottomSheet> {
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
const ActionColumnButton(action: ShareAction(source: .timeline)),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const ActionColumnButton(action: ShareLinkAction(source: .timeline)),
|
||||
const ActionColumnButton(action: ArchiveAction(source: .timeline)),
|
||||
const ActionColumnButton(action: FavoriteAction(source: .timeline)),
|
||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share_link.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/edit_location.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/delete.action.dart';
|
||||
@@ -13,8 +15,6 @@ import 'package:immich_mobile/presentation/actions/archive.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/stack.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
@@ -73,9 +73,9 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
maxChildSize: 0.7,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
const ActionColumnButton(action: ShareAction(source: .timeline)),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const ActionColumnButton(action: ShareLinkAction(source: .timeline)),
|
||||
const ActionColumnButton(action: FavoriteAction(source: .timeline)),
|
||||
const ActionColumnButton(action: ArchiveAction(source: .timeline)),
|
||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share_link.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/edit_location.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/archive.action.dart';
|
||||
@@ -13,8 +15,6 @@ import 'package:immich_mobile/presentation/actions/lock.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/stack.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
@@ -83,9 +83,9 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
const ActionColumnButton(action: FavoriteAction(source: .timeline)),
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
const ActionColumnButton(action: ShareAction(source: .timeline)),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const ActionColumnButton(action: ShareLinkAction(source: .timeline)),
|
||||
if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline),
|
||||
const ActionColumnButton(action: FavoriteAction(source: .timeline)),
|
||||
const ActionColumnButton(action: ArchiveAction(source: .timeline)),
|
||||
|
||||
@@ -4,8 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/delete.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
@@ -66,7 +66,7 @@ class _LocalAlbumBottomSheetState extends ConsumerState<LocalAlbumBottomSheet> {
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: const [
|
||||
ShareActionButton(source: ActionSource.timeline),
|
||||
ActionColumnButton(action: ShareAction(source: .timeline)),
|
||||
ActionColumnButton(action: DeleteAction(source: .timeline)),
|
||||
ActionColumnButton(action: CleanupLocalAction(source: .timeline)),
|
||||
UploadActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/delete.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/lock.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
|
||||
class LockedFolderBottomSheet extends ConsumerWidget {
|
||||
@@ -18,7 +18,7 @@ class LockedFolderBottomSheet extends ConsumerWidget {
|
||||
maxChildSize: 0.4,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
ShareActionButton(source: ActionSource.timeline),
|
||||
ActionColumnButton(action: ShareAction(source: .timeline)),
|
||||
DownloadActionButton(source: ActionSource.timeline),
|
||||
ActionColumnButton(action: DeleteAction(source: .timeline)),
|
||||
ActionColumnButton(action: LockAction(source: .timeline)),
|
||||
|
||||
+3
-2
@@ -4,8 +4,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -64,7 +65,7 @@ class _PartnerDetailBottomSheetState extends ConsumerState<PartnerDetailBottomSh
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: const [
|
||||
ShareActionButton(source: ActionSource.timeline),
|
||||
ActionColumnButton(action: ShareAction(source: .timeline)),
|
||||
DownloadActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
slivers: [
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share_link.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/edit_location.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/delete.action.dart';
|
||||
@@ -14,8 +16,6 @@ import 'package:immich_mobile/presentation/actions/favorite.action.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
@@ -86,9 +86,9 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
const ActionColumnButton(action: ShareAction(source: .timeline)),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
const ActionColumnButton(action: ShareLinkAction(source: .timeline)),
|
||||
|
||||
if (ownsAlbum) ...[
|
||||
const ActionColumnButton(action: ArchiveAction(source: .timeline)),
|
||||
|
||||
@@ -82,17 +82,6 @@ class ActionNotifier extends Notifier<void> {
|
||||
return ActionResult(count: assets.length, success: true);
|
||||
}
|
||||
|
||||
Future<ActionResult> shareLink(ActionSource source, BuildContext context) async {
|
||||
final ids = _getRemoteIdsForSource(source);
|
||||
try {
|
||||
await _service.shareLink(ids, context);
|
||||
return ActionResult(count: ids.length, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to create shared link for assets', error, stack);
|
||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> emptyTrash(String userId) async {
|
||||
try {
|
||||
final count = await _service.emptyTrash(userId);
|
||||
@@ -239,30 +228,6 @@ class ActionNotifier extends Notifier<void> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> shareAssets(
|
||||
ActionSource source,
|
||||
BuildContext context, {
|
||||
ShareAssetType fileType = ShareAssetType.original,
|
||||
Completer<void>? cancelCompleter,
|
||||
void Function(double progress)? onAssetDownloadProgress,
|
||||
}) async {
|
||||
final ids = _getAssets(source).toList(growable: false);
|
||||
|
||||
try {
|
||||
final count = await _service.shareAssets(
|
||||
ids,
|
||||
context,
|
||||
fileType: fileType,
|
||||
cancelCompleter: cancelCompleter,
|
||||
onAssetDownloadProgress: onAssetDownloadProgress,
|
||||
);
|
||||
return ActionResult(count: count, success: count > 0 || ids.isEmpty);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to share assets', error, stack);
|
||||
return ActionResult(count: ids.length, success: false, error: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> downloadAll(ActionSource source) async {
|
||||
final assets = _getAssets(source).whereType<RemoteAsset>().toList(growable: false);
|
||||
try {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/tag.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||
@@ -11,10 +9,8 @@ import 'package:immich_mobile/infrastructure/repositories/remote_asset.repositor
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/download.repository.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/common/tag_picker.dart';
|
||||
|
||||
final actionServiceProvider = Provider<ActionService>(
|
||||
@@ -23,7 +19,6 @@ final actionServiceProvider = Provider<ActionService>(
|
||||
ref.watch(remoteAssetRepositoryProvider),
|
||||
ref.watch(driftAlbumApiRepositoryProvider),
|
||||
ref.watch(remoteAlbumRepository),
|
||||
ref.watch(assetMediaRepositoryProvider),
|
||||
ref.watch(downloadRepositoryProvider),
|
||||
ref.watch(tagServiceProvider),
|
||||
),
|
||||
@@ -34,7 +29,6 @@ class ActionService {
|
||||
final RemoteAssetRepository _remoteAssetRepository;
|
||||
final DriftAlbumApiRepository _albumApiRepository;
|
||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||
final AssetMediaRepository _assetMediaRepository;
|
||||
final DownloadRepository _downloadRepository;
|
||||
final TagService _tagService;
|
||||
|
||||
@@ -43,15 +37,10 @@ class ActionService {
|
||||
this._remoteAssetRepository,
|
||||
this._albumApiRepository,
|
||||
this._remoteAlbumRepository,
|
||||
this._assetMediaRepository,
|
||||
this._downloadRepository,
|
||||
this._tagService,
|
||||
);
|
||||
|
||||
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
||||
unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds)));
|
||||
}
|
||||
|
||||
Future<int> emptyTrash(String userId) async {
|
||||
final count = await _assetApiRepository.emptyTrash();
|
||||
await _remoteAssetRepository.emptyTrash(userId);
|
||||
@@ -108,22 +97,6 @@ class ActionService {
|
||||
return _tagService.bulkTagAssets(remoteIds, selectedTagIds.toList());
|
||||
}
|
||||
|
||||
Future<int> shareAssets(
|
||||
List<BaseAsset> assets,
|
||||
BuildContext context, {
|
||||
ShareAssetType fileType = ShareAssetType.original,
|
||||
Completer<void>? cancelCompleter,
|
||||
void Function(double progress)? onAssetDownloadProgress,
|
||||
}) {
|
||||
return _assetMediaRepository.shareAssets(
|
||||
assets,
|
||||
context,
|
||||
fileType: fileType,
|
||||
cancelCompleter: cancelCompleter,
|
||||
onAssetDownloadProgress: onAssetDownloadProgress,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<bool>> downloadAll(List<RemoteAsset> assets) {
|
||||
return _downloadRepository.downloadAllAssets(assets);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import 'package:immich_mobile/presentation/actions/lock.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/restore.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/set_profile_picture.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share_link.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/similar_photos.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/slideshow.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/stack.action.dart';
|
||||
@@ -24,8 +26,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/download_actio
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
@@ -173,12 +173,8 @@ enum ActionButtonType {
|
||||
]) {
|
||||
return switch (this) {
|
||||
ActionButtonType.advancedInfo => ActionMenuItem(action: AssetDebugAction(source: context.source)),
|
||||
ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||
ActionButtonType.shareLink => ShareLinkActionButton(
|
||||
source: context.source,
|
||||
iconOnly: iconOnly,
|
||||
menuItem: menuItem,
|
||||
),
|
||||
ActionButtonType.share => ActionMenuItem(action: ShareAction(source: context.source)),
|
||||
ActionButtonType.shareLink => ActionMenuItem(action: ShareLinkAction(source: context.source)),
|
||||
ActionButtonType.slideshow => const ActionMenuItem(action: SlideshowAction()),
|
||||
ActionButtonType.archive ||
|
||||
ActionButtonType.unarchive => ActionMenuItem(action: ArchiveAction(source: context.source)),
|
||||
|
||||
@@ -19,7 +19,6 @@ void main() {
|
||||
late MockRemoteAssetRepository remoteAssetRepository;
|
||||
late MockDriftAlbumApiRepository albumApiRepository;
|
||||
late MockRemoteAlbumRepository remoteAlbumRepository;
|
||||
late MockAssetMediaRepository assetMediaRepository;
|
||||
late MockDownloadRepository downloadRepository;
|
||||
late MockTagService tagService;
|
||||
|
||||
@@ -44,7 +43,6 @@ void main() {
|
||||
remoteAssetRepository = MockRemoteAssetRepository();
|
||||
albumApiRepository = MockDriftAlbumApiRepository();
|
||||
remoteAlbumRepository = MockRemoteAlbumRepository();
|
||||
assetMediaRepository = MockAssetMediaRepository();
|
||||
downloadRepository = MockDownloadRepository();
|
||||
tagService = MockTagService();
|
||||
|
||||
@@ -53,7 +51,6 @@ void main() {
|
||||
remoteAssetRepository,
|
||||
albumApiRepository,
|
||||
remoteAlbumRepository,
|
||||
assetMediaRepository,
|
||||
downloadRepository,
|
||||
tagService,
|
||||
);
|
||||
@@ -88,5 +85,4 @@ void main() {
|
||||
verify(() => remoteAssetRepository.updateRating(assetId, null)).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
|
||||
import '../../factories/remote_asset_factory.dart';
|
||||
import '../presentation_context.dart';
|
||||
|
||||
class _RecordingActionNotifier extends ActionNotifier {
|
||||
final List<ShareAssetType> sharedFileTypes = [];
|
||||
|
||||
@override
|
||||
void build() {}
|
||||
|
||||
@override
|
||||
Future<ActionResult> shareAssets(
|
||||
ActionSource source,
|
||||
BuildContext context, {
|
||||
ShareAssetType fileType = ShareAssetType.original,
|
||||
Completer<void>? cancelCompleter,
|
||||
void Function(double progress)? onAssetDownloadProgress,
|
||||
}) async {
|
||||
sharedFileTypes.add(fileType);
|
||||
return const ActionResult(count: 1, success: true);
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeAssetViewerNotifier extends AssetViewerStateNotifier {
|
||||
final BaseAsset asset;
|
||||
|
||||
_FakeAssetViewerNotifier(this.asset);
|
||||
|
||||
@override
|
||||
AssetViewerState build() => AssetViewerState(currentAsset: asset);
|
||||
}
|
||||
|
||||
void main() {
|
||||
late PresentationContext context;
|
||||
late _RecordingActionNotifier actionNotifier;
|
||||
|
||||
setUpAll(() async {
|
||||
final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
await SettingsRepository.ensureInitialized(db);
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
context = await PresentationContext.create();
|
||||
actionNotifier = _RecordingActionNotifier();
|
||||
await SettingsRepository.instance.clear([SettingsKey.shareFileType]);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
context.dispose();
|
||||
});
|
||||
|
||||
Future<void> pumpShareButton(WidgetTester tester) async {
|
||||
final asset = RemoteAssetFactory.create(ownerId: context.currentUser.id);
|
||||
await tester.pumpTestWidget(
|
||||
context,
|
||||
const ShareActionButton(source: ActionSource.viewer),
|
||||
overrides: [
|
||||
actionProvider.overrideWith(() => actionNotifier),
|
||||
assetViewerProvider.overrideWith(() => _FakeAssetViewerNotifier(asset)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> longPressAndPickPreview(WidgetTester tester) async {
|
||||
await tester.longPress(find.byType(BaseActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byIcon(Icons.photo_size_select_large_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
group('ShareActionButton', () {
|
||||
testWidgets('single press shares with the configured default quality', (tester) async {
|
||||
await pumpShareButton(tester);
|
||||
|
||||
await tester.tap(find.byType(BaseActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(actionNotifier.sharedFileTypes, [ShareAssetType.original]);
|
||||
});
|
||||
|
||||
testWidgets('long press shares with the quality picked in the dialog', (tester) async {
|
||||
await pumpShareButton(tester);
|
||||
|
||||
await longPressAndPickPreview(tester);
|
||||
|
||||
expect(actionNotifier.sharedFileTypes, [ShareAssetType.preview]);
|
||||
});
|
||||
|
||||
testWidgets('quality picked on long press is a one-time choice and does not change the default', (tester) async {
|
||||
await pumpShareButton(tester);
|
||||
|
||||
await longPressAndPickPreview(tester);
|
||||
expect(actionNotifier.sharedFileTypes, [ShareAssetType.preview]);
|
||||
|
||||
await tester.tap(find.byType(BaseActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(actionNotifier.sharedFileTypes, [ShareAssetType.preview, ShareAssetType.original]);
|
||||
expect(SettingsRepository.instance.appConfig.share.fileType, ShareAssetType.original);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/presentation/actions/action.widget.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share.action.dart';
|
||||
import 'package:immich_mobile/presentation/actions/share_link.action.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../factories/local_asset_factory.dart';
|
||||
import '../../factories/remote_asset_factory.dart';
|
||||
import '../presentation_context.dart';
|
||||
|
||||
void main() {
|
||||
late PresentationContext context;
|
||||
|
||||
setUpAll(() async {
|
||||
final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
await SettingsRepository.ensureInitialized(db);
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
context = await PresentationContext.create();
|
||||
await SettingsRepository.instance.clear([SettingsKey.shareFileType]);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
context.dispose();
|
||||
});
|
||||
|
||||
late BuildContext actionContext;
|
||||
late WidgetRef actionRef;
|
||||
|
||||
Future<void> pumpShare(WidgetTester tester, {Set<BaseAsset>? selection}) => tester.pumpTestWidget(
|
||||
context,
|
||||
Consumer(
|
||||
builder: (widgetContext, ref, _) {
|
||||
actionContext = widgetContext;
|
||||
actionRef = ref;
|
||||
return const ActionIconButton(action: ShareAction(source: .timeline));
|
||||
},
|
||||
),
|
||||
overrides: [
|
||||
...context.selected(selection ?? {RemoteAssetFactory.create(ownerId: context.currentUser.id)}),
|
||||
],
|
||||
);
|
||||
|
||||
// TODO: Replace with button tap once long press support in ui is merged
|
||||
Future<void> invokeSecondaryAction() async {
|
||||
final resolved = const ShareAction(source: .timeline).create(actionContext, actionRef);
|
||||
await resolved!.onSecondaryAction!();
|
||||
}
|
||||
|
||||
List<ShareAssetType> sharedFileTypes() => verify(
|
||||
() => context.repository.assetMedia.api.shareAssets(
|
||||
any(),
|
||||
any(),
|
||||
fileType: captureAny(named: 'fileType'),
|
||||
cancelCompleter: any(named: 'cancelCompleter'),
|
||||
onAssetDownloadProgress: any(named: 'onAssetDownloadProgress'),
|
||||
),
|
||||
).captured.cast<ShareAssetType>();
|
||||
|
||||
Future<void> settle(WidgetTester tester) async {
|
||||
await tester.pump();
|
||||
await tester.pump(const .new(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> pickPreviewQuality(WidgetTester tester) async {
|
||||
final shared = invokeSecondaryAction();
|
||||
await settle(tester);
|
||||
await tester.tap(find.byIcon(Icons.photo_size_select_large_rounded));
|
||||
await settle(tester);
|
||||
await shared;
|
||||
}
|
||||
|
||||
group('ShareAction', () {
|
||||
testWidgets('single press shares with the configured default quality', (tester) async {
|
||||
await pumpShare(tester);
|
||||
|
||||
await tester.tap(find.byType(ImmichIconButton));
|
||||
await settle(tester);
|
||||
|
||||
expect(sharedFileTypes(), [ShareAssetType.original]);
|
||||
});
|
||||
|
||||
testWidgets('the secondary action shares with the quality picked in the dialog', (tester) async {
|
||||
await pumpShare(tester);
|
||||
|
||||
await pickPreviewQuality(tester);
|
||||
|
||||
expect(sharedFileTypes(), [ShareAssetType.preview]);
|
||||
});
|
||||
|
||||
testWidgets('quality picked there is a one-time choice and does not change the default', (tester) async {
|
||||
await pumpShare(tester);
|
||||
|
||||
await pickPreviewQuality(tester);
|
||||
await tester.tap(find.byType(ImmichIconButton));
|
||||
await settle(tester);
|
||||
|
||||
expect(sharedFileTypes(), [ShareAssetType.preview, ShareAssetType.original]);
|
||||
expect(SettingsRepository.instance.appConfig.share.fileType, ShareAssetType.original);
|
||||
});
|
||||
|
||||
testWidgets('offers no preview option for a video, which has none to share', (tester) async {
|
||||
await pumpShare(
|
||||
tester,
|
||||
selection: {RemoteAssetFactory.create(ownerId: context.currentUser.id, type: .video)},
|
||||
);
|
||||
|
||||
final shared = invokeSecondaryAction();
|
||||
await settle(tester);
|
||||
|
||||
expect(find.byIcon(Icons.high_quality_rounded), findsOneWidget);
|
||||
expect(find.byIcon(Icons.photo_size_select_large_rounded), findsNothing);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.high_quality_rounded));
|
||||
await settle(tester);
|
||||
await shared;
|
||||
expect(find.byIcon(Icons.photo_size_select_large_rounded), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('is hidden when nothing is selected', (tester) async {
|
||||
await pumpShare(tester, selection: const {});
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
group('ShareLinkAction', () {
|
||||
testWidgets('offers a link for a remote asset', (tester) async {
|
||||
await tester.pumpTestWidget(
|
||||
context,
|
||||
const ActionIconButton(action: ShareLinkAction(source: .timeline)),
|
||||
overrides: context.selected({RemoteAssetFactory.create()}),
|
||||
);
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('is hidden for a local-only asset, which has nothing to link to', (tester) async {
|
||||
await tester.pumpTestWidget(
|
||||
context,
|
||||
const ActionIconButton(action: ShareLinkAction(source: .timeline)),
|
||||
overrides: context.selected({LocalAssetFactory.create()}),
|
||||
);
|
||||
|
||||
expect(find.byType(ImmichIconButton), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/services/cleanup.service.dart';
|
||||
import 'package:immich_mobile/services/gcast.service.dart';
|
||||
import 'package:immich_mobile/services/server_info.service.dart';
|
||||
@@ -55,6 +56,7 @@ class PresentationContext {
|
||||
serverInfoServiceProvider.overrideWithValue(service.serverInfo),
|
||||
inLockedViewProvider.overrideWithValue(false),
|
||||
remoteAssetRepositoryProvider.overrideWithValue(repository.remoteAsset.repo),
|
||||
assetMediaRepositoryProvider.overrideWithValue(repository.assetMedia.api),
|
||||
];
|
||||
|
||||
List<Override> selected(Set<BaseAsset> assets) => [
|
||||
|
||||
Reference in New Issue
Block a user