chore: linting fixes && github actions for linting (#612)

This commit is contained in:
Ahmed Sbai
2025-03-31 07:44:10 +02:00
committed by GitHub
parent 16b834cf71
commit b9bb109f4a
105 changed files with 604 additions and 570 deletions

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

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` 1. Use node `>20`
2. Install dependencies `bun i && bun run submodule-reload` 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/) 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` 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. 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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -157,9 +157,8 @@ const page: React.FC = () => {
if (accumulatedItems < totalItems) { if (accumulatedItems < totalItems) {
return lastPage?.Items?.length * pages.length; return lastPage?.Items?.length * pages.length;
} else {
return undefined;
} }
return undefined;
}, },
initialPageParam: 0, initialPageParam: 0,
enabled: !!api && !!user?.Id && !!collection, enabled: !!api && !!user?.Id && !!collection,
@@ -412,7 +411,7 @@ const page: React.FC = () => {
width: 10, width: 10,
height: 10, height: 10,
}} }}
></View> />
)} )}
/> />
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -216,9 +216,8 @@ const Page = () => {
if (accumulatedItems < totalItems) { if (accumulatedItems < totalItems) {
return lastPage?.Items?.length * pages.length; return lastPage?.Items?.length * pages.length;
} else {
return undefined;
} }
return undefined;
}, },
initialPageParam: 0, initialPageParam: 0,
enabled: !!api && !!user?.Id && !!library, enabled: !!api && !!user?.Id && !!library,
@@ -478,7 +477,7 @@ const Page = () => {
width: 10, width: 10,
height: 10, height: 10,
}} }}
></View> />
)} )}
/> />
); );

View File

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

View File

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

View File

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

View File

