mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-22 08:44:41 +01:00
Merge branch 'master' into feat/i18n
This commit is contained in:
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -4,9 +4,7 @@ title: "[Bug]: "
|
|||||||
labels:
|
labels:
|
||||||
- ["❌ bug"]
|
- ["❌ bug"]
|
||||||
projects:
|
projects:
|
||||||
- ["fredrikburmester/5"]
|
- ["streamyfin/3"]
|
||||||
assignees:
|
|
||||||
- fredrikburmester
|
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -4,7 +4,8 @@ about: Suggest an idea for this project
|
|||||||
title: ''
|
title: ''
|
||||||
labels: '✨ enhancement'
|
labels: '✨ enhancement'
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
projects:
|
||||||
|
- streamyfin/3
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
**Describe the solution you'd like**
|
||||||
|
|||||||
6
.github/workflows/build-ios.yaml
vendored
6
.github/workflows/build-ios.yaml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: release
|
name: Automatic Build and Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -27,8 +27,8 @@ jobs:
|
|||||||
pods-path: "ios/Podfile"
|
pods-path: "ios/Podfile"
|
||||||
configuration: Release
|
configuration: Release
|
||||||
# Change later to app-store if wanted
|
# Change later to app-store if wanted
|
||||||
#export-method: app-store
|
export-method: appstore
|
||||||
export-method: ad-hoc
|
#export-method: ad-hoc
|
||||||
workspace-path: "ios/Streamyfin.xcodeproj/project.xcworkspace/"
|
workspace-path: "ios/Streamyfin.xcodeproj/project.xcworkspace/"
|
||||||
project-path: "ios/Streamyfin.xcodeproj"
|
project-path: "ios/Streamyfin.xcodeproj"
|
||||||
scheme: Streamyfin
|
scheme: Streamyfin
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Exp
|
|||||||
<img width=150 src="./assets/images/screenshots/screenshot1.png" />
|
<img width=150 src="./assets/images/screenshots/screenshot1.png" />
|
||||||
<img width=150 src="./assets/images/screenshots/screenshot3.png" />
|
<img width=150 src="./assets/images/screenshots/screenshot3.png" />
|
||||||
<img width=150 src="./assets/images/screenshots/screenshot2.png" />
|
<img width=150 src="./assets/images/screenshots/screenshot2.png" />
|
||||||
|
<img width=150 src="./assets/images/jellyseerr.PNG"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { AppLanguageSelector } from "@/components/settings/AppLanguageSelector";
|
|||||||
import { UserInfo } from "@/components/settings/UserInfo";
|
import { UserInfo } from "@/components/settings/UserInfo";
|
||||||
import { useJellyfin } from "@/providers/JellyfinProvider";
|
import { useJellyfin } from "@/providers/JellyfinProvider";
|
||||||
import { clearLogs } from "@/utils/log";
|
import { clearLogs } from "@/utils/log";
|
||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useNavigation, useRouter } from "expo-router";
|
import { useNavigation, useRouter } from "expo-router";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -25,10 +25,11 @@ export default function settings() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { logout } = useJellyfin();
|
const { logout } = useJellyfin();
|
||||||
|
const successHapticFeedback = useHaptic("success");
|
||||||
|
|
||||||
const onClearLogsClicked = async () => {
|
const onClearLogsClicked = async () => {
|
||||||
clearLogs();
|
clearLogs();
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
successHapticFeedback();
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
getUserLibraryApi,
|
getUserLibraryApi,
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useFocusEffect, useGlobalSearchParams } from "expo-router";
|
import { useFocusEffect, useGlobalSearchParams } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import React, {
|
import React, {
|
||||||
@@ -70,9 +70,11 @@ export default function page() {
|
|||||||
const { getDownloadedItem } = useDownload();
|
const { getDownloadedItem } = useDownload();
|
||||||
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
||||||
|
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const setShowControls = useCallback((show: boolean) => {
|
const setShowControls = useCallback((show: boolean) => {
|
||||||
_setShowControls(show);
|
_setShowControls(show);
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -177,7 +179,7 @@ export default function page() {
|
|||||||
const togglePlay = useCallback(async () => {
|
const togglePlay = useCallback(async () => {
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
await videoRef.current?.pause();
|
await videoRef.current?.pause();
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
getUserLibraryApi,
|
getUserLibraryApi,
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
@@ -47,6 +47,8 @@ export default function page() {
|
|||||||
const isSeeking = useSharedValue(false);
|
const isSeeking = useSharedValue(false);
|
||||||
const cacheProgress = useSharedValue(0);
|
const cacheProgress = useSharedValue(0);
|
||||||
|
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
itemId,
|
itemId,
|
||||||
audioIndex: audioIndexStr,
|
audioIndex: audioIndexStr,
|
||||||
@@ -126,7 +128,7 @@ export default function page() {
|
|||||||
|
|
||||||
const togglePlay = useCallback(
|
const togglePlay = useCallback(
|
||||||
async (ticks: number) => {
|
async (ticks: number) => {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
videoRef.current?.pause();
|
videoRef.current?.pause();
|
||||||
await getPlaystateApi(api!).onPlaybackProgress({
|
await getPlaystateApi(api!).onPlaybackProgress({
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
getUserLibraryApi,
|
getUserLibraryApi,
|
||||||
} from "@jellyfin/sdk/lib/utils/api";
|
} from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import React, {
|
import React, {
|
||||||
@@ -50,6 +50,7 @@ const Player = () => {
|
|||||||
|
|
||||||
const firstTime = useRef(true);
|
const firstTime = useRef(true);
|
||||||
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
const revalidateProgressCache = useInvalidatePlaybackProgressCache();
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
||||||
const [showControls, _setShowControls] = useState(true);
|
const [showControls, _setShowControls] = useState(true);
|
||||||
@@ -60,7 +61,7 @@ const Player = () => {
|
|||||||
|
|
||||||
const setShowControls = useCallback((show: boolean) => {
|
const setShowControls = useCallback((show: boolean) => {
|
||||||
_setShowControls(show);
|
_setShowControls(show);
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const progress = useSharedValue(0);
|
const progress = useSharedValue(0);
|
||||||
@@ -169,7 +170,7 @@ const Player = () => {
|
|||||||
const videoSource = useVideoSource(item, api, poster, stream?.url);
|
const videoSource = useVideoSource(item, api, poster, stream?.url);
|
||||||
|
|
||||||
const togglePlay = useCallback(async () => {
|
const togglePlay = useCallback(async () => {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
videoRef.current?.pause();
|
videoRef.current?.pause();
|
||||||
await getPlaystateApi(api!).onPlaybackProgress({
|
await getPlaystateApi(api!).onPlaybackProgress({
|
||||||
|
|||||||
BIN
assets/images/jellyseerr.PNG
Normal file
BIN
assets/images/jellyseerr.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
@@ -1,4 +1,4 @@
|
|||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import React, { PropsWithChildren, ReactNode, useMemo } from "react";
|
import React, { PropsWithChildren, ReactNode, useMemo } from "react";
|
||||||
import { Text, TouchableOpacity, View } from "react-native";
|
import { Text, TouchableOpacity, View } from "react-native";
|
||||||
import { Loader } from "./Loader";
|
import { Loader } from "./Loader";
|
||||||
@@ -43,6 +43,8 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
|||||||
}
|
}
|
||||||
}, [color]);
|
}, [color]);
|
||||||
|
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
className={`
|
className={`
|
||||||
@@ -54,7 +56,7 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!loading && !disabled && onPress) {
|
if (!loading && !disabled && onPress) {
|
||||||
onPress();
|
onPress();
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ import Animated, {
|
|||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { SelectedOptions } from "./ItemContent";
|
import { SelectedOptions } from "./ItemContent";
|
||||||
import { chromecastProfile } from "@/utils/profiles/chromecast";
|
import { chromecastProfile } from "@/utils/profiles/chromecast";
|
||||||
import * as Haptics from "expo-haptics";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof Button> {
|
interface Props extends React.ComponentProps<typeof Button> {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -66,6 +66,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const widthProgress = useSharedValue(0);
|
const widthProgress = useSharedValue(0);
|
||||||
const colorChangeProgress = useSharedValue(0);
|
const colorChangeProgress = useSharedValue(0);
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const goToPlayer = useCallback(
|
const goToPlayer = useCallback(
|
||||||
(q: string, bitrateValue: number | undefined) => {
|
(q: string, bitrateValue: number | undefined) => {
|
||||||
@@ -81,7 +82,7 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
const onPress = useCallback(async () => {
|
const onPress = useCallback(async () => {
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
|
|
||||||
const queryParams = new URLSearchParams({
|
const queryParams = new URLSearchParams({
|
||||||
itemId: item.Id!,
|
itemId: item.Id!,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
TouchableOpacityProps,
|
TouchableOpacityProps,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
|
|
||||||
interface Props extends TouchableOpacityProps {
|
interface Props extends TouchableOpacityProps {
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
@@ -29,10 +29,11 @@ export const RoundButton: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const buttonSize = size === "large" ? "h-10 w-10" : "h-9 w-9";
|
const buttonSize = size === "large" ? "h-10 w-10" : "h-9 w-9";
|
||||||
const fillColorClass = fillColor === "primary" ? "bg-purple-600" : "";
|
const fillColorClass = fillColor === "primary" ? "bg-purple-600" : "";
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const handlePress = () => {
|
const handlePress = () => {
|
||||||
if (hapticFeedback) {
|
if (hapticFeedback) {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
}
|
}
|
||||||
onPress?.();
|
onPress?.();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
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 React, { useCallback, useMemo } from "react";
|
||||||
import { TouchableOpacity, TouchableOpacityProps, View } from "react-native";
|
import { TouchableOpacity, TouchableOpacityProps, View } from "react-native";
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +26,7 @@ export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item, ...props }) => {
|
|||||||
const { deleteFile } = useDownload();
|
const { deleteFile } = useDownload();
|
||||||
const { openFile } = useDownloadedFileOpener();
|
const { openFile } = useDownloadedFileOpener();
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
|
const successHapticFeedback = useHaptic("success");
|
||||||
|
|
||||||
const base64Image = useMemo(() => {
|
const base64Image = useMemo(() => {
|
||||||
return storage.getString(item.Id!);
|
return storage.getString(item.Id!);
|
||||||
@@ -41,7 +42,7 @@ export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item, ...props }) => {
|
|||||||
const handleDeleteFile = useCallback(() => {
|
const handleDeleteFile = useCallback(() => {
|
||||||
if (item.Id) {
|
if (item.Id) {
|
||||||
deleteFile(item.Id);
|
deleteFile(item.Id);
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
successHapticFeedback();
|
||||||
}
|
}
|
||||||
}, [deleteFile, item.Id]);
|
}, [deleteFile, item.Id]);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
useActionSheet,
|
useActionSheet,
|
||||||
} from "@expo/react-native-action-sheet";
|
} from "@expo/react-native-action-sheet";
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
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 React, { useCallback, useMemo } from "react";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
|
|||||||
const { deleteFile } = useDownload();
|
const { deleteFile } = useDownload();
|
||||||
const { openFile } = useDownloadedFileOpener();
|
const { openFile } = useDownloadedFileOpener();
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
|
const successHapticFeedback = useHaptic("success");
|
||||||
|
|
||||||
const handleOpenFile = useCallback(() => {
|
const handleOpenFile = useCallback(() => {
|
||||||
openFile(item);
|
openFile(item);
|
||||||
@@ -43,7 +44,7 @@ export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
|
|||||||
const handleDeleteFile = useCallback(() => {
|
const handleDeleteFile = useCallback(() => {
|
||||||
if (item.Id) {
|
if (item.Id) {
|
||||||
deleteFile(item.Id);
|
deleteFile(item.Id);
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
successHapticFeedback();
|
||||||
}
|
}
|
||||||
}, [deleteFile, item.Id]);
|
}, [deleteFile, item.Id]);
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { itemRouter, TouchableItemRouter } from "../common/TouchableItemRouter";
|
|||||||
import { Loader } from "../Loader";
|
import { Loader } from "../Loader";
|
||||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||||
import { useRouter, useSegments } from "expo-router";
|
import { useRouter, useSegments } from "expo-router";
|
||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
@@ -128,6 +128,7 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
|||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const screenWidth = Dimensions.get("screen").width;
|
const screenWidth = Dimensions.get("screen").width;
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const uri = useMemo(() => {
|
const uri = useMemo(() => {
|
||||||
if (!api) return null;
|
if (!api) return null;
|
||||||
@@ -153,7 +154,7 @@ const RenderItem: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
|||||||
const handleRoute = useCallback(() => {
|
const handleRoute = useCallback(() => {
|
||||||
if (!from) return;
|
if (!from) return;
|
||||||
const url = itemRouter(item, from);
|
const url = itemRouter(item, from);
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (url) router.push(url);
|
if (url) router.push(url);
|
||||||
}, [item, from]);
|
}, [item, from]);
|
||||||
|
|||||||
@@ -181,6 +181,15 @@ export const OtherSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem title="Disable Haptic Feedback">
|
||||||
|
<Switch
|
||||||
|
value={settings.disableHapticFeedback}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateSettings({ disableHapticFeedback: value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
BottomSheetView,
|
BottomSheetView,
|
||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import * as Haptics from "expo-haptics";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import React, { useCallback, useRef, useState } from "react";
|
import React, { useCallback, useRef, useState } from "react";
|
||||||
import { Alert, View, ViewProps } from "react-native";
|
import { Alert, View, ViewProps } from "react-native";
|
||||||
@@ -24,6 +24,8 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
|||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const [quickConnectCode, setQuickConnectCode] = useState<string>();
|
const [quickConnectCode, setQuickConnectCode] = useState<string>();
|
||||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||||
|
const successHapticFeedback = useHaptic("success");
|
||||||
|
const errorHapticFeedback = useHaptic("error");
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -46,16 +48,16 @@ export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
|||||||
userId: user?.Id,
|
userId: user?.Id,
|
||||||
});
|
});
|
||||||
if (res.status === 200) {
|
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"));
|
Alert.alert(t("home.settings.quick_connect.success"), t("home.settings.quick_connect.quick_connect_autorized"));
|
||||||
setQuickConnectCode(undefined);
|
setQuickConnectCode(undefined);
|
||||||
bottomSheetModalRef?.current?.close();
|
bottomSheetModalRef?.current?.close();
|
||||||
} else {
|
} else {
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
errorHapticFeedback();
|
||||||
Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code"));
|
Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code"));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
errorHapticFeedback();
|
||||||
Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code"));
|
Alert.alert(t("home.settings.quick_connect.error"), t("home.settings.quick_connect.invalid_code"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useDownload } from "@/providers/DownloadProvider";
|
|||||||
import { clearLogs } from "@/utils/log";
|
import { clearLogs } from "@/utils/log";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import * as FileSystem from "expo-file-system";
|
import * as FileSystem from "expo-file-system";
|
||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import * as Progress from "react-native-progress";
|
import * as Progress from "react-native-progress";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
@@ -15,6 +15,8 @@ import { useTranslation } from "react-i18next";
|
|||||||
export const StorageSettings = () => {
|
export const StorageSettings = () => {
|
||||||
const { deleteAllFiles, appSizeUsage } = useDownload();
|
const { deleteAllFiles, appSizeUsage } = useDownload();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const successHapticFeedback = useHaptic("success");
|
||||||
|
const errorHapticFeedback = useHaptic("error");
|
||||||
|
|
||||||
const { data: size, isLoading: appSizeLoading } = useQuery({
|
const { data: size, isLoading: appSizeLoading } = useQuery({
|
||||||
queryKey: ["appSize", appSizeUsage],
|
queryKey: ["appSize", appSizeUsage],
|
||||||
@@ -31,9 +33,9 @@ export const StorageSettings = () => {
|
|||||||
const onDeleteClicked = async () => {
|
const onDeleteClicked = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteAllFiles();
|
await deleteAllFiles();
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
successHapticFeedback();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
errorHapticFeedback();
|
||||||
toast.error(t("home.settings.toasts.error_deleting_files"));
|
toast.error(t("home.settings.toasts.error_deleting_files"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
@@ -157,10 +157,12 @@ export const Controls: React.FC<Props> = ({
|
|||||||
isVlc
|
isVlc
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const goToPreviousItem = useCallback(() => {
|
const goToPreviousItem = useCallback(() => {
|
||||||
if (!previousItem || !settings) return;
|
if (!previousItem || !settings) return;
|
||||||
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
|
|
||||||
const previousIndexes: previousIndexes = {
|
const previousIndexes: previousIndexes = {
|
||||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||||
@@ -198,7 +200,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const goToNextItem = useCallback(() => {
|
const goToNextItem = useCallback(() => {
|
||||||
if (!nextItem || !settings) return;
|
if (!nextItem || !settings) return;
|
||||||
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
|
|
||||||
const previousIndexes: previousIndexes = {
|
const previousIndexes: previousIndexes = {
|
||||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||||
@@ -326,7 +328,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const handleSkipBackward = useCallback(async () => {
|
const handleSkipBackward = useCallback(async () => {
|
||||||
if (!settings?.rewindSkipTime) return;
|
if (!settings?.rewindSkipTime) return;
|
||||||
wasPlayingRef.current = isPlaying;
|
wasPlayingRef.current = isPlaying;
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
try {
|
try {
|
||||||
const curr = progress.value;
|
const curr = progress.value;
|
||||||
if (curr !== undefined) {
|
if (curr !== undefined) {
|
||||||
@@ -344,7 +346,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const handleSkipForward = useCallback(async () => {
|
const handleSkipForward = useCallback(async () => {
|
||||||
if (!settings?.forwardSkipTime) return;
|
if (!settings?.forwardSkipTime) return;
|
||||||
wasPlayingRef.current = isPlaying;
|
wasPlayingRef.current = isPlaying;
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
try {
|
try {
|
||||||
const curr = progress.value;
|
const curr = progress.value;
|
||||||
if (curr !== undefined) {
|
if (curr !== undefined) {
|
||||||
@@ -361,7 +363,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
|
|
||||||
const toggleIgnoreSafeAreas = useCallback(() => {
|
const toggleIgnoreSafeAreas = useCallback(() => {
|
||||||
setIgnoreSafeAreas((prev) => !prev);
|
setIgnoreSafeAreas((prev) => !prev);
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const memoizedRenderBubble = useCallback(() => {
|
const memoizedRenderBubble = useCallback(() => {
|
||||||
@@ -440,7 +442,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
const gotoItem = await getItemById(api, itemId);
|
const gotoItem = await getItemById(api, itemId);
|
||||||
if (!settings || !gotoItem) return;
|
if (!settings || !gotoItem) return;
|
||||||
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
|
|
||||||
const previousIndexes: previousIndexes = {
|
const previousIndexes: previousIndexes = {
|
||||||
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
subtitleIndex: subtitleIndex ? parseInt(subtitleIndex) : undefined,
|
||||||
@@ -584,7 +586,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
router.back();
|
router.back();
|
||||||
}}
|
}}
|
||||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { apiAtom } from "@/providers/JellyfinProvider";
|
|||||||
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import { msToSeconds, secondsToMs } from "@/utils/time";
|
import { msToSeconds, secondsToMs } from "@/utils/time";
|
||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "./useHaptic";
|
||||||
|
|
||||||
interface CreditTimestamps {
|
interface CreditTimestamps {
|
||||||
Introduction: {
|
Introduction: {
|
||||||
@@ -29,6 +29,7 @@ export const useCreditSkipper = (
|
|||||||
) => {
|
) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [showSkipCreditButton, setShowSkipCreditButton] = useState(false);
|
const [showSkipCreditButton, setShowSkipCreditButton] = useState(false);
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
if (isVlc) {
|
if (isVlc) {
|
||||||
currentTime = msToSeconds(currentTime);
|
currentTime = msToSeconds(currentTime);
|
||||||
@@ -79,7 +80,7 @@ export const useCreditSkipper = (
|
|||||||
if (!creditTimestamps) return;
|
if (!creditTimestamps) return;
|
||||||
console.log(`Skipping credits to ${creditTimestamps.Credits.End}`);
|
console.log(`Skipping credits to ${creditTimestamps.Credits.End}`);
|
||||||
try {
|
try {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
wrappedSeek(creditTimestamps.Credits.End);
|
wrappedSeek(creditTimestamps.Credits.End);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
play();
|
play();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
// Used only for intial play settings.
|
// Used only for initial play settings.
|
||||||
const useDefaultPlaySettings = (
|
const useDefaultPlaySettings = (
|
||||||
item: BaseItemDto,
|
item: BaseItemDto,
|
||||||
settings: Settings | null
|
settings: Settings | null
|
||||||
|
|||||||
54
hooks/useHaptic.ts
Normal file
54
hooks/useHaptic.ts
Normal file
@@ -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];
|
||||||
|
};
|
||||||
@@ -5,7 +5,7 @@ import { apiAtom } from "@/providers/JellyfinProvider";
|
|||||||
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import { msToSeconds, secondsToMs } from "@/utils/time";
|
import { msToSeconds, secondsToMs } from "@/utils/time";
|
||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "./useHaptic";
|
||||||
|
|
||||||
interface IntroTimestamps {
|
interface IntroTimestamps {
|
||||||
EpisodeId: string;
|
EpisodeId: string;
|
||||||
@@ -33,6 +33,7 @@ export const useIntroSkipper = (
|
|||||||
if (isVlc) {
|
if (isVlc) {
|
||||||
currentTime = msToSeconds(currentTime);
|
currentTime = msToSeconds(currentTime);
|
||||||
}
|
}
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const wrappedSeek = (seconds: number) => {
|
const wrappedSeek = (seconds: number) => {
|
||||||
if (isVlc) {
|
if (isVlc) {
|
||||||
@@ -78,7 +79,7 @@ export const useIntroSkipper = (
|
|||||||
const skipIntro = useCallback(() => {
|
const skipIntro = useCallback(() => {
|
||||||
if (!introTimestamps) return;
|
if (!introTimestamps) return;
|
||||||
try {
|
try {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
wrappedSeek(introTimestamps.IntroEnd);
|
wrappedSeek(introTimestamps.IntroEnd);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
play();
|
play();
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed";
|
|||||||
import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed";
|
import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed";
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "./useHaptic";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
export const useMarkAsPlayed = (item: BaseItemDto) => {
|
export const useMarkAsPlayed = (item: BaseItemDto) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
const invalidateQueries = () => {
|
const invalidateQueries = () => {
|
||||||
const queriesToInvalidate = [
|
const queriesToInvalidate = [
|
||||||
@@ -29,7 +30,7 @@ export const useMarkAsPlayed = (item: BaseItemDto) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const markAsPlayedStatus = async (played: boolean) => {
|
const markAsPlayedStatus = async (played: boolean) => {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
lightHapticFeedback();
|
||||||
|
|
||||||
// Optimistic update
|
// Optimistic update
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import useImageStorage from "@/hooks/useImageStorage";
|
|||||||
import { storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
import useDownloadHelper from "@/utils/download";
|
import useDownloadHelper from "@/utils/download";
|
||||||
import { FileInfo } from "expo-file-system";
|
import { FileInfo } from "expo-file-system";
|
||||||
import * as Haptics from "expo-haptics";
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
import * as Application from "expo-application";
|
import * as Application from "expo-application";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -80,6 +80,8 @@ function useDownloadProvider() {
|
|||||||
|
|
||||||
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
|
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
|
||||||
|
|
||||||
|
const successHapticFeedback = useHaptic("success");
|
||||||
|
|
||||||
const authHeader = useMemo(() => {
|
const authHeader = useMemo(() => {
|
||||||
return api?.accessToken;
|
return api?.accessToken;
|
||||||
}, [api]);
|
}, [api]);
|
||||||
@@ -534,9 +536,7 @@ function useDownloadProvider() {
|
|||||||
if (i.Id) return deleteFile(i.Id);
|
if (i.Id) return deleteFile(i.Id);
|
||||||
return;
|
return;
|
||||||
})
|
})
|
||||||
).then(() =>
|
).then(() => successHapticFeedback());
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanCacheDirectory = async () => {
|
const cleanCacheDirectory = async () => {
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export type Settings = {
|
|||||||
downloadMethod: "optimized" | "remux";
|
downloadMethod: "optimized" | "remux";
|
||||||
autoDownload: boolean;
|
autoDownload: boolean;
|
||||||
showCustomMenuLinks: boolean;
|
showCustomMenuLinks: boolean;
|
||||||
|
disableHapticFeedback: boolean;
|
||||||
subtitleSize: number;
|
subtitleSize: number;
|
||||||
remuxConcurrentLimit: 1 | 2 | 3 | 4;
|
remuxConcurrentLimit: 1 | 2 | 3 | 4;
|
||||||
safeAreaInControlsEnabled: boolean;
|
safeAreaInControlsEnabled: boolean;
|
||||||
@@ -125,6 +126,7 @@ const loadSettings = (): Settings => {
|
|||||||
downloadMethod: "remux",
|
downloadMethod: "remux",
|
||||||
autoDownload: false,
|
autoDownload: false,
|
||||||
showCustomMenuLinks: false,
|
showCustomMenuLinks: false,
|
||||||
|
disableHapticFeedback: false,
|
||||||
subtitleSize: Platform.OS === "ios" ? 60 : 100,
|
subtitleSize: Platform.OS === "ios" ? 60 : 100,
|
||||||
remuxConcurrentLimit: 1,
|
remuxConcurrentLimit: 1,
|
||||||
safeAreaInControlsEnabled: true,
|
safeAreaInControlsEnabled: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user