Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 86 additions & 3 deletions src/api/queries/playlist/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { PlaylistTracksQueryKey, PublicPlaylistsQueryKey, UserPlaylistsQueryKey } from './keys'
import { useInfiniteQuery } from '@tanstack/react-query'
import {
PlaylistTracksQueryKey,
PlaylistUsersQueryKey,
PublicPlaylistsQueryKey,
UserPlaylistsQueryKey,
} from './keys'
import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'
import { fetchUserPlaylists, fetchPublicPlaylists, fetchPlaylistTracks } from './utils'
import { ApiLimits } from '../../../configs/query.config'
import { getApi, getUser } from '../../../stores'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
import { BaseItemDto, PlaylistUserPermissions, UserDto } from '@jellyfin/sdk/lib/generated-client'
import { usePlaylistLibrary } from '../libraries'
import { addPlaylistUser, getPlaylistUsers, removePlaylistUser } from './utils/users'
import { ONE_MINUTE, queryClient } from '../../../constants/query-client'
import { triggerHaptic } from '../../../hooks/use-haptic-feedback'
import Toast from 'react-native-toast-message'

export const useUserPlaylists = () => {
const api = getApi()
Expand Down Expand Up @@ -55,3 +64,77 @@ export const usePublicPlaylists = () => {
initialPageParam: 0,
})
}

//hooks - used in react components
//invoke user functions (getPlaylistUsers, etc)
//following react convention
export const usePlaylistUsers = (playlist: BaseItemDto) => {
return useQuery({
queryKey: PlaylistUsersQueryKey(playlist),
queryFn: () => getPlaylistUsers(playlist.Id!),
staleTime: ONE_MINUTE * 15, //refreshes every 15mins
})
}

interface addPlaylistUserMutation {
playlist: BaseItemDto
user: UserDto
CanEdit: boolean
}

//mutations not queries for add/remove
//no params
export const useAddPlaylistUser = () => {
return useMutation({
//playlistId: string, userId: string, CanEdit: boolean
mutationFn: (variables: addPlaylistUserMutation) =>
addPlaylistUser(variables.playlist.Id!, variables.user.Id!, variables.CanEdit),

onSuccess: (data, variables) => {
triggerHaptic('notificationSuccess')
queryClient.setQueryData(
PlaylistUsersQueryKey(variables.playlist),
(previous: PlaylistUserPermissions[] | undefined) => {
if (previous == undefined) {
//return
return [{ userId: variables.user.Id, canEdit: true }]
} else {
return [...previous, { userId: variables.user.Id, canEdit: true }]
}
},
)
},

onError: (error, variables) => {
console.log(error)
Toast.show({ type: 'error', text1: 'Unable to add user to playlist.' })
},
})
}

interface removePlaylistUser {
playlist: BaseItemDto
user: UserDto
}

//remove user as playlist collaborator
export const useRemovePlaylistUser = () => {
return useMutation({
mutationFn: (variables: removePlaylistUser) =>
removePlaylistUser(variables.playlist.Id!, variables.user.Id!),
onSuccess: (data, variables) => {
triggerHaptic('notificationSuccess')
queryClient.setQueryData(
PlaylistUsersQueryKey(variables.playlist),
(previous: PlaylistUserPermissions[] | undefined) => {
if (previous == undefined) {
//return
return []
} else {
return previous.filter((user) => user.UserId != variables.user.Id)
}
},
)
},
})
}
6 changes: 6 additions & 0 deletions src/api/queries/playlist/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
enum PlaylistQueryKeys {
UserPlaylists,
PublicPlaylists,
PlaylistUsers,
}

