Favorite playlists, item detail changes

You can now view your favorite playlists in the Favorites tab

You can now see an updated layout of the item detail modal, including big clunky buttons for the queuing options and for viewing the album
This commit is contained in:
Violet Caulfield
2025-02-05 17:59:14 -06:00
parent 172d4af286
commit 31f5a552db
12 changed files with 207 additions and 95 deletions

View File

@@ -1,21 +1,20 @@
import { QueryKeys } from "../../enums/query-keys";
import { useQuery } from "@tanstack/react-query";
import { fetchFavoriteAlbums, fetchFavoriteArtists, fetchFavoriteTracks, fetchUserData } from "./functions/favorites";
import { fetchFavoriteAlbums, fetchFavoriteArtists, fetchFavoritePlaylists, fetchFavoriteTracks, fetchUserData } from "./functions/favorites";
export const useFavoriteArtists = () => useQuery({
queryKey: [QueryKeys.FavoriteArtists],
queryFn: () => {
return fetchFavoriteArtists()
}
queryFn: () => fetchFavoriteArtists()
});
export const useFavoriteAlbums = () => useQuery({
queryKey: [QueryKeys.FavoriteAlbums],
queryFn: () => {
queryFn: () => fetchFavoriteAlbums()
});
return fetchFavoriteAlbums()
}
export const useFavoritePlaylists = () => useQuery({
queryKey: [QueryKeys.FavoritePlaylists],
queryFn: () => fetchFavoritePlaylists()
});
export const useFavoriteTracks = () => useQuery({

View File

@@ -70,6 +70,38 @@ export function fetchFavoriteAlbums(): Promise<BaseItemDto[]> {
})
}
export function fetchFavoritePlaylists(): Promise<BaseItemDto[]> {
console.debug(`Fetching user's favorite playlists`);
return new Promise(async (resolver, reject) => {
getItemsApi(Client.api!)
.getItems({
parentId: Client.library!.playlistLibraryId,
includeItemTypes: [
BaseItemKind.Playlist
],
isFavorite: true,
sortBy: [
ItemSortBy.SortName
],
sortOrder: [
SortOrder.Ascending
]
})
.then((response) => {
if (response.data.Items)
resolver(response.data.Items)
else
resolver([])
})
.catch((error) => {
console.error(error);
reject(error);
});
});
}
export function fetchFavoriteTracks(): Promise<BaseItemDto[]> {
console.debug(`Fetching user's favorite tracks`);

View File

@@ -1,17 +1,12 @@
import React from "react"
import { StackParamList } from "../types"
import { RouteProp } from "@react-navigation/native"
import { NativeStackNavigationProp } from "@react-navigation/native-stack"
import { NativeStackScreenProps } from "@react-navigation/native-stack"
import Albums from "./component"
export default function AlbumsScreen({
route,
navigation
} : {
route: RouteProp<StackParamList, "Albums">,
navigation: NativeStackNavigationProp<StackParamList, "Albums", undefined>
}) : React.JSX.Element {
export default function AlbumsScreen(
props: NativeStackScreenProps<StackParamList, 'Albums'>
) : React.JSX.Element {
return (
<Albums route={route} navigation={navigation}/>
<Albums {...props} />
)
}

View File

@@ -9,6 +9,7 @@ import ArtistsScreen from "../Artists/screen";
import AlbumsScreen from "../Albums/screen";
import TracksScreen from "../Tracks/screen";
import DetailsScreen from "../ItemDetail/screen";
import PlaylistsScreen from "../Playlists/screen";
const LibraryStack = createNativeStackNavigator<StackParamList>();
@@ -82,6 +83,17 @@ export default function Library(): React.JSX.Element {
}}
/>
<LibraryStack.Screen
name="Playlists"
component={PlaylistsScreen}
options={{
headerLargeTitle: true,
headerLargeTitleStyle: {
fontFamily: 'Aileron-Bold'
}
}}
/>
<LibraryStack.Screen
name="Playlist"
component={PlaylistScreen}

View File

@@ -9,7 +9,6 @@ const Categories : CategoryRoute[] = [
{ name: "Albums", iconName: "music-box-multiple" },
{ name: "Tracks", iconName: "music-note"},
{ name: "Playlists", iconName: "playlist-music"},
{ name: "Genres", iconName: "guitar-electric"}
];
export default Categories;

View File

@@ -2,10 +2,12 @@ import React from "react";
import { Square, Theme } from "tamagui";
import Icon from "./icon";
import { TouchableOpacity } from "react-native";
import { Text } from "./text";
interface IconButtonProps {
onPress: () => void;
name: string;
title?: string | undefined;
circular?: boolean | undefined;
size?: number;
}
@@ -13,6 +15,7 @@ interface IconButtonProps {
export default function IconButton({
name,
onPress,
title,
circular,
size
} : IconButtonProps) : React.JSX.Element {
@@ -37,7 +40,11 @@ export default function IconButton({
large
name={name}
color={"$color"}
/>
/>
{ title && (
<Text>{ title }</Text>
)}
</Square>
</TouchableOpacity>
</Theme>

View File

@@ -58,7 +58,7 @@ export default function ItemDetail({
return (
<ScrollView contentInsetAdjustmentBehavior="automatic">
<YStack alignItems="center" flex={1}>
<YStack width={width / 1.5} alignItems="center" flex={1}>
<XStack
justifyContent="center"
@@ -73,51 +73,54 @@ export default function ItemDetail({
/>
</XStack>
<YStack
marginLeft={"$0.5"}
alignContent="center"
justifyContent="center"
flex={2}
>
<Text textAlign="center" bold fontSize={"$6"}>
{ item.Name ?? "Untitled Track" }
</Text>
{/* Item Name, Artist, Album, and Favorite Button */}
<XStack maxWidth={width / 1.5}>
<YStack
marginLeft={"$0.5"}
alignContent="center"
justifyContent="flex-start"
flex={3}
>
<Text textAlign="center" bold fontSize={"$6"}>
{ item.Name ?? "Untitled Track" }
</Text>
<Text
textAlign="center"
fontSize={"$6"}
color={getTokens().color.telemagenta}
onPress={() => {
if (item.ArtistItems) {
<Text
textAlign="center"
fontSize={"$6"}
color={getTokens().color.telemagenta}
onPress={() => {
if (item.ArtistItems) {
if (isNested)
navigation.getParent()!.goBack();
navigation.goBack();
navigation.push("Artist", {
artist: item.ArtistItems[0]
});
}
}}>
{ item.Artists?.join(", ") ?? "Unknown Artist"}
</Text>
<Text
textAlign="center"
fontSize={"$6"}
color={"$borderColor"}
>
{ item.Album ?? "" }
</Text>
</YStack>
if (isNested)
navigation.getParent()!.goBack();
navigation.goBack();
navigation.push("Artist", {
artist: item.ArtistItems[0]
});
}
}}>
{ item.Artists?.join(", ") ?? "Unknown Artist"}
</Text>
<Text
textAlign="center"
fontSize={"$6"}
color={"$borderColor"}
>
{ item.Album ?? "" }
</Text>
<Spacer />
<FavoriteButton item={item} />
<YStack flex={1}>
<FavoriteButton item={item} />
</YStack>
</XStack>
<Spacer />
<Spacer />
{ options ?? <View /> }
</YStack>
{ options ?? <View /> }
</YStack>
</ScrollView>

View File

@@ -4,8 +4,10 @@ import Icon from "../../../components/Global/helpers/icon";
import { StackParamList } from "../../../components/types";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { XStack } from "tamagui";
import { Spacer, XStack, YStack } from "tamagui";
import { QueuingType } from "../../../enums/queuing-type";
import { useSafeAreaFrame } from "react-native-safe-area-context";
import IconButton from "@/components/Global/helpers/icon-button";
export default function TrackOptions({
item,
@@ -20,43 +22,53 @@ export default function TrackOptions({
const { data: album, isSuccess } = useItem(item.AlbumId ?? "");
const { useAddToQueue } = usePlayerContext();
const { width } = useSafeAreaFrame();
return (
<XStack alignContent="flex-end" justifyContent="space-between">
{ isSuccess && (
<Icon
name="music-box"
<YStack width={width / 1.5}>
<XStack alignContent="flex-end" justifyContent="space-evenly">
{ isSuccess ? (
<IconButton
name="music-box"
title="Go to Album"
onPress={() => {
if (isNested)
navigation.getParent()!.goBack();
navigation.goBack();
navigation.push("Album", {
album
});
}}
/>
) : (
<Spacer />
)}
<IconButton
name="table-column-plus-before"
title="Play Next"
onPress={() => {
if (isNested)
navigation.getParent()!.goBack();
navigation.goBack();
navigation.push("Album", {
album
});
useAddToQueue.mutate({
track: item,
queuingType: QueuingType.PlayingNext
})
}}
/>
)}
<Icon
name="table-column-plus-before"
onPress={() => {
useAddToQueue.mutate({
track: item,
queuingType: QueuingType.PlayingNext
})
}}
/>
<Icon
name="table-column-plus-after"
onPress={() => {
useAddToQueue.mutate({
track: item
})
}}
/>
</XStack>
<IconButton
name="table-column-plus-after"
title="Add to Queue"
onPress={() => {
useAddToQueue.mutate({
track: item
})
}}
/>
</XStack>
</YStack>
)
}

View File

@@ -0,0 +1,38 @@
import { useFavoritePlaylists } from "@/api/queries/favorites";
import { FlatList, RefreshControl } from "react-native-gesture-handler";
import { useSafeAreaFrame } from "react-native-safe-area-context";
import { ItemCard } from "../Global/components/item-card";
import { PlaylistsProps } from "../types";
export default function Playlists({ navigation }: PlaylistsProps) : React.JSX.Element {
const { data: playlists, isPending, refetch } = useFavoritePlaylists();
const { width } = useSafeAreaFrame();
return (
<FlatList
contentInsetAdjustmentBehavior="automatic"
numColumns={2}
data={playlists}
refreshControl={
<RefreshControl
refreshing={isPending}
onRefresh={refetch}
/>
}
renderItem={({ index, item: playlist }) => {
return (
<ItemCard
item={playlist}
caption={playlist.Name ?? "Untitled Playlist"}
onPress={() => {
navigation.push("Playlist", { playlist })
}}
width={width / 2.1}
/>
)
}}
/>
)
}

View File

@@ -0,0 +1,12 @@
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { StackParamList } from "../types";
import Playlists from "./component";
import React from "react";
export default function PlaylistsScreen(
props: NativeStackScreenProps<StackParamList, 'Playlists'>
) : React.JSX.Element {
return (
<Playlists {...props} />
)
}

View File

@@ -62,6 +62,8 @@ export type ArtistsProps = NativeStackScreenProps<StackParamList, "Artists">;
export type AlbumsProps = NativeStackScreenProps<StackParamList, "Albums">;
export type PlaylistsProps = NativeStackScreenProps<StackParamList, "Playlists">;
export type TracksProps = NativeStackScreenProps<StackParamList, "Tracks">;
export type GenresProps = NativeStackScreenProps<StackParamList, "Genres">;

View File

@@ -38,4 +38,5 @@ export enum QueryKeys {
Item = "Item",
Search = "Search",
SearchSuggestions = "SearchSuggestions",
FavoritePlaylists = "FavoritePlaylists",
}