@@ -13,8 +13,7 @@ import SeriesPoster from "@/components/posters/SeriesPoster";
import { LoadingSkeleton } from "@/components/search/LoadingSkeleton"; import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
import { SearchItemWrapper } from "@/components/search/SearchItemWrapper"; import { SearchItemWrapper } from "@/components/search/SearchItemWrapper";
import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
import { sortOrderOptions } from "@/utils/atoms/filters";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus"; import { eventBus } from "@/utils/eventBus";
import type { import type {
@@ -64,7 +63,6 @@ export default function search() {
const [debouncedSearch] = useDebounce(search, 500); const [debouncedSearch] = useDebounce(search, 500);
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const [settings] = useSettings(); const [settings] = useSettings();
const { jellyseerrApi } = useJellyseerr(); const { jellyseerrApi } = useJellyseerr();
@@ -83,7 +81,9 @@ export default function search() {
}, [settings]); }, [settings]);
useEffect(() => { useEffect(() => {
if (q && q.length > 0) setSearch(q); if (q && q.length > 0) {
setSearch(q);
}
}, [q]); }, [q]);
const searchFn = useCallback( const searchFn = useCallback(
@@ -94,7 +94,9 @@ export default function search() {
types: BaseItemKind[]; types: BaseItemKind[];
query: string; query: string;
}): Promise<BaseItemDto[]> => { }): Promise<BaseItemDto[]> => {
if (!api || !query) return []; if (!api || !query) {
return [];
}
try { try {
if (searchEngine === "Jellyfin") { if (searchEngine === "Jellyfin") {
@@ -105,28 +107,31 @@ export default function search() {
}); });
return (searchApi.data.SearchHints as BaseItemDto[]) || []; 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[]) || [];
} }
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) { } catch (error) {
console.error("Error during search:", error); console.error("Error during search:", error);
return []; // Ensure an empty array is returned in case of an error return []; // Ensure an empty array is returned in case of an error
@@ -162,8 +167,10 @@ export default function search() {
useEffect(() => { useEffect(() => {
const unsubscribe = eventBus.on("searchTabPressed", () => { const unsubscribe = eventBus.on("searchTabPressed", () => {
// Screen not actuve // Screen not active
if (!searchBarRef.current) return; if (!searchBarRef.current) {
return;
}
// Screen is active, focus search bar // Screen is active, focus search bar
searchBarRef.current?.focus(); searchBarRef.current?.focus();
}); });
@@ -254,64 +261,62 @@ export default function search() {
}} }}
> >
{jellyseerrApi && ( {jellyseerrApi && (
<> <ScrollView
<ScrollView horizontal
horizontal className='flex flex-row flex-wrap space-x-2 px-4 mb-2'
className='flex flex-row flex-wrap space-x-2 px-4 mb-2' >
> <TouchableOpacity onPress={() => setSearchType("Library")}>
<TouchableOpacity onPress={() => setSearchType("Library")}> <Tag
<Tag text={t("search.library")}
text={t("search.library")} textClass='p-1'
textClass='p-1' className={
className={ searchType === "Library" ? "bg-purple-600" : undefined
searchType === "Library" ? "bg-purple-600" : undefined }
} />
/> </TouchableOpacity>
</TouchableOpacity> <TouchableOpacity onPress={() => setSearchType("Discover")}>
<TouchableOpacity onPress={() => setSearchType("Discover")}> <Tag
<Tag text={t("search.discover")}
text={t("search.discover")} textClass='p-1'
textClass='p-1' className={
className={ searchType === "Discover" ? "bg-purple-600" : undefined
searchType === "Discover" ? "bg-purple-600" : undefined }
} />
/> </TouchableOpacity>
</TouchableOpacity> {searchType === "Discover" &&
{searchType === "Discover" && !loading &&
!loading && noResults &&
noResults && debouncedSearch.length > 0 && (
debouncedSearch.length > 0 && ( <View className='flex flex-row justify-end items-center space-x-1'>
<View className='flex flex-row justify-end items-center space-x-1'> <FilterButton
<FilterButton id='search'
id='search' queryKey='jellyseerr_search'
queryKey='jellyseerr_search' queryFn={async () =>
queryFn={async () => Object.keys(JellyseerrSearchSort).filter((v) =>
Object.keys(JellyseerrSearchSort).filter((v) => Number.isNaN(Number(v)),
isNaN(Number(v)), )
) }
} set={(value) => setJellyseerrOrderBy(value[0])}
set={(value) => setJellyseerrOrderBy(value[0])} values={[jellyseerrOrderBy]}
values={[jellyseerrOrderBy]} title={t("library.filters.sort_by")}
title={t("library.filters.sort_by")} renderItemLabel={(item) =>
renderItemLabel={(item) => t(`home.settings.plugins.jellyseerr.order_by.${item}`)
t(`home.settings.plugins.jellyseerr.order_by.${item}`) }
} showSearch={false}
showSearch={false} />
/> <FilterButton
<FilterButton id='order'
id='order' queryKey='jellysearr_search'
queryKey='jellysearr_search' queryFn={async () => ["asc", "desc"]}
queryFn={async () => ["asc", "desc"]} set={(value) => setJellyseerrSortOrder(value[0])}
set={(value) => setJellyseerrSortOrder(value[0])} values={[jellyseerrSortOrder]}
values={[jellyseerrSortOrder]} title={t("library.filters.sort_order")}
title={t("library.filters.sort_order")} renderItemLabel={(item) => t(`library.filters.${item}`)}
renderItemLabel={(item) => t(`library.filters.${item}`)} showSearch={false}
showSearch={false} />
/> </View>
</View> )}
)} </ScrollView>
</ScrollView>
</>
)} )}
<View className='mt-2'> <View className='mt-2'>
@@ -411,32 +416,29 @@ export default function search() {
/> />
)} )}
{searchType === "Library" && ( {searchType === "Library" &&
<> (!loading && noResults && debouncedSearch.length > 0 ? (
{!loading && noResults && debouncedSearch.length > 0 ? ( <View>
<View> <Text className='text-center text-lg font-bold mt-4'>
<Text className='text-center text-lg font-bold mt-4'> {t("search.no_results_found_for")}
{t("search.no_results_found_for")} </Text>
</Text> <Text className='text-xs text-purple-600 text-center'>
<Text className='text-xs text-purple-600 text-center'> "{debouncedSearch}"
"{debouncedSearch}" </Text>
</Text> </View>
</View> ) : debouncedSearch.length === 0 ? (
) : debouncedSearch.length === 0 ? ( <View className='mt-4 flex flex-col items-center space-y-2'>
<View className='mt-4 flex flex-col items-center space-y-2'> {exampleSearches.map((e) => (
{exampleSearches.map((e) => ( <TouchableOpacity
<TouchableOpacity onPress={() => setSearch(e)}
onPress={() => setSearch(e)} key={e}
key={e} className='mb-2'
className='mb-2' >
> <Text className='text-purple-600'>{e}</Text>
<Text className='text-purple-600'>{e}</Text> </TouchableOpacity>
</TouchableOpacity> ))}
))} </View>
</View> ) : null)}
) : null}
</>
)}
</View> </View>
</ScrollView> </ScrollView>
</> </>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -237,5 +237,5 @@ const formatFileSize = (bytes?: number | null) => {
const i = Number.parseInt( const i = Number.parseInt(
Math.floor(Math.log(bytes) / Math.log(1024)).toString(), 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); const x = acc.find((item) => item.Id === current.Id);
if (!x) { if (!x) {
return acc.concat([current]); return acc.concat([current]);
} else {
return acc;
} }
return acc;
}, [] as BaseItemDto[]) || []; }, [] as BaseItemDto[]) || [];
return uniqueItems; return uniqueItems;