export const UserPlaylistsQueryKey = (
Expand All @@ -22,3 +23,8 @@ export const PublicPlaylistsQueryKey = (library: BaseItemDto | undefined) => [
PlaylistQueryKeys.PublicPlaylists,
library?.Id,
]

export const PlaylistUsersQueryKey = (playlist: BaseItemDto) => [
PlaylistQueryKeys.PlaylistUsers,
playlist.Id,
]
50 changes: 50 additions & 0 deletions src/api/queries/playlist/utils/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//playlist id

import { getApi, getUser } from '../../../../stores'
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api'

//get playlist users
export async function getPlaylistUsers(playlistId: string) {
//use api
const api = getApi()

if (!api) {
throw new Error('API Instance not set')
}

const playlist = getPlaylistsApi(api)

return (await playlist.getPlaylistUsers({ playlistId })).data
}

//also need user id for add and remove user functions

export async function addPlaylistUser(playlistId: string, userId: string, CanEdit: boolean) {
//use api
const api = getApi()
const playlist = getPlaylistsApi(api!)

//use dto
return await playlist.updatePlaylist({
playlistId,
updatePlaylistDto: {
Users: [
{
UserId: userId,
CanEdit,
},
],
},
})
}

export async function removePlaylistUser(playlistId: string, userId: string) {
//use api
const api = getApi()
const playlist = getPlaylistsApi(api!)

return await playlist.removeUserFromPlaylist({
playlistId,
userId,
})
}
30 changes: 30 additions & 0 deletions src/api/queries/users/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useQuery } from '@tanstack/react-query'
import { UserQueryKey } from './keys'
import { getApi, getUser } from '../../../stores'
import { getUserApi } from '@jellyfin/sdk/lib/utils/api'

//hook to get users on server
export const useUsers = () => {
//using a query to call fetchUsers for server (not playlist)
return useQuery({ queryKey: UserQueryKey, queryFn: fetchUsers })
}

