diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a4d04018..1ac06eda 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -4,9 +4,7 @@ title: "[Bug]: " labels: - ["❌ bug"] projects: - - ["fredrikburmester/5"] -assignees: - - fredrikburmester + - ["streamyfin/3"] body: - type: textarea diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 544b2743..0a4ed68b 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,8 @@ about: Suggest an idea for this project title: '' labels: '✨ enhancement' assignees: '' - +projects: + - streamyfin/3 --- **Describe the solution you'd like** diff --git a/.github/workflows/build-ios.yaml b/.github/workflows/build-ios.yaml index d66594ea..354ecb00 100644 --- a/.github/workflows/build-ios.yaml +++ b/.github/workflows/build-ios.yaml @@ -1,4 +1,4 @@ -name: release +name: Automatic Build and Deploy on: workflow_dispatch: @@ -27,8 +27,8 @@ jobs: pods-path: "ios/Podfile" configuration: Release # Change later to app-store if wanted - #export-method: app-store - export-method: ad-hoc + export-method: appstore + #export-method: ad-hoc workspace-path: "ios/Streamyfin.xcodeproj/project.xcworkspace/" project-path: "ios/Streamyfin.xcodeproj" scheme: Streamyfin diff --git a/README.md b/README.md index 6b3bcf99..87a6420c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Exp - + ## 🌟 Features diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index d43d73ea..8f1131a7 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -14,7 +14,7 @@ import { AppLanguageSelector } from "@/components/settings/AppLanguageSelector"; 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 { t } from "i18next"; import { useEffect } from "react"; @@ -25,10 +25,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 606c2d1e..a260bcfe 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, { @@ -70,9 +70,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 { @@ -177,7 +179,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 d48b0840..76d70ce4 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"; @@ -47,6 +47,8 @@ export default function page() { const isSeeking = useSharedValue(false); const cacheProgress = useSharedValue(0); + const lightHapticFeedback = useHaptic("light"); + const { itemId, audioIndex: audioIndexStr, @@ -126,7 +128,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 b174cc41..e02fdef3 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, { @@ -50,6 +50,7 @@ const Player = () => { const firstTime = useRef(true); const revalidateProgressCache = useInvalidatePlaybackProgressCache(); + const lightHapticFeedback = useHaptic("light"); const [isPlaybackStopped, setIsPlaybackStopped] = useState(false); const [showControls, _setShowControls] = useState(true); @@ -60,7 +61,7 @@ const Player = () => { const setShowControls = useCallback((show: boolean) => { _setShowControls(show); - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + lightHapticFeedback(); }, []); const progress = useSharedValue(0); @@ -169,7 +170,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/assets/images/jellyseerr.PNG b/assets/images/jellyseerr.PNG new file mode 100644 index 00000000..c72a8da1 Binary files /dev/null and b/assets/images/jellyseerr.PNG differ 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 5a6b6dbf..611999d4 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -32,8 +32,8 @@ import Animated, { import { Button } from "./Button"; import { SelectedOptions } from "./ItemContent"; import { chromecastProfile } from "@/utils/profiles/chromecast"; -import * as Haptics from "expo-haptics"; import { useTranslation } from "react-i18next"; +import { useHaptic } from "@/hooks/useHaptic"; interface Props extends React.ComponentProps { item: BaseItemDto; @@ -66,6 +66,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) => { @@ -81,7 +82,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 fdd27206..e74c1ea8 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -181,6 +181,15 @@ export const OtherSettings: React.FC = () => { } /> + + + + updateSettings({ disableHapticFeedback: value }) + } + /> + ); }; diff --git a/components/settings/QuickConnect.tsx b/components/settings/QuickConnect.tsx index 04e0ee5d..774935d9 100644 --- a/components/settings/QuickConnect.tsx +++ b/components/settings/QuickConnect.tsx @@ -7,8 +7,8 @@ import { BottomSheetView, } from "@gorhom/bottom-sheet"; import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api"; -import * as Haptics from "expo-haptics"; import { useTranslation } from "react-i18next"; +import { useHaptic } from "@/hooks/useHaptic"; import { useAtom } from "jotai"; import React, { useCallback, useRef, useState } from "react"; import { Alert, View, ViewProps } from "react-native"; @@ -24,6 +24,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 { t } = useTranslation(); @@ -46,16 +48,16 @@ export const QuickConnect: React.FC = ({ ...props }) => { userId: user?.Id, }); if (res.status === 200) { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + successHapticFeedback(); Alert.alert(t("home.settings.quick_connect.success"), t("home.settings.quick_connect.quick_connect_autorized")); setQuickConnectCode(undefined); bottomSheetModalRef?.current?.close(); } else { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + errorHapticFeedback(); Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code")); } } catch (e) { - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); + errorHapticFeedback(); Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code")); } } diff --git a/components/settings/StorageSettings.tsx b/components/settings/StorageSettings.tsx index 77ced95d..cd7a3df0 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"; @@ -15,6 +15,8 @@ import { useTranslation } from "react-i18next"; export const StorageSettings = () => { const { deleteAllFiles, appSizeUsage } = useDownload(); const { t } = useTranslation(); + const successHapticFeedback = useHaptic("success"); + const errorHapticFeedback = useHaptic("error"); const { data: size, isLoading: appSizeLoading } = useQuery({ queryKey: ["appSize", appSizeUsage], @@ -31,9 +33,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(t("home.settings.toasts.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/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 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 ba414bd8..1c912e2e 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"; import { useTranslation } from "react-i18next"; @@ -80,6 +80,8 @@ function useDownloadProvider() { const [processes, setProcesses] = useAtom(processesAtom); + const successHapticFeedback = useHaptic("success"); + const authHeader = useMemo(() => { return api?.accessToken; }, [api]); @@ -534,9 +536,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/svenska_kyrkan.sql b/svenska_kyrkan.sql deleted file mode 100644 index e69de29b..00000000 diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 2e6675ba..3b63009a 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -86,6 +86,7 @@ export type Settings = { downloadMethod: "optimized" | "remux"; autoDownload: boolean; showCustomMenuLinks: boolean; + disableHapticFeedback: boolean; subtitleSize: number; remuxConcurrentLimit: 1 | 2 | 3 | 4; safeAreaInControlsEnabled: boolean; @@ -125,6 +126,7 @@ const loadSettings = (): Settings => { downloadMethod: "remux", autoDownload: false, showCustomMenuLinks: false, + disableHapticFeedback: false, subtitleSize: Platform.OS === "ios" ? 60 : 100, remuxConcurrentLimit: 1, safeAreaInControlsEnabled: true,