Merge pull request #157 from anultravioletaurora/121-add-glitchtip-logging

Scrubber / Player Optimizations
This commit is contained in:
Violet Caulfield
2025-02-22 16:27:46 -06:00
committed by GitHub
7 changed files with 150 additions and 164 deletions

View File

@@ -17,9 +17,10 @@ export function fetchRecentlyAdded(limit: number = QueryConfig.limits.recents, o
.getLatestMedia({
parentId: Client.library.musicLibraryId,
limit,
})
.then(({ data }) => {
resolve(data);
resolve(offset ? data.slice(offset, data.length - 1) : data);
});
})
}

View File

@@ -14,7 +14,7 @@ export default function Albums({ navigation, route }: AlbumsProps) : React.JSX.E
const { data: albums, refetch, isPending } = useQuery({
queryKey: [route.params.query],
queryFn: () => fetchRecentlyAddedAlbums ? fetchRecentlyAdded(QueryConfig.limits.recents * 4, 20) : fetchFavoriteAlbums()
queryFn: () => fetchRecentlyAddedAlbums ? fetchRecentlyAdded(QueryConfig.limits.recents * 4, QueryConfig.limits.recents) : fetchFavoriteAlbums()
});
const { width } = useSafeAreaFrame();

View File

@@ -32,7 +32,7 @@ export default function BlurhashedImage({
Math.ceil(height ?? width / 100) * 100 // So these keys need to match
],
queryFn: () => fetchItemImage(item.AlbumId ? item.AlbumId : item.Id!, type ?? ImageType.Primary, width, height ?? width),
staleTime: (1000 * 60 * 60) * 4 // 4 hours
staleTime: (1000 * 60 * 60) * 24 * 7 // 1 week, to prevent overloading servers
});
const blurhash = !isEmpty(item.ImageBlurHashes)

View File

@@ -0,0 +1,106 @@
import React, { useEffect, useState } from "react";
import { useProgress } from "react-native-track-player";
import { ProgressMultiplier } from "../component.config";
import { HorizontalSlider } from "../../../components/Global/helpers/slider";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { trigger } from "react-native-haptic-feedback";
import { XStack, YStack } from "tamagui";
import { useSafeAreaFrame } from "react-native-safe-area-context";
import { usePlayerContext } from "../../../player/provider";
import { RunTimeSeconds } from "../../../components/Global/helpers/time-codes";
const scrubGesture = Gesture.Pan();
export default function Scrubber() : React.JSX.Element {
const {
useSeekTo,
} = usePlayerContext();
const { width } = useSafeAreaFrame();
const progress = useProgress();
const [seeking, setSeeking] = useState<boolean>(false);
const [position, setPosition] = useState<number>(progress && progress.position ?
Math.floor(progress.position * ProgressMultiplier)
: 0
);
useEffect(() => {
if (!seeking)
progress && progress.position
? setPosition(
Math.floor(
progress.position * ProgressMultiplier
)
) : 0;
}, [
progress
]);
return (
<YStack>
<GestureDetector gesture={scrubGesture}>
<HorizontalSlider
value={position}
max={
progress && progress.duration > 0
? progress.duration * ProgressMultiplier
: 1
}
width={width / 1.1}
props={{
// If user swipes off of the slider we should seek to the spot
onPressOut: (event) => {
trigger("notificationSuccess")
setSeeking(false);
useSeekTo.mutate(Math.floor(position / ProgressMultiplier));
},
onSlideStart: (event, value) => {
trigger("impactLight");
setSeeking(true);
setPosition(value)
},
onSlideMove: (event, value) => {
trigger("clockTick")
setSeeking(true);
setPosition(value);
},
onSlideEnd: (event, value) => {
trigger("notificationSuccess")
setSeeking(false);
setPosition(value)
useSeekTo.mutate(Math.floor(value / ProgressMultiplier));
}
}}
/>
</GestureDetector>
<XStack marginHorizontal={20} marginTop={"$3"} marginBottom={"$2"}>
<XStack flex={1} justifyContent="flex-start">
<RunTimeSeconds>{Math.floor(position / ProgressMultiplier)}</RunTimeSeconds>
</XStack>
<XStack flex={1} justifyContent="space-between">
{ /** Track metadata can go here */}
</XStack>
<XStack flex={1} justifyContent="flex-end">
<RunTimeSeconds>
{
progress && progress.duration
? Math.ceil(progress.duration)
: 0
}
</RunTimeSeconds>
</XStack>
</XStack>
</YStack>
)
}