View File

@@ -239,7 +239,7 @@ export const PlayButton: React.FC<Props> = ({
const derivedTargetWidth = useDerivedValue(() => { const derivedTargetWidth = useDerivedValue(() => {
if (!item || !item.RunTimeTicks) return 0; if (!item || !item.RunTimeTicks) return 0;
const userData = item.UserData; const userData = item.UserData;
if (userData && userData.PlaybackPositionTicks) { if (userData?.PlaybackPositionTicks) {
return userData.PlaybackPositionTicks > 0 return userData.PlaybackPositionTicks > 0
? Math.max( ? Math.max(
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100, (userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
@@ -331,7 +331,7 @@ export const PlayButton: React.FC<Props> = ({
accessibilityLabel='Play button' accessibilityLabel='Play button'
accessibilityHint='Tap to play the media' accessibilityHint='Tap to play the media'
onPress={onPress} onPress={onPress}
className={`relative`} className={"relative"}
{...props} {...props}
> >
<View className='absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden'> <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(() => { const derivedTargetWidth = useDerivedValue(() => {
if (!item || !item.RunTimeTicks) return 0; if (!item || !item.RunTimeTicks) return 0;
const userData = item.UserData; const userData = item.UserData;
if (userData && userData.PlaybackPositionTicks) { if (userData?.PlaybackPositionTicks) {
return userData.PlaybackPositionTicks > 0 return userData.PlaybackPositionTicks > 0
? Math.max( ? Math.max(
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100, (userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
@@ -176,7 +176,7 @@ export const PlayButton: React.FC<Props> = ({
accessibilityLabel='Play button' accessibilityLabel='Play button'
accessibilityHint='Tap to play the media' accessibilityHint='Tap to play the media'
onPress={onPress} onPress={onPress}
className={`relative`} className={"relative"}
{...props} {...props}
> >
<View className='absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden'> <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.UserData?.Played === false &&
(item.Type === "Movie" || item.Type === "Episode") && ( (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"; import { ThemedText } from "../ThemedText";
it(`renders correctly`, () => { it("renders correctly", () => {
const tree = renderer const tree = renderer
.create(<ThemedText>Snapshot test!</ThemedText>) .create(<ThemedText>Snapshot test!</ThemedText>)
.toJSON(); .toJSON();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ export const LargePoster: React.FC<{ url?: string | null }> = ({ url }) => {
if (!url) if (!url)
return ( return (
<View className='p-4 rounded-xl overflow-hidden '> <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>
); );

View File

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

View File

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

View File

@@ -80,8 +80,6 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
task.stop(); task.stop();
} }
} }
} catch (e) {
throw e;
} finally { } finally {
await removeProcess(id); await removeProcess(id);
await queryClient.refetchQueries({ queryKey: ["jobs"] }); await queryClient.refetchQueries({ queryKey: ["jobs"] });
@@ -131,7 +129,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
? `${Math.max(5, process.progress)}%` ? `${Math.max(5, process.progress)}%`
: "5%", : "5%",
}} }}
></View> />
)} )}
<View className='px-3 py-1.5 flex flex-col w-full'> <View className='px-3 py-1.5 flex flex-col w-full'>
<View className='flex flex-row items-center 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, destructiveButtonIndex,
}, },
(selectedIndex) => { (selectedIndex) => {
if (selectedIndex == destructiveButtonIndex) { if (selectedIndex === destructiveButtonIndex) {
deleteSeries(); deleteSeries();
} }
}, },

View File

@@ -15,7 +15,7 @@ interface FilterButtonProps<T> extends ViewProps {
queryFn: (params: any) => Promise<any>; queryFn: (params: any) => Promise<any>;
searchFilter?: (item: T, query: string) => boolean; searchFilter?: (item: T, query: string) => boolean;
renderItemLabel: (item: T) => React.ReactNode; renderItemLabel: (item: T) => React.ReactNode;
multiple?: boolean, multiple?: boolean;
icon?: "filter" | "sort"; icon?: "filter" | "sort";
} }

View File

@@ -183,15 +183,13 @@ export const FilterSheet = <T,>({
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
if (multiple) { if (multiple) {
if (!values.includes(item)) if (!values.includes(item)) set(values.concat(item));
set(values.concat(item)) else set(values.filter((v) => v !== item));
else set(values.filter(v => v !== item))
setTimeout(() => { setTimeout(() => {
setOpen(false); setOpen(false);
}, 250); }, 250);
} } else {
else {
if (!values.includes(item)) { if (!values.includes(item)) {
set([item]); set([item]);
setTimeout(() => { setTimeout(() => {
@@ -214,7 +212,7 @@ export const FilterSheet = <T,>({
height: StyleSheet.hairlineWidth, height: StyleSheet.hairlineWidth,
}} }}
className='h-1 divide-neutral-700 ' className='h-1 divide-neutral-700 '
></View> />
</View> </View>
))} ))}
</View> </View>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,7 +61,7 @@ const RequestModal = forwardRef<
const { data: serviceSettings } = useQuery({ const { data: serviceSettings } = useQuery({
queryKey: ["jellyseerr", "request", type, "service"], queryKey: ["jellyseerr", "request", type, "service"],
queryFn: async () => queryFn: async () =>
jellyseerrApi?.service(type == "movie" ? "radarr" : "sonarr"), jellyseerrApi?.service(type === "movie" ? "radarr" : "sonarr"),
enabled: !!jellyseerrApi && !!jellyseerrUser, enabled: !!jellyseerrApi && !!jellyseerrUser,
refetchOnMount: "always", refetchOnMount: "always",
}); });

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ const Slide = <T,>({
return ( return (
<View {...props}> <View {...props}>
<Text className='font-bold text-lg mb-2 px-4'> <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> </Text>
<FlashList <FlashList
horizontal horizontal

View File

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

View File

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

View File

@@ -129,7 +129,7 @@ const JellyseerrPoster: React.FC<Props> = ({
posterSrc={posterSrc!} posterSrc={posterSrc!}
mediaType={mediaType} mediaType={mediaType}
> >
<View className={`flex flex-col mr-2 h-auto`}> <View className={"flex flex-col mr-2 h-auto"}>
<View <View
className={`relative rounded-lg overflow-hidden border border-neutral-900 ${size} aspect-[${ratio}]`} 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 blurhash = useMemo(() => {
const key = item.ImageTags?.["Primary"] as string; const key = item.ImageTags?.Primary as string;
return item.ImageBlurHashes?.["Primary"]?.[key]; return item.ImageBlurHashes?.Primary?.[key];
}, [item]); }, [item]);
return ( return (
@@ -59,7 +59,7 @@ const MoviePoster: React.FC<MoviePosterProps> = ({
/> />
<WatchedIndicator item={item} /> <WatchedIndicator item={item} />
{showProgress && progress > 0 && ( {showProgress && progress > 0 && (
<View className='h-1 bg-red-600 w-full'></View> <View className='h-1 bg-red-600 w-full' />
)} )}
</View> </View>
); );

View File

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

View File

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

View File

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

View File

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

View File

@@ -57,7 +57,7 @@ export const NextItemButton: React.FC<Props> = ({
return ( return (
<Button <Button
onPress={() => router.setParams({ id: nextItem?.Id })} onPress={() => router.setParams({ id: nextItem?.Id })}
className={`h-12 aspect-square`} className={"h-12 aspect-square"}
disabled={disabled} disabled={disabled}
{...props} {...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'> <View className='flex flex-row items-center space-x-2'>
<DownloadItems <DownloadItems
title={t("item_card.download.download_season")} title={t("item_card.download.download_season")}

View File

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

View File

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

View File

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

View File

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

View File

@@ -81,7 +81,7 @@ export const StorageSettings = () => {
{size && ( {size && (
<> <>
<View className='flex flex-row items-center'> <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'> <Text className='text-white text-xs'>
{t("home.settings.storage.app_usage", { {t("home.settings.storage.app_usage", {
usedSpace: calculatePercentage(size.app, size.total), usedSpace: calculatePercentage(size.app, size.total),
@@ -89,7 +89,7 @@ export const StorageSettings = () => {
</Text> </Text>
</View> </View>
<View className='flex flex-row items-center'> <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'> <Text className='text-white text-xs'>
{t("home.settings.storage.device_usage", { {t("home.settings.storage.device_usage", {
availableSpace: calculatePercentage( availableSpace: calculatePercentage(

View File

@@ -168,7 +168,7 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
style={{ style={{
justifyContent: "space-between", 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 && ( {seriesItem && (
<SeasonDropdown <SeasonDropdown

View File

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

View File

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

View File

@@ -32,8 +32,8 @@ export const useTapDetection = ({
const touchDuration = touchEndTime - touchStartTime.current; const touchDuration = touchEndTime - touchStartTime.current;
const touchDistance = Math.sqrt( const touchDistance = Math.sqrt(
Math.pow(touchEndPosition.x - touchStartPosition.current.x, 2) + (touchEndPosition.x - touchStartPosition.current.x) ** 2 +
Math.pow(touchEndPosition.y - touchStartPosition.current.y, 2), (touchEndPosition.y - touchStartPosition.current.y) ** 2,
); );
if (touchDuration < maxDuration && touchDistance < maxDistance) { 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'>{t("player.playback_state")}</Text>
<Text className='font-bold mt-2.5'>{t("player.audio_tracks")}</Text> <Text className='font-bold mt-2.5'>{t("player.audio_tracks")}</Text>
{audioTracks && {audioTracks?.map((track, index) => (
audioTracks.map((track, index) => ( <Text key={index}>
<Text key={index}> {track.name} ({t("player.index")} {track.index})
{track.name} ({t("player.index")} {track.index}) </Text>
</Text> ))}
))}
<Text className='font-bold mt-2.5'>{t("player.subtitles_tracks")}</Text> <Text className='font-bold mt-2.5'>{t("player.subtitles_tracks")}</Text>
{subtitleTracks && {subtitleTracks?.map((track, index) => (
subtitleTracks.map((track, index) => ( <Text key={index}>
<Text key={index}> {track.name} ({t("player.index")} {track.index})
{track.name} ({t("player.index")} {track.index}) </Text>
</Text> ))}
))}
<TouchableOpacity <TouchableOpacity
className='mt-2.5 bg-blue-500 p-2 rounded' className='mt-2.5 bg-blue-500 p-2 rounded'
onPress={() => { onPress={() => {

View File

@@ -35,7 +35,7 @@ export const useDownloadedFileOpener = () => {
async (item: BaseItemDto) => { async (item: BaseItemDto) => {
try { try {
// @ts-expect-error // @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) { } catch (error) {
writeToLog("ERROR", "Error opening file", error); writeToLog("ERROR", "Error opening file", error);
console.error("Error opening file:", error); console.error("Error opening file:", error);

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,8 @@
"prebuild": "EXPO_TV=0 bun run clean", "prebuild": "EXPO_TV=0 bun run clean",
"prebuild:tv": "EXPO_TV=1 bun run clean", "prebuild:tv": "EXPO_TV=1 bun run clean",
"prepare": "husky", "prepare": "husky",
"lint": "biome format --write ." "check": "biome check .",
"lint": "biome check --write --unsafe"
}, },
"dependencies": { "dependencies": {
"@bottom-tabs/react-navigation": "0.8.6", "@bottom-tabs/react-navigation": "0.8.6",
@@ -134,6 +135,7 @@
} }
}, },
"lint-staged": { "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 { readFileSync, writeFileSync } = require("node:fs");
const { join } = require("path"); const { join } = require("node:path");
const { withDangerousMod } = require("@expo/config-plugins"); const { withDangerousMod } = require("@expo/config-plugins");
const withChangeNativeAndroidTextToWhite = (expoConfig) => const withChangeNativeAndroidTextToWhite = (expoConfig) =>

View File

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

View File

@@ -63,7 +63,7 @@ function useDownloadProvider() {
function saveDownloadedItemInfo(item: BaseItemDto, size = 0) {} function saveDownloadedItemInfo(item: BaseItemDto, size = 0) {}
function getDownloadedItemSize(itemId: string): number { function getDownloadedItemSize(itemId: string): number {
const size = storage.getString("downloadedItemSize-" + itemId); const size = storage.getString(`downloadedItemSize-${itemId}`);
return size ? Number.parseInt(size) : 0; return size ? Number.parseInt(size) : 0;
} }

View File

@@ -101,7 +101,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
if (!api || !deviceId) return; if (!api || !deviceId) return;
try { try {
const response = await api.axiosInstance.post( const response = await api.axiosInstance.post(
api.basePath + "/QuickConnect/Initiate", `${api.basePath}/QuickConnect/Initiate`,
null, null,
{ {
headers, headers,
@@ -111,9 +111,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setSecret(response?.data?.Secret); setSecret(response?.data?.Secret);
setIsPolling(true); setIsPolling(true);
return response.data?.Code; return response.data?.Code;
} else {
throw new Error("Failed to initiate quick connect");
} }
throw new Error("Failed to initiate quick connect");
} catch (error) { } catch (error) {
console.error(error); console.error(error);
throw error; throw error;
@@ -133,7 +132,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setIsPolling(false); setIsPolling(false);
const authResponse = await api.axiosInstance.post( const authResponse = await api.axiosInstance.post(
api.basePath + "/Users/AuthenticateWithQuickConnect", `${api.basePath}/Users/AuthenticateWithQuickConnect`,
{ {
secret, secret,
}, },
@@ -156,10 +155,9 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setIsPolling(false); setIsPolling(false);
setSecret(null); setSecret(null);
throw new Error("The code has expired. Please try again."); throw new Error("The code has expired. Please try again.");
} else {
console.error("Error polling Quick Connect:", error);
throw error;
} }
console.error("Error polling Quick Connect:", error);
throw error;
} }
}, [api, secret, headers]); }, [api, secret, headers]);
@@ -290,7 +288,9 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
api api
?.delete(`/Streamyfin/device/${deviceId}`) ?.delete(`/Streamyfin/device/${deviceId}`)
.then((r) => writeInfoLog("Deleted expo push token for device")) .then((r) => writeInfoLog("Deleted expo push token for device"))
.catch((e) => writeErrorLog("Failed to delete expo push token for device")); .catch((e) =>
writeErrorLog("Failed to delete expo push token for device"),
);
storage.delete("token"); storage.delete("token");
setUser(null); setUser(null);

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
const fs = require("fs"); const fs = require("node:fs");
const path = require("path"); const path = require("node:path");
const process = require("process"); const process = require("node:process");
const { execSync } = require("child_process"); const { execSync } = require("node:child_process");
const root = process.cwd(); const root = process.cwd();
// const tvosPath = path.join(root, 'iostv'); // const tvosPath = path.join(root, 'iostv');

View File

@@ -15,7 +15,6 @@ function orientationToOrientationLock(
return OrientationLock.LANDSCAPE_LEFT; return OrientationLock.LANDSCAPE_LEFT;
case Orientation.LANDSCAPE_RIGHT: case Orientation.LANDSCAPE_RIGHT:
return OrientationLock.LANDSCAPE_RIGHT; return OrientationLock.LANDSCAPE_RIGHT;
case Orientation.UNKNOWN:
default: default:
return OrientationLock.DEFAULT; return OrientationLock.DEFAULT;
} }

View File

@@ -27,8 +27,8 @@ export const useJellyseerrCanRequest = (
const canNotRequest = const canNotRequest =
item?.mediaInfo?.requests?.some( item?.mediaInfo?.requests?.some(
(r: MediaRequest) => (r: MediaRequest) =>
r.status == MediaRequestStatus.PENDING || r.status === MediaRequestStatus.PENDING ||
r.status == MediaRequestStatus.APPROVED, r.status === MediaRequestStatus.APPROVED,
) || ) ||
item.mediaInfo?.status === MediaStatus.AVAILABLE || item.mediaInfo?.status === MediaStatus.AVAILABLE ||
item.mediaInfo?.status === MediaStatus.BLACKLISTED || item.mediaInfo?.status === MediaStatus.BLACKLISTED ||

View File

@@ -41,7 +41,7 @@ const calculateContrastRatio = (rgb1: number[], rgb2: number[]): number => {
const calculateRelativeLuminance = (rgb: number[]): number => { const calculateRelativeLuminance = (rgb: number[]): number => {
const [r, g, b] = rgb.map((c) => { const [r, g, b] = rgb.map((c) => {
c /= 255; c /= 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
}); });
return 0.2126 * r + 0.7152 * g + 0.0722 * b; return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}; };

View File

@@ -11,9 +11,8 @@ export function convertBitsToMegabitsOrGigabits(bits?: number | null): string {
const megabits = bits / 1000000; const megabits = bits / 1000000;
if (megabits < 1000) { if (megabits < 1000) {
return Math.round(megabits) + "MB"; return `${Math.round(megabits)}MB`;
} else {
const gigabits = megabits / 1000;
return gigabits.toFixed(1) + "GB";
} }
const gigabits = megabits / 1000;
return `${gigabits.toFixed(1)}GB`;
} }

View File

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

View File

@@ -5,7 +5,7 @@ import {
/** /**
* Converts a ColletionType to a BaseItemKind (also called ItemType) * Converts a ColletionType to a BaseItemKind (also called ItemType)
* *
* CollectionTypes * CollectionTypes
* readonly Unknown: "unknown"; * readonly Unknown: "unknown";
readonly Movies: "movies"; readonly Movies: "movies";
@@ -41,8 +41,6 @@ export const colletionTypeToItemType = (
return BaseItemKind.Photo; return BaseItemKind.Photo;
case CollectionType.Trailers: case CollectionType.Trailers:
return BaseItemKind.Trailer; return BaseItemKind.Trailer;
case CollectionType.Playlists:
return BaseItemKind.Playlist;
} }
return undefined; return undefined;

View File

@@ -54,7 +54,7 @@ export const getItemImage = ({
}; };
break; break;
case "Primary": case "Primary":
tag = item.ImageTags?.["Primary"]; tag = item.ImageTags?.Primary;
if (!tag) break; if (!tag) break;
blurhash = item.ImageBlurHashes?.Primary?.[tag]; blurhash = item.ImageBlurHashes?.Primary?.[tag];
@@ -64,7 +64,7 @@ export const getItemImage = ({
}; };
break; break;
case "Thumb": case "Thumb":
tag = item.ImageTags?.["Thumb"]; tag = item.ImageTags?.Thumb;
if (!tag) break; if (!tag) break;
blurhash = item.ImageBlurHashes?.Thumb?.[tag]; blurhash = item.ImageBlurHashes?.Thumb?.[tag];
@@ -74,7 +74,7 @@ export const getItemImage = ({
}; };
break; break;
default: default:
tag = item.ImageTags?.["Primary"]; tag = item.ImageTags?.Primary;
src = { src = {
uri: `${api.basePath}/Items/${item.Id}/Images/Primary?quality=${quality}&tag=${tag}&width=${width}`, uri: `${api.basePath}/Items/${item.Id}/Images/Primary?quality=${quality}&tag=${tag}&width=${width}`,
}; };

View File

@@ -24,12 +24,12 @@ export async function parseM3U8ForSubtitles(
const attributes = parseAttributes(line); const attributes = parseAttributes(line);
const track: SubtitleTrack = { const track: SubtitleTrack = {
index: index++, index: index++,
name: attributes["NAME"] || "", name: attributes.NAME || "",
uri: attributes["URI"] || "", uri: attributes.URI || "",
language: attributes["LANGUAGE"] || "", language: attributes.LANGUAGE || "",
default: attributes["DEFAULT"] === "YES", default: attributes.DEFAULT === "YES",
forced: attributes["FORCED"] === "YES", forced: attributes.FORCED === "YES",
autoSelect: attributes["AUTOSELECT"] === "YES", autoSelect: attributes.AUTOSELECT === "YES",
}; };
subtitleTracks.push(track); subtitleTracks.push(track);
} }

View File

@@ -45,7 +45,6 @@ export const getBackdropUrl = ({
return `${api.basePath}/Items/${ return `${api.basePath}/Items/${
item.Id item.Id
}/Images/Backdrop/0?${params.toString()}`; }/Images/Backdrop/0?${params.toString()}`;
} else {
return getPrimaryImageUrl({ api, item, quality, width });
} }
return getPrimaryImageUrl({ api, item, quality, width });
}; };

View File

@@ -39,7 +39,7 @@ export const getLogoImageUrlById = ({
return `${api.basePath}/Items/${parentId}/Images/Logo?${params.toString()}`; return `${api.basePath}/Items/${parentId}/Images/Logo?${params.toString()}`;
} }
const imageTag = item.ImageTags?.["Logo"]; const imageTag = item.ImageTags?.Logo;
if (!imageTag) return null; if (!imageTag) return null;

View File

@@ -31,7 +31,7 @@ export const getPrimaryImageUrl = ({
return `${api?.basePath}/Items/${item?.Id}/Images/Primary`; return `${api?.basePath}/Items/${item?.Id}/Images/Primary`;
} }
const primaryTag = item.ImageTags?.["Primary"]; const primaryTag = item.ImageTags?.Primary;
const backdropTag = item.BackdropImageTags?.[0]; const backdropTag = item.BackdropImageTags?.[0];
const parentBackdropTag = item.ParentBackdropImageTags?.[0]; const parentBackdropTag = item.ParentBackdropImageTags?.[0];

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