mirror of
https://github.com/Jellify-Music/App.git
synced 2026-01-07 19:40:19 -06:00
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:
@@ -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({
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
38
components/Playlists/component.tsx
Normal file
38
components/Playlists/component.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
12
components/Playlists/screen.tsx
Normal file
12
components/Playlists/screen.tsx
Normal 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} />
|
||||
)
|
||||
}
|
||||
@@ -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">;
|
||||
|
||||
@@ -38,4 +38,5 @@ export enum QueryKeys {
|
||||
Item = "Item",
|
||||
Search = "Search",
|
||||
SearchSuggestions = "SearchSuggestions",
|
||||
FavoritePlaylists = "FavoritePlaylists",
|
||||
}
|
||||
Reference in New Issue
Block a user