mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-02 12:08:37 +01:00
Merge branch 'master' into feat/i18n
This commit is contained in:
@@ -1,40 +1,62 @@
|
||||
import React, {useCallback, useRef, useState} from "react";
|
||||
import {useLocalSearchParams} from "expo-router";
|
||||
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
||||
import {Text} from "@/components/common/Text";
|
||||
import {ParallaxScrollView} from "@/components/ParallaxPage";
|
||||
import {Image} from "expo-image";
|
||||
import {TouchableOpacity, View} from "react-native";
|
||||
import {Ionicons} from "@expo/vector-icons";
|
||||
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
||||
import {OverviewText} from "@/components/OverviewText";
|
||||
import {GenreTags} from "@/components/GenreTags";
|
||||
import {MediaType} from "@/utils/jellyseerr/server/constants/media";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {useJellyseerr} from "@/hooks/useJellyseerr";
|
||||
import {Button} from "@/components/Button";
|
||||
import {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView} from "@gorhom/bottom-sheet";
|
||||
import {IssueType, IssueTypeName} from "@/utils/jellyseerr/server/constants/issue";
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { ParallaxScrollView } from "@/components/ParallaxPage";
|
||||
import { Image } from "expo-image";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { OverviewText } from "@/components/OverviewText";
|
||||
import { GenreTags } from "@/components/GenreTags";
|
||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import { Button } from "@/components/Button";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetBackdropProps,
|
||||
BottomSheetModal,
|
||||
BottomSheetView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import {
|
||||
IssueType,
|
||||
IssueTypeName,
|
||||
} from "@/utils/jellyseerr/server/constants/issue";
|
||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||
import {Input} from "@/components/common/Input";
|
||||
import {TvDetails} from "@/utils/jellyseerr/server/models/Tv";
|
||||
import { Input } from "@/components/common/Input";
|
||||
import { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
|
||||
import JellyseerrSeasons from "@/components/series/JellyseerrSeasons";
|
||||
import {JellyserrRatings} from "@/components/Ratings";
|
||||
import { JellyserrRatings } from "@/components/Ratings";
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const params = useLocalSearchParams();
|
||||
const {mediaTitle, releaseYear, canRequest: canRequestString, posterSrc, ...result} =
|
||||
params as unknown as {mediaTitle: string, releaseYear: number, canRequest: string, posterSrc: string} & Partial<MovieResult | TvResult>;
|
||||
const {
|
||||
mediaTitle,
|
||||
releaseYear,
|
||||
canRequest: canRequestString,
|
||||
posterSrc,
|
||||
...result
|
||||
} = params as unknown as {
|
||||
mediaTitle: string;
|
||||
releaseYear: number;
|
||||
canRequest: string;
|
||||
posterSrc: string;
|
||||
} & Partial<MovieResult | TvResult>;
|
||||
|
||||
const canRequest = canRequestString === "true";
|
||||
const {jellyseerrApi, requestMedia} = useJellyseerr();
|
||||
const { jellyseerrApi, requestMedia } = useJellyseerr();
|
||||
|
||||
const [issueType, setIssueType] = useState<IssueType>();
|
||||
const [issueMessage, setIssueMessage] = useState<string>();
|
||||
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
||||
|
||||
const {data: details, isLoading} = useQuery({
|
||||
const {
|
||||
data: details,
|
||||
isFetching,
|
||||
isLoading,
|
||||
} = useQuery({
|
||||
enabled: !!jellyseerrApi && !!result && !!result.id,
|
||||
queryKey: ["jellyseerr", "detail", result.mediaType, result.id],
|
||||
staleTime: 0,
|
||||
@@ -45,8 +67,8 @@ const Page: React.FC = () => {
|
||||
queryFn: async () => {
|
||||
return result.mediaType === MediaType.MOVIE
|
||||
? jellyseerrApi?.movieDetails(result.id!!)
|
||||
: jellyseerrApi?.tvDetails(result.id!!)
|
||||
}
|
||||
: jellyseerrApi?.tvDetails(result.id!!);
|
||||
},
|
||||
});
|
||||
|
||||
const renderBackdrop = useCallback(
|
||||
@@ -62,23 +84,30 @@ const Page: React.FC = () => {
|
||||
|
||||
const submitIssue = useCallback(() => {
|
||||
if (result.id && issueType && issueMessage && details) {
|
||||
jellyseerrApi?.submitIssue(details.mediaInfo.id, Number(issueType), issueMessage)
|
||||
jellyseerrApi
|
||||
?.submitIssue(details.mediaInfo.id, Number(issueType), issueMessage)
|
||||
.then(() => {
|
||||
setIssueType(undefined)
|
||||
setIssueMessage(undefined)
|
||||
bottomSheetModalRef?.current?.close()
|
||||
})
|
||||
setIssueType(undefined);
|
||||
setIssueMessage(undefined);
|
||||
bottomSheetModalRef?.current?.close();
|
||||
});
|
||||
}
|
||||
}, [jellyseerrApi, details, result, issueType, issueMessage])
|
||||
}, [jellyseerrApi, details, result, issueType, issueMessage]);
|
||||
|
||||
const request = useCallback(() => requestMedia(mediaTitle, {
|
||||
mediaId: Number(result.id!!),
|
||||
mediaType: result.mediaType!!,
|
||||
tvdbId: details?.externalIds?.tvdbId,
|
||||
seasons: (details as TvDetails)?.seasons.filter(s => s.seasonNumber !== 0).map(s => s.seasonNumber)
|
||||
}), [details, result, requestMedia]);
|
||||
const request = useCallback(
|
||||
() =>
|
||||
requestMedia(mediaTitle, {
|
||||
mediaId: Number(result.id!!),
|
||||
mediaType: result.mediaType!!,
|
||||
tvdbId: details?.externalIds?.tvdbId,
|
||||
seasons: (details as TvDetails)?.seasons
|
||||
?.filter?.((s) => s.seasonNumber !== 0)
|
||||
?.map?.((s) => s.seasonNumber),
|
||||
}),
|
||||
[details, result, requestMedia]
|
||||
);
|
||||
|
||||
return (
|
||||
return (
|
||||
<View
|
||||
className="flex-1 relative"
|
||||
style={{
|
||||
@@ -115,7 +144,7 @@ const Page: React.FC = () => {
|
||||
name="image-outline"
|
||||
size={24}
|
||||
color="white"
|
||||
style={{opacity: 0.4}}
|
||||
style={{ opacity: 0.4 }}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
@@ -123,12 +152,18 @@ const Page: React.FC = () => {
|
||||
}
|
||||
>
|
||||
<View className="flex flex-col">
|
||||
<View className="p-4 space-y-4">
|
||||
<>
|
||||
<View className="space-y-4">
|
||||
<View className="px-4">
|
||||
<View className="flex flex-row justify-between w-full">
|
||||
<View className="flex flex-col w-56">
|
||||
<JellyserrRatings result={(result as MovieResult | TvResult)}/>
|
||||
<Text uiTextView selectable className="font-bold text-2xl mb-1">{mediaTitle}</Text>
|
||||
<JellyserrRatings result={result as MovieResult | TvResult} />
|
||||
<Text
|
||||
uiTextView
|
||||
selectable
|
||||
className="font-bold text-2xl mb-1"
|
||||
>
|
||||
{mediaTitle}
|
||||
</Text>
|
||||
<Text className="opacity-50">{releaseYear}</Text>
|
||||
</View>
|
||||
<Image
|
||||
@@ -140,34 +175,39 @@ const Page: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
<GenreTags genres={details?.genres?.map(g => g.name) || []} />
|
||||
{canRequest ?
|
||||
<Button color="purple" onPress={request}>Request</Button>
|
||||
:
|
||||
<Button
|
||||
className="bg-yellow-500/50 border-yellow-400 ring-yellow-400 text-yellow-100"
|
||||
color="transparent"
|
||||
onPress={() => bottomSheetModalRef?.current?.present()}
|
||||
iconLeft={
|
||||
<Ionicons name="warning-outline" size={24} color="white"/>
|
||||
}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
}}>
|
||||
Report issue
|
||||
</Button>
|
||||
}
|
||||
<OverviewText text={result.overview} className="mb-4" />
|
||||
<View className="mb-4">
|
||||
<GenreTags genres={details?.genres?.map((g) => g.name) || []} />
|
||||
</View>
|
||||
{canRequest ? (
|
||||
<Button color="purple" onPress={request}>
|
||||
Request
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="bg-yellow-500/50 border-yellow-400 ring-yellow-400 text-yellow-100"
|
||||
color="transparent"
|
||||
onPress={() => bottomSheetModalRef?.current?.present()}
|
||||
iconLeft={
|
||||
<Ionicons name="warning-outline" size={24} color="white" />
|
||||
}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
}}
|
||||
>
|
||||
Report issue
|
||||
</Button>
|
||||
)}
|
||||
<OverviewText text={result.overview} className="mt-4" />
|
||||
</View>
|
||||
|
||||
{result.mediaType === MediaType.TV &&
|
||||
<JellyseerrSeasons
|
||||
isLoading={isLoading}
|
||||
result={result as TvResult}
|
||||
details={details as TvDetails}
|
||||
/>
|
||||
}
|
||||
{result.mediaType === MediaType.TV && (
|
||||
<JellyseerrSeasons
|
||||
isLoading={isLoading || isFetching}
|
||||
result={result as TvResult}
|
||||
details={details as TvDetails}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ParallaxScrollView>
|
||||
@@ -185,17 +225,23 @@ const Page: React.FC = () => {
|
||||
<BottomSheetView>
|
||||
<View className="flex flex-col space-y-4 px-4 pb-8 pt-2">
|
||||
<View>
|
||||
<Text className="font-bold text-2xl text-neutral-100">Whats wrong?</Text>
|
||||
<Text className="font-bold text-2xl text-neutral-100">
|
||||
Whats wrong?
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex flex-col space-y-2 items-start">
|
||||
<View className="flex flex-col">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<View className="flex flex-col">
|
||||
<Text className="opacity-50 mb-1 text-xs">Issue Type</Text>
|
||||
<Text className="opacity-50 mb-1 text-xs">
|
||||
Issue Type
|
||||
</Text>
|
||||
<TouchableOpacity className="bg-neutral-900 h-10 rounded-xl border-neutral-800 border px-3 py-2 flex flex-row items-center justify-between">
|
||||
<Text style={{}} className="" numberOfLines={1}>
|
||||
{issueType ? IssueTypeName[issueType] : 'Select an issue' }
|
||||
{issueType
|
||||
? IssueTypeName[issueType]
|
||||
: "Select an issue"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -210,14 +256,20 @@ const Page: React.FC = () => {
|
||||
sideOffset={0}
|
||||
>
|
||||
<DropdownMenu.Label>Types</DropdownMenu.Label>
|
||||
{Object.entries(IssueTypeName).reverse().map(([key, value], idx) => (
|
||||
<DropdownMenu.Item
|
||||
key={value}
|
||||
onSelect={() => setIssueType(key as unknown as IssueType)}
|
||||
>
|
||||
<DropdownMenu.ItemTitle>{value}</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
{Object.entries(IssueTypeName)
|
||||
.reverse()
|
||||
.map(([key, value], idx) => (
|
||||
<DropdownMenu.Item
|
||||
key={value}
|
||||
onSelect={() =>
|
||||
setIssueType(key as unknown as IssueType)
|
||||
}
|
||||
>
|
||||
<DropdownMenu.ItemTitle>
|
||||
{value}
|
||||
</DropdownMenu.ItemTitle>
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</View>
|
||||
@@ -234,11 +286,7 @@ const Page: React.FC = () => {
|
||||
onChangeText={setIssueMessage}
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
className="mt-auto"
|
||||
onPress={submitIssue}
|
||||
color="purple"
|
||||
>
|
||||
<Button className="mt-auto" onPress={submitIssue} color="purple">
|
||||
Submit
|
||||
</Button>
|
||||
</View>
|
||||
@@ -246,6 +294,6 @@ const Page: React.FC = () => {
|
||||
</BottomSheetModal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Page;
|
||||
export default Page;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { DownloadItems } from "@/components/DownloadItem";
|
||||
import { ParallaxScrollView } from "@/components/ParallaxPage";
|
||||
import { Ratings } from "@/components/Ratings";
|
||||
import { NextUp } from "@/components/series/NextUp";
|
||||
import { SeasonPicker } from "@/components/series/SeasonPicker";
|
||||
import { SeriesActions } from "@/components/series/SeriesActions";
|
||||
import { SeriesHeader } from "@/components/series/SeriesHeader";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
|
||||
@@ -70,6 +73,7 @@ const page: React.FC = () => {
|
||||
});
|
||||
return res?.data.Items || [];
|
||||
},
|
||||
staleTime: 60,
|
||||
enabled: !!api && !!user?.Id && !!item?.Id,
|
||||
});
|
||||
|
||||
@@ -133,10 +137,7 @@ const page: React.FC = () => {
|
||||
}
|
||||
>
|
||||
<View className="flex flex-col pt-4">
|
||||
<View className="px-4 py-4">
|
||||
<Text className="text-3xl font-bold">{item?.Name}</Text>
|
||||
<Text className="">{item?.Overview}</Text>
|
||||
</View>
|
||||
<SeriesHeader item={item} />
|
||||
<View className="mb-4">
|
||||
<NextUp seriesId={seriesId} />
|
||||
</View>
|
||||
|
||||
@@ -144,8 +144,6 @@ const Page = () => {
|
||||
}): Promise<BaseItemDtoQueryResult | null> => {
|
||||
if (!api || !library) return null;
|
||||
|
||||
console.log("[libraryId] ~", library);
|
||||
|
||||
let itemType: BaseItemKind | undefined;
|
||||
|
||||
// This fix makes sure to only return 1 type of items, if defined.
|
||||
|
||||
@@ -30,16 +30,16 @@ import React, {
|
||||
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import {useJellyseerr} from "@/hooks/useJellyseerr";
|
||||
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
||||
import {MediaType} from "@/utils/jellyseerr/server/constants/media";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
|
||||
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
|
||||
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
||||
import {Tag} from "@/components/GenreTags";
|
||||
import { Tag } from "@/components/GenreTags";
|
||||
import DiscoverSlide from "@/components/jellyseerr/DiscoverSlide";
|
||||
import {sortBy} from "lodash";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {sortBy} from "lodash";
|
||||
|
||||
type SearchType = 'Library' | 'Discover';
|
||||
type SearchType = "Library" | "Discover";
|
||||
|
||||
const exampleSearches = [
|
||||
"Lord of the rings",
|
||||
@@ -155,29 +155,41 @@ export default function search() {
|
||||
const response = await jellyseerrApi?.search({
|
||||
query: new URLSearchParams(debouncedSearch).toString(),
|
||||
page: 1, // todo: maybe rework page & page-size if first results are not enough...
|
||||
language: 'en'
|
||||
})
|
||||
language: "en",
|
||||
});
|
||||
|
||||
return response?.results;
|
||||
},
|
||||
enabled: !!jellyseerrApi && searchType === "Discover" && debouncedSearch.length > 0,
|
||||
enabled:
|
||||
!!jellyseerrApi &&
|
||||
searchType === "Discover" &&
|
||||
debouncedSearch.length > 0,
|
||||
});
|
||||
|
||||
const { data: jellyseerrDiscoverSettings, isFetching: j2 } = useQuery({
|
||||
queryKey: ["search", "jellyseerrDiscoverSettings", debouncedSearch],
|
||||
queryFn: async () => jellyseerrApi?.discoverSettings(),
|
||||
enabled: !!jellyseerrApi && searchType === "Discover" && debouncedSearch.length == 0,
|
||||
enabled:
|
||||
!!jellyseerrApi &&
|
||||
searchType === "Discover" &&
|
||||
debouncedSearch.length == 0,
|
||||
});
|
||||
|
||||
const jellyseerrMovieResults: MovieResult[] | undefined = useMemo(() =>
|
||||
jellyseerrResults?.filter(r => r.mediaType === MediaType.MOVIE) as MovieResult[],
|
||||
const jellyseerrMovieResults: MovieResult[] | undefined = useMemo(
|
||||
() =>
|
||||
jellyseerrResults?.filter(
|
||||
(r) => r.mediaType === MediaType.MOVIE
|
||||
) as MovieResult[],
|
||||
[jellyseerrResults]
|
||||
)
|
||||
);
|
||||
|
||||
const jellyseerrTvResults: TvResult[] | undefined = useMemo(() =>
|
||||
jellyseerrResults?.filter(r => r.mediaType === MediaType.TV) as TvResult[],
|
||||
const jellyseerrTvResults: TvResult[] | undefined = useMemo(
|
||||
() =>
|
||||
jellyseerrResults?.filter(
|
||||
(r) => r.mediaType === MediaType.TV
|
||||
) as TvResult[],
|
||||
[jellyseerrResults]
|
||||
)
|
||||
);
|
||||
|
||||
const { data: series, isFetching: l2 } = useQuery({
|
||||
queryKey: ["search", "series", debouncedSearch],
|
||||
@@ -262,7 +274,17 @@ export default function search() {
|
||||
jellyseerrMovieResults?.length ||
|
||||
jellyseerrTvResults?.length
|
||||
);
|
||||
}, [artists, episodes, albums, songs, movies, series, collections, actors, jellyseerrResults]);
|
||||
}, [
|
||||
artists,
|
||||
episodes,
|
||||
albums,
|
||||
songs,
|
||||
movies,
|
||||
series,
|
||||
collections,
|
||||
actors,
|
||||
jellyseerrResults,
|
||||
]);
|
||||
|
||||
const loading = useMemo(() => {
|
||||
return l1 || l2 || l3 || l4 || l5 || l6 || l7 || l8 || j1 || j2;
|
||||
@@ -292,14 +314,24 @@ export default function search() {
|
||||
</View>
|
||||
)}
|
||||
{jellyseerrApi && (
|
||||
<View className="flex flex-row flex-wrap space-x-2 px-4">
|
||||
<TouchableOpacity onPress={() => setSearchType('Library')}>
|
||||
<Tag text="Library" textClass="p-1"
|
||||
className={searchType === "Library" ? "bg-neutral-600" : undefined}/>
|
||||
<View className="flex flex-row flex-wrap space-x-2 px-4 mb-2">
|
||||
<TouchableOpacity onPress={() => setSearchType("Library")}>
|
||||
<Tag
|
||||
text="Library"
|
||||
textClass="p-1"
|
||||
className={
|
||||
searchType === "Library" ? "bg-neutral-600" : undefined
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setSearchType('Discover')}>
|
||||
<Tag text="Discover" textClass="p-1"
|
||||
className={searchType === "Discover" ? "bg-neutral-600" : undefined}/>
|
||||
<TouchableOpacity onPress={() => setSearchType("Discover")}>
|
||||
<Tag
|
||||
text="Discover"
|
||||
textClass="p-1"
|
||||
className={
|
||||
searchType === "Discover" ? "bg-neutral-600" : undefined
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
@@ -321,7 +353,7 @@ export default function search() {
|
||||
className="flex flex-col w-28 mr-2"
|
||||
item={item}
|
||||
>
|
||||
<MoviePoster item={item} key={item.Id}/>
|
||||
<MoviePoster item={item} key={item.Id} />
|
||||
<Text numberOfLines={2} className="mt-2">
|
||||
{item.Name}
|
||||
</Text>
|
||||
@@ -340,7 +372,7 @@ export default function search() {
|
||||
item={item}
|
||||
className="flex flex-col w-28 mr-2"
|
||||
>
|
||||
<SeriesPoster item={item} key={item.Id}/>
|
||||
<SeriesPoster item={item} key={item.Id} />
|
||||
<Text numberOfLines={2} className="mt-2">
|
||||
{item.Name}
|
||||
</Text>
|
||||
@@ -359,8 +391,8 @@ export default function search() {
|
||||
key={item.Id}
|
||||
className="flex flex-col w-44 mr-2"
|
||||
>
|
||||
<ContinueWatchingPoster item={item}/>
|
||||
<ItemCardText item={item}/>
|
||||
<ContinueWatchingPoster item={item} />
|
||||
<ItemCardText item={item} />
|
||||
</TouchableItemRouter>
|
||||
)}
|
||||
/>
|
||||
@@ -373,7 +405,7 @@ export default function search() {
|
||||
item={item}
|
||||
className="flex flex-col w-28 mr-2"
|
||||
>
|
||||
<MoviePoster item={item} key={item.Id}/>
|
||||
<MoviePoster item={item} key={item.Id} />
|
||||
<Text numberOfLines={2} className="mt-2">
|
||||
{item.Name}
|
||||
</Text>
|
||||
@@ -389,8 +421,8 @@ export default function search() {
|
||||
key={item.Id}
|
||||
className="flex flex-col w-28 mr-2"
|
||||
>
|
||||
<MoviePoster item={item}/>
|
||||
<ItemCardText item={item}/>
|
||||
<MoviePoster item={item} />
|
||||
<ItemCardText item={item} />
|
||||
</TouchableItemRouter>
|
||||
)}
|
||||
/>
|
||||
@@ -403,8 +435,8 @@ export default function search() {
|
||||
key={item.Id}
|
||||
className="flex flex-col w-28 mr-2"
|
||||
>
|
||||
<AlbumCover id={item.Id}/>
|
||||
<ItemCardText item={item}/>
|
||||
<AlbumCover id={item.Id} />
|
||||
<ItemCardText item={item} />
|
||||
</TouchableItemRouter>
|
||||
)}
|
||||
/>
|
||||
@@ -417,8 +449,8 @@ export default function search() {
|
||||
key={item.Id}
|
||||
className="flex flex-col w-28 mr-2"
|
||||
>
|
||||
<AlbumCover id={item.Id}/>
|
||||
<ItemCardText item={item}/>
|
||||
<AlbumCover id={item.Id} />
|
||||
<ItemCardText item={item} />
|
||||
</TouchableItemRouter>
|
||||
)}
|
||||
/>
|
||||
@@ -431,8 +463,8 @@ export default function search() {
|
||||
key={item.Id}
|
||||
className="flex flex-col w-28 mr-2"
|
||||
>
|
||||
<AlbumCover id={item.AlbumId}/>
|
||||
<ItemCardText item={item}/>
|
||||
<AlbumCover id={item.AlbumId} />
|
||||
<ItemCardText item={item} />
|
||||
</TouchableItemRouter>
|
||||
)}
|
||||
/>
|
||||
@@ -444,14 +476,14 @@ export default function search() {
|
||||
header="Request Movies"
|
||||
items={jellyseerrMovieResults}
|
||||
renderItem={(item: MovieResult) => (
|
||||
<JellyseerrPoster item={item} key={item.id}/>
|
||||
<JellyseerrPoster item={item} key={item.id} />
|
||||
)}
|
||||
/>
|
||||
<SearchItemWrapper
|
||||
header="Request Series"
|
||||
items={jellyseerrTvResults}
|
||||
renderItem={(item: TvResult) => (
|
||||
<JellyseerrPoster item={item} key={item.id}/>
|
||||
<JellyseerrPoster item={item} key={item.id} />
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
@@ -470,7 +502,7 @@ export default function search() {
|
||||
"{debouncedSearch}"
|
||||
</Text>
|
||||
</View>
|
||||
) : debouncedSearch.length === 0 && searchType === 'Library' ? (
|
||||
) : debouncedSearch.length === 0 && searchType === "Library" ? (
|
||||
<View className="mt-4 flex flex-col items-center space-y-2">
|
||||
{exampleSearches.map((e) => (
|
||||
<TouchableOpacity
|
||||
@@ -482,11 +514,14 @@ export default function search() {
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
) : debouncedSearch.length === 0 && searchType === 'Discover' ? (
|
||||
<View className="mt-4 flex flex-col space-y-2 px-2">
|
||||
{sortBy?.(jellyseerrDiscoverSettings?.filter(s => s.enabled), 'order')
|
||||
.map((slide) => <DiscoverSlide key={slide.id} slide={slide}/>)
|
||||
}
|
||||
) : debouncedSearch.length === 0 && searchType === "Discover" ? (
|
||||
<View className="flex flex-col px-4">
|
||||
{sortBy?.(
|
||||
jellyseerrDiscoverSettings?.filter((s) => s.enabled),
|
||||
"order"
|
||||
).map((slide) => (
|
||||
<DiscoverSlide key={slide.id} slide={slide} />
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -502,7 +537,12 @@ type Props<T> = {
|
||||
header?: string;
|
||||
};
|
||||
|
||||
const SearchItemWrapper = <T extends unknown> ({ ids, items, renderItem, header }: PropsWithChildren<Props<T>>) => {
|
||||
const SearchItemWrapper = <T extends unknown>({
|
||||
ids,
|
||||
items,
|
||||
renderItem,
|
||||
header,
|
||||
}: PropsWithChildren<Props<T>>) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
@@ -542,14 +582,11 @@ const SearchItemWrapper = <T extends unknown> ({ ids, items, renderItem, header
|
||||
className="px-4 mb-2"
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
{
|
||||
data && data?.length > 0
|
||||
? data.map((item) => renderItem(item))
|
||||
:
|
||||
items && items?.length > 0
|
||||
? items.map(i => renderItem(i))
|
||||
: undefined
|
||||
}
|
||||
{data && data?.length > 0
|
||||
? data.map((item) => renderItem(item))
|
||||
: items && items?.length > 0
|
||||
? items.map((i) => renderItem(i))
|
||||
: undefined}
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -282,13 +282,6 @@ export default function page() {
|
||||
|
||||
if (!item?.Id || !stream) return;
|
||||
|
||||
console.log(
|
||||
"onProgress ~",
|
||||
currentTimeInTicks,
|
||||
isPlaying,
|
||||
`AUDIO index: ${audioIndex} SUB index" ${subtitleIndex}`
|
||||
);
|
||||
|
||||
await getPlaystateApi(api!).onPlaybackProgress({
|
||||
itemId: item.Id,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
|
||||
53
app/(auth)/trailer/page.tsx
Normal file
53
app/(auth)/trailer/page.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useGlobalSearchParams, useNavigation } from "expo-router";
|
||||
import { useState, useCallback, useEffect, useMemo } from "react";
|
||||
import { Button, Dimensions } from "react-native";
|
||||
import { Alert, View } from "react-native";
|
||||
import YoutubePlayer, { PLAYER_STATES } from "react-native-youtube-iframe";
|
||||
|
||||
export default function page() {
|
||||
const searchParams = useGlobalSearchParams();
|
||||
const navigation = useNavigation();
|
||||
console.log(searchParams);
|
||||
|
||||
const { url } = searchParams as { url: string };
|
||||
|
||||
const videoId = useMemo(() => {
|
||||
return url.split("v=")[1];
|
||||
}, [url]);
|
||||
|
||||
const [playing, setPlaying] = useState(false);
|
||||
|
||||
const onStateChange = useCallback((state: PLAYER_STATES) => {
|
||||
if (state === "ended") {
|
||||
setPlaying(false);
|
||||
Alert.alert("video has finished playing!");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const togglePlaying = useCallback(() => {
|
||||
setPlaying((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
|
||||
togglePlaying();
|
||||
}, []);
|
||||
|
||||
const screenWidth = Dimensions.get("screen").width;
|
||||
const screenHeight = Dimensions.get("screen").height;
|
||||
|
||||
return (
|
||||
<View className="flex flex-col bg-black items-center justify-center h-full">
|
||||
<YoutubePlayer
|
||||
height={300}
|
||||
play={playing}
|
||||
videoId={videoId}
|
||||
onChangeState={onStateChange}
|
||||
width={screenWidth}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -348,6 +348,13 @@ function Layout() {
|
||||
header: () => null,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/trailer/page"
|
||||
options={{
|
||||
presentation: "modal",
|
||||
title: "",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="login"
|
||||
options={{
|
||||
|
||||
Reference in New Issue
Block a user