mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-17 18:03:01 +01:00
Compare commits
2 Commits
chore/expo
...
refactor/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad751fd2c8 | ||
|
|
42e66b39cc |
3
app.json
3
app.json
@@ -130,7 +130,6 @@
|
|||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
"url": "https://u.expo.dev/e79219d1-797f-4fbe-9fa1-cfd360690a68"
|
"url": "https://u.expo.dev/e79219d1-797f-4fbe-9fa1-cfd360690a68"
|
||||||
},
|
}
|
||||||
"newArchEnabled": false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
app/(auth)/trailer/page.tsx
Normal file
45
app/(auth)/trailer/page.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useGlobalSearchParams } from "expo-router";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Alert, Dimensions, View } from "react-native";
|
||||||
|
import YoutubePlayer, { PLAYER_STATES } from "react-native-youtube-iframe";
|
||||||
|
|
||||||
|
export default function page() {
|
||||||
|
const searchParams = useGlobalSearchParams();
|
||||||
|
|
||||||
|
const { url } = searchParams as { url: string };
|
||||||
|
|
||||||
|
const videoId = useMemo(() => {
|
||||||
|
return url.split("v=")[1];
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
const [playing, setPlaying] = useState(false);
|
||||||
|
|
||||||
|
const onStateChange = useCallback((state: PLAYER_STATES) => {
|
||||||
|
if (state === "ended") {
|
||||||
|
setPlaying(false);
|
||||||
|
Alert.alert("video has finished playing!");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const togglePlaying = useCallback(() => {
|
||||||
|
setPlaying((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
togglePlaying();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const screenWidth = Dimensions.get("screen").width;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex flex-col bg-black items-center justify-center h-full">
|
||||||
|
<YoutubePlayer
|
||||||
|
height={300}
|
||||||
|
play={playing}
|
||||||
|
videoId={videoId}
|
||||||
|
onChangeState={onStateChange}
|
||||||
|
width={screenWidth}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 { storage } from "@/utils/mmkv";
|
import { formatItemName, 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";
|
||||||
@@ -319,7 +319,7 @@ function Layout() {
|
|||||||
<BottomSheetModalProvider>
|
<BottomSheetModalProvider>
|
||||||
<SystemBars style="light" hidden={false} />
|
<SystemBars style="light" hidden={false} />
|
||||||
<ThemeProvider value={DarkTheme}>
|
<ThemeProvider value={DarkTheme}>
|
||||||
<Stack>
|
<Stack initialRouteName="/home">
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="(auth)/(tabs)"
|
name="(auth)/(tabs)"
|
||||||
options={{
|
options={{
|
||||||
@@ -336,6 +336,14 @@ function Layout() {
|
|||||||
header: () => null,
|
header: () => null,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="(auth)/trailer/page"
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
presentation: "modal",
|
||||||
|
title: "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="login"
|
name="login"
|
||||||
options={{
|
options={{
|
||||||
@@ -377,18 +385,15 @@ function Layout() {
|
|||||||
function saveDownloadedItemInfo(item: BaseItemDto) {
|
function saveDownloadedItemInfo(item: BaseItemDto) {
|
||||||
try {
|
try {
|
||||||
const downloadedItems = storage.getString("downloadedItems");
|
const downloadedItems = storage.getString("downloadedItems");
|
||||||
let items: BaseItemDto[] = downloadedItems
|
let items: { [key: string]: BaseItemDto } = downloadedItems
|
||||||
? JSON.parse(downloadedItems)
|
? JSON.parse(downloadedItems)
|
||||||
: [];
|
: {};
|
||||||
|
|
||||||
const existingItemIndex = items.findIndex((i) => i.Id === item.Id);
|
if (item.Id) {
|
||||||
if (existingItemIndex !== -1) {
|
item.Path = `${FileSystem.documentDirectory}${formatItemName(item)}.mp4`;
|
||||||
items[existingItemIndex] = item;
|
items[item.Id] = item;
|
||||||
} else {
|
storage.set("downloadedItems", JSON.stringify(items));
|
||||||
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);
|
||||||
|
|||||||
10
components/__tests__/ThemedText-test.tsx
Normal file
10
components/__tests__/ThemedText-test.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
|
import { ThemedText } from '../ThemedText';
|
||||||
|
|
||||||
|
it(`renders correctly`, () => {
|
||||||
|
const tree = renderer.create(<ThemedText>Snapshot test!</ThemedText>).toJSON();
|
||||||
|
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
24
components/__tests__/__snapshots__/ThemedText-test.tsx.snap
Normal file
24
components/__tests__/__snapshots__/ThemedText-test.tsx.snap
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`renders correctly 1`] = `
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#11181C",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fontSize": 16,
|
||||||
|
"lineHeight": 24,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Snapshot test!
|
||||||
|
</Text>
|
||||||
|
`;
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
import {useRouter, useSegments} from "expo-router";
|
||||||
import {
|
import React, {PropsWithChildren, useCallback, useMemo} from "react";
|
||||||
hasPermission,
|
import {TouchableOpacity, TouchableOpacityProps} from "react-native";
|
||||||
Permission,
|
import * as ContextMenu from "zeego/context-menu";
|
||||||
} from "@/utils/jellyseerr/server/lib/permissions";
|
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
||||||
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
import {useJellyseerr} from "@/hooks/useJellyseerr";
|
||||||
import { useRouter, useSegments } from "expo-router";
|
import {hasPermission, Permission} from "@/utils/jellyseerr/server/lib/permissions";
|
||||||
import React, { PropsWithChildren, useCallback, useMemo } from "react";
|
import {MediaType} from "@/utils/jellyseerr/server/constants/media";
|
||||||
import { TouchableOpacity, TouchableOpacityProps } from "react-native";
|
|
||||||
|
|
||||||
interface Props extends TouchableOpacityProps {
|
interface Props extends TouchableOpacityProps {
|
||||||
result: MovieResult | TvResult;
|
result: MovieResult | TvResult;
|
||||||
@@ -27,49 +26,78 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const segments = useSegments();
|
const segments = useSegments();
|
||||||
const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr();
|
const {jellyseerrApi, jellyseerrUser, requestMedia} = useJellyseerr()
|
||||||
|
|
||||||
const from = segments[2];
|
const from = segments[2];
|
||||||
|
|
||||||
const autoApprove = useMemo(() => {
|
const autoApprove = useMemo(() => {
|
||||||
return (
|
return jellyseerrUser && hasPermission(
|
||||||
jellyseerrUser &&
|
Permission.AUTO_APPROVE,
|
||||||
hasPermission(Permission.AUTO_APPROVE, jellyseerrUser.permissions, {
|
jellyseerrUser.permissions,
|
||||||
type: "or",
|
{type: 'or'}
|
||||||
})
|
)
|
||||||
);
|
}, [jellyseerrApi, jellyseerrUser])
|
||||||
}, [jellyseerrApi, jellyseerrUser]);
|
|
||||||
|
|
||||||
const request = useCallback(
|
const request = useCallback(() =>
|
||||||
() =>
|
|
||||||
requestMedia(mediaTitle, {
|
requestMedia(mediaTitle, {
|
||||||
mediaId: result.id,
|
mediaId: result.id,
|
||||||
mediaType: result.mediaType,
|
mediaType: result.mediaType
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
[jellyseerrApi, result]
|
[jellyseerrApi, result]
|
||||||
);
|
)
|
||||||
|
|
||||||
if (from === "(home)" || from === "(search)" || from === "(libraries)")
|
if (from === "(home)" || from === "(search)" || from === "(libraries)")
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TouchableOpacity
|
<ContextMenu.Root>
|
||||||
onPress={() => {
|
<ContextMenu.Trigger>
|
||||||
router.push({
|
<TouchableOpacity
|
||||||
pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
|
onPress={() => {
|
||||||
params: {
|
// @ts-ignore
|
||||||
...result,
|
router.push({pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`, params: {...result, mediaTitle, releaseYear, canRequest, posterSrc}});
|
||||||
mediaTitle,
|
}}
|
||||||
releaseYear,
|
{...props}
|
||||||
// @ts-expect-error
|
>
|
||||||
canRequest,
|
{children}
|
||||||
posterSrc,
|
</TouchableOpacity>
|
||||||
},
|
</ContextMenu.Trigger>
|
||||||
});
|
<ContextMenu.Content
|
||||||
}}
|
avoidCollisions
|
||||||
{...props}
|
alignOffset={0}
|
||||||
>
|
collisionPadding={0}
|
||||||
{children}
|
loop={false}
|
||||||
</TouchableOpacity>
|
key={"content"}
|
||||||
|
>
|
||||||
|
<ContextMenu.Label key="label-1">Actions</ContextMenu.Label>
|
||||||
|
{canRequest && result.mediaType === MediaType.MOVIE && (
|
||||||
|
<ContextMenu.Item
|
||||||
|
key="item-1"
|
||||||
|
onSelect={() => {
|
||||||
|
if (autoApprove) {
|
||||||
|
request()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
shouldDismissMenuOnSelect
|
||||||
|
>
|
||||||
|
<ContextMenu.ItemTitle key="item-1-title">Request</ContextMenu.ItemTitle>
|
||||||
|
<ContextMenu.ItemIcon
|
||||||
|
ios={{
|
||||||
|
name: "arrow.down.to.line",
|
||||||
|
pointSize: 18,
|
||||||
|
weight: "semibold",
|
||||||
|
scale: "medium",
|
||||||
|
hierarchicalColor: {
|
||||||
|
dark: "purple",
|
||||||
|
light: "purple",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
androidIconName="download"
|
||||||
|
/>
|
||||||
|
</ContextMenu.Item>
|
||||||
|
)}
|
||||||
|
</ContextMenu.Content>
|
||||||
|
</ContextMenu.Root>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
import { useRouter, useSegments } from "expo-router";
|
import { useRouter, useSegments } from "expo-router";
|
||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren } from "react";
|
||||||
import { TouchableOpacity, TouchableOpacityProps } from "react-native";
|
import { TouchableOpacity, TouchableOpacityProps } from "react-native";
|
||||||
|
import * as ContextMenu from "zeego/context-menu";
|
||||||
|
|
||||||
interface Props extends TouchableOpacityProps {
|
interface Props extends TouchableOpacityProps {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
@@ -15,6 +16,8 @@ export const itemRouter = (
|
|||||||
item: BaseItemDto | BaseItemPerson,
|
item: BaseItemDto | BaseItemPerson,
|
||||||
from: string
|
from: string
|
||||||
) => {
|
) => {
|
||||||
|
console.log(item.Type, item?.CollectionType);
|
||||||
|
|
||||||
if ("CollectionType" in item && item.CollectionType === "livetv") {
|
if ("CollectionType" in item && item.CollectionType === "livetv") {
|
||||||
return `/(auth)/(tabs)/${from}/livetv`;
|
return `/(auth)/(tabs)/${from}/livetv`;
|
||||||
}
|
}
|
||||||
@@ -77,15 +80,78 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
from === "(favorites)"
|
from === "(favorites)"
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<ContextMenu.Root>
|
||||||
onPress={() => {
|
<ContextMenu.Trigger>
|
||||||
const url = itemRouter(item, from);
|
<TouchableOpacity
|
||||||
// @ts-ignore
|
onPress={() => {
|
||||||
router.push(url);
|
const url = itemRouter(item, from);
|
||||||
}}
|
// @ts-ignore
|
||||||
{...props}
|
router.push(url);
|
||||||
>
|
}}
|
||||||
{children}
|
{...props}
|
||||||
</TouchableOpacity>
|
>
|
||||||
|
{children}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ContextMenu.Trigger>
|
||||||
|
<ContextMenu.Content
|
||||||
|
avoidCollisions
|
||||||
|
alignOffset={0}
|
||||||
|
collisionPadding={0}
|
||||||
|
loop={false}
|
||||||
|
key={"content"}
|
||||||
|
>
|
||||||
|
<ContextMenu.Label key="label-1">Actions</ContextMenu.Label>
|
||||||
|
<ContextMenu.Item
|
||||||
|
key="item-1"
|
||||||
|
onSelect={() => {
|
||||||
|
markAsPlayedStatus(true);
|
||||||
|
}}
|
||||||
|
shouldDismissMenuOnSelect
|
||||||
|
>
|
||||||
|
<ContextMenu.ItemTitle key="item-1-title">
|
||||||
|
Mark as watched
|
||||||
|
</ContextMenu.ItemTitle>
|
||||||
|
<ContextMenu.ItemIcon
|
||||||
|
ios={{
|
||||||
|
name: "checkmark.circle", // Changed to "checkmark.circle" which represents "watched"
|
||||||
|
pointSize: 18,
|
||||||
|
weight: "semibold",
|
||||||
|
scale: "medium",
|
||||||
|
hierarchicalColor: {
|
||||||
|
dark: "green", // Changed to green for "watched"
|
||||||
|
light: "green",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
androidIconName="checkmark-circle"
|
||||||
|
></ContextMenu.ItemIcon>
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item
|
||||||
|
key="item-2"
|
||||||
|
onSelect={() => {
|
||||||
|
markAsPlayedStatus(false);
|
||||||
|
}}
|
||||||
|
shouldDismissMenuOnSelect
|
||||||
|
destructive
|
||||||
|
>
|
||||||
|
<ContextMenu.ItemTitle key="item-2-title">
|
||||||
|
Mark as not watched
|
||||||
|
</ContextMenu.ItemTitle>
|
||||||
|
<ContextMenu.ItemIcon
|
||||||
|
ios={{
|
||||||
|
name: "eye.slash", // Changed to "eye.slash" which represents "not watched"
|
||||||
|
pointSize: 18, // Adjusted for better visibility
|
||||||
|
weight: "semibold",
|
||||||
|
scale: "medium",
|
||||||
|
hierarchicalColor: {
|
||||||
|
dark: "red", // Changed to red for "not watched"
|
||||||
|
light: "red",
|
||||||
|
},
|
||||||
|
// Removed paletteColors as it's not necessary in this case
|
||||||
|
}}
|
||||||
|
androidIconName="eye-slash"
|
||||||
|
></ContextMenu.ItemIcon>
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.Content>
|
||||||
|
</ContextMenu.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
import { View } from "react-native";
|
import { userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { Text } from "../common/Text";
|
|
||||||
import { useCallback, useRef, useState } from "react";
|
|
||||||
import { Input } from "../common/Input";
|
|
||||||
import { ListItem } from "../list/ListItem";
|
|
||||||
import { Loader } from "../Loader";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { Button } from "../Button";
|
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { toast } from "sonner-native";
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import { toast } from "sonner-native";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
import { Input } from "../common/Input";
|
||||||
|
import { Text } from "../common/Text";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
export const JellyseerrSettings = () => {
|
export const JellyseerrSettings = () => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,59 +1,113 @@
|
|||||||
import { Alert, View, ViewProps } from "react-native";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { Text } from "../common/Text";
|
import {
|
||||||
import { ListItem } from "../list/ListItem";
|
BottomSheetBackdrop,
|
||||||
import { Button } from "../Button";
|
BottomSheetBackdropProps,
|
||||||
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
BottomSheetModal,
|
||||||
import { useAtom } from "jotai";
|
BottomSheetTextInput,
|
||||||
import Constants from "expo-constants";
|
BottomSheetView,
|
||||||
import Application from "expo-application";
|
} from "@gorhom/bottom-sheet";
|
||||||
import { ListGroup } from "../list/ListGroup";
|
|
||||||
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import * as Haptics from "expo-haptics";
|
import * as Haptics from "expo-haptics";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import React, { useCallback, useRef, useState } from "react";
|
||||||
|
import { Alert, View, ViewProps } from "react-native";
|
||||||
|
import { Button } from "../Button";
|
||||||
|
import { Text } from "../common/Text";
|
||||||
|
import { ListGroup } from "../list/ListGroup";
|
||||||
|
import { ListItem } from "../list/ListItem";
|
||||||
|
|
||||||
interface Props extends ViewProps {}
|
interface Props extends ViewProps {}
|
||||||
|
|
||||||
export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
export const QuickConnect: React.FC<Props> = ({ ...props }) => {
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
|
const [quickConnectCode, setQuickConnectCode] = useState<string>();
|
||||||
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||||
|
|
||||||
const openQuickConnectAuthCodeInput = () => {
|
const renderBackdrop = useCallback(
|
||||||
Alert.prompt(
|
(props: BottomSheetBackdropProps) => (
|
||||||
"Quick connect",
|
<BottomSheetBackdrop
|
||||||
"Enter the quick connect code",
|
{...props}
|
||||||
async (text) => {
|
disappearsOnIndex={-1}
|
||||||
if (text) {
|
appearsOnIndex={0}
|
||||||
try {
|
/>
|
||||||
const res = await getQuickConnectApi(api!).authorizeQuickConnect({
|
),
|
||||||
code: text,
|
[]
|
||||||
userId: user?.Id,
|
);
|
||||||
});
|
|
||||||
if (res.status === 200) {
|
const authorizeQuickConnect = useCallback(async () => {
|
||||||
Haptics.notificationAsync(
|
if (quickConnectCode) {
|
||||||
Haptics.NotificationFeedbackType.Success
|
try {
|
||||||
);
|
const res = await getQuickConnectApi(api!).authorizeQuickConnect({
|
||||||
Alert.alert("Success", "Quick connect authorized");
|
code: quickConnectCode,
|
||||||
} else {
|
userId: user?.Id,
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
});
|
||||||
Alert.alert("Error", "Invalid code");
|
if (res.status === 200) {
|
||||||
}
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||||
} catch (e) {
|
Alert.alert("Success", "Quick connect authorized");
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
setQuickConnectCode(undefined);
|
||||||
Alert.alert("Error", "Invalid code");
|
bottomSheetModalRef?.current?.close();
|
||||||
}
|
} else {
|
||||||
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||||
|
Alert.alert("Error", "Invalid code");
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||||
|
Alert.alert("Error", "Invalid code");
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
};
|
}, [api, user, quickConnectCode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...props}>
|
<View {...props}>
|
||||||
<ListGroup title={"Quick Connect"}>
|
<ListGroup title={"Quick Connect"}>
|
||||||
<ListItem
|
<ListItem
|
||||||
onPress={openQuickConnectAuthCodeInput}
|
onPress={() => bottomSheetModalRef?.current?.present()}
|
||||||
title="Authorize Quick Connect"
|
title="Authorize Quick Connect"
|
||||||
textColor="blue"
|
textColor="blue"
|
||||||
></ListItem>
|
/>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
|
||||||
|
<BottomSheetModal
|
||||||
|
ref={bottomSheetModalRef}
|
||||||
|
enableDynamicSizing
|
||||||
|
handleIndicatorStyle={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
}}
|
||||||
|
backgroundStyle={{
|
||||||
|
backgroundColor: "#171717",
|
||||||
|
}}
|
||||||
|
backdropComponent={renderBackdrop}
|
||||||
|
>
|
||||||
|
<BottomSheetView>
|
||||||
|
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
||||||
|
<View>
|
||||||
|
<Text className="font-bold text-2xl text-neutral-100">
|
||||||
|
Quick Connect
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View className="flex flex-col space-y-2">
|
||||||
|
<View className="p-4 border border-neutral-800 rounded-xl bg-neutral-900 w-full">
|
||||||
|
<BottomSheetTextInput
|
||||||
|
style={{ color: "white" }}
|
||||||
|
clearButtonMode="always"
|
||||||
|
placeholder="Enter the quick connect code..."
|
||||||
|
placeholderTextColor="#9CA3AF"
|
||||||
|
value={quickConnectCode}
|
||||||
|
onChangeText={setQuickConnectCode}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Button
|
||||||
|
className="mt-auto"
|
||||||
|
onPress={authorizeQuickConnect}
|
||||||
|
color="purple"
|
||||||
|
>
|
||||||
|
Authorize
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</BottomSheetView>
|
||||||
|
</BottomSheetModal>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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";
|
||||||
@@ -17,13 +18,13 @@ export const getDownloadedFileUrl = async (itemId: string): Promise<string> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const files = await FileSystem.readDirectoryAsync(directory);
|
const files = await FileSystem.readDirectoryAsync(directory);
|
||||||
const path = itemId!;
|
const filePath = getFilePathFromItemId(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 ${path}`);
|
throw new Error(`No file found for item ${filePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${directory}${matchingFile}`;
|
return `${directory}${matchingFile}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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
|
||||||
@@ -53,7 +54,12 @@ export const useRemuxHlsToMp4 = () => {
|
|||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const { saveImage } = useImageStorage();
|
const { saveImage } = useImageStorage();
|
||||||
const { saveSeriesPrimaryImage } = useDownloadHelper();
|
const { saveSeriesPrimaryImage } = useDownloadHelper();
|
||||||
const { saveDownloadedItemInfo, setProcesses, processes, APP_CACHE_DOWNLOAD_DIRECTORY } = useDownload();
|
const {
|
||||||
|
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);
|
||||||
@@ -73,13 +79,12 @@ 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}${item.Id}.mp4`
|
to: `${FileSystem.documentDirectory}${formatItemName(item)}.mp4`,
|
||||||
})
|
});
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ["downloadedItems"],
|
queryKey: ["downloadedItems"],
|
||||||
});
|
});
|
||||||
@@ -131,12 +136,16 @@ 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(APP_CACHE_DOWNLOAD_DIRECTORY);
|
const cacheDir = await FileSystem.getInfoAsync(
|
||||||
|
APP_CACHE_DOWNLOAD_DIRECTORY
|
||||||
|
);
|
||||||
if (!cacheDir.exists) {
|
if (!cacheDir.exists) {
|
||||||
await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, {intermediates: true})
|
await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, {
|
||||||
|
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");
|
||||||
|
|||||||
104
package.json
104
package.json
@@ -4,100 +4,102 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"submodule-reload": "git submodule update --init --remote --recursive",
|
"submodule-reload": "git submodule update --init --remote --recursive",
|
||||||
"clean": "echo y | expo prebuild --clean",
|
|
||||||
"start": "bun run submodule-reload && expo start",
|
"start": "bun run submodule-reload && expo start",
|
||||||
"reset-project": "node ./scripts/reset-project.js",
|
"reset-project": "node ./scripts/reset-project.js",
|
||||||
"android": "bun run submodule-reload && expo run:android",
|
"android": "bun run submodule-reload && expo run:android",
|
||||||
"ios": "bun run submodule-reload && expo run:ios",
|
"ios": "bun run submodule-reload && expo run:ios",
|
||||||
"web": "bun run submodule-reload && expo start --web",
|
"web": "bun run submodule-reload && expo start --web",
|
||||||
|
"test": "jest --watchAll",
|
||||||
"lint": "expo lint",
|
"lint": "expo lint",
|
||||||
"postinstall": "patch-package"
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "jest-expo"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bottom-tabs/react-navigation": "0.8.0",
|
"@bottom-tabs/react-navigation": "^0.7.1",
|
||||||
"react-native-bottom-tabs": "0.8.0",
|
|
||||||
"@react-navigation/material-top-tabs": "^7.1.0",
|
|
||||||
"@react-navigation/native": "^7.0.14",
|
|
||||||
"@config-plugins/ffmpeg-kit-react-native": "^8.0.0",
|
"@config-plugins/ffmpeg-kit-react-native": "^8.0.0",
|
||||||
"@expo/react-native-action-sheet": "^4.1.0",
|
"@expo/react-native-action-sheet": "^4.1.0",
|
||||||
"@expo/vector-icons": "^14.0.4",
|
"@expo/vector-icons": "^14.0.4",
|
||||||
"@futurejj/react-native-visibility-sensor": "^1.3.5",
|
"@futurejj/react-native-visibility-sensor": "^1.3.5",
|
||||||
"@gorhom/bottom-sheet": "^5.0.6",
|
"@gorhom/bottom-sheet": "^4.6.4",
|
||||||
"@jellyfin/sdk": "^0.11.0",
|
"@jellyfin/sdk": "^0.11.0",
|
||||||
"@kesha-antonov/react-native-background-downloader": "3.2.6",
|
"@kesha-antonov/react-native-background-downloader": "3.1.2",
|
||||||
"@react-native-async-storage/async-storage": "1.23.1",
|
"@react-native-async-storage/async-storage": "1.23.1",
|
||||||
"@react-native-community/netinfo": "11.4.1",
|
"@react-native-community/netinfo": "11.3.1",
|
||||||
"@react-native-menu/menu": "^1.1.6",
|
"@react-native-menu/menu": "^1.1.6",
|
||||||
"@shopify/flash-list": "1.7.1",
|
"@react-navigation/material-top-tabs": "^6.6.14",
|
||||||
|
"@react-navigation/native": "^6.1.18",
|
||||||
|
"@shopify/flash-list": "1.6.4",
|
||||||
"@tanstack/react-query": "^5.59.20",
|
"@tanstack/react-query": "^5.59.20",
|
||||||
"@types/lodash": "^4.17.13",
|
"@types/lodash": "^4.17.13",
|
||||||
"@types/react-native-vector-icons": "^6.4.18",
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"expo": "^52.0.0",
|
"expo": "~51.0.39",
|
||||||
"expo-asset": "~11.0.2",
|
"expo-asset": "~10.0.10",
|
||||||
"expo-background-fetch": "~13.0.4",
|
"expo-background-fetch": "~12.0.1",
|
||||||
"expo-blur": "~14.0.3",
|
"expo-blur": "~13.0.2",
|
||||||
"expo-brightness": "~13.0.3",
|
"expo-brightness": "~12.0.1",
|
||||||
"expo-build-properties": "~0.13.2",
|
"expo-build-properties": "~0.12.5",
|
||||||
"expo-constants": "~17.0.4",
|
"expo-constants": "~16.0.2",
|
||||||
"expo-dev-client": "~5.0.10",
|
"expo-dev-client": "~4.0.29",
|
||||||
"expo-device": "~7.0.2",
|
"expo-device": "~6.0.2",
|
||||||
"expo-font": "~13.0.3",
|
"expo-font": "~12.0.10",
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~13.0.1",
|
||||||
"expo-image": "~2.0.4",
|
"expo-image": "~1.13.0",
|
||||||
"expo-keep-awake": "~14.0.2",
|
"expo-keep-awake": "~13.0.2",
|
||||||
"expo-linear-gradient": "~14.0.2",
|
"expo-linear-gradient": "~13.0.2",
|
||||||
"expo-linking": "~7.0.4",
|
"expo-linking": "~6.3.1",
|
||||||
"expo-network": "~7.0.5",
|
"expo-network": "~6.0.1",
|
||||||
"expo-notifications": "~0.29.12",
|
"expo-notifications": "~0.28.19",
|
||||||
"expo-router": "~4.0.17",
|
"expo-router": "~3.5.24",
|
||||||
"expo-screen-orientation": "~8.0.4",
|
"expo-screen-orientation": "~7.0.5",
|
||||||
"expo-sensors": "~14.0.2",
|
"expo-sensors": "~13.0.9",
|
||||||
"expo-splash-screen": "~0.29.21",
|
"expo-splash-screen": "~0.27.7",
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~1.12.1",
|
||||||
"expo-system-ui": "~4.0.7",
|
"expo-system-ui": "^3.0.7",
|
||||||
"expo-task-manager": "~12.0.4",
|
"expo-task-manager": "~11.8.2",
|
||||||
"expo-updates": "~0.26.13",
|
"expo-updates": "~0.25.27",
|
||||||
"expo-web-browser": "~14.0.2",
|
"expo-web-browser": "~13.0.3",
|
||||||
"ffmpeg-kit-react-native": "^6.0.2",
|
"ffmpeg-kit-react-native": "^6.0.2",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"jotai": "^2.10.1",
|
"jotai": "^2.10.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nativewind": "^2.0.11",
|
"nativewind": "^2.0.11",
|
||||||
"react": "18.3.1",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.76.6",
|
"react-native": "0.74.5",
|
||||||
"react-native-awesome-slider": "^2.5.6",
|
"react-native-awesome-slider": "^2.5.6",
|
||||||
|
"react-native-bottom-tabs": "0.7.1",
|
||||||
"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-country-flag": "^2.0.2",
|
|
||||||
"react-native-device-info": "^14.0.1",
|
"react-native-device-info": "^14.0.1",
|
||||||
"react-native-edge-to-edge": "^1.1.3",
|
"react-native-edge-to-edge": "^1.1.1",
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"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",
|
||||||
"react-native-image-colors": "^2.4.0",
|
"react-native-image-colors": "^2.4.0",
|
||||||
"react-native-ios-context-menu": "^2.5.2",
|
"react-native-ios-context-menu": "^2.5.2",
|
||||||
"react-native-ios-utilities": "4.5.3",
|
"react-native-ios-utilities": "^4.5.1",
|
||||||
"react-native-mmkv": "^2.12.2",
|
"react-native-mmkv": "^2.12.2",
|
||||||
"react-native-pager-view": "6.5.1",
|
"react-native-pager-view": "6.3.0",
|
||||||
"react-native-progress": "^5.0.1",
|
"react-native-progress": "^5.0.1",
|
||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-reanimated": "~3.10.1",
|
||||||
"react-native-reanimated-carousel": "4.0.0-canary.22",
|
"react-native-reanimated-carousel": "4.0.0-canary.22",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.10.5",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "3.31.1",
|
||||||
"react-native-svg": "15.8.0",
|
"react-native-svg": "15.2.0",
|
||||||
"react-native-tab-view": "^3.5.2",
|
"react-native-tab-view": "^3.5.2",
|
||||||
"react-native-udp": "^4.1.7",
|
|
||||||
"react-native-uitextview": "^1.4.0",
|
"react-native-uitextview": "^1.4.0",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"react-native-uuid": "^2.0.2",
|
"react-native-uuid": "^2.0.2",
|
||||||
"react-native-video": "^6.7.0",
|
"react-native-video": "^6.7.0",
|
||||||
"react-native-volume-manager": "^1.10.0",
|
"react-native-volume-manager": "^1.10.0",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-webview": "13.12.5",
|
"react-native-webview": "13.8.6",
|
||||||
|
"react-native-youtube-iframe": "^2.3.0",
|
||||||
"sonner-native": "^0.14.2",
|
"sonner-native": "^0.14.2",
|
||||||
"tailwindcss": "3.3.2",
|
"tailwindcss": "3.3.2",
|
||||||
"use-debounce": "^10.0.4",
|
"use-debounce": "^10.0.4",
|
||||||
@@ -108,8 +110,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.26.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/react": "~18.3.12",
|
"@types/react": "~18.2.79",
|
||||||
"@types/react-test-renderer": "^18.0.7",
|
"@types/react-test-renderer": "^18.0.7",
|
||||||
|
"jest": "^29.2.1",
|
||||||
|
"jest-expo": "~51.0.4",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"react-test-renderer": "18.2.0",
|
"react-test-renderer": "18.2.0",
|
||||||
|
|||||||
@@ -19,14 +19,7 @@ import {
|
|||||||
download,
|
download,
|
||||||
setConfig,
|
setConfig,
|
||||||
} from "@kesha-antonov/react-native-background-downloader";
|
} from "@kesha-antonov/react-native-background-downloader";
|
||||||
import MMKV from "react-native-mmkv";
|
import { focusManager, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
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";
|
||||||
@@ -45,7 +38,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 { storage } from "@/utils/mmkv";
|
import { formatItemName, saveItemMapping, 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";
|
||||||
@@ -54,8 +47,11 @@ 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) {
|
||||||
@@ -512,8 +508,10 @@ function useDownloadProvider() {
|
|||||||
|
|
||||||
const downloadedItems = storage.getString("downloadedItems");
|
const downloadedItems = storage.getString("downloadedItems");
|
||||||
if (downloadedItems) {
|
if (downloadedItems) {
|
||||||
let items = JSON.parse(downloadedItems) as DownloadedItem[];
|
let items: { [key: string]: BaseItemDto } = downloadedItems
|
||||||
items = items.filter((item) => item.item.Id !== id);
|
? JSON.parse(downloadedItems)
|
||||||
|
: {};
|
||||||
|
delete items[id];
|
||||||
storage.set("downloadedItems", JSON.stringify(items));
|
storage.set("downloadedItems", JSON.stringify(items));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,7 +584,7 @@ function useDownloadProvider() {
|
|||||||
const appSizeUsage = useMemo(async () => {
|
const appSizeUsage = useMemo(async () => {
|
||||||
const sizes: number[] =
|
const sizes: number[] =
|
||||||
downloadedFiles?.map((d) => {
|
downloadedFiles?.map((d) => {
|
||||||
return getDownloadedItemSize(d.item.Id!!);
|
return d.fileSize;
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
await forEveryDocumentDirFile(
|
await forEveryDocumentDirFile(
|
||||||
@@ -608,8 +606,10 @@ function useDownloadProvider() {
|
|||||||
try {
|
try {
|
||||||
const downloadedItems = storage.getString("downloadedItems");
|
const downloadedItems = storage.getString("downloadedItems");
|
||||||
if (downloadedItems) {
|
if (downloadedItems) {
|
||||||
const items: DownloadedItem[] = JSON.parse(downloadedItems);
|
const items: { [key: string]: BaseItemDto } = downloadedItems
|
||||||
const item = items.find((i) => i.item.Id === itemId);
|
? JSON.parse(downloadedItems)
|
||||||
|
: {};
|
||||||
|
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 JSON.parse(downloadedItems) as DownloadedItem[];
|
return Object.values(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");
|
||||||
let items: DownloadedItem[] = downloadedItems
|
const items: { [key: string]: BaseItemDto } = downloadedItems
|
||||||
? JSON.parse(downloadedItems)
|
? JSON.parse(downloadedItems)
|
||||||
: [];
|
: {};
|
||||||
|
|
||||||
const existingItemIndex = items.findIndex((i) => i.item.Id === item.Id);
|
const chosenItem = items[item.Id!] || {};
|
||||||
|
|
||||||
const data = getDownloadItemInfoFromDiskTmp(item.Id!);
|
const data = getDownloadItemInfoFromDiskTmp(item.Id!);
|
||||||
|
|
||||||
@@ -651,12 +651,6 @@ 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));
|
||||||
|
|||||||
@@ -1,3 +1,30 @@
|
|||||||
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { MMKV } from "react-native-mmkv";
|
import { MMKV } from "react-native-mmkv";
|
||||||
|
|
||||||
export const storage = new MMKV();
|
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