mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
chore: linting fixes && github actions for linting (#612)
This commit is contained in:
28
.github/workflows/lint.yaml
vendored
Normal file
28
.github/workflows/lint.yaml
vendored
Normal 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
|
||||
@@ -86,6 +86,7 @@ We welcome any help to make Streamyfin better. If you'd like to contribute, plea
|
||||
1. Use node `>20`
|
||||
2. Install dependencies `bun i && bun run submodule-reload`
|
||||
3. Make sure you have xcode and/or android studio installed. (follow the guides for expo: https://docs.expo.dev/workflow/android-studio-emulator/)
|
||||
4. Install BiomeJS extension in VSCode/Your IDE (https://biomejs.dev/)
|
||||
4. run `npm run prebuild`
|
||||
5. Create an expo dev build by running `npm run ios` or `npm run android`. This will open a simulator on your computer and run the app.
|
||||
|
||||
|
||||
@@ -26,11 +26,11 @@ export default function menuLinks() {
|
||||
const getMenuLinks = useCallback(async () => {
|
||||
try {
|
||||
const response = await api?.axiosInstance.get(
|
||||
api?.basePath + "/web/config.json",
|
||||
`${api?.basePath}/web/config.json`,
|
||||
);
|
||||
const config = response?.data;
|
||||
|
||||
if (!config && !config.hasOwnProperty("menuLinks")) {
|
||||
if (!config && !Object.hasOwn(config, "menuLinks")) {
|
||||
console.error("Menu links not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function SearchLayout() {
|
||||
backgroundColor: "black",
|
||||
},
|
||||
headerBlurEffect: "prominent",
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerTransparent: Platform.OS === "ios",
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function IndexLayout() {
|
||||
{!Platform.isTV && (
|
||||
<>
|
||||
<Chromecast.Chromecast />
|
||||
{user && user.Policy?.IsAdministrator && <SessionsButton />}
|
||||
{user?.Policy?.IsAdministrator && <SessionsButton />}
|
||||
<SettingsButton />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function page() {
|
||||
try {
|
||||
return (
|
||||
downloadedFiles
|
||||
?.filter((f) => f.item.SeriesId == seriesId)
|
||||
?.filter((f) => f.item.SeriesId === seriesId)
|
||||
?.sort(
|
||||
(a, b) => a?.item.ParentIndexNumber! - b.item.ParentIndexNumber!,
|
||||
) || []
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function page() {
|
||||
</View>
|
||||
);
|
||||
|
||||
if (!sessions || sessions.length == 0)
|
||||
if (!sessions || sessions.length === 0)
|
||||
return (
|
||||
<View className='h-full w-full flex justify-center items-center'>
|
||||
<Text className='text-lg text-neutral-500'>
|
||||
@@ -175,7 +175,7 @@ const SessionCard = ({ session }: SessionCardProps) => {
|
||||
</View>
|
||||
<View className='align-bottom bg-gray-800 h-1'>
|
||||
<View
|
||||
className={`bg-purple-600 h-full`}
|
||||
className={"bg-purple-600 h-full"}
|
||||
style={{
|
||||
width: `${getProgressPercentage()}%`,
|
||||
}}
|
||||
@@ -298,7 +298,7 @@ const TranscodingStreamView = ({
|
||||
const TranscodingView = ({ session }: SessionCardProps) => {
|
||||
const videoStream = useMemo(() => {
|
||||
return session.NowPlayingItem?.MediaStreams?.filter(
|
||||
(s) => s.Type == "Video",
|
||||
(s) => s.Type === "Video",
|
||||
)[0];
|
||||
}, [session]);
|
||||
|
||||
@@ -318,7 +318,7 @@ const TranscodingView = ({ session }: SessionCardProps) => {
|
||||
|
||||
const isTranscoding = useMemo(() => {
|
||||
return (
|
||||
session.PlayState?.PlayMethod == "Transcode" && session.TranscodingInfo
|
||||
session.PlayState?.PlayMethod === "Transcode" && session.TranscodingInfo
|
||||
);
|
||||
}, [session.PlayState?.PlayMethod, session.TranscodingInfo]);
|
||||
|
||||
@@ -341,9 +341,7 @@ const TranscodingView = ({ session }: SessionCardProps) => {
|
||||
codec: session.TranscodingInfo?.VideoCodec,
|
||||
}}
|
||||
isTranscoding={
|
||||
isTranscoding && !session.TranscodingInfo?.IsVideoDirect
|
||||
? true
|
||||
: false
|
||||
!!(isTranscoding && !session.TranscodingInfo?.IsVideoDirect)
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -360,14 +358,11 @@ const TranscodingView = ({ session }: SessionCardProps) => {
|
||||
audioChannels: session.TranscodingInfo?.AudioChannels?.toString(),
|
||||
}}
|
||||
isTranscoding={
|
||||
isTranscoding && !session.TranscodingInfo?.IsVideoDirect
|
||||
? true
|
||||
: false
|
||||
!!(isTranscoding && !session.TranscodingInfo?.IsVideoDirect)
|
||||
}
|
||||
/>
|
||||
|
||||
{subtitleStream && (
|
||||
<>
|
||||
<TranscodingStreamView
|
||||
title='Subtitle'
|
||||
isTranscoding={false}
|
||||
@@ -377,7 +372,6 @@ const TranscodingView = ({ session }: SessionCardProps) => {
|
||||
}}
|
||||
transcodeValue={null}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,73 +1,77 @@
|
||||
import { Loader } from "@/components/Loader";
|
||||
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 {ScrollView, TouchableOpacity, View} from "react-native";
|
||||
import { ScrollView, TouchableOpacity, View } from "react-native";
|
||||
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() {
|
||||
const navigation = useNavigation();
|
||||
const { logs } = useLog();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const defaultLevels: LogLevel[] = ["INFO", "ERROR", "DEBUG", "WARN"]
|
||||
const defaultLevels: LogLevel[] = ["INFO", "ERROR", "DEBUG", "WARN"];
|
||||
const codeBlockStyle = {
|
||||
backgroundColor: '#000',
|
||||
backgroundColor: "#000",
|
||||
padding: 10,
|
||||
fontFamily: 'monospace',
|
||||
maxHeight: 300
|
||||
}
|
||||
fontFamily: "monospace",
|
||||
maxHeight: 300,
|
||||
};
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [state, setState] = useState<Record<string, boolean>>({})
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [state, setState] = useState<Record<string, boolean>>({});
|
||||
|
||||
const [order, setOrder] = useState<"asc" | "desc">("desc");
|
||||
const [levels, setLevels] = useState<LogLevel[]>(defaultLevels);
|
||||
|
||||
const filteredLogs = useMemo(
|
||||
() => logs
|
||||
?.filter(log => levels.includes(log.level))
|
||||
() =>
|
||||
logs
|
||||
?.filter((log) => levels.includes(log.level))
|
||||
?.[
|
||||
// Already in asc order as they are recorded. just reverse for desc
|
||||
?.[order === "desc" ? "reverse" : "concat"]?.(),
|
||||
[logs, order, levels]
|
||||
)
|
||||
order === "desc" ? "reverse" : "concat"
|
||||
]?.(),
|
||||
[logs, order, levels],
|
||||
);
|
||||
|
||||
// Sharing it as txt while its formatted allows us to share it with many more applications
|
||||
const share = useCallback(async () => {
|
||||
const uri = FileSystem.documentDirectory + "logs.txt"
|
||||
const uri = `${FileSystem.documentDirectory}logs.txt`;
|
||||
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
FileSystem.writeAsStringAsync(uri, JSON.stringify(filteredLogs))
|
||||
.then(() => {
|
||||
setLoading(false)
|
||||
Sharing.shareAsync(uri, {mimeType: "txt", UTI: "txt"})
|
||||
setLoading(false);
|
||||
Sharing.shareAsync(uri, { mimeType: "txt", UTI: "txt" });
|
||||
})
|
||||
.catch((e) => writeErrorLog("Something went wrong attempting to export", e))
|
||||
.finally(() => setLoading(false))
|
||||
}, [filteredLogs])
|
||||
.catch((e) =>
|
||||
writeErrorLog("Something went wrong attempting to export", e),
|
||||
)
|
||||
.finally(() => setLoading(false));
|
||||
}, [filteredLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => (
|
||||
loading
|
||||
? <Loader/>
|
||||
: (
|
||||
headerRight: () =>
|
||||
loading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<TouchableOpacity onPress={share}>
|
||||
<Text>{t("home.settings.logs.export_logs")}</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
),
|
||||
});
|
||||
}, [share, loading]);
|
||||
|
||||
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
|
||||
id='order'
|
||||
queryKey='log'
|
||||
@@ -93,25 +97,30 @@ export default function page() {
|
||||
<ScrollView className='pb-4 px-4'>
|
||||
<View className='flex flex-col space-y-2'>
|
||||
{filteredLogs?.map((log, index) => (
|
||||
<View
|
||||
className='bg-neutral-900 rounded-xl p-3'
|
||||
key={index}
|
||||
>
|
||||
<View className='bg-neutral-900 rounded-xl p-3' key={index}>
|
||||
<TouchableOpacity
|
||||
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
|
||||
className={`mb-1
|
||||
${log.level === "INFO" && "text-blue-500"}
|
||||
${log.level === "ERROR" && "text-red-500"}
|
||||
${log.level === "DEBUG" && "text-purple-500"}
|
||||
`}>
|
||||
`}
|
||||
>
|
||||
{log.level}
|
||||
</Text>
|
||||
|
||||
<Text className="text-xs">{new Date(log.timestamp).toLocaleString()}</Text>
|
||||
<Text className='text-xs'>
|
||||
{new Date(log.timestamp).toLocaleString()}
|
||||
</Text>
|
||||
</View>
|
||||
<Text uiTextView selectable className='text-xs'>
|
||||
{log.message}
|
||||
@@ -121,14 +130,14 @@ export default function page() {
|
||||
{log.data && (
|
||||
<>
|
||||
{!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]}>
|
||||
<View className="mt-2 flex flex-col space-y-2">
|
||||
<ScrollView className="rounded-xl" style={codeBlockStyle}>
|
||||
<Text>
|
||||
{JSON.stringify(log.data, null, 2)}
|
||||
</Text>
|
||||
<View className='mt-2 flex flex-col space-y-2'>
|
||||
<ScrollView className='rounded-xl' style={codeBlockStyle}>
|
||||
<Text>{JSON.stringify(log.data, null, 2)}</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Collapsible>
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function page() {
|
||||
showText={!pluginSettings?.searchEngine?.locked}
|
||||
className='mt-2 flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4'
|
||||
>
|
||||
<View className={`flex flex-row items-center bg-neutral-900 h-11 pr-4`}>
|
||||
<View className={"flex flex-row items-center bg-neutral-900 h-11 pr-4"}>
|
||||
<Text className='mr-4'>
|
||||
{t("home.settings.plugins.marlin_search.url")}
|
||||
</Text>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function page() {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedUrl = newVal.endsWith("/") ? newVal : newVal + "/";
|
||||
const updatedUrl = newVal.endsWith("/") ? newVal : `${newVal}/`;
|
||||
|
||||
updateSettings({
|
||||
optimizedVersionsServerUrl: updatedUrl,
|
||||
|
||||
@@ -133,7 +133,7 @@ const page: React.FC = () => {
|
||||
queryFn={fetchItems}
|
||||
queryKey={["actor", "movies", actorId]}
|
||||
/>
|
||||
<View className='h-12'></View>
|
||||
<View className='h-12' />
|
||||
</View>
|
||||
</ParallaxScrollView>
|
||||
);
|
||||
|
||||
@@ -157,9 +157,8 @@ const page: React.FC = () => {
|
||||
|
||||
if (accumulatedItems < totalItems) {
|
||||
return lastPage?.Items?.length * pages.length;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
initialPageParam: 0,
|
||||
enabled: !!api && !!user?.Id && !!collection,
|
||||
@@ -412,7 +411,7 @@ const page: React.FC = () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
}}
|
||||
></View>
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -93,19 +93,19 @@ const Page: React.FC = () => {
|
||||
height: item?.Type === "Episode" ? 300 : 450,
|
||||
}}
|
||||
className='bg-transparent rounded-lg mb-4 w-full'
|
||||
></View>
|
||||
<View className='h-6 bg-neutral-900 rounded mb-4 w-14'></View>
|
||||
<View className='h-10 bg-neutral-900 rounded-lg mb-2 w-1/2'></View>
|
||||
<View className='h-3 bg-neutral-900 rounded mb-3 w-8'></View>
|
||||
/>
|
||||
<View className='h-6 bg-neutral-900 rounded mb-4 w-14' />
|
||||
<View className='h-10 bg-neutral-900 rounded-lg mb-2 w-1/2' />
|
||||
<View className='h-3 bg-neutral-900 rounded mb-3 w-8' />
|
||||
<View className='flex flex-row space-x-1 mb-8'>
|
||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14'></View>
|
||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14'></View>
|
||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14'></View>
|
||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||
</View>
|
||||
<View className='h-3 bg-neutral-900 rounded w-2/3 mb-1'></View>
|
||||
<View className='h-10 bg-neutral-900 rounded-lg w-full mb-2'></View>
|
||||
<View className='h-12 bg-neutral-900 rounded-lg w-full mb-2'></View>
|
||||
<View className='h-24 bg-neutral-900 rounded-lg mb-1 w-full'></View>
|
||||
<View className='h-3 bg-neutral-900 rounded w-2/3 mb-1' />
|
||||
<View className='h-10 bg-neutral-900 rounded-lg w-full mb-2' />
|
||||
<View className='h-12 bg-neutral-900 rounded-lg w-full mb-2' />
|
||||
<View className='h-24 bg-neutral-900 rounded-lg mb-1 w-full' />
|
||||
</Animated.View>
|
||||
{item && <ItemContent item={item} />}
|
||||
</View>
|
||||
|
||||
@@ -33,9 +33,11 @@ export default function page() {
|
||||
};
|
||||
|
||||
return jellyseerrApi?.discover(
|
||||
(type == DiscoverSliderType.NETWORKS
|
||||
`${
|
||||
type === DiscoverSliderType.NETWORKS
|
||||
? Endpoints.DISCOVER_TV_NETWORK
|
||||
: Endpoints.DISCOVER_MOVIES_STUDIO) + `/${companyId}`,
|
||||
: Endpoints.DISCOVER_MOVIES_STUDIO
|
||||
}/${companyId}`,
|
||||
params,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function page() {
|
||||
};
|
||||
|
||||
return jellyseerrApi?.discover(
|
||||
type == DiscoverSliderType.MOVIE_GENRES
|
||||
type === DiscoverSliderType.MOVIE_GENRES
|
||||
? Endpoints.DISCOVER_MOVIES
|
||||
: Endpoints.DISCOVER_TV,
|
||||
params,
|
||||
|
||||
@@ -240,7 +240,7 @@ const Page: React.FC = () => {
|
||||
<GenreTags genres={details?.genres?.map((g) => g.name) || []} />
|
||||
</View>
|
||||
{isLoading || isFetching ? (
|
||||
<Button loading={true} disabled={true} color='purple'></Button>
|
||||
<Button loading={true} disabled={true} color='purple' />
|
||||
) : canRequest ? (
|
||||
<Button color='purple' onPress={request}>
|
||||
{t("jellyseerr.request_button")}
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function page() {
|
||||
height: HOUR_HEIGHT,
|
||||
}}
|
||||
className='bg-neutral-800'
|
||||
></View>
|
||||
/>
|
||||
{channels?.Items?.map((c, i) => (
|
||||
<View className='h-16 w-16 mr-4 rounded-lg overflow-hidden' key={i}>
|
||||
<ItemImage
|
||||
|
||||
@@ -87,7 +87,6 @@ const page: React.FC = () => {
|
||||
<View className='flex flex-row items-center space-x-2'>
|
||||
<AddToFavorites item={item} />
|
||||
{!Platform.isTV && (
|
||||
<>
|
||||
<DownloadItems
|
||||
size='large'
|
||||
title={t("item_card.download.download_series")}
|
||||
@@ -103,7 +102,6 @@ const page: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
),
|
||||
@@ -127,8 +125,7 @@ const page: React.FC = () => {
|
||||
/>
|
||||
}
|
||||
logo={
|
||||
<>
|
||||
{logoUrl ? (
|
||||
logoUrl ? (
|
||||
<Image
|
||||
source={{
|
||||
uri: logoUrl,
|
||||
@@ -139,8 +136,7 @@ const page: React.FC = () => {
|
||||
resizeMode: "contain",
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<View className='flex flex-col pt-4'>
|
||||
|
||||
@@ -216,9 +216,8 @@ const Page = () => {
|
||||
|
||||
if (accumulatedItems < totalItems) {
|
||||
return lastPage?.Items?.length * pages.length;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
initialPageParam: 0,
|
||||
enabled: !!api && !!user?.Id && !!library,
|
||||
@@ -478,7 +477,7 @@ const Page = () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
}}
|
||||
></View>
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function IndexLayout() {
|
||||
headerLargeStyle: {
|
||||
backgroundColor: "black",
|
||||
},
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerTransparent: Platform.OS === "ios",
|
||||
headerShadowVisible: false,
|
||||
headerRight: () =>
|
||||
!pluginSettings?.libraryOptions?.locked &&
|
||||
@@ -159,7 +159,7 @@ export default function IndexLayout() {
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
showTitles: newValue === "on" ? true : false,
|
||||
showTitles: newValue === "on",
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -176,7 +176,7 @@ export default function IndexLayout() {
|
||||
updateSettings({
|
||||
libraryOptions: {
|
||||
...settings.libraryOptions,
|
||||
showStats: newValue === "on" ? true : false,
|
||||
showStats: newValue === "on",
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -200,7 +200,7 @@ export default function IndexLayout() {
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerBlurEffect: "prominent",
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerTransparent: Platform.OS === "ios",
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
@@ -213,7 +213,7 @@ export default function IndexLayout() {
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerBlurEffect: "prominent",
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerTransparent: Platform.OS === "ios",
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function index() {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
}}
|
||||
className='bg-neutral-800 mx-2 my-4'
|
||||
></View>
|
||||
/>
|
||||
) : (
|
||||
<View className='h-4' />
|
||||
)
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function SearchLayout() {
|
||||
backgroundColor: "black",
|
||||
},
|
||||
headerBlurEffect: "prominent",
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerTransparent: Platform.OS === "ios",
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
@@ -33,7 +33,7 @@ export default function SearchLayout() {
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerBlurEffect: "prominent",
|
||||
headerTransparent: Platform.OS === "ios" ? true : false,
|
||||
headerTransparent: Platform.OS === "ios",
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -13,8 +13,7 @@ import SeriesPoster from "@/components/posters/SeriesPoster";
|
||||
import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
|
||||
import { SearchItemWrapper } from "@/components/search/SearchItemWrapper";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { sortOrderOptions } from "@/utils/atoms/filters";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { eventBus } from "@/utils/eventBus";
|
||||
import type {
|
||||
@@ -64,7 +63,6 @@ export default function search() {
|
||||
const [debouncedSearch] = useDebounce(search, 500);
|
||||
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
const [settings] = useSettings();
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
@@ -83,7 +81,9 @@ export default function search() {
|
||||
}, [settings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (q && q.length > 0) setSearch(q);
|
||||
if (q && q.length > 0) {
|
||||
setSearch(q);
|
||||
}
|
||||
}, [q]);
|
||||
|
||||
const searchFn = useCallback(
|
||||
@@ -94,7 +94,9 @@ export default function search() {
|
||||
types: BaseItemKind[];
|
||||
query: string;
|
||||
}): Promise<BaseItemDto[]> => {
|
||||
if (!api || !query) return [];
|
||||
if (!api || !query) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
if (searchEngine === "Jellyfin") {
|
||||
@@ -105,8 +107,10 @@ export default function search() {
|
||||
});
|
||||
|
||||
return (searchApi.data.SearchHints as BaseItemDto[]) || [];
|
||||
} else {
|
||||
if (!settings?.marlinServerUrl) return [];
|
||||
}
|
||||
if (!settings?.marlinServerUrl) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const url = `${
|
||||
settings.marlinServerUrl
|
||||
@@ -118,7 +122,9 @@ export default function search() {
|
||||
|
||||
const ids = response1.data.ids;
|
||||
|
||||
if (!ids || !ids.length) return [];
|
||||
if (!ids || !ids.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const response2 = await getItemsApi(api).getItems({
|
||||
ids,
|
||||
@@ -126,7 +132,6 @@ export default function search() {
|
||||
});
|
||||
|
||||
return (response2.data.Items as BaseItemDto[]) || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during search:", error);
|
||||
return []; // Ensure an empty array is returned in case of an error
|
||||
@@ -162,8 +167,10 @@ export default function search() {
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = eventBus.on("searchTabPressed", () => {
|
||||
// Screen not actuve
|
||||
if (!searchBarRef.current) return;
|
||||
// Screen not active
|
||||
if (!searchBarRef.current) {
|
||||
return;
|
||||
}
|
||||
// Screen is active, focus search bar
|
||||
searchBarRef.current?.focus();
|
||||
});
|
||||
@@ -254,7 +261,6 @@ export default function search() {
|
||||
}}
|
||||
>
|
||||
{jellyseerrApi && (
|
||||
<>
|
||||
<ScrollView
|
||||
horizontal
|
||||
className='flex flex-row flex-wrap space-x-2 px-4 mb-2'
|
||||
@@ -287,7 +293,7 @@ export default function search() {
|
||||
queryKey='jellyseerr_search'
|
||||
queryFn={async () =>
|
||||
Object.keys(JellyseerrSearchSort).filter((v) =>
|
||||
isNaN(Number(v)),
|
||||
Number.isNaN(Number(v)),
|
||||
)
|
||||
}
|
||||
set={(value) => setJellyseerrOrderBy(value[0])}
|
||||
@@ -311,7 +317,6 @@ export default function search() {
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View className='mt-2'>
|
||||
@@ -411,9 +416,8 @@ export default function search() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{searchType === "Library" && (
|
||||
<>
|
||||
{!loading && noResults && debouncedSearch.length > 0 ? (
|
||||
{searchType === "Library" &&
|
||||
(!loading && noResults && debouncedSearch.length > 0 ? (
|
||||
<View>
|
||||
<Text className='text-center text-lg font-bold mt-4'>
|
||||
{t("search.no_results_found_for")}
|
||||
@@ -434,9 +438,7 @@ export default function search() {
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
) : null)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</>
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function TabLayout() {
|
||||
options={{
|
||||
title: t("tabs.home"),
|
||||
tabBarIcon:
|
||||
Platform.OS == "android"
|
||||
Platform.OS === "android"
|
||||
? ({ color, focused, size }) =>
|
||||
require("@/assets/icons/house.fill.png")
|
||||
: ({ focused }) =>
|
||||
@@ -91,7 +91,7 @@ export default function TabLayout() {
|
||||
options={{
|
||||
title: t("tabs.search"),
|
||||
tabBarIcon:
|
||||
Platform.OS == "android"
|
||||
Platform.OS === "android"
|
||||
? ({ color, focused, size }) =>
|
||||
require("@/assets/icons/magnifyingglass.png")
|
||||
: ({ focused }) =>
|
||||
@@ -105,7 +105,7 @@ export default function TabLayout() {
|
||||
options={{
|
||||
title: t("tabs.favorites"),
|
||||
tabBarIcon:
|
||||
Platform.OS == "android"
|
||||
Platform.OS === "android"
|
||||
? ({ color, focused, size }) =>
|
||||
focused
|
||||
? require("@/assets/icons/heart.fill.png")
|
||||
@@ -121,7 +121,7 @@ export default function TabLayout() {
|
||||
options={{
|
||||
title: t("tabs.library"),
|
||||
tabBarIcon:
|
||||
Platform.OS == "android"
|
||||
Platform.OS === "android"
|
||||
? ({ color, focused, size }) =>
|
||||
require("@/assets/icons/server.rack.png")
|
||||
: ({ focused }) =>
|
||||
@@ -135,9 +135,9 @@ export default function TabLayout() {
|
||||
options={{
|
||||
title: t("tabs.custom_links"),
|
||||
// @ts-expect-error
|
||||
tabBarItemHidden: settings?.showCustomMenuLinks ? false : true,
|
||||
tabBarItemHidden: !settings?.showCustomMenuLinks,
|
||||
tabBarIcon:
|
||||
Platform.OS == "android"
|
||||
Platform.OS === "android"
|
||||
? ({ focused }) => require("@/assets/icons/list.png")
|
||||
: ({ focused }) =>
|
||||
focused
|
||||
|
||||
@@ -16,7 +16,12 @@ import {
|
||||
BACKGROUND_FETCH_TASK_SESSIONS,
|
||||
registerBackgroundFetchAsyncSessions,
|
||||
} 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 { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
|
||||
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
||||
@@ -31,8 +36,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
const BackgroundFetch = !Platform.isTV
|
||||
? require("expo-background-fetch")
|
||||
: null;
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import * as Device from "expo-device";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
const Notifications = !Platform.isTV ? require("expo-notifications") : null;
|
||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||
import { Stack, router, useSegments } from "expo-router";
|
||||
@@ -161,7 +166,7 @@ if (!Platform.isTV) {
|
||||
|
||||
for (const job of jobs) {
|
||||
if (job.status === "completed") {
|
||||
const downloadUrl = url + "download/" + job.id;
|
||||
const downloadUrl = `${url}download/${job.id}`;
|
||||
const tasks = await BackGroundDownloader.checkForExistingDownloads();
|
||||
|
||||
if (tasks.find((task: { id: string }) => task.id === job.id)) {
|
||||
@@ -194,7 +199,7 @@ if (!Platform.isTV) {
|
||||
title: job.item.Name,
|
||||
body: "Download completed",
|
||||
data: {
|
||||
url: `/downloads`,
|
||||
url: "/downloads",
|
||||
},
|
||||
},
|
||||
trigger: null,
|
||||
@@ -208,7 +213,7 @@ if (!Platform.isTV) {
|
||||
title: job.item.Name,
|
||||
body: "Download failed",
|
||||
data: {
|
||||
url: `/downloads`,
|
||||
url: "/downloads",
|
||||
},
|
||||
},
|
||||
trigger: null,
|
||||
@@ -333,7 +338,7 @@ function Layout() {
|
||||
}
|
||||
|
||||
// only create push token for real devices (pointless for emulators)
|
||||
if(Device.isDevice) {
|
||||
if (Device.isDevice) {
|
||||
Notifications?.getExpoPushTokenAsync()
|
||||
.then((token: ExpoPushToken) => token && setExpoPushToken(token))
|
||||
.catch((reason: any) => console.log("Failed to get token", reason));
|
||||
@@ -357,9 +362,12 @@ function Layout() {
|
||||
Notifications?.addNotificationResponseReceivedListener(
|
||||
(response: NotificationResponse) => {
|
||||
// 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) {
|
||||
const type = data?.type?.toLower?.();
|
||||
@@ -367,12 +375,12 @@ function Layout() {
|
||||
|
||||
switch (type) {
|
||||
case "movie":
|
||||
router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`)
|
||||
router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`);
|
||||
break;
|
||||
case "episode":
|
||||
// We just clicked a notification for an individual episode.
|
||||
if (itemId) {
|
||||
router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`)
|
||||
router.push(`/(auth)/(tabs)/home/items/page?id=${itemId}`);
|
||||
}
|
||||
// summarized season notification for multiple episodes. Bring them to series season
|
||||
else {
|
||||
@@ -380,10 +388,11 @@ function Layout() {
|
||||
const seasonIndex = data.seasonIndex;
|
||||
|
||||
if (seasonIndex) {
|
||||
router.push(`/(auth)/(tabs)/home/series/${seriesId}?seasonIndex=${seasonIndex}`)
|
||||
}
|
||||
else {
|
||||
router.push(`/(auth)/(tabs)/home/series/${seriesId}`)
|
||||
router.push(
|
||||
`/(auth)/(tabs)/home/series/${seriesId}?seasonIndex=${seasonIndex}`,
|
||||
);
|
||||
} else {
|
||||
router.push(`/(auth)/(tabs)/home/series/${seriesId}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -218,16 +218,14 @@ const Login: React.FC = () => {
|
||||
<View className='px-4 -mt-20 w-full'>
|
||||
<View className='flex flex-col space-y-2'>
|
||||
<Text className='text-2xl font-bold -mb-2'>
|
||||
<>
|
||||
{serverName ? (
|
||||
<>
|
||||
{t("login.login_to_title") + " "}
|
||||
{`${t("login.login_to_title")} `}
|
||||
<Text className='text-purple-600'>{serverName}</Text>
|
||||
</>
|
||||
) : (
|
||||
t("login.login_title")
|
||||
)}
|
||||
</>
|
||||
</Text>
|
||||
<Text className='text-xs text-neutral-400'>
|
||||
{api.basePath}
|
||||
@@ -284,7 +282,7 @@ const Login: React.FC = () => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='absolute bottom-0 left-0 w-full px-4 mb-2'></View>
|
||||
<View className='absolute bottom-0 left-0 w-full px-4 mb-2' />
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -17,9 +17,7 @@ Number.prototype.bytesToReadable = function (decimals = 2) {
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return (
|
||||
Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]
|
||||
);
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
Number.prototype.secondsToMilliseconds = function () {
|
||||
|
||||
13
biome.json
13
biome.json
@@ -9,7 +9,8 @@
|
||||
"ios",
|
||||
"android",
|
||||
"Streamyfin.app",
|
||||
"utils/jellyseerr"
|
||||
"utils/jellyseerr",
|
||||
".expo"
|
||||
]
|
||||
},
|
||||
"linter": {
|
||||
@@ -17,12 +18,18 @@
|
||||
"rules": {
|
||||
"style": {
|
||||
"useImportType": "off",
|
||||
"noNonNullAssertion": "off"
|
||||
"noNonNullAssertion": "off",
|
||||
"noParameterAssign": "off",
|
||||
"useLiteralEnumMembers": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off"
|
||||
},
|
||||
"recommended": true,
|
||||
"correctness": { "useExhaustiveDependencies": "off" },
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off"
|
||||
"noExplicitAny": "off",
|
||||
"noArrayIndexKey": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -73,7 +73,7 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
||||
flex flex-row items-center justify-between w-full
|
||||
${justify === "between" ? "justify-between" : "justify-center"}`}
|
||||
>
|
||||
{iconLeft ? iconLeft : <View className='w-4'></View>}
|
||||
{iconLeft ? iconLeft : <View className='w-4' />}
|
||||
<Text
|
||||
className={`
|
||||
text-white font-bold text-base
|
||||
@@ -85,7 +85,7 @@ export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
{iconRight ? iconRight : <View className='w-4'></View>}
|
||||
{iconRight ? iconRight : <View className='w-4' />}
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -27,32 +27,38 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
|
||||
* Get horizontal poster for movie and episode, with failover to primary.
|
||||
*/
|
||||
const url = useMemo(() => {
|
||||
if (!api) return;
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
if (item.Type === "Episode" && useEpisodePoster) {
|
||||
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
|
||||
}
|
||||
if (item.Type === "Episode") {
|
||||
if (item.ParentBackdropItemId && item.ParentThumbImageTag)
|
||||
if (item.ParentBackdropItemId && item.ParentThumbImageTag) {
|
||||
return `${api?.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ParentThumbImageTag}`;
|
||||
else
|
||||
}
|
||||
|
||||
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
|
||||
}
|
||||
if (item.Type === "Movie") {
|
||||
if (item.ImageTags?.["Thumb"])
|
||||
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.["Thumb"]}`;
|
||||
else
|
||||
if (item.ImageTags?.Thumb) {
|
||||
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.Thumb}`;
|
||||
}
|
||||
|
||||
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
|
||||
}
|
||||
if (item.Type === "Program") {
|
||||
if (item.ImageTags?.["Thumb"])
|
||||
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.["Thumb"]}`;
|
||||
else
|
||||
if (item.ImageTags?.Thumb) {
|
||||
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.Thumb}`;
|
||||
}
|
||||
|
||||
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
|
||||
}
|
||||
|
||||
if (item.ImageTags?.["Thumb"])
|
||||
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.["Thumb"]}`;
|
||||
else
|
||||
if (item.ImageTags?.Thumb) {
|
||||
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.Thumb}`;
|
||||
}
|
||||
|
||||
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
|
||||
}, [item]);
|
||||
|
||||
@@ -64,15 +70,12 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
|
||||
const total = endDate.getTime() - startDate.getTime();
|
||||
const elapsed = now.getTime() - startDate.getTime();
|
||||
return (elapsed / total) * 100;
|
||||
} else {
|
||||
return item.UserData?.PlayedPercentage || 0;
|
||||
}
|
||||
return item.UserData?.PlayedPercentage || 0;
|
||||
}, [item]);
|
||||
|
||||
if (!url)
|
||||
return (
|
||||
<View className='aspect-video border border-neutral-800 w-44'></View>
|
||||
);
|
||||
return <View className='aspect-video border border-neutral-800 w-44' />;
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -102,14 +105,16 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
|
||||
{progress > 0 && (
|
||||
<>
|
||||
<View
|
||||
className={`absolute w-100 bottom-0 left-0 h-1 bg-neutral-700 opacity-80 w-full`}
|
||||
></View>
|
||||
className={
|
||||
"absolute w-100 bottom-0 left-0 h-1 bg-neutral-700 opacity-80 w-full"
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
}}
|
||||
className={`absolute bottom-0 left-0 h-1 bg-purple-600 w-full`}
|
||||
></View>
|
||||
className={"absolute bottom-0 left-0 h-1 bg-purple-600 w-full"}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -147,8 +147,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
||||
</View>
|
||||
}
|
||||
logo={
|
||||
<>
|
||||
{logoUrl ? (
|
||||
logoUrl ? (
|
||||
<Image
|
||||
source={{
|
||||
uri: logoUrl,
|
||||
@@ -161,8 +160,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
||||
onLoad={() => setLoadingLogo(false)}
|
||||
onError={() => setLoadingLogo(false)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<View className='flex flex-col bg-transparent shrink'>
|
||||
|
||||
@@ -237,5 +237,5 @@ const formatFileSize = (bytes?: number | null) => {
|
||||
const i = Number.parseInt(
|
||||
Math.floor(Math.log(bytes) / Math.log(1024)).toString(),
|
||||
);
|
||||
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + " " + sizes[i];
|
||||
return `${Math.round((bytes / 1024 ** i) * 100) / 100} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
@@ -63,9 +63,8 @@ export const MoreMoviesWithActor: React.FC<Props> = ({
|
||||
const x = acc.find((item) => item.Id === current.Id);
|
||||
if (!x) {
|
||||
return acc.concat([current]);
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
return acc;
|
||||
}, [] as BaseItemDto[]) || [];
|
||||
|
||||
return uniqueItems;
|
||||
|
||||
@@ -239,7 +239,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
const derivedTargetWidth = useDerivedValue(() => {
|
||||
if (!item || !item.RunTimeTicks) return 0;
|
||||
const userData = item.UserData;
|
||||
if (userData && userData.PlaybackPositionTicks) {
|
||||
if (userData?.PlaybackPositionTicks) {
|
||||
return userData.PlaybackPositionTicks > 0
|
||||
? Math.max(
|
||||
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
|
||||
@@ -331,7 +331,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
accessibilityLabel='Play button'
|
||||
accessibilityHint='Tap to play the media'
|
||||
onPress={onPress}
|
||||
className={`relative`}
|
||||
className={"relative"}
|
||||
{...props}
|
||||
>
|
||||
<View className='absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden'>
|
||||
|
||||
@@ -85,7 +85,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
const derivedTargetWidth = useDerivedValue(() => {
|
||||
if (!item || !item.RunTimeTicks) return 0;
|
||||
const userData = item.UserData;
|
||||
if (userData && userData.PlaybackPositionTicks) {
|
||||
if (userData?.PlaybackPositionTicks) {
|
||||
return userData.PlaybackPositionTicks > 0
|
||||
? Math.max(
|
||||
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
|
||||
@@ -176,7 +176,7 @@ export const PlayButton: React.FC<Props> = ({
|
||||
accessibilityLabel='Play button'
|
||||
accessibilityHint='Tap to play the media'
|
||||
onPress={onPress}
|
||||
className={`relative`}
|
||||
className={"relative"}
|
||||
{...props}
|
||||
>
|
||||
<View className='absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden'>
|
||||
|
||||
@@ -7,7 +7,7 @@ export const WatchedIndicator: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
<>
|
||||
{item.UserData?.Played === false &&
|
||||
(item.Type === "Movie" || item.Type === "Episode") && (
|
||||
<View className='bg-purple-600 w-8 h-8 absolute -top-4 -right-4 rotate-45'></View>
|
||||
<View className='bg-purple-600 w-8 h-8 absolute -top-4 -right-4 rotate-45' />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import renderer from "react-test-renderer";
|
||||
|
||||
import { ThemedText } from "../ThemedText";
|
||||
|
||||
it(`renders correctly`, () => {
|
||||
it("renders correctly", () => {
|
||||
const tree = renderer
|
||||
.create(<ThemedText>Snapshot test!</ThemedText>)
|
||||
.toJSON();
|
||||
|
||||
@@ -6,7 +6,7 @@ interface Props extends ViewProps {}
|
||||
export const TitleHeader: React.FC<Props> = ({ ...props }) => {
|
||||
return (
|
||||
<View {...props}>
|
||||
<Text></Text>
|
||||
<Text />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -75,7 +75,7 @@ const Dropdown = <T,>({
|
||||
multiple ? (
|
||||
<DropdownMenu.CheckboxItem
|
||||
value={
|
||||
selected?.some((s) => keyExtractor(s) == keyExtractor(item))
|
||||
selected?.some((s) => keyExtractor(s) === keyExtractor(item))
|
||||
? "on"
|
||||
: "off"
|
||||
}
|
||||
@@ -83,7 +83,7 @@ const Dropdown = <T,>({
|
||||
onValueChange={(next: "on" | "off", previous: "on" | "off") => {
|
||||
setSelected((p) => {
|
||||
const prev = p || [];
|
||||
if (next == "on") {
|
||||
if (next === "on") {
|
||||
return [...prev, item];
|
||||
}
|
||||
return [
|
||||
|
||||
@@ -65,17 +65,13 @@ export const HorizontalScroll = forwardRef<
|
||||
}: {
|
||||
item: T;
|
||||
index: number;
|
||||
}) => (
|
||||
<View className='mr-2'>
|
||||
<React.Fragment>{renderItem(item, index)}</React.Fragment>
|
||||
</View>
|
||||
);
|
||||
}) => <View className='mr-2'>{renderItem(item, index)}</View>;
|
||||
|
||||
if (!data || loading) {
|
||||
return (
|
||||
<View className='px-4 mb-2'>
|
||||
<View className='bg-neutral-950 h-24 w-full rounded-md mb-2'></View>
|
||||
<View className='bg-neutral-950 h-10 w-full rounded-md mb-1'></View>
|
||||
<View className='bg-neutral-950 h-24 w-full rounded-md mb-2' />
|
||||
<View className='bg-neutral-950 h-10 w-full rounded-md mb-1' />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,9 +75,8 @@ export function InfiniteHorizontalScroll({
|
||||
|
||||
if (accumulatedItems < totalItems) {
|
||||
return lastPage?.Items?.length * pages.length;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
initialPageParam: 0,
|
||||
enabled: !!api && !!user?.Id,
|
||||
@@ -118,9 +117,7 @@ export function InfiniteHorizontalScroll({
|
||||
<FlashList
|
||||
data={flatData}
|
||||
renderItem={({ item, index }) => (
|
||||
<View className='mr-2'>
|
||||
<React.Fragment>{renderItem(item, index)}</React.Fragment>
|
||||
</View>
|
||||
<View className='mr-2'>{renderItem(item, index)}</View>
|
||||
)}
|
||||
estimatedItemSize={height}
|
||||
horizontal
|
||||
|
||||
@@ -36,7 +36,7 @@ export const ItemImage: FC<Props> = ({
|
||||
|
||||
const source = useMemo(() => {
|
||||
if (!api) {
|
||||
onError && onError();
|
||||
onError?.();
|
||||
return;
|
||||
}
|
||||
return getItemImage({
|
||||
|
||||
@@ -5,7 +5,7 @@ export const LargePoster: React.FC<{ url?: string | null }> = ({ url }) => {
|
||||
if (!url)
|
||||
return (
|
||||
<View className='p-4 rounded-xl overflow-hidden '>
|
||||
<View className='w-full aspect-video rounded-xl overflow-hidden border border-neutral-800'></View>
|
||||
<View className='w-full aspect-video rounded-xl overflow-hidden border border-neutral-800' />
|
||||
</View>
|
||||
);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export function Text(
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
else
|
||||
|
||||
return (
|
||||
<UITextView
|
||||
allowFontScaling={false}
|
||||
|
||||
@@ -20,10 +20,10 @@ export const VerticalSkeleton: React.FC<Props> = ({ index, ...props }) => {
|
||||
aspectRatio: "10/15",
|
||||
}}
|
||||
className='w-full bg-neutral-800 mb-2 rounded-lg'
|
||||
></View>
|
||||
<View className='h-2 bg-neutral-800 rounded-full mb-1'></View>
|
||||
<View className='h-2 bg-neutral-800 rounded-full mb-1'></View>
|
||||
<View className='h-2 bg-neutral-800 rounded-full mb-2 w-1/2'></View>
|
||||
/>
|
||||
<View className='h-2 bg-neutral-800 rounded-full mb-1' />
|
||||
<View className='h-2 bg-neutral-800 rounded-full mb-1' />
|
||||
<View className='h-2 bg-neutral-800 rounded-full mb-2 w-1/2' />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -80,8 +80,6 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
||||
task.stop();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
await removeProcess(id);
|
||||
await queryClient.refetchQueries({ queryKey: ["jobs"] });
|
||||
@@ -131,7 +129,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
|
||||
? `${Math.max(5, process.progress)}%`
|
||||
: "5%",
|
||||
}}
|
||||
></View>
|
||||
/>
|
||||
)}
|
||||
<View className='px-3 py-1.5 flex flex-col w-full'>
|
||||
<View className='flex flex-row items-center w-full'>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const SeriesCard: React.FC<{ items: BaseItemDto[] }> = ({ items }) => {
|
||||
destructiveButtonIndex,
|
||||
},
|
||||
(selectedIndex) => {
|
||||
if (selectedIndex == destructiveButtonIndex) {
|
||||
if (selectedIndex === destructiveButtonIndex) {
|
||||
deleteSeries();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ interface FilterButtonProps<T> extends ViewProps {
|
||||
queryFn: (params: any) => Promise<any>;
|
||||
searchFilter?: (item: T, query: string) => boolean;
|
||||
renderItemLabel: (item: T) => React.ReactNode;
|
||||
multiple?: boolean,
|
||||
multiple?: boolean;
|
||||
icon?: "filter" | "sort";
|
||||
}
|
||||
|
||||
|
||||
@@ -183,15 +183,13 @@ export const FilterSheet = <T,>({
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (multiple) {
|
||||
if (!values.includes(item))
|
||||
set(values.concat(item))
|
||||
else set(values.filter(v => v !== item))
|
||||
if (!values.includes(item)) set(values.concat(item));
|
||||
else set(values.filter((v) => v !== item));
|
||||
|
||||
setTimeout(() => {
|
||||
setOpen(false);
|
||||
}, 250);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (!values.includes(item)) {
|
||||
set([item]);
|
||||
setTimeout(() => {
|
||||
@@ -214,7 +212,7 @@ export const FilterSheet = <T,>({
|
||||
height: StyleSheet.hairlineWidth,
|
||||
}}
|
||||
className='h-1 divide-neutral-700 '
|
||||
></View>
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
@@ -63,7 +63,7 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
||||
>
|
||||
{[1, 2, 3].map((i) => (
|
||||
<View className='w-44' key={i}>
|
||||
<View className='bg-neutral-900 h-24 w-full rounded-md mb-1'></View>
|
||||
<View className='bg-neutral-900 h-24 w-full rounded-md mb-1' />
|
||||
<View className='rounded-md overflow-hidden mb-1 self-start'>
|
||||
<Text
|
||||
className='text-neutral-900 bg-neutral-900 rounded-md'
|
||||
|
||||
@@ -34,10 +34,7 @@ export const Stepper: React.FC<StepperProps> = ({
|
||||
<Text>-</Text>
|
||||
</TouchableOpacity>
|
||||
<Text
|
||||
className={
|
||||
"w-auto h-8 bg-neutral-800 py-2 px-1 flex items-center justify-center" +
|
||||
(appendValue ? "first-letter:px-2" : "")
|
||||
}
|
||||
className={`w-auto h-8 bg-neutral-800 py-2 px-1 flex items-center justify-center${appendValue ? "first-letter:px-2" : ""}`}
|
||||
>
|
||||
{value}
|
||||
{appendValue}
|
||||
|
||||
@@ -52,7 +52,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({
|
||||
} = useReactNavigationQuery({
|
||||
queryKey: ["search", "jellyseerr", "discoverSettings", searchQuery],
|
||||
queryFn: async () => jellyseerrApi?.discoverSettings(),
|
||||
enabled: !!jellyseerrApi && searchQuery.length == 0,
|
||||
enabled: !!jellyseerrApi && searchQuery.length === 0,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -110,7 +110,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({
|
||||
(r) => r.mediaType === MediaType.MOVIE,
|
||||
) as MovieResult[],
|
||||
sortingType || [
|
||||
(m) => m.title.toLowerCase() == searchQuery.toLowerCase(),
|
||||
(m) => m.title.toLowerCase() === searchQuery.toLowerCase(),
|
||||
],
|
||||
order || "desc",
|
||||
),
|
||||
@@ -124,7 +124,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({
|
||||
(r) => r.mediaType === MediaType.TV,
|
||||
) as TvResult[],
|
||||
sortingType || [
|
||||
(t) => t.name.toLowerCase() == searchQuery.toLowerCase(),
|
||||
(t) => t.name.toLowerCase() === searchQuery.toLowerCase(),
|
||||
],
|
||||
order || "desc",
|
||||
),
|
||||
@@ -138,7 +138,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({
|
||||
(r) => r.mediaType === "person",
|
||||
) as PersonResult[],
|
||||
sortingType || [
|
||||
(p) => p.name.toLowerCase() == searchQuery.toLowerCase(),
|
||||
(p) => p.name.toLowerCase() === searchQuery.toLowerCase(),
|
||||
],
|
||||
order || "desc",
|
||||
),
|
||||
|
||||
@@ -62,7 +62,7 @@ const JellyseerrStatusIcon: React.FC<Props & ViewProps> = ({
|
||||
|
||||
return (
|
||||
badgeIcon && (
|
||||
<TouchableOpacity onPress={onPress} disabled={onPress == undefined}>
|
||||
<TouchableOpacity onPress={onPress} disabled={onPress === undefined}>
|
||||
<View
|
||||
className={`${badgeStyle ?? "bg-purple-600"} rounded-full h-6 w-6 flex items-center justify-center ${props.className}`}
|
||||
{...props}
|
||||
|
||||
@@ -123,11 +123,9 @@ const ParallaxSlideShow = <T,>({
|
||||
>
|
||||
<View className='flex flex-col space-y-4 px-4'>
|
||||
<View className='flex flex-row justify-between w-full'>
|
||||
<View className='flex flex-col w-full'>
|
||||
{HeaderContent && HeaderContent()}
|
||||
<View className='flex flex-col w-full'>{HeaderContent?.()}</View>
|
||||
</View>
|
||||
</View>
|
||||
{MainContent && MainContent()}
|
||||
{MainContent?.()}
|
||||
<View>
|
||||
<FlashList
|
||||
data={data}
|
||||
|
||||
@@ -61,7 +61,7 @@ const RequestModal = forwardRef<
|
||||
const { data: serviceSettings } = useQuery({
|
||||
queryKey: ["jellyseerr", "request", type, "service"],
|
||||
queryFn: async () =>
|
||||
jellyseerrApi?.service(type == "movie" ? "radarr" : "sonarr"),
|
||||
jellyseerrApi?.service(type === "movie" ? "radarr" : "sonarr"),
|
||||
enabled: !!jellyseerrApi && !!jellyseerrUser,
|
||||
refetchOnMount: "always",
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ const GenreSlide: React.FC<SlideProps & ViewProps> = ({ slide, ...props }) => {
|
||||
queryKey: ["jellyseerr", "discover", slide.type, slide.id],
|
||||
queryFn: async () => {
|
||||
return jellyseerrApi?.getGenreSliders(
|
||||
slide.type == DiscoverSliderType.MOVIE_GENRES
|
||||
slide.type === DiscoverSliderType.MOVIE_GENRES
|
||||
? Endpoints.MOVIE
|
||||
: Endpoints.TV,
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ const RequestCard: React.FC<{ request: MediaRequest }> = ({ request }) => {
|
||||
request.media.tmdbId,
|
||||
],
|
||||
queryFn: async () => {
|
||||
return request.media.mediaType == MediaType.MOVIE
|
||||
return request.media.mediaType === MediaType.MOVIE
|
||||
? jellyseerrApi?.movieDetails(request.media.tmdbId)
|
||||
: jellyseerrApi?.tvDetails(request.media.tmdbId);
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@ const Slide = <T,>({
|
||||
return (
|
||||
<View {...props}>
|
||||
<Text className='font-bold text-lg mb-2 px-4'>
|
||||
{t("search." + DiscoverSliderType[slide.type].toString().toLowerCase())}
|
||||
{t(`search.${DiscoverSliderType[slide.type].toString().toLowerCase()}`)}
|
||||
</Text>
|
||||
<FlashList
|
||||
horizontal
|
||||
|
||||
@@ -29,8 +29,8 @@ export const EpisodePoster: React.FC<MoviePosterProps> = ({
|
||||
);
|
||||
|
||||
const blurhash = useMemo(() => {
|
||||
const key = item.ImageTags?.["Primary"] as string;
|
||||
return item.ImageBlurHashes?.["Primary"]?.[key];
|
||||
const key = item.ImageTags?.Primary as string;
|
||||
return item.ImageBlurHashes?.Primary?.[key];
|
||||
}, [item]);
|
||||
|
||||
return (
|
||||
@@ -57,7 +57,7 @@ export const EpisodePoster: React.FC<MoviePosterProps> = ({
|
||||
/>
|
||||
<WatchedIndicator item={item} />
|
||||
{showProgress && progress > 0 && (
|
||||
<View className='h-1 bg-red-600 w-full'></View>
|
||||
<View className='h-1 bg-red-600 w-full' />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ export const ItemPoster: React.FC<Props> = ({
|
||||
/>
|
||||
<WatchedIndicator item={item} />
|
||||
{showProgress && progress > 0 && (
|
||||
<View className='h-1 bg-red-600 w-full'></View>
|
||||
<View className='h-1 bg-red-600 w-full' />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -129,7 +129,7 @@ const JellyseerrPoster: React.FC<Props> = ({
|
||||
posterSrc={posterSrc!}
|
||||
mediaType={mediaType}
|
||||
>
|
||||
<View className={`flex flex-col mr-2 h-auto`}>
|
||||
<View className={"flex flex-col mr-2 h-auto"}>
|
||||
<View
|
||||
className={`relative rounded-lg overflow-hidden border border-neutral-900 ${size} aspect-[${ratio}]`}
|
||||
>
|
||||
|
||||
@@ -31,8 +31,8 @@ const MoviePoster: React.FC<MoviePosterProps> = ({
|
||||
);
|
||||
|
||||
const blurhash = useMemo(() => {
|
||||
const key = item.ImageTags?.["Primary"] as string;
|
||||
return item.ImageBlurHashes?.["Primary"]?.[key];
|
||||
const key = item.ImageTags?.Primary as string;
|
||||
return item.ImageBlurHashes?.Primary?.[key];
|
||||
}, [item]);
|
||||
|
||||
return (
|
||||
@@ -59,7 +59,7 @@ const MoviePoster: React.FC<MoviePosterProps> = ({
|
||||
/>
|
||||
<WatchedIndicator item={item} />
|
||||
{showProgress && progress > 0 && (
|
||||
<View className='h-1 bg-red-600 w-full'></View>
|
||||
<View className='h-1 bg-red-600 w-full' />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ const ParentPoster: React.FC<PosterProps> = ({ id }) => {
|
||||
style={{
|
||||
aspectRatio: "10/15",
|
||||
}}
|
||||
></View>
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,7 +16,7 @@ const Poster: React.FC<PosterProps> = ({ id, url, blurhash }) => {
|
||||
style={{
|
||||
aspectRatio: "10/15",
|
||||
}}
|
||||
></View>
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -27,8 +27,8 @@ const SeriesPoster: React.FC<MoviePosterProps> = ({ item }) => {
|
||||
}, [item]);
|
||||
|
||||
const blurhash = useMemo(() => {
|
||||
const key = item.ImageTags?.["Primary"] as string;
|
||||
return item.ImageBlurHashes?.["Primary"]?.[key];
|
||||
const key = item.ImageTags?.Primary as string;
|
||||
return item.ImageBlurHashes?.Primary?.[key];
|
||||
}, [item]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -35,11 +35,11 @@ export const LoadingSkeleton: React.FC<Props> = ({ isLoading }) => {
|
||||
<Animated.View style={animatedStyle} className='mt-2 absolute w-full'>
|
||||
{[1, 2, 3].map((s) => (
|
||||
<View className='px-4 mb-4' key={s}>
|
||||
<View className='w-1/2 bg-neutral-900 h-6 mb-2 rounded-lg'></View>
|
||||
<View className='w-1/2 bg-neutral-900 h-6 mb-2 rounded-lg' />
|
||||
<View className='flex flex-row gap-2'>
|
||||
{[1, 2, 3].map((i) => (
|
||||
<View className='w-28' key={i}>
|
||||
<View className='bg-neutral-900 h-40 w-full rounded-md mb-1'></View>
|
||||
<View className='bg-neutral-900 h-40 w-full rounded-md mb-1' />
|
||||
<View className='rounded-md overflow-hidden mb-1 self-start'>
|
||||
<Text
|
||||
className='text-neutral-900 bg-neutral-900 rounded-md'
|
||||
|
||||
@@ -57,7 +57,7 @@ export const NextItemButton: React.FC<Props> = ({
|
||||
return (
|
||||
<Button
|
||||
onPress={() => router.setParams({ id: nextItem?.Id })}
|
||||
className={`h-12 aspect-square`}
|
||||
className={"h-12 aspect-square"}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -146,7 +146,7 @@ export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
{episodes?.length || 0 > 0 ? (
|
||||
{episodes?.length ? (
|
||||
<View className='flex flex-row items-center space-x-2'>
|
||||
<DownloadItems
|
||||
title={t("item_card.download.download_season")}
|
||||
|
||||
@@ -19,7 +19,7 @@ export const Dashboard = () => {
|
||||
<View>
|
||||
<ListGroup title={t("home.settings.dashboard.title")} className='mt-4'>
|
||||
<ListItem
|
||||
className={sessions.length != 0 ? "bg-purple-900" : ""}
|
||||
className={sessions.length !== 0 ? "bg-purple-900" : ""}
|
||||
onPress={() => router.push("/settings/dashboard/sessions")}
|
||||
title={t("home.settings.dashboard.sessions_title")}
|
||||
showArrow
|
||||
|
||||
@@ -140,7 +140,7 @@ export default function DownloadSettings({ ...props }) {
|
||||
onPress={() => router.push("/settings/optimized-server/page")}
|
||||
showArrow
|
||||
title={t("home.settings.downloads.optimized_versions_server")}
|
||||
></ListItem>
|
||||
/>
|
||||
</ListGroup>
|
||||
</DisabledSetting>
|
||||
);
|
||||
|
||||
@@ -51,13 +51,13 @@ type ScrollingCollectionListSection = {
|
||||
orientation?: "horizontal" | "vertical";
|
||||
};
|
||||
|
||||
type MediaListSection = {
|
||||
type MediaListSectionType = {
|
||||
type: "MediaListSection";
|
||||
queryKey: (string | undefined)[];
|
||||
queryFn: QueryFunction<BaseItemDto>;
|
||||
};
|
||||
|
||||
type Section = ScrollingCollectionListSection | MediaListSection;
|
||||
type Section = ScrollingCollectionListSection | MediaListSectionType;
|
||||
|
||||
export const HomeIndex = () => {
|
||||
const router = useRouter();
|
||||
@@ -133,7 +133,7 @@ export const HomeIndex = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = NetInfo.addEventListener((state) => {
|
||||
if (state.isConnected == false || state.isInternetReachable === false)
|
||||
if (state.isConnected === false || state.isInternetReachable === false)
|
||||
setIsConnected(false);
|
||||
else setIsConnected(true);
|
||||
});
|
||||
@@ -236,7 +236,7 @@ export const HomeIndex = () => {
|
||||
const title = t("home.recently_added_in", { libraryName: c.Name });
|
||||
const queryKey = [
|
||||
"home",
|
||||
"recentlyAddedIn" + c.CollectionType,
|
||||
`recentlyAddedIn${c.CollectionType}`,
|
||||
user?.Id!,
|
||||
c.Id!,
|
||||
];
|
||||
@@ -353,7 +353,8 @@ export const HomeIndex = () => {
|
||||
parentId: section.items?.parentId,
|
||||
});
|
||||
return response.data.Items || [];
|
||||
} else if (section.nextUp) {
|
||||
}
|
||||
if (section.nextUp) {
|
||||
const response = await getTvShowsApi(api).getNextUp({
|
||||
userId: user?.Id,
|
||||
fields: ["MediaSourceCount"],
|
||||
@@ -363,7 +364,9 @@ export const HomeIndex = () => {
|
||||
enableRewatching: section.items?.enableRewatching,
|
||||
});
|
||||
return response.data.Items || [];
|
||||
} else if (section.latest) {
|
||||
}
|
||||
|
||||
if (section.latest) {
|
||||
const response = await getUserLibraryApi(api).getLatestMedia({
|
||||
userId: user?.Id,
|
||||
includeItemTypes: section.latest?.includeItemTypes,
|
||||
@@ -472,7 +475,8 @@ export const HomeIndex = () => {
|
||||
hideIfEmpty
|
||||
/>
|
||||
);
|
||||
} else if (section.type === "MediaListSection") {
|
||||
}
|
||||
if (section.type === "MediaListSection") {
|
||||
return (
|
||||
<MediaListSection
|
||||
key={index}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const OptimizedServerForm: React.FC<Props> = ({
|
||||
return (
|
||||
<View>
|
||||
<View className='flex flex-col rounded-xl overflow-hidden pl-4 bg-neutral-900 px-4'>
|
||||
<View className={`flex flex-row items-center bg-neutral-900 h-11 pr-4`}>
|
||||
<View className={"flex flex-row items-center bg-neutral-900 h-11 pr-4"}>
|
||||
<Text className='mr-4'>{t("home.settings.downloads.url")}</Text>
|
||||
<TextInput
|
||||
className='text-white'
|
||||
|
||||
@@ -81,7 +81,7 @@ export const StorageSettings = () => {
|
||||
{size && (
|
||||
<>
|
||||
<View className='flex flex-row items-center'>
|
||||
<View className='w-3 h-3 rounded-full bg-purple-600 mr-1'></View>
|
||||
<View className='w-3 h-3 rounded-full bg-purple-600 mr-1' />
|
||||
<Text className='text-white text-xs'>
|
||||
{t("home.settings.storage.app_usage", {
|
||||
usedSpace: calculatePercentage(size.app, size.total),
|
||||
@@ -89,7 +89,7 @@ export const StorageSettings = () => {
|
||||
</Text>
|
||||
</View>
|
||||
<View className='flex flex-row items-center'>
|
||||
<View className='w-3 h-3 rounded-full bg-purple-400 mr-1'></View>
|
||||
<View className='w-3 h-3 rounded-full bg-purple-400 mr-1' />
|
||||
<Text className='text-white text-xs'>
|
||||
{t("home.settings.storage.device_usage", {
|
||||
availableSpace: calculatePercentage(
|
||||
|
||||
@@ -168,7 +168,7 @@ export const EpisodeList: React.FC<Props> = ({ item, close, goToItem }) => {
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
className={`flex flex-row items-center space-x-2 z-10 p-4`}
|
||||
className={"flex flex-row items-center space-x-2 z-10 p-4"}
|
||||
>
|
||||
{seriesItem && (
|
||||
<SeasonDropdown
|
||||
|
||||
@@ -47,7 +47,7 @@ const SliderScrubber: React.FC<SliderScrubberProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<View className={`flex flex-col w-full shrink`}>
|
||||
<View className={"flex flex-col w-full shrink"}>
|
||||
<Slider
|
||||
theme={{
|
||||
maximumTrackTintColor: "rgba(255,255,255,0.2)",
|
||||
|
||||
@@ -115,7 +115,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
});
|
||||
return;
|
||||
}
|
||||
setTrack && setTrack(index);
|
||||
setTrack?.(index);
|
||||
router.setParams({
|
||||
[paramKey]: serverIndex.toString(),
|
||||
});
|
||||
|
||||
@@ -32,8 +32,8 @@ export const useTapDetection = ({
|
||||
|
||||
const touchDuration = touchEndTime - touchStartTime.current;
|
||||
const touchDistance = Math.sqrt(
|
||||
Math.pow(touchEndPosition.x - touchStartPosition.current.x, 2) +
|
||||
Math.pow(touchEndPosition.y - touchStartPosition.current.y, 2),
|
||||
(touchEndPosition.x - touchStartPosition.current.x) ** 2 +
|
||||
(touchEndPosition.y - touchStartPosition.current.y) ** 2,
|
||||
);
|
||||
|
||||
if (touchDuration < maxDuration && touchDistance < maxDistance) {
|
||||
|
||||
@@ -45,15 +45,13 @@ export const VideoDebugInfo: React.FC<Props> = ({ playerRef, ...props }) => {
|
||||
>
|
||||
<Text className='font-bold'>{t("player.playback_state")}</Text>
|
||||
<Text className='font-bold mt-2.5'>{t("player.audio_tracks")}</Text>
|
||||
{audioTracks &&
|
||||
audioTracks.map((track, index) => (
|
||||
{audioTracks?.map((track, index) => (
|
||||
<Text key={index}>
|
||||
{track.name} ({t("player.index")} {track.index})
|
||||
</Text>
|
||||
))}
|
||||
<Text className='font-bold mt-2.5'>{t("player.subtitles_tracks")}</Text>
|
||||
{subtitleTracks &&
|
||||
subtitleTracks.map((track, index) => (
|
||||
{subtitleTracks?.map((track, index) => (
|
||||
<Text key={index}>
|
||||
{track.name} ({t("player.index")} {track.index})
|
||||
</Text>
|
||||
|
||||
@@ -35,7 +35,7 @@ export const useDownloadedFileOpener = () => {
|
||||
async (item: BaseItemDto) => {
|
||||
try {
|
||||
// @ts-expect-error
|
||||
router.push("/player/direct-player?offline=true&itemId=" + item.Id);
|
||||
router.push(`/player/direct-player?offline=true&itemId=${item.Id}`);
|
||||
} catch (error) {
|
||||
writeToLog("ERROR", "Error opening file", error);
|
||||
console.error("Error opening file:", error);
|
||||
|
||||
@@ -38,7 +38,7 @@ export const useImageColors = ({
|
||||
const source = useMemo(() => {
|
||||
if (!api) return;
|
||||
if (url) return { uri: url };
|
||||
else if (item)
|
||||
if (item)
|
||||
return getItemImage({
|
||||
item,
|
||||
api,
|
||||
@@ -46,7 +46,7 @@ export const useImageColors = ({
|
||||
quality: 80,
|
||||
width: 300,
|
||||
});
|
||||
else return null;
|
||||
return null;
|
||||
}, [api, item]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -87,11 +87,11 @@ export enum Endpoints {
|
||||
STUDIO = "/studio",
|
||||
GENRE_SLIDER = "/genreslider",
|
||||
DISCOVER = "/discover",
|
||||
DISCOVER_TRENDING = DISCOVER + "/trending",
|
||||
DISCOVER_MOVIES = DISCOVER + "/movies",
|
||||
DISCOVER_TRENDING = `${DISCOVER}/trending`,
|
||||
DISCOVER_MOVIES = `${DISCOVER}/movies`,
|
||||
DISCOVER_TV = DISCOVER + TV,
|
||||
DISCOVER_TV_NETWORK = DISCOVER + TV + NETWORK,
|
||||
DISCOVER_MOVIES_STUDIO = DISCOVER + `${MOVIE}s` + STUDIO,
|
||||
DISCOVER_MOVIES_STUDIO = `${DISCOVER}${MOVIE}s${STUDIO}`,
|
||||
AUTH_JELLYFIN = "/auth/jellyfin",
|
||||
}
|
||||
|
||||
@@ -159,9 +159,8 @@ export class JellyseerrApi {
|
||||
}
|
||||
toast.error(t("jellyseerr.toasts.jellyseerr_test_failed"));
|
||||
writeErrorLog(
|
||||
`Jellyseerr returned a ${status} for url:\n` +
|
||||
response.config.url ,
|
||||
response.data
|
||||
`Jellyseerr returned a ${status} for url:\n${response.config.url}`,
|
||||
response.data,
|
||||
);
|
||||
return {
|
||||
isValid: false,
|
||||
@@ -240,7 +239,7 @@ export class JellyseerrApi {
|
||||
|
||||
async getRequest(id: number): Promise<MediaRequest> {
|
||||
return this.axios
|
||||
?.get<MediaRequest>(Endpoints.API_V1 + Endpoints.REQUEST + `/${id}`)
|
||||
?.get<MediaRequest>(`${Endpoints.API_V1 + Endpoints.REQUEST}/${id}`)
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
|
||||
@@ -261,7 +260,7 @@ export class JellyseerrApi {
|
||||
|
||||
async movieDetails(id: number) {
|
||||
return this.axios
|
||||
?.get<MovieDetails>(Endpoints.API_V1 + Endpoints.MOVIE + `/${id}`)
|
||||
?.get<MovieDetails>(`${Endpoints.API_V1 + Endpoints.MOVIE}/${id}`)
|
||||
.then((response) => {
|
||||
return response?.data;
|
||||
});
|
||||
@@ -269,7 +268,7 @@ export class JellyseerrApi {
|
||||
|
||||
async personDetails(id: number | string): Promise<PersonDetails> {
|
||||
return this.axios
|
||||
?.get<PersonDetails>(Endpoints.API_V1 + Endpoints.PERSON + `/${id}`)
|
||||
?.get<PersonDetails>(`${Endpoints.API_V1 + Endpoints.PERSON}/${id}`)
|
||||
.then((response) => {
|
||||
return response?.data;
|
||||
});
|
||||
@@ -278,10 +277,9 @@ export class JellyseerrApi {
|
||||
async personCombinedCredits(id: number | string): Promise<CombinedCredit> {
|
||||
return this.axios
|
||||
?.get<CombinedCredit>(
|
||||
Endpoints.API_V1 +
|
||||
Endpoints.PERSON +
|
||||
`/${id}` +
|
||||
Endpoints.COMBINED_CREDITS,
|
||||
`${
|
||||
Endpoints.API_V1 + Endpoints.PERSON
|
||||
}/${id}${Endpoints.COMBINED_CREDITS}`,
|
||||
)
|
||||
.then((response) => {
|
||||
return response?.data;
|
||||
@@ -332,13 +330,10 @@ export class JellyseerrApi {
|
||||
|
||||
imageProxy(path?: string, filter = "original", width = 1920, quality = 75) {
|
||||
return path
|
||||
? this.axios.defaults.baseURL +
|
||||
`/_next/image?` +
|
||||
new URLSearchParams(
|
||||
? `${this.axios.defaults.baseURL}/_next/image?${new URLSearchParams(
|
||||
`url=https://image.tmdb.org/t/p/${filter}/${path}&w=${width}&q=${quality}`,
|
||||
).toString()
|
||||
: this.axios?.defaults.baseURL +
|
||||
`/images/overseerr_poster_not_found_logo_top.png`;
|
||||
).toString()}`
|
||||
: `${this.axios?.defaults.baseURL}/images/overseerr_poster_not_found_logo_top.png`;
|
||||
}
|
||||
|
||||
async submitIssue(mediaId: number, issueType: IssueType, message: string) {
|
||||
@@ -361,7 +356,7 @@ export class JellyseerrApi {
|
||||
async service(type: "radarr" | "sonarr") {
|
||||
return this.axios
|
||||
?.get<ServiceCommonServer[]>(
|
||||
Endpoints.API_V1 + Endpoints.SERVICE + `/${type}`,
|
||||
`${Endpoints.API_V1 + Endpoints.SERVICE}/${type}`,
|
||||
)
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
@@ -369,7 +364,7 @@ export class JellyseerrApi {
|
||||
async serviceDetails(type: "radarr" | "sonarr", id: number) {
|
||||
return this.axios
|
||||
?.get<ServiceCommonServerWithDetails>(
|
||||
Endpoints.API_V1 + Endpoints.SERVICE + `/${type}` + `/${id}`,
|
||||
`${Endpoints.API_V1 + Endpoints.SERVICE}/${type}/${id}`,
|
||||
)
|
||||
.then(({ data }) => data);
|
||||
}
|
||||
@@ -388,9 +383,7 @@ export class JellyseerrApi {
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
writeErrorLog(
|
||||
"Jellyseerr response error\n" +
|
||||
`error: ${error.toString()}\n` +
|
||||
`url: ${error?.config?.url}`,
|
||||
`Jellyseerr response error\nerror: ${error.toString()}\nurl: ${error?.config?.url}`,
|
||||
error.response?.data,
|
||||
);
|
||||
if (error.status === 403) {
|
||||
@@ -407,7 +400,7 @@ export class JellyseerrApi {
|
||||
const headerName = this.axios.defaults.xsrfHeaderName!;
|
||||
const xsrfToken = cookies
|
||||
.find((c) => c.includes(headerName))
|
||||
?.split(headerName + "=")?.[1];
|
||||
?.split(`${headerName}=`)?.[1];
|
||||
if (xsrfToken) {
|
||||
config.headers[headerName] = xsrfToken;
|
||||
}
|
||||
@@ -479,7 +472,7 @@ export const useJellyseerr = () => {
|
||||
return (
|
||||
items &&
|
||||
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,
|
||||
) => {
|
||||
return isJellyseerrResult(item)
|
||||
? item.mediaType == MediaType.MOVIE
|
||||
? item.mediaType === MediaType.MOVIE
|
||||
? item?.title
|
||||
: item?.name
|
||||
: item?.mediaInfo.mediaType == MediaType.MOVIE
|
||||
: item?.mediaInfo.mediaType === MediaType.MOVIE
|
||||
? (item as MovieDetails)?.title
|
||||
: (item as TvDetails)?.name;
|
||||
};
|
||||
@@ -500,10 +493,10 @@ export const useJellyseerr = () => {
|
||||
) => {
|
||||
return new Date(
|
||||
(isJellyseerrResult(item)
|
||||
? item.mediaType == MediaType.MOVIE
|
||||
? item.mediaType === MediaType.MOVIE
|
||||
? item?.releaseDate
|
||||
: item?.firstAirDate
|
||||
: item?.mediaInfo.mediaType == MediaType.MOVIE
|
||||
: item?.mediaInfo.mediaType === MediaType.MOVIE
|
||||
? (item as MovieDetails)?.releaseDate
|
||||
: (item as TvDetails)?.firstAirDate) || "",
|
||||
)?.getFullYear?.();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ViewStyle } from "react-native";
|
||||
|
||||
export type PlaybackStatePayload = {
|
||||
nativeEvent: {
|
||||
target: number;
|
||||
@@ -59,7 +61,7 @@ export type ChapterInfo = {
|
||||
|
||||
export type VlcPlayerViewProps = {
|
||||
source: VlcPlayerSource;
|
||||
style?: Record<string, unknown>;
|
||||
style?: ViewStyle | ViewStyle[];
|
||||
progressUpdateInterval?: number;
|
||||
paused?: boolean;
|
||||
muted?: boolean;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { requireNativeViewManager } from "expo-modules-core";
|
||||
import * as React from "react";
|
||||
|
||||
import { VideoPlayer, useSettings } from "@/utils/atoms/settings";
|
||||
import { Platform } from "react-native";
|
||||
import { Platform, ViewStyle } from "react-native";
|
||||
import type {
|
||||
VlcPlayerSource,
|
||||
VlcPlayerViewProps,
|
||||
@@ -22,7 +22,7 @@ const NativeView = React.forwardRef<NativeViewRef, VlcPlayerViewProps>(
|
||||
const [settings] = useSettings();
|
||||
|
||||
if (Platform.OS === "ios" || Platform.isTVOS) {
|
||||
if (settings.defaultPlayer == VideoPlayer.VLC_3) {
|
||||
if (settings.defaultPlayer === VideoPlayer.VLC_3) {
|
||||
console.log("[Apple] Using Vlc Player 3");
|
||||
return <VLC3ViewManager {...props} ref={ref} />;
|
||||
}
|
||||
@@ -118,7 +118,9 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
||||
} = props;
|
||||
|
||||
const processedSource: VlcPlayerSource =
|
||||
typeof source === "string" ? { uri: source } : source;
|
||||
typeof source === "string"
|
||||
? ({ uri: source } as unknown as VlcPlayerSource)
|
||||
: source;
|
||||
|
||||
if (processedSource.startPosition !== undefined) {
|
||||
processedSource.startPosition = Math.floor(processedSource.startPosition);
|
||||
@@ -129,7 +131,7 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
||||
{...otherProps}
|
||||
ref={nativeRef}
|
||||
source={processedSource}
|
||||
style={[{ width: "100%", height: "100%" }, style]}
|
||||
style={[{ width: "100%", height: "100%" }, style as ViewStyle]}
|
||||
progressUpdateInterval={progressUpdateInterval}
|
||||
paused={paused}
|
||||
muted={muted}
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"prebuild": "EXPO_TV=0 bun run clean",
|
||||
"prebuild:tv": "EXPO_TV=1 bun run clean",
|
||||
"prepare": "husky",
|
||||
"lint": "biome format --write ."
|
||||
"check": "biome check .",
|
||||
"lint": "biome check --write --unsafe"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bottom-tabs/react-navigation": "0.8.6",
|
||||
@@ -134,6 +135,7 @@
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": ["biome check --no-errors-on-unmatched --files-ignore-unknown=true"]
|
||||
"*.{js,jsx,ts,tsx}": ["biome check --write --unsafe"],
|
||||
"*.{json,md}": ["biome format --write"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { readFileSync, writeFileSync } = require("fs");
|
||||
const { join } = require("path");
|
||||
const { readFileSync, writeFileSync } = require("node:fs");
|
||||
const { join } = require("node:path");
|
||||
const { withDangerousMod } = require("@expo/config-plugins");
|
||||
|
||||
const withChangeNativeAndroidTextToWhite = (expoConfig) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { AndroidConfig, withAndroidManifest } = require("@expo/config-plugins");
|
||||
const { Paths } = require("@expo/config-plugins/build/android");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const fsPromises = fs.promises;
|
||||
|
||||
const { getMainApplicationOrThrow } = AndroidConfig.Manifest;
|
||||
|
||||
@@ -63,7 +63,7 @@ function useDownloadProvider() {
|
||||
function saveDownloadedItemInfo(item: BaseItemDto, size = 0) {}
|
||||
|
||||
function getDownloadedItemSize(itemId: string): number {
|
||||
const size = storage.getString("downloadedItemSize-" + itemId);
|
||||
const size = storage.getString(`downloadedItemSize-${itemId}`);
|
||||
return size ? Number.parseInt(size) : 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
if (!api || !deviceId) return;
|
||||
try {
|
||||
const response = await api.axiosInstance.post(
|
||||
api.basePath + "/QuickConnect/Initiate",
|
||||
`${api.basePath}/QuickConnect/Initiate`,
|
||||
null,
|
||||
{
|
||||
headers,
|
||||
@@ -111,9 +111,8 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
setSecret(response?.data?.Secret);
|
||||
setIsPolling(true);
|
||||
return response.data?.Code;
|
||||
} else {
|
||||
throw new Error("Failed to initiate quick connect");
|
||||
}
|
||||
throw new Error("Failed to initiate quick connect");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
@@ -133,7 +132,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
setIsPolling(false);
|
||||
|
||||
const authResponse = await api.axiosInstance.post(
|
||||
api.basePath + "/Users/AuthenticateWithQuickConnect",
|
||||
`${api.basePath}/Users/AuthenticateWithQuickConnect`,
|
||||
{
|
||||
secret,
|
||||
},
|
||||
@@ -156,11 +155,10 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
setIsPolling(false);
|
||||
setSecret(null);
|
||||
throw new Error("The code has expired. Please try again.");
|
||||
} else {
|
||||
}
|
||||
console.error("Error polling Quick Connect:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}, [api, secret, headers]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -290,7 +288,9 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
api
|
||||
?.delete(`/Streamyfin/device/${deviceId}`)
|
||||
.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");
|
||||
setUser(null);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const process = require("process");
|
||||
const { execSync } = require("child_process");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const process = require("node:process");
|
||||
const { execSync } = require("node:child_process");
|
||||
|
||||
const root = process.cwd();
|
||||
// const tvosPath = path.join(root, 'iostv');
|
||||
|
||||
@@ -15,7 +15,6 @@ function orientationToOrientationLock(
|
||||
return OrientationLock.LANDSCAPE_LEFT;
|
||||
case Orientation.LANDSCAPE_RIGHT:
|
||||
return OrientationLock.LANDSCAPE_RIGHT;
|
||||
case Orientation.UNKNOWN:
|
||||
default:
|
||||
return OrientationLock.DEFAULT;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ export const useJellyseerrCanRequest = (
|
||||
const canNotRequest =
|
||||
item?.mediaInfo?.requests?.some(
|
||||
(r: MediaRequest) =>
|
||||
r.status == MediaRequestStatus.PENDING ||
|
||||
r.status == MediaRequestStatus.APPROVED,
|
||||
r.status === MediaRequestStatus.PENDING ||
|
||||
r.status === MediaRequestStatus.APPROVED,
|
||||
) ||
|
||||
item.mediaInfo?.status === MediaStatus.AVAILABLE ||
|
||||
item.mediaInfo?.status === MediaStatus.BLACKLISTED ||
|
||||
|
||||
@@ -41,7 +41,7 @@ const calculateContrastRatio = (rgb1: number[], rgb2: number[]): number => {
|
||||
const calculateRelativeLuminance = (rgb: number[]): number => {
|
||||
const [r, g, b] = rgb.map((c) => {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -11,9 +11,8 @@ export function convertBitsToMegabitsOrGigabits(bits?: number | null): string {
|
||||
const megabits = bits / 1000000;
|
||||
|
||||
if (megabits < 1000) {
|
||||
return Math.round(megabits) + "MB";
|
||||
} else {
|
||||
const gigabits = megabits / 1000;
|
||||
return gigabits.toFixed(1) + "GB";
|
||||
return `${Math.round(megabits)}MB`;
|
||||
}
|
||||
const gigabits = megabits / 1000;
|
||||
return `${gigabits.toFixed(1)}GB`;
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ export const formatBitrate = (bitrate?: number | null) => {
|
||||
const i = Number.parseInt(
|
||||
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]}`;
|
||||
};
|
||||
|
||||
@@ -41,8 +41,6 @@ export const colletionTypeToItemType = (
|
||||
return BaseItemKind.Photo;
|
||||
case CollectionType.Trailers:
|
||||
return BaseItemKind.Trailer;
|
||||
case CollectionType.Playlists:
|
||||
return BaseItemKind.Playlist;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -54,7 +54,7 @@ export const getItemImage = ({
|
||||
};
|
||||
break;
|
||||
case "Primary":
|
||||
tag = item.ImageTags?.["Primary"];
|
||||
tag = item.ImageTags?.Primary;
|
||||
if (!tag) break;
|
||||
blurhash = item.ImageBlurHashes?.Primary?.[tag];
|
||||
|
||||
@@ -64,7 +64,7 @@ export const getItemImage = ({
|
||||
};
|
||||
break;
|
||||
case "Thumb":
|
||||
tag = item.ImageTags?.["Thumb"];
|
||||
tag = item.ImageTags?.Thumb;
|
||||
if (!tag) break;
|
||||
blurhash = item.ImageBlurHashes?.Thumb?.[tag];
|
||||
|
||||
@@ -74,7 +74,7 @@ export const getItemImage = ({
|
||||
};
|
||||
break;
|
||||
default:
|
||||
tag = item.ImageTags?.["Primary"];
|
||||
tag = item.ImageTags?.Primary;
|
||||
src = {
|
||||
uri: `${api.basePath}/Items/${item.Id}/Images/Primary?quality=${quality}&tag=${tag}&width=${width}`,
|
||||
};
|
||||
|
||||
@@ -24,12 +24,12 @@ export async function parseM3U8ForSubtitles(
|
||||
const attributes = parseAttributes(line);
|
||||
const track: SubtitleTrack = {
|
||||
index: index++,
|
||||
name: attributes["NAME"] || "",
|
||||
uri: attributes["URI"] || "",
|
||||
language: attributes["LANGUAGE"] || "",
|
||||
default: attributes["DEFAULT"] === "YES",
|
||||
forced: attributes["FORCED"] === "YES",
|
||||
autoSelect: attributes["AUTOSELECT"] === "YES",
|
||||
name: attributes.NAME || "",
|
||||
uri: attributes.URI || "",
|
||||
language: attributes.LANGUAGE || "",
|
||||
default: attributes.DEFAULT === "YES",
|
||||
forced: attributes.FORCED === "YES",
|
||||
autoSelect: attributes.AUTOSELECT === "YES",
|
||||
};
|
||||
subtitleTracks.push(track);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ export const getBackdropUrl = ({
|
||||
return `${api.basePath}/Items/${
|
||||
item.Id
|
||||
}/Images/Backdrop/0?${params.toString()}`;
|
||||
} else {
|
||||
return getPrimaryImageUrl({ api, item, quality, width });
|
||||
}
|
||||
return getPrimaryImageUrl({ api, item, quality, width });
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export const getLogoImageUrlById = ({
|
||||
return `${api.basePath}/Items/${parentId}/Images/Logo?${params.toString()}`;
|
||||
}
|
||||
|
||||
const imageTag = item.ImageTags?.["Logo"];
|
||||
const imageTag = item.ImageTags?.Logo;
|
||||
|
||||
if (!imageTag) return null;
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export const getPrimaryImageUrl = ({
|
||||
return `${api?.basePath}/Items/${item?.Id}/Images/Primary`;
|
||||
}
|
||||
|
||||
const primaryTag = item.ImageTags?.["Primary"];
|
||||
const primaryTag = item.ImageTags?.Primary;
|
||||
const backdropTag = item.BackdropImageTags?.[0];
|
||||
const parentBackdropTag = item.ParentBackdropImageTags?.[0];
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user