Merge pull request #51 from anultravioletaurora/9-implement-playlist-crud

9 implement playlist crud
This commit is contained in:
Violet Caulfield
2025-02-05 20:36:39 -06:00
committed by GitHub
35 changed files with 401 additions and 130 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
node-version: 20
- name: Echo package.json version to Github ENV
run: echo VERISON_NUMBER=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV
run: echo VERSION_NUMBER=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV
- run: npm run init
+10 -2
View File
@@ -17,7 +17,7 @@ jobs:
node-version: 20
- name: Echo package.json version to Github ENV
run: echo VERISON_NUMBER=$(npm version minor --tag-version-prefix="" --no-commit-hooks) >> $GITHUB_ENV
run: echo VERSION_NUMBER=$(npm version minor --tag-version-prefix="" --no-commit-hooks) >> $GITHUB_ENV
- run: npm run init
@@ -37,4 +37,12 @@ jobs:
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "[skip actions]"
file_pattern: "ios/Jellify.xcodeproj/project.pbxproj"
file_pattern: "ios/Jellify.xcodeproj/project.pbxproj"
- name: Create Github Release
uses: elgohr/Github-Release-Action@v5
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
title: "Jellify - ${{ env.VERSION_NUMBER }}"
tag: ${{ env.VERSION_NUMBER }}
+14 -11
View File
@@ -1,29 +1,34 @@
# Jellify (verb) - to make gelatinous
# Jellify
![Jellify App Icon](assets/icon_60pt_3x.jpg)
A music player for [Jellyfin](https://jellyfin.org/) built with [React Native](https://reactnative.dev/).
jellify (verb) - to make gelatinous
*Jellify* is a music player for [Jellyfin](https://jellyfin.org/) built with [React Native](https://reactnative.dev/). It has a UX meant to feel familiar if youve used other music streaming apps.
### Background
I wanted to create a music app that could handle extremely large music libraries (i.e., 100K+ songs) and not get bogged down. I wanted to avoid syncing the database to the device when the library changes, and instead opt for a heavily cached persistence layer instead. I discovered Tanstack Query and combined it with React Native MMKV and the rest was history!
I was after a music app for Jellyfin that showcased my music with artwork and had the ability to algorithmically curate music (not that you have to use *Jellify* that way). I also wanted to create a music app that could handle my extremely large music libraries (i.e., 100K+ songs) and not get bogged down. The end goal was to build a music streaming app that worked like the big guys, all while being FOSS and powered by self hosting.
This app was designed with me and my dad in mind, since I wanted to give him a sleek, one stop shop for live recordings of bands he likes (read: the Grateful Dead), with a UI that he'd find instantly familiar and useful. CarPlay / Android Auto support is also a must for him.
This app was designed with me and my dad in mind, since I wanted to give him a sleek, one stop shop for live recordings of bands he likes (read: the Grateful Dead). The UI was designed so that he'd find it instantly familiar and useful. CarPlay / Android Auto support was also a must for us, as we both use CarPlay religiously.
Designed to be lightweight and scalable, *Jellify* caters to those who want a music player experience similar to what's provided by music streaming services.
**TL;DR** Designed to be lightweight and scalable, *Jellify* caters to those who want a mobile Jellyfin music experience similar to what's provided by the big music streaming services.
## Features
### Current
- Available via Private Testflight
- iOS and Android support
- Carefully crafted Light and Dark modes
- Home screen access to previously played tracks, artists, and your playlists
- Full Last.FM Plugin support
- Library of Favorited Music, not too dissimilar to how streaming services handle your 'library'
- Full playlist support, including creating, updating, and reordering
### Roadmap
- Full playlist support, including creating, updating, and reordering
- Quick access to similar artists and items for discovering music in your library
- Support for Jellyfin mixes
- CarPlay / Android Auto Support
- Public Testflight
- Offline Playback
- Web / Desktop support
## Lemme see!
### Home
@@ -37,7 +42,7 @@ Designed to be lightweight and scalable, *Jellify* caters to those who want a mu
![Album](screenshots/album.png)
### Player
![Player with Blurhash](screenshots/blurred_player.png)
![Player](screenshots/player.png)
![Queue](screenshots/player_queue.png)
@@ -62,7 +67,5 @@ This is undoubtedly a passion project of [mine](https://github.com/anultraviolet
## Special Thanks To
- The [Jellyfin Team](https://jellyfin.org/) for their amazing server software
- Tony, Alyssa for their contributions
## Running Locally
Clone the repository and run ```npm i``` to install the dependencies
- Tony and Jordan for their testing and feedback from the early stages of development
- Alyssa, for your artistic abilities and giving Jellify the flair it needed
+10 -1
View File
@@ -1,7 +1,16 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import Client from "../../../api/client";
import { getPlaylistsApi } from "@jellyfin/sdk/lib/utils/api";
export async function addToPlaylist(track: BaseItemDto, playlist: BaseItemDto) {
return getPlaylistsApi(Client.api!)
.addItemToPlaylist({
ids: [
track.Id!
],
playlistId: playlist.Id!
})
}
export async function reorderPlaylist(playlistId: string, itemId: string, to: number) {
return getPlaylistsApi(Client.api!)
+7 -8
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({
+34
View File
@@ -70,6 +70,40 @@ export function fetchFavoriteAlbums(): Promise<BaseItemDto[]> {
})
}
export function fetchFavoritePlaylists(): Promise<BaseItemDto[]> {
console.debug(`Fetching user's favorite playlists`);
return new Promise(async (resolve, reject) => {
getItemsApi(Client.api!)
.getItems({
userId: Client.user!.id,
parentId: Client.library!.playlistLibraryId,
fields: [
"Path"
],
sortBy: [
ItemSortBy.SortName
],
sortOrder: [
SortOrder.Ascending
]
})
.then((response) => {
if (response.data.Items)
resolve(response.data.Items.filter(item =>
item.UserData?.IsFavorite ||
item.Path?.includes("/config/data/playlists")
))
else
resolve([])
})
.catch((error) => {
console.error(error);
reject(error);
});
});
}
export function fetchFavoriteTracks(): Promise<BaseItemDto[]> {
console.debug(`Fetching user's favorite tracks`);
+4 -4
View File
@@ -22,10 +22,10 @@ export function fetchUserPlaylists(): Promise<BaseItemDto[]> {
]
})
.then((response) => {
if (response.data.Items) {
console.log(response.data.Items);
resolve(response.data.Items.filter(playlist => playlist.Path?.includes("/config/data/playlists")))
}
if (response.data.Items)
resolve(response.data.Items.filter(playlist =>
playlist.Path?.includes("/config/data/playlists")
))
else
resolve([]);
})
+6
View File
@@ -1,6 +1,12 @@
import { QueryKeys } from "../../enums/query-keys";
import { useQuery } from "@tanstack/react-query";
import { fetchUserPlaylists } from "./functions/playlists";
import { fetchFavoritePlaylists } from "./functions/favorites";
export const useFavoritePlaylists = () => useQuery({
queryKey: [QueryKeys.FavoritePlaylists],
queryFn: () => fetchFavoritePlaylists()
});
export const useUserPlaylists = () => useQuery({
queryKey: [QueryKeys.UserPlaylists],
+5 -10
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} />
)
}
+12
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}
@@ -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;
@@ -1,8 +1,9 @@
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 { View } from "tamagui";
import { isEmpty } from "lodash";
import { Image } from "react-native";
interface BlurhashLoadingProps {
item: BaseItemDto;
@@ -38,7 +39,8 @@ export default function BlurhashedImage({
style={{
height: height ?? width,
width,
borderRadius: cornered ? 2 : 25
borderRadius: cornered ? 2 : 25,
resizeMode: "contain"
}}
/>
) : blurhash && (
@@ -8,6 +8,9 @@ import { useUserData } from "../../../api/queries/favorites";
import { getTokens, Spinner } from "tamagui";
import Client from "../../../api/client";
import { usePlayerContext } from "../../..//player/provider";
import { queryClient } from "../../../constants/query-client";
import { QueryKeys } from "../../../enums/query-keys";
import { trigger } from "react-native-haptic-feedback";
interface SetFavoriteMutation {
item: BaseItemDto,
@@ -35,8 +38,22 @@ export default function FavoriteButton({
})
},
onSuccess: () => {
trigger("notificationSuccess");
setIsFavorite(true);
onToggle ? onToggle() : {};
// Force refresh of track user data
queryClient.invalidateQueries({
queryKey: [QueryKeys.UserData, item.Id],
exact: true
});
if (item.Type === 'Audio') {
queryClient.invalidateQueries({
queryKey: [QueryKeys.ItemTracks]
});
}
}
})
+2 -1
View File
@@ -18,7 +18,7 @@ export function ItemCard(props: CardProps) {
const dimensions = props.width && typeof(props.width) === "number" ? { width: props.width, height: props.width } : { width: 150, height: 150 };
const logoDimensions = props.width && typeof(props.width) === "number" ? { width: props.width / 2, height: props.width / 6 }: { width: 100, height: 30 };
const logoDimensions = props.width && typeof(props.width) === "number" ? { width: props.width / 2, height: props.width / 6 }: { width: 100, height: 75 };
return (
<View
@@ -40,6 +40,7 @@ export function ItemCard(props: CardProps) {
<TamaguiCard.Footer padded>
{ props.item.Type === 'MusicArtist' && (
<BlurhashedImage
cornered
item={props.item}
type={ImageType.Logo}
width={logoDimensions.width}
+8 -1
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>
+1 -1
View File
@@ -32,7 +32,7 @@ export default function IconCard({
<Icon color={getTokens().color.purpleDark.val} name={name} large />
</Card.Header>
<Card.Footer padded>
<H4 color={getTokens().color.purpleDark.val}>{ caption ?? "" }</H4>
<H4 color={getTokens().color.purpleDark}>{ caption ?? "" }</H4>
</Card.Footer>
<Card.Background backgroundColor={getTokens().color.telemagenta}>
+1
View File
@@ -62,6 +62,7 @@ export function H4(props: TamaguiTextProps): React.JSX.Element {
<TamaguiH4
fontWeight={800}
marginVertical={3}
{...props}
>
{ props.children }
</TamaguiH4>
+53 -42
View File
@@ -32,7 +32,7 @@ export default function ItemDetail({
switch (item.Type) {
case "Audio": {
options = TrackOptions({ item, navigation, isNested });
options = TrackOptions({ track: item, navigation, isNested });
break;
}
@@ -58,7 +58,11 @@ export default function ItemDetail({
return (
<ScrollView contentInsetAdjustmentBehavior="automatic">
<YStack alignItems="center" flex={1}>
<YStack
alignItems="center"
flex={1}
marginTop={"$4"}
>
<XStack
justifyContent="center"
@@ -73,51 +77,58 @@ 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"}
alignItems="flex-start"
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"}
<YStack
flex={1}
alignContent="center"
justifyContent="center"
>
{ item.Album ?? "" }
</Text>
<Spacer />
<FavoriteButton item={item} />
<FavoriteButton item={item} />
</YStack>
</XStack>
<Spacer />
<Spacer />
{ options ?? <View /> }
</YStack>
{ options ?? <View /> }
</YStack>
</ScrollView>
+144 -39
View File
@@ -1,62 +1,167 @@
import { usePlayerContext } from "../../../player/provider";
import { useItem } from "../../../api/queries/item";
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 { getToken, getTokens, ListItem, Separator, Spacer, Spinner, XStack, YGroup, 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";
import { Text } from "../../../components/Global/helpers/text";
import { useUserPlaylists } from "../../../api/queries/playlist";
import React from "react";
import BlurhashedImage from "../../../components/Global/components/blurhashed-image";
import { useMutation } from "@tanstack/react-query";
import { AddToPlaylistMutation } from "../types";
import { addToPlaylist } from "../../../api/mutations/functions/playlists";
import { trigger } from "react-native-haptic-feedback";
import { queryClient } from "../../../constants/query-client";
import { QueryKeys } from "../../../enums/query-keys";
interface TrackOptionsProps {
track: BaseItemDto;
navigation: NativeStackNavigationProp<StackParamList>;
/**
* Whether this is nested in the player modal
*/
isNested: boolean | undefined;
}
export default function TrackOptions({
item,
track,
navigation,
isNested
} : {
item: BaseItemDto,
navigation: NativeStackNavigationProp<StackParamList>,
isNested: boolean | undefined// Whether this is nested in the player modal
}) : React.JSX.Element {
} : TrackOptionsProps) : React.JSX.Element {
const { data: album, isSuccess } = useItem(item.AlbumId ?? "");
const { data: album, isSuccess: albumFetchSuccess } = useItem(track.AlbumId ?? "");
const { data: playlists, isPending : playlistsFetchPending, isSuccess: playlistsFetchSuccess } = useUserPlaylists();
const { useAddToQueue } = usePlayerContext();
const { width } = useSafeAreaFrame();
return (
<XStack alignContent="flex-end" justifyContent="space-between">
{ isSuccess && (
<Icon
name="music-box"
<YStack width={width}>
<XStack justifyContent="space-evenly">
{ albumFetchSuccess ? (
<IconButton
name="music-box"
title="Go to Album"
onPress={() => {
if (isNested)
navigation.getParent()!.goBack();
navigation.goBack();
navigation.push("Album", {
album
});
}}
size={width / 5}
/>
) : (
<Spacer />
)}
<IconButton
circular
name="table-column-plus-before"
title="Play Next"
onPress={() => {
if (isNested)
navigation.getParent()!.goBack();
navigation.goBack();
navigation.push("Album", {
album
});
useAddToQueue.mutate({
track: track,
queuingType: QueuingType.PlayingNext
})
}}
size={width / 5}
/>
<IconButton
circular
name="table-column-plus-after"
title="Queue"
onPress={() => {
useAddToQueue.mutate({
track: track
})
}}
size={width / 5}
/>
</XStack>
<Spacer />
{ playlistsFetchPending && (
<Spinner />
)}
<Icon
name="table-column-plus-before"
onPress={() => {
useAddToQueue.mutate({
track: item,
queuingType: QueuingType.PlayingNext
})
}}
/>
{ playlistsFetchSuccess && (
<>
<Text
bold
fontSize={"$6"}
>
Add to Playlist
</Text>
<Icon
name="table-column-plus-after"
onPress={() => {
useAddToQueue.mutate({
track: item
})
}}
/>
</XStack>
<YGroup separator={(<Separator />)}>
{ playlists.map(playlist => {
const useAddToPlaylist = useMutation({
mutationFn: ({ track, playlist }: AddToPlaylistMutation) => {
return addToPlaylist(track, playlist)
},
onSuccess: (data, { playlist }) => {
trigger("notificationSuccess")
queryClient.invalidateQueries({
queryKey: [QueryKeys.ItemTracks, playlist.Id!, false],
exact: true
});
},
onError: () => {
trigger("notificationError")
}
})
return (
<YGroup.Item>
<ListItem hoverTheme onPress={() => {
useAddToPlaylist.mutate({
track,
playlist
})
}}>
<XStack alignItems="center">
<YStack flex={1}>
<BlurhashedImage
cornered
item={playlist}
width={width / 6}
/>
</YStack>
<YStack
alignItems="flex-start"
flex={4}
>
<Text bold fontSize={"$6"}>{playlist.Name ?? "Untitled Playlist"}</Text>
<Text color={getTokens().color.amethyst}>{`${playlist.ChildCount ?? 0} tracks`}</Text>
</YStack>
</XStack>
</ListItem>
</YGroup.Item>
)
})}
</YGroup>
</>
)}
</YStack>
)
}
+6
View File
@@ -0,0 +1,6 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
export interface AddToPlaylistMutation {
track: BaseItemDto;
playlist: BaseItemDto;
}
+38
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}
/>
)
}}
/>
)
}
+12
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} />
)
}
+4 -2
View File
@@ -1,6 +1,5 @@
import React, { useCallback, useState } from "react";
import Input from "../Global/helpers/input";
import { debounce } from "lodash";
import Item from "../Global/components/item";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { StackParamList } from "../types";
@@ -8,6 +7,7 @@ import { QueryKeys } from "../../enums/query-keys";
import { fetchSearchResults } from "../../api/queries/functions/search";
import { useQuery } from "@tanstack/react-query";
import { FlatList, useColorScheme } from "react-native";
import { Text } from "../Global/helpers/text";
export default function Search({
navigation
@@ -42,7 +42,6 @@ export default function Search({
<FlatList
contentInsetAdjustmentBehavior="automatic"
progressViewOffset={10}
indicatorStyle={isDarkMode ? 'white' : 'default'}
ListHeaderComponent={(
<Input
placeholder="Seek and ye shall find..."
@@ -50,6 +49,9 @@ export default function Search({
value={searchString}
/>
)}
ListEmptyComponent={(
<Text>No results found</Text>
)}
data={items}
refreshing={isFetching}
renderItem={({ index, item }) => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { StackParamList } from "@/components/types"
import { StackParamList } from "../../../components/types"
import { NativeStackNavigationProp } from "@react-navigation/native-stack"
import DevTools from "../helpers/dev-tools"
+2
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">;
+1 -1
View File
@@ -6,4 +6,4 @@ export const queryClient = new QueryClient({
gcTime: (1000 * 60 * 24 * 24) * 5 // 5 days
}
}
})
});
+1
View File
@@ -38,4 +38,5 @@ export enum QueryKeys {
Item = "Item",
Search = "Search",
SearchSuggestions = "SearchSuggestions",
FavoritePlaylists = "FavoritePlaylists",
}
+1 -1
View File
@@ -40,7 +40,7 @@ platform :ios do
)
increment_version_number(
version_number: ENV['VERISON_NUMBER'],
version_number: ENV['VERSION_NUMBER'],
xcodeproj: "Jellify.xcodeproj"
)
+1
View File
@@ -10,6 +10,7 @@
"start": "react-native start",
"test": "jest",
"pod:install": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=0 pod install",
"pod:install-new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 pod install",
"pod:clean": "cd ios && pod deintegrate"
},
"dependencies": {
+1 -1
View File
@@ -1,4 +1,4 @@
import { JellifyTrack } from "@/types/JellifyTrack";
import { JellifyTrack } from "../types/JellifyTrack";
import { QueuingType } from "../enums/queuing-type";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 690 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 KiB

After

Width:  |  Height:  |  Size: 576 KiB