//function to call get user API (jellyfin), no export because it's only used here
const fetchUsers = async () => {
//use api (only get api when this function is called to get users)
const api = getApi()

//get owner of playlist (self)
const owner = getUser()

//check set
if (!api) {
throw new Error('API Instance not set')
}

const usersResponse = await getUserApi(api).getUsers()

//return users where there isn't a user with owner id in array
//return users from api
return usersResponse.data.filter((user) => user.Id != owner?.id)
}
2 changes: 2 additions & 0 deletions src/api/queries/users/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
//key to get users (array of one string) on server
export const UserQueryKey = ['Users']
5 changes: 5 additions & 0 deletions src/components/Playlist/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { Text } from '../Global/helpers/text'
import { RefreshControl } from 'react-native'
import { queryClient } from '../../constants/query-client'
import { PlaylistTracksQueryKey } from '../../api/queries/playlist/keys'
import { addPlaylistUser } from '../../api/queries/playlist/utils/users'
import { useIsDownloaded } from '../../hooks/downloads'
import useDownloadTracks, { useDeleteDownloads } from '../../hooks/downloads/mutations'
import { loadNewQueue } from '../../hooks/player/functions/queue'
Expand Down Expand Up @@ -220,6 +221,10 @@ export default function Playlist({
navigation.setOptions({
headerRight: () => (
<XStack gap={'$2'}>
<Icon
name='account-multiple-plus-outline'
onPress={() => navigation.push('addPlaylistUser')}
/>
{playlistTracks && !editing && downloadActions}
{canEdit && (
<XStack gap={'$2'}>
Expand Down
133 changes: 133 additions & 0 deletions src/screens/Library/add-playlist-users.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import LibraryStackParamList, { LibraryAddPlaylistUsers } from './types'
import { Paragraph, Text, View, XStack, YStack } from 'tamagui'
import {
useAddPlaylistUser,
usePlaylistUsers,
useRemovePlaylistUser,
} from '../../../src/api/queries/playlist'
import { useUsers } from '../../../src/api/queries/users'
import ItemImage from '../../../src/components/Global/components/image'
import TextTicker from 'react-native-text-ticker'
import { TextTickerConfig } from '../../../src/components/Player/component.config'
import { getItemName } from '../../../src/utils/formatting/item-names'
import { SectionList } from 'react-native'
import Icon from '../../../src/components/Global/components/icon'

//screen in react native
export default function addPlaylistUsers({
navigation,
route,
}: LibraryAddPlaylistUsers): React.JSX.Element {
const { playlist } = route.params
const {
data: playlistUsers,
isPending: playlistUserIsPending,
refetch: refetchPlaylistUser,
} = usePlaylistUsers(playlist) //make this playlist an easy access variable (with const variable above)
const { data: users, isPending: useUsersIsPending, refetch: refetchUseUsers } = useUsers()

//invoke mutations on icon press
//add
const addUser = useAddPlaylistUser()
//remove
const removeUser = useRemovePlaylistUser()

//get string array of all playlist user IDs
const playlistUserIds = playlistUsers?.map((playlistUser) => playlistUser.UserId) ?? []

//if user exists in playlist already, do not display
//take all users, filter any users that also appear in playlistUserIds
const otherUsers = users?.filter((user) => playlistUserIds?.includes(user.Id)) ?? []

//any user not included in listed users will get filtered out
const usersInPlaylist = users?.filter((user) => !playlistUserIds?.includes(user.Id)) ?? []

//use formatting for sections component later on
const playlistUserData = [
{
title: 'Shared With',
data: usersInPlaylist,
},
{
title: 'Users on Server',
data: otherUsers,
},
]

//return component here
return (
//return view that occupies full screen
<View flex={1}>
{
//no conditional statement here (have to have a playlist to see this view anyways)
<XStack gap={'$2'} margin={'$4'}>
<ItemImage
item={playlist}
width={'$12'}
height={'$12'}
imageOptions={{ maxWidth: 85, maxHeight: 85, quality: 90 }}
/>

<YStack gap={'$2'}>
<TextTicker {...TextTickerConfig}>
<Paragraph fontWeight={'bold'} fontSize={'$6'}>
{getItemName(playlist)}
</Paragraph>
</TextTicker>

{/* <TextTicker {...TextTickerConfig}>
<Text bold>
{`${(source ?? tracks[0])!.ArtistItems?.map((artist) => getItemName(artist)).join(', ')}`}
</Text>
</TextTicker> */}
</YStack>
</XStack>
}

{/* conditional in react - only render if some variable meet criteria */}
{
//list of users and section list
<SectionList
sections={playlistUserData}
renderItem={({ item: user }) => (
<XStack>
<ItemImage
item={user}
width={'$12'}
height={'$12'}
imageOptions={{ maxWidth: 85, maxHeight: 85, quality: 90 }}
/>
<Paragraph fontWeight='bold' flex={1}>
{user.Name ?? 'Unknown User'}
</Paragraph>
{playlistUserIds.includes(user.Id) ? ( //send playlist id and user id (with bang! because it likely won't be undefined)
<Icon
onPress={() =>
removeUser.mutate({ playlist: playlist, user: user })
}
name='account-remove'
color='$warning'
/>
) : (
//same stuff and canEdit as true bcs you know anyone you're sharing with
<Icon
onPress={() =>
addUser.mutate({
playlist: playlist,
user: user,
CanEdit: true,
})
}
name='account-plus'
color='$borderColor'
/>
)}
</XStack>
)}
keyExtractor={(item) => item.Id!}
/>
}
</View>
)
}
10 changes: 10 additions & 0 deletions src/screens/Library/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import InstantMix from '../../components/InstantMix/component'
import { getItemName } from '../../utils/formatting/item-names'
import { Platform } from 'react-native'
import TracksScreen from '../Tracks'
import addPlaylistUsers from './add-playlist-users'

const LibraryStack = createNativeStackNavigator<LibraryStackParamList>()

Expand Down Expand Up @@ -82,6 +83,15 @@ export default function LibraryScreen(): React.JSX.Element {
sheetAllowedDetents: 'fitToContents',
}}
/>
<LibraryStack.Screen
name='AddPlaylistUsers'
component={addPlaylistUsers}
options={{
title: 'Add Playlist Users',
presentation: 'formSheet',
sheetAllowedDetents: Platform.OS === 'ios' ? 'fitToContents' : [1.0], //screen full size
}}
/>

<LibraryStack.Screen name='Tracks' component={TracksScreen} />
</LibraryStack.Navigator>
Expand Down
7 changes: 7 additions & 0 deletions src/screens/Library/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ type LibraryStackParamList = BaseStackParamList & {
DeletePlaylist: {
playlist: BaseItemDto
}
AddPlaylistUsers: {
playlist: BaseItemDto
}
}

export default LibraryStackParamList
Expand All @@ -23,3 +26,7 @@ export type LibraryDeletePlaylistProps = NativeStackScreenProps<
LibraryStackParamList,
'DeletePlaylist'
>
export type LibraryAddPlaylistUsers = NativeStackScreenProps<
LibraryStackParamList,
'AddPlaylistUsers'
>
Loading