feat(mobile): haptic feedback setting (#8723)

* feat(mobile): haptic feedback testing

* linting
This commit is contained in:
Alex 2024-04-15 07:50:47 +02:00 committed by GitHub
parent 85df3f1e99
commit 3c7f70ec30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 136 additions and 29 deletions

View File

@ -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"
}
"viewer_unstack": "Un-Stack",
"haptic_feedback_title": "Haptic Feedback",
"haptic_feedback_switch": "Enable haptic feedback"
}

View File

@ -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;

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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<Asset>,
);
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<StatefulWidget> createState() {
createState() {
return ImmichAssetGridViewState();
}
}
class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
final ItemScrollController _itemScrollController = ItemScrollController();
final ScrollOffsetController _scrollOffsetController =
ScrollOffsetController();
@ -314,7 +315,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
final now = Timeline.now;
if (now > (_hapticFeedbackTS + feedbackInterval)) {
_hapticFeedbackTS = now;
HapticFeedback.mediumImpact();
ref.read(hapticFeedbackProvider.notifier).mediumImpact();
}
}
}

View File

@ -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: [

View File

@ -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<AuthenticationState> {
serverUrl: serverUrl,
);
} catch (e) {
HapticFeedback.vibrate();
debugPrint("Error logging in $e");
return false;
}

View File

@ -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,

View File

@ -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<void> 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];

View File

@ -58,6 +58,7 @@ enum AppSettingsEnum<T> {
null,
false,
),
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
;
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);

View File

@ -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,
),
],
);
}
}

View File

@ -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);

View File

@ -191,6 +191,7 @@ enum StoreKey<T> {
selectedAlbumSortReverse<bool>(123, type: bool),
mapThemeMode<int>(124, type: int),
mapwithPartners<bool>(125, type: bool),
enableHapticFeedback<bool>(126, type: bool),
;
const StoreKey(

View File

@ -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<HapticNotifier, void>((ref) {
return HapticNotifier(ref);
});
class HapticNotifier extends StateNotifier<void> {
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();
}
}
}

View File

@ -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];
},