mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-16 14:01:58 +01:00
wip: refactor
This commit is contained in:
@@ -1,67 +0,0 @@
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { Image } from "expo-image";
|
||||
import { useAtom } from "jotai";
|
||||
import { useMemo, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { WatchedIndicator } from "./WatchedIndicator";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
|
||||
type MoviePosterProps = {
|
||||
item: BaseItemDto;
|
||||
showProgress?: boolean;
|
||||
};
|
||||
|
||||
const MoviePoster: React.FC<MoviePosterProps> = ({
|
||||
item,
|
||||
showProgress = false,
|
||||
}) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
|
||||
const url = useMemo(
|
||||
() =>
|
||||
getPrimaryImageUrl({
|
||||
api,
|
||||
item,
|
||||
}),
|
||||
[item],
|
||||
);
|
||||
|
||||
const [progress, setProgress] = useState(
|
||||
item.UserData?.PlayedPercentage || 0,
|
||||
);
|
||||
|
||||
if (!url)
|
||||
return (
|
||||
<View
|
||||
className="rounded-md overflow-hidden border border-neutral-900"
|
||||
style={{
|
||||
aspectRatio: "10/15",
|
||||
}}
|
||||
></View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View className="relative rounded-md overflow-hidden border border-neutral-900">
|
||||
<Image
|
||||
key={item.Id}
|
||||
id={item.Id}
|
||||
source={{
|
||||
uri: url,
|
||||
}}
|
||||
cachePolicy={"memory-disk"}
|
||||
contentFit="cover"
|
||||
style={{
|
||||
aspectRatio: "10/15",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
<WatchedIndicator item={item} />
|
||||
{showProgress && progress > 0 && (
|
||||
<View className="h-1 bg-red-600 w-full"></View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoviePoster;
|
||||
@@ -6,7 +6,7 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { useAtom } from "jotai";
|
||||
import React, { useCallback } from "react";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
|
||||
export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
@@ -15,15 +15,15 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const invalidateQueries = useCallback(() => {
|
||||
const invalidateQueries = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["item", item.Id],
|
||||
queryKey: ["item"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["resumeItems", user?.Id],
|
||||
queryKey: ["resumeItems"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["nextUp", item.SeriesId],
|
||||
queryKey: ["nextUp"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["episodes"],
|
||||
@@ -31,7 +31,10 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["seasons"],
|
||||
});
|
||||
}, [api, item.Id, queryClient, user?.Id]);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["nextUp-all"],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import MoviePoster from "@/components/posters/MoviePoster";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { router } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import ContinueWatchingPoster from "./ContinueWatchingPoster";
|
||||
import { ItemCardText } from "./ItemCardText";
|
||||
import { Text } from "./common/Text";
|
||||
import MoviePoster from "./MoviePoster";
|
||||
import { useMemo } from "react";
|
||||
|
||||
type SimilarItemsProps = {
|
||||
itemId: string;
|
||||
@@ -42,7 +41,7 @@ export const SimilarItems: React.FC<SimilarItemsProps> = ({ itemId }) => {
|
||||
|
||||
const movies = useMemo(
|
||||
() => similarItems?.filter((i) => i.Type === "Movie") || [],
|
||||
[similarItems],
|
||||
[similarItems]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Text } from "@/components/common/Text";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { useRouter } from "expo-router";
|
||||
import * as Haptics from "expo-haptics";
|
||||
|
||||
interface Props extends TouchableOpacityProps {
|
||||
item: BaseItemDto;
|
||||
@@ -22,11 +23,18 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
|
||||
if (item.Type === "Series") router.push(`/series/${item.Id}`);
|
||||
if (item.Type === "Episode") router.push(`/items/${item.Id}`);
|
||||
if (item.Type === "MusicAlbum") router.push(`/albums/${item.Id}`);
|
||||
if (item.Type === "Movie") router.push(`/songs/${item.Id}`);
|
||||
if (item.Type === "Audio") router.push(`/albums/${item.AlbumId}`);
|
||||
if (item.Type === "MusicArtist")
|
||||
router.push(`/artists/${item.Id}/page`);
|
||||
|
||||
// Movies and all other cases
|
||||
if (item.Type === "BoxSet") router.push(`/collections/${item.Id}`);
|
||||
router.push(`/items/${item.Id}`);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -33,6 +33,8 @@ export const FilterButton: React.FC<Props> = ({
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
if (filters?.length === 0) return null;
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { View, ViewProps } from "react-native";
|
||||
import { ActivityIndicator, View, ViewProps } from "react-native";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -42,7 +42,7 @@ export const LargeMovieCarousel: React.FC<Props> = ({ ...props }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const { data: mediaListCollection } = useQuery<string | null>({
|
||||
const { data: mediaListCollection, isLoading: l1 } = useQuery<string | null>({
|
||||
queryKey: ["mediaListCollection", user?.Id],
|
||||
queryFn: async () => {
|
||||
if (!api || !user?.Id) return null;
|
||||
@@ -62,9 +62,7 @@ export const LargeMovieCarousel: React.FC<Props> = ({ ...props }) => {
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const { data: popularItems, isLoading: isLoadingPopular } = useQuery<
|
||||
BaseItemDto[]
|
||||
>({
|
||||
const { data: popularItems, isLoading: l2 } = useQuery<BaseItemDto[]>({
|
||||
queryKey: ["popular", user?.Id],
|
||||
queryFn: async () => {
|
||||
if (!api || !user?.Id || !mediaListCollection) return [];
|
||||
@@ -83,6 +81,13 @@ export const LargeMovieCarousel: React.FC<Props> = ({ ...props }) => {
|
||||
|
||||
const width = Dimensions.get("screen").width;
|
||||
|
||||
if (l1 || l2)
|
||||
return (
|
||||
<View className="h-[242px] flex items-center justify-center">
|
||||
<ActivityIndicator size={"small"} color="#fff" />
|
||||
</View>
|
||||
);
|
||||
|
||||
if (!popularItems) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { TouchableOpacity, View, ViewProps } from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import MoviePoster from "@/components/posters/MoviePoster";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { router } from "expo-router";
|
||||
import { View, ViewProps } from "react-native";
|
||||
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
||||
import { ItemCardText } from "../ItemCardText";
|
||||
import { HorizontalScroll } from "../common/HorrizontalScroll";
|
||||
import MoviePoster from "../MoviePoster";
|
||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
title: string;
|
||||
@@ -29,22 +29,17 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<View {...props}>
|
||||
<Text className="px-4 text-2xl font-bold mb-2">{title}</Text>
|
||||
<Text className="px-4 text-2xl font-bold mb-2 text-neutral-100">
|
||||
{title}
|
||||
</Text>
|
||||
<HorizontalScroll<BaseItemDto>
|
||||
data={data}
|
||||
height={orientation === "vertical" ? 247 : 164}
|
||||
loading={loading}
|
||||
renderItem={(item, index) => (
|
||||
<TouchableOpacity
|
||||
<TouchableItemRouter
|
||||
key={index}
|
||||
onPress={() => {
|
||||
if (item.Type === "Series") router.push(`/series/${item.Id}`);
|
||||
else if (item.CollectionType === "music")
|
||||
router.push(`/artists/page?collectionId=${item.Id}`);
|
||||
else if (item.Type === "CollectionFolder")
|
||||
router.push(`/collections/${item.Id}`);
|
||||
else router.push(`/items/${item.Id}`);
|
||||
}}
|
||||
item={item}
|
||||
className={`flex flex-col
|
||||
${orientation === "vertical" ? "w-32" : "w-48"}
|
||||
`}
|
||||
@@ -57,7 +52,7 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
||||
)}
|
||||
<ItemCardText item={item} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableItemRouter>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -28,7 +28,7 @@ export const MediaListSection: React.FC<Props> = ({ collection, ...props }) => {
|
||||
const { data: popularItems, isLoading: isLoadingPopular } = useQuery<
|
||||
BaseItemDto[]
|
||||
>({
|
||||
queryKey: ["popular", user?.Id],
|
||||
queryKey: [collection.Id, user?.Id],
|
||||
queryFn: async () => {
|
||||
if (!api || !user?.Id || !collection.Id) return [];
|
||||
|
||||
|
||||
84
components/posters/AlbumCover.tsx
Normal file
84
components/posters/AlbumCover.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { Image } from "expo-image";
|
||||
import { useAtom } from "jotai";
|
||||
import { useMemo } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
type ArtistPosterProps = {
|
||||
item?: BaseItemDto | null;
|
||||
id?: string | null;
|
||||
showProgress?: boolean;
|
||||
};
|
||||
|
||||
const AlbumCover: React.FC<ArtistPosterProps> = ({ item, id }) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
|
||||
const url = useMemo(() => {
|
||||
const u = getPrimaryImageUrl({
|
||||
api,
|
||||
item,
|
||||
});
|
||||
console.log("Image A", u);
|
||||
return u;
|
||||
}, [item]);
|
||||
|
||||
const url2 = useMemo(() => {
|
||||
const u = getPrimaryImageUrlById({
|
||||
api,
|
||||
id,
|
||||
quality: 85,
|
||||
width: 300,
|
||||
});
|
||||
console.log("Image B", u);
|
||||
return u;
|
||||
}, [item]);
|
||||
|
||||
if (!item && id)
|
||||
return (
|
||||
<View className="relative rounded-md overflow-hidden border border-neutral-900">
|
||||
<Image
|
||||
key={id}
|
||||
id={id}
|
||||
source={
|
||||
url2
|
||||
? {
|
||||
uri: url2,
|
||||
}
|
||||
: null
|
||||
}
|
||||
cachePolicy={"memory-disk"}
|
||||
contentFit="cover"
|
||||
style={{
|
||||
aspectRatio: "1/1",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (item)
|
||||
return (
|
||||
<View className="relative rounded-md overflow-hidden border border-neutral-900">
|
||||
<Image
|
||||
key={item.Id}
|
||||
id={item.Id}
|
||||
source={
|
||||
url
|
||||
? {
|
||||
uri: url,
|
||||
}
|
||||
: null
|
||||
}
|
||||
cachePolicy={"memory-disk"}
|
||||
contentFit="cover"
|
||||
style={{
|
||||
aspectRatio: "1/1",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlbumCover;
|
||||
@@ -1,11 +1,10 @@
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { Image } from "expo-image";
|
||||
import { useAtom } from "jotai";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { View } from "react-native";
|
||||
import { WatchedIndicator } from "./WatchedIndicator";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
|
||||
type ArtistPosterProps = {
|
||||
item: BaseItemDto;
|
||||
@@ -24,7 +23,7 @@ const ArtistPoster: React.FC<ArtistPosterProps> = ({
|
||||
api,
|
||||
item,
|
||||
}),
|
||||
[item],
|
||||
[item]
|
||||
);
|
||||
|
||||
if (!url)
|
||||
@@ -1,5 +1,4 @@
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Image } from "expo-image";
|
||||
import { useAtom } from "jotai";
|
||||
import { useMemo } from "react";
|
||||
@@ -9,10 +9,11 @@ type PosterProps = {
|
||||
item?: BaseItemDto | BaseItemPerson | null;
|
||||
url?: string | null;
|
||||
showProgress?: boolean;
|
||||
blurhash?: string | null;
|
||||
};
|
||||
|
||||
const Poster: React.FC<PosterProps> = ({ item, url }) => {
|
||||
if (!url || !item)
|
||||
const Poster: React.FC<PosterProps> = ({ item, url, blurhash }) => {
|
||||
if (!item)
|
||||
return (
|
||||
<View
|
||||
className="border border-neutral-900"
|
||||
@@ -25,11 +26,22 @@ const Poster: React.FC<PosterProps> = ({ item, url }) => {
|
||||
return (
|
||||
<View className="rounded-md overflow-hidden border border-neutral-900">
|
||||
<Image
|
||||
placeholder={
|
||||
blurhash
|
||||
? {
|
||||
blurhash,
|
||||
}
|
||||
: null
|
||||
}
|
||||
key={item.Id}
|
||||
id={item.Id}
|
||||
source={{
|
||||
uri: url,
|
||||
}}
|
||||
source={
|
||||
url
|
||||
? {
|
||||
uri: url,
|
||||
}
|
||||
: null
|
||||
}
|
||||
cachePolicy={"memory-disk"}
|
||||
contentFit="cover"
|
||||
style={{
|
||||
@@ -6,7 +6,7 @@ import React from "react";
|
||||
import { Linking, TouchableOpacity, View } from "react-native";
|
||||
import { HorizontalScroll } from "../common/HorrizontalScroll";
|
||||
import { Text } from "../common/Text";
|
||||
import Poster from "../Poster";
|
||||
import Poster from "../posters/Poster";
|
||||
import { useAtom } from "jotai";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { router } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import Poster from "../Poster";
|
||||
import Poster from "../posters/Poster";
|
||||
import { HorizontalScroll } from "../common/HorrizontalScroll";
|
||||
import { Text } from "../common/Text";
|
||||
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { HorizontalScroll } from "../common/HorrizontalScroll";
|
||||
import { Text } from "../common/Text";
|
||||
import Poster from "../Poster";
|
||||
import Poster from "../posters/Poster";
|
||||
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
||||
import { ItemCardText } from "../ItemCardText";
|
||||
import { router } from "expo-router";
|
||||
|
||||
Reference in New Issue
Block a user