mirror of
https://github.com/Jellify-Music/App.git
synced 2026-01-07 19:40:19 -06:00
Buiding out item detail component
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
@@ -96,7 +96,7 @@ export default function Library(): React.JSX.Element {
|
||||
name="Details"
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerTitle: "",
|
||||
headerShown: false,
|
||||
presentation: "modal"
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
38
components/Global/helpers/blurhashed-image.tsx
Normal file
38
components/Global/helpers/blurhashed-image.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export default function Home(): React.JSX.Element {
|
||||
name="Details"
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerTitle: "",
|
||||
headerShown: false,
|
||||
presentation: "modal"
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function Player({ navigation }: { navigation: NativeStackNavigati
|
||||
name="Details"
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerTitle: ""
|
||||
headerShown: false
|
||||
}}
|
||||
/>
|
||||
|
||||
13
components/Settings/screens/account-details.tsx
Normal file
13
components/Settings/screens/account-details.tsx
Normal 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} />
|
||||
)
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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">;
|
||||
Reference in New Issue
Block a user