View File

@@ -1,9 +1,7 @@
import { HorizontalSlider } from "../../../components/Global/helpers/slider";
import { RunTimeSeconds } from "../../../components/Global/helpers/time-codes";
import { StackParamList } from "../../../components/types";
import { usePlayerContext } from "../../../player/provider";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import React, { useState, useEffect, useMemo } from "react";
import React, { useState, useMemo } from "react";
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
import { YStack, XStack, Spacer, getTokens } from "tamagui";
import PlayPauseButton from "../helpers/buttons";
@@ -13,15 +11,10 @@ import FavoriteButton from "../../Global/components/favorite-button";
import BlurhashedImage from "../../Global/components/blurhashed-image";
import TextTicker from "react-native-text-ticker";
import { ProgressMultiplier, TextTickerConfig } from "../component.config";
import { toUpper } from "lodash";
import { trigger } from "react-native-haptic-feedback";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useIsFocused } from "@react-navigation/native";
import { useProgress } from "react-native-track-player";
import { UPDATE_INTERVAL } from "../../../player/config";
const scrubGesture = Gesture.Pan();
import Scrubber from "../helpers/scrubber";
export default function PlayerScreen({
navigation
@@ -42,8 +35,6 @@ export default function PlayerScreen({
const progress = useProgress(UPDATE_INTERVAL);
const [seeking, setSeeking] = useState<boolean>(false);
/**
* TrackPlayer.getProgress() returns a high sig-fig number. We're going to apply
* a multiplier so that the scrubber bar can take advantage of those extra numbers
@@ -54,22 +45,8 @@ export default function PlayerScreen({
: 0
);
const freeze = !useIsFocused()
const { width } = useSafeAreaFrame();
useEffect(() => {
if (!seeking)
progress && progress.position
? setProgressState(
Math.ceil(
progress.position * ProgressMultiplier
)
) : 0;
}, [
progress
]);
return (
<SafeAreaView edges={["right", "left"]}>
{ nowPlaying && (
@@ -215,95 +192,7 @@ export default function PlayerScreen({
<XStack justifyContent="center" marginTop={"$3"}>
{/* playback progress goes here */}
{ useMemo(() => {
return (
<GestureDetector gesture={scrubGesture}>
<HorizontalSlider
value={progressState}
max={
progress && progress.duration > 0
? progress.duration * ProgressMultiplier
: 1
}
width={width / 1.1}
props={{
// If user swipes off of the slider we should seek to the spot
onPressOut: () => {
setSeeking(false);
navigation.setOptions({
gestureEnabled: true
});
useSeekTo.mutate(Math.floor(progressState / ProgressMultiplier));
},
onSlideStart: () => {
trigger("impactLight");
setSeeking(true);
navigation.setOptions({
gestureEnabled: false
});
},
onSlideMove: (event, value) => {
setSeeking(true);
navigation.setOptions({
gestureEnabled: false
});
setProgressState(value);
},
onSlideEnd: (event, value) => {
setSeeking(false);
navigation.setOptions({
gestureEnabled: true
});
useSeekTo.mutate(Math.floor(value / ProgressMultiplier));
}
}}
/>
</GestureDetector>
)}, [
progressState
]
)}
</XStack>
<XStack marginHorizontal={20} marginTop={"$3"} marginBottom={"$2"}>
<XStack flex={1} justifyContent="flex-start">
{ useMemo(() => {
return (
<RunTimeSeconds>{Math.floor(progressState / ProgressMultiplier)}</RunTimeSeconds>
)
}, [
progressState,
])}
</XStack>
<XStack flex={1} justifyContent="space-between">
{ /** Track metadata can go here */}
{ nowPlaying!.item.MediaSources && (
<>
<Text>{toUpper(nowPlaying!.item.MediaSources[0].Container ?? "")}</Text>
<Text>{nowPlaying!.item.MediaSources[0].Bitrate?.toString() ?? ""}</Text>
</>
)}
</XStack>
<XStack flex={1} justifyContent="flex-end">
<RunTimeSeconds>
{
progress && progress.duration
? Math.ceil(progress.duration)
: 0
}
</RunTimeSeconds>
</XStack>
<Scrubber />
</XStack>
{ useMemo(() => {
@@ -317,11 +206,7 @@ export default function PlayerScreen({
color={getTokens().color.amethyst.val}
name="rewind-15"
onPress={() => {
setSeeking(true);
setProgressState(progressState - (15 * ProgressMultiplier));
useSeekTo.mutate(progress!.position - 15);
setSeeking(false);
}}
/>
@@ -334,10 +219,7 @@ export default function PlayerScreen({
if (progressState / ProgressMultiplier < 3)
usePrevious.mutate()
else {
setSeeking(true);
setProgressState(0);
useSeekTo.mutate(0);
setSeeking(false);
}
}}
large
@@ -357,10 +239,7 @@ export default function PlayerScreen({
color={getTokens().color.amethyst.val}
name="fast-forward-15"
onPress={() => {
setSeeking(true);
setProgressState(progressState + (15 * ProgressMultiplier));
useSeekTo.mutate(progress!.position + 15);
setSeeking(false);
}}
/>
</XStack>

View File

@@ -8,10 +8,10 @@
/* Begin PBXBuildFile section */
00E356F31AD99517003FC87E /* JellifyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* JellifyTests.m */; };
034195DF3B8F1C78926A1336 /* libPods-Jellify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DD972EC601574B1EAFFF9416 /* libPods-Jellify.a */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
217EBE16A3E8C5FBF476C905 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F757EB73303E0AC21EF34F64 /* PrivacyInfo.xcprivacy */; };
66BC9C5D1B536CD0799EEC89 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82DE980BB8253E3C5F2207CE /* ExpoModulesProvider.swift */; };
6B4B9F0E46E99B0A2EB6124A /* libPods-Jellify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 48BB786983F32A16C8675251 /* libPods-Jellify.a */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
CF620D0C2CF2BB210045E433 /* Aileron-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = CF620CFC2CF2BB1F0045E433 /* Aileron-Italic.otf */; };
CF620D0D2CF2BB210045E433 /* Aileron-Thin.otf in Resources */ = {isa = PBXBuildFile; fileRef = CF620CFD2CF2BB1F0045E433 /* Aileron-Thin.otf */; };
@@ -93,14 +93,15 @@
00E356EE1AD99517003FC87E /* JellifyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JellifyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
00E356F21AD99517003FC87E /* JellifyTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JellifyTests.m; sourceTree = "<group>"; };
0ED95C4266474027C42A391B /* Pods-Jellify.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.release.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.release.xcconfig"; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* Jellify.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Jellify.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Jellify/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Jellify/Info.plist; sourceTree = "<group>"; };
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = Jellify/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
48BB786983F32A16C8675251 /* libPods-Jellify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Jellify.a"; sourceTree = BUILT_PRODUCTS_DIR; };
518045BFAF0062742ACB0F12 /* Pods-Jellify.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.release.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.release.xcconfig"; sourceTree = "<group>"; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Jellify/LaunchScreen.storyboard; sourceTree = "<group>"; };
82DE980BB8253E3C5F2207CE /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Jellify/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
94895B86D899ADED148B71DC /* Pods-Jellify.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.debug.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.debug.xcconfig"; sourceTree = "<group>"; };
864F3550DF529DA8E784B2B9 /* Pods-Jellify.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.debug.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.debug.xcconfig"; sourceTree = "<group>"; };
CF620CFC2CF2BB1F0045E433 /* Aileron-Italic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Aileron-Italic.otf"; path = "../assets/fonts/Aileron-Italic.otf"; sourceTree = "<group>"; };
CF620CFD2CF2BB1F0045E433 /* Aileron-Thin.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Aileron-Thin.otf"; path = "../assets/fonts/Aileron-Thin.otf"; sourceTree = "<group>"; };
CF620CFE2CF2BB1F0045E433 /* Aileron-HeavyItalic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Aileron-HeavyItalic.otf"; path = "../assets/fonts/Aileron-HeavyItalic.otf"; sourceTree = "<group>"; };
@@ -167,7 +168,6 @@
CF98CA442D3E99DF003D88B7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
CF98CA452D3E99DF003D88B7 /* CarScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarScene.swift; sourceTree = "<group>"; };
CF98CA462D3E99DF003D88B7 /* PhoneScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneScene.swift; sourceTree = "<group>"; };
DD972EC601574B1EAFFF9416 /* libPods-Jellify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Jellify.a"; sourceTree = BUILT_PRODUCTS_DIR; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
F757EB73303E0AC21EF34F64 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Jellify/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -184,7 +184,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
034195DF3B8F1C78926A1336 /* libPods-Jellify.a in Frameworks */,
6B4B9F0E46E99B0A2EB6124A /* libPods-Jellify.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -230,7 +230,7 @@
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
DD972EC601574B1EAFFF9416 /* libPods-Jellify.a */,
48BB786983F32A16C8675251 /* libPods-Jellify.a */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -303,8 +303,8 @@
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
isa = PBXGroup;
children = (
94895B86D899ADED148B71DC /* Pods-Jellify.debug.xcconfig */,
0ED95C4266474027C42A391B /* Pods-Jellify.release.xcconfig */,
864F3550DF529DA8E784B2B9 /* Pods-Jellify.debug.xcconfig */,
518045BFAF0062742ACB0F12 /* Pods-Jellify.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -394,14 +394,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Jellify" */;
buildPhases = (
F683752C16F968F41DA24D1A /* [CP] Check Pods Manifest.lock */,
A2A2D99B34926301B9B71068 /* [CP] Check Pods Manifest.lock */,
41245AA25E9CF87045F16E79 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
DE4A159AEFEBDE821700578E /* [CP] Embed Pods Frameworks */,
D8E280F2303043E9566F0E8F /* [CP] Copy Pods Resources */,
790E1FAE0BAFB8ECE3839DB2 /* [CP] Embed Pods Frameworks */,
9DCCD5540BF35D589BD5B454 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -564,24 +564,7 @@
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Jellify/expo-configure-project.sh\"\n";
};
D8E280F2303043E9566F0E8F /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n";
showEnvVarsInLog = 0;
};
DE4A159AEFEBDE821700578E /* [CP] Embed Pods Frameworks */ = {
790E1FAE0BAFB8ECE3839DB2 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -598,7 +581,24 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
F683752C16F968F41DA24D1A /* [CP] Check Pods Manifest.lock */ = {
9DCCD5540BF35D589BD5B454 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n";
showEnvVarsInLog = 0;
};
A2A2D99B34926301B9B71068 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -709,7 +709,7 @@
};
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 94895B86D899ADED148B71DC /* Pods-Jellify.debug.xcconfig */;
baseConfigurationReference = 864F3550DF529DA8E784B2B9 /* Pods-Jellify.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
@@ -748,7 +748,7 @@
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 0ED95C4266474027C42A391B /* Pods-Jellify.release.xcconfig */;
baseConfigurationReference = 518045BFAF0062742ACB0F12 /* Pods-Jellify.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;

View File

@@ -9,12 +9,12 @@
"lint": "eslint .",
"start": "react-native start",
"test": "jest",
"pod:install": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=0 pod install",
"pod:install-new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 pod install",
"pod:install": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=0 bundle exec pod install",
"pod:install-new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install",
"pod:clean": "cd ios && pod deintegrate",
"fastlane:ios:build": "cd ios && fastlane build",
"fastlane:ios:beta": "cd ios && fastlane beta",
"fastlane:android:build": "cd android && bundle install && fastlane build"
"fastlane:ios:build": "cd ios && bundle exec fastlane build",
"fastlane:ios:beta": "cd ios && bundle exec fastlane beta",
"fastlane:android:build": "cd android && bundle install && bundle exec fastlane build"
},
"dependencies": {
"@jellyfin/sdk": "^0.11.0",