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,