wip: refactoring file structure

This commit is contained in:
Fredrik Burmester
2024-08-15 21:08:35 +02:00
parent d0baf56fd8
commit 1f0ff1594b
26 changed files with 1660 additions and 41 deletions

View File

@@ -0,0 +1,58 @@
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 ArtistPosterProps = {
item: BaseItemDto;
showProgress?: boolean;
};
const ArtistPoster: React.FC<ArtistPosterProps> = ({
item,
showProgress = false,
}) => {
const [api] = useAtom(apiAtom);
const url = useMemo(
() =>
getPrimaryImageUrl({
api,
item,
}),
[item],
);
if (!url)
return (
<View
className="rounded-md overflow-hidden border border-neutral-900"
style={{
aspectRatio: "1/1",
}}
></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: "1/1",
}}
/>
</View>
);
};
export default ArtistPoster;

View File

@@ -150,7 +150,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
sessionId: sessionData.PlaySessionId,
});
},
[sessionData?.PlaySessionId, item, api, paused],
[sessionData?.PlaySessionId, item, api, paused]
);
const play = () => {
@@ -187,7 +187,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
item?.UserData?.PlaybackPositionTicks
? Math.round(item.UserData.PlaybackPositionTicks / 10000)
: 0,
[item],
[item]
);
const backdropUrl = useMemo(
@@ -198,7 +198,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
quality: 70,
width: 200,
}),
[item],
[item]
);
/**
@@ -234,7 +234,9 @@ export const CurrentlyPlayingBar: React.FC = () => {
<BlurView
intensity={Platform.OS === "android" ? 60 : 100}
experimentalBlurMethod={Platform.OS === "android" ? "none" : undefined}
className={`h-full w-full rounded-xl overflow-hidden ${Platform.OS === "android" && "bg-black"}`}
className={`h-full w-full rounded-xl overflow-hidden ${
Platform.OS === "android" && "bg-black"
}`}
>
<Animated.View
style={[
@@ -306,7 +308,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
console.log(e);
writeToLog(
"ERROR",
"Video playback error: " + JSON.stringify(e),
"Video playback error: " + JSON.stringify(e)
);
}}
renderLoader={
@@ -322,27 +324,41 @@ export const CurrentlyPlayingBar: React.FC = () => {
<View className="shrink text-xs">
<TouchableOpacity
onPress={() => {
router.push(`/(auth)/items/${item?.Id}/page`);
console.log(JSON.stringify(item));
if (item?.Type === "Audio")
router.push(`/albums/${item?.AlbumId}`);
else router.push(`/items/${item?.Id}`);
}}
>
<Text>{item?.Name}</Text>
</TouchableOpacity>
{item?.SeriesName ? (
{item?.Type === "Episode" && (
<TouchableOpacity
onPress={() => {
router.push(`/(auth)/series/${item.SeriesId}/page`);
router.push(`/(auth)/series/${item.SeriesId}`);
}}
className="text-xs opacity-50"
>
<Text>{item.SeriesName}</Text>
</TouchableOpacity>
) : (
)}
{item?.Type === "Movie" && (
<View>
<Text className="text-xs opacity-50">
{item?.ProductionYear}
</Text>
</View>
)}
{item?.Type === "Audio" && (
<TouchableOpacity
onPress={() => {
console.log(JSON.stringify(item));
router.push(`/albums/${item?.AlbumId}`);
}}
>
<Text className="text-xs opacity-50">{item?.Album}</Text>
</TouchableOpacity>
)}
</View>
</View>
<View className="flex flex-row items-center space-x-2">

View File

@@ -42,7 +42,7 @@ export const SimilarItems: React.FC<SimilarItemsProps> = ({ itemId }) => {
const movies = useMemo(
() => similarItems?.filter((i) => i.Type === "Movie") || [],
[similarItems]
[similarItems],
);
return (
@@ -58,7 +58,7 @@ export const SimilarItems: React.FC<SimilarItemsProps> = ({ itemId }) => {
{movies.map((item) => (
<TouchableOpacity
key={item.Id}
onPress={() => router.push(`/items/${item.Id}/page`)}
onPress={() => router.push(`/items/${item.Id}`)}
className="flex flex-col w-32"
>
<MoviePoster item={item} />

View File

@@ -38,11 +38,12 @@ export const ScrollingCollectionList: React.FC<Props> = ({
<TouchableOpacity
key={index}
onPress={() => {
if (item.Type === "Series")
router.push(`/series/${item.Id}/page`);
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}/page`);
else router.push(`/items/${item.Id}/page`);
router.push(`/collections/${item.Id}`);
else router.push(`/items/${item.Id}`);
}}
className={`flex flex-col
${orientation === "vertical" ? "w-32" : "w-48"}

View File

@@ -0,0 +1,38 @@
import { TouchableOpacity, View, ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import ArtistPoster from "../ArtistPoster";
import { runtimeTicksToMinutes, runtimeTicksToSeconds } from "@/utils/time";
import { useRouter } from "expo-router";
import { SongsListItem } from "./SongsListItem";
interface Props extends ViewProps {
songs?: BaseItemDto[] | null;
collectionId: string;
artistId: string;
albumId: string;
}
export const SongsList: React.FC<Props> = ({
collectionId,
artistId,
albumId,
songs = [],
...props
}) => {
const router = useRouter();
return (
<View className="flex flex-col space-y-2" {...props}>
{songs?.map((item: BaseItemDto, index: number) => (
<SongsListItem
key={item.Id}
item={item}
index={index}
collectionId={collectionId}
artistId={artistId}
albumId={albumId}
/>
))}
</View>
);
};

View File

@@ -0,0 +1,152 @@
import {
TouchableOpacity,
TouchableOpacityProps,
View,
ViewProps,
} from "react-native";
import { Text } from "@/components/common/Text";
import index from "@/app/(auth)/(tabs)/home";
import { runtimeTicksToSeconds } from "@/utils/time";
import { router } from "expo-router";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { useAtom } from "jotai";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { chromecastProfile } from "@/utils/profiles/chromecast";
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
import CastContext, {
PlayServicesState,
useCastDevice,
useRemoteMediaClient,
} from "react-native-google-cast";
import ios12 from "@/utils/profiles/ios12";
import {
currentlyPlayingItemAtom,
triggerPlayAtom,
} from "../CurrentlyPlayingBar";
import { useActionSheet } from "@expo/react-native-action-sheet";
interface Props extends TouchableOpacityProps {
collectionId: string;
artistId: string;
albumId: string;
item: BaseItemDto;
index: number;
}
export const SongsListItem: React.FC<Props> = ({
collectionId,
artistId,
albumId,
item,
index,
...props
}) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const castDevice = useCastDevice();
const [, setCp] = useAtom(currentlyPlayingItemAtom);
const [, setPlayTrigger] = useAtom(triggerPlayAtom);
const client = useRemoteMediaClient();
const { showActionSheetWithOptions } = useActionSheet();
const openSelect = () => {
if (!castDevice?.deviceId) {
play("device");
return;
}
const options = ["Chromecast", "Device", "Cancel"];
const cancelButtonIndex = 2;
showActionSheetWithOptions(
{
options,
cancelButtonIndex,
},
(selectedIndex: number | undefined) => {
switch (selectedIndex) {
case 0:
play("cast");
break;
case 1:
play("device");
break;
case cancelButtonIndex:
console.log("calcel");
}
},
);
};
const play = async (type: "device" | "cast") => {
if (!user?.Id || !api || !item.Id) return;
const response = await getMediaInfoApi(api!).getPlaybackInfo({
itemId: item?.Id,
userId: user?.Id,
});
const sessionData = response.data;
const url = await getStreamUrl({
api,
userId: user.Id,
item,
startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0,
sessionData,
deviceProfile: castDevice?.deviceId ? chromecastProfile : ios12,
});
if (!url || !item) return;
if (type === "cast" && client) {
await CastContext.getPlayServicesState().then((state) => {
if (state && state !== PlayServicesState.SUCCESS)
CastContext.showPlayServicesErrorDialog(state);
else {
client.loadMedia({
mediaInfo: {
contentUrl: url,
contentType: "video/mp4",
metadata: {
type: item.Type === "Episode" ? "tvShow" : "movie",
title: item.Name || "",
subtitle: item.Overview || "",
},
},
startTime: 0,
});
}
});
} else {
setCp({
item,
playbackUrl: url,
});
// Use this trigger to initiate playback in another component (CurrentlyPlayingBar)
setPlayTrigger((prev) => prev + 1);
}
};
return (
<TouchableOpacity
onPress={() => {
openSelect();
}}
{...props}
>
<View className="flex flex-row items-center space-x-4 bg-neutral-900 border-neutral-800 px-4 py-4 rounded-xl">
<Text className="opacity-50">{index + 1}</Text>
<View>
<Text className="mb-0.5 font-semibold">{item.Name}</Text>
<Text className="opacity-50 text-xs">
{runtimeTicksToSeconds(item.RunTimeTicks)}
</Text>
</View>
</View>
</TouchableOpacity>
);
};

View File

@@ -20,7 +20,7 @@ export const CurrentSeries = ({ item }: { item: BaseItemDto }) => {
renderItem={(item, index) => (
<TouchableOpacity
key={item.Id}
onPress={() => router.push(`/series/${item.SeriesId}/page`)}
onPress={() => router.push(`/series/${item.SeriesId}`)}
className="flex flex-col space-y-2 w-32"
>
<Poster

View File

@@ -92,7 +92,7 @@ export const NextEpisodeButton: React.FC<Props> = ({
return (
<Button
onPress={() => router.replace(`/items/${nextEpisode?.Id}/page`)}
onPress={() => router.replace(`/items/${nextEpisode?.Id}`)}
className={`h-12 aspect-square`}
disabled={disabled}
{...props}

View File

@@ -49,7 +49,7 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
renderItem={(item, index) => (
<TouchableOpacity
onPress={() => {
router.push(`/(auth)/items/${item.Id}/page`);
router.push(`/(auth)/items/${item.Id}`);
}}
key={item.Id}
className="flex flex-col w-32"

View File

@@ -118,7 +118,7 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
<TouchableOpacity
key={item.Id}
onPress={() => {
router.push(`/(auth)/items/${item.Id}/page`);
router.push(`/(auth)/items/${item.Id}`);
}}
className="flex flex-col w-48"
>

View File

@@ -12,7 +12,7 @@ export const SeriesTitleHeader: React.FC<Props> = ({ item, ...props }) => {
return (
<>
<TouchableOpacity
onPress={() => router.push(`/(auth)/series/${item.SeriesId}/page`)}
onPress={() => router.push(`/(auth)/series/${item.SeriesId}`)}
>
<Text className="text-center opacity-50">{item?.SeriesName}</Text>
</TouchableOpacity>