diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 3f2c0519b8..1a6ca76757 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -511,5 +511,7 @@ "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" -} \ No newline at end of file + "viewer_unstack": "Un-Stack", + "haptic_feedback_title": "Haptic Feedback", + "haptic_feedback_switch": "Enable haptic feedback" +} diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 059c0c976d..33de70d757 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -20,6 +20,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/gallery_app_bar.dart'; import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_thumbnail.dart'; import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart'; @@ -303,7 +304,9 @@ class GalleryViewerPage extends HookConsumerWidget { scrollDirection: Axis.horizontal, onPageChanged: (value) async { final next = currentIndex.value < value ? value + 1 : value - 1; - HapticFeedback.selectionClick(); + + ref.read(hapticFeedbackProvider.notifier).selectionClick(); + currentIndex.value = value; stackIndex.value = -1; isPlayingVideo.value = false; diff --git a/mobile/lib/modules/backup/ui/album_info_card.dart b/mobile/lib/modules/backup/ui/album_info_card.dart index 5380360ff1..a274f1c5e8 100644 --- a/mobile/lib/modules/backup/ui/album_info_card.dart +++ b/mobile/lib/modules/backup/ui/album_info_card.dart @@ -1,13 +1,13 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; class AlbumInfoCard extends HookConsumerWidget { @@ -21,6 +21,7 @@ class AlbumInfoCard extends HookConsumerWidget { ref.watch(backupProvider).selectedBackupAlbums.contains(album); final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); + final isDarkTheme = context.isDarkTheme; ColorFilter selectedFilter = ColorFilter.mode( @@ -78,7 +79,7 @@ class AlbumInfoCard extends HookConsumerWidget { return GestureDetector( onTap: () { - HapticFeedback.selectionClick(); + ref.read(hapticFeedbackProvider.notifier).selectionClick(); if (isSelected) { ref.read(backupProvider.notifier).removeAlbumForBackup(album); @@ -87,7 +88,7 @@ class AlbumInfoCard extends HookConsumerWidget { } }, onDoubleTap: () { - HapticFeedback.selectionClick(); + ref.read(hapticFeedbackProvider.notifier).selectionClick(); if (isExcluded) { // Remove from exclude album list diff --git a/mobile/lib/modules/backup/ui/album_info_list_tile.dart b/mobile/lib/modules/backup/ui/album_info_list_tile.dart index dcf0923a11..40fdfa8897 100644 --- a/mobile/lib/modules/backup/ui/album_info_list_tile.dart +++ b/mobile/lib/modules/backup/ui/album_info_list_tile.dart @@ -1,6 +1,5 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -8,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; class AlbumInfoListTile extends HookConsumerWidget { @@ -68,7 +68,7 @@ class AlbumInfoListTile extends HookConsumerWidget { return GestureDetector( onDoubleTap: () { - HapticFeedback.selectionClick(); + ref.watch(hapticFeedbackProvider.notifier).selectionClick(); if (isExcluded) { // Remove from exclude album list @@ -93,7 +93,7 @@ class AlbumInfoListTile extends HookConsumerWidget { tileColor: buildTileColor(), contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), onTap: () { - HapticFeedback.selectionClick(); + ref.read(hapticFeedbackProvider.notifier).selectionClick(); if (isSelected) { ref.read(backupProvider.notifier).removeAlbumForBackup(album); } else { diff --git a/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart b/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart index 0c06fc1a1b..03236e3930 100644 --- a/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart +++ b/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart'; class GroupDividerTitle extends HookConsumerWidget { const GroupDividerTitle({ @@ -38,7 +38,7 @@ class GroupDividerTitle extends HookConsumerWidget { ); void handleTitleIconClick() { - HapticFeedback.heavyImpact(); + ref.read(hapticFeedbackProvider.notifier).heavyImpact(); if (selected) { onDeselect(); } else { diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart index 4c520fe6fc..5ece42d5cf 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart @@ -6,7 +6,7 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart'; @@ -15,6 +15,7 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart'; import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'asset_grid_data_structure.dart'; @@ -27,7 +28,7 @@ typedef ImmichAssetGridSelectionListener = void Function( Set, ); -class ImmichAssetGridView extends StatefulWidget { +class ImmichAssetGridView extends ConsumerStatefulWidget { final RenderList renderList; final int assetsPerRow; final double margin; @@ -69,12 +70,12 @@ class ImmichAssetGridView extends StatefulWidget { }); @override - State createState() { + createState() { return ImmichAssetGridViewState(); } } -class ImmichAssetGridViewState extends State { +class ImmichAssetGridViewState extends ConsumerState { final ItemScrollController _itemScrollController = ItemScrollController(); final ScrollOffsetController _scrollOffsetController = ScrollOffsetController(); @@ -314,7 +315,7 @@ class ImmichAssetGridViewState extends State { final now = Timeline.now; if (now > (_hapticFeedbackTS + feedbackInterval)) { _hapticFeedbackTS = now; - HapticFeedback.mediumImpact(); + ref.read(hapticFeedbackProvider.notifier).mediumImpact(); } } } diff --git a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart index a194bc2ade..f06be0289b 100644 --- a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart @@ -1,14 +1,15 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/shared/ui/immich_thumbnail.dart'; import 'package:immich_mobile/utils/storage_indicator.dart'; import 'package:isar/isar.dart'; -class ThumbnailImage extends StatelessWidget { +class ThumbnailImage extends ConsumerWidget { final Asset asset; final int index; final Asset Function(int index) loadAsset; @@ -37,7 +38,7 @@ class ThumbnailImage extends StatelessWidget { }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final assetContainerColor = context.isDarkTheme ? Colors.blueGrey : context.themeData.primaryColorLight; @@ -186,7 +187,7 @@ class ThumbnailImage extends StatelessWidget { }, onLongPress: () { onSelect?.call(); - HapticFeedback.heavyImpact(); + ref.read(hapticFeedbackProvider.notifier).heavyImpact(); }, child: Stack( children: [ diff --git a/mobile/lib/modules/login/providers/authentication.provider.dart b/mobile/lib/modules/login/providers/authentication.provider.dart index e010024332..03d06bd140 100644 --- a/mobile/lib/modules/login/providers/authentication.provider.dart +++ b/mobile/lib/modules/login/providers/authentication.provider.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_udid/flutter_udid.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart'; @@ -92,7 +91,6 @@ class AuthenticationNotifier extends StateNotifier { serverUrl: serverUrl, ); } catch (e) { - HapticFeedback.vibrate(); debugPrint("Error logging in $e"); return false; } diff --git a/mobile/lib/modules/memories/ui/memory_lane.dart b/mobile/lib/modules/memories/ui/memory_lane.dart index eb72c15e8e..d48785a78b 100644 --- a/mobile/lib/modules/memories/ui/memory_lane.dart +++ b/mobile/lib/modules/memories/ui/memory_lane.dart @@ -1,10 +1,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/modules/memories/providers/memory.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; class MemoryLane extends HookConsumerWidget { @@ -33,7 +33,9 @@ class MemoryLane extends HookConsumerWidget { return GestureDetector( onTap: () { - HapticFeedback.heavyImpact(); + ref + .read(hapticFeedbackProvider.notifier) + .heavyImpact(); context.pushRoute( MemoryRoute( memories: memories, diff --git a/mobile/lib/modules/memories/views/memory_page.dart b/mobile/lib/modules/memories/views/memory_page.dart index aa968303b3..9a7032f828 100644 --- a/mobile/lib/modules/memories/views/memory_page.dart +++ b/mobile/lib/modules/memories/views/memory_page.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/modules/memories/ui/memory_card.dart'; import 'package:immich_mobile/modules/memories/ui/memory_epilogue.dart'; import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart'; import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; @RoutePage() @@ -127,7 +128,7 @@ class MemoryPage extends HookConsumerWidget { } Future onAssetChanged(int otherIndex) async { - HapticFeedback.selectionClick(); + ref.read(hapticFeedbackProvider.notifier).selectionClick(); currentAssetPage.value = otherIndex; updateProgressText(); // Wait for page change animation to finish @@ -169,7 +170,7 @@ class MemoryPage extends HookConsumerWidget { scrollDirection: Axis.vertical, controller: memoryPageController, onPageChanged: (pageNumber) { - HapticFeedback.mediumImpact(); + ref.read(hapticFeedbackProvider.notifier).mediumImpact(); if (pageNumber < memories.length) { currentMemoryIndex.value = pageNumber; currentMemory.value = memories[pageNumber]; diff --git a/mobile/lib/modules/settings/services/app_settings.service.dart b/mobile/lib/modules/settings/services/app_settings.service.dart index 98e8464425..b7be3ca5e3 100644 --- a/mobile/lib/modules/settings/services/app_settings.service.dart +++ b/mobile/lib/modules/settings/services/app_settings.service.dart @@ -58,6 +58,7 @@ enum AppSettingsEnum { null, false, ), + enableHapticFeedback(StoreKey.enableHapticFeedback, null, true), ; const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/modules/settings/ui/preference_settings/haptic_setting.dart b/mobile/lib/modules/settings/ui/preference_settings/haptic_setting.dart new file mode 100644 index 0000000000..290dd5aafa --- /dev/null +++ b/mobile/lib/modules/settings/ui/preference_settings/haptic_setting.dart @@ -0,0 +1,38 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_sub_title.dart'; +import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; +import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart'; + +class HapticSetting extends HookConsumerWidget { + const HapticSetting({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final hapticFeedbackSetting = + useAppSettingsState(AppSettingsEnum.enableHapticFeedback); + final isHapticFeedbackEnabled = + useValueNotifier(hapticFeedbackSetting.value); + + onHapticFeedbackChange(bool isEnabled) { + hapticFeedbackSetting.value = isEnabled; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsSubTitle(title: "haptic_feedback_title".tr()), + SettingsSwitchListTile( + valueNotifier: isHapticFeedbackEnabled, + title: 'haptic_feedback_switch'.tr(), + onChanged: onHapticFeedbackChange, + ), + ], + ); + } +} diff --git a/mobile/lib/modules/settings/ui/preference_settings/preference_setting.dart b/mobile/lib/modules/settings/ui/preference_settings/preference_setting.dart index f75891437c..ccc0e5b161 100644 --- a/mobile/lib/modules/settings/ui/preference_settings/preference_setting.dart +++ b/mobile/lib/modules/settings/ui/preference_settings/preference_setting.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/modules/settings/ui/preference_settings/haptic_setting.dart'; import 'package:immich_mobile/modules/settings/ui/preference_settings/theme_setting.dart'; import 'package:immich_mobile/modules/settings/ui/settings_sub_page_scaffold.dart'; @@ -11,6 +12,7 @@ class PreferenceSetting extends StatelessWidget { Widget build(BuildContext context) { const preferenceSettings = [ ThemeSetting(), + HapticSetting(), ]; return const SettingsSubPageScaffold(settings: preferenceSettings); diff --git a/mobile/lib/shared/models/store.dart b/mobile/lib/shared/models/store.dart index b1f28ec0f0..233f6231af 100644 --- a/mobile/lib/shared/models/store.dart +++ b/mobile/lib/shared/models/store.dart @@ -191,6 +191,7 @@ enum StoreKey { selectedAlbumSortReverse(123, type: bool), mapThemeMode(124, type: int), mapwithPartners(125, type: bool), + enableHapticFeedback(126, type: bool), ; const StoreKey( diff --git a/mobile/lib/shared/providers/haptic_feedback.provider.dart b/mobile/lib/shared/providers/haptic_feedback.provider.dart new file mode 100644 index 0000000000..47373a67e9 --- /dev/null +++ b/mobile/lib/shared/providers/haptic_feedback.provider.dart @@ -0,0 +1,56 @@ +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; + +final hapticFeedbackProvider = + StateNotifierProvider((ref) { + return HapticNotifier(ref); +}); + +class HapticNotifier extends StateNotifier { + void build() {} + final Ref _ref; + + HapticNotifier(this._ref) : super(null); + + selectionClick() { + if (_ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.enableHapticFeedback)) { + HapticFeedback.selectionClick(); + } + } + + lightImpact() { + if (_ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.enableHapticFeedback)) { + HapticFeedback.lightImpact(); + } + } + + mediumImpact() { + if (_ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.enableHapticFeedback)) { + HapticFeedback.mediumImpact(); + } + } + + heavyImpact() { + if (_ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.enableHapticFeedback)) { + HapticFeedback.heavyImpact(); + } + } + + vibrate() { + if (_ref + .read(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.enableHapticFeedback)) { + HapticFeedback.vibrate(); + } + } +} diff --git a/mobile/lib/shared/views/tab_controller_page.dart b/mobile/lib/shared/views/tab_controller_page.dart index 40de493d0b..e1f6fde1ad 100644 --- a/mobile/lib/shared/views/tab_controller_page.dart +++ b/mobile/lib/shared/views/tab_controller_page.dart @@ -1,13 +1,13 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart'; import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; +import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/shared/providers/tab.provider.dart'; @RoutePage() @@ -53,7 +53,7 @@ class TabControllerPage extends HookConsumerWidget { scrollToTopNotifierProvider.scrollToTop(); } - HapticFeedback.selectionClick(); + ref.read(hapticFeedbackProvider.notifier).selectionClick(); tabsRouter.setActiveIndex(index); ref.read(tabProvider.notifier).state = TabEnum.values[index]; }, @@ -107,7 +107,7 @@ class TabControllerPage extends HookConsumerWidget { scrollToTopNotifierProvider.scrollToTop(); } - HapticFeedback.selectionClick(); + ref.read(hapticFeedbackProvider.notifier).selectionClick(); tabsRouter.setActiveIndex(index); ref.read(tabProvider.notifier).state = TabEnum.values[index]; },