Buiding out item detail component

This commit is contained in:
Violet Caulfield
2025-01-20 19:31:38 -06:00
parent fa063fd2cb
commit e5cfef9b38
15 changed files with 175 additions and 101 deletions

View File

@@ -3,6 +3,7 @@ import { ImageFormat, ImageType } from "@jellyfin/sdk/lib/generated-client/model
import { getImageApi } from "@jellyfin/sdk/lib/utils/api"
import _ from "lodash"
import { queryConfig } from "../query.config"
import Client from "@/api/client"
@@ -26,35 +27,18 @@ export function fetchImage(api: Api, itemId: string, imageType?: ImageType) : Pr
});
}
export function fetchArtistImage(api: Api, artistId: string, imageType?: ImageType) : Promise<string> {
return new Promise(async (resolve, reject) => {
let response = await getImageApi(api).getArtistImage({
name: "",
imageIndex: 1,
imageType: imageType ? imageType : ImageType.Primary
})
console.log(response.data)
if (_.isEmpty(response.data))
reject(new Error("No image for artist"))
resolve(convertFileToBase64(response.data));
});
}
export function fetchItemImage(api: Api, itemId: string, imageType?: ImageType, width?: number) {
export function fetchItemImage(itemId: string, imageType?: ImageType, width?: number) {
return getImageApi(api).getItemImage({
itemId,
imageType: imageType ? imageType : ImageType.Primary,
format: ImageFormat.Jpg
})
.then((response) => {
console.log(convertFileToBase64(response.data))
return convertFileToBase64(response.data);
})
return getImageApi(Client.api!)
.getItemImage({
itemId,
imageType: imageType ? imageType : ImageType.Primary,
format: ImageFormat.Jpg
})
.then((response) => {
console.log(convertFileToBase64(response.data))
return convertFileToBase64(response.data);
});
}
function base64toJpeg(encode: string) : string {

View File

@@ -1,20 +1,9 @@
import { useQuery } from "@tanstack/react-query";
import { QueryKeys } from "../../enums/query-keys";
import { Api } from "@jellyfin/sdk";
import { fetchArtistImage, fetchImage, fetchItemImage } from "./functions/images";
import { fetchItemImage } from "./functions/images";
import { ImageType } from "@jellyfin/sdk/lib/generated-client/models";
export const useImage = (api: Api, itemId: string, imageType?: ImageType) => useQuery({
queryKey: [QueryKeys.ItemImage, api, itemId, imageType],
queryFn: ({ queryKey }) => fetchImage(queryKey[1] as Api, queryKey[2] as string, queryKey[3] as ImageType | undefined)
});
export const useArtistImage = (api: Api, artistName: string, imageType?: ImageType) => useQuery({
queryKey: [QueryKeys.ArtistImage, api, artistName, imageType],
queryFn: ({ queryKey }) => fetchArtistImage(queryKey[1] as Api, queryKey[2] as string, queryKey[3] as ImageType | undefined)
})
export const useItemImage = (api: Api, itemId: string, imageType?: ImageType, width?: number) => useQuery({
queryKey: [QueryKeys.ItemImage, api, itemId, imageType, width],
queryFn: ({ queryKey }) => fetchItemImage(queryKey[1] as Api, queryKey[2] as string, queryKey[3] as ImageType | undefined, queryKey[4] as number | undefined)
export const useItemImage = (itemId: string, imageType?: ImageType, width?: number) => useQuery({
queryKey: [QueryKeys.ItemImage, itemId, imageType, width],
queryFn: () => fetchItemImage(itemId, imageType, width)
});

View File

@@ -96,7 +96,7 @@ export default function Library(): React.JSX.Element {
name="Details"
component={DetailsScreen}
options={{
headerTitle: "",
headerShown: false,
presentation: "modal"
}}
/>

View File

@@ -1,33 +0,0 @@
import Client from "../../../api/client";
import { useItemImage } from "../../../api/queries/image";
import { Blurhash } from "react-native-blurhash";
import { Image, View } from "tamagui";
interface BlurhashLoadingProps {
itemId: string;
blurhash: string;
size: number
}
export default function BlurhashLoading(props: BlurhashLoadingProps) : React.JSX.Element {
const { data: image, isSuccess } = useItemImage(Client.api!, props.itemId);
return (
<View minHeight={props.size}>
{ isSuccess ? (
<Image
src={image}
style={{
height: props.size,
width: props.size,
}}
/>
) : (
<Blurhash blurhash={props.blurhash} style={{ flex: 1 }} />
)
}
</View>
)
}

View File

@@ -0,0 +1,38 @@
import { BaseItemDto, ImageType } from "@jellyfin/sdk/lib/generated-client/models";
import { useItemImage } from "../../../api/queries/image";
import { Blurhash } from "react-native-blurhash";
import { Image, View } from "tamagui";
import { isEmpty } from "lodash";
interface BlurhashLoadingProps {
item: BaseItemDto;
size: number
}
export default function BlurhashedImage({ item, size, type }: { item: BaseItemDto, size: number, type?: ImageType }) : React.JSX.Element {
const { data: image, isSuccess } = useItemImage(item.Id!, type);
const blurhash = !isEmpty(item.ImageBlurHashes)
&& !isEmpty(item.ImageBlurHashes.Primary)
? Object.values(item.ImageBlurHashes.Primary)[0]
: undefined;
return (
<View minHeight={size}>
{ isSuccess ? (
<Image
src={image}
style={{
height: size,
width: size,
}}
/>
) : blurhash && (
<Blurhash blurhash={blurhash!} style={{ flex: 1 }} />
)
}
</View>
)
}

View File

@@ -69,7 +69,7 @@ export default function Home(): React.JSX.Element {
name="Details"
component={DetailsScreen}
options={{
headerTitle: "",
headerShown: false,
presentation: "modal"
}}
/>

View File

@@ -1,9 +1,12 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { SafeAreaView } from "react-native-safe-area-context";
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
import { StackParamList } from "../types";
import TrackOptions from "./helpers/TrackOptions";
import { View } from "tamagui";
import { View, XStack, YStack } from "tamagui";
import BlurhashedImage from "../Global/helpers/blurhashed-image";
import { Text } from "../Global/helpers/text";
import { Colors } from "@/enums/colors";
export default function ItemDetail({
item,
@@ -15,6 +18,8 @@ export default function ItemDetail({
let options: React.JSX.Element | undefined = undefined;
const { width } = useSafeAreaFrame();
switch (item.Type) {
case "Audio": {
options = TrackOptions({ item, navigation });
@@ -43,7 +48,39 @@ export default function ItemDetail({
return (
<SafeAreaView edges={["right", "left"]}>
<XStack>
<BlurhashedImage
item={item}
size={width / 3}
/>
<YStack justifyContent="flex-start">
<Text bold fontSize={"$6"}>
{ item.Name ?? "Untitled Track" }
</Text>
<Text
fontSize={"$6"}
color={Colors.Primary}
onPress={() => {
if (item.ArtistItems) {
navigation.navigate("Artist", {
artist: item.ArtistItems[0]
});
}
}}>
{ item.Artists?.join(", ") ?? "Unknown Artist"}
</Text>
<Text
fontSize={"$6"}
color={"$gray10"}
>
{ item.Album ?? "" }
</Text>
</YStack>
</XStack>
{ options ?? <View /> }
</SafeAreaView>
)

View File

@@ -1,4 +1,4 @@
import { StackParamList } from "../../components/types";
import { StackParamList } from "../../../components/types";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { View } from "tamagui";

View File

@@ -35,7 +35,7 @@ export default function Player({ navigation }: { navigation: NativeStackNavigati
name="Details"
component={DetailsScreen}
options={{
headerTitle: ""
headerShown: false
}}
/>

View File

@@ -0,0 +1,13 @@
import { StackParamList } from "@/components/types";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
export default function AccountDetails({
navigation
} : {
navigation: NativeStackNavigationProp<StackParamList>
}) : React.JSX.Element {
return (
<AccountDetails navigation={navigation} />
)
}

View File

@@ -1,17 +1,41 @@
import React from "react";
import { SafeAreaView } from "react-native";
import { ScrollView, Separator } from "tamagui";
import { ListItem, ScrollView, Separator, YGroup } from "tamagui";
import AccountDetails from "../helpers/account-details";
import SignOut from "../helpers/sign-out";
import ServerDetails from "../helpers/server-details";
import LibraryDetails from "../helpers/library-details";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { StackParamList } from "@/components/types";
export default function Root({
navigation
}: {
navigation: NativeStackNavigationProp<StackParamList>
}) : React.JSX.Element {
export default function Root() : React.JSX.Element {
return (
<SafeAreaView>
<ScrollView contentInsetAdjustmentBehavior="automatic">
<AccountDetails />
<Separator marginVertical={15} />
<YGroup
alignSelf="center"
bordered
width={240}
size="$5"
>
<YGroup.Item>
<ListItem
hoverTheme
pressTheme
title="Account Details"
subTitle="Everything is about you, man"
onPress={() => {
navigation.push("AccountDetails")
}}
/>
</YGroup.Item>
</YGroup>
<ServerDetails />
<Separator marginVertical={15} />
<LibraryDetails />

View File

@@ -1,6 +1,7 @@
import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import Root from "./screens/root";
import AccountDetails from "./screens/account-details";
export const SettingsStack = createNativeStackNavigator();
@@ -18,6 +19,17 @@ export default function Settings(): React.JSX.Element {
}
}}
/>
<SettingsStack.Screen
name="Account"
component={AccountDetails}
options={{
headerLargeTitle: true,
headerLargeTitleStyle: {
fontFamily: 'Aileron-Bold'
}
}}
/>
</SettingsStack.Navigator>
)
}

View File

@@ -1,5 +1,5 @@
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import Player from "./Player/component";
import Player from "./Player/stack";
import { Tabs } from "./tabs";
import { StackParamList } from "./types";

View File

@@ -6,7 +6,7 @@ import { useColorScheme } from "react-native";
import { Colors } from "../enums/colors";
import Search from "./Search/component";
import Favorites from "./Favorites/component";
import Settings from "./Settings/component";
import Settings from "./Settings/stack";
import { Discover } from "./Discover/component";
import { Miniplayer } from "./Player/mini-player";
import { Separator } from "tamagui";

View File

@@ -4,18 +4,26 @@ import { NativeStackScreenProps } from "@react-navigation/native-stack";
export type StackParamList = {
Home: undefined;
Discover: undefined;
Favorites: undefined,
Artists: undefined,
Albums: undefined,
Tracks: undefined,
Genres: undefined,
Playlists: undefined,
Search: undefined,
Settings: undefined,
Tabs: undefined,
Player: undefined,
Queue: undefined,
Favorites: undefined;
Artists: undefined;
Albums: undefined;
Tracks: undefined;
Genres: undefined;
Playlists: undefined;
Search: undefined;
Settings: undefined;
AccountDetails: undefined;
Tabs: undefined;
Player: undefined;
Queue: undefined;
Artist: {
artist: BaseItemDto
};
@@ -56,4 +64,6 @@ export type TracksProps = NativeStackScreenProps<StackParamList, "Tracks">;
export type GenresProps = NativeStackScreenProps<StackParamList, "Genres">;
export type DetailsProps = NativeStackScreenProps<StackParamList, "Details">;
export type DetailsProps = NativeStackScreenProps<StackParamList, "Details">;
export type AccountDetailsProps = NativeStackScreenProps<StackParamList, "AccountDetails">;