From a297ac48435fecf8f208afb27486e7cf4eef1c00 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Sun, 5 Jan 2025 23:08:44 +0900 Subject: [PATCH 1/2] chore: update useDefaultPlaySettings.ts intial -> initial --- hooks/useDefaultPlaySettings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/useDefaultPlaySettings.ts b/hooks/useDefaultPlaySettings.ts index 5c2d9cc6..39009305 100644 --- a/hooks/useDefaultPlaySettings.ts +++ b/hooks/useDefaultPlaySettings.ts @@ -6,7 +6,7 @@ import { } from "@jellyfin/sdk/lib/generated-client"; import { useMemo } from "react"; -// Used only for intial play settings. +// Used only for initial play settings. const useDefaultPlaySettings = ( item: BaseItemDto, settings: Settings | null From 9692c173ae05ea3671f83b237d8f753b1c22081f Mon Sep 17 00:00:00 2001 From: Anubhav Saha Date: Sun, 5 Jan 2025 15:22:52 +0100 Subject: [PATCH 2/2] feat: haptic feedback settings and custom hook --- app/(auth)/(tabs)/(home)/settings.tsx | 5 +- app/(auth)/player/direct-player.tsx | 8 +-- app/(auth)/player/music-player.tsx | 6 ++- app/(auth)/player/transcoding-player.tsx | 7 +-- components/Button.tsx | 6 ++- components/PlayButton.tsx | 5 +- components/RoundButton.tsx | 5 +- components/downloads/EpisodeCard.tsx | 5 +- components/downloads/MovieCard.tsx | 5 +- components/home/LargeMovieCarousel.tsx | 5 +- components/settings/OtherSettings.tsx | 9 ++++ components/settings/QuickConnect.tsx | 10 ++-- components/settings/StorageSettings.tsx | 8 +-- components/video-player/controls/Controls.tsx | 18 ++++--- hooks/useCreditSkipper.ts | 5 +- hooks/useHaptic.ts | 54 +++++++++++++++++++ hooks/useIntroSkipper.ts | 5 +- hooks/useMarkAsPlayed.ts | 5 +- providers/DownloadProvider.tsx | 8 +-- utils/atoms/settings.ts | 2 + 20 files changed, 134 insertions(+), 47 deletions(-) create mode 100644 hooks/useHaptic.ts diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index 8f6d102a..559611a8 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -13,7 +13,7 @@ import { SubtitleToggles } from "@/components/settings/SubtitleToggles"; import { UserInfo } from "@/components/settings/UserInfo"; import { useJellyfin } from "@/providers/JellyfinProvider"; import { clearLogs } from "@/utils/log"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; import { useNavigation, useRouter } from "expo-router"; import { useEffect } from "react"; import { ScrollView, TouchableOpacity, View } from "react-native"; @@ -23,10 +23,11 @@ export default function settings() { const router = useRouter(); const insets = useSafeAreaInsets(); const { logout } = useJellyfin(); + const successHapticFeedback = useHaptic("success"); const onClearLogsClicked = async () => { clearLogs(); - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + successHapticFeedback(); }; const navigation = useNavigation(); diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 4d924938..44b0eb34 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -27,7 +27,7 @@ import { getUserLibraryApi, } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; import { useFocusEffect, useGlobalSearchParams } from "expo-router"; import { useAtomValue } from "jotai"; import React, { @@ -68,9 +68,11 @@ export default function page() { const { getDownloadedItem } = useDownload(); const revalidateProgressCache = useInvalidatePlaybackProgressCache(); + const lightHapticFeedback = useHaptic("light"); + const setShowControls = useCallback((show: boolean) => { _setShowControls(show); - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); }, []); const { @@ -175,7 +177,7 @@ export default function page() { const togglePlay = useCallback(async () => { if (!api) return; - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); if (isPlaying) { await videoRef.current?.pause(); diff --git a/app/(auth)/player/music-player.tsx b/app/(auth)/player/music-player.tsx index eca16b4c..fc4b8863 100644 --- a/app/(auth)/player/music-player.tsx +++ b/app/(auth)/player/music-player.tsx @@ -17,7 +17,7 @@ import { getUserLibraryApi, } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; import { Image } from "expo-image"; import { useFocusEffect, useLocalSearchParams } from "expo-router"; import { useAtomValue } from "jotai"; @@ -45,6 +45,8 @@ export default function page() { const isSeeking = useSharedValue(false); const cacheProgress = useSharedValue(0); + const lightHapticFeedback = useHaptic("light"); + const { itemId, audioIndex: audioIndexStr, @@ -124,7 +126,7 @@ export default function page() { const togglePlay = useCallback( async (ticks: number) => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); if (isPlaying) { videoRef.current?.pause(); await getPlaystateApi(api!).onPlaybackProgress({ diff --git a/app/(auth)/player/transcoding-player.tsx b/app/(auth)/player/transcoding-player.tsx index bcb9a6e4..971410f4 100644 --- a/app/(auth)/player/transcoding-player.tsx +++ b/app/(auth)/player/transcoding-player.tsx @@ -20,7 +20,7 @@ import { getUserLibraryApi, } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; import { useFocusEffect, useLocalSearchParams } from "expo-router"; import { useAtomValue } from "jotai"; import React, { @@ -48,6 +48,7 @@ const Player = () => { const firstTime = useRef(true); const revalidateProgressCache = useInvalidatePlaybackProgressCache(); + const lightHapticFeedback = useHaptic("light"); const [isPlaybackStopped, setIsPlaybackStopped] = useState(false); const [showControls, _setShowControls] = useState(true); @@ -58,7 +59,7 @@ const Player = () => { const setShowControls = useCallback((show: boolean) => { _setShowControls(show); - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); }, []); const progress = useSharedValue(0); @@ -167,7 +168,7 @@ const Player = () => { const videoSource = useVideoSource(item, api, poster, stream?.url); const togglePlay = useCallback(async () => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); if (isPlaying) { videoRef.current?.pause(); await getPlaystateApi(api!).onPlaybackProgress({ diff --git a/components/Button.tsx b/components/Button.tsx index 1a73ad01..2c41ad50 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -1,4 +1,4 @@ -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; import React, { PropsWithChildren, ReactNode, useMemo } from "react"; import { Text, TouchableOpacity, View } from "react-native"; import { Loader } from "./Loader"; @@ -43,6 +43,8 @@ export const Button: React.FC> = ({ } }, [color]); + const lightHapticFeedback = useHaptic("light"); + return ( > = ({ onPress={() => { if (!loading && !disabled && onPress) { onPress(); - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); } }} disabled={disabled || loading} diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index e5c5dd87..e432f2a8 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -32,7 +32,7 @@ import Animated, { import { Button } from "./Button"; import { SelectedOptions } from "./ItemContent"; import { chromecastProfile } from "@/utils/profiles/chromecast"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; interface Props extends React.ComponentProps { item: BaseItemDto; @@ -64,6 +64,7 @@ export const PlayButton: React.FC = ({ const widthProgress = useSharedValue(0); const colorChangeProgress = useSharedValue(0); const [settings] = useSettings(); + const lightHapticFeedback = useHaptic("light"); const goToPlayer = useCallback( (q: string, bitrateValue: number | undefined) => { @@ -79,7 +80,7 @@ export const PlayButton: React.FC = ({ const onPress = useCallback(async () => { if (!item) return; - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); const queryParams = new URLSearchParams({ itemId: item.Id!, diff --git a/components/RoundButton.tsx b/components/RoundButton.tsx index 049c5ed0..5d2faf73 100644 --- a/components/RoundButton.tsx +++ b/components/RoundButton.tsx @@ -6,7 +6,7 @@ import { TouchableOpacity, TouchableOpacityProps, } from "react-native"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; interface Props extends TouchableOpacityProps { onPress?: () => void; @@ -29,10 +29,11 @@ export const RoundButton: React.FC> = ({ }) => { const buttonSize = size === "large" ? "h-10 w-10" : "h-9 w-9"; const fillColorClass = fillColor === "primary" ? "bg-purple-600" : ""; + const lightHapticFeedback = useHaptic("light"); const handlePress = () => { if (hapticFeedback) { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); } onPress?.(); }; diff --git a/components/downloads/EpisodeCard.tsx b/components/downloads/EpisodeCard.tsx index e8387da5..53b3ecec 100644 --- a/components/downloads/EpisodeCard.tsx +++ b/components/downloads/EpisodeCard.tsx @@ -1,5 +1,5 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; import React, { useCallback, useMemo } from "react"; import { TouchableOpacity, TouchableOpacityProps, View } from "react-native"; import { @@ -26,6 +26,7 @@ export const EpisodeCard: React.FC = ({ item, ...props }) => { const { deleteFile } = useDownload(); const { openFile } = useDownloadedFileOpener(); const { showActionSheetWithOptions } = useActionSheet(); + const successHapticFeedback = useHaptic("success"); const base64Image = useMemo(() => { return storage.getString(item.Id!); @@ -41,7 +42,7 @@ export const EpisodeCard: React.FC = ({ item, ...props }) => { const handleDeleteFile = useCallback(() => { if (item.Id) { deleteFile(item.Id); - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + successHapticFeedback(); } }, [deleteFile, item.Id]); diff --git a/components/downloads/MovieCard.tsx b/components/downloads/MovieCard.tsx index 3073bd0a..bb61f3c8 100644 --- a/components/downloads/MovieCard.tsx +++ b/components/downloads/MovieCard.tsx @@ -3,7 +3,7 @@ import { useActionSheet, } from "@expo/react-native-action-sheet"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; import React, { useCallback, useMemo } from "react"; import { TouchableOpacity, View } from "react-native"; @@ -28,6 +28,7 @@ export const MovieCard: React.FC = ({ item }) => { const { deleteFile } = useDownload(); const { openFile } = useDownloadedFileOpener(); const { showActionSheetWithOptions } = useActionSheet(); + const successHapticFeedback = useHaptic("success"); const handleOpenFile = useCallback(() => { openFile(item); @@ -43,7 +44,7 @@ export const MovieCard: React.FC = ({ item }) => { const handleDeleteFile = useCallback(() => { if (item.Id) { deleteFile(item.Id); - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + successHapticFeedback(); } }, [deleteFile, item.Id]); diff --git a/components/home/LargeMovieCarousel.tsx b/components/home/LargeMovieCarousel.tsx index a22c586f..00767621 100644 --- a/components/home/LargeMovieCarousel.tsx +++ b/components/home/LargeMovieCarousel.tsx @@ -22,7 +22,7 @@ import { itemRouter, TouchableItemRouter } from "../common/TouchableItemRouter"; import { Loader } from "../Loader"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { useRouter, useSegments } from "expo-router"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; interface Props extends ViewProps {} @@ -128,6 +128,7 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => { const [api] = useAtom(apiAtom); const router = useRouter(); const screenWidth = Dimensions.get("screen").width; + const lightHapticFeedback = useHaptic("light"); const uri = useMemo(() => { if (!api) return null; @@ -153,7 +154,7 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => { const handleRoute = useCallback(() => { if (!from) return; const url = itemRouter(item, from); - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); // @ts-ignore if (url) router.push(url); }, [item, from]); diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index d280a167..66f73ef4 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -178,6 +178,15 @@ export const OtherSettings: React.FC = () => { } /> + + + + updateSettings({ disableHapticFeedback: value }) + } + /> + ); }; diff --git a/components/settings/QuickConnect.tsx b/components/settings/QuickConnect.tsx index 9efbec43..85a8259f 100644 --- a/components/settings/QuickConnect.tsx +++ b/components/settings/QuickConnect.tsx @@ -7,7 +7,7 @@ import { BottomSheetView, } from "@gorhom/bottom-sheet"; import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; import { useAtom } from "jotai"; import React, { useCallback, useRef, useState } from "react"; import { Alert, View, ViewProps } from "react-native"; @@ -23,6 +23,8 @@ export const QuickConnect: React.FC = ({ ...props }) => { const [user] = useAtom(userAtom); const [quickConnectCode, setQuickConnectCode] = useState(); const bottomSheetModalRef = useRef(null); + const successHapticFeedback = useHaptic("success"); + const errorHapticFeedback = useHaptic("error"); const renderBackdrop = useCallback( (props: BottomSheetBackdropProps) => ( @@ -43,16 +45,16 @@ export const QuickConnect: React.FC = ({ ...props }) => { userId: user?.Id, }); if (res.status === 200) { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + successHapticFeedback(); Alert.alert("Success", "Quick connect authorized"); setQuickConnectCode(undefined); bottomSheetModalRef?.current?.close(); } else { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + errorHapticFeedback(); Alert.alert("Error", "Invalid code"); } } catch (e) { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + errorHapticFeedback(); Alert.alert("Error", "Invalid code"); } } diff --git a/components/settings/StorageSettings.tsx b/components/settings/StorageSettings.tsx index 5b693acd..9064bc14 100644 --- a/components/settings/StorageSettings.tsx +++ b/components/settings/StorageSettings.tsx @@ -4,7 +4,7 @@ import { useDownload } from "@/providers/DownloadProvider"; import { clearLogs } from "@/utils/log"; import { useQuery } from "@tanstack/react-query"; import * as FileSystem from "expo-file-system"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; import { View } from "react-native"; import * as Progress from "react-native-progress"; import { toast } from "sonner-native"; @@ -13,6 +13,8 @@ import { ListItem } from "../list/ListItem"; export const StorageSettings = () => { const { deleteAllFiles, appSizeUsage } = useDownload(); + const successHapticFeedback = useHaptic("success"); + const errorHapticFeedback = useHaptic("error"); const { data: size, isLoading: appSizeLoading } = useQuery({ queryKey: ["appSize", appSizeUsage], @@ -29,9 +31,9 @@ export const StorageSettings = () => { const onDeleteClicked = async () => { try { await deleteAllFiles(); - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + successHapticFeedback(); } catch (e) { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + errorHapticFeedback(); toast.error("Error deleting files"); } }; diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 2fd1cba3..620e112e 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -29,7 +29,7 @@ import { BaseItemDto, MediaSourceInfo, } from "@jellyfin/sdk/lib/generated-client"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; import { Image } from "expo-image"; import { useLocalSearchParams, useRouter } from "expo-router"; import { useAtom } from "jotai"; @@ -157,10 +157,12 @@ export const Controls: React.FC = ({ isVlc ); + const lightHapticFeedback = useHaptic("light"); + const goToPreviousItem = useCallback(() => { if (!previousItem || !settings) return; - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); const previousIndexes: previousIndexes = { subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, @@ -198,7 +200,7 @@ export const Controls: React.FC = ({ const goToNextItem = useCallback(() => { if (!nextItem || !settings) return; - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); const previousIndexes: previousIndexes = { subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, @@ -326,7 +328,7 @@ export const Controls: React.FC = ({ const handleSkipBackward = useCallback(async () => { if (!settings?.rewindSkipTime) return; wasPlayingRef.current = isPlaying; - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); try { const curr = progress.value; if (curr !== undefined) { @@ -344,7 +346,7 @@ export const Controls: React.FC = ({ const handleSkipForward = useCallback(async () => { if (!settings?.forwardSkipTime) return; wasPlayingRef.current = isPlaying; - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); try { const curr = progress.value; if (curr !== undefined) { @@ -361,7 +363,7 @@ export const Controls: React.FC = ({ const toggleIgnoreSafeAreas = useCallback(() => { setIgnoreSafeAreas((prev) => !prev); - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); }, []); const memoizedRenderBubble = useCallback(() => { @@ -440,7 +442,7 @@ export const Controls: React.FC = ({ const gotoItem = await getItemById(api, itemId); if (!settings || !gotoItem) return; - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); const previousIndexes: previousIndexes = { subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined, @@ -584,7 +586,7 @@ export const Controls: React.FC = ({ )} { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); router.back(); }} className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" diff --git a/hooks/useCreditSkipper.ts b/hooks/useCreditSkipper.ts index 1430e7c9..14a77161 100644 --- a/hooks/useCreditSkipper.ts +++ b/hooks/useCreditSkipper.ts @@ -5,7 +5,7 @@ import { apiAtom } from "@/providers/JellyfinProvider"; import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; import { writeToLog } from "@/utils/log"; import { msToSeconds, secondsToMs } from "@/utils/time"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "./useHaptic"; interface CreditTimestamps { Introduction: { @@ -29,6 +29,7 @@ export const useCreditSkipper = ( ) => { const [api] = useAtom(apiAtom); const [showSkipCreditButton, setShowSkipCreditButton] = useState(false); + const lightHapticFeedback = useHaptic("light"); if (isVlc) { currentTime = msToSeconds(currentTime); @@ -79,7 +80,7 @@ export const useCreditSkipper = ( if (!creditTimestamps) return; console.log(`Skipping credits to ${creditTimestamps.Credits.End}`); try { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); wrappedSeek(creditTimestamps.Credits.End); setTimeout(() => { play(); diff --git a/hooks/useHaptic.ts b/hooks/useHaptic.ts new file mode 100644 index 00000000..c992def1 --- /dev/null +++ b/hooks/useHaptic.ts @@ -0,0 +1,54 @@ +import { useCallback, useMemo } from "react"; +import { Platform } from "react-native"; +import * as Haptics from "expo-haptics"; +import { useSettings } from "@/utils/atoms/settings"; + +export type HapticFeedbackType = + | "light" + | "medium" + | "heavy" + | "selection" + | "success" + | "warning" + | "error"; + +export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => { + const [settings] = useSettings(); + + const createHapticHandler = useCallback( + (type: Haptics.ImpactFeedbackStyle) => { + return Platform.OS === "web" ? () => {} : () => Haptics.impactAsync(type); + }, + [] + ); + const createNotificationFeedback = useCallback( + (type: Haptics.NotificationFeedbackType) => { + return Platform.OS === "web" + ? () => {} + : () => Haptics.notificationAsync(type); + }, + [] + ); + + const hapticHandlers = useMemo( + () => ({ + light: createHapticHandler(Haptics.ImpactFeedbackStyle.Light), + medium: createHapticHandler(Haptics.ImpactFeedbackStyle.Medium), + heavy: createHapticHandler(Haptics.ImpactFeedbackStyle.Heavy), + selection: Platform.OS === "web" ? () => {} : Haptics.selectionAsync, + success: createNotificationFeedback( + Haptics.NotificationFeedbackType.Success + ), + warning: createNotificationFeedback( + Haptics.NotificationFeedbackType.Warning + ), + error: createNotificationFeedback(Haptics.NotificationFeedbackType.Error), + }), + [createHapticHandler, createNotificationFeedback] + ); + + if (settings?.disableHapticFeedback) { + return () => {}; + } + return hapticHandlers[feedbackType]; +}; diff --git a/hooks/useIntroSkipper.ts b/hooks/useIntroSkipper.ts index 15aaff05..b41872dc 100644 --- a/hooks/useIntroSkipper.ts +++ b/hooks/useIntroSkipper.ts @@ -5,7 +5,7 @@ import { apiAtom } from "@/providers/JellyfinProvider"; import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; import { writeToLog } from "@/utils/log"; import { msToSeconds, secondsToMs } from "@/utils/time"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "./useHaptic"; interface IntroTimestamps { EpisodeId: string; @@ -33,6 +33,7 @@ export const useIntroSkipper = ( if (isVlc) { currentTime = msToSeconds(currentTime); } + const lightHapticFeedback = useHaptic("light"); const wrappedSeek = (seconds: number) => { if (isVlc) { @@ -78,7 +79,7 @@ export const useIntroSkipper = ( const skipIntro = useCallback(() => { if (!introTimestamps) return; try { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); wrappedSeek(introTimestamps.IntroEnd); setTimeout(() => { play(); diff --git a/hooks/useMarkAsPlayed.ts b/hooks/useMarkAsPlayed.ts index ff039cc8..fb30bd14 100644 --- a/hooks/useMarkAsPlayed.ts +++ b/hooks/useMarkAsPlayed.ts @@ -3,13 +3,14 @@ import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed"; import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useQueryClient } from "@tanstack/react-query"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "./useHaptic"; import { useAtom } from "jotai"; export const useMarkAsPlayed = (item: BaseItemDto) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const queryClient = useQueryClient(); + const lightHapticFeedback = useHaptic("light"); const invalidateQueries = () => { const queriesToInvalidate = [ @@ -29,7 +30,7 @@ export const useMarkAsPlayed = (item: BaseItemDto) => { }; const markAsPlayedStatus = async (played: boolean) => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); // Optimistic update queryClient.setQueryData( diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index 78fbbe6f..fb8b137f 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -48,7 +48,7 @@ import useImageStorage from "@/hooks/useImageStorage"; import { storage } from "@/utils/mmkv"; import useDownloadHelper from "@/utils/download"; import { FileInfo } from "expo-file-system"; -import * as Haptics from "expo-haptics"; +import { useHaptic } from "@/hooks/useHaptic"; import * as Application from "expo-application"; export type DownloadedItem = { @@ -78,6 +78,8 @@ function useDownloadProvider() { const [processes, setProcesses] = useAtom(processesAtom); + const successHapticFeedback = useHaptic("success"); + const authHeader = useMemo(() => { return api?.accessToken; }, [api]); @@ -532,9 +534,7 @@ function useDownloadProvider() { if (i.Id) return deleteFile(i.Id); return; }) - ).then(() => - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success) - ); + ).then(() => successHapticFeedback()); }; const cleanCacheDirectory = async () => { diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index c37dd4eb..c6e8a46a 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -84,6 +84,7 @@ export type Settings = { downloadMethod: "optimized" | "remux"; autoDownload: boolean; showCustomMenuLinks: boolean; + disableHapticFeedback: boolean; subtitleSize: number; remuxConcurrentLimit: 1 | 2 | 3 | 4; safeAreaInControlsEnabled: boolean; @@ -122,6 +123,7 @@ const loadSettings = (): Settings => { downloadMethod: "remux", autoDownload: false, showCustomMenuLinks: false, + disableHapticFeedback: false, subtitleSize: Platform.OS === "ios" ? 60 : 100, remuxConcurrentLimit: 1, safeAreaInControlsEnabled: true,