From 8132e8a38cf3962bfb15531941bb6e17a6aace54 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 10 Jun 2026 09:26:09 -0500 Subject: [PATCH] feat: image quality option in sharing (#28918) * feat: share with quality options * merge main * clean up * refactor * translation * translation * add settings and default behavior * fix: lint * cleanup * merge main --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- i18n/en.json | 5 + mobile/lib/constants/enums.dart | 2 + .../lib/domain/models/config/app_config.dart | 14 +- .../domain/models/config/share_config.dart | 18 ++ mobile/lib/domain/models/settings_key.dart | 3 + .../base_action_button.widget.dart | 1 + .../share_action_button.widget.dart | 67 ++++- .../infrastructure/action.provider.dart | 6 +- .../repositories/asset_media.repository.dart | 249 +++++++++++++----- mobile/lib/services/action.service.dart | 2 + .../preference_setting.dart | 3 +- .../preference_settings/share_setting.dart | 48 ++++ 12 files changed, 343 insertions(+), 75 deletions(-) create mode 100644 mobile/lib/domain/models/config/share_config.dart create mode 100644 mobile/lib/widgets/settings/preference_settings/share_setting.dart diff --git a/i18n/en.json b/i18n/en.json index a4290dadd5..455e1cad21 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -915,6 +915,8 @@ "deduplicate_all": "Deduplicate All", "default_locale": "Default Locale", "default_locale_description": "Format dates and numbers based on your browser locale", + "default_quality_subtitle": "Quality used when tapping share. Long press the share button to choose each time.", + "default_share_quality": "Default share quality", "delete": "Delete", "delete_action_confirmation_message": "Are you sure you want to delete this asset? This action will move the asset to the server's trash and will prompt if you want to delete it locally", "delete_action_prompt": "{count} deleted", @@ -2084,6 +2086,7 @@ "select_person": "Select person", "select_person_to_tag": "Select a person to tag", "select_photos": "Select photos", + "select_quality": "Select quality", "select_trash_all": "Select trash all", "select_user_for_sharing_page_err_album": "Failed to create album", "selected": "Selected", @@ -2147,6 +2150,8 @@ "share_assets_selected": "{count} selected", "share_dialog_preparing": "Preparing...", "share_link": "Share Link", + "share_original": "Use original (large)", + "share_preview": "Use thumbnail (small)", "shared": "Shared", "shared_album_activities_input_disable": "Comment is disabled", "shared_album_activity_remove_content": "Do you want to delete this activity?", diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index 902b40b395..72479416a8 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -13,6 +13,8 @@ enum AssetVisibilityEnum { timeline, hidden, archive, locked } enum ActionSource { timeline, viewer } +enum ShareAssetType { original, preview } + enum CleanupStep { selectDate, scan, delete } enum AssetKeepType { none, photosOnly, videosOnly } diff --git a/mobile/lib/domain/models/config/app_config.dart b/mobile/lib/domain/models/config/app_config.dart index a955fb73a8..aed344a872 100644 --- a/mobile/lib/domain/models/config/app_config.dart +++ b/mobile/lib/domain/models/config/app_config.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/config/cleanup_config.dart'; import 'package:immich_mobile/domain/models/config/image_config.dart'; import 'package:immich_mobile/domain/models/config/map_config.dart'; import 'package:immich_mobile/domain/models/config/network_config.dart'; +import 'package:immich_mobile/domain/models/config/share_config.dart'; import 'package:immich_mobile/domain/models/config/slideshow_config.dart'; import 'package:immich_mobile/domain/models/config/theme_config.dart'; import 'package:immich_mobile/domain/models/config/timeline_config.dart'; @@ -30,6 +31,7 @@ class AppConfig { final AlbumConfig album; final BackupConfig backup; final NetworkConfig network; + final ShareConfig share; const AppConfig({ this.logLevel = .info, @@ -43,6 +45,7 @@ class AppConfig { this.album = const .new(), this.backup = const .new(), this.network = const .new(), + this.share = const .new(), }); AppConfig copyWith({ @@ -57,6 +60,7 @@ class AppConfig { AlbumConfig? album, BackupConfig? backup, NetworkConfig? network, + ShareConfig? share, }) => .new( logLevel: logLevel ?? this.logLevel, theme: theme ?? this.theme, @@ -69,6 +73,7 @@ class AppConfig { album: album ?? this.album, backup: backup ?? this.backup, network: network ?? this.network, + share: share ?? this.share, ); @override @@ -85,15 +90,16 @@ class AppConfig { other.slideshow == slideshow && other.album == album && other.backup == backup && - other.network == network); + other.network == network && + other.share == share); @override int get hashCode => - Object.hash(logLevel, theme, cleanup, map, timeline, image, viewer, slideshow, album, backup, network); + Object.hash(logLevel, theme, cleanup, map, timeline, image, viewer, slideshow, album, backup, network, share); @override String toString() => - 'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network)'; + 'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network, share: $share)'; T read(SettingsKey key) => (switch (key) { @@ -135,6 +141,7 @@ class AppConfig { .cleanupKeepAlbumIds => cleanup.keepAlbumIds, .cleanupCutoffDaysAgo => cleanup.cutoffDaysAgo, .cleanupDefaultsInitialized => cleanup.defaultsInitialized, + .shareFileType => share.fileType, .slideshowTransition => slideshow.transition, .slideshowRepeat => slideshow.repeat, .slideshowDuration => slideshow.duration, @@ -186,6 +193,7 @@ class AppConfig { .cleanupKeepAlbumIds => copyWith(cleanup: cleanup.copyWith(keepAlbumIds: value as List)), .cleanupCutoffDaysAgo => copyWith(cleanup: cleanup.copyWith(cutoffDaysAgo: value as int)), .cleanupDefaultsInitialized => copyWith(cleanup: cleanup.copyWith(defaultsInitialized: value as bool)), + .shareFileType => copyWith(share: share.copyWith(fileType: value as ShareAssetType)), .slideshowTransition => copyWith(slideshow: slideshow.copyWith(transition: value as bool)), .slideshowRepeat => copyWith(slideshow: slideshow.copyWith(repeat: value as bool)), .slideshowDuration => copyWith(slideshow: slideshow.copyWith(duration: value as int)), diff --git a/mobile/lib/domain/models/config/share_config.dart b/mobile/lib/domain/models/config/share_config.dart new file mode 100644 index 0000000000..898867ff78 --- /dev/null +++ b/mobile/lib/domain/models/config/share_config.dart @@ -0,0 +1,18 @@ +import 'package:immich_mobile/constants/enums.dart'; + +class ShareConfig { + final ShareAssetType fileType; + + const ShareConfig({this.fileType = ShareAssetType.original}); + + ShareConfig copyWith({ShareAssetType? fileType}) => ShareConfig(fileType: fileType ?? this.fileType); + + @override + bool operator ==(Object other) => identical(this, other) || (other is ShareConfig && other.fileType == fileType); + + @override + int get hashCode => fileType.hashCode; + + @override + String toString() => 'ShareConfig(fileType: $fileType)'; +} diff --git a/mobile/lib/domain/models/settings_key.dart b/mobile/lib/domain/models/settings_key.dart index 5277a37a60..497114a464 100644 --- a/mobile/lib/domain/models/settings_key.dart +++ b/mobile/lib/domain/models/settings_key.dart @@ -66,6 +66,9 @@ enum SettingsKey { cleanupCutoffDaysAgo(), cleanupDefaultsInitialized(), + // Share + shareFileType(codec: _EnumCodec(ShareAssetType.values)), + // Slideshow slideshowTransition(), slideshowRepeat(), diff --git a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart index 5ed61c3bbe..d55f5e25a3 100644 --- a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart @@ -42,6 +42,7 @@ class BaseActionButton extends ConsumerWidget { return IconButton( onPressed: onPressed, + onLongPress: onLongPressed, icon: Icon(iconData, size: iconSize, color: iconColor), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart index 7bc5dacb16..1217638452 100644 --- a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart @@ -6,10 +6,12 @@ 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/settings_key.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_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/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'; @@ -48,6 +50,34 @@ class _SharePreparingDialog extends StatelessWidget { } } +class _ShareFileTypeDialog extends StatelessWidget { + const _ShareFileTypeDialog(); + + @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), + ), + 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; @@ -60,6 +90,35 @@ class ShareActionButton extends ConsumerWidget { return; } + final fileType = ref.read(appConfigProvider).share.fileType; + await _share(context, ref, fileType); + } + + void _onLongPress(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final fileType = await showDialog( + context: context, + builder: (_) => const _ShareFileTypeDialog(), + useRootNavigator: false, + ); + + if (fileType == null || !context.mounted) { + return; + } + + await ref.read(settingsProvider).write(SettingsKey.shareFileType, fileType); + + if (!context.mounted) { + return; + } + + await _share(context, ref, fileType); + } + + Future _share(BuildContext context, WidgetRef ref, ShareAssetType fileType) async { final cancelCompleter = Completer(); final progress = ValueNotifier(null); final preparingDialog = _SharePreparingDialog(progress: progress); @@ -71,6 +130,7 @@ class ShareActionButton extends ConsumerWidget { .shareAssets( source, context, + fileType: fileType, cancelCompleter: cancelCompleter, onAssetDownloadProgress: (value) => progress.value = value, ) @@ -84,7 +144,7 @@ class ShareActionButton extends ConsumerWidget { if (!result.success) { ImmichToast.show( context: context, - msg: 'scaffold_body_error_occurred'.t(context: context), + msg: context.t.scaffold_body_error_occurred, gravity: ToastGravity.BOTTOM, toastType: ToastType.error, ); @@ -110,10 +170,11 @@ class ShareActionButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return BaseActionButton( iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, - label: 'share'.t(context: context), + label: context.t.share, iconOnly: iconOnly, menuItem: menuItem, onPressed: () => _onTap(context, ref), + onLongPressed: () => _onLongPress(context, ref), ); } } diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 426c028822..95c665560e 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -514,19 +514,21 @@ class ActionNotifier extends Notifier { Future shareAssets( ActionSource source, BuildContext context, { + ShareAssetType fileType = ShareAssetType.original, Completer? cancelCompleter, void Function(double progress)? onAssetDownloadProgress, }) async { final ids = _getAssets(source).toList(growable: false); try { - await _service.shareAssets( + final count = await _service.shareAssets( ids, context, + fileType: fileType, cancelCompleter: cancelCompleter, onAssetDownloadProgress: onAssetDownloadProgress, ); - return ActionResult(count: ids.length, success: true); + 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()); diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index e86a372768..de544e42bb 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -6,24 +6,34 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.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/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; +import 'package:path/path.dart' as p; import 'package:photo_manager/photo_manager.dart'; import 'package:share_plus/share_plus.dart'; -final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(nativeSyncApiProvider))); +typedef _ShareFile = ({File file, bool cleanup, String displayName}); + +final assetMediaRepositoryProvider = Provider( + (ref) => AssetMediaRepository(ref.watch(nativeSyncApiProvider), ref.watch(storageRepositoryProvider)), +); class AssetMediaRepository { final NativeSyncApi _nativeSyncApi; + final StorageRepository _storageRepository; static final Logger _log = Logger("AssetMediaRepository"); - const AssetMediaRepository(this._nativeSyncApi); + const AssetMediaRepository(this._nativeSyncApi, this._storageRepository); Future _androidSupportsTrash() async { if (Platform.isAndroid) { @@ -105,9 +115,149 @@ class AssetMediaRepository { ); } + String _sanitizeFilename(String filename) { + return filename.replaceAll(RegExp(r'[\\/]'), '_'); + } + + String _getPreviewFilename(BaseAsset asset) { + final sanitizedFilename = _sanitizeFilename(asset.name); + final baseName = p.basenameWithoutExtension(sanitizedFilename); + final fallbackName = asset.remoteId ?? asset.localId ?? 'asset'; + return '${baseName.isEmpty ? fallbackName : baseName}-preview.jpg'; + } + + bool _isCancelled(Completer? cancelCompleter) => cancelCompleter?.isCompleted ?? false; + + Future<_ShareFile?> _getLocalOriginalShareFile(BaseAsset asset, String localId) async { + final file = await _storageRepository.getFileForAsset(localId); + if (file == null) { + _log.warning("Local original file not found for sharing: $asset"); + return null; + } + + return (file: file, cleanup: CurrentPlatform.isIOS, displayName: _sanitizeFilename(asset.name)); + } + + Future<_ShareFile?> _downloadRemoteShareFile({ + required String taskId, + required String url, + required String displayName, + Completer? cancelCompleter, + required void Function(double progress) onProgress, + }) async { + final task = DownloadTask( + taskId: taskId, + url: url, + headers: ApiService.getRequestHeaders(), + filename: '$taskId-$displayName', + baseDirectory: BaseDirectory.temporary, + group: kShareDownloadGroup, + updates: Updates.statusAndProgress, + ); + final downloader = FileDownloader(); + final statusUpdate = await downloader.download( + task, + onProgress: (value) { + if (_isCancelled(cancelCompleter)) { + unawaited(downloader.cancelTaskWithId(taskId)); + return; + } + onProgress(value); + }, + ); + + if (_isCancelled(cancelCompleter)) { + return null; + } + + if (statusUpdate.status == TaskStatus.complete) { + return (file: File(await task.filePath()), cleanup: true, displayName: displayName); + } + + _log.severe("Download for $displayName failed with status ${statusUpdate.status}", statusUpdate.exception); + return null; + } + + Future<_ShareFile?> _getRemoteOriginalShareFile( + BaseAsset asset, + String remoteId, { + Completer? cancelCompleter, + required void Function(double progress) onProgress, + }) { + return _downloadRemoteShareFile( + taskId: 'share-original-$remoteId-${DateTime.now().microsecondsSinceEpoch}', + url: getOriginalUrlForRemoteId(remoteId, edited: asset.isEdited), + displayName: _sanitizeFilename(asset.name), + cancelCompleter: cancelCompleter, + onProgress: onProgress, + ); + } + + Future<_ShareFile?> _getRemotePreviewShareFile( + BaseAsset asset, + String remoteId, { + Completer? cancelCompleter, + required void Function(double progress) onProgress, + }) { + return _downloadRemoteShareFile( + taskId: 'share-preview-$remoteId-${DateTime.now().microsecondsSinceEpoch}', + url: getThumbnailUrlForRemoteId(remoteId, type: AssetMediaSize.preview, edited: asset.isEdited), + displayName: _getPreviewFilename(asset), + cancelCompleter: cancelCompleter, + onProgress: onProgress, + ); + } + + Future<_ShareFile?> _getOriginalShareFile( + BaseAsset asset, { + Completer? cancelCompleter, + required void Function(double progress) onProgress, + }) { + final localId = asset.localId; + if (localId != null && !asset.isEdited) { + return _getLocalOriginalShareFile(asset, localId); + } + + final remoteId = asset.remoteId; + if (remoteId == null) { + _log.warning("Asset has no remote ID for sharing: $asset"); + return Future.value(null); + } + + return _getRemoteOriginalShareFile(asset, remoteId, cancelCompleter: cancelCompleter, onProgress: onProgress); + } + + Future<_ShareFile?> _getPreviewShareFile( + BaseAsset asset, { + Completer? cancelCompleter, + required void Function(double progress) onProgress, + }) async { + final remoteId = asset.remoteId; + if (remoteId != null) { + final remotePreview = await _getRemotePreviewShareFile( + asset, + remoteId, + cancelCompleter: cancelCompleter, + onProgress: onProgress, + ); + if (remotePreview != null || asset.isEdited) { + return remotePreview; + } + } + + final localId = asset.localId; + if (localId != null) { + return _getLocalOriginalShareFile(asset, localId); + } + + _log.warning("Asset has no local or remote ID for preview sharing: $asset"); + return null; + } + Future shareAssets( List assets, BuildContext context, { + ShareAssetType fileType = ShareAssetType.original, Completer? cancelCompleter, void Function(double progress)? onAssetDownloadProgress, }) async { @@ -129,75 +279,42 @@ class AssetMediaRepository { updateProgress(); - for (var asset in assets) { - if (cancelCompleter != null && cancelCompleter.isCompleted) { - // if cancelled, delete any temp files created so far + for (final asset in assets) { + if (_isCancelled(cancelCompleter)) { await _cleanupTempFiles(tempFiles); return 0; } - final localId = (asset is LocalAsset) - ? asset.id - : asset is RemoteAsset - ? asset.localId - : null; - if (localId != null && !asset.isEdited) { - File? f = await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0).originFile; - downloadedXFiles.add(XFile(f!.path)); - processedAssets++; - updateProgress(); - if (CurrentPlatform.isIOS) { - tempFiles.add(f); - } - } else { - final remoteId = (asset is RemoteAsset) ? asset.id : asset.remoteId; - if (remoteId == null) { - _log.warning("Asset has no remote ID for sharing: $asset"); - processedAssets++; - updateProgress(); - continue; - } + final shareFile = switch (fileType) { + ShareAssetType.original => await _getOriginalShareFile( + asset, + cancelCompleter: cancelCompleter, + onProgress: updateProgress, + ), + ShareAssetType.preview => await _getPreviewShareFile( + asset, + cancelCompleter: cancelCompleter, + onProgress: updateProgress, + ), + }; - final taskId = 'share-$remoteId-${DateTime.now().microsecondsSinceEpoch}'; - final sanitizedFilename = asset.name.replaceAll(RegExp(r'[\\/]'), '_'); - final task = DownloadTask( - taskId: taskId, - url: getOriginalUrlForRemoteId(remoteId, edited: asset.isEdited), - headers: ApiService.getRequestHeaders(), - filename: sanitizedFilename, - baseDirectory: BaseDirectory.temporary, - group: kShareDownloadGroup, - updates: Updates.statusAndProgress, - ); - final statusUpdate = await FileDownloader().download( - task, - onProgress: (value) { - if (cancelCompleter != null && cancelCompleter.isCompleted) { - unawaited(FileDownloader().cancelTaskWithId(taskId)); - return; - } - updateProgress(value); - }, - ); - - if (cancelCompleter != null && cancelCompleter.isCompleted) { - await _cleanupTempFiles(tempFiles); - return 0; - } - - if (statusUpdate.status == TaskStatus.complete) { - final filePath = await task.filePath(); - final file = File(filePath); - tempFiles.add(file); - downloadedXFiles.add(XFile(filePath)); - processedAssets++; - updateProgress(); - continue; - } - _log.severe("Download for ${asset.name} failed with status ${statusUpdate.status}", statusUpdate.exception); - processedAssets++; - updateProgress(); + if (_isCancelled(cancelCompleter)) { + await _cleanupTempFiles(tempFiles); + return 0; } + + if (shareFile == null) { + processedAssets++; + updateProgress(); + continue; + } + + downloadedXFiles.add(XFile(shareFile.file.path, name: shareFile.displayName)); + if (shareFile.cleanup) { + tempFiles.add(shareFile.file); + } + processedAssets++; + updateProgress(); } if (downloadedXFiles.isEmpty) { @@ -205,7 +322,7 @@ class AssetMediaRepository { return 0; } - if (cancelCompleter != null && cancelCompleter.isCompleted) { + if (_isCancelled(cancelCompleter)) { await _cleanupTempFiles(tempFiles); return 0; } diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 88f33395f2..d77ec44492 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -272,12 +272,14 @@ class ActionService { Future shareAssets( List assets, BuildContext context, { + ShareAssetType fileType = ShareAssetType.original, Completer? cancelCompleter, void Function(double progress)? onAssetDownloadProgress, }) { return _assetMediaRepository.shareAssets( assets, context, + fileType: fileType, cancelCompleter: cancelCompleter, onAssetDownloadProgress: onAssetDownloadProgress, ); diff --git a/mobile/lib/widgets/settings/preference_settings/preference_setting.dart b/mobile/lib/widgets/settings/preference_settings/preference_setting.dart index 144fbf9758..afb3b478f0 100644 --- a/mobile/lib/widgets/settings/preference_settings/preference_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/preference_setting.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/haptic_setting.dart'; +import 'package:immich_mobile/widgets/settings/preference_settings/share_setting.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/theme_setting.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; @@ -8,7 +9,7 @@ class PreferenceSetting extends StatelessWidget { @override Widget build(BuildContext context) { - const preferenceSettings = [ThemeSetting(), HapticSetting()]; + const preferenceSettings = [ThemeSetting(), HapticSetting(), ShareSetting()]; return const SettingsSubPageScaffold(settings: preferenceSettings, showDivider: true); } diff --git a/mobile/lib/widgets/settings/preference_settings/share_setting.dart b/mobile/lib/widgets/settings/preference_settings/share_setting.dart new file mode 100644 index 0000000000..2435810566 --- /dev/null +++ b/mobile/lib/widgets/settings/preference_settings/share_setting.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; +import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; + +class ShareSetting extends HookConsumerWidget { + const ShareSetting({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final fileType = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.share.fileType))); + + void onChanged(ShareAssetType? value) { + if (value != null) { + fileType.value = value; + ref.read(settingsProvider).write(SettingsKey.shareFileType, value); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingGroupTitle(title: context.t.default_share_quality, icon: Icons.ios_share_outlined), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + context.t.default_quality_subtitle, + style: context.textTheme.bodyMedium!.copyWith(color: context.textTheme.bodyMedium!.color!.withAlpha(215)), + ), + ), + SettingsRadioListTile( + groups: [ + SettingsRadioGroup(title: context.t.share_original, value: ShareAssetType.original), + SettingsRadioGroup(title: context.t.share_preview, value: ShareAssetType.preview), + ], + groupBy: fileType.value, + onRadioChanged: onChanged, + ), + ], + ); + } +}