Compare commits

...

26 Commits

Author SHA1 Message Date
Alex Kim
cce8672898 Remove Alamofire 2025-04-19 04:19:50 +10:00
Gap
2ce04b3fd3 feat(lang): Added Russian localization (#613) 2025-04-11 20:08:09 +02:00
Leonardo
bf5203348b feat: add portuguese (pt-BR) translation (#625) 2025-04-11 18:06:41 +02:00
lance chant
16fb1a52ca fix: Fixed the import of expo-application to be the as expo docs and it allowed application to be populated (#648) 2025-04-11 17:56:29 +02:00
sarendsen
d8be7b2463 fix: disable downloads for the moment 2025-04-10 19:39:05 +02:00
sarendsen
ec37b5ab2c fix: use ffmpegkit fork 2025-04-10 16:30:55 +02:00
sarendsen
29eb072e5d fix: use items endpoint for search 2025-04-08 12:32:43 +02:00
sarendsen
2a4a7f5f2d fix: return of export log 2025-04-07 14:33:03 +02:00
sarendsen
8b3f950bc5 fix: use ffmpegkit fork 2025-04-07 12:44:39 +02:00
sarendsen
db527311d6 fix: use ffmpegkit fork 2025-04-07 10:44:45 +02:00
lance chant
b76e834be1 feat: adding reportPlaybackStart which allows tracking to work well (#636) 2025-04-06 10:24:33 +02:00
herrrta
c9905d9d88 fix: add null safety to default folder path 2025-04-01 19:31:27 -04:00
Ahmed Sbai
b9bb109f4a chore: linting fixes && github actions for linting (#612) 2025-03-31 07:44:10 +02:00
Fredrik Burmester
16b834cf71 fix: lint issues 2025-03-30 10:21:41 +02:00
herrrta
f6baf490fb chore: add environment names to builds 2025-03-29 11:42:52 -04:00
herrrta
3201499397 chore: add export log string 2025-03-29 10:52:45 -04:00
herrrta
6555251c2e feat: expo env variables & export logs 2025-03-29 10:44:28 -04:00
sarendsen
71c15f3651 feat: Implement latest for custom home 2025-03-29 14:47:38 +01:00
herrrta
25da30d6e2 fix: env variable 2025-03-28 19:16:16 -04:00
herrrta
1394eae01e feat: better logs
- added ability to write debug logs for development builds
- added filtering to log page
- modified filter button to allow for multiple selection if required
2025-03-28 19:11:36 -04:00
herrrta
587d419502 feat: new notification deep links 2025-03-21 20:52:22 -04:00
herrrta
bc081b535e chore: version bump 2025-03-21 20:22:31 -04:00
herrrta
62d2d1f7ca fix: generate expo push tokens for real device only 2025-03-19 19:17:22 -04:00
lostb1t
66aab5b771 Update README.md 2025-03-19 12:14:00 +01:00
lostb1t
5c89100afd Update README.md 2025-03-19 11:58:08 +01:00
Fredrik Burmester
f1a3b48017 chore 2025-03-19 11:33:27 +01:00
135 changed files with 1904 additions and 684 deletions

1
.env.development Normal file
View File

@@ -0,0 +1 @@
EXPO_PUBLIC_WRITE_DEBUG=1

1
.env.production Normal file
View File

@@ -0,0 +1 @@
EXPO_PUBLIC_WRITE_DEBUG=0

View File

@@ -43,6 +43,7 @@ body:
label: Version
description: What version of Streamyfin are you running?
options:
- 0.28.0
- 0.27.0
- 0.26.1
- 0.26.0

28
.github/workflows/lint.yaml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Lint
on:
pull_request:
branches: [ develop, master ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20.x'
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run linting checks
run: bun run check

3
.gitignore vendored
View File

@@ -46,4 +46,5 @@ credentials.json
.ruby-lsp
modules/hls-downloader/android/build
streamyfin-4fec1-firebase-adminsdk.json
.env
.env
.env.local

View File

@@ -86,6 +86,7 @@ We welcome any help to make Streamyfin better. If you'd like to contribute, plea
1. Use node `>20`
2. Install dependencies `bun i && bun run submodule-reload`
3. Make sure you have xcode and/or android studio installed. (follow the guides for expo: https://docs.expo.dev/workflow/android-studio-emulator/)
4. Install BiomeJS extension in VSCode/Your IDE (https://biomejs.dev/)
4. run `npm run prebuild`
5. Create an expo dev build by running `npm run ios` or `npm run android`. This will open a simulator on your computer and run the app.
@@ -123,6 +124,10 @@ Streamyfin is developed by [Fredrik Burmester](https://github.com/fredrikburmest
## ✨ Acknowledgements
We would like to thank the Jellyfin team for their great software and awesome support on discord.
Special shoutout to the JF official clients for being an inspiration to ours.
### Core Developers
Thanks to the following contributors for their significant contributions:

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "Streamyfin",
"slug": "streamyfin",
"version": "0.27.0",
"version": "0.28.0",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
@@ -31,7 +31,7 @@
},
"android": {
"jsEngine": "hermes",
"versionCode": 53,
"versionCode": 54,
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive_icon.png",
"backgroundColor": "#464646"

View File

@@ -26,11 +26,11 @@ export default function menuLinks() {
const getMenuLinks = useCallback(async () => {
try {
const response = await api?.axiosInstance.get(
api?.basePath + "/web/config.json",
`${api?.basePath}/web/config.json`,
);
const config = response?.data;
if (!config && !config.hasOwnProperty("menuLinks")) {
if (!config && !Object.hasOwn(config, "menuLinks")) {
console.error("Menu links not found");
return;
}

View File

@@ -17,7 +17,7 @@ export default function SearchLayout() {
backgroundColor: "black",
},
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios" ? true : false,
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
/>

View File

@@ -32,7 +32,7 @@ export default function IndexLayout() {
{!Platform.isTV && (
<>
<Chromecast.Chromecast />
{user && user.Policy?.IsAdministrator && <SessionsButton />}
{user?.Policy?.IsAdministrator && <SessionsButton />}
<SettingsButton />
</>
)}

View File

@@ -29,7 +29,7 @@ export default function page() {
try {
return (
downloadedFiles
?.filter((f) => f.item.SeriesId == seriesId)
?.filter((f) => f.item.SeriesId === seriesId)
?.sort(
(a, b) => a?.item.ParentIndexNumber! - b.item.ParentIndexNumber!,
) || []

View File

@@ -36,7 +36,7 @@ export default function page() {
</View>
);
if (!sessions || sessions.length == 0)
if (!sessions || sessions.length === 0)
return (
<View className='h-full w-full flex justify-center items-center'>
<Text className='text-lg text-neutral-500'>
@@ -175,7 +175,7 @@ const SessionCard = ({ session }: SessionCardProps) => {
</View>
<View className='align-bottom bg-gray-800 h-1'>
<View
className={`bg-purple-600 h-full`}
className={"bg-purple-600 h-full"}
style={{
width: `${getProgressPercentage()}%`,
}}
@@ -298,7 +298,7 @@ const TranscodingStreamView = ({
const TranscodingView = ({ session }: SessionCardProps) => {
const videoStream = useMemo(() => {
return session.NowPlayingItem?.MediaStreams?.filter(
(s) => s.Type == "Video",
(s) => s.Type === "Video",
)[0];
}, [session]);
@@ -318,7 +318,7 @@ const TranscodingView = ({ session }: SessionCardProps) => {
const isTranscoding = useMemo(() => {
return (
session.PlayState?.PlayMethod == "Transcode" && session.TranscodingInfo
session.PlayState?.PlayMethod === "Transcode" && session.TranscodingInfo
);
}, [session.PlayState?.PlayMethod, session.TranscodingInfo]);
@@ -341,9 +341,7 @@ const TranscodingView = ({ session }: SessionCardProps) => {
codec: session.TranscodingInfo?.VideoCodec,
}}
isTranscoding={
isTranscoding && !session.TranscodingInfo?.IsVideoDirect
? true
: false
!!(isTranscoding && !session.TranscodingInfo?.IsVideoDirect)
}
/>
@@ -360,24 +358,20 @@ const TranscodingView = ({ session }: SessionCardProps) => {
audioChannels: session.TranscodingInfo?.AudioChannels?.toString(),
}}
isTranscoding={
isTranscoding && !session.TranscodingInfo?.IsVideoDirect
? true
: false
!!(isTranscoding && !session.TranscodingInfo?.IsVideoDirect)
}
/>
{subtitleStream && (
<>
<TranscodingStreamView
title='Subtitle'
isTranscoding={false}
properties={{
language: subtitleStream?.Language,
codec: subtitleStream?.Codec,
}}
transcodeValue={null}
/>
</>
<TranscodingStreamView
title='Subtitle'
isTranscoding={false}
properties={{
language: subtitleStream?.Language,
codec: subtitleStream?.Codec,
}}
transcodeValue={null}
/>
)}
</View>
);

View File

@@ -1,37 +1,157 @@
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import { useLog } from "@/utils/log";
import { FilterButton } from "@/components/filters/FilterButton";
import { LogLevel, useLog, writeErrorLog } from "@/utils/log";
import * as FileSystem from "expo-file-system";
import { useNavigation } from "expo-router";
import * as Sharing from "expo-sharing";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { ScrollView, View } from "react-native";
import { ScrollView, TouchableOpacity, View } from "react-native";
import Collapsible from "react-native-collapsible";
export default function page() {
const navigation = useNavigation();
const { logs } = useLog();
const { t } = useTranslation();
const defaultLevels: LogLevel[] = ["INFO", "ERROR", "DEBUG", "WARN"];
const codeBlockStyle = {
backgroundColor: "#000",
padding: 10,
fontFamily: "monospace",
maxHeight: 300,
};
const [loading, setLoading] = useState<boolean>(false);
const [state, setState] = useState<Record<string, boolean>>({});
const [order, setOrder] = useState<"asc" | "desc">("desc");
const [levels, setLevels] = useState<LogLevel[]>(defaultLevels);
const filteredLogs = useMemo(
() =>
logs
?.filter((log) => levels.includes(log.level))
?.[
// Already in asc order as they are recorded. just reverse for desc
order === "desc" ? "reverse" : "concat"
]?.(),
[logs, order, levels],
);
// Sharing it as txt while its formatted allows us to share it with many more applications
const share = useCallback(async () => {
const uri = `${FileSystem.documentDirectory}logs.txt`;
setLoading(true);
FileSystem.writeAsStringAsync(uri, JSON.stringify(filteredLogs))
.then(() => {
setLoading(false);
Sharing.shareAsync(uri, { mimeType: "txt", UTI: "txt" });
})
.catch((e) =>
writeErrorLog("Something went wrong attempting to export", e),
)
.finally(() => setLoading(false));
}, [filteredLogs]);
useEffect(() => {
navigation.setOptions({
headerRight: () =>
loading ? (
<Loader />
) : (
<TouchableOpacity onPress={share}>
<Text>{t("home.settings.logs.export_logs")}</Text>
</TouchableOpacity>
),
});
}, [share, loading]);
return (
<ScrollView className='p-4'>
<View className='flex flex-col space-y-2'>
{logs?.map((log, index) => (
<View key={index} className='bg-neutral-900 rounded-xl p-3'>
<Text
className={`
mb-1
${log.level === "INFO" && "text-blue-500"}
${log.level === "ERROR" && "text-red-500"}
`}
>
{log.level}
</Text>
<Text uiTextView selectable className='text-xs'>
{log.message}
</Text>
</View>
))}
{logs?.length === 0 && (
<Text className='opacity-50'>
{t("home.settings.logs.no_logs_available")}
</Text>
)}
<>
<View className='flex flex-row justify-end py-2 px-4 space-x-2'>
<FilterButton
id='order'
queryKey='log'
queryFn={async () => ["asc", "desc"]}
set={(values) => setOrder(values[0])}
values={[order]}
title={t("library.filters.sort_order")}
renderItemLabel={(order) => t(`library.filters.${order}`)}
showSearch={false}
/>
<FilterButton
id='levels'
queryKey='log'
queryFn={async () => defaultLevels}
set={setLevels}
values={levels}
title={t("home.settings.logs.level")}
renderItemLabel={(level) => level}
showSearch={false}
multiple={true}
/>
</View>
</ScrollView>
<ScrollView className='pb-4 px-4'>
<View className='flex flex-col space-y-2'>
{filteredLogs?.map((log, index) => (
<View className='bg-neutral-900 rounded-xl p-3' key={index}>
<TouchableOpacity
disabled={!log.data}
onPress={() =>
setState((v) => ({
...v,
[log.timestamp]: !v[log.timestamp],
}))
}
>
<View className='flex flex-row justify-between'>
<Text
className={`mb-1
${log.level === "INFO" && "text-blue-500"}
${log.level === "ERROR" && "text-red-500"}
${log.level === "DEBUG" && "text-purple-500"}
`}
>
{log.level}
</Text>
<Text className='text-xs'>
{new Date(log.timestamp).toLocaleString()}
</Text>
</View>
<Text uiTextView selectable className='text-xs'>
{log.message}
</Text>
</TouchableOpacity>
{log.data && (
<>
{!state[log.timestamp] && (
<Text className='text-xs mt-0.5'>
{t("home.settings.logs.click_for_more_info")}
</Text>
)}
<Collapsible collapsed={!state[log.timestamp]}>
<View className='mt-2 flex flex-col space-y-2'>
<ScrollView className='rounded-xl' style={codeBlockStyle}>
<Text>{JSON.stringify(log.data, null, 2)}</Text>
</ScrollView>
</View>
</Collapsible>
</>
)}
</View>
))}
{filteredLogs?.length === 0 && (
<Text className='opacity-50'>
{t("home.settings.logs.no_logs_available")}
</Text>
)}
</View>
</ScrollView>
</>
);
}

View File

@@ -93,7 +93,7 @@ export default function page() {
showText={!pluginSettings?.searchEngine?.locked}
className='mt-2 flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4'
>
<View className={`flex flex-row items-center bg-neutral-900 h-11 pr-4`}>
<View className={"flex flex-row items-center bg-neutral-900 h-11 pr-4"}>
<Text className='mr-4'>
{t("home.settings.plugins.marlin_search.url")}
</Text>

View File

@@ -31,7 +31,7 @@ export default function page() {
return;
}
const updatedUrl = newVal.endsWith("/") ? newVal : newVal + "/";
const updatedUrl = newVal.endsWith("/") ? newVal : `${newVal}/`;
updateSettings({
optimizedVersionsServerUrl: updatedUrl,

View File

@@ -133,7 +133,7 @@ const page: React.FC = () => {
queryFn={fetchItems}
queryKey={["actor", "movies", actorId]}
/>
<View className='h-12'></View>
<View className='h-12' />
</View>
</ParallaxScrollView>
);

View File

@@ -157,9 +157,8 @@ const page: React.FC = () => {
if (accumulatedItems < totalItems) {
return lastPage?.Items?.length * pages.length;
} else {
return undefined;
}
return undefined;
},
initialPageParam: 0,
enabled: !!api && !!user?.Id && !!collection,
@@ -234,7 +233,7 @@ const page: React.FC = () => {
component: (
<FilterButton
className='mr-1'
collectionId={collectionId}
id={collectionId}
queryKey='genreFilter'
queryFn={async () => {
if (!api) return null;
@@ -261,7 +260,7 @@ const page: React.FC = () => {
component: (
<FilterButton
className='mr-1'
collectionId={collectionId}
id={collectionId}
queryKey='yearFilter'
queryFn={async () => {
if (!api) return null;
@@ -286,7 +285,7 @@ const page: React.FC = () => {
component: (
<FilterButton
className='mr-1'
collectionId={collectionId}
id={collectionId}
queryKey='tagsFilter'
queryFn={async () => {
if (!api) return null;
@@ -313,7 +312,7 @@ const page: React.FC = () => {
component: (
<FilterButton
className='mr-1'
collectionId={collectionId}
id={collectionId}
queryKey='sortBy'
queryFn={async () => sortOptions.map((s) => s.key)}
set={setSortBy}
@@ -333,7 +332,7 @@ const page: React.FC = () => {
component: (
<FilterButton
className='mr-1'
collectionId={collectionId}
id={collectionId}
queryKey='sortOrder'
queryFn={async () => sortOrderOptions.map((s) => s.key)}
set={setSortOrder}
@@ -412,7 +411,7 @@ const page: React.FC = () => {
width: 10,
height: 10,
}}
></View>
/>
)}
/>
);

View File

@@ -93,19 +93,19 @@ const Page: React.FC = () => {
height: item?.Type === "Episode" ? 300 : 450,
}}
className='bg-transparent rounded-lg mb-4 w-full'
></View>
<View className='h-6 bg-neutral-900 rounded mb-4 w-14'></View>
<View className='h-10 bg-neutral-900 rounded-lg mb-2 w-1/2'></View>
<View className='h-3 bg-neutral-900 rounded mb-3 w-8'></View>
/>
<View className='h-6 bg-neutral-900 rounded mb-4 w-14' />
<View className='h-10 bg-neutral-900 rounded-lg mb-2 w-1/2' />
<View className='h-3 bg-neutral-900 rounded mb-3 w-8' />
<View className='flex flex-row space-x-1 mb-8'>
<View className='h-6 bg-neutral-900 rounded mb-3 w-14'></View>
<View className='h-6 bg-neutral-900 rounded mb-3 w-14'></View>
<View className='h-6 bg-neutral-900 rounded mb-3 w-14'></View>
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
</View>
<View className='h-3 bg-neutral-900 rounded w-2/3 mb-1'></View>
<View className='h-10 bg-neutral-900 rounded-lg w-full mb-2'></View>
<View className='h-12 bg-neutral-900 rounded-lg w-full mb-2'></View>
<View className='h-24 bg-neutral-900 rounded-lg mb-1 w-full'></View>
<View className='h-3 bg-neutral-900 rounded w-2/3 mb-1' />
<View className='h-10 bg-neutral-900 rounded-lg w-full mb-2' />
<View className='h-12 bg-neutral-900 rounded-lg w-full mb-2' />
<View className='h-24 bg-neutral-900 rounded-lg mb-1 w-full' />
</Animated.View>
{item && <ItemContent item={item} />}
</View>

View File

@@ -33,9 +33,11 @@ export default function page() {
};
return jellyseerrApi?.discover(
(type == DiscoverSliderType.NETWORKS
? Endpoints.DISCOVER_TV_NETWORK
: Endpoints.DISCOVER_MOVIES_STUDIO) + `/${companyId}`,
`${
type === DiscoverSliderType.NETWORKS
? Endpoints.DISCOVER_TV_NETWORK
: Endpoints.DISCOVER_MOVIES_STUDIO
}/${companyId}`,
params,
);
},

View File

@@ -36,7 +36,7 @@ export default function page() {
};
return jellyseerrApi?.discover(
type == DiscoverSliderType.MOVIE_GENRES
type === DiscoverSliderType.MOVIE_GENRES
? Endpoints.DISCOVER_MOVIES
: Endpoints.DISCOVER_TV,
params,

View File

@@ -240,7 +240,7 @@ const Page: React.FC = () => {
<GenreTags genres={details?.genres?.map((g) => g.name) || []} />
</View>
{isLoading || isFetching ? (
<Button loading={true} disabled={true} color='purple'></Button>
<Button loading={true} disabled={true} color='purple' />
) : canRequest ? (
<Button color='purple' onPress={request}>
{t("jellyseerr.request_button")}

View File

@@ -124,7 +124,7 @@ export default function page() {
height: HOUR_HEIGHT,
}}
className='bg-neutral-800'
></View>
/>
{channels?.Items?.map((c, i) => (
<View className='h-16 w-16 mr-4 rounded-lg overflow-hidden' key={i}>
<ItemImage

View File

@@ -87,23 +87,21 @@ const page: React.FC = () => {
<View className='flex flex-row items-center space-x-2'>
<AddToFavorites item={item} />
{!Platform.isTV && (
<>
<DownloadItems
size='large'
title={t("item_card.download.download_series")}
items={allEpisodes || []}
MissingDownloadIconComponent={() => (
<Ionicons name='download' size={22} color='white' />
)}
DownloadedIconComponent={() => (
<Ionicons
name='checkmark-done-outline'
size={24}
color='#9333ea'
/>
)}
/>
</>
<DownloadItems
size='large'
title={t("item_card.download.download_series")}
items={allEpisodes || []}
MissingDownloadIconComponent={() => (
<Ionicons name='download' size={22} color='white' />
)}
DownloadedIconComponent={() => (
<Ionicons
name='checkmark-done-outline'
size={24}
color='#9333ea'
/>
)}
/>
)}
</View>
),
@@ -127,20 +125,18 @@ const page: React.FC = () => {
/>
}
logo={
<>
{logoUrl ? (
<Image
source={{
uri: logoUrl,
}}
style={{
height: 130,
width: "100%",
resizeMode: "contain",
}}
/>
) : null}
</>
logoUrl ? (
<Image
source={{
uri: logoUrl,
}}
style={{
height: 130,
width: "100%",
resizeMode: "contain",
}}
/>
) : null
}
>
<View className='flex flex-col pt-4'>

View File

@@ -216,9 +216,8 @@ const Page = () => {
if (accumulatedItems < totalItems) {
return lastPage?.Items?.length * pages.length;
} else {
return undefined;
}
return undefined;
},
initialPageParam: 0,
enabled: !!api && !!user?.Id && !!library,
@@ -287,7 +286,7 @@ const Page = () => {
component: (
<FilterButton
className='mr-1'
collectionId={libraryId}
id={libraryId}
queryKey='genreFilter'
queryFn={async () => {
if (!api) return null;
@@ -314,7 +313,7 @@ const Page = () => {
component: (
<FilterButton
className='mr-1'
collectionId={libraryId}
id={libraryId}
queryKey='yearFilter'
queryFn={async () => {
if (!api) return null;
@@ -339,7 +338,7 @@ const Page = () => {
component: (
<FilterButton
className='mr-1'
collectionId={libraryId}
id={libraryId}
queryKey='tagsFilter'
queryFn={async () => {
if (!api) return null;
@@ -366,7 +365,7 @@ const Page = () => {
component: (
<FilterButton
className='mr-1'
collectionId={libraryId}
id={libraryId}
queryKey='sortBy'
queryFn={async () => sortOptions.map((s) => s.key)}
set={setSortBy}
@@ -386,7 +385,7 @@ const Page = () => {
component: (
<FilterButton
className='mr-1'
collectionId={libraryId}
id={libraryId}
queryKey='sortOrder'
queryFn={async () => sortOrderOptions.map((s) => s.key)}
set={setSortOrder}
@@ -478,7 +477,7 @@ const Page = () => {
width: 10,
height: 10,
}}
></View>
/>
)}
/>
);

View File

@@ -25,7 +25,7 @@ export default function IndexLayout() {
headerLargeStyle: {
backgroundColor: "black",
},
headerTransparent: Platform.OS === "ios" ? true : false,
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
headerRight: () =>
!pluginSettings?.libraryOptions?.locked &&
@@ -159,7 +159,7 @@ export default function IndexLayout() {
updateSettings({
libraryOptions: {
...settings.libraryOptions,
showTitles: newValue === "on" ? true : false,
showTitles: newValue === "on",
},
});
}}
@@ -176,7 +176,7 @@ export default function IndexLayout() {
updateSettings({
libraryOptions: {
...settings.libraryOptions,
showStats: newValue === "on" ? true : false,
showStats: newValue === "on",
},
});
}}
@@ -200,7 +200,7 @@ export default function IndexLayout() {
title: "",
headerShown: true,
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios" ? true : false,
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
/>
@@ -213,7 +213,7 @@ export default function IndexLayout() {
title: "",
headerShown: true,
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios" ? true : false,
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
/>

View File

@@ -100,7 +100,7 @@ export default function index() {
height: StyleSheet.hairlineWidth,
}}
className='bg-neutral-800 mx-2 my-4'
></View>
/>
) : (
<View className='h-4' />
)

View File

@@ -20,7 +20,7 @@ export default function SearchLayout() {
backgroundColor: "black",
},
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios" ? true : false,
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
/>
@@ -33,7 +33,7 @@ export default function SearchLayout() {
title: "",
headerShown: true,
headerBlurEffect: "prominent",
headerTransparent: Platform.OS === "ios" ? true : false,
headerTransparent: Platform.OS === "ios",
headerShadowVisible: false,
}}
/>

View File

@@ -14,7 +14,6 @@ import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
import { SearchItemWrapper } from "@/components/search/SearchItemWrapper";
import { useJellyseerr } from "@/hooks/useJellyseerr";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { sortOrderOptions } from "@/utils/atoms/filters";
import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus";
import type {
@@ -54,6 +53,8 @@ export default function search() {
const params = useLocalSearchParams();
const insets = useSafeAreaInsets();
const [user] = useAtom(userAtom);
const { t } = useTranslation();
const { q } = params as { q: string };
@@ -64,7 +65,6 @@ export default function search() {
const [debouncedSearch] = useDebounce(search, 500);
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const [settings] = useSettings();
const { jellyseerrApi } = useJellyseerr();
@@ -83,7 +83,9 @@ export default function search() {
}, [settings]);
useEffect(() => {
if (q && q.length > 0) setSearch(q);
if (q && q.length > 0) {
setSearch(q);
}
}, [q]);
const searchFn = useCallback(
@@ -94,39 +96,46 @@ export default function search() {
types: BaseItemKind[];
query: string;
}): Promise<BaseItemDto[]> => {
if (!api || !query) return [];
if (!api || !query) {
return [];
}
try {
if (searchEngine === "Jellyfin") {
const searchApi = await getSearchApi(api).getSearchHints({
const searchApi = await getItemsApi(api).getItems({
searchTerm: query,
limit: 10,
includeItemTypes: types,
recursive: true,
userId: user?.Id,
});
return (searchApi.data.SearchHints as BaseItemDto[]) || [];
} else {
if (!settings?.marlinServerUrl) return [];
const url = `${
settings.marlinServerUrl
}/search?q=${encodeURIComponent(query)}&includeItemTypes=${types
.map((type) => encodeURIComponent(type))
.join("&includeItemTypes=")}`;
const response1 = await axios.get(url);
const ids = response1.data.ids;
if (!ids || !ids.length) return [];
const response2 = await getItemsApi(api).getItems({
ids,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
});
return (response2.data.Items as BaseItemDto[]) || [];
return (searchApi.data.Items as BaseItemDto[]) || [];
}
if (!settings?.marlinServerUrl) {
return [];
}
const url = `${
settings.marlinServerUrl
}/search?q=${encodeURIComponent(query)}&includeItemTypes=${types
.map((type) => encodeURIComponent(type))
.join("&includeItemTypes=")}`;
const response1 = await axios.get(url);
const ids = response1.data.ids;
if (!ids || !ids.length) {
return [];
}
const response2 = await getItemsApi(api).getItems({
ids,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
});
return (response2.data.Items as BaseItemDto[]) || [];
} catch (error) {
console.error("Error during search:", error);
return []; // Ensure an empty array is returned in case of an error
@@ -162,8 +171,10 @@ export default function search() {
useEffect(() => {
const unsubscribe = eventBus.on("searchTabPressed", () => {
// Screen not actuve
if (!searchBarRef.current) return;
// Screen not active
if (!searchBarRef.current) {
return;
}
// Screen is active, focus search bar
searchBarRef.current?.focus();
});
@@ -254,64 +265,62 @@ export default function search() {
}}
>
{jellyseerrApi && (
<>
<ScrollView
horizontal
className='flex flex-row flex-wrap space-x-2 px-4 mb-2'
>
<TouchableOpacity onPress={() => setSearchType("Library")}>
<Tag
text={t("search.library")}
textClass='p-1'
className={
searchType === "Library" ? "bg-purple-600" : undefined
}
/>
</TouchableOpacity>
<TouchableOpacity onPress={() => setSearchType("Discover")}>
<Tag
text={t("search.discover")}
textClass='p-1'
className={
searchType === "Discover" ? "bg-purple-600" : undefined
}
/>
</TouchableOpacity>
{searchType === "Discover" &&
!loading &&
noResults &&
debouncedSearch.length > 0 && (
<View className='flex flex-row justify-end items-center space-x-1'>
<FilterButton
collectionId='search'
queryKey='jellyseerr_search'
queryFn={async () =>
Object.keys(JellyseerrSearchSort).filter((v) =>
isNaN(Number(v)),
)
}
set={(value) => setJellyseerrOrderBy(value[0])}
values={[jellyseerrOrderBy]}
title={t("library.filters.sort_by")}
renderItemLabel={(item) =>
t(`home.settings.plugins.jellyseerr.order_by.${item}`)
}
showSearch={false}
/>
<FilterButton
collectionId='order'
queryKey='jellysearr_search'
queryFn={async () => ["asc", "desc"]}
set={(value) => setJellyseerrSortOrder(value[0])}
values={[jellyseerrSortOrder]}
title={t("library.filters.sort_order")}
renderItemLabel={(item) => t(`library.filters.${item}`)}
showSearch={false}
/>
</View>
)}
</ScrollView>
</>
<ScrollView
horizontal
className='flex flex-row flex-wrap space-x-2 px-4 mb-2'
>
<TouchableOpacity onPress={() => setSearchType("Library")}>
<Tag
text={t("search.library")}
textClass='p-1'
className={
searchType === "Library" ? "bg-purple-600" : undefined
}
/>
</TouchableOpacity>
<TouchableOpacity onPress={() => setSearchType("Discover")}>
<Tag
text={t("search.discover")}
textClass='p-1'
className={
searchType === "Discover" ? "bg-purple-600" : undefined
}
/>
</TouchableOpacity>
{searchType === "Discover" &&
!loading &&
noResults &&
debouncedSearch.length > 0 && (
<View className='flex flex-row justify-end items-center space-x-1'>
<FilterButton
id='search'
queryKey='jellyseerr_search'
queryFn={async () =>
Object.keys(JellyseerrSearchSort).filter((v) =>
Number.isNaN(Number(v)),
)
}
set={(value) => setJellyseerrOrderBy(value[0])}
values={[jellyseerrOrderBy]}
title={t("library.filters.sort_by")}
renderItemLabel={(item) =>
t(`home.settings.plugins.jellyseerr.order_by.${item}`)
}
showSearch={false}
/>
<FilterButton
id='order'
queryKey='jellysearr_search'
queryFn={async () => ["asc", "desc"]}
set={(value) => setJellyseerrSortOrder(value[0])}
values={[jellyseerrSortOrder]}
title={t("library.filters.sort_order")}
renderItemLabel={(item) => t(`library.filters.${item}`)}
showSearch={false}
/>
</View>
)}
</ScrollView>
)}
<View className='mt-2'>
@@ -411,32 +420,29 @@ export default function search() {
/>
)}
{searchType === "Library" && (
<>
{!loading && noResults && debouncedSearch.length > 0 ? (
<View>
<Text className='text-center text-lg font-bold mt-4'>
{t("search.no_results_found_for")}
</Text>
<Text className='text-xs text-purple-600 text-center'>
"{debouncedSearch}"
</Text>
</View>
) : debouncedSearch.length === 0 ? (
<View className='mt-4 flex flex-col items-center space-y-2'>
{exampleSearches.map((e) => (
<TouchableOpacity
onPress={() => setSearch(e)}
key={e}
className='mb-2'
>
<Text className='text-purple-600'>{e}</Text>
</TouchableOpacity>
))}
</View>
) : null}
</>
)}
{searchType === "Library" &&
(!loading && noResults && debouncedSearch.length > 0 ? (
<View>
<Text className='text-center text-lg font-bold mt-4'>
{t("search.no_results_found_for")}
</Text>
<Text className='text-xs text-purple-600 text-center'>
"{debouncedSearch}"
</Text>
</View>
) : debouncedSearch.length === 0 ? (
<View className='mt-4 flex flex-col items-center space-y-2'>
{exampleSearches.map((e) => (
<TouchableOpacity
onPress={() => setSearch(e)}
key={e}
className='mb-2'
>
<Text className='text-purple-600'>{e}</Text>
</TouchableOpacity>
))}
</View>
) : null)}
</View>
</ScrollView>
</>

View File

@@ -72,7 +72,7 @@ export default function TabLayout() {
options={{
title: t("tabs.home"),
tabBarIcon:
Platform.OS == "android"
Platform.OS === "android"
? ({ color, focused, size }) =>
require("@/assets/icons/house.fill.png")
: ({ focused }) =>
@@ -91,7 +91,7 @@ export default function TabLayout() {
options={{
title: t("tabs.search"),
tabBarIcon:
Platform.OS == "android"
Platform.OS === "android"
? ({ color, focused, size }) =>
require("@/assets/icons/magnifyingglass.png")
: ({ focused }) =>
@@ -105,7 +105,7 @@ export default function TabLayout() {
options={{
title: t("tabs.favorites"),
tabBarIcon:
Platform.OS == "android"
Platform.OS === "android"
? ({ color, focused, size }) =>
focused
? require("@/assets/icons/heart.fill.png")
@@ -121,7 +121,7 @@ export default function TabLayout() {
options={{
title: t("tabs.library"),
tabBarIcon:
Platform.OS == "android"
Platform.OS === "android"
? ({ color, focused, size }) =>
require("@/assets/icons/server.rack.png")
: ({ focused }) =>
@@ -135,9 +135,9 @@ export default function TabLayout() {
options={{
title: t("tabs.custom_links"),
// @ts-expect-error
tabBarItemHidden: settings?.showCustomMenuLinks ? false : true,
tabBarItemHidden: !settings?.showCustomMenuLinks,
tabBarIcon:
Platform.OS == "android"
Platform.OS === "android"
? ({ focused }) => require("@/assets/icons/list.png")
: ({ focused }) =>
focused

View File

@@ -24,6 +24,7 @@ import {
type MediaSourceInfo,
PlaybackOrder,
type PlaybackProgressInfo,
PlaybackStartInfo,
RepeatMode,
} from "@jellyfin/sdk/lib/generated-client";
import {
@@ -201,13 +202,29 @@ export default function page() {
fetchStreamData();
}, [itemId, mediaSourceId, bitrateValue, api, item, user?.Id]);
useEffect(() => {
if (!stream) return;
const reportPlaybackStart = async () => {
await getPlaystateApi(api!).reportPlaybackStart({
playbackStartInfo: currentPlayStateInfo() as PlaybackStartInfo,
});
};
reportPlaybackStart();
}, [stream]);
const togglePlay = async () => {
lightHapticFeedback();
setIsPlaying(!isPlaying);
if (isPlaying) {
await videoRef.current?.pause();
reportPlaybackStopped();
} else {
videoRef.current?.play();
await getPlaystateApi(api!).reportPlaybackStart({
playbackStartInfo: currentPlayStateInfo() as PlaybackStartInfo,
});
}
};

View File

@@ -16,7 +16,12 @@ import {
BACKGROUND_FETCH_TASK_SESSIONS,
registerBackgroundFetchAsyncSessions,
} from "@/utils/background-tasks";
import { LogProvider, writeErrorLog, writeToLog } from "@/utils/log";
import {
LogProvider,
writeDebugLog,
writeErrorLog,
writeToLog,
} from "@/utils/log";
import { storage } from "@/utils/mmkv";
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
@@ -31,6 +36,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const BackgroundFetch = !Platform.isTV
? require("expo-background-fetch")
: null;
import * as Device from "expo-device";
import * as FileSystem from "expo-file-system";
const Notifications = !Platform.isTV ? require("expo-notifications") : null;
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
@@ -160,7 +166,7 @@ if (!Platform.isTV) {
for (const job of jobs) {
if (job.status === "completed") {
const downloadUrl = url + "download/" + job.id;
const downloadUrl = `${url}download/${job.id}`;
const tasks = await BackGroundDownloader.checkForExistingDownloads();
if (tasks.find((task: { id: string }) => task.id === job.id)) {
@@ -193,7 +199,7 @@ if (!Platform.isTV) {
title: job.item.Name,
body: "Download completed",
data: {
url: `/downloads`,
url: "/downloads",
},
},
trigger: null,
@@ -207,7 +213,7 @@ if (!Platform.isTV) {
title: job.item.Name,
body: "Download failed",
data: {
url: `/downloads`,
url: "/downloads",
},
},
trigger: null,
@@ -331,9 +337,12 @@ function Layout() {
await registerBackgroundFetchAsyncSessions();
}
Notifications?.getExpoPushTokenAsync()
.then((token: ExpoPushToken) => token && setExpoPushToken(token))
.catch((reason: any) => console.log("Failed to get token", reason));
// only create push token for real devices (pointless for emulators)
if (Device.isDevice) {
Notifications?.getExpoPushTokenAsync()
.then((token: ExpoPushToken) => token && setExpoPushToken(token))
.catch((reason: any) => console.log("Failed to get token", reason));
}
}
useEffect(() => {
@@ -352,7 +361,43 @@ function Layout() {
responseListener.current =
Notifications?.addNotificationResponseReceivedListener(
(response: NotificationResponse) => {
console.log("Notification interacted with", response);
// Currently the notifications supported by the plugin will send data for deep links.
const { title, data } = response.notification.request.content;
writeDebugLog(
`Notification ${title} opened`,
response.notification.request.content,
);
if (data && Object.keys(data).length > 0) {
const type = data?.type?.toLower?.();
const itemId = data?.id;
switch (type) {
case "movie":
router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`);
break;
case "episode":
// We just clicked a notification for an individual episode.
if (itemId) {
router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`);
}
// summarized season notification for multiple episodes. Bring them to series season
else {
const seriesId = data.seriesId;
const seasonIndex = data.seasonIndex;
if (seasonIndex) {
router.push(
`/(auth)/(tabs)/home/series/${seriesId}?seasonIndex=${seasonIndex}`,
);
} else {
router.push(`/(auth)/(tabs)/home/series/${seriesId}`);
}
}
break;
}
}
},
);

View File

@@ -218,16 +218,14 @@ const Login: React.FC = () => {
<View className='px-4 -mt-20 w-full'>
<View className='flex flex-col space-y-2'>
<Text className='text-2xl font-bold -mb-2'>
<>
{serverName ? (
<>
{t("login.login_to_title") + " "}
<Text className='text-purple-600'>{serverName}</Text>
</>
) : (
t("login.login_title")
)}
</>
{serverName ? (
<>
{`${t("login.login_to_title")} `}
<Text className='text-purple-600'>{serverName}</Text>
</>
) : (
t("login.login_title")
)}
</Text>
<Text className='text-xs text-neutral-400'>
{api.basePath}
@@ -284,7 +282,7 @@ const Login: React.FC = () => {
</View>
</View>
<View className='absolute bottom-0 left-0 w-full px-4 mb-2'></View>
<View className='absolute bottom-0 left-0 w-full px-4 mb-2' />
</View>
</>
) : (

View File

@@ -17,9 +17,7 @@ Number.prototype.bytesToReadable = function (decimals = 2) {
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (
Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]
);
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
};
Number.prototype.secondsToMilliseconds = function () {

View File

@@ -9,7 +9,8 @@
"ios",
"android",
"Streamyfin.app",
"utils/jellyseerr"
"utils/jellyseerr",
".expo"
]
},
"linter": {
@@ -17,12 +18,18 @@
"rules": {
"style": {
"useImportType": "off",
"noNonNullAssertion": "off"
"noNonNullAssertion": "off",
"noParameterAssign": "off",
"useLiteralEnumMembers": "off"
},
"complexity": {
"noForEach": "off"
},
"recommended": true,
"correctness": { "useExhaustiveDependencies": "off" },
"suspicious": {
"noExplicitAny": "off"
"noExplicitAny": "off",
"noArrayIndexKey": "off"
}
}
},

View File

@@ -44,6 +44,7 @@
"expo-router": "~4.0.17",
"expo-screen-orientation": "~8.0.4",
"expo-sensors": "~14.0.2",
"expo-sharing": "~13.0.1",
"expo-splash-screen": "~0.29.22",
"expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.8",
@@ -62,6 +63,7 @@
"react-native-awesome-slider": "^2.9.0",
"react-native-bottom-tabs": "0.8.6",
"react-native-circular-progress": "^1.4.1",
"react-native-collapsible": "^1.6.2",
"react-native-compressor": "^1.10.3",
"react-native-country-flag": "^2.0.2",
"react-native-device-info": "^14.0.4",
@@ -109,7 +111,6 @@
"@types/uuid": "^10.0.0",
"husky": "^9.1.7",
"lint-staged": "^15.5.0",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0",
"react-test-renderer": "19.0.0",
"typescript": "~5.7.3",
@@ -761,8 +762,6 @@
"@xmldom/xmldom": ["@xmldom/xmldom@0.7.13", "", {}, "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g=="],
"@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="],
"@zxing/text-encoding": ["@zxing/text-encoding@0.9.0", "", {}, "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
@@ -1203,6 +1202,8 @@
"expo-sensors": ["expo-sensors@14.0.2", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-nCb1Q3ctb0oVTZ9p6eFmQ2fINa6KoxXXIhagPpdN0qR82p00YosP27IuyxjVB3fnCJFeC4TffNxNjBxwAUk+nA=="],
"expo-sharing": ["expo-sharing@13.0.1", "", { "peerDependencies": { "expo": "*" } }, "sha512-qych3Nw65wlFcnzE/gRrsdtvmdV0uF4U4qVMZBJYPG90vYyWh2QM9rp1gVu0KWOBc7N8CC2dSVYn4/BXqJy6Xw=="],
"expo-splash-screen": ["expo-splash-screen@0.29.22", "", { "dependencies": { "@expo/prebuild-config": "^8.0.27" }, "peerDependencies": { "expo": "*" } }, "sha512-f+bPpF06bqiuW1Fbrd3nxeaSsmTVTBEKEYe3epYt4IE6y4Ulli3qEUamMLlRQiDGuIXPU6zQlscpy2mdBUI5cA=="],
"expo-status-bar": ["expo-status-bar@2.0.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-AkIPX7jWHRPp83UBZ1iXtVvyr0g+DgBVvIXTtlmPtmUsm8Vq9Bb5IGj86PW8osuFlgoTVAg7HI/+Ok7yEYwiRg=="],
@@ -1263,8 +1264,6 @@
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"find-yarn-workspace-root": ["find-yarn-workspace-root@2.0.0", "", { "dependencies": { "micromatch": "^4.0.2" } }, "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ=="],
"flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="],
"flow-parser": ["flow-parser@0.261.2", "", {}, "sha512-RtunoakA3YjtpAxPSOBVW6lmP5NYmETwkpAfNkdr8Ovf86ENkbD3mtPWnswFTIUtRvjwv0i8ZSkHK+AzsUg1JA=="],
@@ -1437,8 +1436,6 @@
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="],
@@ -1501,18 +1498,12 @@
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-stable-stringify": ["json-stable-stringify@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" } }, "sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="],
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
@@ -1715,7 +1706,7 @@
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
"open": ["open@6.4.0", "", { "dependencies": { "is-wsl": "^1.1.0" } }, "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg=="],
"ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
@@ -1753,8 +1744,6 @@
"password-prompt": ["password-prompt@1.1.3", "", { "dependencies": { "ansi-escapes": "^4.3.2", "cross-spawn": "^7.0.3" } }, "sha512-HkrjG2aJlvF0t2BMH0e2LB/EHf3Lcq3fNMzy4GYHcQblAvOl+QQji1Lx7WRBMqpVK8p+KR7bCg7oqAMXtdgqyw=="],
"patch-package": ["patch-package@8.0.0", "", { "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", "ci-info": "^3.7.0", "cross-spawn": "^7.0.3", "find-yarn-workspace-root": "^2.0.0", "fs-extra": "^9.0.0", "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", "rimraf": "^2.6.3", "semver": "^7.5.3", "slash": "^2.0.0", "tmp": "^0.0.33", "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" } }, "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
@@ -1873,6 +1862,8 @@
"react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="],
"react-native-collapsible": ["react-native-collapsible@1.6.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MCOBVJWqHNjnDaGkvxX997VONmJeebh6wyJxnHEgg0L1PrlcXU1e/bo6eK+CDVFuMrCafw8Qh4DOv/C4V/+Iew=="],
"react-native-compressor": ["react-native-compressor@1.10.4", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-58gbmJ+8IvsKP8JKK1E8XW5trfQY3dNuH7S0hYw0tSRQc6l0GZ3k8TYtoUbySOc1xcQSrUo51o0Chwe8x7mUTg=="],
"react-native-country-flag": ["react-native-country-flag@2.0.2", "", {}, "sha512-5LMWxS79ZQ0Q9ntYgDYzWp794+HcQGXQmzzZNBR1AT7z5HcJHtX7rlk8RHi7RVzfp5gW6plWSZ4dKjRpu/OafQ=="],
@@ -1995,7 +1986,7 @@
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"rtl-detect": ["rtl-detect@1.1.2", "", {}, "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ=="],
@@ -2061,7 +2052,7 @@
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="],
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
@@ -2353,8 +2344,6 @@
"@expo/config-plugins/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"@expo/config-plugins/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"@expo/devcert/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"@expo/devcert/sudo-prompt": ["sudo-prompt@8.2.5", "", {}, "sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw=="],
@@ -2403,8 +2392,6 @@
"@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
"@jest/transform/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"@jest/transform/write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="],
"@npmcli/fs/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
@@ -2437,8 +2424,6 @@
"@react-native-community/cli-server-api/pretty-format": ["pretty-format@26.6.2", "", { "dependencies": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^17.0.1" } }, "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg=="],
"@react-native-community/cli-tools/open": ["open@6.4.0", "", { "dependencies": { "is-wsl": "^1.1.0" } }, "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg=="],
"@react-native-community/cli-tools/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.76.7", "", { "dependencies": { "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.23.1", "invariant": "^2.2.4", "jscodeshift": "^0.14.0", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" } }, "sha512-FAn585Ll65YvkSrKDyAcsdjHhhAGiMlSTUpHh0x7J5ntudUns+voYms0xMP+pEPt0XuLdjhD7zLIIlAWP407+g=="],
@@ -2453,6 +2438,8 @@
"@react-native/dev-middleware/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
"@react-native/metro-babel-transformer/@react-native/babel-preset": ["@react-native/babel-preset@0.77.0", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.77.0", "babel-plugin-syntax-hermes-parser": "0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-Z4yxE66OvPyQ/iAlaETI1ptRLcDm7Tk6ZLqtCPuUX3AMg+JNgIA86979T4RSk486/JrBUBH5WZe2xjj7eEHXsA=="],
"@react-navigation/core/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
@@ -2465,8 +2452,6 @@
"ansi-fragments/slice-ansi": ["slice-ansi@2.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" } }, "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ=="],
"babel-jest/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
"cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@@ -2477,8 +2462,6 @@
"chromium-edge-launcher/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"chromium-edge-launcher/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -2495,10 +2478,6 @@
"default-gateway/execa": ["execa@1.0.0", "", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="],
"del/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"del/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"error-ex/is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
"expo-build-properties/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
@@ -2527,8 +2506,6 @@
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@@ -2537,8 +2514,6 @@
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"jest-message-util/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"jscodeshift/tmp": ["tmp@0.2.3", "", {}, "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w=="],
@@ -2595,16 +2570,14 @@
"npm-package-arg/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"open/is-wsl": ["is-wsl@1.1.0", "", {}, "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw=="],
"ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
"password-prompt/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
"patch-package/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"patch-package/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"pkg-dir/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
@@ -2783,8 +2756,6 @@
"@react-native-community/cli-server-api/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"@react-native-community/cli-tools/open/is-wsl": ["is-wsl@1.1.0", "", {}, "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw=="],
"@react-native/babel-plugin-codegen/@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser": ["hermes-parser@0.23.1", "", { "dependencies": { "hermes-estree": "0.23.1" } }, "sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA=="],
@@ -2795,6 +2766,8 @@
"@react-native/community-cli-plugin/@react-native/dev-middleware/@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.77.0", "", {}, "sha512-glOvSEjCbVXw+KtfiOAmrq21FuLE1VsmBsyT7qud4KWbXP43aUEhzn70mWyFuiIdxnzVPKe2u8iWTQTdJksR1w=="],
"@react-native/community-cli-plugin/@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
"@react-native/community-cli-plugin/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"@react-native/dev-middleware/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
@@ -2805,8 +2778,6 @@
"ansi-fragments/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="],
"chromium-edge-launcher/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"cli-truncate/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
@@ -2823,8 +2794,6 @@
"default-gateway/execa/npm-run-path": ["npm-run-path@2.0.2", "", { "dependencies": { "path-key": "^2.0.0" } }, "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw=="],
"del/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"expo-modules-autolinking/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
"expo-modules-autolinking/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
@@ -2885,10 +2854,6 @@
"password-prompt/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"patch-package/fs-extra/jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
"patch-package/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"pkg-dir/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
"react-native-tvos/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
@@ -2949,8 +2914,6 @@
"ansi-fragments/slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
"chromium-edge-launcher/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"default-gateway/execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="],
@@ -2963,8 +2926,6 @@
"default-gateway/execa/npm-run-path/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="],
"del/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"lint-staged/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"lint-staged/execa/onetime/mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
@@ -3021,12 +2982,8 @@
"ansi-fragments/slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"chromium-edge-launcher/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"default-gateway/execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="],
"del/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"logkitty/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],

View File

@@ -73,7 +73,7 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
flex flex-row items-center justify-between w-full
${justify === "between" ? "justify-between" : "justify-center"}`}
>
{iconLeft ? iconLeft : <View className='w-4'></View>}
{iconLeft ? iconLeft : <View className='w-4' />}
<Text
className={`
text-white font-bold text-base
@@ -85,7 +85,7 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
>
{children}
</Text>
{iconRight ? iconRight : <View className='w-4'></View>}
{iconRight ? iconRight : <View className='w-4' />}
</View>
)}
</TouchableOpacity>

View File

@@ -27,33 +27,39 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
* Get horizontal poster for movie and episode, with failover to primary.
*/
const url = useMemo(() => {
if (!api) return;
if (!api) {
return;
}
if (item.Type === "Episode" && useEpisodePoster) {
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
}
if (item.Type === "Episode") {
if (item.ParentBackdropItemId && item.ParentThumbImageTag)
if (item.ParentBackdropItemId && item.ParentThumbImageTag) {
return `${api?.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ParentThumbImageTag}`;
else
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
}
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
}
if (item.Type === "Movie") {
if (item.ImageTags?.["Thumb"])
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.["Thumb"]}`;
else
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
if (item.ImageTags?.Thumb) {
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.Thumb}`;
}
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
}
if (item.Type === "Program") {
if (item.ImageTags?.["Thumb"])
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.["Thumb"]}`;
else
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
if (item.ImageTags?.Thumb) {
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.Thumb}`;
}
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
}
if (item.ImageTags?.["Thumb"])
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.["Thumb"]}`;
else
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
if (item.ImageTags?.Thumb) {
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.Thumb}`;
}
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
}, [item]);
const progress = useMemo(() => {
@@ -64,15 +70,12 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
const total = endDate.getTime() - startDate.getTime();
const elapsed = now.getTime() - startDate.getTime();
return (elapsed / total) * 100;
} else {
return item.UserData?.PlayedPercentage || 0;
}
return item.UserData?.PlayedPercentage || 0;
}, [item]);
if (!url)
return (
<View className='aspect-video border border-neutral-800 w-44'></View>
);
return <View className='aspect-video border border-neutral-800 w-44' />;
return (
<View
@@ -102,14 +105,16 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
{progress > 0 && (
<>
<View
className={`absolute w-100 bottom-0 left-0 h-1 bg-neutral-700 opacity-80 w-full`}
></View>
className={
"absolute w-100 bottom-0 left-0 h-1 bg-neutral-700 opacity-80 w-full"
}
/>
<View
style={{
width: `${progress}%`,
}}
className={`absolute bottom-0 left-0 h-1 bg-purple-600 w-full`}
></View>
className={"absolute bottom-0 left-0 h-1 bg-purple-600 w-full"}
/>
</>
)}
</View>

View File

@@ -1,4 +1,4 @@
import { useRemuxHlsToMp4 } from "@/hooks/useRemuxHlsToMp4";
//import { useRemuxHlsToMp4 } from "@/hooks/useRemuxHlsToMp4";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { queueActions, queueAtom } from "@/utils/atoms/queue";
@@ -59,7 +59,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
const [settings] = useSettings();
const { processes, startBackgroundDownload, downloadedFiles } = useDownload();
const { startRemuxing } = useRemuxHlsToMp4();
//const { startRemuxing } = useRemuxHlsToMp4();
const [selectedMediaSource, setSelectedMediaSource] = useState<
MediaSourceInfo | undefined | null
@@ -113,7 +113,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
);
const progress = useMemo(() => {
if (itemIds.length == 1)
if (itemIds.length === 1)
return itemsProcesses.reduce((acc, p) => acc + p.progress, 0);
return (
((itemIds.length -
@@ -126,7 +126,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
const itemsQueued = useMemo(() => {
return (
itemsNotDownloaded.length > 0 &&
itemsNotDownloaded.every((p) => queue.some((q) => p.Id == q.item.Id))
itemsNotDownloaded.every((p) => queue.some((q) => p.Id === q.item.Id))
);
}, [queue, itemsNotDownloaded]);
const navigateToDownloads = () => router.push("/downloads");
@@ -230,12 +230,11 @@ export const DownloadItems: React.FC<DownloadProps> = ({
if (!url || !source) throw new Error("No url");
saveDownloadItemInfoToDiskTmp(item, source, url);
if (usingOptimizedServer) {
saveDownloadItemInfoToDiskTmp(item, source, url);
await startBackgroundDownload(url, item, source);
} else {
await startRemuxing(item, url, source);
//await startRemuxing(item, url, source);
}
}
},
@@ -250,7 +249,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
maxBitrate,
usingOptimizedServer,
startBackgroundDownload,
startRemuxing,
//startRemuxing,
],
);
@@ -279,7 +278,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
);
const renderButtonContent = () => {
if (processes && itemsProcesses.length > 0) {
if (processes.length > 0 && itemsProcesses.length > 0) {
return progress === 0 ? (
<Loader />
) : (
@@ -293,13 +292,17 @@ export const DownloadItems: React.FC<DownloadProps> = ({
/>
</View>
);
} else if (itemsQueued) {
return <Ionicons name='hourglass' size={24} color='white' />;
} else if (allItemsDownloaded) {
return <DownloadedIconComponent />;
} else {
return <MissingDownloadIconComponent />;
}
if (itemsQueued) {
return <Ionicons name='hourglass' size={24} color='white' />;
}
if (allItemsDownloaded) {
return <DownloadedIconComponent />;
}
return <MissingDownloadIconComponent />;
};
const onButtonPress = () => {
@@ -405,7 +408,7 @@ export const DownloadSingleItem: React.FC<{
<DownloadItems
size={size}
title={
item.Type == "Episode"
item.Type === "Episode"
? t("item_card.download.download_episode")
: t("item_card.download.download_movie")
}

View File

@@ -147,22 +147,20 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
</View>
}
logo={
<>
{logoUrl ? (
<Image
source={{
uri: logoUrl,
}}
style={{
height: 130,
width: "100%",
resizeMode: "contain",
}}
onLoad={() => setLoadingLogo(false)}
onError={() => setLoadingLogo(false)}
/>
) : null}
</>
logoUrl ? (
<Image
source={{
uri: logoUrl,
}}
style={{
height: 130,
width: "100%",
resizeMode: "contain",
}}
onLoad={() => setLoadingLogo(false)}
onError={() => setLoadingLogo(false)}
/>
) : null
}
>
<View className='flex flex-col bg-transparent shrink'>

View File

@@ -237,5 +237,5 @@ const formatFileSize = (bytes?: number | null) => {
const i = Number.parseInt(
Math.floor(Math.log(bytes) / Math.log(1024)).toString(),
);
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + " " + sizes[i];
return `${Math.round((bytes / 1024 ** i) * 100) / 100} ${sizes[i]}`;
};

View File

@@ -63,9 +63,8 @@ export const MoreMoviesWithActor: React.FC<Props> = ({
const x = acc.find((item) => item.Id === current.Id);
if (!x) {
return acc.concat([current]);
} else {
return acc;
}
return acc;
}, [] as BaseItemDto[]) || [];
return uniqueItems;

View File

@@ -239,7 +239,7 @@ export const PlayButton: React.FC<Props> = ({
const derivedTargetWidth = useDerivedValue(() => {
if (!item || !item.RunTimeTicks) return 0;
const userData = item.UserData;
if (userData && userData.PlaybackPositionTicks) {
if (userData?.PlaybackPositionTicks) {
return userData.PlaybackPositionTicks > 0
? Math.max(
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
@@ -331,7 +331,7 @@ export const PlayButton: React.FC<Props> = ({
accessibilityLabel='Play button'
accessibilityHint='Tap to play the media'
onPress={onPress}
className={`relative`}
className={"relative"}
{...props}
>
<View className='absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden'>

View File

@@ -85,7 +85,7 @@ export const PlayButton: React.FC<Props> = ({
const derivedTargetWidth = useDerivedValue(() => {
if (!item || !item.RunTimeTicks) return 0;
const userData = item.UserData;
if (userData && userData.PlaybackPositionTicks) {
if (userData?.PlaybackPositionTicks) {
return userData.PlaybackPositionTicks > 0
? Math.max(
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
@@ -176,7 +176,7 @@ export const PlayButton: React.FC<Props> = ({
accessibilityLabel='Play button'
accessibilityHint='Tap to play the media'
onPress={onPress}
className={`relative`}
className={"relative"}
{...props}
>
<View className='absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden'>

View File

@@ -7,7 +7,7 @@ export const WatchedIndicator: React.FC<{ item: BaseItemDto }> = ({ item }) => {
<>
{item.UserData?.Played === false &&
(item.Type === "Movie" || item.Type === "Episode") && (
<View className='bg-purple-600 w-8 h-8 absolute -top-4 -right-4 rotate-45'></View>
<View className='bg-purple-600 w-8 h-8 absolute -top-4 -right-4 rotate-45' />
)}
</>
);

View File

@@ -3,7 +3,7 @@ import renderer from "react-test-renderer";
import { ThemedText } from "../ThemedText";
it(`renders correctly`, () => {
it("renders correctly", () => {
const tree = renderer
.create(<ThemedText>Snapshot test!</ThemedText>)
.toJSON();

View File

@@ -6,7 +6,7 @@ interface Props extends ViewProps {}
export const TitleHeader: React.FC<Props> = ({ ...props }) => {
return (
<View {...props}>
<Text></Text>
<Text />
</View>
);
};

View File

@@ -75,7 +75,7 @@ const Dropdown = <T,>({
multiple ? (
<DropdownMenu.CheckboxItem
value={
selected?.some((s) => keyExtractor(s) == keyExtractor(item))
selected?.some((s) => keyExtractor(s) === keyExtractor(item))
? "on"
: "off"
}
@@ -83,7 +83,7 @@ const Dropdown = <T,>({
onValueChange={(next: "on" | "off", previous: "on" | "off") => {
setSelected((p) => {
const prev = p || [];
if (next == "on") {
if (next === "on") {
return [...prev, item];
}
return [

View File

@@ -65,17 +65,13 @@ export const HorizontalScroll = forwardRef<
}: {
item: T;
index: number;
}) => (
<View className='mr-2'>
<React.Fragment>{renderItem(item, index)}</React.Fragment>
</View>
);
}) => <View className='mr-2'>{renderItem(item, index)}</View>;
if (!data || loading) {
return (
<View className='px-4 mb-2'>
<View className='bg-neutral-950 h-24 w-full rounded-md mb-2'></View>
<View className='bg-neutral-950 h-10 w-full rounded-md mb-1'></View>
<View className='bg-neutral-950 h-24 w-full rounded-md mb-2' />
<View className='bg-neutral-950 h-10 w-full rounded-md mb-1' />
</View>
);
}

View File

@@ -75,9 +75,8 @@ export function InfiniteHorizontalScroll({
if (accumulatedItems < totalItems) {
return lastPage?.Items?.length * pages.length;
} else {
return undefined;
}
return undefined;
},
initialPageParam: 0,
enabled: !!api && !!user?.Id,
@@ -118,9 +117,7 @@ export function InfiniteHorizontalScroll({
<FlashList
data={flatData}
renderItem={({ item, index }) => (
<View className='mr-2'>
<React.Fragment>{renderItem(item, index)}</React.Fragment>
</View>
<View className='mr-2'>{renderItem(item, index)}</View>
)}
estimatedItemSize={height}
horizontal

View File

@@ -36,7 +36,7 @@ export const ItemImage: FC<Props> = ({
const source = useMemo(() => {
if (!api) {
onError && onError();
onError?.();
return;
}
return getItemImage({

View File

@@ -5,7 +5,7 @@ export const LargePoster: React.FC<{ url?: string | null }> = ({ url }) => {
if (!url)
return (
<View className='p-4 rounded-xl overflow-hidden '>
<View className='w-full aspect-video rounded-xl overflow-hidden border border-neutral-800'></View>
<View className='w-full aspect-video rounded-xl overflow-hidden border border-neutral-800' />
</View>
);

View File

@@ -16,12 +16,12 @@ export function Text(
{...otherProps}
/>
);
else
return (
<UITextView
allowFontScaling={false}
style={[{ color: "white" }, style]}
{...otherProps}
/>
);
return (
<UITextView
allowFontScaling={false}
style={[{ color: "white" }, style]}
{...otherProps}
/>
);
}

View File

@@ -20,10 +20,10 @@ export const VerticalSkeleton: React.FC<Props> = ({ index, ...props }) => {
aspectRatio: "10/15",
}}
className='w-full bg-neutral-800 mb-2 rounded-lg'
></View>
<View className='h-2 bg-neutral-800 rounded-full mb-1'></View>
<View className='h-2 bg-neutral-800 rounded-full mb-1'></View>
<View className='h-2 bg-neutral-800 rounded-full mb-2 w-1/2'></View>
/>
<View className='h-2 bg-neutral-800 rounded-full mb-1' />
<View className='h-2 bg-neutral-800 rounded-full mb-1' />
<View className='h-2 bg-neutral-800 rounded-full mb-2 w-1/2' />
</View>
);
};

View File

@@ -23,9 +23,9 @@ import { Button } from "../Button";
const BackGroundDownloader = !Platform.isTV
? require("@kesha-antonov/react-native-background-downloader")
: null;
const FFmpegKitProvider = !Platform.isTV
? require("ffmpeg-kit-react-native")
: null;
//const FFmpegKitProvider = !Platform.isTV
// ? require("ffmpeg-kit-react-native")
// : null;
interface Props extends ViewProps {}
@@ -80,14 +80,12 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
task.stop();
}
}
} catch (e) {
throw e;
} finally {
await removeProcess(id);
await queryClient.refetchQueries({ queryKey: ["jobs"] });
}
} else {
FFmpegKitProvider.FFmpegKit.cancel(Number(id));
//FFmpegKitProvider.FFmpegKit.cancel(Number(id));
setProcesses((prev: any[]) =>
prev.filter((p: { id: string }) => p.id !== id),
);
@@ -131,7 +129,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
? `${Math.max(5, process.progress)}%`
: "5%",
}}
></View>
/>
)}
<View className='px-3 py-1.5 flex flex-col w-full'>
<View className='flex flex-row items-center w-full'>

View File

@@ -31,7 +31,7 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {
destructiveButtonIndex,
},
(selectedIndex) => {
if (selectedIndex == destructiveButtonIndex) {
if (selectedIndex === destructiveButtonIndex) {
deleteSeries();
}
},

View File

@@ -6,7 +6,7 @@ import { TouchableOpacity, View, type ViewProps } from "react-native";
import { FilterSheet } from "./FilterSheet";
interface FilterButtonProps<T> extends ViewProps {
collectionId: string;
id: string;
showSearch?: boolean;
queryKey: string;
values: T[];
@@ -15,11 +15,12 @@ interface FilterButtonProps<T> extends ViewProps {
queryFn: (params: any) => Promise<any>;
searchFilter?: (item: T, query: string) => boolean;
renderItemLabel: (item: T) => React.ReactNode;
multiple?: boolean;
icon?: "filter" | "sort";
}
export const FilterButton = <T,>({
collectionId,
id,
queryFn,
queryKey,
set,
@@ -28,16 +29,17 @@ export const FilterButton = <T,>({
renderItemLabel,
searchFilter,
showSearch = true,
multiple = false,
icon = "filter",
...props
}: FilterButtonProps<T>) => {
const [open, setOpen] = useState(false);
const { data: filters } = useQuery<T[]>({
queryKey: ["filters", title, queryKey, collectionId],
queryKey: ["filters", title, queryKey, id],
queryFn,
staleTime: 0,
enabled: !!collectionId && !!queryFn && !!queryKey,
enabled: !!id && !!queryFn && !!queryKey,
});
return (
@@ -93,6 +95,7 @@ export const FilterButton = <T,>({
renderItemLabel={renderItemLabel}
searchFilter={searchFilter}
showSearch={showSearch}
multiple={multiple}
/>
</>
);

View File

@@ -31,6 +31,7 @@ interface Props<T> extends ViewProps {
searchFilter?: (item: T, query: string) => boolean;
renderItemLabel: (item: T) => React.ReactNode;
showSearch?: boolean;
multiple?: boolean;
}
const LIMIT = 100;
@@ -73,6 +74,7 @@ export const FilterSheet = <T,>({
searchFilter,
renderItemLabel,
showSearch = true,
multiple = false,
...props
}: Props<T>) => {
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
@@ -180,11 +182,20 @@ export const FilterSheet = <T,>({
<View key={index}>
<TouchableOpacity
onPress={() => {
if (!values.includes(item)) {
set([item]);
if (multiple) {
if (!values.includes(item)) set(values.concat(item));
else set(values.filter((v) => v !== item));
setTimeout(() => {
setOpen(false);
}, 250);
} else {
if (!values.includes(item)) {
set([item]);
setTimeout(() => {
setOpen(false);
}, 250);
}
}
}}
className=' bg-neutral-800 px-4 py-3 flex flex-row items-center justify-between'
@@ -201,7 +212,7 @@ export const FilterSheet = <T,>({
height: StyleSheet.hairlineWidth,
}}
className='h-1 divide-neutral-700 '
></View>
/>
</View>
))}
</View>

View File

@@ -63,7 +63,7 @@ export const ScrollingCollectionList: React.FC<Props> = ({
>
{[1, 2, 3].map((i) => (
<View className='w-44' key={i}>
<View className='bg-neutral-900 h-24 w-full rounded-md mb-1'></View>
<View className='bg-neutral-900 h-24 w-full rounded-md mb-1' />
<View className='rounded-md overflow-hidden mb-1 self-start'>
<Text
className='text-neutral-900 bg-neutral-900 rounded-md'

View File

@@ -34,10 +34,7 @@ export const Stepper: React.FC<StepperProps> = ({
<Text>-</Text>
</TouchableOpacity>
<Text
className={
"w-auto h-8 bg-neutral-800 py-2 px-1 flex items-center justify-center" +
(appendValue ? "first-letter:px-2" : "")
}
className={`w-auto h-8 bg-neutral-800 py-2 px-1 flex items-center justify-center${appendValue ? "first-letter:px-2" : ""}`}
>
{value}
{appendValue}

View File

@@ -52,7 +52,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({
} = useReactNavigationQuery({
queryKey: ["search", "jellyseerr", "discoverSettings", searchQuery],
queryFn: async () => jellyseerrApi?.discoverSettings(),
enabled: !!jellyseerrApi && searchQuery.length == 0,
enabled: !!jellyseerrApi && searchQuery.length === 0,
});
const {
@@ -110,7 +110,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({
(r) => r.mediaType === MediaType.MOVIE,
) as MovieResult[],
sortingType || [
(m) => m.title.toLowerCase() == searchQuery.toLowerCase(),
(m) => m.title.toLowerCase() === searchQuery.toLowerCase(),
],
order || "desc",
),
@@ -124,7 +124,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({
(r) => r.mediaType === MediaType.TV,
) as TvResult[],
sortingType || [
(t) => t.name.toLowerCase() == searchQuery.toLowerCase(),
(t) => t.name.toLowerCase() === searchQuery.toLowerCase(),
],
order || "desc",
),
@@ -138,7 +138,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({
(r) => r.mediaType === "person",
) as PersonResult[],
sortingType || [
(p) => p.name.toLowerCase() == searchQuery.toLowerCase(),
(p) => p.name.toLowerCase() === searchQuery.toLowerCase(),
],
order || "desc",
),

View File

@@ -62,7 +62,7 @@ const JellyseerrStatusIcon: React.FC<Props & ViewProps> = ({
return (
badgeIcon && (
<TouchableOpacity onPress={onPress} disabled={onPress == undefined}>
<TouchableOpacity onPress={onPress} disabled={onPress === undefined}>
<View
className={`${badgeStyle ?? "bg-purple-600"} rounded-full h-6 w-6 flex items-center justify-center ${props.className}`}
{...props}

View File

@@ -123,11 +123,9 @@ const ParallaxSlideShow = <T,>({
>
<View className='flex flex-col space-y-4 px-4'>
<View className='flex flex-row justify-between w-full'>
<View className='flex flex-col w-full'>
{HeaderContent && HeaderContent()}
</View>
<View className='flex flex-col w-full'>{HeaderContent?.()}</View>
</View>
{MainContent && MainContent()}
{MainContent?.()}
<View>
<FlashList
data={data}

View File

@@ -9,6 +9,7 @@ import type {
} from "@/utils/jellyseerr/server/api/servarr/base";
import type { MediaType } from "@/utils/jellyseerr/server/constants/media";
import type { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
import { writeDebugLog } from "@/utils/log";
import {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
@@ -61,7 +62,7 @@ const RequestModal = forwardRef<
const { data: serviceSettings } = useQuery({
queryKey: ["jellyseerr", "request", type, "service"],
queryFn: async () =>
jellyseerrApi?.service(type == "movie" ? "radarr" : "sonarr"),
jellyseerrApi?.service(type === "movie" ? "radarr" : "sonarr"),
enabled: !!jellyseerrApi && !!jellyseerrUser,
refetchOnMount: "always",
});
@@ -147,16 +148,20 @@ const RequestModal = forwardRef<
}, [requestBody?.seasons]);
const request = useCallback(() => {
const body = {
is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k,
profileId: defaultProfile?.id,
rootFolder: defaultFolder?.path,
tags: defaultTags.map((t) => t.id),
...requestBody,
...requestOverrides,
};
writeDebugLog("Sending Jellyseerr advanced request", body);
requestMedia(
seasonTitle ? `${title}, ${seasonTitle}` : title,
{
is4k: defaultService?.is4k || defaultServiceDetails?.server.is4k,
profileId: defaultProfile.id,
rootFolder: defaultFolder.path,
tags: defaultTags.map((t) => t.id),
...requestBody,
...requestOverrides,
},
body,
onRequested,
);
}, [

View File

@@ -28,7 +28,7 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
queryKey: ["jellyseerr", "discover", slide.type, slide.id],
queryFn: async () => {
return jellyseerrApi?.getGenreSliders(
slide.type == DiscoverSliderType.MOVIE_GENRES
slide.type === DiscoverSliderType.MOVIE_GENRES
? Endpoints.MOVIE
: Endpoints.TV,
);

View File

@@ -23,7 +23,7 @@ const RequestCard: React.FC<{ request: MediaRequest }> = ({ request }) => {
request.media.tmdbId,
],
queryFn: async () => {
return request.media.mediaType == MediaType.MOVIE
return request.media.mediaType === MediaType.MOVIE
? jellyseerrApi?.movieDetails(request.media.tmdbId)
: jellyseerrApi?.tvDetails(request.media.tmdbId);
},

View File

@@ -35,7 +35,7 @@ const Slide = <T,>({
return (
<View {...props}>
<Text className='font-bold text-lg mb-2 px-4'>
{t("search." + DiscoverSliderType[slide.type].toString().toLowerCase())}
{t(`search.${DiscoverSliderType[slide.type].toString().toLowerCase()}`)}
</Text>
<FlashList
horizontal

View File

@@ -29,8 +29,8 @@ export const EpisodePoster: React.FC<MoviePosterProps> = ({
);
const blurhash = useMemo(() => {
const key = item.ImageTags?.["Primary"] as string;
return item.ImageBlurHashes?.["Primary"]?.[key];
const key = item.ImageTags?.Primary as string;
return item.ImageBlurHashes?.Primary?.[key];
}, [item]);
return (
@@ -57,7 +57,7 @@ export const EpisodePoster: React.FC<MoviePosterProps> = ({
/>
<WatchedIndicator item={item} />
{showProgress && progress > 0 && (
<View className='h-1 bg-red-600 w-full'></View>
<View className='h-1 bg-red-600 w-full' />
)}
</View>
);

View File

@@ -37,7 +37,7 @@ export const ItemPoster: React.FC<Props> = ({
/>
<WatchedIndicator item={item} />
{showProgress && progress > 0 && (
<View className='h-1 bg-red-600 w-full'></View>
<View className='h-1 bg-red-600 w-full' />
)}
</View>
);

View File

@@ -129,7 +129,7 @@ const JellyseerrPoster: React.FC<Props> = ({
posterSrc={posterSrc!}
mediaType={mediaType}
>
<View className={`flex flex-col mr-2 h-auto`}>
<View className={"flex flex-col mr-2 h-auto"}>
<View
className={`relative rounded-lg overflow-hidden border border-neutral-900 ${size} aspect-[${ratio}]`}
>

View File

@@ -31,8 +31,8 @@ const MoviePoster: React.FC<MoviePosterProps> = ({
);
const blurhash = useMemo(() => {
const key = item.ImageTags?.["Primary"] as string;
return item.ImageBlurHashes?.["Primary"]?.[key];
const key = item.ImageTags?.Primary as string;
return item.ImageBlurHashes?.Primary?.[key];
}, [item]);
return (
@@ -59,7 +59,7 @@ const MoviePoster: React.FC<MoviePosterProps> = ({
/>
<WatchedIndicator item={item} />
{showProgress && progress > 0 && (
<View className='h-1 bg-red-600 w-full'></View>
<View className='h-1 bg-red-600 w-full' />
)}
</View>
);

View File

@@ -24,7 +24,7 @@ const ParentPoster: React.FC<PosterProps> = ({ id }) => {
style={{
aspectRatio: "10/15",
}}
></View>
/>
);
return (

View File

@@ -16,7 +16,7 @@ const Poster: React.FC<PosterProps> = ({ id, url, blurhash }) => {
style={{
aspectRatio: "10/15",
}}
></View>
/>
);
return (

View File

@@ -27,8 +27,8 @@ const SeriesPoster: React.FC<MoviePosterProps> = ({ item }) => {
}, [item]);
const blurhash = useMemo(() => {
const key = item.ImageTags?.["Primary"] as string;
return item.ImageBlurHashes?.["Primary"]?.[key];
const key = item.ImageTags?.Primary as string;
return item.ImageBlurHashes?.Primary?.[key];
}, [item]);
return (

View File

@@ -35,11 +35,11 @@ export const LoadingSkeleton: React.FC<Props> = ({ isLoading }) => {
<Animated.View style={animatedStyle} className='mt-2 absolute w-full'>
{[1, 2, 3].map((s) => (
<View className='px-4 mb-4' key={s}>
<View className='w-1/2 bg-neutral-900 h-6 mb-2 rounded-lg'></View>
<View className='w-1/2 bg-neutral-900 h-6 mb-2 rounded-lg' />
<View className='flex flex-row gap-2'>
{[1, 2, 3].map((i) => (
<View className='w-28' key={i}>
<View className='bg-neutral-900 h-40 w-full rounded-md mb-1'></View>
<View className='bg-neutral-900 h-40 w-full rounded-md mb-1' />
<View className='rounded-md overflow-hidden mb-1 self-start'>
<Text
className='text-neutral-900 bg-neutral-900 rounded-md'

View File

@@ -57,7 +57,7 @@ export const NextItemButton: React.FC<Props> = ({
return (
<Button
onPress={() => router.setParams({ id: nextItem?.Id })}
className={`h-12 aspect-square`}
className={"h-12 aspect-square"}
disabled={disabled}
{...props}
>

View File

@@ -146,7 +146,7 @@ export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
}));
}}
/>
{episodes?.length || 0 > 0 ? (
{episodes?.length ? (
<View className='flex flex-row items-center space-x-2'>
<DownloadItems
title={t("item_card.download.download_season")}

View File

@@ -19,7 +19,7 @@ export const Dashboard = () => {
<View>
<ListGroup title={t("home.settings.dashboard.title")} className='mt-4'>
<ListItem
className={sessions.length != 0 ? "bg-purple-900" : ""}
className={sessions.length !== 0 ? "bg-purple-900" : ""}
onPress={() => router.push("/settings/dashboard/sessions")}
title={t("home.settings.dashboard.sessions_title")}
showArrow

View File

@@ -140,7 +140,7 @@ export default function DownloadSettings({ ...props }) {
onPress={() => router.push("/settings/optimized-server/page")}
showArrow
title={t("home.settings.downloads.optimized_versions_server")}
></ListItem>
/>
</ListGroup>
</DisabledSetting>
);

View File

@@ -51,13 +51,13 @@ type ScrollingCollectionListSection = {
orientation?: "horizontal" | "vertical";
};
type MediaListSection = {
type MediaListSectionType = {
type: "MediaListSection";
queryKey: (string | undefined)[];
queryFn: QueryFunction<BaseItemDto>;
};
type Section = ScrollingCollectionListSection | MediaListSection;
type Section = ScrollingCollectionListSection | MediaListSectionType;
export const HomeIndex = () => {
const router = useRouter();
@@ -133,7 +133,7 @@ export const HomeIndex = () => {
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
if (state.isConnected == false || state.isInternetReachable === false)
if (state.isConnected === false || state.isInternetReachable === false)
setIsConnected(false);
else setIsConnected(true);
});
@@ -236,7 +236,7 @@ export const HomeIndex = () => {
const title = t("home.recently_added_in", { libraryName: c.Name });
const queryKey = [
"home",
"recentlyAddedIn" + c.CollectionType,
`recentlyAddedIn${c.CollectionType}`,
user?.Id!,
c.Id!,
];
@@ -353,17 +353,29 @@ export const HomeIndex = () => {
parentId: section.items?.parentId,
});
return response.data.Items || [];
} else if (section.nextUp) {
}
if (section.nextUp) {
const response = await getTvShowsApi(api).getNextUp({
userId: user?.Id,
fields: ["MediaSourceCount"],
limit: section.items?.limit || 25,
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
enableResumable: section.items?.enableResumable || false,
enableRewatching: section.items?.enableRewatching || false,
enableResumable: section.items?.enableResumable,
enableRewatching: section.items?.enableRewatching,
});
return response.data.Items || [];
}
if (section.latest) {
const response = await getUserLibraryApi(api).getLatestMedia({
userId: user?.Id,
includeItemTypes: section.latest?.includeItemTypes,
limit: section.latest?.limit || 25,
isPlayed: section.latest?.isPlayed,
groupItems: section.latest?.groupItems,
});
return response.data || [];
}
return [];
},
type: "ScrollingCollectionList",
@@ -463,7 +475,8 @@ export const HomeIndex = () => {
hideIfEmpty
/>
);
} else if (section.type === "MediaListSection") {
}
if (section.type === "MediaListSection") {
return (
<MediaListSection
key={index}

View File

@@ -20,7 +20,7 @@ export const OptimizedServerForm: React.FC<Props> = ({
return (
<View>
<View className='flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4'>
<View className={`flex flex-row items-center bg-neutral-900 h-11 pr-4`}>
<View className={"flex flex-row items-center bg-neutral-900 h-11 pr-4"}>
<Text className='mr-4'>{t("home.settings.downloads.url")}</Text>
<TextInput
className='text-white'

View File

@@ -81,7 +81,7 @@ export const StorageSettings = () => {
{size && (
<>
<View className='flex flex-row items-center'>
<View className='w-3 h-3 rounded-full bg-purple-600 mr-1'></View>
<View className='w-3 h-3 rounded-full bg-purple-600 mr-1' />
<Text className='text-white text-xs'>
{t("home.settings.storage.app_usage", {
usedSpace: calculatePercentage(size.app, size.total),
@@ -89,7 +89,7 @@ export const StorageSettings = () => {
</Text>
</View>
<View className='flex flex-row items-center'>
<View className='w-3 h-3 rounded-full bg-purple-400 mr-1'></View>
<View className='w-3 h-3 rounded-full bg-purple-400 mr-1' />
<Text className='text-white text-xs'>
{t("home.settings.storage.device_usage", {
availableSpace: calculatePercentage(

View File

@@ -1,5 +1,5 @@
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
import Application from "expo-application";
import * as Application from "expo-application";
import Constants from "expo-constants";
import { useAtom } from "jotai";
import { useTranslation } from "react-i18next";

View File

@@ -168,7 +168,7 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
style={{
justifyContent: "space-between",
}}
className={`flex flex-row items-center space-x-2 z-10 p-4`}
className={"flex flex-row items-center space-x-2 z-10 p-4"}
>
{seriesItem && (
<SeasonDropdown

View File

@@ -47,7 +47,7 @@ const SliderScrubber: React.FC<SliderScrubberProps> = ({
};
return (
<View className={`flex flex-col w-full shrink`}>
<View className={"flex flex-col w-full shrink"}>
<Slider
theme={{
maximumTrackTintColor: "rgba(255,255,255,0.2)",

View File

@@ -115,7 +115,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
});
return;
}
setTrack && setTrack(index);
setTrack?.(index);
router.setParams({
[paramKey]: serverIndex.toString(),
});

View File

@@ -32,8 +32,8 @@ export const useTapDetection = ({
const touchDuration = touchEndTime - touchStartTime.current;
const touchDistance = Math.sqrt(
Math.pow(touchEndPosition.x - touchStartPosition.current.x, 2) +
Math.pow(touchEndPosition.y - touchStartPosition.current.y, 2),
(touchEndPosition.x - touchStartPosition.current.x) ** 2 +
(touchEndPosition.y - touchStartPosition.current.y) ** 2,
);
if (touchDuration < maxDuration && touchDistance < maxDistance) {

View File

@@ -45,19 +45,17 @@ export const VideoDebugInfo: React.FC<Props> = ({ playerRef, ...props }) => {
>
<Text className='font-bold'>{t("player.playback_state")}</Text>
<Text className='font-bold mt-2.5'>{t("player.audio_tracks")}</Text>
{audioTracks &&
audioTracks.map((track, index) => (
<Text key={index}>
{track.name} ({t("player.index")} {track.index})
</Text>
))}
{audioTracks?.map((track, index) => (
<Text key={index}>
{track.name} ({t("player.index")} {track.index})
</Text>
))}
<Text className='font-bold mt-2.5'>{t("player.subtitles_tracks")}</Text>
{subtitleTracks &&
subtitleTracks.map((track, index) => (
<Text key={index}>
{track.name} ({t("player.index")} {track.index})
</Text>
))}
{subtitleTracks?.map((track, index) => (
<Text key={index}>
{track.name} ({t("player.index")} {track.index})
</Text>
))}
<TouchableOpacity
className='mt-2.5 bg-blue-500 p-2 rounded'
onPress={() => {

View File

@@ -5,47 +5,64 @@
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"development_tv": {
"environment": "development",
"developmentClient": true,
"distribution": "internal",
"android": {
"buildType": "apk"
},
"env": {
"EXPO_TV": "1"
"EXPO_PUBLIC_WRITE_DEBUG": "1"
}
},
"development_tv": {
"environment": "development",
"developmentClient": true,
"distribution": "internal",
"android": {
"buildType": "apk"
},
"env": {
"EXPO_TV": "1",
"EXPO_PUBLIC_WRITE_DEBUG": "1"
}
},
"preview": {
"distribution": "internal"
"environment": "development",
"distribution": "internal",
"env": {
"EXPO_PUBLIC_WRITE_DEBUG": "1"
}
},
"development-simulator": {
"environment": "development",
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true
},
"env": {
"EXPO_PUBLIC_WRITE_DEBUG": "1"
}
},
"production": {
"channel": "0.27.0",
"environment": "production",
"channel": "0.28.0",
"android": {
"image": "latest"
}
},
"production-apk": {
"channel": "0.27.0",
"environment": "production",
"channel": "0.28.0",
"android": {
"buildType": "apk",
"image": "latest"
}
},
"production-apk-tv": {
"channel": "0.27.0",
"environment": "production",
"channel": "0.28.0",
"android": {
"buildType": "apk",
"image": "latest"

View File

@@ -35,7 +35,7 @@ export const useDownloadedFileOpener = () => {
async (item: BaseItemDto) => {
try {
// @ts-expect-error
router.push("/player/direct-player?offline=true&itemId=" + item.Id);
router.push(`/player/direct-player?offline=true&itemId=${item.Id}`);
} catch (error) {
writeToLog("ERROR", "Error opening file", error);
console.error("Error opening file:", error);

View File

@@ -38,7 +38,7 @@ export const useImageColors = ({
const source = useMemo(() => {
if (!api) return;
if (url) return { uri: url };
else if (item)
if (item)
return getItemImage({
item,
api,
@@ -46,7 +46,7 @@ export const useImageColors = ({
quality: 80,
width: 300,
});
else return null;
return null;
}, [api, item]);
useEffect(() => {

View File

@@ -87,11 +87,11 @@ export enum Endpoints {
STUDIO = "/studio",
GENRE_SLIDER = "/genreslider",
DISCOVER = "/discover",
DISCOVER_TRENDING = DISCOVER + "/trending",
DISCOVER_MOVIES = DISCOVER + "/movies",
DISCOVER_TRENDING = `${DISCOVER}/trending`,
DISCOVER_MOVIES = `${DISCOVER}/movies`,
DISCOVER_TV = DISCOVER + TV,
DISCOVER_TV_NETWORK = DISCOVER + TV + NETWORK,
DISCOVER_MOVIES_STUDIO = DISCOVER + `${MOVIE}s` + STUDIO,
DISCOVER_MOVIES_STUDIO = `${DISCOVER}${MOVIE}s${STUDIO}`,
AUTH_JELLYFIN = "/auth/jellyfin",
}
@@ -159,10 +159,8 @@ export class JellyseerrApi {
}
toast.error(t("jellyseerr.toasts.jellyseerr_test_failed"));
writeErrorLog(
`Jellyseerr returned a ${status} for url:\n` +
response.config.url +
"\n" +
JSON.stringify(response.data),
`Jellyseerr returned a ${status} for url:\n${response.config.url}`,
response.data,
);
return {
isValid: false,
@@ -241,7 +239,7 @@ export class JellyseerrApi {
async getRequest(id: number): Promise<MediaRequest> {
return this.axios
?.get<MediaRequest>(Endpoints.API_V1 + Endpoints.REQUEST + `/${id}`)
?.get<MediaRequest>(`${Endpoints.API_V1 + Endpoints.REQUEST}/${id}`)
.then(({ data }) => data);
}
@@ -262,7 +260,7 @@ export class JellyseerrApi {
async movieDetails(id: number) {
return this.axios
?.get<MovieDetails>(Endpoints.API_V1 + Endpoints.MOVIE + `/${id}`)
?.get<MovieDetails>(`${Endpoints.API_V1 + Endpoints.MOVIE}/${id}`)
.then((response) => {
return response?.data;
});
@@ -270,7 +268,7 @@ export class JellyseerrApi {
async personDetails(id: number | string): Promise<PersonDetails> {
return this.axios
?.get<PersonDetails>(Endpoints.API_V1 + Endpoints.PERSON + `/${id}`)
?.get<PersonDetails>(`${Endpoints.API_V1 + Endpoints.PERSON}/${id}`)
.then((response) => {
return response?.data;
});
@@ -279,10 +277,9 @@ export class JellyseerrApi {
async personCombinedCredits(id: number | string): Promise<CombinedCredit> {
return this.axios
?.get<CombinedCredit>(
Endpoints.API_V1 +
Endpoints.PERSON +
`/${id}` +
Endpoints.COMBINED_CREDITS,
`${
Endpoints.API_V1 + Endpoints.PERSON
}/${id}${Endpoints.COMBINED_CREDITS}`,
)
.then((response) => {
return response?.data;
@@ -333,13 +330,10 @@ export class JellyseerrApi {
imageProxy(path?: string, filter = "original", width = 1920, quality = 75) {
return path
? this.axios.defaults.baseURL +
`/_next/image?` +
new URLSearchParams(
`url=https://image.tmdb.org/t/p/${filter}/${path}&w=${width}&q=${quality}`,
).toString()
: this.axios?.defaults.baseURL +
`/images/overseerr_poster_not_found_logo_top.png`;
? `${this.axios.defaults.baseURL}/_next/image?${new URLSearchParams(
`url=https://image.tmdb.org/t/p/${filter}/${path}&w=${width}&q=${quality}`,
).toString()}`
: `${this.axios?.defaults.baseURL}/images/overseerr_poster_not_found_logo_top.png`;
}
async submitIssue(mediaId: number, issueType: IssueType, message: string) {
@@ -362,7 +356,7 @@ export class JellyseerrApi {
async service(type: "radarr" | "sonarr") {
return this.axios
?.get<ServiceCommonServer[]>(
Endpoints.API_V1 + Endpoints.SERVICE + `/${type}`,
`${Endpoints.API_V1 + Endpoints.SERVICE}/${type}`,
)
.then(({ data }) => data);
}
@@ -370,7 +364,7 @@ export class JellyseerrApi {
async serviceDetails(type: "radarr" | "sonarr", id: number) {
return this.axios
?.get<ServiceCommonServerWithDetails>(
Endpoints.API_V1 + Endpoints.SERVICE + `/${type}` + `/${id}`,
`${Endpoints.API_V1 + Endpoints.SERVICE}/${type}/${id}`,
)
.then(({ data }) => data);
}
@@ -388,15 +382,9 @@ export class JellyseerrApi {
return response;
},
(error: AxiosError) => {
const errorMsg = "Jellyseerr response error";
console.error(errorMsg, error, error.response?.data);
writeErrorLog(
errorMsg +
`\n` +
`error: ${error.toString()}\n` +
`url: ${error?.config?.url}\n` +
`data:\n` +
JSON.stringify(error.response?.data),
`Jellyseerr response error\nerror: ${error.toString()}\nurl: ${error?.config?.url}`,
error.response?.data,
);
if (error.status === 403) {
clearJellyseerrStorageData();
@@ -412,7 +400,7 @@ export class JellyseerrApi {
const headerName = this.axios.defaults.xsrfHeaderName!;
const xsrfToken = cookies
.find((c) => c.includes(headerName))
?.split(headerName + "=")?.[1];
?.split(`${headerName}=`)?.[1];
if (xsrfToken) {
config.headers[headerName] = xsrfToken;
}
@@ -484,7 +472,7 @@ export const useJellyseerr = () => {
return (
items &&
Object.hasOwn(items, "mediaType") &&
Object.values(MediaType).includes(items["mediaType"])
Object.values(MediaType).includes(items.mediaType)
);
};
@@ -492,10 +480,10 @@ export const useJellyseerr = () => {
item?: TvResult | TvDetails | MovieResult | MovieDetails,
) => {
return isJellyseerrResult(item)
? item.mediaType == MediaType.MOVIE
? item.mediaType === MediaType.MOVIE
? item?.title
: item?.name
: item?.mediaInfo.mediaType == MediaType.MOVIE
: item?.mediaInfo.mediaType === MediaType.MOVIE
? (item as MovieDetails)?.title
: (item as TvDetails)?.name;
};
@@ -505,10 +493,10 @@ export const useJellyseerr = () => {
) => {
return new Date(
(isJellyseerrResult(item)
? item.mediaType == MediaType.MOVIE
? item.mediaType === MediaType.MOVIE
? item?.releaseDate
: item?.firstAirDate
: item?.mediaInfo.mediaType == MediaType.MOVIE
: item?.mediaInfo.mediaType === MediaType.MOVIE
? (item as MovieDetails)?.releaseDate
: (item as TvDetails)?.firstAirDate) || "",
)?.getFullYear?.();

View File

@@ -1,7 +1,7 @@
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getItemImage } from "@/utils/getItemImage";
import { writeErrorLog, writeInfoLog, writeToLog } from "@/utils/log";
import { writeErrorLog, writeInfoLog } from "@/utils/log";
import type {
BaseItemDto,
MediaSourceInfo,
@@ -132,8 +132,8 @@ export const useRemuxHlsToMp4 = () => {
totalFrames > 0 ? Math.floor((processedFrames / totalFrames) * 100) : 0;
if (!item.Id) throw new Error("Item is undefined");
setProcesses((prev: any[]) => {
return prev.map((process: { itemId: string | undefined }) => {
setProcesses((prev: JobStatus[]) => {
return prev.map((process: JobStatus) => {
if (process.itemId === item.Id) {
return {
...process,
@@ -160,7 +160,7 @@ export const useRemuxHlsToMp4 = () => {
});
}
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 (!item.Id) throw new Error("Item must have an Id");

View File

@@ -10,7 +10,9 @@ import it from "./translations/it.json";
import ja from "./translations/ja.json";
import nl from "./translations/nl.json";
import pl from "./translations/pl.json";
import ptBR from "./translations/pt-BR.json";
import sv from "./translations/sv.json";
import ru from "./translations/ru.json";
import tr from "./translations/tr.json";
import ua from "./translations/ua.json";
import zhCN from "./translations/zh-CN.json";
@@ -26,7 +28,9 @@ export const APP_LANGUAGES = [
{ label: "Türkçe", value: "tr" },
{ label: "Nederlands", value: "nl" },
{ label: "Polski", value: "pl" },
{ label: "Português (Brasil)", value: "pt-BR" },
{ label: "Svenska", value: "sv" },
{ label: "Русский", value: "ru" },
{ label: "Українська", value: "ua" },
{ label: "简体中文", value: "zh-CN" },
{ label: "繁體中文", value: "zh-TW" },
@@ -43,7 +47,9 @@ i18n.use(initReactI18next).init({
ja: { translation: ja },
nl: { translation: nl },
pl: { translation: pl },
"pt-BR": { translation: ptBR },
sv: { translation: sv },
ru: { translation: ru },
tr: { translation: tr },
ua: { translation: ua },
"zh-CN": { translation: zhCN },

View File

@@ -1,3 +1,5 @@
import { ViewStyle } from "react-native";
export type PlaybackStatePayload = {
nativeEvent: {
target: number;
@@ -59,7 +61,7 @@ export type ChapterInfo = {
export type VlcPlayerViewProps = {
source: VlcPlayerSource;
style?: Record<string, unknown>;
style?: ViewStyle | ViewStyle[];
progressUpdateInterval?: number;
paused?: boolean;
muted?: boolean;

View File

@@ -2,7 +2,7 @@ import { requireNativeViewManager } from "expo-modules-core";
import * as React from "react";
import { VideoPlayer, useSettings } from "@/utils/atoms/settings";
import { Platform } from "react-native";
import { Platform, ViewStyle } from "react-native";
import type {
VlcPlayerSource,
VlcPlayerViewProps,
@@ -22,7 +22,7 @@ const NativeView = React.forwardRef<NativeViewRef, VlcPlayerViewProps>(
const [settings] = useSettings();
if (Platform.OS === "ios" || Platform.isTVOS) {
if (settings.defaultPlayer == VideoPlayer.VLC_3) {
if (settings.defaultPlayer === VideoPlayer.VLC_3) {
console.log("[Apple] Using Vlc Player 3");
return <VLC3ViewManager {...props} ref={ref} />;
}
@@ -118,7 +118,9 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
} = props;
const processedSource: VlcPlayerSource =
typeof source === "string" ? { uri: source } : source;
typeof source === "string"
? ({ uri: source } as unknown as VlcPlayerSource)
: source;
if (processedSource.startPosition !== undefined) {
processedSource.startPosition = Math.floor(processedSource.startPosition);
@@ -129,7 +131,7 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
{...otherProps}
ref={nativeRef}
source={processedSource}
style={[{ width: "100%", height: "100%" }, style]}
style={[{ width: "100%", height: "100%" }, style as ViewStyle]}
progressUpdateInterval={progressUpdateInterval}
paused={paused}
muted={muted}

View File

@@ -12,7 +12,6 @@ Pod::Spec.new do |s|
s.dependency 'ExpoModulesCore'
s.ios.dependency 'VLCKit', s.version
s.tvos.dependency 'VLCKit', s.version
s.dependency 'Alamofire', '~> 5.10'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {

View File

@@ -13,7 +13,8 @@
"prebuild": "EXPO_TV=0 bun run clean",
"prebuild:tv": "EXPO_TV=1 bun run clean",
"prepare": "husky",
"lint": "biome format --write ."
"check": "biome check .",
"lint": "biome check --write --unsafe"
},
"dependencies": {
"@bottom-tabs/react-navigation": "0.8.6",
@@ -56,13 +57,13 @@
"expo-router": "~4.0.17",
"expo-screen-orientation": "~8.0.4",
"expo-sensors": "~14.0.2",
"expo-sharing": "~13.1.0",
"expo-splash-screen": "~0.29.22",
"expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.8",
"expo-task-manager": "~12.0.5",
"expo-updates": "~0.26.17",
"expo-web-browser": "~14.0.2",
"ffmpeg-kit-react-native": "^6.0.2",
"i18next": "^24.2.2",
"jotai": "^2.11.3",
"lodash": "^4.17.21",
@@ -74,6 +75,7 @@
"react-native-awesome-slider": "^2.9.0",
"react-native-bottom-tabs": "0.8.6",
"react-native-circular-progress": "^1.4.1",
"react-native-collapsible": "^1.6.2",
"react-native-compressor": "^1.10.3",
"react-native-country-flag": "^2.0.2",
"react-native-device-info": "^14.0.4",
@@ -132,6 +134,7 @@
}
},
"lint-staged": {
"*": ["biome check --no-errors-on-unmatched --files-ignore-unknown=true"]
"*.{js,jsx,ts,tsx}": ["biome check --write --unsafe"],
"*.{json,md}": ["biome format --write"]
}
}

View File

@@ -1,5 +1,5 @@
const { readFileSync, writeFileSync } = require("fs");
const { join } = require("path");
const { readFileSync, writeFileSync } = require("node:fs");
const { join } = require("node:path");
const { withDangerousMod } = require("@expo/config-plugins");
const withChangeNativeAndroidTextToWhite = (expoConfig) =>

View File

@@ -1,7 +1,7 @@
const { AndroidConfig, withAndroidManifest } = require("@expo/config-plugins");
const { Paths } = require("@expo/config-plugins/build/android");
const path = require("path");
const fs = require("fs");
const path = require("node:path");
const fs = require("node:fs");
const fsPromises = fs.promises;
const { getMainApplicationOrThrow } = AndroidConfig.Manifest;

View File

@@ -148,7 +148,7 @@ function useDownloadProvider() {
title: job.item.Name,
body: `${job.item.Name} is ready to be downloaded`,
data: {
url: `/downloads`,
url: "/downloads",
},
},
trigger: null,
@@ -238,7 +238,7 @@ function useDownloadProvider() {
BackGroundDownloader?.download({
id: process.id,
url: settings?.optimizedVersionsServerUrl + "download/" + process.id,
url: `${settings?.optimizedVersionsServerUrl}download/${process.id}`,
destination: `${baseDirectory}/${process.item.Id}.mp4`,
})
.begin(() => {
@@ -347,7 +347,7 @@ function useDownloadProvider() {
await saveImage(item.Id, itemImage?.uri);
const response = await axios.post(
settings?.optimizedVersionsServerUrl + "optimize-version",
`${settings?.optimizedVersionsServerUrl}optimize-version`,
{
url,
fileExtension,
@@ -447,8 +447,8 @@ function useDownloadProvider() {
};
const forEveryDocumentDirFile = async (
includeMMKV = true,
ignoreList: string[] = [],
includeMMKV: boolean,
ignoreList: string[],
callback: (file: FileInfo) => void,
) => {
const baseDirectory = FileSystem.documentDirectory;
@@ -461,7 +461,7 @@ function useDownloadProvider() {
// Exclude mmkv directory.
// Deleting this deletes all user information as well. Logout should handle this.
if (
(item == "mmkv" && !includeMMKV) ||
(item === "mmkv" && !includeMMKV) ||
ignoreList.some((i) => item.includes(i))
) {
continue;
@@ -603,10 +603,10 @@ function useDownloadProvider() {
const deleteFileByType = async (type: BaseItemDto["Type"]) => {
await Promise.all(
downloadedFiles
?.filter((file) => file.item.Type == type)
?.filter((file) => file.item.Type === type)
?.flatMap((file) => {
const promises = [];
if (type == "Episode" && file.item.SeriesId)
if (type === "Episode" && file.item.SeriesId)
promises.push(deleteFile(file.item.SeriesId));
promises.push(deleteFile(file.item.Id!));
return promises;
@@ -655,9 +655,8 @@ function useDownloadProvider() {
const downloadedItems = storage.getString("downloadedItems");
if (downloadedItems) {
return JSON.parse(downloadedItems) as DownloadedItem[];
} else {
return [];
}
return [];
} catch (error) {
console.error("Failed to retrieve downloaded items:", error);
return [];
@@ -691,7 +690,7 @@ function useDownloadProvider() {
deleteDownloadItemInfoFromDiskTmp(item.Id!);
storage.set("downloadedItems", JSON.stringify(items));
storage.set("downloadedItemSize-" + item.Id, size.toString());
storage.set(`downloadedItemSize-${item.Id}`, size.toString());
queryClient.invalidateQueries({ queryKey: ["downloadedItems"] });
refetch();
@@ -704,7 +703,7 @@ function useDownloadProvider() {
}
function getDownloadedItemSize(itemId: string): number {
const size = storage.getString("downloadedItemSize-" + itemId);
const size = storage.getString(`downloadedItemSize-${itemId}`);
return size ? Number.parseInt(size) : 0;
}

Some files were not shown because too many files have changed in this diff Show More