mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-22 16:54:42 +01:00
Compare commits
20 Commits
refactor/d
...
revert-377
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e73299429 | ||
|
|
293a9517a5 | ||
|
|
38b6215046 | ||
|
|
9692c173ae | ||
|
|
a297ac4843 | ||
|
|
8470cbe8d5 | ||
|
|
1e869a2c2f | ||
|
|
b6502c042a | ||
|
|
7f0446b85f | ||
|
|
11fbe19f80 | ||
|
|
5c97b85492 | ||
|
|
e60cec69f8 | ||
|
|
7bc1c22770 | ||
|
|
e86dab5613 | ||
|
|
eeb803223c | ||
|
|
1a43f7ef1b | ||
|
|
f4624bdc25 | ||
|
|
3c5f2b4079 | ||
|
|
955190a9cc | ||
|
|
e1e4f4833c |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.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
|
||||||
@@ -45,7 +43,7 @@ body:
|
|||||||
label: Version
|
label: Version
|
||||||
description: What version of Streamyfin are you running?
|
description: What version of Streamyfin are you running?
|
||||||
options:
|
options:
|
||||||
- 0.23.0
|
- 0.24.0
|
||||||
- 0.22.0
|
- 0.22.0
|
||||||
- 0.21.0
|
- 0.21.0
|
||||||
- older
|
- older
|
||||||
|
|||||||
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**
|
||||||
|
|||||||
49
.github/workflows/build-ios.yaml
vendored
Normal file
49
.github/workflows/build-ios.yaml
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
name: Automatic Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: macos-15
|
||||||
|
name: Build IOS
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
name: Check out repository
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
- run: |
|
||||||
|
bun i && bun run submodule-reload
|
||||||
|
npx expo prebuild
|
||||||
|
- uses: sparkfabrik/ios-build-action@v2.3.0
|
||||||
|
with:
|
||||||
|
upload-to-testflight: false
|
||||||
|
increment-build-number: false
|
||||||
|
build-pods: true
|
||||||
|
pods-path: "ios/Podfile"
|
||||||
|
configuration: Release
|
||||||
|
# Change later to app-store if wanted
|
||||||
|
export-method: appstore
|
||||||
|
#export-method: ad-hoc
|
||||||
|
workspace-path: "ios/Streamyfin.xcodeproj/project.xcworkspace/"
|
||||||
|
project-path: "ios/Streamyfin.xcodeproj"
|
||||||
|
scheme: Streamyfin
|
||||||
|
apple-key-id: ${{ secrets.APPLE_KEY_ID }}
|
||||||
|
apple-key-issuer-id: ${{ secrets.APPLE_KEY_ISSUER_ID }}
|
||||||
|
apple-key-content: ${{ secrets.APPLE_KEY_CONTENT }}
|
||||||
|
team-id: ${{ secrets.TEAM_ID }}
|
||||||
|
team-name: ${{ secrets.TEAM_NAME }}
|
||||||
|
#match-password: ${{ secrets.MATCH_PASSWORD }}
|
||||||
|
#match-git-url: ${{ secrets.MATCH_GIT_URL }}
|
||||||
|
#match-git-basic-authorization: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
|
||||||
|
#match-build-type: "appstore"
|
||||||
|
#browserstack-upload: true
|
||||||
|
#browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }}
|
||||||
|
#browserstack-access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
|
||||||
|
#fastlane-env: stage
|
||||||
|
ios-app-id: com.stetsed.teststreamyfin
|
||||||
|
output-path: build-${{ github.sha }}.ipa
|
||||||
@@ -13,7 +13,7 @@ Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Exp
|
|||||||
|
|
||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|
||||||
- 🚀 **Skp intro / credits support**
|
- 🚀 **Skip Intro / Credits Support**
|
||||||
- 🖼️ **Trickplay images**: The new golden standard for chapter previews when seeking.
|
- 🖼️ **Trickplay images**: The new golden standard for chapter previews when seeking.
|
||||||
- 🔊 **Background audio**: Stream music in the background, even when locking the phone.
|
- 🔊 **Background audio**: Stream music in the background, even when locking the phone.
|
||||||
- 📥 **Download media** (Experimental): Save your media locally and watch it offline.
|
- 📥 **Download media** (Experimental): Save your media locally and watch it offline.
|
||||||
|
|||||||
4
app.json
4
app.json
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Streamyfin",
|
"name": "Streamyfin",
|
||||||
"slug": "streamyfin",
|
"slug": "streamyfin",
|
||||||
"version": "0.23.0",
|
"version": "0.24.0",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"icon": "./assets/images/icon.png",
|
"icon": "./assets/images/icon.png",
|
||||||
"scheme": "streamyfin",
|
"scheme": "streamyfin",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"jsEngine": "hermes",
|
"jsEngine": "hermes",
|
||||||
"versionCode": 49,
|
"versionCode": 50,
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/images/adaptive_icon.png"
|
"foregroundImage": "./assets/images/adaptive_icon.png"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -435,7 +435,6 @@ export default function page() {
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
opacity: showControls ? (Platform.OS === "android" ? 0.7 : 0.5) : 1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<VlcPlayerView
|
<VlcPlayerView
|
||||||
|
|||||||
@@ -387,7 +387,6 @@ const Player = () => {
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
opacity: showControls ? 0.5 : 1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{videoSource ? (
|
{videoSource ? (
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { orientationAtom } from "@/utils/atoms/orientation";
|
|||||||
import { Settings, useSettings } from "@/utils/atoms/settings";
|
import { Settings, useSettings } from "@/utils/atoms/settings";
|
||||||
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
|
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
|
||||||
import { LogProvider, writeToLog } from "@/utils/log";
|
import { LogProvider, writeToLog } from "@/utils/log";
|
||||||
import { formatItemName, storage } from "@/utils/mmkv";
|
import { storage } from "@/utils/mmkv";
|
||||||
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
|
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
|
||||||
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
||||||
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
||||||
@@ -385,15 +385,18 @@ function Layout() {
|
|||||||
function saveDownloadedItemInfo(item: BaseItemDto) {
|
function saveDownloadedItemInfo(item: BaseItemDto) {
|
||||||
try {
|
try {
|
||||||
const downloadedItems = storage.getString("downloadedItems");
|
const downloadedItems = storage.getString("downloadedItems");
|
||||||
let items: { [key: string]: BaseItemDto } = downloadedItems
|
let items: BaseItemDto[] = downloadedItems
|
||||||
? JSON.parse(downloadedItems)
|
? JSON.parse(downloadedItems)
|
||||||
: {};
|
: [];
|
||||||
|
|
||||||
if (item.Id) {
|
const existingItemIndex = items.findIndex((i) => i.Id === item.Id);
|
||||||
item.Path = `${FileSystem.documentDirectory}${formatItemName(item)}.mp4`;
|
if (existingItemIndex !== -1) {
|
||||||
items[item.Id] = item;
|
items[existingItemIndex] = item;
|
||||||
storage.set("downloadedItems", JSON.stringify(items));
|
} else {
|
||||||
|
items.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storage.set("downloadedItems", JSON.stringify(items));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
writeToLog("ERROR", "Failed to save downloaded item information:", error);
|
writeToLog("ERROR", "Failed to save downloaded item information:", error);
|
||||||
console.error("Failed to save downloaded item information:", error);
|
console.error("Failed to save downloaded item information:", error);
|
||||||
|
|||||||
242
app/login.tsx
242
app/login.tsx
@@ -189,133 +189,129 @@ const Login: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (api?.basePath) {
|
|
||||||
return (
|
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
|
||||||
<KeyboardAvoidingView
|
|
||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
||||||
style={{ flex: 1, height: "100%" }}
|
|
||||||
>
|
|
||||||
<View className="flex flex-col h-full relative items-center justify-center">
|
|
||||||
<View className="px-4 -mt-20 w-full">
|
|
||||||
<View className="flex flex-col space-y-2">
|
|
||||||
<Text className="text-2xl font-bold -mb-2">
|
|
||||||
Log in
|
|
||||||
<>
|
|
||||||
{serverName ? (
|
|
||||||
<>
|
|
||||||
{" to "}
|
|
||||||
<Text className="text-purple-600">{serverName}</Text>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
</Text>
|
|
||||||
<Text className="text-xs text-neutral-400">{api.basePath}</Text>
|
|
||||||
<Input
|
|
||||||
placeholder="Username"
|
|
||||||
onChangeText={(text) =>
|
|
||||||
setCredentials({ ...credentials, username: text })
|
|
||||||
}
|
|
||||||
value={credentials.username}
|
|
||||||
autoFocus
|
|
||||||
secureTextEntry={false}
|
|
||||||
keyboardType="default"
|
|
||||||
returnKeyType="done"
|
|
||||||
autoCapitalize="none"
|
|
||||||
textContentType="username"
|
|
||||||
clearButtonMode="while-editing"
|
|
||||||
maxLength={500}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
className="mb-2"
|
|
||||||
placeholder="Password"
|
|
||||||
onChangeText={(text) =>
|
|
||||||
setCredentials({ ...credentials, password: text })
|
|
||||||
}
|
|
||||||
value={credentials.password}
|
|
||||||
secureTextEntry
|
|
||||||
keyboardType="default"
|
|
||||||
returnKeyType="done"
|
|
||||||
autoCapitalize="none"
|
|
||||||
textContentType="password"
|
|
||||||
clearButtonMode="while-editing"
|
|
||||||
maxLength={500}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text className="text-red-600 mb-2">{error}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="absolute bottom-0 left-0 w-full px-4 mb-2">
|
|
||||||
<Button
|
|
||||||
color="black"
|
|
||||||
onPress={handleQuickConnect}
|
|
||||||
className="w-full mb-2"
|
|
||||||
>
|
|
||||||
Use Quick Connect
|
|
||||||
</Button>
|
|
||||||
<Button onPress={handleLogin} loading={loading}>
|
|
||||||
Log in
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</KeyboardAvoidingView>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
<SafeAreaView style={{ flex: 1, paddingBottom: 16 }}>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
style={{ flex: 1, height: "100%" }}
|
|
||||||
>
|
>
|
||||||
<View className="flex flex-col h-full relative items-center justify-center w-full">
|
{api?.basePath ? (
|
||||||
<View className="flex flex-col gap-y-2 px-4 w-full -mt-36">
|
<>
|
||||||
<Image
|
<View className="flex flex-col h-full relative items-center justify-center">
|
||||||
style={{
|
<View className="px-4 -mt-20 w-full">
|
||||||
width: 100,
|
<View className="flex flex-col space-y-2">
|
||||||
height: 100,
|
<Text className="text-2xl font-bold -mb-2">
|
||||||
marginLeft: -23,
|
Log in
|
||||||
marginBottom: -20,
|
<>
|
||||||
}}
|
{serverName ? (
|
||||||
source={require("@/assets/images/StreamyFinFinal.png")}
|
<>
|
||||||
/>
|
{" to "}
|
||||||
<Text className="text-3xl font-bold">Streamyfin</Text>
|
<Text className="text-purple-600">{serverName}</Text>
|
||||||
<Text className="text-neutral-500">
|
</>
|
||||||
Enter the URL to your Jellyfin server
|
) : null}
|
||||||
</Text>
|
</>
|
||||||
<Input
|
</Text>
|
||||||
placeholder="Server URL"
|
<Text className="text-xs text-neutral-400">
|
||||||
onChangeText={setServerURL}
|
{api.basePath}
|
||||||
value={serverURL}
|
</Text>
|
||||||
keyboardType="url"
|
<Input
|
||||||
returnKeyType="done"
|
placeholder="Username"
|
||||||
autoCapitalize="none"
|
onChangeText={(text) =>
|
||||||
textContentType="URL"
|
setCredentials({ ...credentials, username: text })
|
||||||
maxLength={500}
|
}
|
||||||
/>
|
value={credentials.username}
|
||||||
<Text className="text-xs text-neutral-500 ml-4">
|
autoFocus
|
||||||
Make sure to include http or https
|
secureTextEntry={false}
|
||||||
</Text>
|
keyboardType="default"
|
||||||
<PreviousServersList
|
returnKeyType="done"
|
||||||
onServerSelect={(s) => {
|
autoCapitalize="none"
|
||||||
handleConnect(s.address);
|
textContentType="username"
|
||||||
}}
|
clearButtonMode="while-editing"
|
||||||
/>
|
maxLength={500}
|
||||||
</View>
|
/>
|
||||||
<View className="mb-2 absolute bottom-0 left-0 w-full px-4">
|
|
||||||
<Button
|
<Input
|
||||||
loading={loadingServerCheck}
|
className="mb-2"
|
||||||
disabled={loadingServerCheck}
|
placeholder="Password"
|
||||||
onPress={async () => await handleConnect(serverURL)}
|
onChangeText={(text) =>
|
||||||
className="w-full grow"
|
setCredentials({ ...credentials, password: text })
|
||||||
>
|
}
|
||||||
Connect
|
value={credentials.password}
|
||||||
</Button>
|
secureTextEntry
|
||||||
</View>
|
keyboardType="default"
|
||||||
</View>
|
returnKeyType="done"
|
||||||
|
autoCapitalize="none"
|
||||||
|
textContentType="password"
|
||||||
|
clearButtonMode="while-editing"
|
||||||
|
maxLength={500}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text className="text-red-600 mb-2">{error}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="absolute bottom-0 left-0 w-full px-4 mb-2">
|
||||||
|
<Button
|
||||||
|
color="black"
|
||||||
|
onPress={handleQuickConnect}
|
||||||
|
className="w-full mb-2"
|
||||||
|
>
|
||||||
|
Use Quick Connect
|
||||||
|
</Button>
|
||||||
|
<Button onPress={handleLogin} loading={loading}>
|
||||||
|
Log in
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<View className="flex flex-col h-full relative items-center justify-center w-full">
|
||||||
|
<View className="flex flex-col gap-y-2 px-4 w-full -mt-36">
|
||||||
|
<Image
|
||||||
|
style={{
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
marginLeft: -23,
|
||||||
|
marginBottom: -20,
|
||||||
|
}}
|
||||||
|
source={require("@/assets/images/StreamyFinFinal.png")}
|
||||||
|
/>
|
||||||
|
<Text className="text-3xl font-bold">Streamyfin</Text>
|
||||||
|
<Text className="text-neutral-500">
|
||||||
|
Enter the URL to your Jellyfin server
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
placeholder="Server URL"
|
||||||
|
onChangeText={setServerURL}
|
||||||
|
value={serverURL}
|
||||||
|
keyboardType="url"
|
||||||
|
returnKeyType="done"
|
||||||
|
autoCapitalize="none"
|
||||||
|
textContentType="URL"
|
||||||
|
maxLength={500}
|
||||||
|
/>
|
||||||
|
<Text className="text-xs text-neutral-500 ml-4">
|
||||||
|
Make sure to include http or https
|
||||||
|
</Text>
|
||||||
|
<PreviousServersList
|
||||||
|
onServerSelect={(s) => {
|
||||||
|
handleConnect(s.address);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View className="mb-2 absolute bottom-0 left-0 w-full px-4">
|
||||||
|
<Button
|
||||||
|
loading={loadingServerCheck}
|
||||||
|
disabled={loadingServerCheck}
|
||||||
|
onPress={async () => await handleConnect(serverURL)}
|
||||||
|
className="w-full grow"
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -497,33 +497,6 @@ export const Controls: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<VideoProvider
|
|
||||||
getAudioTracks={getAudioTracks}
|
|
||||||
getSubtitleTracks={getSubtitleTracks}
|
|
||||||
setAudioTrack={setAudioTrack}
|
|
||||||
setSubtitleTrack={setSubtitleTrack}
|
|
||||||
setSubtitleURL={setSubtitleURL}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
position: "absolute",
|
|
||||||
top: settings?.safeAreaInControlsEnabled ? insets.top : 0,
|
|
||||||
left: settings?.safeAreaInControlsEnabled ? insets.left : 0,
|
|
||||||
opacity: showControls ? 1 : 0,
|
|
||||||
zIndex: 1000,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
className={`flex flex-row items-center space-x-2 z-10 p-4 `}
|
|
||||||
>
|
|
||||||
{!mediaSource?.TranscodingUrl ? (
|
|
||||||
<DropdownViewDirect showControls={showControls} />
|
|
||||||
) : (
|
|
||||||
<DropdownViewTranscoding showControls={showControls} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</VideoProvider>
|
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPressIn={() => {
|
onPressIn={() => {
|
||||||
toggleControls();
|
toggleControls();
|
||||||
@@ -532,6 +505,8 @@ export const Controls: React.FC<Props> = ({
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
width: Dimensions.get("window").width,
|
width: Dimensions.get("window").width,
|
||||||
height: Dimensions.get("window").height,
|
height: Dimensions.get("window").height,
|
||||||
|
backgroundColor: "black",
|
||||||
|
opacity: showControls ? 0.5 : 0,
|
||||||
}}
|
}}
|
||||||
></Pressable>
|
></Pressable>
|
||||||
|
|
||||||
@@ -541,61 +516,82 @@ export const Controls: React.FC<Props> = ({
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: settings?.safeAreaInControlsEnabled ? insets.top : 0,
|
top: settings?.safeAreaInControlsEnabled ? insets.top : 0,
|
||||||
right: settings?.safeAreaInControlsEnabled ? insets.right : 0,
|
right: settings?.safeAreaInControlsEnabled ? insets.right : 0,
|
||||||
|
width: settings?.safeAreaInControlsEnabled
|
||||||
|
? Dimensions.get("window").width - insets.left - insets.right
|
||||||
|
: Dimensions.get("window").width,
|
||||||
opacity: showControls ? 1 : 0,
|
opacity: showControls ? 1 : 0,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
pointerEvents={showControls ? "auto" : "none"}
|
pointerEvents={showControls ? "auto" : "none"}
|
||||||
className={`flex flex-row items-center space-x-2 z-10 p-4 `}
|
className={`flex flex-row w-full p-4 `}
|
||||||
>
|
>
|
||||||
{item?.Type === "Episode" && !offline && (
|
<View className="mr-auto">
|
||||||
|
<VideoProvider
|
||||||
|
getAudioTracks={getAudioTracks}
|
||||||
|
getSubtitleTracks={getSubtitleTracks}
|
||||||
|
setAudioTrack={setAudioTrack}
|
||||||
|
setSubtitleTrack={setSubtitleTrack}
|
||||||
|
setSubtitleURL={setSubtitleURL}
|
||||||
|
>
|
||||||
|
{!mediaSource?.TranscodingUrl ? (
|
||||||
|
<DropdownViewDirect showControls={showControls} />
|
||||||
|
) : (
|
||||||
|
<DropdownViewTranscoding showControls={showControls} />
|
||||||
|
)}
|
||||||
|
</VideoProvider>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex flex-row items-center space-x-2 ">
|
||||||
|
{item?.Type === "Episode" && !offline && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => {
|
||||||
|
switchOnEpisodeMode();
|
||||||
|
}}
|
||||||
|
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
||||||
|
>
|
||||||
|
<Ionicons name="list" size={24} color="white" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
{previousItem && !offline && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={goToPreviousItem}
|
||||||
|
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
||||||
|
>
|
||||||
|
<Ionicons name="play-skip-back" size={24} color="white" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{nextItem && !offline && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={goToNextItem}
|
||||||
|
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
||||||
|
>
|
||||||
|
<Ionicons name="play-skip-forward" size={24} color="white" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mediaSource?.TranscodingUrl && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={toggleIgnoreSafeAreas}
|
||||||
|
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={ignoreSafeAreas ? "contract-outline" : "expand"}
|
||||||
|
size={24}
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={async () => {
|
||||||
switchOnEpisodeMode();
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
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"
|
||||||
>
|
>
|
||||||
<Ionicons name="list" size={24} color="white" />
|
<Ionicons name="close" size={24} color="white" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
</View>
|
||||||
{previousItem && !offline && (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={goToPreviousItem}
|
|
||||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
|
||||||
>
|
|
||||||
<Ionicons name="play-skip-back" size={24} color="white" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{nextItem && !offline && (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={goToNextItem}
|
|
||||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
|
||||||
>
|
|
||||||
<Ionicons name="play-skip-forward" size={24} color="white" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mediaSource?.TranscodingUrl && (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={toggleIgnoreSafeAreas}
|
|
||||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name={ignoreSafeAreas ? "contract-outline" : "expand"}
|
|
||||||
size={24}
|
|
||||||
color="white"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={async () => {
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
||||||
router.back();
|
|
||||||
}}
|
|
||||||
className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2"
|
|
||||||
>
|
|
||||||
<Ionicons name="close" size={24} color="white" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -118,14 +118,7 @@ const DropdownView: React.FC<DropdownViewProps> = ({ showControls }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View>
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
zIndex: 1000,
|
|
||||||
opacity: showControls ? 1 : 0,
|
|
||||||
}}
|
|
||||||
className="p-4"
|
|
||||||
>
|
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<TouchableOpacity className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2">
|
<TouchableOpacity className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2">
|
||||||
|
|||||||
4
eas.json
4
eas.json
@@ -22,13 +22,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"channel": "0.23.0",
|
"channel": "0.24.0",
|
||||||
"android": {
|
"android": {
|
||||||
"image": "latest"
|
"image": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production-apk": {
|
"production-apk": {
|
||||||
"channel": "0.23.0",
|
"channel": "0.24.0",
|
||||||
"android": {
|
"android": {
|
||||||
"buildType": "apk",
|
"buildType": "apk",
|
||||||
"image": "latest"
|
"image": "latest"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
|
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import { getFilePathFromItemId } from "@/utils/mmkv";
|
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
import * as FileSystem from "expo-file-system";
|
import * as FileSystem from "expo-file-system";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
@@ -18,13 +17,13 @@ export const getDownloadedFileUrl = async (itemId: string): Promise<string> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const files = await FileSystem.readDirectoryAsync(directory);
|
const files = await FileSystem.readDirectoryAsync(directory);
|
||||||
const filePath = getFilePathFromItemId(itemId);
|
const path = itemId!;
|
||||||
|
const matchingFile = files.find((file) => file.startsWith(path));
|
||||||
const matchingFile = files.find((file) => file === filePath);
|
|
||||||
|
|
||||||
if (!matchingFile) {
|
if (!matchingFile) {
|
||||||
throw new Error(`No file found for item ${filePath}`);
|
throw new Error(`No file found for item ${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${directory}${matchingFile}`;
|
return `${directory}${matchingFile}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import useDownloadHelper from "@/utils/download";
|
|||||||
import { Api } from "@jellyfin/sdk";
|
import { Api } from "@jellyfin/sdk";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { JobStatus } from "@/utils/optimize-server";
|
import { JobStatus } from "@/utils/optimize-server";
|
||||||
import { formatItemName } from "@/utils/mmkv";
|
|
||||||
|
|
||||||
const createFFmpegCommand = (url: string, output: string) => [
|
const createFFmpegCommand = (url: string, output: string) => [
|
||||||
"-y", // overwrite output files without asking
|
"-y", // overwrite output files without asking
|
||||||
@@ -54,12 +53,7 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const { saveImage } = useImageStorage();
|
const { saveImage } = useImageStorage();
|
||||||
const { saveSeriesPrimaryImage } = useDownloadHelper();
|
const { saveSeriesPrimaryImage } = useDownloadHelper();
|
||||||
const {
|
const { saveDownloadedItemInfo, setProcesses, processes, APP_CACHE_DOWNLOAD_DIRECTORY } = useDownload();
|
||||||
saveDownloadedItemInfo,
|
|
||||||
setProcesses,
|
|
||||||
processes,
|
|
||||||
APP_CACHE_DOWNLOAD_DIRECTORY,
|
|
||||||
} = useDownload();
|
|
||||||
|
|
||||||
const onSaveAssets = async (api: Api, item: BaseItemDto) => {
|
const onSaveAssets = async (api: Api, item: BaseItemDto) => {
|
||||||
await saveSeriesPrimaryImage(item);
|
await saveSeriesPrimaryImage(item);
|
||||||
@@ -79,12 +73,13 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
try {
|
try {
|
||||||
console.log("completeCallback");
|
console.log("completeCallback");
|
||||||
const returnCode = await session.getReturnCode();
|
const returnCode = await session.getReturnCode();
|
||||||
|
|
||||||
if (returnCode.isValueSuccess()) {
|
if (returnCode.isValueSuccess()) {
|
||||||
const stat = await session.getLastReceivedStatistics();
|
const stat = await session.getLastReceivedStatistics();
|
||||||
await FileSystem.moveAsync({
|
await FileSystem.moveAsync({
|
||||||
from: `${APP_CACHE_DOWNLOAD_DIRECTORY}${item.Id}.mp4`,
|
from: `${APP_CACHE_DOWNLOAD_DIRECTORY}${item.Id}.mp4`,
|
||||||
to: `${FileSystem.documentDirectory}${formatItemName(item)}.mp4`,
|
to: `${FileSystem.documentDirectory}${item.Id}.mp4`
|
||||||
});
|
})
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ["downloadedItems"],
|
queryKey: ["downloadedItems"],
|
||||||
});
|
});
|
||||||
@@ -136,16 +131,12 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
|
|
||||||
const startRemuxing = useCallback(
|
const startRemuxing = useCallback(
|
||||||
async (item: BaseItemDto, url: string, mediaSource: MediaSourceInfo) => {
|
async (item: BaseItemDto, url: string, mediaSource: MediaSourceInfo) => {
|
||||||
const cacheDir = await FileSystem.getInfoAsync(
|
const cacheDir = await FileSystem.getInfoAsync(APP_CACHE_DOWNLOAD_DIRECTORY);
|
||||||
APP_CACHE_DOWNLOAD_DIRECTORY
|
|
||||||
);
|
|
||||||
if (!cacheDir.exists) {
|
if (!cacheDir.exists) {
|
||||||
await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, {
|
await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, {intermediates: true})
|
||||||
intermediates: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const output = APP_CACHE_DOWNLOAD_DIRECTORY + `${item.Id}.mp4`;
|
const output = APP_CACHE_DOWNLOAD_DIRECTORY + `${item.Id}.mp4`
|
||||||
|
|
||||||
if (!api) throw new Error("API is not defined");
|
if (!api) throw new Error("API is not defined");
|
||||||
if (!item.Id) throw new Error("Item must have an Id");
|
if (!item.Id) throw new Error("Item must have an Id");
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
"react-native-circular-progress": "^1.4.1",
|
"react-native-circular-progress": "^1.4.1",
|
||||||
"react-native-compressor": "^1.9.0",
|
"react-native-compressor": "^1.9.0",
|
||||||
"react-native-device-info": "^14.0.1",
|
"react-native-device-info": "^14.0.1",
|
||||||
"react-native-edge-to-edge": "^1.1.1",
|
"react-native-edge-to-edge": "^1.1.3",
|
||||||
"react-native-gesture-handler": "~2.16.1",
|
"react-native-gesture-handler": "~2.16.1",
|
||||||
"react-native-get-random-values": "^1.11.0",
|
"react-native-get-random-values": "^1.11.0",
|
||||||
"react-native-google-cast": "^4.8.3",
|
"react-native-google-cast": "^4.8.3",
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ import {
|
|||||||
download,
|
download,
|
||||||
setConfig,
|
setConfig,
|
||||||
} from "@kesha-antonov/react-native-background-downloader";
|
} from "@kesha-antonov/react-native-background-downloader";
|
||||||
import { focusManager, useQuery, useQueryClient } from "@tanstack/react-query";
|
import MMKV from "react-native-mmkv";
|
||||||
|
import {
|
||||||
|
focusManager,
|
||||||
|
QueryClient,
|
||||||
|
QueryClientProvider,
|
||||||
|
useQuery,
|
||||||
|
useQueryClient,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import * as FileSystem from "expo-file-system";
|
import * as FileSystem from "expo-file-system";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
@@ -38,7 +45,7 @@ import { apiAtom } from "./JellyfinProvider";
|
|||||||
import * as Notifications from "expo-notifications";
|
import * as Notifications from "expo-notifications";
|
||||||
import { getItemImage } from "@/utils/getItemImage";
|
import { getItemImage } from "@/utils/getItemImage";
|
||||||
import useImageStorage from "@/hooks/useImageStorage";
|
import useImageStorage from "@/hooks/useImageStorage";
|
||||||
import { formatItemName, saveItemMapping, 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 * as Haptics from "expo-haptics";
|
||||||
@@ -47,11 +54,8 @@ import * as Application from "expo-application";
|
|||||||
export type DownloadedItem = {
|
export type DownloadedItem = {
|
||||||
item: Partial<BaseItemDto>;
|
item: Partial<BaseItemDto>;
|
||||||
mediaSource: MediaSourceInfo;
|
mediaSource: MediaSourceInfo;
|
||||||
fileSize: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DownloadedItem2 = {};
|
|
||||||
|
|
||||||
export const processesAtom = atom<JobStatus[]>([]);
|
export const processesAtom = atom<JobStatus[]>([]);
|
||||||
|
|
||||||
function onAppStateChange(status: AppStateStatus) {
|
function onAppStateChange(status: AppStateStatus) {
|
||||||
@@ -508,10 +512,8 @@ function useDownloadProvider() {
|
|||||||
|
|
||||||
const downloadedItems = storage.getString("downloadedItems");
|
const downloadedItems = storage.getString("downloadedItems");
|
||||||
if (downloadedItems) {
|
if (downloadedItems) {
|
||||||
let items: { [key: string]: BaseItemDto } = downloadedItems
|
let items = JSON.parse(downloadedItems) as DownloadedItem[];
|
||||||
? JSON.parse(downloadedItems)
|
items = items.filter((item) => item.item.Id !== id);
|
||||||
: {};
|
|
||||||
delete items[id];
|
|
||||||
storage.set("downloadedItems", JSON.stringify(items));
|
storage.set("downloadedItems", JSON.stringify(items));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +586,7 @@ function useDownloadProvider() {
|
|||||||
const appSizeUsage = useMemo(async () => {
|
const appSizeUsage = useMemo(async () => {
|
||||||
const sizes: number[] =
|
const sizes: number[] =
|
||||||
downloadedFiles?.map((d) => {
|
downloadedFiles?.map((d) => {
|
||||||
return d.fileSize;
|
return getDownloadedItemSize(d.item.Id!!);
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
await forEveryDocumentDirFile(
|
await forEveryDocumentDirFile(
|
||||||
@@ -606,10 +608,8 @@ function useDownloadProvider() {
|
|||||||
try {
|
try {
|
||||||
const downloadedItems = storage.getString("downloadedItems");
|
const downloadedItems = storage.getString("downloadedItems");
|
||||||
if (downloadedItems) {
|
if (downloadedItems) {
|
||||||
const items: { [key: string]: BaseItemDto } = downloadedItems
|
const items: DownloadedItem[] = JSON.parse(downloadedItems);
|
||||||
? JSON.parse(downloadedItems)
|
const item = items.find((i) => i.item.Id === itemId);
|
||||||
: {};
|
|
||||||
const item = items[itemId] as DownloadedItem;
|
|
||||||
return item || null;
|
return item || null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -623,7 +623,7 @@ function useDownloadProvider() {
|
|||||||
try {
|
try {
|
||||||
const downloadedItems = storage.getString("downloadedItems");
|
const downloadedItems = storage.getString("downloadedItems");
|
||||||
if (downloadedItems) {
|
if (downloadedItems) {
|
||||||
return Object.values(JSON.parse(downloadedItems)) as DownloadedItem[];
|
return JSON.parse(downloadedItems) as DownloadedItem[];
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -636,11 +636,11 @@ function useDownloadProvider() {
|
|||||||
function saveDownloadedItemInfo(item: BaseItemDto, size: number = 0) {
|
function saveDownloadedItemInfo(item: BaseItemDto, size: number = 0) {
|
||||||
try {
|
try {
|
||||||
const downloadedItems = storage.getString("downloadedItems");
|
const downloadedItems = storage.getString("downloadedItems");
|
||||||
const items: { [key: string]: BaseItemDto } = downloadedItems
|
let items: DownloadedItem[] = downloadedItems
|
||||||
? JSON.parse(downloadedItems)
|
? JSON.parse(downloadedItems)
|
||||||
: {};
|
: [];
|
||||||
|
|
||||||
const chosenItem = items[item.Id!] || {};
|
const existingItemIndex = items.findIndex((i) => i.item.Id === item.Id);
|
||||||
|
|
||||||
const data = getDownloadItemInfoFromDiskTmp(item.Id!);
|
const data = getDownloadItemInfoFromDiskTmp(item.Id!);
|
||||||
|
|
||||||
@@ -651,6 +651,12 @@ function useDownloadProvider() {
|
|||||||
|
|
||||||
const newItem = { item, mediaSource: data.mediaSource };
|
const newItem = { item, mediaSource: data.mediaSource };
|
||||||
|
|
||||||
|
if (existingItemIndex !== -1) {
|
||||||
|
items[existingItemIndex] = newItem;
|
||||||
|
} else {
|
||||||
|
items.push(newItem);
|
||||||
|
}
|
||||||
|
|
||||||
deleteDownloadItemInfoFromDiskTmp(item.Id!);
|
deleteDownloadItemInfoFromDiskTmp(item.Id!);
|
||||||
|
|
||||||
storage.set("downloadedItems", JSON.stringify(items));
|
storage.set("downloadedItems", JSON.stringify(items));
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
setJellyfin(
|
setJellyfin(
|
||||||
() =>
|
() =>
|
||||||
new Jellyfin({
|
new Jellyfin({
|
||||||
clientInfo: { name: "Streamyfin", version: "0.23.0" },
|
clientInfo: { name: "Streamyfin", version: "0.24.0" },
|
||||||
deviceInfo: {
|
deviceInfo: {
|
||||||
name: deviceName,
|
name: deviceName,
|
||||||
id,
|
id,
|
||||||
@@ -92,7 +92,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
return {
|
return {
|
||||||
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
authorization: `MediaBrowser Client="Streamyfin", Device=${
|
||||||
Platform.OS === "android" ? "Android" : "iOS"
|
Platform.OS === "android" ? "Android" : "iOS"
|
||||||
}, DeviceId="${deviceId}", Version="0.23.0"`,
|
}, DeviceId="${deviceId}", Version="0.24.0"`,
|
||||||
};
|
};
|
||||||
}, [deviceId]);
|
}, [deviceId]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,3 @@
|
|||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
|
||||||
import { MMKV } from "react-native-mmkv";
|
import { MMKV } from "react-native-mmkv";
|
||||||
|
|
||||||
const storage = new MMKV();
|
export const storage = new MMKV();
|
||||||
|
|
||||||
const saveItemMapping = (itemId: string | undefined, fileName: string) => {
|
|
||||||
if (!itemId) return;
|
|
||||||
storage.set(itemId, fileName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilePathFromItemId = (itemId: string): string | undefined => {
|
|
||||||
return storage.getString(itemId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatItemName = (item: BaseItemDto) => {
|
|
||||||
if (item.Type === "Episode") {
|
|
||||||
const formattedParentIndexNumber = (item.ParentIndexNumber ?? 0)
|
|
||||||
.toString()
|
|
||||||
.padStart(2, "0");
|
|
||||||
const formattedIndexNumber = (item.IndexNumber ?? 0)
|
|
||||||
.toString()
|
|
||||||
.padStart(2, "0");
|
|
||||||
|
|
||||||
const formattedString = `S${formattedParentIndexNumber}E${formattedIndexNumber}`;
|
|
||||||
return `${item.SeriesName} - ${formattedString} - ${item.Name}`;
|
|
||||||
}
|
|
||||||
return item.Name;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { saveItemMapping, getFilePathFromItemId, storage, formatItemName };
|
|
||||||
|
|||||||
Reference in New Issue
Block a user