mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2026-02-15 19:29:01 -06:00
refactor: applied linter and prettier
This commit is contained in:
@@ -16,7 +16,7 @@ module.exports = {
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'error', // Disallow usage of any
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'error', // Ensure types are explicitly declared
|
||||
'no-mixed-spaces-and-tabs': 'off', // refer https://github.com/prettier/prettier/issues/4199
|
||||
semi: ['error', 'never'],
|
||||
},
|
||||
settings: {
|
||||
|
||||
@@ -11,4 +11,4 @@ module.exports = {
|
||||
arrowParens: 'always',
|
||||
endOfLine: 'lf',
|
||||
trailingComma: 'all',
|
||||
};
|
||||
}
|
||||
|
||||
260
api/client.ts
260
api/client.ts
@@ -1,170 +1,162 @@
|
||||
import { Api } from "@jellyfin/sdk/lib/api";
|
||||
import { JellyfinInfo } from "./info";
|
||||
import { JellifyServer } from "../types/JellifyServer";
|
||||
import { JellifyUser } from "../types/JellifyUser";
|
||||
import { storage } from '../constants/storage';
|
||||
import { MMKVStorageKeys } from "../enums/mmkv-storage-keys";
|
||||
import uuid from 'react-native-uuid';
|
||||
import { JellifyLibrary } from "../types/JellifyLibrary";
|
||||
import { Api } from '@jellyfin/sdk/lib/api'
|
||||
import { JellyfinInfo } from './info'
|
||||
import { JellifyServer } from '../types/JellifyServer'
|
||||
import { JellifyUser } from '../types/JellifyUser'
|
||||
import { storage } from '../constants/storage'
|
||||
import { MMKVStorageKeys } from '../enums/mmkv-storage-keys'
|
||||
import uuid from 'react-native-uuid'
|
||||
import { JellifyLibrary } from '../types/JellifyLibrary'
|
||||
|
||||
export default class Client {
|
||||
static #instance: Client;
|
||||
static #instance: Client
|
||||
|
||||
private api : Api | undefined = undefined;
|
||||
private user : JellifyUser | undefined = undefined;
|
||||
private server : JellifyServer | undefined = undefined;
|
||||
private library : JellifyLibrary | undefined = undefined;
|
||||
private sessionId : string = uuid.v4();
|
||||
private api: Api | undefined = undefined
|
||||
private user: JellifyUser | undefined = undefined
|
||||
private server: JellifyServer | undefined = undefined
|
||||
private library: JellifyLibrary | undefined = undefined
|
||||
private sessionId: string = uuid.v4()
|
||||
|
||||
private constructor(
|
||||
api?: Api | undefined,
|
||||
user?: JellifyUser | undefined,
|
||||
server?: JellifyServer | undefined,
|
||||
library?: JellifyLibrary | undefined
|
||||
) {
|
||||
private constructor(
|
||||
api?: Api | undefined,
|
||||
user?: JellifyUser | undefined,
|
||||
server?: JellifyServer | undefined,
|
||||
library?: JellifyLibrary | undefined,
|
||||
) {
|
||||
const userJson = storage.getString(MMKVStorageKeys.User)
|
||||
const serverJson = storage.getString(MMKVStorageKeys.Server)
|
||||
const libraryJson = storage.getString(MMKVStorageKeys.Library)
|
||||
|
||||
const userJson = storage.getString(MMKVStorageKeys.User)
|
||||
const serverJson = storage.getString(MMKVStorageKeys.Server);
|
||||
const libraryJson = storage.getString(MMKVStorageKeys.Library);
|
||||
if (user) this.setAndPersistUser(user)
|
||||
else if (userJson) this.user = JSON.parse(userJson)
|
||||
else this.user = undefined
|
||||
|
||||
if (user)
|
||||
this.setAndPersistUser(user)
|
||||
else if (userJson)
|
||||
this.user = JSON.parse(userJson)
|
||||
else
|
||||
this.user = undefined;
|
||||
|
||||
if (server)
|
||||
this.setAndPersistServer(server)
|
||||
else if (serverJson)
|
||||
this.server = JSON.parse(serverJson);
|
||||
else
|
||||
this.server = undefined;
|
||||
|
||||
if (library)
|
||||
this.setAndPersistLibrary(library)
|
||||
else if (libraryJson)
|
||||
this.library = JSON.parse(libraryJson)
|
||||
else
|
||||
this.library = undefined;
|
||||
if (server) this.setAndPersistServer(server)
|
||||
else if (serverJson) this.server = JSON.parse(serverJson)
|
||||
else this.server = undefined
|
||||
|
||||
if (api)
|
||||
this.api = api
|
||||
else if (this.user && this.server)
|
||||
this.api = new Api(this.server.url, JellyfinInfo.clientInfo, JellyfinInfo.deviceInfo, this.user.accessToken);
|
||||
else
|
||||
this.api = undefined;
|
||||
}
|
||||
if (library) this.setAndPersistLibrary(library)
|
||||
else if (libraryJson) this.library = JSON.parse(libraryJson)
|
||||
else this.library = undefined
|
||||
|
||||
public static get instance(): Client {
|
||||
if (!Client.#instance) {
|
||||
Client.#instance = new Client();
|
||||
}
|
||||
if (api) this.api = api
|
||||
else if (this.user && this.server)
|
||||
this.api = new Api(
|
||||
this.server.url,
|
||||
JellyfinInfo.clientInfo,
|
||||
JellyfinInfo.deviceInfo,
|
||||
this.user.accessToken,
|
||||
)
|
||||
else this.api = undefined
|
||||
}
|
||||
|
||||
return Client.#instance;
|
||||
}
|
||||
public static get instance(): Client {
|
||||
if (!Client.#instance) {
|
||||
Client.#instance = new Client()
|
||||
}
|
||||
|
||||
public static get api(): Api | undefined {
|
||||
return Client.#instance.api;
|
||||
}
|
||||
return Client.#instance
|
||||
}
|
||||
|
||||
public static get server(): JellifyServer | undefined {
|
||||
return Client.#instance.server;
|
||||
}
|
||||
public static get api(): Api | undefined {
|
||||
return Client.#instance.api
|
||||
}
|
||||
|
||||
public static get user(): JellifyUser | undefined {
|
||||
return Client.#instance.user;
|
||||
}
|
||||
public static get server(): JellifyServer | undefined {
|
||||
return Client.#instance.server
|
||||
}
|
||||
|
||||
public static get library(): JellifyLibrary | undefined {
|
||||
return Client.#instance.library;
|
||||
}
|
||||
public static get user(): JellifyUser | undefined {
|
||||
return Client.#instance.user
|
||||
}
|
||||
|
||||
public static get sessionId(): string {
|
||||
return Client.#instance.sessionId;
|
||||
}
|
||||
public static get library(): JellifyLibrary | undefined {
|
||||
return Client.#instance.library
|
||||
}
|
||||
|
||||
public static signOut(): void {
|
||||
Client.#instance.removeCredentials()
|
||||
}
|
||||
public static get sessionId(): string {
|
||||
return Client.#instance.sessionId
|
||||
}
|
||||
|
||||
public static switchServer() : void {
|
||||
Client.#instance.removeServer();
|
||||
}
|
||||
public static signOut(): void {
|
||||
Client.#instance.removeCredentials()
|
||||
}
|
||||
|
||||
public static switchUser(): void {
|
||||
Client.#instance.removeUser();
|
||||
}
|
||||
public static switchServer(): void {
|
||||
Client.#instance.removeServer()
|
||||
}
|
||||
|
||||
public static setUser(user: JellifyUser): void {
|
||||
Client.#instance.setAndPersistUser(user);
|
||||
}
|
||||
public static switchUser(): void {
|
||||
Client.#instance.removeUser()
|
||||
}
|
||||
|
||||
private setAndPersistUser(user: JellifyUser) {
|
||||
this.user = user;
|
||||
public static setUser(user: JellifyUser): void {
|
||||
Client.#instance.setAndPersistUser(user)
|
||||
}
|
||||
|
||||
// persist user details
|
||||
storage.set(MMKVStorageKeys.User, JSON.stringify(user));
|
||||
}
|
||||
private setAndPersistUser(user: JellifyUser) {
|
||||
this.user = user
|
||||
|
||||
private setAndPersistServer(server : JellifyServer) {
|
||||
this.server = server;
|
||||
// persist user details
|
||||
storage.set(MMKVStorageKeys.User, JSON.stringify(user))
|
||||
}
|
||||
|
||||
storage.set(MMKVStorageKeys.Server, JSON.stringify(server));
|
||||
}
|
||||
private setAndPersistServer(server: JellifyServer) {
|
||||
this.server = server
|
||||
|
||||
private setAndPersistLibrary(library : JellifyLibrary) {
|
||||
this.library = library;
|
||||
storage.set(MMKVStorageKeys.Server, JSON.stringify(server))
|
||||
}
|
||||
|
||||
storage.set(MMKVStorageKeys.Library, JSON.stringify(library))
|
||||
}
|
||||
private setAndPersistLibrary(library: JellifyLibrary) {
|
||||
this.library = library
|
||||
|
||||
private removeCredentials() {
|
||||
this.library = undefined;
|
||||
this.server = undefined;
|
||||
this.user = undefined;
|
||||
storage.set(MMKVStorageKeys.Library, JSON.stringify(library))
|
||||
}
|
||||
|
||||
storage.delete(MMKVStorageKeys.Server)
|
||||
storage.delete(MMKVStorageKeys.Library)
|
||||
storage.delete(MMKVStorageKeys.User)
|
||||
}
|
||||
private removeCredentials() {
|
||||
this.library = undefined
|
||||
this.server = undefined
|
||||
this.user = undefined
|
||||
|
||||
private removeServer() {
|
||||
this.server = undefined;
|
||||
storage.delete(MMKVStorageKeys.Server)
|
||||
storage.delete(MMKVStorageKeys.Library)
|
||||
storage.delete(MMKVStorageKeys.User)
|
||||
}
|
||||
|
||||
storage.delete(MMKVStorageKeys.Server)
|
||||
}
|
||||
private removeServer() {
|
||||
this.server = undefined
|
||||
|
||||
private removeUser() {
|
||||
this.user = undefined;
|
||||
storage.delete(MMKVStorageKeys.Server)
|
||||
}
|
||||
|
||||
storage.delete(MMKVStorageKeys.User)
|
||||
}
|
||||
private removeUser() {
|
||||
this.user = undefined
|
||||
|
||||
/**
|
||||
* Uses the jellifyClient to create a public Jellyfin API instance.
|
||||
* @param serverUrl The URL of the Jellyfin server
|
||||
* @returns
|
||||
*/
|
||||
public static setPublicApiClient(server : JellifyServer) : void {
|
||||
const api = JellyfinInfo.createApi(server.url);
|
||||
storage.delete(MMKVStorageKeys.User)
|
||||
}
|
||||
|
||||
Client.#instance = new Client(api, undefined, server, undefined)
|
||||
}
|
||||
/**
|
||||
* Uses the jellifyClient to create a public Jellyfin API instance.
|
||||
* @param serverUrl The URL of the Jellyfin server
|
||||
* @returns
|
||||
*/
|
||||
public static setPublicApiClient(server: JellifyServer): void {
|
||||
const api = JellyfinInfo.createApi(server.url)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param serverUrl The URL of the Jellyfin server
|
||||
* @param accessToken The assigned accessToken for the Jellyfin user
|
||||
*/
|
||||
public static setPrivateApiClient(server : JellifyServer, user : JellifyUser) : void {
|
||||
const api = JellyfinInfo.createApi(server.url, user.accessToken);
|
||||
Client.#instance = new Client(api, undefined, server, undefined)
|
||||
}
|
||||
|
||||
Client.#instance = new Client(api, user, server, undefined);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param serverUrl The URL of the Jellyfin server
|
||||
* @param accessToken The assigned accessToken for the Jellyfin user
|
||||
*/
|
||||
public static setPrivateApiClient(server: JellifyServer, user: JellifyUser): void {
|
||||
const api = JellyfinInfo.createApi(server.url, user.accessToken)
|
||||
|
||||
public static setLibrary(library : JellifyLibrary) : void {
|
||||
Client.#instance = new Client(api, user, server, undefined)
|
||||
}
|
||||
|
||||
Client.#instance = new Client(undefined, undefined, undefined, library);
|
||||
}
|
||||
}
|
||||
public static setLibrary(library: JellifyLibrary): void {
|
||||
Client.#instance = new Client(undefined, undefined, undefined, library)
|
||||
}
|
||||
}
|
||||
|
||||
26
api/info.ts
26
api/info.ts
@@ -1,18 +1,18 @@
|
||||
import { Jellyfin } from "@jellyfin/sdk";
|
||||
import { getModel, getUniqueIdSync } from "react-native-device-info";
|
||||
import { name, version } from "../package.json"
|
||||
import { capitalize } from "lodash";
|
||||
import { Jellyfin } from '@jellyfin/sdk'
|
||||
import { getModel, getUniqueIdSync } from 'react-native-device-info'
|
||||
import { name, version } from '../package.json'
|
||||
import { capitalize } from 'lodash'
|
||||
|
||||
/**
|
||||
* Client object that represents Jellify on the Jellyfin server.
|
||||
*/
|
||||
export const JellyfinInfo: Jellyfin = new Jellyfin({
|
||||
clientInfo: {
|
||||
name: capitalize(name),
|
||||
version: version
|
||||
},
|
||||
deviceInfo: {
|
||||
name: getModel(),
|
||||
id: getUniqueIdSync()
|
||||
}
|
||||
});
|
||||
clientInfo: {
|
||||
name: capitalize(name),
|
||||
version: version,
|
||||
},
|
||||
deviceInfo: {
|
||||
name: getModel(),
|
||||
id: getUniqueIdSync(),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
import Client from "../../../api/client";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { isUndefined } from "lodash";
|
||||
import Client from '../../../api/client'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
/**
|
||||
* Manually marks an item as played.
|
||||
* This should only be used for non-tracks,
|
||||
* as those playbacks will be handled by the server
|
||||
*
|
||||
*
|
||||
* This is mainly used for marking playlists
|
||||
* and albums as played, so we can use Jellyfin
|
||||
* to fetch recent ones later on
|
||||
*
|
||||
*
|
||||
* @param item The item to mark as played
|
||||
*/
|
||||
export async function markItemPlayed(item: BaseItemDto) : Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
export async function markItemPlayed(item: BaseItemDto): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isUndefined(Client.api) || isUndefined(Client.user))
|
||||
return reject('Client instance not set')
|
||||
|
||||
if (isUndefined(Client.api) || isUndefined(Client.user))
|
||||
return reject("Client instance not set")
|
||||
|
||||
getItemsApi(Client.api)
|
||||
.updateItemUserData({
|
||||
itemId: item.Id!,
|
||||
userId: Client.user.id,
|
||||
updateUserItemDataDto: {
|
||||
LastPlayedDate: new Date().getUTCDate().toLocaleString(),
|
||||
Played: true,
|
||||
}
|
||||
})
|
||||
.then(({ data }) => {
|
||||
resolve()
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
getItemsApi(Client.api)
|
||||
.updateItemUserData({
|
||||
itemId: item.Id!,
|
||||
userId: Client.user.id,
|
||||
updateUserItemDataDto: {
|
||||
LastPlayedDate: new Date().getUTCDate().toLocaleString(),
|
||||
Played: true,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
resolve()
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,86 +1,74 @@
|
||||
import { BaseItemDto, MediaType } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import Client from "../../../api/client";
|
||||
import { getLibraryApi, getPlaylistsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { BaseItemDto, MediaType } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import Client from '../../../api/client'
|
||||
import { getLibraryApi, getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
|
||||
export async function addToPlaylist(track: BaseItemDto, playlist: BaseItemDto) {
|
||||
console.debug('Adding track to playlist')
|
||||
|
||||
console.debug("Adding track to playlist");
|
||||
|
||||
return getPlaylistsApi(Client.api!)
|
||||
.addItemToPlaylist({
|
||||
ids: [
|
||||
track.Id!
|
||||
],
|
||||
playlistId: playlist.Id!
|
||||
})
|
||||
return getPlaylistsApi(Client.api!).addItemToPlaylist({
|
||||
ids: [track.Id!],
|
||||
playlistId: playlist.Id!,
|
||||
})
|
||||
}
|
||||
|
||||
export async function removeFromPlaylist(track: BaseItemDto, playlist: BaseItemDto) {
|
||||
console.debug("Removing track from playlist");
|
||||
console.debug('Removing track from playlist')
|
||||
|
||||
return getPlaylistsApi(Client.api!)
|
||||
.removeItemFromPlaylist({
|
||||
playlistId: playlist.Id!,
|
||||
entryIds: [
|
||||
track.Id!
|
||||
]
|
||||
});
|
||||
return getPlaylistsApi(Client.api!).removeItemFromPlaylist({
|
||||
playlistId: playlist.Id!,
|
||||
entryIds: [track.Id!],
|
||||
})
|
||||
}
|
||||
|
||||
export async function reorderPlaylist(playlistId: string, itemId: string, to: number) {
|
||||
console.debug(`Moving track to index ${to}`)
|
||||
|
||||
console.debug(`Moving track to index ${to}`);
|
||||
|
||||
return getPlaylistsApi(Client.api!)
|
||||
.moveItem({
|
||||
playlistId,
|
||||
itemId,
|
||||
newIndex: to
|
||||
});
|
||||
return getPlaylistsApi(Client.api!).moveItem({
|
||||
playlistId,
|
||||
itemId,
|
||||
newIndex: to,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createPlaylist(name: string) {
|
||||
console.debug("Creating new playlist...");
|
||||
console.debug('Creating new playlist...')
|
||||
|
||||
return getPlaylistsApi(Client.api!)
|
||||
.createPlaylist({
|
||||
userId: Client.user!.id,
|
||||
mediaType: MediaType.Audio,
|
||||
createPlaylistDto: {
|
||||
Name: name,
|
||||
IsPublic: false,
|
||||
MediaType: MediaType.Audio
|
||||
}
|
||||
});
|
||||
return getPlaylistsApi(Client.api!).createPlaylist({
|
||||
userId: Client.user!.id,
|
||||
mediaType: MediaType.Audio,
|
||||
createPlaylistDto: {
|
||||
Name: name,
|
||||
IsPublic: false,
|
||||
MediaType: MediaType.Audio,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function deletePlaylist(playlistId: string) {
|
||||
console.debug("Deleting playlist...");
|
||||
console.debug('Deleting playlist...')
|
||||
|
||||
return getLibraryApi(Client.api!)
|
||||
.deleteItem({
|
||||
itemId: playlistId
|
||||
})
|
||||
return getLibraryApi(Client.api!).deleteItem({
|
||||
itemId: playlistId,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a Jellyfin playlist with the provided options.
|
||||
*
|
||||
*
|
||||
* Right now this just supports renaming playlists, but this will change
|
||||
* when it comes time for collaborative playlists
|
||||
*
|
||||
*
|
||||
* @param playlistId The Jellyfin ID of the playlist to update
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
export async function updatePlaylist(playlistId: string, name: string, trackIds: string[]) {
|
||||
console.debug("Updating playlist");
|
||||
console.debug('Updating playlist')
|
||||
|
||||
return getPlaylistsApi(Client.api!)
|
||||
.updatePlaylist({
|
||||
playlistId,
|
||||
updatePlaylistDto: {
|
||||
Name: name,
|
||||
Ids: trackIds
|
||||
}
|
||||
});
|
||||
}
|
||||
return getPlaylistsApi(Client.api!).updatePlaylist({
|
||||
playlistId,
|
||||
updatePlaylistDto: {
|
||||
Name: name,
|
||||
Ids: trackIds,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { QueryKeys } from "../enums/query-keys";
|
||||
import { createApi } from "./queries/functions/api";
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../enums/query-keys'
|
||||
import { createApi } from './queries/functions/api'
|
||||
|
||||
export const useApi = (serverUrl?: string, username?: string, password?: string, accessToken?: string) => useQuery({
|
||||
queryKey: [QueryKeys.Api, serverUrl, username, password, accessToken],
|
||||
queryFn: ({ queryKey }) => {
|
||||
export const useApi = (
|
||||
serverUrl?: string,
|
||||
username?: string,
|
||||
password?: string,
|
||||
accessToken?: string,
|
||||
): ReturnType<typeof useQuery> =>
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.Api, serverUrl, username, password, accessToken],
|
||||
queryFn: ({ queryKey }) => {
|
||||
const serverUrl: string | undefined = queryKey[1]
|
||||
const username: string | undefined = queryKey[2]
|
||||
const password: string | undefined = queryKey[3]
|
||||
const accessToken: string | undefined = queryKey[4]
|
||||
|
||||
const serverUrl : string | undefined = queryKey[1];
|
||||
const username : string | undefined = queryKey[2];
|
||||
const password : string | undefined = queryKey[3];
|
||||
const accessToken : string | undefined = queryKey[4];
|
||||
|
||||
return createApi(serverUrl, username, password, accessToken)
|
||||
},
|
||||
})
|
||||
return createApi(serverUrl, username, password, accessToken)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { QueryKeys } from "../../enums/query-keys"
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"
|
||||
import { BaseItemKind, ItemSortBy, SortOrder } from "@jellyfin/sdk/lib/generated-client/models"
|
||||
import Client from "../client"
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { BaseItemKind, ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import Client from '../client'
|
||||
|
||||
|
||||
|
||||
export const useArtistFeaturedOnAlbums = (artistId: string) => useQuery({
|
||||
queryKey: [QueryKeys.ArtistFeaturedAlbums, artistId],
|
||||
queryFn: ({ queryKey }) => {
|
||||
return getItemsApi(Client.api!).getItems({
|
||||
includeItemTypes: [BaseItemKind.MusicAlbum],
|
||||
recursive: true,
|
||||
excludeItemIds: [queryKey[1] as string],
|
||||
sortBy: [
|
||||
ItemSortBy.PremiereDate,
|
||||
ItemSortBy.ProductionYear,
|
||||
ItemSortBy.SortName
|
||||
],
|
||||
sortOrder: [ SortOrder.Descending ],
|
||||
contributingArtistIds: [queryKey[1] as string]
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.Items ? response.data.Items! : [];
|
||||
})
|
||||
}
|
||||
})
|
||||
export const useArtistFeaturedOnAlbums = (artistId: string) =>
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.ArtistFeaturedAlbums, artistId],
|
||||
queryFn: ({ queryKey }) => {
|
||||
return getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
includeItemTypes: [BaseItemKind.MusicAlbum],
|
||||
recursive: true,
|
||||
excludeItemIds: [queryKey[1] as string],
|
||||
sortBy: [
|
||||
ItemSortBy.PremiereDate,
|
||||
ItemSortBy.ProductionYear,
|
||||
ItemSortBy.SortName,
|
||||
],
|
||||
sortOrder: [SortOrder.Descending],
|
||||
contributingArtistIds: [queryKey[1] as string],
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.Items ? response.data.Items! : []
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { JellyfinInfo } from "../../info";
|
||||
import _ from "lodash";
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { JellyfinInfo } from '../../info'
|
||||
import _ from 'lodash'
|
||||
|
||||
export function createApi(serverUrl?: string, username?: string, password?: string, accessToken?: string): Promise<Api> {
|
||||
return new Promise((resolve, reject) => {
|
||||
export function createApi(
|
||||
serverUrl?: string,
|
||||
username?: string,
|
||||
password?: string,
|
||||
accessToken?: string,
|
||||
): Promise<Api> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (_.isUndefined(serverUrl)) {
|
||||
console.info("Server Url doesn't exist yet")
|
||||
return reject("Server Url doesn't exist")
|
||||
}
|
||||
|
||||
if (_.isUndefined(serverUrl)) {
|
||||
console.info("Server Url doesn't exist yet")
|
||||
return reject("Server Url doesn't exist");
|
||||
}
|
||||
if (!_.isUndefined(accessToken)) {
|
||||
console.info('Creating API with accessToken')
|
||||
return resolve(JellyfinInfo.createApi(serverUrl, accessToken))
|
||||
}
|
||||
|
||||
if (!_.isUndefined(accessToken)) {
|
||||
console.info("Creating API with accessToken")
|
||||
return resolve(JellyfinInfo.createApi(serverUrl, accessToken));
|
||||
}
|
||||
|
||||
if (_.isUndefined(username) && _.isUndefined(password)) {
|
||||
console.info('Creating public API for server url')
|
||||
return resolve(JellyfinInfo.createApi(serverUrl))
|
||||
}
|
||||
|
||||
if (_.isUndefined(username) && _.isUndefined(password)) {
|
||||
|
||||
console.info("Creating public API for server url")
|
||||
return resolve(JellyfinInfo.createApi(serverUrl));
|
||||
}
|
||||
|
||||
JellyfinInfo.createApi(serverUrl).authenticateUserByName(username!, password)
|
||||
.then(({ data }) => {
|
||||
if (data.AccessToken)
|
||||
return resolve(JellyfinInfo.createApi(serverUrl, data.AccessToken));
|
||||
|
||||
else
|
||||
return reject("Unable to sign in");
|
||||
});
|
||||
});
|
||||
}
|
||||
JellyfinInfo.createApi(serverUrl)
|
||||
.authenticateUserByName(username!, password)
|
||||
.then(({ data }) => {
|
||||
if (data.AccessToken)
|
||||
return resolve(JellyfinInfo.createApi(serverUrl, data.AccessToken))
|
||||
else return reject('Unable to sign in')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { Dirs, FileSystem } from "react-native-file-access";
|
||||
import Client from "../../../api/client";
|
||||
import { getLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { Dirs, FileSystem } from 'react-native-file-access'
|
||||
import Client from '../../../api/client'
|
||||
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
|
||||
export async function downloadTrack(itemId: string) : Promise<void> {
|
||||
export async function downloadTrack(itemId: string): Promise<void> {
|
||||
// Make sure downloads folder exists, create if it doesn't
|
||||
if (!(await FileSystem.exists(`${Dirs.DocumentDir}/downloads`)))
|
||||
await FileSystem.mkdir(`${Dirs.DocumentDir}/downloads`)
|
||||
|
||||
// Make sure downloads folder exists, create if it doesn't
|
||||
if (!(await FileSystem.exists(`${Dirs.DocumentDir}/downloads`)))
|
||||
await FileSystem.mkdir(`${Dirs.DocumentDir}/downloads`)
|
||||
|
||||
getLibraryApi(Client.api!)
|
||||
.getDownload({
|
||||
itemId
|
||||
}, {
|
||||
'responseType': 'blob'
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.status < 300) {
|
||||
await FileSystem.writeFile(getTrackFilePath(itemId), response.data)
|
||||
}
|
||||
})
|
||||
getLibraryApi(Client.api!)
|
||||
.getDownload(
|
||||
{
|
||||
itemId,
|
||||
},
|
||||
{
|
||||
responseType: 'blob',
|
||||
},
|
||||
)
|
||||
.then(async (response) => {
|
||||
if (response.status < 300) {
|
||||
await FileSystem.writeFile(getTrackFilePath(itemId), response.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getTrackFilePath(itemId: string) {
|
||||
return `${Dirs.DocumentDir}/downloads/${itemId}`
|
||||
}
|
||||
return `${Dirs.DocumentDir}/downloads/${itemId}`
|
||||
}
|
||||
|
||||
@@ -1,155 +1,117 @@
|
||||
import Client from "../../client";
|
||||
import { BaseItemDto, BaseItemKind, ItemSortBy, SortOrder, UserItemDataDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from '../../client'
|
||||
import {
|
||||
BaseItemDto,
|
||||
BaseItemKind,
|
||||
ItemSortBy,
|
||||
SortOrder,
|
||||
UserItemDataDto,
|
||||
} from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
|
||||
export function fetchFavoriteArtists(): Promise<BaseItemDto[]> {
|
||||
console.debug(`Fetching user's favorite artists`);
|
||||
export async function fetchFavoriteArtists(): Promise<BaseItemDto[]> {
|
||||
console.debug(`Fetching user's favorite artists`)
|
||||
return await getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
includeItemTypes: [BaseItemKind.MusicArtist],
|
||||
isFavorite: true,
|
||||
parentId: Client.library!.musicLibraryId,
|
||||
recursive: true,
|
||||
sortBy: [ItemSortBy.SortName],
|
||||
sortOrder: [SortOrder.Ascending],
|
||||
})
|
||||
.then((response) => {
|
||||
console.debug(`Received favorite artist response`, response)
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
includeItemTypes: [
|
||||
BaseItemKind.MusicArtist
|
||||
],
|
||||
isFavorite: true,
|
||||
parentId: Client.library!.musicLibraryId,
|
||||
recursive: true,
|
||||
sortBy: [
|
||||
ItemSortBy.SortName
|
||||
],
|
||||
sortOrder: [
|
||||
SortOrder.Ascending
|
||||
]
|
||||
})
|
||||
.then((response) => {
|
||||
console.debug(`Received favorite artist response`, response);
|
||||
|
||||
if (response.data.Items)
|
||||
resolve(response.data.Items)
|
||||
else
|
||||
resolve([]);
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
reject(error);
|
||||
})
|
||||
})
|
||||
if (response.data.Items) return response.data.Items
|
||||
else return []
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchFavoriteAlbums(): Promise<BaseItemDto[]> {
|
||||
console.debug(`Fetching user's favorite albums`);
|
||||
export async function fetchFavoriteAlbums(): Promise<BaseItemDto[]> {
|
||||
console.debug(`Fetching user's favorite albums`)
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
includeItemTypes: [
|
||||
BaseItemKind.MusicAlbum
|
||||
],
|
||||
isFavorite: true,
|
||||
parentId: Client.library!.musicLibraryId!,
|
||||
recursive: true,
|
||||
sortBy: [
|
||||
ItemSortBy.DatePlayed,
|
||||
ItemSortBy.SortName
|
||||
],
|
||||
sortOrder: [
|
||||
SortOrder.Descending,
|
||||
SortOrder.Ascending,
|
||||
]
|
||||
})
|
||||
.then((response) => {
|
||||
console.debug(`Received favorite album response`, response);
|
||||
return await getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
includeItemTypes: [BaseItemKind.MusicAlbum],
|
||||
isFavorite: true,
|
||||
parentId: Client.library!.musicLibraryId!,
|
||||
recursive: true,
|
||||
sortBy: [ItemSortBy.DatePlayed, ItemSortBy.SortName],
|
||||
sortOrder: [SortOrder.Descending, SortOrder.Ascending],
|
||||
})
|
||||
.then((response) => {
|
||||
console.debug(`Received favorite album response`, response)
|
||||
|
||||
if (response.data.Items)
|
||||
resolve(response.data.Items)
|
||||
else
|
||||
resolve([]);
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
reject(error);
|
||||
})
|
||||
})
|
||||
if (response.data.Items) return response.data.Items
|
||||
else return []
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchFavoritePlaylists(): Promise<BaseItemDto[]> {
|
||||
console.debug(`Fetching user's favorite playlists`);
|
||||
export async 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) => {
|
||||
|
||||
console.log(response);
|
||||
|
||||
if (response.data.Items)
|
||||
resolve(response.data.Items.filter(item =>
|
||||
item.UserData?.IsFavorite ||
|
||||
item.Path?.includes("/data/playlists")
|
||||
))
|
||||
else
|
||||
resolve([])
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
return await getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
userId: Client.user!.id,
|
||||
parentId: Client.library!.playlistLibraryId,
|
||||
fields: ['Path'],
|
||||
sortBy: [ItemSortBy.SortName],
|
||||
sortOrder: [SortOrder.Ascending],
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response)
|
||||
if (response.data.Items)
|
||||
return response.data.Items.filter(
|
||||
(item) => item.UserData?.IsFavorite || item.Path?.includes('/data/playlists'),
|
||||
)
|
||||
else return []
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchFavoriteTracks(): Promise<BaseItemDto[]> {
|
||||
console.debug(`Fetching user's favorite tracks`);
|
||||
export async function fetchFavoriteTracks(): Promise<BaseItemDto[]> {
|
||||
console.debug(`Fetching user's favorite tracks`)
|
||||
return await getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
includeItemTypes: [BaseItemKind.Audio],
|
||||
isFavorite: true,
|
||||
parentId: Client.library!.musicLibraryId,
|
||||
recursive: true,
|
||||
sortBy: [ItemSortBy.SortName],
|
||||
sortOrder: [SortOrder.Ascending],
|
||||
})
|
||||
.then((response) => {
|
||||
console.debug(`Received favorite artist response`, response)
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
includeItemTypes: [
|
||||
BaseItemKind.Audio
|
||||
],
|
||||
isFavorite: true,
|
||||
parentId: Client.library!.musicLibraryId,
|
||||
recursive: true,
|
||||
sortBy: [
|
||||
ItemSortBy.SortName
|
||||
],
|
||||
sortOrder: [
|
||||
SortOrder.Ascending
|
||||
]
|
||||
})
|
||||
.then((response) => {
|
||||
console.debug(`Received favorite artist response`, response);
|
||||
|
||||
if (response.data.Items)
|
||||
resolve(response.data.Items)
|
||||
else
|
||||
resolve([]);
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
reject(error);
|
||||
})
|
||||
})
|
||||
if (response.data.Items) return response.data.Items
|
||||
else return []
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUserData(itemId: string): Promise<UserItemDataDto> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
getItemsApi(Client.api!)
|
||||
.getItemUserData({
|
||||
itemId
|
||||
}).then((response) => {
|
||||
resolve(response.data)
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
reject(error);
|
||||
})
|
||||
});
|
||||
}
|
||||
export async function fetchUserData(itemId: string): Promise<UserItemDataDto | void> {
|
||||
return await getItemsApi(Client.api!)
|
||||
.getItemUserData({
|
||||
itemId,
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,46 +1,43 @@
|
||||
import { ImageFormat, ImageType } from "@jellyfin/sdk/lib/generated-client/models"
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api"
|
||||
import _ from "lodash"
|
||||
import Client from "../../../api/client"
|
||||
import { ImageFormat, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import _ from 'lodash'
|
||||
import Client from '../../../api/client'
|
||||
|
||||
export function fetchItemImage(itemId: string, imageType: ImageType, width: number, height: number) {
|
||||
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
console.debug("Fetching item image");
|
||||
|
||||
if (!Client.api)
|
||||
return reject("Client instance not set")
|
||||
else
|
||||
getImageApi(Client.api)
|
||||
.getItemImage({
|
||||
itemId,
|
||||
imageType,
|
||||
width: Math.ceil(width / 100) * 100 * 2, // Round to the nearest 100 for simplicity and to avoid
|
||||
height: Math.ceil(height / 100) * 100 * 2, // redundant images in storage, then double it to make sure it's crispy
|
||||
format: ImageFormat.Png
|
||||
},
|
||||
{
|
||||
responseType: 'blob',
|
||||
})
|
||||
.then(async (response) => {
|
||||
|
||||
if (response.status < 300) {
|
||||
|
||||
return resolve(await blobToBase64(response.data))
|
||||
} else {
|
||||
return reject("Invalid image response");
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
reject(error);
|
||||
})
|
||||
});
|
||||
export async function fetchItemImage(
|
||||
itemId: string,
|
||||
imageType: ImageType,
|
||||
width: number,
|
||||
height: number,
|
||||
) {
|
||||
console.debug('Fetching item image')
|
||||
if (!Client.api) return console.error('Client instance not set')
|
||||
try {
|
||||
const response = await getImageApi(Client.api).getItemImage(
|
||||
{
|
||||
itemId,
|
||||
imageType,
|
||||
width: Math.ceil(width / 100) * 100 * 2, // Round to the nearest 100 for simplicity and to avoid
|
||||
height: Math.ceil(height / 100) * 100 * 2, // redundant images in storage, then double it to make sure it's crispy
|
||||
format: ImageFormat.Png,
|
||||
},
|
||||
{
|
||||
responseType: 'blob',
|
||||
},
|
||||
)
|
||||
if (response.status < 300) {
|
||||
return await blobToBase64(response.data)
|
||||
} else {
|
||||
return console.error('Invalid image response')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
function blobToBase64(blob : Blob) {
|
||||
return new Promise<string>((resolve, _) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
function blobToBase64(blob: Blob) {
|
||||
return new Promise<string>((resolve, _) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => resolve(reader.result as string)
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import Client from "../../../api/client";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { isEmpty } from "lodash";
|
||||
import Client from '../../../api/client'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
export async function fetchItem(itemId: string) : Promise<BaseItemDto> {
|
||||
return new Promise((resolve, reject) => {
|
||||
export async function fetchItem(itemId: string): Promise<BaseItemDto> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isEmpty(itemId)) reject('No item ID proviced')
|
||||
|
||||
if (isEmpty(itemId))
|
||||
reject("No item ID proviced")
|
||||
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
ids: [
|
||||
itemId
|
||||
]
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items && response.data.TotalRecordCount == 1)
|
||||
resolve(response.data.Items[0])
|
||||
else
|
||||
reject(`${response.data.TotalRecordCount} items returned for ID`);
|
||||
})
|
||||
});
|
||||
}
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
ids: [itemId],
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items && response.data.TotalRecordCount == 1)
|
||||
resolve(response.data.Items[0])
|
||||
else reject(`${response.data.TotalRecordCount} items returned for ID`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,76 +1,67 @@
|
||||
import Client from "../../client";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getUserViewsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api/items-api";
|
||||
import { isUndefined } from "lodash";
|
||||
import Client from '../../client'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getUserViewsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
export async function fetchMusicLibraries(): Promise<BaseItemDto[] | void> {
|
||||
console.debug('Fetching music libraries from Jellyfin')
|
||||
|
||||
export function fetchMusicLibraries(): Promise<BaseItemDto[]> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
console.debug("Fetching music libraries from Jellyfin");
|
||||
|
||||
const libraries = await getItemsApi(Client.api!).getItems({
|
||||
includeItemTypes: ['CollectionFolder']
|
||||
});
|
||||
const libraries = await getItemsApi(Client.api!).getItems({
|
||||
includeItemTypes: ['CollectionFolder'],
|
||||
})
|
||||
|
||||
if (isUndefined(libraries.data.Items)) {
|
||||
console.warn("No libraries found on Jellyfin");
|
||||
return reject("No libraries found on Jellyfin");
|
||||
}
|
||||
if (isUndefined(libraries.data.Items)) {
|
||||
console.warn('No libraries found on Jellyfin')
|
||||
return
|
||||
}
|
||||
|
||||
const musicLibraries = libraries.data.Items!.filter(library =>
|
||||
library.CollectionType == 'music');
|
||||
|
||||
return resolve(musicLibraries);
|
||||
});
|
||||
const musicLibraries = libraries.data.Items!.filter(
|
||||
(library) => library.CollectionType == 'music',
|
||||
)
|
||||
|
||||
return musicLibraries
|
||||
}
|
||||
|
||||
export function fetchPlaylistLibrary(): Promise<BaseItemDto> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
console.debug("Fetching playlist library from Jellyfin");
|
||||
|
||||
const libraries = await getItemsApi(Client.api!).getItems({
|
||||
includeItemTypes: ['ManualPlaylistsFolder'],
|
||||
excludeItemTypes: ['CollectionFolder']
|
||||
});
|
||||
export async function fetchPlaylistLibrary(): Promise<BaseItemDto | void> {
|
||||
console.debug('Fetching playlist library from Jellyfin')
|
||||
|
||||
if (isUndefined(libraries.data.Items)) {
|
||||
console.warn("No playlist libraries found on Jellyfin");
|
||||
return reject("No playlist libraries found on Jellyfin");
|
||||
}
|
||||
const libraries = await getItemsApi(Client.api!).getItems({
|
||||
includeItemTypes: ['ManualPlaylistsFolder'],
|
||||
excludeItemTypes: ['CollectionFolder'],
|
||||
})
|
||||
|
||||
console.debug("Playlist libraries", libraries.data.Items!)
|
||||
if (isUndefined(libraries.data.Items)) {
|
||||
console.warn('No playlist libraries found on Jellyfin')
|
||||
return
|
||||
}
|
||||
|
||||
const playlistLibrary = libraries.data.Items!.filter(library =>
|
||||
library.CollectionType == 'playlists'
|
||||
)[0];
|
||||
console.debug('Playlist libraries', libraries.data.Items!)
|
||||
|
||||
if (isUndefined(playlistLibrary)) {
|
||||
console.warn("Playlist libary does not exist on server");
|
||||
return reject("Playlist library does not exist on server");
|
||||
}
|
||||
|
||||
return resolve(playlistLibrary);
|
||||
})
|
||||
const playlistLibrary = libraries.data.Items!.filter(
|
||||
(library) => library.CollectionType == 'playlists',
|
||||
)[0]
|
||||
|
||||
if (isUndefined(playlistLibrary)) {
|
||||
console.warn('Playlist libary does not exist on server')
|
||||
return
|
||||
}
|
||||
|
||||
return playlistLibrary
|
||||
}
|
||||
|
||||
export function fetchUserViews() : Promise<BaseItemDto[]> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
console.debug("Fetching user views")
|
||||
export async function fetchUserViews(): Promise<BaseItemDto[] | void> {
|
||||
console.debug('Fetching user views')
|
||||
|
||||
getUserViewsApi(Client.api!)
|
||||
.getUserViews({
|
||||
userId: Client.user!.id
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items)
|
||||
resolve(response.data.Items)
|
||||
|
||||
else
|
||||
resolve([])
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
});
|
||||
}
|
||||
return await getUserViewsApi(Client.api!)
|
||||
.getUserViews({
|
||||
userId: Client.user!.id,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items) return response.data.Items
|
||||
else return []
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,75 +1,57 @@
|
||||
import Client from "../../client";
|
||||
import { BaseItemDto, ItemSortBy, SortOrder } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from '../../client'
|
||||
import { BaseItemDto, ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
|
||||
export function fetchUserPlaylists(
|
||||
sortBy: ItemSortBy[] = []
|
||||
): Promise<BaseItemDto[]> {
|
||||
console.debug(`Fetching user playlists ${sortBy.length > 0 ? "sorting by " + sortBy.toString() : ""}`);
|
||||
export async function fetchUserPlaylists(sortBy: ItemSortBy[] = []): Promise<BaseItemDto[] | void> {
|
||||
console.debug(
|
||||
`Fetching user playlists ${sortBy.length > 0 ? 'sorting by ' + sortBy.toString() : ''}`,
|
||||
)
|
||||
|
||||
const defaultSorting : ItemSortBy[] = [
|
||||
ItemSortBy.IsFolder,
|
||||
ItemSortBy.SortName,
|
||||
]
|
||||
const defaultSorting: ItemSortBy[] = [ItemSortBy.IsFolder, ItemSortBy.SortName]
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
userId: Client.user!.id,
|
||||
parentId: Client.library!.playlistLibraryId!,
|
||||
fields: [
|
||||
"Path"
|
||||
],
|
||||
sortBy: sortBy.concat(defaultSorting),
|
||||
sortOrder: [
|
||||
SortOrder.Ascending
|
||||
]
|
||||
})
|
||||
.then((response) => {
|
||||
return await getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
userId: Client.user!.id,
|
||||
parentId: Client.library!.playlistLibraryId!,
|
||||
fields: ['Path'],
|
||||
sortBy: sortBy.concat(defaultSorting),
|
||||
sortOrder: [SortOrder.Ascending],
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response)
|
||||
|
||||
console.log(response);
|
||||
|
||||
if (response.data.Items)
|
||||
resolve(response.data.Items.filter(playlist =>
|
||||
playlist.Path?.includes("/data/playlists")
|
||||
))
|
||||
else
|
||||
resolve([]);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
if (response.data.Items)
|
||||
return response.data.Items.filter((playlist) =>
|
||||
playlist.Path?.includes('/data/playlists'),
|
||||
)
|
||||
else return []
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchPublicPlaylists(): Promise<BaseItemDto[]> {
|
||||
console.debug("Fetching public playlists");
|
||||
export async function fetchPublicPlaylists(): Promise<BaseItemDto[]> {
|
||||
console.debug('Fetching public playlists')
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
parentId: Client.library!.playlistLibraryId!,
|
||||
sortBy: [
|
||||
ItemSortBy.IsFolder,
|
||||
ItemSortBy.SortName
|
||||
],
|
||||
sortOrder: [
|
||||
SortOrder.Ascending
|
||||
]
|
||||
})
|
||||
.then((response) => {
|
||||
return await getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
parentId: Client.library!.playlistLibraryId!,
|
||||
sortBy: [ItemSortBy.IsFolder, ItemSortBy.SortName],
|
||||
sortOrder: [SortOrder.Ascending],
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response)
|
||||
|
||||
console.log(response);
|
||||
|
||||
if (response.data.Items)
|
||||
resolve(response.data.Items.filter(playlist => !playlist.Path?.includes("/data/playlists")))
|
||||
else
|
||||
resolve([]);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
if (response.data.Items)
|
||||
return response.data.Items.filter(
|
||||
(playlist) => !playlist.Path?.includes('/data/playlists'),
|
||||
)
|
||||
else return []
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,76 +1,72 @@
|
||||
import { BaseItemDto, BaseItemKind, ItemSortBy, SortOrder } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api/items-api";
|
||||
import { QueryConfig } from "../query.config";
|
||||
import Client from "../../client";
|
||||
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import {
|
||||
BaseItemDto,
|
||||
BaseItemKind,
|
||||
ItemSortBy,
|
||||
SortOrder,
|
||||
} from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'
|
||||
import { QueryConfig } from '../query.config'
|
||||
import Client from '../../client'
|
||||
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
|
||||
export function fetchRecentlyAdded(limit: number = QueryConfig.limits.recents, offset?: number | undefined) : Promise<BaseItemDto[]> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
if (!Client.api)
|
||||
return reject("Client not set")
|
||||
|
||||
if (!Client.library)
|
||||
return reject("Library not set")
|
||||
else
|
||||
getUserLibraryApi(Client.api)
|
||||
.getLatestMedia({
|
||||
parentId: Client.library.musicLibraryId,
|
||||
limit,
|
||||
|
||||
})
|
||||
.then(({ data }) => {
|
||||
resolve(offset ? data.slice(offset, data.length - 1) : data);
|
||||
});
|
||||
})
|
||||
export async function fetchRecentlyAdded(
|
||||
limit: number = QueryConfig.limits.recents,
|
||||
offset?: number | undefined,
|
||||
): Promise<BaseItemDto[]> {
|
||||
if (!Client.api) {
|
||||
console.error('Client not set')
|
||||
return []
|
||||
}
|
||||
if (!Client.library) return []
|
||||
return await getUserLibraryApi(Client.api)
|
||||
.getLatestMedia({
|
||||
parentId: Client.library.musicLibraryId,
|
||||
limit,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
return offset ? data.slice(offset, data.length - 1) : data
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchRecentlyPlayed(limit: number = QueryConfig.limits.recents, offset?: number | undefined): Promise<BaseItemDto[]> {
|
||||
export async function fetchRecentlyPlayed(
|
||||
limit: number = QueryConfig.limits.recents,
|
||||
offset?: number | undefined,
|
||||
): Promise<BaseItemDto[]> {
|
||||
console.debug('Fetching recently played items')
|
||||
|
||||
console.debug("Fetching recently played items");
|
||||
return await getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
includeItemTypes: [BaseItemKind.Audio],
|
||||
startIndex: offset,
|
||||
limit,
|
||||
parentId: Client.library!.musicLibraryId,
|
||||
recursive: true,
|
||||
sortBy: [ItemSortBy.DatePlayed],
|
||||
sortOrder: [SortOrder.Descending],
|
||||
})
|
||||
.then((response) => {
|
||||
console.debug('Received recently played items response')
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
includeItemTypes: [
|
||||
BaseItemKind.Audio
|
||||
],
|
||||
startIndex: offset,
|
||||
limit,
|
||||
parentId: Client.library!.musicLibraryId,
|
||||
recursive: true,
|
||||
sortBy: [
|
||||
ItemSortBy.DatePlayed
|
||||
],
|
||||
sortOrder: [
|
||||
SortOrder.Descending
|
||||
],
|
||||
})
|
||||
.then((response) => {
|
||||
|
||||
console.debug("Received recently played items response");
|
||||
|
||||
if (response.data.Items)
|
||||
resolve(response.data.Items);
|
||||
else
|
||||
resolve([]);
|
||||
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
reject(error);
|
||||
})
|
||||
})
|
||||
if (response.data.Items) return response.data.Items
|
||||
return []
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchRecentlyPlayedArtists(limit: number = QueryConfig.limits.recents, offset?: number | undefined) : Promise<BaseItemDto[]> {
|
||||
return fetchRecentlyPlayed(limit * 2, offset ? offset + 10 : undefined)
|
||||
.then((tracks) => {
|
||||
return getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
ids: tracks.map(track => track.ArtistItems![0].Id!)
|
||||
})
|
||||
.then((recentArtists) => {
|
||||
return recentArtists.data.Items!
|
||||
});
|
||||
});
|
||||
}
|
||||
export function fetchRecentlyPlayedArtists(
|
||||
limit: number = QueryConfig.limits.recents,
|
||||
offset?: number | undefined,
|
||||
): Promise<BaseItemDto[]> {
|
||||
return fetchRecentlyPlayed(limit * 2, offset ? offset + 10 : undefined).then((tracks) => {
|
||||
return getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
ids: tracks.map((track) => track.ArtistItems![0].Id!),
|
||||
})
|
||||
.then((recentArtists) => {
|
||||
return recentArtists.data.Items!
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Client from "../../../api/client";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { isEmpty, trim } from "lodash";
|
||||
import { QueryConfig } from "../query.config";
|
||||
import Client from '../../../api/client'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { isEmpty, trim } from 'lodash'
|
||||
import { QueryConfig } from '../query.config'
|
||||
|
||||
/**
|
||||
* Performs a search for items against the Jellyfin server, trimming whitespace
|
||||
@@ -10,40 +10,27 @@ import { QueryConfig } from "../query.config";
|
||||
* @param searchString The search term to look up against
|
||||
* @returns A promise of a BaseItemDto array, be it empty or not
|
||||
*/
|
||||
export async function fetchSearchResults(searchString: string | undefined) : Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
export async function fetchSearchResults(searchString: string | undefined): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.debug('Searching Jellyfin for items')
|
||||
|
||||
console.debug("Searching Jellyfin for items")
|
||||
if (isEmpty(searchString)) resolve([])
|
||||
|
||||
if (isEmpty(searchString))
|
||||
resolve([]);
|
||||
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
searchTerm: trim(searchString),
|
||||
recursive: true,
|
||||
includeItemTypes: [
|
||||
'MusicArtist',
|
||||
'Audio',
|
||||
'MusicAlbum',
|
||||
'Playlist'
|
||||
],
|
||||
limit: QueryConfig.limits.search,
|
||||
sortBy: [
|
||||
'IsFolder'
|
||||
],
|
||||
sortOrder: [
|
||||
'Descending'
|
||||
]
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items)
|
||||
resolve(response.data.Items)
|
||||
else
|
||||
resolve([]);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
});
|
||||
})
|
||||
}
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
searchTerm: trim(searchString),
|
||||
recursive: true,
|
||||
includeItemTypes: ['MusicArtist', 'Audio', 'MusicAlbum', 'Playlist'],
|
||||
limit: QueryConfig.limits.search,
|
||||
sortBy: ['IsFolder'],
|
||||
sortOrder: ['Descending'],
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items) resolve(response.data.Items)
|
||||
else resolve([])
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import Client from "../../../api/client";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from '../../../api/client'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
|
||||
export default function fetchSimilar(itemId : string, limit : number = 10, startIndex : number = 0) : Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if (!Client.api || !Client.user)
|
||||
reject("Client has not been set")
|
||||
|
||||
else
|
||||
getLibraryApi(Client.api)
|
||||
.getSimilarArtists({
|
||||
userId: Client.user.id,
|
||||
itemId: itemId,
|
||||
limit
|
||||
})
|
||||
.then(({ data }) => {
|
||||
resolve(data.Items ?? [])
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
export default function fetchSimilar(
|
||||
itemId: string,
|
||||
limit: number = 10,
|
||||
startIndex: number = 0,
|
||||
): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!Client.api || !Client.user) reject('Client has not been set')
|
||||
else
|
||||
getLibraryApi(Client.api)
|
||||
.getSimilarArtists({
|
||||
userId: Client.user.id,
|
||||
itemId: itemId,
|
||||
limit,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
resolve(data.Items ?? [])
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,107 +1,90 @@
|
||||
import { getItemsApi, getSuggestionsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from "../../../api/client";
|
||||
import { BaseItemDto, BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi, getSuggestionsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../../api/client'
|
||||
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
export async function fetchSearchSuggestions() : Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
recursive: true,
|
||||
limit: 10,
|
||||
includeItemTypes: [
|
||||
BaseItemKind.MusicArtist,
|
||||
BaseItemKind.Playlist,
|
||||
BaseItemKind.Audio,
|
||||
BaseItemKind.MusicAlbum
|
||||
],
|
||||
sortBy: [
|
||||
"IsFavoriteOrLiked",
|
||||
"Random"
|
||||
]
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.Items)
|
||||
resolve(data.Items);
|
||||
else
|
||||
resolve([]);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
})
|
||||
})
|
||||
export async function fetchSearchSuggestions(): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
recursive: true,
|
||||
limit: 10,
|
||||
includeItemTypes: [
|
||||
BaseItemKind.MusicArtist,
|
||||
BaseItemKind.Playlist,
|
||||
BaseItemKind.Audio,
|
||||
BaseItemKind.MusicAlbum,
|
||||
],
|
||||
sortBy: ['IsFavoriteOrLiked', 'Random'],
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.Items) resolve(data.Items)
|
||||
else resolve([])
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use Items API based functions instead of Suggestions API
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
export async function fetchSuggestedArtists() : Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getSuggestionsApi(Client.api!)
|
||||
.getSuggestions({
|
||||
userId: Client.user!.id,
|
||||
type: [
|
||||
'MusicArtist'
|
||||
]
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items)
|
||||
resolve(response.data.Items)
|
||||
else
|
||||
resolve([]);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
})
|
||||
})
|
||||
export async function fetchSuggestedArtists(): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getSuggestionsApi(Client.api!)
|
||||
.getSuggestions({
|
||||
userId: Client.user!.id,
|
||||
type: ['MusicArtist'],
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items) resolve(response.data.Items)
|
||||
else resolve([])
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use Items API based functions instead of Suggestions API
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
export async function fetchSuggestedAlbums() : Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getSuggestionsApi(Client.api!)
|
||||
.getSuggestions({
|
||||
userId: Client.user!.id,
|
||||
type: [
|
||||
'MusicAlbum'
|
||||
]
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items)
|
||||
resolve(response.data.Items)
|
||||
else
|
||||
resolve([]);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
})
|
||||
})
|
||||
export async function fetchSuggestedAlbums(): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getSuggestionsApi(Client.api!)
|
||||
.getSuggestions({
|
||||
userId: Client.user!.id,
|
||||
type: ['MusicAlbum'],
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items) resolve(response.data.Items)
|
||||
else resolve([])
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use Items API based functions instead of Suggestions API
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
export async function fetchSuggestedTracks() : Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getSuggestionsApi(Client.api!)
|
||||
.getSuggestions({
|
||||
userId: Client.user!.id,
|
||||
type: [
|
||||
'Audio'
|
||||
]
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items)
|
||||
resolve(response.data.Items)
|
||||
else
|
||||
resolve([]);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
})
|
||||
})
|
||||
}
|
||||
export async function fetchSuggestedTracks(): Promise<BaseItemDto[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getSuggestionsApi(Client.api!)
|
||||
.getSuggestions({
|
||||
userId: Client.user!.id,
|
||||
type: ['Audio'],
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.Items) resolve(response.data.Items)
|
||||
else resolve([])
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { ImageFormat } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { ImageFormat } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
export const QueryConfig = {
|
||||
limits: {
|
||||
recents: 20,
|
||||
search: 50, // TODO: make this a paginated search so limits don't even matter
|
||||
},
|
||||
images: {
|
||||
height: 300,
|
||||
width: 300,
|
||||
format: ImageFormat.Jpg
|
||||
},
|
||||
banners: {
|
||||
fillHeight: 300,
|
||||
fillWidth: 1000,
|
||||
format: ImageFormat.Jpg,
|
||||
},
|
||||
logos: {
|
||||
fillHeight: 50,
|
||||
fillWidth: 300,
|
||||
format: ImageFormat.Png
|
||||
},
|
||||
playerArtwork: {
|
||||
height: 1000,
|
||||
width: 1000,
|
||||
format: ImageFormat.Jpg
|
||||
},
|
||||
staleTime: {
|
||||
oneDay: 1000 * 60 * 60 * 24, // 1 Day
|
||||
oneWeek: 1000 * 60 * 60 * 24 * 7, // 7 Days
|
||||
oneFortnight: 1000 * 60 * 60 * 24 * 7 * 14 // 14 Days
|
||||
}
|
||||
}
|
||||
limits: {
|
||||
recents: 20,
|
||||
search: 50, // TODO: make this a paginated search so limits don't even matter
|
||||
},
|
||||
images: {
|
||||
height: 300,
|
||||
width: 300,
|
||||
format: ImageFormat.Jpg,
|
||||
},
|
||||
banners: {
|
||||
fillHeight: 300,
|
||||
fillWidth: 1000,
|
||||
format: ImageFormat.Jpg,
|
||||
},
|
||||
logos: {
|
||||
fillHeight: 50,
|
||||
fillWidth: 300,
|
||||
format: ImageFormat.Png,
|
||||
},
|
||||
playerArtwork: {
|
||||
height: 1000,
|
||||
width: 1000,
|
||||
format: ImageFormat.Jpg,
|
||||
},
|
||||
staleTime: {
|
||||
oneDay: 1000 * 60 * 60 * 24, // 1 Day
|
||||
oneWeek: 1000 * 60 * 60 * 24 * 7, // 7 Days
|
||||
oneFortnight: 1000 * 60 * 60 * 24 * 7 * 14, // 14 Days
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
|
||||
export class JellyfinCredentials {
|
||||
username: string;
|
||||
password?: string | undefined;
|
||||
accessToken?: string | undefined;
|
||||
username: string
|
||||
password?: string | undefined
|
||||
accessToken?: string | undefined
|
||||
|
||||
constructor(username: string, password?: string | undefined, accessToken?: string | undefined) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
}
|
||||
constructor(username: string, password?: string | undefined, accessToken?: string | undefined) {
|
||||
this.username = username
|
||||
this.password = password
|
||||
this.accessToken = accessToken
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,135 +1,119 @@
|
||||
import { HomeAlbumProps } from "../types";
|
||||
import { YStack, XStack, Separator, getToken } from "tamagui";
|
||||
import { ItemSortBy } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { H3, H5, Text } from "../Global/helpers/text";
|
||||
import { FlatList } from "react-native";
|
||||
import { RunTimeTicks } from "../Global/helpers/time-codes";
|
||||
import Track from "../Global/components/track";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import FavoriteButton from "../Global/components/favorite-button";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { getImageApi, getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from "../../api/client";
|
||||
import { useMemo } from "react";
|
||||
import { ItemCard } from "../Global/components/item-card";
|
||||
import { HomeAlbumProps } from '../types'
|
||||
import { YStack, XStack, Separator, getToken } from 'tamagui'
|
||||
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { H3, H5, Text } from '../Global/helpers/text'
|
||||
import { FlatList } from 'react-native'
|
||||
import { RunTimeTicks } from '../Global/helpers/time-codes'
|
||||
import Track from '../Global/components/track'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import FavoriteButton from '../Global/components/favorite-button'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { getImageApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../api/client'
|
||||
import { useMemo } from 'react'
|
||||
import { ItemCard } from '../Global/components/item-card'
|
||||
import { Image } from 'expo-image'
|
||||
|
||||
export function AlbumScreen({
|
||||
route,
|
||||
navigation
|
||||
} : HomeAlbumProps): React.JSX.Element {
|
||||
export function AlbumScreen({ route, navigation }: HomeAlbumProps): React.JSX.Element {
|
||||
const { album } = route.params
|
||||
|
||||
const { album } = route.params;
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
return <FavoriteButton item={album} />
|
||||
},
|
||||
})
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
return (
|
||||
<FavoriteButton item={album} />
|
||||
)
|
||||
}
|
||||
})
|
||||
const { width } = useSafeAreaFrame();
|
||||
const { data: tracks } = useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, album.Id!],
|
||||
queryFn: () => {
|
||||
let sortBy: ItemSortBy[] = []
|
||||
|
||||
const { data: tracks } = useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, album.Id!],
|
||||
queryFn: () => {
|
||||
sortBy = [ItemSortBy.ParentIndexNumber, ItemSortBy.IndexNumber, ItemSortBy.SortName]
|
||||
|
||||
let sortBy: ItemSortBy[] = [];
|
||||
return getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
parentId: album.Id!,
|
||||
sortBy,
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.Items ? response.data.Items! : []
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
sortBy = [
|
||||
ItemSortBy.ParentIndexNumber,
|
||||
ItemSortBy.IndexNumber,
|
||||
ItemSortBy.SortName
|
||||
]
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
data={tracks}
|
||||
keyExtractor={(item) => item.Id!}
|
||||
numColumns={1}
|
||||
ItemSeparatorComponent={() => <Separator />}
|
||||
ListHeaderComponent={useMemo(() => {
|
||||
return (
|
||||
<YStack
|
||||
alignItems='center'
|
||||
alignContent='center'
|
||||
marginTop={'$4'}
|
||||
minHeight={getToken('$20') + getToken('$15')}
|
||||
>
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(album.Id!)}
|
||||
style={{
|
||||
borderRadius: getToken('$5'),
|
||||
width: getToken('$20') + getToken('$15'),
|
||||
height: getToken('$20') + getToken('$15'),
|
||||
}}
|
||||
/>
|
||||
|
||||
return getItemsApi(Client.api!).getItems({
|
||||
parentId: album.Id!,
|
||||
sortBy
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.Items ? response.data.Items! : [];
|
||||
})
|
||||
},
|
||||
});
|
||||
<H5 textAlign='center'>{album.Name ?? 'Untitled Album'}</H5>
|
||||
<Text>{album.ProductionYear?.toString() ?? ''}</Text>
|
||||
</YStack>
|
||||
)
|
||||
}, [album])}
|
||||
renderItem={({ item: track, index }) => (
|
||||
<Track
|
||||
track={track}
|
||||
tracklist={tracks!}
|
||||
index={index}
|
||||
navigation={navigation}
|
||||
queue={album}
|
||||
/>
|
||||
)}
|
||||
ListFooterComponent={
|
||||
<YStack justifyContent='flex-start'>
|
||||
<XStack flex={1} marginTop={'$3'} justifyContent='flex-end'>
|
||||
<Text
|
||||
color={'$borderColor'}
|
||||
style={{ display: 'block' }}
|
||||
marginRight={'$1'}
|
||||
>
|
||||
Total Runtime:
|
||||
</Text>
|
||||
<RunTimeTicks>{album.RunTimeTicks}</RunTimeTicks>
|
||||
</XStack>
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
data={tracks}
|
||||
keyExtractor={(item) => item.Id!}
|
||||
numColumns={1}
|
||||
ItemSeparatorComponent={() => <Separator />}
|
||||
ListHeaderComponent={(
|
||||
useMemo(() => {
|
||||
return (
|
||||
<YStack
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
marginTop={"$4"}
|
||||
minHeight={getToken("$20") + getToken("$15")}
|
||||
>
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(album.Id!)}
|
||||
style={{
|
||||
borderRadius: getToken("$5"),
|
||||
width: getToken("$20") + getToken("$15"),
|
||||
height: getToken("$20") + getToken("$15")
|
||||
}}
|
||||
/>
|
||||
|
||||
<H5 textAlign="center">{ album.Name ?? "Untitled Album" }</H5>
|
||||
<Text>{ album.ProductionYear?.toString() ?? "" }</Text>
|
||||
</YStack>
|
||||
)
|
||||
}, [
|
||||
album
|
||||
])
|
||||
|
||||
)}
|
||||
renderItem={({ item: track, index }) =>
|
||||
<Track
|
||||
track={track}
|
||||
tracklist={tracks!}
|
||||
index={index}
|
||||
navigation={navigation}
|
||||
queue={album}
|
||||
/>
|
||||
}
|
||||
ListFooterComponent={(
|
||||
<YStack justifyContent="flex-start">
|
||||
<XStack flex={1} marginTop={"$3"} justifyContent="flex-end">
|
||||
<Text
|
||||
color={"$borderColor"}
|
||||
style={{ display: "block"}}
|
||||
marginRight={"$1"}
|
||||
>
|
||||
Total Runtime:
|
||||
</Text>
|
||||
<RunTimeTicks>{ album.RunTimeTicks }</RunTimeTicks>
|
||||
</XStack>
|
||||
|
||||
<H3>Album Artists</H3>
|
||||
<FlatList
|
||||
horizontal
|
||||
keyExtractor={(item) => item.Id!}
|
||||
data={album.ArtistItems}
|
||||
renderItem={({ index, item: artist }) =>
|
||||
<ItemCard
|
||||
width={100}
|
||||
item={artist}
|
||||
caption={artist.Name ?? "Unknown Artist"}
|
||||
onPress={() => {
|
||||
navigation.navigate("Artist", {
|
||||
artist
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<H3>Album Artists</H3>
|
||||
<FlatList
|
||||
horizontal
|
||||
keyExtractor={(item) => item.Id!}
|
||||
data={album.ArtistItems}
|
||||
renderItem={({ index, item: artist }) => (
|
||||
<ItemCard
|
||||
width={100}
|
||||
item={artist}
|
||||
caption={artist.Name ?? 'Unknown Artist'}
|
||||
onPress={() => {
|
||||
navigation.navigate('Artist', {
|
||||
artist,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</YStack>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,50 +1,48 @@
|
||||
import { AlbumsProps } from "../types";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import { ItemCard } from "../Global/components/item-card";
|
||||
import { FlatList, RefreshControl } from "react-native";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { fetchFavoriteAlbums } from "../../api/queries/functions/favorites";
|
||||
import { fetchRecentlyAdded } from "../../api/queries/functions/recents";
|
||||
import { QueryConfig } from "../../api/queries/query.config";
|
||||
import { AlbumsProps } from '../types'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { ItemCard } from '../Global/components/item-card'
|
||||
import { FlatList, RefreshControl } from 'react-native'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { fetchFavoriteAlbums } from '../../api/queries/functions/favorites'
|
||||
import { fetchRecentlyAdded } from '../../api/queries/functions/recents'
|
||||
import { QueryConfig } from '../../api/queries/query.config'
|
||||
|
||||
export default function Albums({ navigation, route }: AlbumsProps) : React.JSX.Element {
|
||||
export default function Albums({ navigation, route }: AlbumsProps): React.JSX.Element {
|
||||
const fetchRecentlyAddedAlbums = route.params.query === QueryKeys.RecentlyAdded
|
||||
|
||||
const fetchRecentlyAddedAlbums = route.params.query === QueryKeys.RecentlyAdded;
|
||||
const {
|
||||
data: albums,
|
||||
refetch,
|
||||
isPending,
|
||||
} = useQuery({
|
||||
queryKey: [route.params.query],
|
||||
queryFn: () =>
|
||||
fetchRecentlyAddedAlbums
|
||||
? fetchRecentlyAdded(QueryConfig.limits.recents * 4, QueryConfig.limits.recents)
|
||||
: fetchFavoriteAlbums(),
|
||||
})
|
||||
|
||||
const { data: albums, refetch, isPending } = useQuery({
|
||||
queryKey: [route.params.query],
|
||||
queryFn: () =>
|
||||
fetchRecentlyAddedAlbums
|
||||
? fetchRecentlyAdded(QueryConfig.limits.recents * 4, QueryConfig.limits.recents)
|
||||
: fetchFavoriteAlbums()
|
||||
});
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
numColumns={2}
|
||||
data={albums}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPending}
|
||||
onRefresh={refetch}
|
||||
/>
|
||||
}
|
||||
renderItem={({ index, item: album}) =>
|
||||
<ItemCard
|
||||
item={album}
|
||||
caption={album.Name ?? "Untitled Album"}
|
||||
subCaption={album.ProductionYear?.toString() ?? ""}
|
||||
squared
|
||||
onPress={() => {
|
||||
navigation.navigate("Album", { album })
|
||||
}}
|
||||
width={width / 2.1}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
numColumns={2}
|
||||
data={albums}
|
||||
refreshControl={<RefreshControl refreshing={isPending} onRefresh={refetch} />}
|
||||
renderItem={({ index, item: album }) => (
|
||||
<ItemCard
|
||||
item={album}
|
||||
caption={album.Name ?? 'Untitled Album'}
|
||||
subCaption={album.ProductionYear?.toString() ?? ''}
|
||||
squared
|
||||
onPress={() => {
|
||||
navigation.navigate('Album', { album })
|
||||
}}
|
||||
width={width / 2.1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React from "react"
|
||||
import { StackParamList } from "../types"
|
||||
import { NativeStackScreenProps } from "@react-navigation/native-stack"
|
||||
import Albums from "./component"
|
||||
import React from 'react'
|
||||
import { StackParamList } from '../types'
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
||||
import Albums from './component'
|
||||
|
||||
export default function AlbumsScreen(
|
||||
props: NativeStackScreenProps<StackParamList, 'Albums'>
|
||||
) : React.JSX.Element {
|
||||
return (
|
||||
<Albums {...props} />
|
||||
)
|
||||
}
|
||||
props: NativeStackScreenProps<StackParamList, 'Albums'>,
|
||||
): React.JSX.Element {
|
||||
return <Albums {...props} />
|
||||
}
|
||||
|
||||
@@ -1,131 +1,125 @@
|
||||
import { RouteProp } from "@react-navigation/native";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../types";
|
||||
import Client from "../../api/client";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { BaseItemKind, ItemSortBy, SortOrder } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getImageApi, getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { ScrollView, FlatList } from "react-native";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import { YStack } from "tamagui";
|
||||
import FavoriteButton from "../Global/components/favorite-button";
|
||||
import { ItemCard } from "../Global/components/item-card";
|
||||
import { H3 } from "../Global/helpers/text";
|
||||
import fetchSimilar from "../../api/queries/functions/similar";
|
||||
import { Image } from "expo-image";
|
||||
import { RouteProp } from '@react-navigation/native'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../types'
|
||||
import Client from '../../api/client'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { BaseItemKind, ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getImageApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { ScrollView, FlatList } from 'react-native'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { YStack } from 'tamagui'
|
||||
import FavoriteButton from '../Global/components/favorite-button'
|
||||
import { ItemCard } from '../Global/components/item-card'
|
||||
import { H3 } from '../Global/helpers/text'
|
||||
import fetchSimilar from '../../api/queries/functions/similar'
|
||||
import { Image } from 'expo-image'
|
||||
|
||||
export function ArtistScreen({
|
||||
route,
|
||||
navigation
|
||||
} : {
|
||||
route: RouteProp<StackParamList, "Artist">,
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
export function ArtistScreen({
|
||||
route,
|
||||
navigation,
|
||||
}: {
|
||||
route: RouteProp<StackParamList, 'Artist'>
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { artist } = route.params
|
||||
|
||||
const { artist } = route.params;
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
return <FavoriteButton item={artist} />
|
||||
},
|
||||
})
|
||||
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
return (
|
||||
<FavoriteButton item={artist} />
|
||||
)
|
||||
}
|
||||
});
|
||||
const [columns, setColumns] = useState<number>(2)
|
||||
|
||||
const [columns, setColumns] = useState<number>(2);
|
||||
const { height, width } = useSafeAreaFrame()
|
||||
|
||||
const { height, width } = useSafeAreaFrame();
|
||||
const bannerHeight = height / 6
|
||||
|
||||
const bannerHeight = height / 6;
|
||||
const { data: albums } = useQuery({
|
||||
queryKey: [QueryKeys.ArtistAlbums, artist.Id!],
|
||||
queryFn: ({ queryKey }) => {
|
||||
return getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
includeItemTypes: [BaseItemKind.MusicAlbum],
|
||||
recursive: true,
|
||||
excludeItemIds: [queryKey[1] as string],
|
||||
sortBy: [
|
||||
ItemSortBy.PremiereDate,
|
||||
ItemSortBy.ProductionYear,
|
||||
ItemSortBy.SortName,
|
||||
],
|
||||
sortOrder: [SortOrder.Descending],
|
||||
artistIds: [queryKey[1] as string],
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.Items ? response.data.Items! : []
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const { data: albums } = useQuery({
|
||||
queryKey: [QueryKeys.ArtistAlbums, artist.Id!],
|
||||
queryFn: ({ queryKey }) => {
|
||||
return getItemsApi(Client.api!).getItems({
|
||||
includeItemTypes: [BaseItemKind.MusicAlbum],
|
||||
recursive: true,
|
||||
excludeItemIds: [queryKey[1] as string],
|
||||
sortBy: [
|
||||
ItemSortBy.PremiereDate,
|
||||
ItemSortBy.ProductionYear,
|
||||
ItemSortBy.SortName
|
||||
],
|
||||
sortOrder: [SortOrder.Descending],
|
||||
artistIds: [queryKey[1] as string],
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.Items ? response.data.Items! : [];
|
||||
})
|
||||
}
|
||||
});
|
||||
const { data: similarArtists } = useQuery({
|
||||
queryKey: [QueryKeys.SimilarItems, artist.Id],
|
||||
queryFn: () => fetchSimilar(artist.Id!),
|
||||
})
|
||||
|
||||
const { data: similarArtists } = useQuery({
|
||||
queryKey: [QueryKeys.SimilarItems, artist.Id],
|
||||
queryFn: () => fetchSimilar(artist.Id!)
|
||||
})
|
||||
return (
|
||||
<ScrollView contentInsetAdjustmentBehavior='automatic' removeClippedSubviews>
|
||||
<YStack alignContent='center' justifyContent='center' minHeight={bannerHeight}>
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(artist.Id!)}
|
||||
style={{
|
||||
width: width,
|
||||
height: bannerHeight,
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
removeClippedSubviews
|
||||
>
|
||||
<YStack alignContent="center" justifyContent="center" minHeight={bannerHeight}>
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(artist.Id!)}
|
||||
style={{
|
||||
width: width,
|
||||
height: bannerHeight
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
<H3>Albums</H3>
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
data={albums}
|
||||
numColumns={columns} // TODO: Make this adjustable
|
||||
renderItem={({ item: album }) => (
|
||||
<ItemCard
|
||||
caption={album.Name}
|
||||
subCaption={album.ProductionYear?.toString()}
|
||||
size={'$14'}
|
||||
squared
|
||||
item={album}
|
||||
onPress={() => {
|
||||
navigation.navigate('Album', {
|
||||
album,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
ListFooterComponent={
|
||||
<YStack>
|
||||
<H3>{`Similar to ${artist.Name ?? 'Unknown Artist'}`} </H3>
|
||||
|
||||
<H3>Albums</H3>
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
alignItems: "center"
|
||||
}}
|
||||
data={albums}
|
||||
numColumns={columns} // TODO: Make this adjustable
|
||||
renderItem={({ item: album }) =>
|
||||
<ItemCard
|
||||
caption={album.Name}
|
||||
subCaption={album.ProductionYear?.toString()}
|
||||
size={"$14"}
|
||||
squared
|
||||
item={album}
|
||||
onPress={() => {
|
||||
navigation.navigate('Album', {
|
||||
album
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
ListFooterComponent={(
|
||||
<YStack>
|
||||
|
||||
<H3>{`Similar to ${artist.Name ?? 'Unknown Artist'}`} </H3>
|
||||
|
||||
<FlatList
|
||||
data={similarArtists}
|
||||
horizontal
|
||||
renderItem={({ item: artist }) => (
|
||||
<ItemCard
|
||||
caption={artist.Name ?? "Unknown Artist"}
|
||||
item={artist}
|
||||
onPress={() => {
|
||||
navigation.push('Artist', {
|
||||
artist
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</YStack>
|
||||
)}
|
||||
/>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
<FlatList
|
||||
data={similarArtists}
|
||||
horizontal
|
||||
renderItem={({ item: artist }) => (
|
||||
<ItemCard
|
||||
caption={artist.Name ?? 'Unknown Artist'}
|
||||
item={artist}
|
||||
onPress={() => {
|
||||
navigation.push('Artist', {
|
||||
artist,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</YStack>
|
||||
}
|
||||
/>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,54 +1,55 @@
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import React from "react";
|
||||
import { FlatList, RefreshControl } from "react-native";
|
||||
import { ItemCard } from "../Global/components/item-card";
|
||||
import { ArtistsProps } from "../types";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchRecentlyPlayedArtists } from "../../api/queries/functions/recents";
|
||||
import { fetchFavoriteArtists } from "../../api/queries/functions/favorites";
|
||||
import { QueryConfig } from "../../api/queries/query.config";
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import React from 'react'
|
||||
import { FlatList, RefreshControl } from 'react-native'
|
||||
import { ItemCard } from '../Global/components/item-card'
|
||||
import { ArtistsProps } from '../types'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchRecentlyPlayedArtists } from '../../api/queries/functions/recents'
|
||||
import { fetchFavoriteArtists } from '../../api/queries/functions/favorites'
|
||||
import { QueryConfig } from '../../api/queries/query.config'
|
||||
|
||||
export default function Artists({
|
||||
navigation,
|
||||
route
|
||||
}: ArtistsProps): React.JSX.Element {
|
||||
export default function Artists({ navigation, route }: ArtistsProps): React.JSX.Element {
|
||||
const {
|
||||
data: artists,
|
||||
refetch,
|
||||
isPending,
|
||||
} = route.params.query === QueryKeys.RecentlyPlayedArtists
|
||||
? useQuery({
|
||||
queryKey: [
|
||||
QueryKeys.RecentlyPlayedArtists,
|
||||
QueryConfig.limits.recents * 4,
|
||||
QueryConfig.limits.recents,
|
||||
],
|
||||
queryFn: () =>
|
||||
fetchRecentlyPlayedArtists(
|
||||
QueryConfig.limits.recents * 4,
|
||||
QueryConfig.limits.recents,
|
||||
),
|
||||
})
|
||||
: useQuery({
|
||||
queryKey: [QueryKeys.FavoriteArtists],
|
||||
queryFn: () => fetchFavoriteArtists(),
|
||||
})
|
||||
|
||||
const { data: artists, refetch, isPending } =
|
||||
route.params.query ===
|
||||
QueryKeys.RecentlyPlayedArtists ? useQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayedArtists, QueryConfig.limits.recents * 4, QueryConfig.limits.recents],
|
||||
queryFn: () => fetchRecentlyPlayedArtists(QueryConfig.limits.recents * 4, QueryConfig.limits.recents)
|
||||
}) :
|
||||
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.FavoriteArtists],
|
||||
queryFn: () => fetchFavoriteArtists()
|
||||
});
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
numColumns={2}
|
||||
data={artists}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPending}
|
||||
onRefresh={refetch}
|
||||
/>
|
||||
}
|
||||
renderItem={({ index, item: artist}) =>
|
||||
<ItemCard
|
||||
item={artist}
|
||||
caption={artist.Name ?? "Unknown Artist"}
|
||||
onPress={() => {
|
||||
navigation.navigate("Artist", { artist })
|
||||
}}
|
||||
width={width / 2.1}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
numColumns={2}
|
||||
data={artists}
|
||||
refreshControl={<RefreshControl refreshing={isPending} onRefresh={refetch} />}
|
||||
renderItem={({ index, item: artist }) => (
|
||||
<ItemCard
|
||||
item={artist}
|
||||
caption={artist.Name ?? 'Unknown Artist'}
|
||||
onPress={() => {
|
||||
navigation.navigate('Artist', { artist })
|
||||
}}
|
||||
width={width / 2.1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import React from "react"
|
||||
import { StackParamList } from "../types"
|
||||
import { RouteProp } from "@react-navigation/native"
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack"
|
||||
import Artists from "./component"
|
||||
import React from 'react'
|
||||
import { StackParamList } from '../types'
|
||||
import { RouteProp } from '@react-navigation/native'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import Artists from './component'
|
||||
|
||||
export default function ArtistsScreen({
|
||||
route,
|
||||
navigation
|
||||
} : {
|
||||
route: RouteProp<StackParamList, "Artists">,
|
||||
navigation: NativeStackNavigationProp<StackParamList, "Artists", undefined>
|
||||
}) : React.JSX.Element {
|
||||
return (
|
||||
<Artists route={route} navigation={navigation}/>
|
||||
)
|
||||
}
|
||||
route,
|
||||
navigation,
|
||||
}: {
|
||||
route: RouteProp<StackParamList, 'Artists'>
|
||||
navigation: NativeStackNavigationProp<StackParamList, 'Artists', undefined>
|
||||
}): React.JSX.Element {
|
||||
return <Artists route={route} navigation={navigation} />
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ListTemplate } from "react-native-carplay";
|
||||
import { ListTemplate } from 'react-native-carplay'
|
||||
|
||||
const CarPlayDiscover = new ListTemplate({
|
||||
tabTitle: "Discover"
|
||||
tabTitle: 'Discover',
|
||||
})
|
||||
|
||||
export default CarPlayDiscover;
|
||||
export default CarPlayDiscover
|
||||
|
||||
@@ -1,45 +1,48 @@
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import Client from "../../api/client";
|
||||
import { fetchRecentlyPlayed } from "../../api/queries/functions/recents";
|
||||
import { CarPlay, ListTemplate } from "react-native-carplay";
|
||||
import { queryClient } from "../../constants/query-client";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import ListItemTemplate from "./ListTemplate";
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import Client from '../../api/client'
|
||||
import { fetchRecentlyPlayed } from '../../api/queries/functions/recents'
|
||||
import { CarPlay, ListTemplate } from 'react-native-carplay'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import ListItemTemplate from './ListTemplate'
|
||||
|
||||
const CarPlayHome : ListTemplate = new ListTemplate({
|
||||
id: 'Home',
|
||||
title: "Home",
|
||||
tabTitle: "Home",
|
||||
sections: [
|
||||
{
|
||||
header: `Hi ${Client.user?.name ?? "there"}`,
|
||||
items: [
|
||||
{ id: QueryKeys.RecentlyPlayedArtists, text: 'Recent Artists' },
|
||||
{ id: QueryKeys.RecentlyPlayed, text: 'Recently Played'},
|
||||
{ id: QueryKeys.UserPlaylists, text: 'Your Playlists'}
|
||||
]
|
||||
}
|
||||
],
|
||||
onItemSelect: async ({ index }) => {
|
||||
const CarPlayHome: ListTemplate = new ListTemplate({
|
||||
id: 'Home',
|
||||
title: 'Home',
|
||||
tabTitle: 'Home',
|
||||
sections: [
|
||||
{
|
||||
header: `Hi ${Client.user?.name ?? 'there'}`,
|
||||
items: [
|
||||
{ id: QueryKeys.RecentlyPlayedArtists, text: 'Recent Artists' },
|
||||
{ id: QueryKeys.RecentlyPlayed, text: 'Recently Played' },
|
||||
{ id: QueryKeys.UserPlaylists, text: 'Your Playlists' },
|
||||
],
|
||||
},
|
||||
],
|
||||
onItemSelect: async ({ index }) => {
|
||||
console.debug(`Home item selected`)
|
||||
|
||||
console.debug(`Home item selected`);
|
||||
switch (index) {
|
||||
case 0: {
|
||||
const artists = queryClient.getQueryData<BaseItemDto[]>([
|
||||
QueryKeys.RecentlyPlayedArtists,
|
||||
])
|
||||
CarPlay.pushTemplate(ListItemTemplate(artists))
|
||||
break
|
||||
}
|
||||
case 1: {
|
||||
const tracks = await fetchRecentlyPlayed()
|
||||
CarPlay.pushTemplate(ListItemTemplate(tracks))
|
||||
break
|
||||
}
|
||||
case 2: {
|
||||
const playlists = queryClient.getQueryData<BaseItemDto[]>([QueryKeys.UserPlaylists])
|
||||
CarPlay.pushTemplate(ListItemTemplate(playlists))
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
const artists = queryClient.getQueryData<BaseItemDto[]>([QueryKeys.RecentlyPlayedArtists]);
|
||||
CarPlay.pushTemplate(ListItemTemplate(artists))
|
||||
break;
|
||||
case 1:
|
||||
const tracks = await fetchRecentlyPlayed()
|
||||
CarPlay.pushTemplate(ListItemTemplate(tracks))
|
||||
break;
|
||||
case 2:
|
||||
const playlists = queryClient.getQueryData<BaseItemDto[]>([QueryKeys.UserPlaylists])
|
||||
CarPlay.pushTemplate(ListItemTemplate(playlists))
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default CarPlayHome;
|
||||
export default CarPlayHome
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import { queryClient } from "../../constants/query-client";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { ListTemplate } from "react-native-carplay";
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { ListTemplate } from 'react-native-carplay'
|
||||
|
||||
export default function ListItemTemplate(items: BaseItemDto[] | undefined) : ListTemplate {
|
||||
return new ListTemplate({
|
||||
sections: [
|
||||
{
|
||||
items: items?.map(item => {
|
||||
return {
|
||||
id: item.Id!,
|
||||
text: item.Name ?? "Untitled",
|
||||
image: {
|
||||
uri: queryClient.getQueryData<string | undefined>([QueryKeys.ItemImage, item.Id!])
|
||||
}
|
||||
}
|
||||
}) ?? []
|
||||
}
|
||||
],
|
||||
})
|
||||
}
|
||||
export default function ListItemTemplate(items: BaseItemDto[] | undefined): ListTemplate {
|
||||
return new ListTemplate({
|
||||
sections: [
|
||||
{
|
||||
items:
|
||||
items?.map((item) => {
|
||||
return {
|
||||
id: item.Id!,
|
||||
text: item.Name ?? 'Untitled',
|
||||
image: {
|
||||
uri: queryClient.getQueryData<string | undefined>([
|
||||
QueryKeys.ItemImage,
|
||||
item.Id!,
|
||||
]),
|
||||
},
|
||||
}
|
||||
}) ?? [],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { CarPlay, TabBarTemplate } from "react-native-carplay";
|
||||
import CarPlayHome from "./Home";
|
||||
import CarPlayDiscover from "./Discover";
|
||||
import { CarPlay, TabBarTemplate } from 'react-native-carplay'
|
||||
import CarPlayHome from './Home'
|
||||
import CarPlayDiscover from './Discover'
|
||||
|
||||
const CarPlayNavigation : TabBarTemplate = new TabBarTemplate({
|
||||
id: "Tabs",
|
||||
title: 'Tabs',
|
||||
templates: [
|
||||
CarPlayHome,
|
||||
CarPlayDiscover
|
||||
],
|
||||
onTemplateSelect(template, e) {
|
||||
if (template)
|
||||
CarPlay.pushTemplate(template, true)
|
||||
},
|
||||
});
|
||||
const CarPlayNavigation: TabBarTemplate = new TabBarTemplate({
|
||||
id: 'Tabs',
|
||||
title: 'Tabs',
|
||||
templates: [CarPlayHome, CarPlayDiscover],
|
||||
onTemplateSelect(template, e) {
|
||||
if (template) CarPlay.pushTemplate(template, true)
|
||||
},
|
||||
})
|
||||
|
||||
export default CarPlayNavigation;
|
||||
export default CarPlayNavigation
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { NowPlayingTemplate } from "react-native-carplay";
|
||||
import { NowPlayingTemplate } from 'react-native-carplay'
|
||||
|
||||
const CarPlayNowPlaying : NowPlayingTemplate = new NowPlayingTemplate({
|
||||
|
||||
})
|
||||
const CarPlayNowPlaying: NowPlayingTemplate = new NowPlayingTemplate({})
|
||||
|
||||
export default CarPlayNowPlaying;
|
||||
export default CarPlayNowPlaying
|
||||
|
||||
@@ -1,38 +1,32 @@
|
||||
import React from "react";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { ScrollView } from "tamagui";
|
||||
import RecentlyAdded from "./helpers/just-added";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../types";
|
||||
import { H2 } from "../Global/helpers/text";
|
||||
import { useDiscoverContext } from "./provider";
|
||||
import { RefreshControl } from "react-native";
|
||||
import React from 'react'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { ScrollView } from 'tamagui'
|
||||
import RecentlyAdded from './helpers/just-added'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../types'
|
||||
import { H2 } from '../Global/helpers/text'
|
||||
import { useDiscoverContext } from './provider'
|
||||
import { RefreshControl } from 'react-native'
|
||||
|
||||
export default function Index({
|
||||
navigation
|
||||
} : {
|
||||
navigation : NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
export default function Index({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { refreshing, refresh } = useDiscoverContext()
|
||||
|
||||
const { refreshing, refresh } = useDiscoverContext();
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["top", "left", "right"]}>
|
||||
<ScrollView
|
||||
flexGrow={1}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
removeClippedSubviews
|
||||
paddingBottom={"$15"}
|
||||
refreshControl={(
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={refresh}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<H2>{`Recently added`}</H2>
|
||||
<RecentlyAdded navigation={navigation} />
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<SafeAreaView edges={['top', 'left', 'right']}>
|
||||
<ScrollView
|
||||
flexGrow={1}
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
removeClippedSubviews
|
||||
paddingBottom={'$15'}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={refresh} />}
|
||||
>
|
||||
<H2>{`Recently added`}</H2>
|
||||
<RecentlyAdded navigation={navigation} />
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,41 +1,40 @@
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { QueryKeys } from "../../../enums/query-keys";
|
||||
import HorizontalCardList from "../../../components/Global/components/horizontal-list";
|
||||
import { ItemCard } from "../../../components/Global/components/item-card";
|
||||
import { useDiscoverContext } from "../provider";
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
|
||||
import { ItemCard } from '../../../components/Global/components/item-card'
|
||||
import { useDiscoverContext } from '../provider'
|
||||
|
||||
export default function RecentlyAdded({
|
||||
navigation
|
||||
} : {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
export default function RecentlyAdded({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { recentlyAdded } = useDiscoverContext()
|
||||
|
||||
const { recentlyAdded } = useDiscoverContext();
|
||||
|
||||
return (
|
||||
<HorizontalCardList
|
||||
squared
|
||||
data={recentlyAdded}
|
||||
onSeeMore={() => {
|
||||
navigation.navigate("Albums", {
|
||||
query: QueryKeys.RecentlyAdded
|
||||
})
|
||||
}}
|
||||
renderItem={({ item }) =>
|
||||
<ItemCard
|
||||
caption={item.Name}
|
||||
subCaption={`${item.Artists?.join(", ")}`}
|
||||
squared
|
||||
size={"$12"}
|
||||
item={item}
|
||||
onPress={() => {
|
||||
navigation.navigate("Album", {
|
||||
album: item
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<HorizontalCardList
|
||||
squared
|
||||
data={recentlyAdded}
|
||||
onSeeMore={() => {
|
||||
navigation.navigate('Albums', {
|
||||
query: QueryKeys.RecentlyAdded,
|
||||
})
|
||||
}}
|
||||
renderItem={({ item }) => (
|
||||
<ItemCard
|
||||
caption={item.Name}
|
||||
subCaption={`${item.Artists?.join(', ')}`}
|
||||
squared
|
||||
size={'$12'}
|
||||
item={item}
|
||||
onPress={() => {
|
||||
navigation.navigate('Album', {
|
||||
album: item,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,73 +1,62 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchRecentlyAdded } from "../../api/queries/functions/recents";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { createContext, ReactNode, useContext, useState } from "react";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchRecentlyAdded } from '../../api/queries/functions/recents'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { createContext, ReactNode, useContext, useState } from 'react'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
interface DiscoverContext {
|
||||
refreshing: boolean;
|
||||
refresh: () => void;
|
||||
recentlyAdded: BaseItemDto[] | undefined;
|
||||
interface DiscoverContext {
|
||||
refreshing: boolean
|
||||
refresh: () => void
|
||||
recentlyAdded: BaseItemDto[] | undefined
|
||||
}
|
||||
|
||||
const DiscoverContextInitializer = () => {
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false);
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
|
||||
const { data: recentlyAdded, refetch } = useQuery({
|
||||
queryKey: [QueryKeys.RecentlyAdded],
|
||||
queryFn: () => fetchRecentlyAdded(),
|
||||
staleTime: (1000 * 60 * 5) // 5 minutes
|
||||
});
|
||||
const { data: recentlyAdded, refetch } = useQuery({
|
||||
queryKey: [QueryKeys.RecentlyAdded],
|
||||
queryFn: () => fetchRecentlyAdded(),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
})
|
||||
|
||||
const refresh = async () => {
|
||||
setRefreshing(true);
|
||||
const refresh = async () => {
|
||||
setRefreshing(true)
|
||||
|
||||
await Promise.all([
|
||||
refetch()
|
||||
])
|
||||
setRefreshing(false);
|
||||
}
|
||||
await Promise.all([refetch()])
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
return {
|
||||
refreshing,
|
||||
refresh,
|
||||
recentlyAdded
|
||||
}
|
||||
return {
|
||||
refreshing,
|
||||
refresh,
|
||||
recentlyAdded,
|
||||
}
|
||||
}
|
||||
|
||||
const DiscoverContext = createContext<DiscoverContext>({
|
||||
refreshing: false,
|
||||
refresh: () =>{},
|
||||
recentlyAdded: undefined
|
||||
});
|
||||
refreshing: false,
|
||||
refresh: () => {},
|
||||
recentlyAdded: undefined,
|
||||
})
|
||||
|
||||
export const DiscoverProvider: ({
|
||||
children
|
||||
} : {
|
||||
children: ReactNode
|
||||
}) => React.JSX.Element = ({
|
||||
children
|
||||
} : {
|
||||
children: ReactNode
|
||||
export const DiscoverProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) => {
|
||||
const { refreshing, refresh, recentlyAdded } = DiscoverContextInitializer()
|
||||
|
||||
const {
|
||||
refreshing,
|
||||
refresh,
|
||||
recentlyAdded
|
||||
} = DiscoverContextInitializer();
|
||||
|
||||
return (
|
||||
<DiscoverContext.Provider
|
||||
value={{
|
||||
refreshing,
|
||||
refresh,
|
||||
recentlyAdded
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
</DiscoverContext.Provider>
|
||||
)
|
||||
return (
|
||||
<DiscoverContext.Provider
|
||||
value={{
|
||||
refreshing,
|
||||
refresh,
|
||||
recentlyAdded,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DiscoverContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useDiscoverContext = () => useContext(DiscoverContext);
|
||||
export const useDiscoverContext = () => useContext(DiscoverContext)
|
||||
|
||||
@@ -1,71 +1,63 @@
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../types";
|
||||
import Index from "./component";
|
||||
import DetailsScreen from "../ItemDetail/screen";
|
||||
import Player from "../Player/stack";
|
||||
import Albums from "../Albums/component";
|
||||
import { AlbumScreen } from "../Album";
|
||||
import { ArtistScreen } from "../Artist";
|
||||
import { DiscoverProvider } from "./provider";
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../types'
|
||||
import Index from './component'
|
||||
import DetailsScreen from '../ItemDetail/screen'
|
||||
import Player from '../Player/stack'
|
||||
import Albums from '../Albums/component'
|
||||
import { AlbumScreen } from '../Album'
|
||||
import { ArtistScreen } from '../Artist'
|
||||
import { DiscoverProvider } from './provider'
|
||||
|
||||
export const DiscoverStack = createNativeStackNavigator<StackParamList>();
|
||||
export const DiscoverStack = createNativeStackNavigator<StackParamList>()
|
||||
|
||||
export function Discover(): React.JSX.Element {
|
||||
return (
|
||||
<DiscoverProvider>
|
||||
<DiscoverStack.Navigator
|
||||
initialRouteName="Discover"
|
||||
screenOptions={{
|
||||
return (
|
||||
<DiscoverProvider>
|
||||
<DiscoverStack.Navigator initialRouteName='Discover' screenOptions={{}}>
|
||||
<DiscoverStack.Screen
|
||||
name='Discover'
|
||||
component={Index}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
}}>
|
||||
<DiscoverStack.Screen
|
||||
name='Artist'
|
||||
component={ArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artist.Name ?? 'Unknown Artist',
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<DiscoverStack.Screen
|
||||
name="Discover"
|
||||
component={Index}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DiscoverStack.Screen
|
||||
name='Album'
|
||||
component={AlbumScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.album.Name ?? 'Untitled Album',
|
||||
headerTitle: '',
|
||||
})}
|
||||
/>
|
||||
|
||||
<DiscoverStack.Screen
|
||||
name="Artist"
|
||||
component={ArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artist.Name ?? "Unknown Artist",
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<DiscoverStack.Screen name='Albums' component={Albums} />
|
||||
|
||||
<DiscoverStack.Screen
|
||||
name="Album"
|
||||
component={AlbumScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.album.Name ?? "Untitled Album",
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
|
||||
<DiscoverStack.Screen
|
||||
name="Albums"
|
||||
component={Albums}
|
||||
/>
|
||||
|
||||
<DiscoverStack.Group screenOptions={{ presentation: "modal"}}>
|
||||
<DiscoverStack.Screen
|
||||
name="Details"
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</DiscoverStack.Group>
|
||||
</DiscoverStack.Navigator>
|
||||
</DiscoverProvider>
|
||||
)
|
||||
}
|
||||
<DiscoverStack.Group screenOptions={{ presentation: 'modal' }}>
|
||||
<DiscoverStack.Screen
|
||||
name='Details'
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</DiscoverStack.Group>
|
||||
</DiscoverStack.Navigator>
|
||||
</DiscoverProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export const cardDimensions = {
|
||||
artist: {
|
||||
width: 150,
|
||||
height: 150
|
||||
},
|
||||
album: {
|
||||
width: 150,
|
||||
height: 150
|
||||
}
|
||||
}
|
||||
artist: {
|
||||
width: 150,
|
||||
height: 150,
|
||||
},
|
||||
album: {
|
||||
width: 150,
|
||||
height: 150,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,53 +1,45 @@
|
||||
import type { AvatarProps as TamaguiAvatarProps } from "tamagui";
|
||||
import { Avatar as TamaguiAvatar, YStack } from "tamagui"
|
||||
import { Text } from "../helpers/text"
|
||||
import { BaseItemDto, ImageType } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { QueryKeys } from "../../../enums/query-keys";
|
||||
import { fetchItemImage } from "../../../api/queries/functions/images";
|
||||
import type { AvatarProps as TamaguiAvatarProps } from 'tamagui'
|
||||
import { Avatar as TamaguiAvatar, YStack } from 'tamagui'
|
||||
import { Text } from '../helpers/text'
|
||||
import { BaseItemDto, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchItemImage } from '../../../api/queries/functions/images'
|
||||
|
||||
interface AvatarProps extends TamaguiAvatarProps {
|
||||
item: BaseItemDto,
|
||||
imageType?: ImageType,
|
||||
subheading?: string | null | undefined;
|
||||
item: BaseItemDto
|
||||
imageType?: ImageType
|
||||
subheading?: string | null | undefined
|
||||
}
|
||||
|
||||
export default function Avatar({
|
||||
item,
|
||||
imageType,
|
||||
subheading,
|
||||
...props
|
||||
item,
|
||||
imageType,
|
||||
subheading,
|
||||
...props
|
||||
}: AvatarProps): React.JSX.Element {
|
||||
const { data } = useQuery({
|
||||
queryKey: [
|
||||
QueryKeys.ItemImage,
|
||||
item.Id!,
|
||||
imageType,
|
||||
Math.ceil(150 / 100) * 100, // Images are fetched at a higher, generic resolution
|
||||
Math.ceil(150 / 100) * 100, // So these keys need to match
|
||||
],
|
||||
queryFn: () => fetchItemImage(item.Id!, imageType ?? ImageType.Primary, 150, 150),
|
||||
retry: 2,
|
||||
gcTime: 1000 * 60, // 1 minute
|
||||
staleTime: 1000 * 60, // 1 minute,
|
||||
})
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: [
|
||||
QueryKeys.ItemImage,
|
||||
item.Id!,
|
||||
imageType,
|
||||
Math.ceil(150 / 100) * 100, // Images are fetched at a higher, generic resolution
|
||||
Math.ceil(150 / 100) * 100 // So these keys need to match
|
||||
],
|
||||
queryFn: () => fetchItemImage(item.Id!, imageType ?? ImageType.Primary, 150, 150),
|
||||
retry: 2,
|
||||
gcTime: (1000 * 60), // 1 minute
|
||||
staleTime: (1000 * 60) // 1 minute,
|
||||
});
|
||||
|
||||
return (
|
||||
<YStack alignItems="center" marginHorizontal={10}>
|
||||
<TamaguiAvatar
|
||||
borderRadius={!props.circular ? 4 : 'unset'}
|
||||
{...props}
|
||||
>
|
||||
<TamaguiAvatar.Image src={data} />
|
||||
<TamaguiAvatar.Fallback backgroundColor="$borderColor" />
|
||||
</TamaguiAvatar>
|
||||
{ props.children && (
|
||||
<Text>{props.children}</Text>
|
||||
)}
|
||||
{ subheading && (
|
||||
<Text bold>{ subheading }</Text>
|
||||
)}
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<YStack alignItems='center' marginHorizontal={10}>
|
||||
<TamaguiAvatar borderRadius={!props.circular ? 4 : 'unset'} {...props}>
|
||||
<TamaguiAvatar.Image src={data ?? undefined} />
|
||||
<TamaguiAvatar.Fallback backgroundColor='$borderColor' />
|
||||
</TamaguiAvatar>
|
||||
{props.children && <Text>{props.children}</Text>}
|
||||
{subheading && <Text bold>{subheading}</Text>}
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,97 +1,107 @@
|
||||
import { BaseItemDto, ImageType } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { Blurhash } from "react-native-blurhash";
|
||||
import { Square, View } from "tamagui";
|
||||
import { isEmpty } from "lodash";
|
||||
import { Image } from "react-native";
|
||||
import { QueryKeys } from "../../../enums/query-keys";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchItemImage } from "../../../api/queries/functions/images";
|
||||
import { BaseItemDto, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Blurhash } from 'react-native-blurhash'
|
||||
import { Square, View } from 'tamagui'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { Image } from 'react-native'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchItemImage } from '../../../api/queries/functions/images'
|
||||
|
||||
interface BlurhashLoadingProps {
|
||||
item: BaseItemDto;
|
||||
width: number;
|
||||
height?: number;
|
||||
type?: ImageType;
|
||||
borderRadius?: number | undefined
|
||||
item: BaseItemDto
|
||||
width: number
|
||||
height?: number
|
||||
type?: ImageType
|
||||
borderRadius?: number | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* Please use the `Image` component from
|
||||
* @deprecated
|
||||
*
|
||||
* Please use the `Image` component from
|
||||
* the `expo-image` module instead, as that is more performant
|
||||
*
|
||||
*
|
||||
* A React component that will render a Blurhash
|
||||
* string as an image while loading the full image
|
||||
* from the server
|
||||
*
|
||||
*
|
||||
* Image Query is stale after 30 minutes and collected
|
||||
* after an hour to keep the cache size down and the
|
||||
* after an hour to keep the cache size down and the
|
||||
* app performant
|
||||
*
|
||||
*
|
||||
* TODO: Keep images in offline mode
|
||||
*
|
||||
* @param param0
|
||||
* @returns
|
||||
*
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export default function BlurhashedImage({
|
||||
item,
|
||||
width,
|
||||
height,
|
||||
type,
|
||||
borderRadius
|
||||
} : BlurhashLoadingProps) : React.JSX.Element {
|
||||
export default function BlurhashedImage({
|
||||
item,
|
||||
width,
|
||||
height,
|
||||
type,
|
||||
borderRadius,
|
||||
}: BlurhashLoadingProps): React.JSX.Element {
|
||||
const { data: image, isSuccess } = useQuery({
|
||||
queryKey: [
|
||||
QueryKeys.ItemImage,
|
||||
item.AlbumId ? item.AlbumId : item.Id!,
|
||||
type ?? ImageType.Primary,
|
||||
Math.ceil(width / 100) * 100, // Images are fetched at a higher, generic resolution
|
||||
Math.ceil(height ?? width / 100) * 100, // So these keys need to match
|
||||
],
|
||||
queryFn: () =>
|
||||
fetchItemImage(
|
||||
item.AlbumId ? item.AlbumId : item.Id!,
|
||||
type ?? ImageType.Primary,
|
||||
width,
|
||||
height ?? width,
|
||||
),
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
gcTime: 1000 * 60 * 60, // 1 hour
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const { data: image, isSuccess } = useQuery({
|
||||
queryKey: [
|
||||
QueryKeys.ItemImage,
|
||||
item.AlbumId ? item.AlbumId : item.Id!,
|
||||
type ?? ImageType.Primary,
|
||||
Math.ceil(width / 100) * 100, // Images are fetched at a higher, generic resolution
|
||||
Math.ceil(height ?? width / 100) * 100 // So these keys need to match
|
||||
],
|
||||
queryFn: () => fetchItemImage(item.AlbumId ? item.AlbumId : item.Id!, type ?? ImageType.Primary, width, height ?? width),
|
||||
staleTime: (1000 * 60 * 30), // 30 minutes
|
||||
gcTime: (1000 * 60 * 60), // 1 hour
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
const blurhash =
|
||||
!isEmpty(item.ImageBlurHashes) &&
|
||||
!isEmpty(type ? item.ImageBlurHashes[type] : item.ImageBlurHashes.Primary)
|
||||
? Object.values(type ? item.ImageBlurHashes[type]! : item.ImageBlurHashes.Primary!)[0]
|
||||
: undefined
|
||||
|
||||
const blurhash = !isEmpty(item.ImageBlurHashes)
|
||||
&& !isEmpty(type ? item.ImageBlurHashes[type] : item.ImageBlurHashes.Primary)
|
||||
? Object.values(type ? item.ImageBlurHashes[type]! : item.ImageBlurHashes.Primary!)[0]
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<View minHeight={height ?? width} minWidth={width} borderRadius={borderRadius ? borderRadius : 25}>
|
||||
|
||||
{ isSuccess ? (
|
||||
<Image
|
||||
source={{
|
||||
uri: image
|
||||
}}
|
||||
style={{
|
||||
height: height ?? width,
|
||||
width,
|
||||
borderRadius: borderRadius ? borderRadius : 25,
|
||||
resizeMode: "contain"
|
||||
}}
|
||||
/>
|
||||
) : blurhash ? (
|
||||
<Blurhash blurhash={blurhash!} style={{
|
||||
height: height ?? width,
|
||||
width: width,
|
||||
borderRadius: borderRadius ? borderRadius : 25
|
||||
}} />
|
||||
) : (
|
||||
<Square
|
||||
backgroundColor="$amethyst"
|
||||
width={width}
|
||||
height={height ?? width}
|
||||
borderRadius={borderRadius ? borderRadius : 25}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View
|
||||
minHeight={height ?? width}
|
||||
minWidth={width}
|
||||
borderRadius={borderRadius ? borderRadius : 25}>
|
||||
{isSuccess ? (
|
||||
<Image
|
||||
source={{
|
||||
uri: image ?? undefined,
|
||||
}}
|
||||
style={{
|
||||
height: height ?? width,
|
||||
width,
|
||||
borderRadius: borderRadius ? borderRadius : 25,
|
||||
resizeMode: 'contain',
|
||||
}}
|
||||
/>
|
||||
) : blurhash ? (
|
||||
<Blurhash
|
||||
blurhash={blurhash!}
|
||||
style={{
|
||||
height: height ?? width,
|
||||
width: width,
|
||||
borderRadius: borderRadius ? borderRadius : 25,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Square
|
||||
backgroundColor='$amethyst'
|
||||
width={width}
|
||||
height={height ?? width}
|
||||
borderRadius={borderRadius ? borderRadius : 25}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,113 +1,105 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Icon from "../helpers/icon";
|
||||
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { isUndefined } from "lodash";
|
||||
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";
|
||||
import { fetchUserData } from "../../../api/queries/functions/favorites";
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Icon from '../helpers/icon'
|
||||
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { isUndefined } from 'lodash'
|
||||
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'
|
||||
import { fetchUserData } from '../../../api/queries/functions/favorites'
|
||||
|
||||
import * as Burnt from "burnt";
|
||||
import * as Burnt from 'burnt'
|
||||
|
||||
interface SetFavoriteMutation {
|
||||
item: BaseItemDto,
|
||||
item: BaseItemDto
|
||||
}
|
||||
|
||||
export default function FavoriteButton({
|
||||
item,
|
||||
onToggle
|
||||
export default function FavoriteButton({
|
||||
item,
|
||||
onToggle,
|
||||
}: {
|
||||
item: BaseItemDto;
|
||||
onToggle?: () => void
|
||||
}) : React.JSX.Element {
|
||||
item: BaseItemDto
|
||||
onToggle?: () => void
|
||||
}): React.JSX.Element {
|
||||
const { nowPlaying, nowPlayingIsFavorite } = usePlayerContext()
|
||||
|
||||
const { nowPlaying, nowPlayingIsFavorite } = usePlayerContext();
|
||||
const [isFavorite, setIsFavorite] = useState<boolean>(isFavoriteItem(item))
|
||||
|
||||
const [isFavorite, setIsFavorite] = useState<boolean>(isFavoriteItem(item));
|
||||
const { data, isFetching, isFetched, refetch } = useQuery({
|
||||
queryKey: [QueryKeys.UserData, item.Id!],
|
||||
queryFn: () => fetchUserData(item.Id!),
|
||||
})
|
||||
|
||||
const { data, isFetching, isFetched, refetch } = useQuery({
|
||||
queryKey: [QueryKeys.UserData, item.Id!],
|
||||
queryFn: () => fetchUserData(item.Id!),
|
||||
});
|
||||
const useSetFavorite = useMutation({
|
||||
mutationFn: async (mutation: SetFavoriteMutation) => {
|
||||
return getUserLibraryApi(Client.api!).markFavoriteItem({
|
||||
itemId: mutation.item.Id!,
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
Burnt.alert({
|
||||
title: `Added favorite`,
|
||||
duration: 1,
|
||||
preset: 'heart',
|
||||
})
|
||||
|
||||
const useSetFavorite = useMutation({
|
||||
mutationFn: async (mutation: SetFavoriteMutation) => {
|
||||
return getUserLibraryApi(Client.api!)
|
||||
.markFavoriteItem({
|
||||
itemId: mutation.item.Id!
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
Burnt.alert({
|
||||
title: `Added favorite`,
|
||||
duration: 1,
|
||||
preset: 'heart'
|
||||
});
|
||||
trigger('notificationSuccess')
|
||||
|
||||
trigger("notificationSuccess");
|
||||
setIsFavorite(true)
|
||||
onToggle ? onToggle() : {}
|
||||
|
||||
setIsFavorite(true);
|
||||
onToggle ? onToggle() : {};
|
||||
// Force refresh of track user data
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKeys.UserData, item.Id] })
|
||||
},
|
||||
})
|
||||
|
||||
// Force refresh of track user data
|
||||
queryClient.invalidateQueries({ queryKey: [QueryKeys.UserData, item.Id] });
|
||||
}
|
||||
})
|
||||
|
||||
const useRemoveFavorite = useMutation({
|
||||
mutationFn: async (mutation: SetFavoriteMutation) => {
|
||||
return getUserLibraryApi(Client.api!)
|
||||
.unmarkFavoriteItem({
|
||||
itemId: mutation.item.Id!
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
Burnt.alert({
|
||||
title: `Removed favorite`,
|
||||
duration: 1,
|
||||
preset: 'done'
|
||||
});
|
||||
const useRemoveFavorite = useMutation({
|
||||
mutationFn: async (mutation: SetFavoriteMutation) => {
|
||||
return getUserLibraryApi(Client.api!).unmarkFavoriteItem({
|
||||
itemId: mutation.item.Id!,
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
Burnt.alert({
|
||||
title: `Removed favorite`,
|
||||
duration: 1,
|
||||
preset: 'done',
|
||||
})
|
||||
|
||||
trigger("notificationSuccess")
|
||||
setIsFavorite(false);
|
||||
onToggle ? onToggle(): {};
|
||||
}
|
||||
})
|
||||
trigger('notificationSuccess')
|
||||
setIsFavorite(false)
|
||||
onToggle ? onToggle() : {}
|
||||
},
|
||||
})
|
||||
|
||||
const toggleFavorite = () => {
|
||||
if (isFavorite)
|
||||
useRemoveFavorite.mutate({ item })
|
||||
else
|
||||
useSetFavorite.mutate({ item })
|
||||
}
|
||||
const toggleFavorite = () => {
|
||||
if (isFavorite) useRemoveFavorite.mutate({ item })
|
||||
else useSetFavorite.mutate({ item })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [
|
||||
item
|
||||
]);
|
||||
useEffect(() => {
|
||||
refetch()
|
||||
}, [item])
|
||||
|
||||
return (
|
||||
isFetching && isUndefined(item.UserData) ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Icon
|
||||
name={data?.IsFavorite ?? isFavorite ? "heart" : "heart-outline"}
|
||||
color={getTokens().color.telemagenta.val}
|
||||
onPress={toggleFavorite}
|
||||
/>
|
||||
|
||||
)
|
||||
)
|
||||
return isFetching && isUndefined(item.UserData) ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Icon
|
||||
name={data?.IsFavorite ?? isFavorite ? 'heart' : 'heart-outline'}
|
||||
color={getTokens().color.telemagenta.val}
|
||||
onPress={toggleFavorite}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function isFavoriteItem(item: BaseItemDto) : boolean {
|
||||
return isUndefined(item.UserData) ? false
|
||||
: isUndefined(item.UserData.IsFavorite) ? false
|
||||
: item.UserData.IsFavorite
|
||||
}
|
||||
export function isFavoriteItem(item: BaseItemDto): boolean {
|
||||
return isUndefined(item.UserData)
|
||||
? false
|
||||
: isUndefined(item.UserData.IsFavorite)
|
||||
? false
|
||||
: item.UserData.IsFavorite
|
||||
}
|
||||
|
||||
@@ -1,46 +1,31 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getToken, Spacer, YStack } from "tamagui";
|
||||
import Icon from "../helpers/icon";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { QueryKeys } from "../../../enums/query-keys";
|
||||
import { fetchUserData } from "../../../api/queries/functions/favorites";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getToken, Spacer, YStack } from 'tamagui'
|
||||
import Icon from '../helpers/icon'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchUserData } from '../../../api/queries/functions/favorites'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function FavoriteIcon({
|
||||
item
|
||||
} : {
|
||||
item: BaseItemDto
|
||||
}) : React.JSX.Element {
|
||||
export default function FavoriteIcon({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||
const [isFavorite, setIsFavorite] = useState<boolean>(item.UserData?.IsFavorite ?? false)
|
||||
|
||||
const [isFavorite, setIsFavorite] = useState<boolean>(item.UserData?.IsFavorite ?? false);
|
||||
const { data: userData } = useQuery({
|
||||
queryKey: [QueryKeys.UserData, item.Id!],
|
||||
queryFn: () => fetchUserData(item.Id!),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes,
|
||||
})
|
||||
|
||||
const { data: userData } = useQuery({
|
||||
queryKey: [QueryKeys.UserData, item.Id!],
|
||||
queryFn: () => fetchUserData(item.Id!),
|
||||
staleTime: (1000 * 60 * 5) // 5 minutes,
|
||||
});
|
||||
useEffect(() => {
|
||||
setIsFavorite(userData?.IsFavorite ?? false)
|
||||
}, [userData])
|
||||
|
||||
useEffect(() => {
|
||||
setIsFavorite(userData?.IsFavorite ?? false)
|
||||
}, [
|
||||
userData
|
||||
])
|
||||
|
||||
return (
|
||||
<YStack
|
||||
alignContent="center"
|
||||
justifyContent="center"
|
||||
minWidth={24}
|
||||
>
|
||||
{ isFavorite ? (
|
||||
<Icon
|
||||
small
|
||||
name="heart"
|
||||
color={getToken("$color.telemagenta")}
|
||||
/>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<YStack alignContent='center' justifyContent='center' minWidth={24}>
|
||||
{isFavorite ? (
|
||||
<Icon small name='heart' color={getToken('$color.telemagenta')} />
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,53 +1,48 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models/base-item-dto";
|
||||
import React from "react";
|
||||
import { FlatList, FlatListProps, ListRenderItem } from "react-native";
|
||||
import IconCard from "../helpers/icon-card";
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import React from 'react'
|
||||
import { FlatList, FlatListProps, ListRenderItem } from 'react-native'
|
||||
import IconCard from '../helpers/icon-card'
|
||||
|
||||
interface HorizontalCardListProps extends FlatListProps<BaseItemDto> {
|
||||
squared?: boolean | undefined;
|
||||
/**
|
||||
* The number of items that will be displayed before
|
||||
* we cut it off and display a "Show More" card
|
||||
*/
|
||||
cutoff?: number | undefined;
|
||||
onSeeMore?: () => void | undefined;
|
||||
squared?: boolean | undefined
|
||||
/**
|
||||
* The number of items that will be displayed before
|
||||
* we cut it off and display a "Show More" card
|
||||
*/
|
||||
cutoff?: number | undefined
|
||||
onSeeMore?: () => void | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a Horizontal FlatList of 20 ItemCards
|
||||
* then shows a "See More" button
|
||||
* @param param0
|
||||
* @returns
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export default function HorizontalCardList({
|
||||
onSeeMore,
|
||||
squared = false,
|
||||
...props
|
||||
} : HorizontalCardListProps) : React.JSX.Element {
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
horizontal
|
||||
data={props.data}
|
||||
renderItem={props.renderItem}
|
||||
ListFooterComponent={() => {
|
||||
return props.data && onSeeMore ? (
|
||||
<IconCard
|
||||
name={
|
||||
squared
|
||||
? "arrow-right-box"
|
||||
: "arrow-right-circle"
|
||||
}
|
||||
circular={!squared}
|
||||
caption="See More"
|
||||
onPress={onSeeMore}
|
||||
/>
|
||||
) : undefined}
|
||||
}
|
||||
removeClippedSubviews
|
||||
style={{
|
||||
overflow: "hidden"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onSeeMore,
|
||||
squared = false,
|
||||
...props
|
||||
}: HorizontalCardListProps): React.JSX.Element {
|
||||
return (
|
||||
<FlatList
|
||||
horizontal
|
||||
data={props.data}
|
||||
renderItem={props.renderItem}
|
||||
ListFooterComponent={() => {
|
||||
return props.data && onSeeMore ? (
|
||||
<IconCard
|
||||
name={squared ? 'arrow-right-box' : 'arrow-right-circle'}
|
||||
circular={!squared}
|
||||
caption='See More'
|
||||
onPress={onSeeMore}
|
||||
/>
|
||||
) : undefined
|
||||
}}
|
||||
removeClippedSubviews
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,42 +1,37 @@
|
||||
import React from "react";
|
||||
import type { CardProps as TamaguiCardProps } from "tamagui"
|
||||
import { getToken, Card as TamaguiCard, View, YStack } from "tamagui";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { Text } from "../helpers/text";
|
||||
import { Image } from "expo-image";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from "../../../api/client";
|
||||
import React from 'react'
|
||||
import type { CardProps as TamaguiCardProps } from 'tamagui'
|
||||
import { getToken, Card as TamaguiCard, View, YStack } from 'tamagui'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Text } from '../helpers/text'
|
||||
import { Image } from 'expo-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../../api/client'
|
||||
|
||||
interface CardProps extends TamaguiCardProps {
|
||||
caption?: string | null | undefined;
|
||||
subCaption?: string | null | undefined;
|
||||
item: BaseItemDto;
|
||||
squared?: boolean;
|
||||
caption?: string | null | undefined
|
||||
subCaption?: string | null | undefined
|
||||
item: BaseItemDto
|
||||
squared?: boolean
|
||||
}
|
||||
|
||||
export function ItemCard(props: CardProps) {
|
||||
|
||||
return (
|
||||
<View
|
||||
alignItems="center"
|
||||
margin={5}
|
||||
>
|
||||
<TamaguiCard
|
||||
size={"$12"}
|
||||
height={props.size}
|
||||
width={props.size}
|
||||
backgroundColor={getToken("$color.amethyst")}
|
||||
circular={!props.squared}
|
||||
borderRadius={props.squared ? 5 : 'unset'}
|
||||
animation="bouncy"
|
||||
hoverStyle={props.onPress ? { scale: 0.925 } : {}}
|
||||
pressStyle={props.onPress ? { scale: 0.875 } : {}}
|
||||
{...props}
|
||||
>
|
||||
<TamaguiCard.Header>
|
||||
</TamaguiCard.Header>
|
||||
<TamaguiCard.Footer padded>
|
||||
{/* { props.item.Type === 'MusicArtist' && (
|
||||
return (
|
||||
<View alignItems='center' margin={5}>
|
||||
<TamaguiCard
|
||||
size={'$12'}
|
||||
height={props.size}
|
||||
width={props.size}
|
||||
backgroundColor={getToken('$color.amethyst')}
|
||||
circular={!props.squared}
|
||||
borderRadius={props.squared ? 5 : 'unset'}
|
||||
animation='bouncy'
|
||||
hoverStyle={props.onPress ? { scale: 0.925 } : {}}
|
||||
pressStyle={props.onPress ? { scale: 0.875 } : {}}
|
||||
{...props}
|
||||
>
|
||||
<TamaguiCard.Header></TamaguiCard.Header>
|
||||
<TamaguiCard.Footer padded>
|
||||
{/* { props.item.Type === 'MusicArtist' && (
|
||||
<BlurhashedImage
|
||||
cornered
|
||||
item={props.item}
|
||||
@@ -45,49 +40,38 @@ export function ItemCard(props: CardProps) {
|
||||
height={logoDimensions.height}
|
||||
/>
|
||||
)} */}
|
||||
</TamaguiCard.Footer>
|
||||
<TamaguiCard.Background>
|
||||
<Image
|
||||
source={getImageApi(Client.api!)
|
||||
.getItemImageUrlById(
|
||||
props.item.Type === 'Audio'
|
||||
? props.item.AlbumId!
|
||||
: props.item.Id!
|
||||
)}
|
||||
placeholder={props.item.ImageBlurHashes && props.item.ImageBlurHashes["Primary"] ? props.item.ImageBlurHashes["Primary"][0] : undefined}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: props.squared ? 2 : 100
|
||||
}}
|
||||
/>
|
||||
</TamaguiCard.Background>
|
||||
</TamaguiCard>
|
||||
{ props.caption && (
|
||||
<YStack
|
||||
alignContent="center"
|
||||
alignItems="center"
|
||||
maxWidth={props.size}
|
||||
>
|
||||
<Text
|
||||
bold
|
||||
lineBreakStrategyIOS="standard"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{ props.caption }
|
||||
</Text>
|
||||
|
||||
{ props.subCaption && (
|
||||
<Text
|
||||
lineBreakStrategyIOS="standard"
|
||||
numberOfLines={1}
|
||||
textAlign="center"
|
||||
>
|
||||
{ props.subCaption }
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
</TamaguiCard.Footer>
|
||||
<TamaguiCard.Background>
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(
|
||||
props.item.Type === 'Audio' ? props.item.AlbumId! : props.item.Id!,
|
||||
)}
|
||||
placeholder={
|
||||
props.item.ImageBlurHashes && props.item.ImageBlurHashes['Primary']
|
||||
? props.item.ImageBlurHashes['Primary'][0]
|
||||
: undefined
|
||||
}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: props.squared ? 2 : 100,
|
||||
}}
|
||||
/>
|
||||
</TamaguiCard.Background>
|
||||
</TamaguiCard>
|
||||
{props.caption && (
|
||||
<YStack alignContent='center' alignItems='center' maxWidth={props.size}>
|
||||
<Text bold lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{props.caption}
|
||||
</Text>
|
||||
|
||||
{props.subCaption && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1} textAlign='center'>
|
||||
{props.subCaption}
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,138 +1,122 @@
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { getTokens, Separator, Spacer, View, XStack, YStack } from "tamagui";
|
||||
import { Text } from "../helpers/text";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import BlurhashedImage from "./blurhashed-image";
|
||||
import Icon from "../helpers/icon";
|
||||
import { QueuingType } from "../../../enums/queuing-type";
|
||||
import { RunTimeTicks } from "../helpers/time-codes";
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { getTokens, Separator, Spacer, View, XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../helpers/text'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import BlurhashedImage from './blurhashed-image'
|
||||
import Icon from '../helpers/icon'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { RunTimeTicks } from '../helpers/time-codes'
|
||||
|
||||
export default function Item({
|
||||
item,
|
||||
queueName,
|
||||
navigation,
|
||||
} : {
|
||||
item: BaseItemDto,
|
||||
queueName: string,
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
export default function Item({
|
||||
item,
|
||||
queueName,
|
||||
navigation,
|
||||
}: {
|
||||
item: BaseItemDto
|
||||
queueName: string
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { usePlayNewQueue } = usePlayerContext()
|
||||
|
||||
const { usePlayNewQueue } = usePlayerContext();
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
return (
|
||||
<View flex={1}>
|
||||
<Separator />
|
||||
|
||||
return (
|
||||
<View flex={1}>
|
||||
<Separator />
|
||||
<XStack
|
||||
alignContent='center'
|
||||
flex={1}
|
||||
minHeight={width / 9}
|
||||
onPress={() => {
|
||||
switch (item.Type) {
|
||||
case 'MusicArtist': {
|
||||
navigation.navigate('Artist', {
|
||||
artist: item,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
<XStack
|
||||
alignContent="center"
|
||||
flex={1}
|
||||
minHeight={width / 9}
|
||||
onPress={() => {
|
||||
switch (item.Type) {
|
||||
case ("MusicArtist") : {
|
||||
navigation.navigate("Artist", {
|
||||
artist: item
|
||||
})
|
||||
break;
|
||||
}
|
||||
case 'MusicAlbum': {
|
||||
navigation.navigate('Album', {
|
||||
album: item,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case ("MusicAlbum") : {
|
||||
navigation.navigate("Album", {
|
||||
album: item
|
||||
})
|
||||
break;
|
||||
}
|
||||
case 'Audio': {
|
||||
usePlayNewQueue.mutate({
|
||||
track: item,
|
||||
tracklist: [item],
|
||||
queue: 'Search',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}}
|
||||
onLongPress={() => {
|
||||
navigation.navigate('Details', {
|
||||
item,
|
||||
isNested: false,
|
||||
})
|
||||
}}
|
||||
paddingVertical={'$2'}
|
||||
marginHorizontal={'$1'}
|
||||
>
|
||||
<BlurhashedImage
|
||||
item={item}
|
||||
width={width / 9}
|
||||
borderRadius={item.Type === 'MusicArtist' ? width / 9 : 2}
|
||||
/>
|
||||
|
||||
case ("Audio") : {
|
||||
usePlayNewQueue.mutate({
|
||||
track: item,
|
||||
tracklist: [item],
|
||||
queue: "Search",
|
||||
queuingType: QueuingType.FromSelection
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
<YStack
|
||||
marginLeft={'$1'}
|
||||
alignContent='center'
|
||||
justifyContent='flex-start'
|
||||
flex={3}
|
||||
>
|
||||
<Text bold lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{item.Name ?? ''}
|
||||
</Text>
|
||||
{(item.Type === 'Audio' || item.Type === 'MusicAlbum') && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{item.AlbumArtist ?? 'Untitled Artist'}
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
|
||||
}}
|
||||
onLongPress={() => {
|
||||
navigation.navigate("Details", {
|
||||
item,
|
||||
isNested: false
|
||||
})
|
||||
}}
|
||||
paddingVertical={"$2"}
|
||||
marginHorizontal={"$1"}
|
||||
>
|
||||
<BlurhashedImage item={item} width={width / 9} borderRadius={item.Type === 'MusicArtist' ? width / 9 : 2}/>
|
||||
<XStack justifyContent='space-between' alignItems='center' flex={1}>
|
||||
{item.UserData?.IsFavorite ? (
|
||||
<Icon small color={getTokens().color.telemagenta.val} name='heart' />
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
{/* Runtime ticks for Songs */}
|
||||
{item.Type === 'Audio' ? (
|
||||
<RunTimeTicks>{item.RunTimeTicks}</RunTimeTicks>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
|
||||
<YStack
|
||||
marginLeft={"$1"}
|
||||
alignContent="center"
|
||||
justifyContent="flex-start"
|
||||
flex={3}
|
||||
>
|
||||
<Text
|
||||
bold
|
||||
lineBreakStrategyIOS="standard"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{ item.Name ?? ""}
|
||||
</Text>
|
||||
{ (item.Type === 'Audio' || item.Type === 'MusicAlbum') && (
|
||||
<Text
|
||||
lineBreakStrategyIOS="standard"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{ item.AlbumArtist ?? "Untitled Artist" }
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
|
||||
|
||||
|
||||
<XStack
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
flex={1}
|
||||
>
|
||||
{ item.UserData?.IsFavorite ? (
|
||||
<Icon
|
||||
small
|
||||
color={getTokens().color.telemagenta.val}
|
||||
name="heart"
|
||||
/>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
{/* Runtime ticks for Songs */}
|
||||
{ item.Type ==='Audio' ? (
|
||||
<RunTimeTicks>{item.RunTimeTicks}</RunTimeTicks>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
|
||||
{(item.Type === 'Audio' || item.Type === 'MusicAlbum') ? (
|
||||
|
||||
<Icon
|
||||
name="dots-horizontal"
|
||||
onPress={() => {
|
||||
navigation.navigate("Details", {
|
||||
item,
|
||||
isNested: false
|
||||
})
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
</XStack>
|
||||
</XStack>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
{item.Type === 'Audio' || item.Type === 'MusicAlbum' ? (
|
||||
<Icon
|
||||
name='dots-horizontal'
|
||||
onPress={() => {
|
||||
navigation.navigate('Details', {
|
||||
item,
|
||||
isNested: false,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
</XStack>
|
||||
</XStack>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ToastViewport } from '@tamagui/toast'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
|
||||
export default function SafeToastViewport() : React.JSX.Element {
|
||||
const { left, top, right } = useSafeAreaInsets()
|
||||
return (
|
||||
<ToastViewport flexDirection="column-reverse" top={top} left={left} right={right} />
|
||||
)
|
||||
}
|
||||
export default function SafeToastViewport(): React.JSX.Element {
|
||||
const { left, top, right } = useSafeAreaInsets()
|
||||
return <ToastViewport flexDirection='column-reverse' top={top} left={left} right={right} />
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import {Toast as TamaguiToast, useToastState} from "@tamagui/toast"
|
||||
import { YStack } from "tamagui"
|
||||
import { Toast as TamaguiToast, useToastState } from '@tamagui/toast'
|
||||
import { YStack } from 'tamagui'
|
||||
|
||||
export default function Toast() : React.JSX.Element | null {
|
||||
const currentToast = useToastState()
|
||||
|
||||
if (!currentToast || currentToast.isHandledNatively) return null
|
||||
return (
|
||||
<TamaguiToast
|
||||
key={currentToast.id}
|
||||
duration={currentToast.duration}
|
||||
enterStyle={{ opacity: 0, scale: 0.5, y: -25 }}
|
||||
exitStyle={{ opacity: 0, scale: 1, y: -20 }}
|
||||
y={0}
|
||||
opacity={1}
|
||||
scale={1}
|
||||
animation="200ms"
|
||||
viewportName={currentToast.viewportName}
|
||||
>
|
||||
<YStack>
|
||||
<TamaguiToast.Title>{currentToast.title}</TamaguiToast.Title>
|
||||
{!!currentToast.message && (
|
||||
<TamaguiToast.Description>{currentToast.message}</TamaguiToast.Description>
|
||||
)}
|
||||
</YStack>
|
||||
</TamaguiToast>
|
||||
)
|
||||
}
|
||||
export default function Toast(): React.JSX.Element | null {
|
||||
const currentToast = useToastState()
|
||||
|
||||
if (!currentToast || currentToast.isHandledNatively) return null
|
||||
return (
|
||||
<TamaguiToast
|
||||
key={currentToast.id}
|
||||
duration={currentToast.duration}
|
||||
enterStyle={{ opacity: 0, scale: 0.5, y: -25 }}
|
||||
exitStyle={{ opacity: 0, scale: 1, y: -20 }}
|
||||
y={0}
|
||||
opacity={1}
|
||||
scale={1}
|
||||
animation='200ms'
|
||||
viewportName={currentToast.viewportName}
|
||||
>
|
||||
<YStack>
|
||||
<TamaguiToast.Title>{currentToast.title}</TamaguiToast.Title>
|
||||
{!!currentToast.message && (
|
||||
<TamaguiToast.Description>{currentToast.message}</TamaguiToast.Description>
|
||||
)}
|
||||
</YStack>
|
||||
</TamaguiToast>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,186 +1,163 @@
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
import React from "react";
|
||||
import { getToken, getTokens, Spacer, Theme, useTheme, XStack, YStack } from "tamagui";
|
||||
import { Text } from "../helpers/text";
|
||||
import { RunTimeTicks } from "../helpers/time-codes";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import Icon from "../helpers/icon";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import { QueuingType } from "../../../enums/queuing-type";
|
||||
import { Queue } from "../../../player/types/queue-item";
|
||||
import FavoriteIcon from "./favorite-icon";
|
||||
import { Image } from "expo-image";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from "../../../api/client";
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import React from 'react'
|
||||
import { getToken, getTokens, Spacer, Theme, useTheme, XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../helpers/text'
|
||||
import { RunTimeTicks } from '../helpers/time-codes'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import Icon from '../helpers/icon'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { Queue } from '../../../player/types/queue-item'
|
||||
import FavoriteIcon from './favorite-icon'
|
||||
import { Image } from 'expo-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../../api/client'
|
||||
|
||||
interface TrackProps {
|
||||
track: BaseItemDto;
|
||||
navigation: NativeStackNavigationProp<StackParamList>;
|
||||
tracklist?: BaseItemDto[] | undefined;
|
||||
index?: number | undefined;
|
||||
queue: Queue;
|
||||
showArtwork?: boolean | undefined;
|
||||
onPress?: () => void | undefined;
|
||||
onLongPress?: () => void | undefined;
|
||||
isNested?: boolean | undefined;
|
||||
invertedColors?: boolean | undefined;
|
||||
prependElement?: React.JSX.Element | undefined;
|
||||
showRemove?: boolean | undefined;
|
||||
onRemove?: () => void | undefined;
|
||||
track: BaseItemDto
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
tracklist?: BaseItemDto[] | undefined
|
||||
index?: number | undefined
|
||||
queue: Queue
|
||||
showArtwork?: boolean | undefined
|
||||
onPress?: () => void | undefined
|
||||
onLongPress?: () => void | undefined
|
||||
isNested?: boolean | undefined
|
||||
invertedColors?: boolean | undefined
|
||||
prependElement?: React.JSX.Element | undefined
|
||||
showRemove?: boolean | undefined
|
||||
onRemove?: () => void | undefined
|
||||
}
|
||||
|
||||
export default function Track({
|
||||
track,
|
||||
tracklist,
|
||||
navigation,
|
||||
index,
|
||||
queue,
|
||||
showArtwork,
|
||||
onPress,
|
||||
onLongPress,
|
||||
isNested,
|
||||
invertedColors,
|
||||
prependElement,
|
||||
showRemove,
|
||||
onRemove
|
||||
} : TrackProps) : React.JSX.Element {
|
||||
track,
|
||||
tracklist,
|
||||
navigation,
|
||||
index,
|
||||
queue,
|
||||
showArtwork,
|
||||
onPress,
|
||||
onLongPress,
|
||||
isNested,
|
||||
invertedColors,
|
||||
prependElement,
|
||||
showRemove,
|
||||
onRemove,
|
||||
}: TrackProps): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
const { width } = useSafeAreaFrame()
|
||||
const { nowPlaying, playQueue, usePlayNewQueue } = usePlayerContext()
|
||||
|
||||
const theme = useTheme();
|
||||
const { width } = useSafeAreaFrame();
|
||||
const { nowPlaying, playQueue, usePlayNewQueue } = usePlayerContext();
|
||||
const isPlaying = nowPlaying?.item.Id === track.Id
|
||||
|
||||
const isPlaying = nowPlaying?.item.Id === track.Id;
|
||||
return (
|
||||
<Theme name={invertedColors ? 'inverted_purple' : undefined}>
|
||||
<XStack
|
||||
alignContent='center'
|
||||
alignItems='center'
|
||||
flex={1}
|
||||
onPress={() => {
|
||||
if (onPress) {
|
||||
onPress()
|
||||
} else {
|
||||
usePlayNewQueue.mutate({
|
||||
track,
|
||||
index,
|
||||
tracklist: tracklist ?? playQueue.map((track) => track.item),
|
||||
queue,
|
||||
queuingType: QueuingType.FromSelection,
|
||||
})
|
||||
}
|
||||
}}
|
||||
onLongPress={
|
||||
onLongPress
|
||||
? () => onLongPress()
|
||||
: () => {
|
||||
navigation.navigate('Details', {
|
||||
item: track,
|
||||
isNested: isNested,
|
||||
})
|
||||
}
|
||||
}
|
||||
paddingVertical={'$2'}
|
||||
marginHorizontal={'$1'}
|
||||
>
|
||||
{prependElement && (
|
||||
<YStack alignContent='center' justifyContent='center' flex={1}>
|
||||
{prependElement}
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
return (
|
||||
<Theme name={invertedColors ? "inverted_purple" : undefined}>
|
||||
<XStack
|
||||
alignContent="center"
|
||||
alignItems="center"
|
||||
flex={1}
|
||||
onPress={() => {
|
||||
if (onPress) {
|
||||
onPress();
|
||||
} else {
|
||||
usePlayNewQueue.mutate({
|
||||
track,
|
||||
index,
|
||||
tracklist: tracklist ?? playQueue.map((track) => track.item),
|
||||
queue,
|
||||
queuingType: QueuingType.FromSelection
|
||||
});
|
||||
}
|
||||
}}
|
||||
onLongPress={
|
||||
onLongPress ? () => onLongPress()
|
||||
: () => {
|
||||
navigation.navigate("Details", {
|
||||
item: track,
|
||||
isNested: isNested
|
||||
})
|
||||
}
|
||||
}
|
||||
paddingVertical={"$2"}
|
||||
marginHorizontal={"$1"}
|
||||
>
|
||||
<XStack
|
||||
alignContent='center'
|
||||
justifyContent='center'
|
||||
flex={showArtwork ? 2 : 1}
|
||||
minHeight={showArtwork ? width / 9 : 'unset'}
|
||||
>
|
||||
{showArtwork ? (
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(track.AlbumId!)}
|
||||
style={{
|
||||
width: getToken('$12'),
|
||||
height: getToken('$12'),
|
||||
borderRadius: getToken('$1'),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Text color={isPlaying ? getTokens().color.telemagenta : theme.color}>
|
||||
{track.IndexNumber?.toString() ?? ''}
|
||||
</Text>
|
||||
)}
|
||||
</XStack>
|
||||
|
||||
{ prependElement && (
|
||||
<YStack
|
||||
alignContent="center"
|
||||
justifyContent="center"
|
||||
flex={1}
|
||||
>
|
||||
{ prependElement }
|
||||
</YStack>
|
||||
)}
|
||||
<YStack alignContent='center' justifyContent='flex-start' flex={6}>
|
||||
<Text
|
||||
bold
|
||||
color={isPlaying ? getTokens().color.telemagenta : theme.color}
|
||||
lineBreakStrategyIOS='standard'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{track.Name ?? 'Untitled Track'}
|
||||
</Text>
|
||||
|
||||
<XStack
|
||||
alignContent="center"
|
||||
justifyContent="center"
|
||||
flex={showArtwork ? 2 : 1}
|
||||
minHeight={showArtwork ? width / 9 : "unset"}
|
||||
>
|
||||
{ showArtwork ? (
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(track.AlbumId!)}
|
||||
style={{
|
||||
width: getToken("$12"),
|
||||
height: getToken("$12"),
|
||||
borderRadius: getToken("$1")
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
color={isPlaying ? getTokens().color.telemagenta : theme.color}
|
||||
>
|
||||
{ track.IndexNumber?.toString() ?? "" }
|
||||
</Text>
|
||||
)}
|
||||
</XStack>
|
||||
{(showArtwork || (track.ArtistCount ?? 0 > 1)) && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{track.Artists?.join(', ') ?? ''}
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
|
||||
<YStack
|
||||
alignContent="center"
|
||||
justifyContent="flex-start"
|
||||
flex={6}
|
||||
>
|
||||
<Text
|
||||
bold
|
||||
color={isPlaying ? getTokens().color.telemagenta : theme.color}
|
||||
lineBreakStrategyIOS="standard"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{ track.Name ?? "Untitled Track" }
|
||||
</Text>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
alignContent='center'
|
||||
flex={3}
|
||||
>
|
||||
<FavoriteIcon item={track} />
|
||||
|
||||
{ (showArtwork || (track.ArtistCount ?? 0 > 1)) && (
|
||||
<Text
|
||||
lineBreakStrategyIOS="standard"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{ track.Artists?.join(", ") ?? "" }
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
<YStack alignContent='center' justifyContent='space-around'>
|
||||
<RunTimeTicks>{track.RunTimeTicks}</RunTimeTicks>
|
||||
</YStack>
|
||||
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
alignContent="center"
|
||||
flex={3}
|
||||
>
|
||||
<FavoriteIcon item={track} />
|
||||
|
||||
<YStack
|
||||
alignContent="center"
|
||||
justifyContent="space-around"
|
||||
>
|
||||
<RunTimeTicks>{ track.RunTimeTicks }</RunTimeTicks>
|
||||
</YStack>
|
||||
|
||||
<YStack
|
||||
alignContent="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Icon
|
||||
name={showRemove ? "close" : "dots-horizontal"}
|
||||
onPress={() => {
|
||||
if (showRemove) {
|
||||
if (onRemove)
|
||||
onRemove()
|
||||
}
|
||||
else {
|
||||
navigation.navigate("Details", {
|
||||
item: track,
|
||||
isNested: isNested
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
</YStack>
|
||||
</XStack>
|
||||
</XStack>
|
||||
</Theme>
|
||||
)
|
||||
}
|
||||
<YStack alignContent='center' justifyContent='center'>
|
||||
<Icon
|
||||
name={showRemove ? 'close' : 'dots-horizontal'}
|
||||
onPress={() => {
|
||||
if (showRemove) {
|
||||
if (onRemove) onRemove()
|
||||
} else {
|
||||
navigation.navigate('Details', {
|
||||
item: track,
|
||||
isNested: isNested,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</XStack>
|
||||
</Theme>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import { Button as TamaguiButton, ButtonProps as TamaguiButtonProps } from 'tamagui';
|
||||
import { Button as TamaguiButton, ButtonProps as TamaguiButtonProps } from 'tamagui'
|
||||
|
||||
interface ButtonProps extends TamaguiButtonProps {
|
||||
children?: Element | string | undefined;
|
||||
onPress?: () => void | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
danger?: boolean | undefined;
|
||||
children?: Element | string | undefined
|
||||
onPress?: () => void | undefined
|
||||
disabled?: boolean | undefined
|
||||
danger?: boolean | undefined
|
||||
}
|
||||
|
||||
export default function Button(props: ButtonProps): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<TamaguiButton
|
||||
bordered
|
||||
opacity={props.disabled ? 0.5 : 1}
|
||||
marginVertical={30}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<TamaguiButton bordered opacity={props.disabled ? 0.5 : 1} marginVertical={30} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import React from "react"
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons"
|
||||
import { CheckboxProps, XStack, Checkbox, Label } from "tamagui"
|
||||
import React from 'react'
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||
import { CheckboxProps, XStack, Checkbox, Label } from 'tamagui'
|
||||
|
||||
export function CheckboxWithLabel({
|
||||
size,
|
||||
label = 'Toggle',
|
||||
...checkboxProps
|
||||
}: CheckboxProps & { label?: string }) {
|
||||
const id = `checkbox-${(size || '').toString().slice(1)}`
|
||||
return (
|
||||
<XStack width={150} alignItems="center" gap="$4">
|
||||
<Checkbox id={id} size={size} {...checkboxProps}>
|
||||
<Checkbox.Indicator>
|
||||
<Icon name="check" />
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox>
|
||||
|
||||
<Label size={size} htmlFor={id}>
|
||||
{label}
|
||||
</Label>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
size,
|
||||
label = 'Toggle',
|
||||
...checkboxProps
|
||||
}: CheckboxProps & { label?: string }) {
|
||||
const id = `checkbox-${(size || '').toString().slice(1)}`
|
||||
return (
|
||||
<XStack width={150} alignItems='center' gap='$4'>
|
||||
<Checkbox id={id} size={size} {...checkboxProps}>
|
||||
<Checkbox.Indicator>
|
||||
<Icon name='check' />
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox>
|
||||
|
||||
<Label size={size} htmlFor={id}>
|
||||
{label}
|
||||
</Label>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,56 +1,48 @@
|
||||
import React from "react";
|
||||
import { Square, Theme } from "tamagui";
|
||||
import Icon from "./icon";
|
||||
import { TouchableOpacity } from "react-native";
|
||||
import { Text } from "./text";
|
||||
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;
|
||||
largeIcon?: boolean | undefined;
|
||||
onPress: () => void
|
||||
name: string
|
||||
title?: string | undefined
|
||||
circular?: boolean | undefined
|
||||
size?: number
|
||||
largeIcon?: boolean | undefined
|
||||
}
|
||||
|
||||
export default function IconButton({
|
||||
name,
|
||||
onPress,
|
||||
title,
|
||||
circular,
|
||||
size,
|
||||
largeIcon
|
||||
} : IconButtonProps) : React.JSX.Element {
|
||||
name,
|
||||
onPress,
|
||||
title,
|
||||
circular,
|
||||
size,
|
||||
largeIcon,
|
||||
}: IconButtonProps): React.JSX.Element {
|
||||
return (
|
||||
<Theme name={'inverted_purple'}>
|
||||
<TouchableOpacity>
|
||||
<Square
|
||||
animation={'bouncy'}
|
||||
borderRadius={!circular ? '$4' : undefined}
|
||||
circular={circular}
|
||||
elevate
|
||||
hoverStyle={{ scale: 0.925 }}
|
||||
pressStyle={{ scale: 0.875 }}
|
||||
onPress={onPress}
|
||||
width={size}
|
||||
height={size}
|
||||
alignContent='center'
|
||||
justifyContent='center'
|
||||
backgroundColor={'$background'}
|
||||
>
|
||||
<Icon large={largeIcon} small={!largeIcon} name={name} color={'$color'} />
|
||||
|
||||
return (
|
||||
<Theme name={"inverted_purple"}>
|
||||
<TouchableOpacity>
|
||||
<Square
|
||||
animation={"bouncy"}
|
||||
borderRadius={!circular ? "$4" : undefined}
|
||||
circular={circular}
|
||||
elevate
|
||||
hoverStyle={{ scale: 0.925 }}
|
||||
pressStyle={{ scale: 0.875 }}
|
||||
onPress={onPress}
|
||||
width={size}
|
||||
height={size}
|
||||
alignContent="center"
|
||||
justifyContent="center"
|
||||
backgroundColor={"$background"}
|
||||
>
|
||||
<Icon
|
||||
large={largeIcon}
|
||||
small={!largeIcon}
|
||||
name={name}
|
||||
color={"$color"}
|
||||
/>
|
||||
|
||||
{ title && (
|
||||
<Text>{ title }</Text>
|
||||
)}
|
||||
</Square>
|
||||
</TouchableOpacity>
|
||||
</Theme>
|
||||
)
|
||||
}
|
||||
{title && <Text>{title}</Text>}
|
||||
</Square>
|
||||
</TouchableOpacity>
|
||||
</Theme>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,57 +1,50 @@
|
||||
import { Card, getTokens, View } from "tamagui";
|
||||
import { H2, H4, H5 } from "./text";
|
||||
import Icon from "./icon";
|
||||
import { Card, getTokens, View } from 'tamagui'
|
||||
import { H2, H4, H5 } from './text'
|
||||
import Icon from './icon'
|
||||
|
||||
interface IconCardProps {
|
||||
name: string;
|
||||
circular?: boolean | undefined;
|
||||
onPress: () => void;
|
||||
width?: number | undefined
|
||||
caption?: string | undefined;
|
||||
largeIcon?: boolean | undefined
|
||||
name: string
|
||||
circular?: boolean | undefined
|
||||
onPress: () => void
|
||||
width?: number | undefined
|
||||
caption?: string | undefined
|
||||
largeIcon?: boolean | undefined
|
||||
}
|
||||
|
||||
export default function IconCard({
|
||||
name,
|
||||
circular = false,
|
||||
onPress,
|
||||
width,
|
||||
caption,
|
||||
largeIcon
|
||||
}: IconCardProps) : React.JSX.Element {
|
||||
|
||||
return (
|
||||
<View
|
||||
alignItems="center"
|
||||
margin={5}
|
||||
>
|
||||
<Card
|
||||
animation="bouncy"
|
||||
borderRadius={circular ? 300 : 5}
|
||||
hoverStyle={{ scale: 0.925 }}
|
||||
pressStyle={{ scale: 0.875 }}
|
||||
width={width ? width : "$12"}
|
||||
height={width ? width : "$12"}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Card.Header>
|
||||
<H5 color={getTokens().color.purpleDark}>{ caption ?? "" }</H5>
|
||||
<Icon
|
||||
color={getTokens().color.purpleDark.val}
|
||||
name={name}
|
||||
large={largeIcon}
|
||||
small={!largeIcon}
|
||||
/>
|
||||
</Card.Header>
|
||||
<Card.Footer padded>
|
||||
</Card.Footer>
|
||||
<Card.Background
|
||||
backgroundColor={getTokens().color.telemagenta}
|
||||
borderRadius={circular ? 300 : 5}
|
||||
>
|
||||
|
||||
</Card.Background>
|
||||
</Card>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
export default function IconCard({
|
||||
name,
|
||||
circular = false,
|
||||
onPress,
|
||||
width,
|
||||
caption,
|
||||
largeIcon,
|
||||
}: IconCardProps): React.JSX.Element {
|
||||
return (
|
||||
<View alignItems='center' margin={5}>
|
||||
<Card
|
||||
animation='bouncy'
|
||||
borderRadius={circular ? 300 : 5}
|
||||
hoverStyle={{ scale: 0.925 }}
|
||||
pressStyle={{ scale: 0.875 }}
|
||||
width={width ? width : '$12'}
|
||||
height={width ? width : '$12'}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Card.Header>
|
||||
<H5 color={getTokens().color.purpleDark}>{caption ?? ''}</H5>
|
||||
<Icon
|
||||
color={getTokens().color.purpleDark.val}
|
||||
name={name}
|
||||
large={largeIcon}
|
||||
small={!largeIcon}
|
||||
/>
|
||||
</Card.Header>
|
||||
<Card.Footer padded></Card.Footer>
|
||||
<Card.Background
|
||||
backgroundColor={getTokens().color.telemagenta}
|
||||
borderRadius={circular ? 300 : 5}
|
||||
></Card.Background>
|
||||
</Card>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
import React from "react"
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"
|
||||
import { useTheme } from "tamagui";
|
||||
import React from 'react'
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||
import { useTheme } from 'tamagui'
|
||||
|
||||
const smallSize = 24;
|
||||
const smallSize = 24
|
||||
|
||||
const regularSize = 36;
|
||||
const regularSize = 36
|
||||
|
||||
const largeSize = 48
|
||||
|
||||
const extraLargeSize = 96
|
||||
|
||||
export default function Icon({
|
||||
name,
|
||||
onPress,
|
||||
small,
|
||||
large,
|
||||
extraLarge,
|
||||
color
|
||||
}: {
|
||||
name: string,
|
||||
onPress?: () => void,
|
||||
small?: boolean,
|
||||
large?: boolean,
|
||||
extraLarge?: boolean,
|
||||
color?: string | undefined
|
||||
}) : React.JSX.Element {
|
||||
|
||||
const theme = useTheme();
|
||||
const size = extraLarge? extraLargeSize : large ? largeSize : small ? smallSize : regularSize
|
||||
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
color={color ? color
|
||||
: theme.color.val
|
||||
}
|
||||
name={name}
|
||||
onPress={onPress}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default function Icon({
|
||||
name,
|
||||
onPress,
|
||||
small,
|
||||
large,
|
||||
extraLarge,
|
||||
color,
|
||||
}: {
|
||||
name: string
|
||||
onPress?: () => void
|
||||
small?: boolean
|
||||
large?: boolean
|
||||
extraLarge?: boolean
|
||||
color?: string | undefined
|
||||
}): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
const size = extraLarge ? extraLargeSize : large ? largeSize : small ? smallSize : regularSize
|
||||
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
color={color ? color : theme.color.val}
|
||||
name={name}
|
||||
onPress={onPress}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Input as TamaguiInput, InputProps as TamaguiInputProps, XStack, YStack} from 'tamagui';
|
||||
import React from 'react'
|
||||
import { Input as TamaguiInput, InputProps as TamaguiInputProps, XStack, YStack } from 'tamagui'
|
||||
|
||||
interface InputProps extends TamaguiInputProps {
|
||||
prependElement?: React.JSX.Element | undefined;
|
||||
prependElement?: React.JSX.Element | undefined
|
||||
}
|
||||
|
||||
export default function Input(props: InputProps): React.JSX.Element {
|
||||
return (
|
||||
<XStack>
|
||||
{props.prependElement && (
|
||||
<YStack flex={1} alignItems='center' justifyContent='center'>
|
||||
{props.prependElement}
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
return (
|
||||
<XStack>
|
||||
|
||||
|
||||
{ props.prependElement && (
|
||||
<YStack
|
||||
flex={1}
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
>
|
||||
{ props.prependElement }
|
||||
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
<TamaguiInput
|
||||
flex={props.prependElement ? 8 : 1}
|
||||
{...props}
|
||||
clearButtonMode="always"
|
||||
/>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
<TamaguiInput flex={props.prependElement ? 8 : 1} {...props} clearButtonMode='always' />
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import { SizeTokens, XStack, RadioGroup } from "tamagui"
|
||||
import { Label } from "./text"
|
||||
import { SizeTokens, XStack, RadioGroup } from 'tamagui'
|
||||
import { Label } from './text'
|
||||
|
||||
export function RadioGroupItemWithLabel(props: {
|
||||
size: SizeTokens
|
||||
value: string
|
||||
label: string
|
||||
}) {
|
||||
const id = `radiogroup-${props.value}`
|
||||
return (
|
||||
<XStack width={300} alignItems="center" space="$4">
|
||||
<RadioGroup.Item value={props.value} id={id} size={props.size}>
|
||||
<RadioGroup.Indicator />
|
||||
</RadioGroup.Item>
|
||||
|
||||
<Label size={props.size} htmlFor={id}>
|
||||
{props.label}
|
||||
</Label>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
export function RadioGroupItemWithLabel(props: { size: SizeTokens; value: string; label: string }) {
|
||||
const id = `radiogroup-${props.value}`
|
||||
return (
|
||||
<XStack width={300} alignItems='center' space='$4'>
|
||||
<RadioGroup.Item value={props.value} id={id} size={props.size}>
|
||||
<RadioGroup.Indicator />
|
||||
</RadioGroup.Item>
|
||||
|
||||
<Label size={props.size} htmlFor={id}>
|
||||
{props.label}
|
||||
</Label>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,56 +1,57 @@
|
||||
import React from "react";
|
||||
import { SliderProps as TamaguiSliderProps, Slider as TamaguiSlider, styled, Slider, getTokens, getToken } from "tamagui";
|
||||
import React from 'react'
|
||||
import {
|
||||
SliderProps as TamaguiSliderProps,
|
||||
Slider as TamaguiSlider,
|
||||
styled,
|
||||
Slider,
|
||||
getTokens,
|
||||
getToken,
|
||||
} from 'tamagui'
|
||||
|
||||
interface SliderProps {
|
||||
value?: number | undefined;
|
||||
max: number;
|
||||
width?: number | undefined
|
||||
props: TamaguiSliderProps
|
||||
value?: number | undefined
|
||||
max: number
|
||||
width?: number | undefined
|
||||
props: TamaguiSliderProps
|
||||
}
|
||||
|
||||
const JellifyActiveSliderTrack = styled(Slider.TrackActive, {
|
||||
backgroundColor: getTokens().color.$telemagenta
|
||||
backgroundColor: getTokens().color.$telemagenta,
|
||||
})
|
||||
|
||||
const JellifySliderThumb = styled(Slider.Thumb, {
|
||||
backgroundColor: getToken("$color.purpleDark"),
|
||||
borderColor: getToken("$color.amethyst"),
|
||||
backgroundColor: getToken('$color.purpleDark'),
|
||||
borderColor: getToken('$color.amethyst'),
|
||||
})
|
||||
|
||||
const JellifySliderTrack = styled(Slider.Track, {
|
||||
backgroundColor: getToken("$color.amethyst")
|
||||
});
|
||||
export function HorizontalSlider({
|
||||
value,
|
||||
max,
|
||||
width,
|
||||
props
|
||||
}: SliderProps) : React.JSX.Element {
|
||||
|
||||
return (
|
||||
<TamaguiSlider
|
||||
width={width}
|
||||
value={value ? [value] : []}
|
||||
max={max}
|
||||
step={1}
|
||||
orientation="horizontal"
|
||||
marginHorizontal={10}
|
||||
{ ...props }
|
||||
>
|
||||
<JellifySliderTrack size="$4">
|
||||
<JellifyActiveSliderTrack size={"$4"} />
|
||||
</JellifySliderTrack>
|
||||
<JellifySliderThumb
|
||||
circular
|
||||
index={0}
|
||||
size={"$1"}
|
||||
hitSlop={{
|
||||
top: 35,
|
||||
right: 70,
|
||||
bottom: 70,
|
||||
left: 70
|
||||
}}
|
||||
/>
|
||||
</TamaguiSlider>
|
||||
)
|
||||
}
|
||||
backgroundColor: getToken('$color.amethyst'),
|
||||
})
|
||||
export function HorizontalSlider({ value, max, width, props }: SliderProps): React.JSX.Element {
|
||||
return (
|
||||
<TamaguiSlider
|
||||
width={width}
|
||||
value={value ? [value] : []}
|
||||
max={max}
|
||||
step={1}
|
||||
orientation='horizontal'
|
||||
marginHorizontal={10}
|
||||
{...props}
|
||||
>
|
||||
<JellifySliderTrack size='$4'>
|
||||
<JellifyActiveSliderTrack size={'$4'} />
|
||||
</JellifySliderTrack>
|
||||
<JellifySliderThumb
|
||||
circular
|
||||
index={0}
|
||||
size={'$1'}
|
||||
hitSlop={{
|
||||
top: 35,
|
||||
right: 70,
|
||||
bottom: 70,
|
||||
left: 70,
|
||||
}}
|
||||
/>
|
||||
</TamaguiSlider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,46 +1,44 @@
|
||||
import { SizeTokens, XStack, Separator, Switch, Theme, styled, getToken } from "tamagui";
|
||||
import { Label } from "./text";
|
||||
import { useColorScheme } from "react-native";
|
||||
import { SizeTokens, XStack, Separator, Switch, Theme, styled, getToken } from 'tamagui'
|
||||
import { Label } from './text'
|
||||
import { useColorScheme } from 'react-native'
|
||||
|
||||
interface SwitchWithLabelProps {
|
||||
onCheckedChange: (value: boolean) => void,
|
||||
size: SizeTokens
|
||||
checked: boolean;
|
||||
label: string;
|
||||
width?: number | undefined;
|
||||
onCheckedChange: (value: boolean) => void
|
||||
size: SizeTokens
|
||||
checked: boolean
|
||||
label: string
|
||||
width?: number | undefined
|
||||
}
|
||||
|
||||
const JellifySliderThumb = styled(Switch.Thumb, {
|
||||
borderColor: getToken("$color.amethyst"),
|
||||
backgroundColor: getToken("$color.purpleDark")
|
||||
borderColor: getToken('$color.amethyst'),
|
||||
backgroundColor: getToken('$color.purpleDark'),
|
||||
})
|
||||
|
||||
export function SwitchWithLabel(props: SwitchWithLabelProps) {
|
||||
const isDarkMode = useColorScheme() === 'dark'
|
||||
|
||||
const isDarkMode = useColorScheme() === 'dark'
|
||||
|
||||
const id = `switch-${props.size.toString().slice(1)}-${props.checked ?? ''}}`
|
||||
return (
|
||||
<XStack alignItems="center" gap="$3">
|
||||
<Label
|
||||
|
||||
size={props.size}
|
||||
htmlFor={id}
|
||||
>
|
||||
{props.label}
|
||||
</Label>
|
||||
<Separator minHeight={20} vertical />
|
||||
<Switch
|
||||
id={id}
|
||||
size={props.size}
|
||||
checked={props.checked}
|
||||
onCheckedChange={(checked: boolean) => props.onCheckedChange(checked)}
|
||||
backgroundColor={props.checked ? getToken("$color.telemagenta") : getToken("$color.purpleGray")}
|
||||
borderColor={isDarkMode ? getToken("$color.amethyst") : getToken("$color.purpleDark")}
|
||||
>
|
||||
<JellifySliderThumb animation="bouncy" />
|
||||
</Switch>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
const id = `switch-${props.size.toString().slice(1)}-${props.checked ?? ''}}`
|
||||
return (
|
||||
<XStack alignItems='center' gap='$3'>
|
||||
<Label size={props.size} htmlFor={id}>
|
||||
{props.label}
|
||||
</Label>
|
||||
<Separator minHeight={20} vertical />
|
||||
<Switch
|
||||
id={id}
|
||||
size={props.size}
|
||||
checked={props.checked}
|
||||
onCheckedChange={(checked: boolean) => props.onCheckedChange(checked)}
|
||||
backgroundColor={
|
||||
props.checked ? getToken('$color.telemagenta') : getToken('$color.purpleGray')
|
||||
}
|
||||
borderColor={
|
||||
isDarkMode ? getToken('$color.amethyst') : getToken('$color.purpleDark')
|
||||
}
|
||||
>
|
||||
<JellifySliderThumb animation='bouncy' />
|
||||
</Switch>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,101 +1,84 @@
|
||||
import {
|
||||
H1 as TamaguiH1,
|
||||
H2 as TamaguiH2,
|
||||
H3 as TamaguiH3,
|
||||
H4 as TamaguiH4,
|
||||
H5 as TamaguiH5,
|
||||
Label as TamaguiLabel,
|
||||
SizeTokens,
|
||||
Paragraph,
|
||||
TextProps as TamaguiTextProps
|
||||
} from "tamagui"
|
||||
import {
|
||||
H1 as TamaguiH1,
|
||||
H2 as TamaguiH2,
|
||||
H3 as TamaguiH3,
|
||||
H4 as TamaguiH4,
|
||||
H5 as TamaguiH5,
|
||||
Label as TamaguiLabel,
|
||||
SizeTokens,
|
||||
Paragraph,
|
||||
TextProps as TamaguiTextProps,
|
||||
} from 'tamagui'
|
||||
|
||||
interface LabelProps {
|
||||
htmlFor: string,
|
||||
children: string,
|
||||
size: SizeTokens
|
||||
htmlFor: string
|
||||
children: string
|
||||
size: SizeTokens
|
||||
}
|
||||
|
||||
export function Label(props: LabelProps): React.JSX.Element {
|
||||
return (
|
||||
<TamaguiLabel fontWeight={600} htmlFor={props.htmlFor} justifyContent="flex-end">{ props.children }</TamaguiLabel>
|
||||
)
|
||||
return (
|
||||
<TamaguiLabel fontWeight={600} htmlFor={props.htmlFor} justifyContent='flex-end'>
|
||||
{props.children}
|
||||
</TamaguiLabel>
|
||||
)
|
||||
}
|
||||
|
||||
export function H1({ children }: { children: string }): React.JSX.Element {
|
||||
return (
|
||||
<TamaguiH1
|
||||
fontWeight={900}
|
||||
marginBottom={10}
|
||||
>
|
||||
{ children }
|
||||
</TamaguiH1>
|
||||
)
|
||||
return (
|
||||
<TamaguiH1 fontWeight={900} marginBottom={10}>
|
||||
{children}
|
||||
</TamaguiH1>
|
||||
)
|
||||
}
|
||||
|
||||
export function H2(props: TamaguiTextProps): React.JSX.Element {
|
||||
return (
|
||||
<TamaguiH2
|
||||
fontWeight={800}
|
||||
marginVertical={7}
|
||||
{...props}
|
||||
>
|
||||
{ props.children }
|
||||
</TamaguiH2>
|
||||
)
|
||||
return (
|
||||
<TamaguiH2 fontWeight={800} marginVertical={7} {...props}>
|
||||
{props.children}
|
||||
</TamaguiH2>
|
||||
)
|
||||
}
|
||||
|
||||
export function H3(props: TamaguiTextProps): React.JSX.Element {
|
||||
return (
|
||||
<TamaguiH3
|
||||
fontWeight={800}
|
||||
marginVertical={5}
|
||||
{...props}
|
||||
>
|
||||
{ props.children }
|
||||
</TamaguiH3>
|
||||
)
|
||||
return (
|
||||
<TamaguiH3 fontWeight={800} marginVertical={5} {...props}>
|
||||
{props.children}
|
||||
</TamaguiH3>
|
||||
)
|
||||
}
|
||||
|
||||
export function H4(props: TamaguiTextProps): React.JSX.Element {
|
||||
return (
|
||||
<TamaguiH4
|
||||
fontWeight={800}
|
||||
marginVertical={3}
|
||||
{...props}
|
||||
>
|
||||
{ props.children }
|
||||
</TamaguiH4>
|
||||
)
|
||||
return (
|
||||
<TamaguiH4 fontWeight={800} marginVertical={3} {...props}>
|
||||
{props.children}
|
||||
</TamaguiH4>
|
||||
)
|
||||
}
|
||||
|
||||
export function H5(props: TamaguiTextProps): React.JSX.Element {
|
||||
return (
|
||||
<TamaguiH5
|
||||
{...props}
|
||||
fontWeight={800}
|
||||
marginVertical={2}
|
||||
>
|
||||
{ props.children }
|
||||
</TamaguiH5>
|
||||
)
|
||||
return (
|
||||
<TamaguiH5 {...props} fontWeight={800} marginVertical={2}>
|
||||
{props.children}
|
||||
</TamaguiH5>
|
||||
)
|
||||
}
|
||||
|
||||
interface TextProps extends TamaguiTextProps {
|
||||
bold?: boolean | undefined
|
||||
children: string;
|
||||
bold?: boolean | undefined
|
||||
children: string
|
||||
}
|
||||
|
||||
export function Text(props: TextProps): React.JSX.Element {
|
||||
return (
|
||||
<Paragraph
|
||||
fontWeight={props.bold ? 800 : 600}
|
||||
fontSize="$4"
|
||||
lineBreakMode="clip"
|
||||
userSelect="none"
|
||||
{...props}
|
||||
>
|
||||
{ props.children }
|
||||
</Paragraph>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Paragraph
|
||||
fontWeight={props.bold ? 800 : 600}
|
||||
fontSize='$4'
|
||||
lineBreakMode='clip'
|
||||
userSelect='none'
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</Paragraph>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,48 +1,47 @@
|
||||
import { convertRunTimeTicksToSeconds } from "../../../helpers/runtimeticks";
|
||||
import { Text } from "./text";
|
||||
import React from "react";
|
||||
import { convertRunTimeTicksToSeconds } from '../../../helpers/runtimeticks'
|
||||
import { Text } from './text'
|
||||
import React from 'react'
|
||||
|
||||
export function RunTimeSeconds({ children }: { children: number }) : React.JSX.Element {
|
||||
return <Text bold>{ calculateRunTimeFromSeconds(children) }</Text>
|
||||
export function RunTimeSeconds({ children }: { children: number }): React.JSX.Element {
|
||||
return <Text bold>{calculateRunTimeFromSeconds(children)}</Text>
|
||||
}
|
||||
|
||||
export function RunTimeTicks({ children } : { children?: number | null | undefined }) : React.JSX.Element {
|
||||
if (!children)
|
||||
return <Text>0:00</Text>
|
||||
export function RunTimeTicks({
|
||||
children,
|
||||
}: {
|
||||
children?: number | null | undefined
|
||||
}): React.JSX.Element {
|
||||
if (!children) return <Text>0:00</Text>
|
||||
|
||||
const time = calculateRunTimeFromTicks(children);
|
||||
const time = calculateRunTimeFromTicks(children)
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={{display: "block"}}
|
||||
color="$borderColor"
|
||||
>
|
||||
{ time }
|
||||
</Text>
|
||||
)
|
||||
return (
|
||||
<Text style={{ display: 'block' }} color='$borderColor'>
|
||||
{time}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
function calculateRunTimeFromSeconds(seconds: number) : string {
|
||||
const runTimeHours = Math.floor(seconds / 3600);
|
||||
const runTimeMinutes = Math.floor((seconds % 3600) / 60)
|
||||
const runTimeSeconds = Math.floor(seconds % 60);
|
||||
function calculateRunTimeFromSeconds(seconds: number): string {
|
||||
const runTimeHours = Math.floor(seconds / 3600)
|
||||
const runTimeMinutes = Math.floor((seconds % 3600) / 60)
|
||||
const runTimeSeconds = Math.floor(seconds % 60)
|
||||
|
||||
return (runTimeHours != 0 ? `${padRunTimeNumber(runTimeHours)}:` : "") +
|
||||
(runTimeHours != 0 ? `${padRunTimeNumber(runTimeMinutes)}:` : `${runTimeMinutes}:`) +
|
||||
(padRunTimeNumber(runTimeSeconds));
|
||||
return (
|
||||
(runTimeHours != 0 ? `${padRunTimeNumber(runTimeHours)}:` : '') +
|
||||
(runTimeHours != 0 ? `${padRunTimeNumber(runTimeMinutes)}:` : `${runTimeMinutes}:`) +
|
||||
padRunTimeNumber(runTimeSeconds)
|
||||
)
|
||||
}
|
||||
|
||||
function calculateRunTimeFromTicks(runTimeTicks: number) : string {
|
||||
function calculateRunTimeFromTicks(runTimeTicks: number): string {
|
||||
const runTimeTotalSeconds = convertRunTimeTicksToSeconds(runTimeTicks)
|
||||
|
||||
|
||||
const runTimeTotalSeconds = convertRunTimeTicksToSeconds(runTimeTicks);
|
||||
|
||||
return calculateRunTimeFromSeconds(runTimeTotalSeconds);
|
||||
return calculateRunTimeFromSeconds(runTimeTotalSeconds)
|
||||
}
|
||||
|
||||
function padRunTimeNumber(number: number) : string {
|
||||
if (number >= 10)
|
||||
return `${number}`
|
||||
function padRunTimeNumber(number: number): string {
|
||||
if (number >= 10) return `${number}`
|
||||
|
||||
return `0${number}`;
|
||||
}
|
||||
return `0${number}`
|
||||
}
|
||||
|
||||
@@ -1,50 +1,44 @@
|
||||
import { StackParamList } from "../types";
|
||||
import { ScrollView, RefreshControl } from "react-native";
|
||||
import { YStack, XStack, Separator } from "tamagui";
|
||||
import Playlists from "./helpers/playlists";
|
||||
import RecentArtists from "./helpers/recent-artists";
|
||||
import RecentlyPlayed from "./helpers/recently-played";
|
||||
import { useHomeContext } from "./provider";
|
||||
import { H3 } from "../Global/helpers/text";
|
||||
import Client from "../../api/client";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from '../types'
|
||||
import { ScrollView, RefreshControl } from 'react-native'
|
||||
import { YStack, XStack, Separator } from 'tamagui'
|
||||
import Playlists from './helpers/playlists'
|
||||
import RecentArtists from './helpers/recent-artists'
|
||||
import RecentlyPlayed from './helpers/recently-played'
|
||||
import { useHomeContext } from './provider'
|
||||
import { H3 } from '../Global/helpers/text'
|
||||
import Client from '../../api/client'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
|
||||
export function ProvidedHome({
|
||||
navigation
|
||||
} : {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
export function ProvidedHome({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { refreshing: refetching, onRefresh } = useHomeContext()
|
||||
|
||||
const { refreshing: refetching, onRefresh } = useHomeContext()
|
||||
return (
|
||||
<ScrollView
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
refreshControl={<RefreshControl refreshing={refetching} onRefresh={onRefresh} />}
|
||||
removeClippedSubviews // Save memory usage
|
||||
>
|
||||
<YStack alignContent='flex-start'>
|
||||
<XStack margin={'$2'}>
|
||||
<H3>{`Hi, ${Client.user!.name}`}</H3>
|
||||
</XStack>
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refetching}
|
||||
onRefresh={onRefresh}
|
||||
/>
|
||||
}
|
||||
removeClippedSubviews // Save memory usage
|
||||
>
|
||||
<YStack alignContent='flex-start'>
|
||||
<XStack margin={"$2"}>
|
||||
<H3>{`Hi, ${Client.user!.name}`}</H3>
|
||||
</XStack>
|
||||
<Separator marginVertical={'$2'} />
|
||||
|
||||
<Separator marginVertical={"$2"} />
|
||||
<RecentArtists navigation={navigation} />
|
||||
|
||||
<RecentArtists navigation={navigation} />
|
||||
<Separator marginVertical={'$3'} />
|
||||
|
||||
<Separator marginVertical={"$3"} />
|
||||
<RecentlyPlayed navigation={navigation} />
|
||||
|
||||
<RecentlyPlayed navigation={navigation} />
|
||||
<Separator marginVertical={'$3'} />
|
||||
|
||||
<Separator marginVertical={"$3"} />
|
||||
|
||||
<Playlists navigation={navigation}/>
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
<Playlists navigation={navigation} />
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { ItemCard } from "../../Global/components/item-card";
|
||||
import { H2 } from "../../../components/Global/helpers/text";
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import React from "react";
|
||||
import { FlatList } from "react-native";
|
||||
import { View, XStack } from "tamagui";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { QueryKeys } from "../../../enums/query-keys";
|
||||
import { fetchUserPlaylists } from "../../../api/queries/functions/playlists";
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { ItemCard } from '../../Global/components/item-card'
|
||||
import { H2 } from '../../../components/Global/helpers/text'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import React from 'react'
|
||||
import { FlatList } from 'react-native'
|
||||
import { View, XStack } from 'tamagui'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchUserPlaylists } from '../../../api/queries/functions/playlists'
|
||||
|
||||
export default function Playlists({ navigation }: { navigation: NativeStackNavigationProp<StackParamList>}) : React.JSX.Element {
|
||||
export default function Playlists({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { data: playlists } = useQuery({
|
||||
queryKey: [QueryKeys.UserPlaylists],
|
||||
queryFn: () => fetchUserPlaylists(),
|
||||
})
|
||||
|
||||
const { data: playlists } = useQuery({
|
||||
queryKey: [QueryKeys.UserPlaylists],
|
||||
queryFn: () => fetchUserPlaylists()
|
||||
});
|
||||
|
||||
return (
|
||||
<View>
|
||||
<XStack alignContent="center" marginHorizontal={"$2"}>
|
||||
<H2 textAlign="left">Your Playlists</H2>
|
||||
</XStack>
|
||||
<FlatList horizontal
|
||||
data={playlists}
|
||||
renderItem={({ item: playlist }) =>
|
||||
<ItemCard
|
||||
item={playlist}
|
||||
size={"$11"}
|
||||
squared
|
||||
caption={playlist.Name ?? "Untitled Playlist"}
|
||||
onPress={() => {
|
||||
navigation.navigate('Playlist', {
|
||||
playlist
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
<XStack alignContent='center' marginHorizontal={'$2'}>
|
||||
<H2 textAlign='left'>Your Playlists</H2>
|
||||
</XStack>
|
||||
<FlatList
|
||||
horizontal
|
||||
data={playlists}
|
||||
renderItem={({ item: playlist }) => (
|
||||
<ItemCard
|
||||
item={playlist}
|
||||
size={'$11'}
|
||||
squared
|
||||
caption={playlist.Name ?? 'Untitled Playlist'}
|
||||
onPress={() => {
|
||||
navigation.navigate('Playlist', {
|
||||
playlist,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,42 +1,44 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { View } from "tamagui";
|
||||
import { useHomeContext } from "../provider";
|
||||
import { H2 } from "../../Global/helpers/text";
|
||||
import { StackParamList } from "../../types";
|
||||
import { ItemCard } from "../../Global/components/item-card";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import HorizontalCardList from "../../../components/Global/components/horizontal-list";
|
||||
import { QueryKeys } from "../../../enums/query-keys";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import React, { useMemo } from 'react'
|
||||
import { View } from 'tamagui'
|
||||
import { useHomeContext } from '../provider'
|
||||
import { H2 } from '../../Global/helpers/text'
|
||||
import { StackParamList } from '../../types'
|
||||
import { ItemCard } from '../../Global/components/item-card'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
export default function RecentArtists({ navigation }: { navigation: NativeStackNavigationProp<StackParamList>}): React.JSX.Element {
|
||||
export default function RecentArtists({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { recentArtists } = useHomeContext()
|
||||
|
||||
const { recentArtists } = useHomeContext();
|
||||
return (
|
||||
<View>
|
||||
<H2 marginLeft={'$2'}>Recent Artists</H2>
|
||||
|
||||
return (
|
||||
<View>
|
||||
<H2 marginLeft={"$2"}>Recent Artists</H2>
|
||||
|
||||
<HorizontalCardList
|
||||
data={recentArtists}
|
||||
onSeeMore={() => {
|
||||
navigation.navigate("Artists", {
|
||||
query: QueryKeys.RecentlyPlayedArtists
|
||||
})
|
||||
}}
|
||||
renderItem={({ item: recentArtist}) =>
|
||||
<ItemCard
|
||||
item={recentArtist}
|
||||
caption={recentArtist.Name ?? "Unknown Artist"}
|
||||
onPress={() => {
|
||||
navigation.navigate('Artist',
|
||||
{
|
||||
artist: recentArtist,
|
||||
}
|
||||
)}
|
||||
}>
|
||||
</ItemCard>
|
||||
}/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
<HorizontalCardList
|
||||
data={recentArtists}
|
||||
onSeeMore={() => {
|
||||
navigation.navigate('Artists', {
|
||||
query: QueryKeys.RecentlyPlayedArtists,
|
||||
})
|
||||
}}
|
||||
renderItem={({ item: recentArtist }) => (
|
||||
<ItemCard
|
||||
item={recentArtist}
|
||||
caption={recentArtist.Name ?? 'Unknown Artist'}
|
||||
onPress={() => {
|
||||
navigation.navigate('Artist', {
|
||||
artist: recentArtist,
|
||||
})
|
||||
}}
|
||||
></ItemCard>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,70 +1,64 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { View } from "tamagui";
|
||||
import { useHomeContext } from "../provider";
|
||||
import { H2 } from "../../Global/helpers/text";
|
||||
import { ItemCard } from "../../Global/components/item-card";
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { trigger } from "react-native-haptic-feedback";
|
||||
import { QueuingType } from "../../../enums/queuing-type";
|
||||
import HorizontalCardList from "../../../components/Global/components/horizontal-list";
|
||||
import { QueryKeys } from "../../../enums/query-keys";
|
||||
import React, { useMemo } from 'react'
|
||||
import { View } from 'tamagui'
|
||||
import { useHomeContext } from '../provider'
|
||||
import { H2 } from '../../Global/helpers/text'
|
||||
import { ItemCard } from '../../Global/components/item-card'
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
|
||||
export default function RecentlyPlayed({
|
||||
navigation
|
||||
} : {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
export default function RecentlyPlayed({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { nowPlaying, usePlayNewQueue } = usePlayerContext()
|
||||
const { recentTracks } = useHomeContext()
|
||||
|
||||
const { nowPlaying, usePlayNewQueue } = usePlayerContext();
|
||||
const { recentTracks } = useHomeContext();
|
||||
return useMemo(() => {
|
||||
return (
|
||||
<View>
|
||||
<H2 marginLeft={'$2'}>Play it again</H2>
|
||||
|
||||
return (
|
||||
useMemo(() => {
|
||||
return (
|
||||
<View>
|
||||
<H2 marginLeft={"$2"}>Play it again</H2>
|
||||
|
||||
<HorizontalCardList
|
||||
squared
|
||||
data={recentTracks}
|
||||
onSeeMore={() => {
|
||||
navigation.navigate("Tracks", {
|
||||
query: QueryKeys.RecentlyPlayed
|
||||
})
|
||||
}}
|
||||
renderItem={({ index, item: recentlyPlayedTrack }) =>
|
||||
<ItemCard
|
||||
size={"$12"}
|
||||
caption={recentlyPlayedTrack.Name}
|
||||
subCaption={`${recentlyPlayedTrack.Artists?.join(", ")}`}
|
||||
squared
|
||||
item={recentlyPlayedTrack}
|
||||
onPress={() => {
|
||||
usePlayNewQueue.mutate({
|
||||
track: recentlyPlayedTrack,
|
||||
index: index,
|
||||
tracklist: recentTracks ?? [recentlyPlayedTrack],
|
||||
queue: "Recently Played",
|
||||
queuingType: QueuingType.FromSelection
|
||||
});
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger("impactMedium");
|
||||
navigation.navigate("Details", {
|
||||
item: recentlyPlayedTrack,
|
||||
isNested: false
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}, [
|
||||
recentTracks,
|
||||
nowPlaying
|
||||
])
|
||||
)
|
||||
}
|
||||
<HorizontalCardList
|
||||
squared
|
||||
data={recentTracks}
|
||||
onSeeMore={() => {
|
||||
navigation.navigate('Tracks', {
|
||||
query: QueryKeys.RecentlyPlayed,
|
||||
})
|
||||
}}
|
||||
renderItem={({ index, item: recentlyPlayedTrack }) => (
|
||||
<ItemCard
|
||||
size={'$12'}
|
||||
caption={recentlyPlayedTrack.Name}
|
||||
subCaption={`${recentlyPlayedTrack.Artists?.join(', ')}`}
|
||||
squared
|
||||
item={recentlyPlayedTrack}
|
||||
onPress={() => {
|
||||
usePlayNewQueue.mutate({
|
||||
track: recentlyPlayedTrack,
|
||||
index: index,
|
||||
tracklist: recentTracks ?? [recentlyPlayedTrack],
|
||||
queue: 'Recently Played',
|
||||
queuingType: QueuingType.FromSelection,
|
||||
})
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactMedium')
|
||||
navigation.navigate('Details', {
|
||||
item: recentlyPlayedTrack,
|
||||
isNested: false,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}, [recentTracks, nowPlaying])
|
||||
}
|
||||
|
||||
@@ -1,89 +1,91 @@
|
||||
import React, { createContext, ReactNode, useContext, useState } from "react";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { fetchRecentlyPlayed, fetchRecentlyPlayedArtists } from "../../api/queries/functions/recents";
|
||||
import { queryClient } from "../../constants/query-client";
|
||||
import { QueryConfig } from "../../api/queries/query.config";
|
||||
import React, { createContext, ReactNode, useContext, useState } from 'react'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import {
|
||||
fetchRecentlyPlayed,
|
||||
fetchRecentlyPlayedArtists,
|
||||
} from '../../api/queries/functions/recents'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { QueryConfig } from '../../api/queries/query.config'
|
||||
|
||||
interface HomeContext {
|
||||
refreshing: boolean;
|
||||
onRefresh: () => void;
|
||||
recentArtists: BaseItemDto[] | undefined;
|
||||
recentTracks: BaseItemDto[] | undefined;
|
||||
refreshing: boolean
|
||||
onRefresh: () => void
|
||||
recentArtists: BaseItemDto[] | undefined
|
||||
recentTracks: BaseItemDto[] | undefined
|
||||
}
|
||||
|
||||
const HomeContextInitializer = () => {
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false);
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
|
||||
const { data : recentTracks, refetch : refetchRecentTracks } = useQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayed],
|
||||
queryFn: () => fetchRecentlyPlayed()
|
||||
});
|
||||
const { data : recentArtists, refetch : refetchRecentArtists } = useQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayedArtists],
|
||||
queryFn: () => fetchRecentlyPlayedArtists()
|
||||
});
|
||||
const { data: recentTracks, refetch: refetchRecentTracks } = useQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayed],
|
||||
queryFn: () => fetchRecentlyPlayed(),
|
||||
})
|
||||
const { data: recentArtists, refetch: refetchRecentArtists } = useQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayedArtists],
|
||||
queryFn: () => fetchRecentlyPlayedArtists(),
|
||||
})
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.RecentlyPlayedArtists, QueryConfig.limits.recents * 4, QueryConfig.limits.recents]
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [
|
||||
QueryKeys.RecentlyPlayedArtists,
|
||||
QueryConfig.limits.recents * 4,
|
||||
QueryConfig.limits.recents,
|
||||
],
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.RecentlyPlayed, QueryConfig.limits.recents * 4, QueryConfig.limits.recents]
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
refetchRecentTracks(),
|
||||
refetchRecentArtists()
|
||||
])
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [
|
||||
QueryKeys.RecentlyPlayed,
|
||||
QueryConfig.limits.recents * 4,
|
||||
QueryConfig.limits.recents,
|
||||
],
|
||||
})
|
||||
|
||||
setRefreshing(false);
|
||||
}
|
||||
await Promise.all([refetchRecentTracks(), refetchRecentArtists()])
|
||||
|
||||
return {
|
||||
refreshing,
|
||||
onRefresh,
|
||||
recentArtists,
|
||||
recentTracks,
|
||||
};
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
return {
|
||||
refreshing,
|
||||
onRefresh,
|
||||
recentArtists,
|
||||
recentTracks,
|
||||
}
|
||||
}
|
||||
|
||||
const HomeContext = createContext<HomeContext>({
|
||||
refreshing: false,
|
||||
onRefresh: () => {},
|
||||
recentArtists: undefined,
|
||||
recentTracks: undefined
|
||||
});
|
||||
refreshing: false,
|
||||
onRefresh: () => {},
|
||||
recentArtists: undefined,
|
||||
recentTracks: undefined,
|
||||
})
|
||||
|
||||
export const HomeProvider: ({ children }: {
|
||||
children: ReactNode;
|
||||
}) => React.JSX.Element = ({
|
||||
children
|
||||
} : {
|
||||
children: ReactNode
|
||||
export const HomeProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) => {
|
||||
const { refreshing, onRefresh, recentTracks, recentArtists } = HomeContextInitializer()
|
||||
|
||||
const {
|
||||
refreshing,
|
||||
onRefresh,
|
||||
recentTracks,
|
||||
recentArtists,
|
||||
} = HomeContextInitializer();
|
||||
|
||||
return (
|
||||
<HomeContext.Provider value={{
|
||||
refreshing,
|
||||
onRefresh,
|
||||
recentTracks,
|
||||
recentArtists
|
||||
}}>
|
||||
{ children }
|
||||
</HomeContext.Provider>
|
||||
)
|
||||
return (
|
||||
<HomeContext.Provider
|
||||
value={{
|
||||
refreshing,
|
||||
onRefresh,
|
||||
recentTracks,
|
||||
recentArtists,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</HomeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useHomeContext = () => useContext(HomeContext);
|
||||
export const useHomeContext = () => useContext(HomeContext)
|
||||
|
||||
@@ -1,90 +1,76 @@
|
||||
import _ from "lodash";
|
||||
import { HomeProvider } from "./provider";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../types";
|
||||
import { AlbumScreen } from "../Album";
|
||||
import { PlaylistScreen } from "../Playlist/screens";
|
||||
import { ProvidedHome } from "./component";
|
||||
import DetailsScreen from "../ItemDetail/screen";
|
||||
import AddPlaylist from "../Library/components/add-playlist";
|
||||
import ArtistsScreen from "../Artists/screen";
|
||||
import TracksScreen from "../Tracks/screen";
|
||||
import { ArtistScreen } from "../Artist";
|
||||
import _ from 'lodash'
|
||||
import { HomeProvider } from './provider'
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../types'
|
||||
import { AlbumScreen } from '../Album'
|
||||
import { PlaylistScreen } from '../Playlist/screens'
|
||||
import { ProvidedHome } from './component'
|
||||
import DetailsScreen from '../ItemDetail/screen'
|
||||
import AddPlaylist from '../Library/components/add-playlist'
|
||||
import ArtistsScreen from '../Artists/screen'
|
||||
import TracksScreen from '../Tracks/screen'
|
||||
import { ArtistScreen } from '../Artist'
|
||||
|
||||
const Stack = createNativeStackNavigator<StackParamList>();
|
||||
const Stack = createNativeStackNavigator<StackParamList>()
|
||||
|
||||
export default function Home(): React.JSX.Element {
|
||||
return (
|
||||
<HomeProvider>
|
||||
<Stack.Navigator initialRouteName='Home' screenOptions={{}}>
|
||||
<Stack.Group>
|
||||
<Stack.Screen name='Home' component={ProvidedHome} />
|
||||
|
||||
return (
|
||||
<HomeProvider>
|
||||
<Stack.Navigator
|
||||
|
||||
initialRouteName="Home"
|
||||
screenOptions={{
|
||||
|
||||
}}
|
||||
>
|
||||
<Stack.Group>
|
||||
<Stack.Screen
|
||||
name="Home"
|
||||
component={ProvidedHome}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Artist'
|
||||
component={ArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artist.Name ?? 'Unknown Artist',
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="Artist"
|
||||
component={ArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artist.Name ?? "Unknown Artist",
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen name='Artists' component={ArtistsScreen} />
|
||||
|
||||
<Stack.Screen
|
||||
name="Artists"
|
||||
component={ArtistsScreen}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Tracks'
|
||||
component={TracksScreen}
|
||||
options={{
|
||||
title: 'Recent Tracks',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="Tracks"
|
||||
component={TracksScreen}
|
||||
options={{
|
||||
title: "Recent Tracks"
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Album'
|
||||
component={AlbumScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.album.Name ?? 'Untitled Album',
|
||||
headerTitle: '',
|
||||
})}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="Album"
|
||||
component={AlbumScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.album.Name ?? "Untitled Album",
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Playlist'
|
||||
component={PlaylistScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: '',
|
||||
})}
|
||||
/>
|
||||
</Stack.Group>
|
||||
|
||||
<Stack.Screen
|
||||
name="Playlist"
|
||||
component={PlaylistScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
|
||||
</Stack.Group>
|
||||
|
||||
<Stack.Group screenOptions={{ presentation: 'modal' }}>
|
||||
<Stack.Screen
|
||||
name="Details"
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</Stack.Group>
|
||||
</Stack.Navigator>
|
||||
</HomeProvider>
|
||||
);
|
||||
}
|
||||
<Stack.Group screenOptions={{ presentation: 'modal' }}>
|
||||
<Stack.Screen
|
||||
name='Details'
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</Stack.Group>
|
||||
</Stack.Navigator>
|
||||
</HomeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,180 +1,153 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import { StackParamList } from "../types";
|
||||
import TrackOptions from "./helpers/TrackOptions";
|
||||
import { getToken, getTokens, ScrollView, Spacer, View, XStack, YStack } from "tamagui";
|
||||
import { Text } from "../Global/helpers/text";
|
||||
import FavoriteButton from "../Global/components/favorite-button";
|
||||
import { useEffect } from "react";
|
||||
import { trigger } from "react-native-haptic-feedback";
|
||||
import TextTicker from "react-native-text-ticker";
|
||||
import { TextTickerConfig } from "../Player/component.config";
|
||||
import { Image } from "expo-image";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from "../../api/client";
|
||||
import Icon from "../Global/helpers/icon";
|
||||
import { Platform } from "react-native";
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { StackParamList } from '../types'
|
||||
import TrackOptions from './helpers/TrackOptions'
|
||||
import { getToken, getTokens, ScrollView, Spacer, View, XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import FavoriteButton from '../Global/components/favorite-button'
|
||||
import { useEffect } from 'react'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import TextTicker from 'react-native-text-ticker'
|
||||
import { TextTickerConfig } from '../Player/component.config'
|
||||
import { Image } from 'expo-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../api/client'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import { Platform } from 'react-native'
|
||||
|
||||
export default function ItemDetail({
|
||||
item,
|
||||
navigation,
|
||||
isNested
|
||||
} : {
|
||||
item: BaseItemDto,
|
||||
navigation: NativeStackNavigationProp<StackParamList>,
|
||||
isNested?: boolean | undefined
|
||||
}) : React.JSX.Element {
|
||||
export default function ItemDetail({
|
||||
item,
|
||||
navigation,
|
||||
isNested,
|
||||
}: {
|
||||
item: BaseItemDto
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
isNested?: boolean | undefined
|
||||
}): React.JSX.Element {
|
||||
let options: React.JSX.Element | undefined = undefined
|
||||
|
||||
let options: React.JSX.Element | undefined = undefined;
|
||||
useEffect(() => {
|
||||
trigger('impactMedium')
|
||||
}, [item])
|
||||
|
||||
useEffect(() => {
|
||||
trigger("impactMedium");
|
||||
}, [
|
||||
item
|
||||
]);
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
switch (item.Type) {
|
||||
case 'Audio': {
|
||||
options = TrackOptions({ track: item, navigation, isNested })
|
||||
break
|
||||
}
|
||||
|
||||
switch (item.Type) {
|
||||
case "Audio": {
|
||||
options = TrackOptions({ track: item, navigation, isNested });
|
||||
break;
|
||||
}
|
||||
case 'MusicAlbum': {
|
||||
break
|
||||
}
|
||||
|
||||
case "MusicAlbum" : {
|
||||
case 'MusicArtist': {
|
||||
break
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'Playlist': {
|
||||
break
|
||||
}
|
||||
|
||||
case "MusicArtist" : {
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<ScrollView contentInsetAdjustmentBehavior='automatic' removeClippedSubviews>
|
||||
<YStack alignItems='center' flex={1} marginTop={'$4'}>
|
||||
<XStack
|
||||
justifyContent='center'
|
||||
alignItems='flex-start'
|
||||
minHeight={width / 1.5}
|
||||
minWidth={width / 1.5}
|
||||
>
|
||||
{/**
|
||||
* Android needs a dismiss chevron here
|
||||
*/}
|
||||
{Platform.OS === 'android' ? (
|
||||
<Icon
|
||||
name='chevron-down'
|
||||
onPress={() => {
|
||||
navigation.goBack()
|
||||
}}
|
||||
small
|
||||
/>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
|
||||
case "Playlist" : {
|
||||
<Spacer />
|
||||
|
||||
break;
|
||||
}
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(
|
||||
item.Type === 'Audio' ? item.AlbumId! : item.Id!,
|
||||
)}
|
||||
style={{
|
||||
width: width / 1.5,
|
||||
height: width / 1.5,
|
||||
borderRadius:
|
||||
item.Type === 'MusicArtist' ? width / 1.5 : getToken('$5'),
|
||||
}}
|
||||
/>
|
||||
|
||||
default : {
|
||||
break;
|
||||
}
|
||||
}
|
||||
<Spacer />
|
||||
<Spacer />
|
||||
</XStack>
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
removeClippedSubviews
|
||||
>
|
||||
<YStack
|
||||
alignItems="center"
|
||||
flex={1}
|
||||
marginTop={"$4"}
|
||||
>
|
||||
{/* Item Name, Artist, Album, and Favorite Button */}
|
||||
<XStack maxWidth={width / 1.5}>
|
||||
<YStack
|
||||
marginLeft={'$0.5'}
|
||||
alignItems='flex-start'
|
||||
alignContent='flex-start'
|
||||
justifyContent='flex-start'
|
||||
flex={3}
|
||||
>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text bold fontSize={'$6'}>
|
||||
{item.Name ?? 'Untitled Track'}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
|
||||
<XStack
|
||||
justifyContent="center"
|
||||
alignItems="flex-start"
|
||||
minHeight={width / 1.5}
|
||||
minWidth={width / 1.5}
|
||||
>
|
||||
{/**
|
||||
* Android needs a dismiss chevron here
|
||||
*/}
|
||||
{ Platform.OS === 'android' ? (
|
||||
<Icon
|
||||
name="chevron-down"
|
||||
onPress={() => {
|
||||
navigation.goBack();
|
||||
}}
|
||||
small
|
||||
/>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text
|
||||
fontSize={'$6'}
|
||||
color={getTokens().color.telemagenta}
|
||||
onPress={() => {
|
||||
if (item.ArtistItems) {
|
||||
if (isNested) navigation.getParent()!.goBack()
|
||||
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
navigation.goBack()
|
||||
navigation.navigate('Artist', {
|
||||
artist: item.ArtistItems[0],
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.Artists?.join(', ') ?? 'Unknown Artist'}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
|
||||
<Spacer />
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text fontSize={'$6'} color={'$borderColor'}>
|
||||
{item.Album ?? ''}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
</YStack>
|
||||
|
||||
<Image
|
||||
source={getImageApi(Client.api!)
|
||||
.getItemImageUrlById(
|
||||
item.Type === 'Audio'
|
||||
? item.AlbumId!
|
||||
: item.Id!
|
||||
)
|
||||
}
|
||||
style={{
|
||||
width: width / 1.5,
|
||||
height: width / 1.5,
|
||||
borderRadius: item.Type === "MusicArtist" ? width / 1.5 : getToken("$5")
|
||||
}}
|
||||
/>
|
||||
<YStack flex={1} alignItems='flex-end' justifyContent='center'>
|
||||
<FavoriteButton item={item} />
|
||||
</YStack>
|
||||
</XStack>
|
||||
|
||||
<Spacer />
|
||||
<Spacer />
|
||||
</XStack>
|
||||
<Spacer />
|
||||
|
||||
{/* Item Name, Artist, Album, and Favorite Button */}
|
||||
<XStack maxWidth={width / 1.5}>
|
||||
<YStack
|
||||
marginLeft={"$0.5"}
|
||||
alignItems="flex-start"
|
||||
alignContent="flex-start"
|
||||
justifyContent="flex-start"
|
||||
flex={3}
|
||||
>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text bold fontSize={"$6"}>
|
||||
{ item.Name ?? "Untitled Track" }
|
||||
</Text>
|
||||
</TextTicker>
|
||||
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text
|
||||
fontSize={"$6"}
|
||||
color={getTokens().color.telemagenta}
|
||||
onPress={() => {
|
||||
if (item.ArtistItems) {
|
||||
|
||||
if (isNested)
|
||||
navigation.getParent()!.goBack();
|
||||
|
||||
navigation.goBack();
|
||||
navigation.navigate("Artist", {
|
||||
artist: item.ArtistItems[0]
|
||||
});
|
||||
}
|
||||
}}>
|
||||
{ item.Artists?.join(", ") ?? "Unknown Artist"}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text
|
||||
fontSize={"$6"}
|
||||
color={"$borderColor"}
|
||||
>
|
||||
{ item.Album ?? "" }
|
||||
</Text>
|
||||
</TextTicker>
|
||||
</YStack>
|
||||
|
||||
<YStack
|
||||
flex={1}
|
||||
alignItems="flex-end"
|
||||
justifyContent="center"
|
||||
>
|
||||
<FavoriteButton item={item} />
|
||||
</YStack>
|
||||
</XStack>
|
||||
|
||||
<Spacer />
|
||||
|
||||
{ options ?? <View /> }
|
||||
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
{options ?? <View />}
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,216 +1,217 @@
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
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 React from "react";
|
||||
import { useMutation, useQuery } 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";
|
||||
import { fetchItem } from "../../../api/queries/functions/item";
|
||||
import { fetchUserPlaylists } from "../../../api/queries/functions/playlists";
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
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 React from 'react'
|
||||
import { useMutation, useQuery } 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'
|
||||
import { fetchItem } from '../../../api/queries/functions/item'
|
||||
import { fetchUserPlaylists } from '../../../api/queries/functions/playlists'
|
||||
|
||||
import * as Burnt from "burnt";
|
||||
import { Image } from "expo-image";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from "../../../api/client";
|
||||
import * as Burnt from 'burnt'
|
||||
import { Image } from 'expo-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../../api/client'
|
||||
|
||||
interface TrackOptionsProps {
|
||||
track: BaseItemDto;
|
||||
navigation: NativeStackNavigationProp<StackParamList>;
|
||||
|
||||
/**
|
||||
* Whether this is nested in the player modal
|
||||
*/
|
||||
isNested: boolean | undefined;
|
||||
track: BaseItemDto
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
|
||||
/**
|
||||
* Whether this is nested in the player modal
|
||||
*/
|
||||
isNested: boolean | undefined
|
||||
}
|
||||
|
||||
export default function TrackOptions({
|
||||
track,
|
||||
navigation,
|
||||
isNested
|
||||
} : TrackOptionsProps) : React.JSX.Element {
|
||||
export default function TrackOptions({
|
||||
track,
|
||||
navigation,
|
||||
isNested,
|
||||
}: TrackOptionsProps): React.JSX.Element {
|
||||
const { data: album, isSuccess: albumFetchSuccess } = useQuery({
|
||||
queryKey: [QueryKeys.Item, track.AlbumId!],
|
||||
queryFn: () => fetchItem(track.AlbumId!),
|
||||
})
|
||||
|
||||
const { data: album, isSuccess: albumFetchSuccess } = useQuery({
|
||||
queryKey: [QueryKeys.Item, track.AlbumId!],
|
||||
queryFn: () => fetchItem(track.AlbumId!)
|
||||
});
|
||||
const {
|
||||
data: playlists,
|
||||
isPending: playlistsFetchPending,
|
||||
isSuccess: playlistsFetchSuccess,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.UserPlaylists],
|
||||
queryFn: () => fetchUserPlaylists(),
|
||||
})
|
||||
|
||||
const { data: playlists, isPending : playlistsFetchPending, isSuccess: playlistsFetchSuccess, refetch } = useQuery({
|
||||
queryKey: [QueryKeys.UserPlaylists],
|
||||
queryFn: () => fetchUserPlaylists()
|
||||
});
|
||||
|
||||
const { useAddToQueue } = usePlayerContext()
|
||||
|
||||
const { useAddToQueue } = usePlayerContext();
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
const useAddToPlaylist = useMutation({
|
||||
mutationFn: ({ track, playlist }: AddToPlaylistMutation) => {
|
||||
trigger('impactLight')
|
||||
return addToPlaylist(track, playlist)
|
||||
},
|
||||
onSuccess: (data, { playlist }) => {
|
||||
Burnt.alert({
|
||||
title: `Added to playlist`,
|
||||
duration: 1,
|
||||
preset: 'done',
|
||||
})
|
||||
|
||||
const useAddToPlaylist = useMutation({
|
||||
mutationFn: ({ track, playlist }: AddToPlaylistMutation) => {
|
||||
trigger("impactLight");
|
||||
return addToPlaylist(track, playlist)
|
||||
},
|
||||
onSuccess: (data, { playlist }) => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
Burnt.alert({
|
||||
title: `Added to playlist`,
|
||||
duration: 1,
|
||||
preset: 'done'
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.UserPlaylists],
|
||||
})
|
||||
|
||||
|
||||
trigger("notificationSuccess");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.ItemTracks, playlist.Id!],
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
Burnt.alert({
|
||||
title: `Unable to add`,
|
||||
duration: 1,
|
||||
preset: 'error',
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.UserPlaylists]
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.ItemTracks, playlist.Id!],
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
Burnt.alert({
|
||||
title: `Unable to add`,
|
||||
duration: 1,
|
||||
preset: 'error'
|
||||
});
|
||||
trigger('notificationError')
|
||||
},
|
||||
})
|
||||
|
||||
trigger("notificationError")
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<YStack width={width}>
|
||||
return (
|
||||
<YStack width={width}>
|
||||
<XStack justifyContent='space-evenly'>
|
||||
{albumFetchSuccess && album ? (
|
||||
<IconButton
|
||||
name='music-box'
|
||||
title='Go to Album'
|
||||
onPress={() => {
|
||||
if (isNested) navigation.goBack()
|
||||
|
||||
<XStack justifyContent="space-evenly">
|
||||
{ albumFetchSuccess && album ? (
|
||||
<IconButton
|
||||
name="music-box"
|
||||
title="Go to Album"
|
||||
onPress={() => {
|
||||
|
||||
if (isNested)
|
||||
navigation.goBack();
|
||||
|
||||
navigation.goBack();
|
||||
navigation.goBack()
|
||||
|
||||
if (isNested)
|
||||
navigation.navigate('Tabs', {
|
||||
screen: 'Home',
|
||||
params: {
|
||||
screen: 'Album',
|
||||
params: {
|
||||
album
|
||||
}
|
||||
}
|
||||
});
|
||||
else
|
||||
navigation.navigate('Album', {
|
||||
album
|
||||
});
|
||||
}}
|
||||
size={width / 6}
|
||||
/>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
if (isNested)
|
||||
navigation.navigate('Tabs', {
|
||||
screen: 'Home',
|
||||
params: {
|
||||
screen: 'Album',
|
||||
params: {
|
||||
album,
|
||||
},
|
||||
},
|
||||
})
|
||||
else
|
||||
navigation.navigate('Album', {
|
||||
album,
|
||||
})
|
||||
}}
|
||||
size={width / 6}
|
||||
/>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
circular
|
||||
name="table-column-plus-before"
|
||||
title="Play Next"
|
||||
onPress={() => {
|
||||
useAddToQueue.mutate({
|
||||
track: track,
|
||||
queuingType: QueuingType.PlayingNext
|
||||
})
|
||||
}}
|
||||
size={width / 6}
|
||||
/>
|
||||
<IconButton
|
||||
circular
|
||||
name='table-column-plus-before'
|
||||
title='Play Next'
|
||||
onPress={() => {
|
||||
useAddToQueue.mutate({
|
||||
track: track,
|
||||
queuingType: QueuingType.PlayingNext,
|
||||
})
|
||||
}}
|
||||
size={width / 6}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
circular
|
||||
name="table-column-plus-after"
|
||||
title="Queue"
|
||||
onPress={() => {
|
||||
useAddToQueue.mutate({
|
||||
track: track
|
||||
})
|
||||
}}
|
||||
size={width / 6}
|
||||
/>
|
||||
</XStack>
|
||||
<IconButton
|
||||
circular
|
||||
name='table-column-plus-after'
|
||||
title='Queue'
|
||||
onPress={() => {
|
||||
useAddToQueue.mutate({
|
||||
track: track,
|
||||
})
|
||||
}}
|
||||
size={width / 6}
|
||||
/>
|
||||
</XStack>
|
||||
|
||||
<Spacer />
|
||||
<Spacer />
|
||||
|
||||
{ playlistsFetchPending && (
|
||||
<Spinner />
|
||||
)}
|
||||
{playlistsFetchPending && <Spinner />}
|
||||
|
||||
{ !playlistsFetchPending && playlistsFetchSuccess && (
|
||||
<>
|
||||
<Text
|
||||
bold
|
||||
fontSize={"$6"}
|
||||
>
|
||||
Add to Playlist
|
||||
</Text>
|
||||
{!playlistsFetchPending && playlistsFetchSuccess && (
|
||||
<>
|
||||
<Text bold fontSize={'$6'}>
|
||||
Add to Playlist
|
||||
</Text>
|
||||
|
||||
<YGroup separator={(<Separator />)}>
|
||||
{ playlists.map(playlist => {
|
||||
<YGroup separator={<Separator />}>
|
||||
{playlists.map((playlist) => {
|
||||
return (
|
||||
<YGroup.Item key={playlist.Id!}>
|
||||
<ListItem
|
||||
hoverTheme
|
||||
onPress={() => {
|
||||
useAddToPlaylist.mutate({
|
||||
track,
|
||||
playlist,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<XStack alignItems='center'>
|
||||
<YStack flex={1}>
|
||||
<Image
|
||||
source={getImageApi(
|
||||
Client.api!,
|
||||
).getItemImageUrlById(playlist.Id!)}
|
||||
style={{
|
||||
borderRadius: getToken('$1.5'),
|
||||
width: getToken('$12'),
|
||||
height: getToken('$12'),
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
return (
|
||||
<YGroup.Item key={playlist.Id!}>
|
||||
<ListItem
|
||||
hoverTheme
|
||||
onPress={() => {
|
||||
useAddToPlaylist.mutate({
|
||||
track,
|
||||
playlist
|
||||
})
|
||||
}}
|
||||
>
|
||||
<XStack alignItems="center">
|
||||
<YStack
|
||||
flex={1}
|
||||
>
|
||||
<YStack alignItems='flex-start' flex={5}>
|
||||
<Text bold fontSize={'$6'}>
|
||||
{playlist.Name ?? 'Untitled Playlist'}
|
||||
</Text>
|
||||
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(playlist.Id!)}
|
||||
style={{
|
||||
borderRadius: getToken("$1.5"),
|
||||
width: getToken("$12"),
|
||||
height: getToken("$12")
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
<YStack
|
||||
alignItems="flex-start"
|
||||
flex={5}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
<Text color={getTokens().color.amethyst}>{`${
|
||||
playlist.ChildCount ?? 0
|
||||
} tracks`}</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</ListItem>
|
||||
</YGroup.Item>
|
||||
)
|
||||
})}
|
||||
</YGroup>
|
||||
</>
|
||||
)}
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import ItemDetail from "../../components/ItemDetail/component";
|
||||
import { StackParamList } from "../../components/types";
|
||||
import { RouteProp } from "@react-navigation/native";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import React from "react";
|
||||
import ItemDetail from '../../components/ItemDetail/component'
|
||||
import { StackParamList } from '../../components/types'
|
||||
import { RouteProp } from '@react-navigation/native'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import React from 'react'
|
||||
|
||||
export default function DetailsScreen({
|
||||
route,
|
||||
navigation,
|
||||
} : {
|
||||
route: RouteProp<StackParamList, "Details">,
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
return (
|
||||
<ItemDetail
|
||||
item={route.params.item}
|
||||
navigation={navigation}
|
||||
isNested={route.params.isNested}
|
||||
/>
|
||||
)
|
||||
}
|
||||
route,
|
||||
navigation,
|
||||
}: {
|
||||
route: RouteProp<StackParamList, 'Details'>
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<ItemDetail
|
||||
item={route.params.item}
|
||||
navigation={navigation}
|
||||
isNested={route.params.isNested}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
export interface AddToPlaylistMutation {
|
||||
track: BaseItemDto;
|
||||
playlist: BaseItemDto;
|
||||
}
|
||||
track: BaseItemDto
|
||||
playlist: BaseItemDto
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
|
||||
interface CategoryRoute {
|
||||
name: any; // ¯\_(ツ)_/¯
|
||||
iconName: string;
|
||||
params?: {
|
||||
query: QueryKeys
|
||||
};
|
||||
name: any // ¯\_(ツ)_/¯
|
||||
iconName: string
|
||||
params?: {
|
||||
query: QueryKeys
|
||||
}
|
||||
}
|
||||
|
||||
const Categories : CategoryRoute[] = [
|
||||
{ name: "Artists", iconName: "microphone-variant", params: { query: QueryKeys.FavoriteArtists } },
|
||||
{ name: "Albums", iconName: "music-box-multiple", params: { query: QueryKeys.FavoriteAlbums} },
|
||||
{ name: "Tracks", iconName: "music-note", params: { query: QueryKeys.FavoriteTracks } },
|
||||
{ name: "Playlists", iconName: "playlist-music" },
|
||||
];
|
||||
const Categories: CategoryRoute[] = [
|
||||
{
|
||||
name: 'Artists',
|
||||
iconName: 'microphone-variant',
|
||||
params: { query: QueryKeys.FavoriteArtists },
|
||||
},
|
||||
{ name: 'Albums', iconName: 'music-box-multiple', params: { query: QueryKeys.FavoriteAlbums } },
|
||||
{ name: 'Tracks', iconName: 'music-note', params: { query: QueryKeys.FavoriteTracks } },
|
||||
{ name: 'Playlists', iconName: 'playlist-music' },
|
||||
]
|
||||
|
||||
export default Categories;
|
||||
export default Categories
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
import { FlatList } from "react-native";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import Categories from "./categories";
|
||||
import IconCard from "../../components/Global/helpers/icon-card";
|
||||
import { StackParamList } from "../../components/types";
|
||||
import { RouteProp } from "@react-navigation/native";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { FlatList } from 'react-native'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import Categories from './categories'
|
||||
import IconCard from '../../components/Global/helpers/icon-card'
|
||||
import { StackParamList } from '../../components/types'
|
||||
import { RouteProp } from '@react-navigation/native'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
|
||||
export default function Library({
|
||||
route,
|
||||
navigation
|
||||
} : {
|
||||
route: RouteProp<StackParamList, "Library">,
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
export default function Library({
|
||||
route,
|
||||
navigation,
|
||||
}: {
|
||||
route: RouteProp<StackParamList, 'Library'>
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
data={Categories}
|
||||
numColumns={2}
|
||||
renderItem={({ index, item }) =>
|
||||
<IconCard
|
||||
name={item.iconName}
|
||||
caption={item.name}
|
||||
width={width / 2.1}
|
||||
onPress={() => {
|
||||
navigation.navigate(item.name, item.params)
|
||||
}}
|
||||
largeIcon
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
data={Categories}
|
||||
numColumns={2}
|
||||
renderItem={({ index, item }) => (
|
||||
<IconCard
|
||||
name={item.iconName}
|
||||
caption={item.name}
|
||||
width={width / 2.1}
|
||||
onPress={() => {
|
||||
navigation.navigate(item.name, item.params)
|
||||
}}
|
||||
largeIcon
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,58 +1,61 @@
|
||||
import { Label } from "../../Global/helpers/text";
|
||||
import Input from "../../Global/helpers/input";
|
||||
import React, { useState } from "react";
|
||||
import { View, XStack } from "tamagui";
|
||||
import Button from "../../Global/helpers/button";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../../types";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { createPlaylist } from "../../../api/mutations/functions/playlists";
|
||||
import { trigger } from "react-native-haptic-feedback";
|
||||
import { queryClient } from "../../../constants/query-client";
|
||||
import { QueryKeys } from "../../../enums/query-keys";
|
||||
import { Label } from '../../Global/helpers/text'
|
||||
import Input from '../../Global/helpers/input'
|
||||
import React, { useState } from 'react'
|
||||
import { View, XStack } from 'tamagui'
|
||||
import Button from '../../Global/helpers/button'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../../types'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { createPlaylist } from '../../../api/mutations/functions/playlists'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
|
||||
import * as Burnt from "burnt";
|
||||
import * as Burnt from 'burnt'
|
||||
|
||||
export default function AddPlaylist({
|
||||
navigation
|
||||
}: {
|
||||
navigation : NativeStackNavigationProp<StackParamList, 'AddPlaylist'>
|
||||
}) : React.JSX.Element {
|
||||
export default function AddPlaylist({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList, 'AddPlaylist'>
|
||||
}): React.JSX.Element {
|
||||
const [name, setName] = useState<string>('')
|
||||
|
||||
const [name, setName] = useState<string>("");
|
||||
const useAddPlaylist = useMutation({
|
||||
mutationFn: ({ name }: { name: string }) => createPlaylist(name),
|
||||
onSuccess: (data, { name }) => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
const useAddPlaylist = useMutation({
|
||||
mutationFn: ({ name } : { name : string}) => createPlaylist(name),
|
||||
onSuccess: (data, { name }) => {
|
||||
trigger("notificationSuccess");
|
||||
Burnt.alert({
|
||||
title: `Playlist created`,
|
||||
message: `Created playlist ${name}`,
|
||||
duration: 1,
|
||||
preset: 'done',
|
||||
})
|
||||
|
||||
Burnt.alert({
|
||||
title: `Playlist created`,
|
||||
message: `Created playlist ${name}`,
|
||||
duration: 1,
|
||||
preset: 'done'
|
||||
});
|
||||
navigation.goBack()
|
||||
|
||||
navigation.goBack();
|
||||
// Refresh user playlists component in library
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.UserPlaylists],
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
trigger('notificationError')
|
||||
},
|
||||
})
|
||||
|
||||
// Refresh user playlists component in library
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.UserPlaylists]
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
trigger("notificationError");
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<View marginHorizontal={"$2"}>
|
||||
<Label size="$2" htmlFor="name">Name</Label>
|
||||
<Input id="name" onChangeText={setName} />
|
||||
<XStack justifyContent="space-evenly">
|
||||
<Button danger onPress={() => navigation.goBack()}>Cancel</Button>
|
||||
<Button onPress={() => useAddPlaylist.mutate({ name })}>Create</Button>
|
||||
</XStack>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View marginHorizontal={'$2'}>
|
||||
<Label size='$2' htmlFor='name'>
|
||||
Name
|
||||
</Label>
|
||||
<Input id='name' onChangeText={setName} />
|
||||
<XStack justifyContent='space-evenly'>
|
||||
<Button danger onPress={() => navigation.goBack()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onPress={() => useAddPlaylist.mutate({ name })}>Create</Button>
|
||||
</XStack>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
import { View, XStack } from "tamagui";
|
||||
import { DeletePlaylistProps } from "../../../components/types";
|
||||
import Button from "../../../components/Global/helpers/button";
|
||||
import { Text } from "../../../components/Global/helpers/text";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { deletePlaylist } from "../../../api/mutations/functions/playlists";
|
||||
import { trigger } from "react-native-haptic-feedback";
|
||||
import { queryClient } from "../../../constants/query-client";
|
||||
import { QueryKeys } from "../../../enums/query-keys";
|
||||
import { View, XStack } from 'tamagui'
|
||||
import { DeletePlaylistProps } from '../../../components/types'
|
||||
import Button from '../../../components/Global/helpers/button'
|
||||
import { Text } from '../../../components/Global/helpers/text'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { deletePlaylist } from '../../../api/mutations/functions/playlists'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
|
||||
import * as Burnt from "burnt";
|
||||
import * as Burnt from 'burnt'
|
||||
|
||||
export default function DeletePlaylist(
|
||||
{
|
||||
navigation,
|
||||
route
|
||||
}: DeletePlaylistProps) : React.JSX.Element {
|
||||
export default function DeletePlaylist({
|
||||
navigation,
|
||||
route,
|
||||
}: DeletePlaylistProps): React.JSX.Element {
|
||||
const useDeletePlaylist = useMutation({
|
||||
mutationFn: (playlist: BaseItemDto) => deletePlaylist(playlist.Id!),
|
||||
onSuccess: (data, playlist) => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
navigation.goBack()
|
||||
navigation.goBack()
|
||||
Burnt.alert({
|
||||
title: `Playlist deleted`,
|
||||
message: `Deleted ${playlist.Name ?? 'Untitled Playlist'}`,
|
||||
duration: 1,
|
||||
preset: 'done',
|
||||
})
|
||||
|
||||
const useDeletePlaylist = useMutation({
|
||||
mutationFn: (playlist: BaseItemDto) => deletePlaylist(playlist.Id!),
|
||||
onSuccess: (data, playlist) => {
|
||||
trigger("notificationSuccess");
|
||||
// Refresh favorite playlists component in library
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.FavoritePlaylists],
|
||||
})
|
||||
|
||||
navigation.goBack();
|
||||
navigation.goBack();
|
||||
Burnt.alert({
|
||||
title: `Playlist deleted`,
|
||||
message: `Deleted ${playlist.Name ?? "Untitled Playlist"}`,
|
||||
duration: 1,
|
||||
preset: 'done'
|
||||
});
|
||||
// Refresh home screen user playlists
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.UserPlaylists],
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
trigger('notificationError')
|
||||
},
|
||||
})
|
||||
|
||||
// Refresh favorite playlists component in library
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.FavoritePlaylists]
|
||||
});
|
||||
|
||||
// Refresh home screen user playlists
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.UserPlaylists]
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
trigger("notificationError");
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<View marginHorizontal={"$2"}>
|
||||
<Text bold textAlign="center">{`Delete playlist ${route.params.playlist.Name ?? "Untitled Playlist"}?`}</Text>
|
||||
<XStack justifyContent="space-evenly">
|
||||
<Button onPress={() => navigation.goBack()}>Cancel</Button>
|
||||
<Button danger onPress={() => useDeletePlaylist.mutate(route.params.playlist)}>Delete</Button>
|
||||
</XStack>
|
||||
</View>
|
||||
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View marginHorizontal={'$2'}>
|
||||
<Text bold textAlign='center'>{`Delete playlist ${
|
||||
route.params.playlist.Name ?? 'Untitled Playlist'
|
||||
}?`}</Text>
|
||||
<XStack justifyContent='space-evenly'>
|
||||
<Button onPress={() => navigation.goBack()}>Cancel</Button>
|
||||
<Button danger onPress={() => useDeletePlaylist.mutate(route.params.playlist)}>
|
||||
Delete
|
||||
</Button>
|
||||
</XStack>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,127 +1,106 @@
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import React from "react";
|
||||
import { StackParamList } from "../types";
|
||||
import Library from "./component";
|
||||
import { AlbumScreen } from "../Album";
|
||||
import { PlaylistScreen } from "../Playlist/screens";
|
||||
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";
|
||||
import AddPlaylist from "./components/add-playlist";
|
||||
import DeletePlaylist from "./components/delete-playlist";
|
||||
import { ArtistScreen } from "../Artist";
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||
import React from 'react'
|
||||
import { StackParamList } from '../types'
|
||||
import Library from './component'
|
||||
import { AlbumScreen } from '../Album'
|
||||
import { PlaylistScreen } from '../Playlist/screens'
|
||||
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'
|
||||
import AddPlaylist from './components/add-playlist'
|
||||
import DeletePlaylist from './components/delete-playlist'
|
||||
import { ArtistScreen } from '../Artist'
|
||||
|
||||
const Stack = createNativeStackNavigator<StackParamList>();
|
||||
const Stack = createNativeStackNavigator<StackParamList>()
|
||||
|
||||
export default function LibraryStack(): React.JSX.Element {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
initialRouteName="Library"
|
||||
>
|
||||
<Stack.Screen
|
||||
name="Library"
|
||||
component={Library}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
return (
|
||||
<Stack.Navigator initialRouteName='Library'>
|
||||
<Stack.Screen
|
||||
name='Library'
|
||||
component={Library}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="Artist"
|
||||
component={ArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artist.Name ?? "Unknown Artist",
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Artist'
|
||||
component={ArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artist.Name ?? 'Unknown Artist',
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="Artists"
|
||||
component={ArtistsScreen}
|
||||
options={({ route }) => ({
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen name='Artists' component={ArtistsScreen} options={({ route }) => ({})} />
|
||||
|
||||
<Stack.Screen
|
||||
name="Album"
|
||||
component={AlbumScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Album'
|
||||
component={AlbumScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: '',
|
||||
})}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="Albums"
|
||||
component={AlbumsScreen}
|
||||
options={{
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name='Albums' component={AlbumsScreen} options={{}} />
|
||||
|
||||
<Stack.Screen
|
||||
name="Tracks"
|
||||
component={TracksScreen}
|
||||
options={{
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name='Tracks' component={TracksScreen} options={{}} />
|
||||
|
||||
<Stack.Screen
|
||||
name="Playlists"
|
||||
component={PlaylistsScreen}
|
||||
options={{
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name='Playlists' component={PlaylistsScreen} options={{}} />
|
||||
|
||||
<Stack.Screen
|
||||
name="Playlist"
|
||||
component={PlaylistScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Playlist'
|
||||
component={PlaylistScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: '',
|
||||
})}
|
||||
/>
|
||||
|
||||
<Stack.Group screenOptions={{ presentation: 'modal' }}>
|
||||
<Stack.Screen
|
||||
name="Details"
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</Stack.Group>
|
||||
<Stack.Group screenOptions={{ presentation: 'modal' }}>
|
||||
<Stack.Screen
|
||||
name='Details'
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</Stack.Group>
|
||||
|
||||
{/* https://www.reddit.com/r/reactnative/comments/1dgktbn/comment/lxd23sj/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button */}
|
||||
<Stack.Group screenOptions={{
|
||||
presentation: 'formSheet',
|
||||
sheetInitialDetentIndex: 0,
|
||||
sheetAllowedDetents: [0.35]
|
||||
}}>
|
||||
<Stack.Screen
|
||||
name="AddPlaylist"
|
||||
component={AddPlaylist}
|
||||
options={{
|
||||
title: "Add Playlist",
|
||||
}}
|
||||
/>
|
||||
{/* https://www.reddit.com/r/reactnative/comments/1dgktbn/comment/lxd23sj/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button */}
|
||||
<Stack.Group
|
||||
screenOptions={{
|
||||
presentation: 'formSheet',
|
||||
sheetInitialDetentIndex: 0,
|
||||
sheetAllowedDetents: [0.35],
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name='AddPlaylist'
|
||||
component={AddPlaylist}
|
||||
options={{
|
||||
title: 'Add Playlist',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="DeletePlaylist"
|
||||
component={DeletePlaylist}
|
||||
options={{
|
||||
title: "Delete Playlist"
|
||||
}}
|
||||
/>
|
||||
</Stack.Group>
|
||||
|
||||
</Stack.Navigator>
|
||||
)
|
||||
}
|
||||
<Stack.Screen
|
||||
name='DeletePlaylist'
|
||||
component={DeletePlaylist}
|
||||
options={{
|
||||
title: 'Delete Playlist',
|
||||
}}
|
||||
/>
|
||||
</Stack.Group>
|
||||
</Stack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,56 +1,54 @@
|
||||
import _, { isUndefined } from "lodash"
|
||||
import ServerAuthentication from "./screens/server-authentication";
|
||||
import ServerAddress from "./screens/server-address";
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import ServerLibrary from "./screens/server-library";
|
||||
import { useAuthenticationContext } from "./provider";
|
||||
import { useEffect } from "react";
|
||||
import _, { isUndefined } from 'lodash'
|
||||
import ServerAuthentication from './screens/server-authentication'
|
||||
import ServerAddress from './screens/server-address'
|
||||
import { createStackNavigator } from '@react-navigation/stack'
|
||||
import ServerLibrary from './screens/server-library'
|
||||
import { useAuthenticationContext } from './provider'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Login(): React.JSX.Element {
|
||||
const { user, server, setTriggerAuth } = useAuthenticationContext()
|
||||
|
||||
const { user, server, setTriggerAuth } = useAuthenticationContext();
|
||||
const Stack = createStackNavigator()
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
useEffect(() => {
|
||||
setTriggerAuth(false)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setTriggerAuth(false);
|
||||
});
|
||||
return (
|
||||
<Stack.Navigator
|
||||
initialRouteName={
|
||||
isUndefined(server)
|
||||
? 'ServerAddress'
|
||||
: isUndefined(user)
|
||||
? 'ServerAuthentication'
|
||||
: 'LibrarySelection'
|
||||
}
|
||||
screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen
|
||||
name='ServerAddress'
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
component={ServerAddress}
|
||||
/>
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
initialRouteName={
|
||||
isUndefined(server)
|
||||
? "ServerAddress"
|
||||
: isUndefined(user)
|
||||
? "ServerAuthentication"
|
||||
: "LibrarySelection"
|
||||
}
|
||||
screenOptions={{ headerShown: false }}
|
||||
>
|
||||
<Stack.Screen
|
||||
name="ServerAddress"
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
component={ServerAddress}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="ServerAuthentication"
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
initialParams={{ server }}
|
||||
//@ts-ignore
|
||||
component={ServerAuthentication}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="LibrarySelection"
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
component={ServerLibrary}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
<Stack.Screen
|
||||
name='ServerAuthentication'
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
initialParams={{ server }}
|
||||
//@ts-expect-error TOOD: Explain why this exists
|
||||
component={ServerAuthentication}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='LibrarySelection'
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
component={ServerLibrary}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,82 +1,75 @@
|
||||
import React, { createContext, ReactNode, SetStateAction, useContext, useState } from "react";
|
||||
import _ from "lodash";
|
||||
import { JellifyServer } from "../../types/JellifyServer";
|
||||
import Client from "../../api/client";
|
||||
import { JellifyUser } from "../../types/JellifyUser";
|
||||
import { JellifyLibrary } from "../../types/JellifyLibrary";
|
||||
import React, { createContext, ReactNode, SetStateAction, useContext, useState } from 'react'
|
||||
import _ from 'lodash'
|
||||
import { JellifyServer } from '../../types/JellifyServer'
|
||||
import Client from '../../api/client'
|
||||
import { JellifyUser } from '../../types/JellifyUser'
|
||||
import { JellifyLibrary } from '../../types/JellifyLibrary'
|
||||
|
||||
interface JellyfinAuthenticationContext {
|
||||
server: JellifyServer | undefined;
|
||||
setServer: React.Dispatch<React.SetStateAction<JellifyServer | undefined>>;
|
||||
user: JellifyUser | undefined;
|
||||
setUser: React.Dispatch<React.SetStateAction<JellifyUser | undefined>>;
|
||||
library: JellifyLibrary | undefined;
|
||||
setLibrary: React.Dispatch<React.SetStateAction<JellifyLibrary | undefined>>;
|
||||
triggerAuth: boolean;
|
||||
setTriggerAuth: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
server: JellifyServer | undefined
|
||||
setServer: React.Dispatch<React.SetStateAction<JellifyServer | undefined>>
|
||||
user: JellifyUser | undefined
|
||||
setUser: React.Dispatch<React.SetStateAction<JellifyUser | undefined>>
|
||||
library: JellifyLibrary | undefined
|
||||
setLibrary: React.Dispatch<React.SetStateAction<JellifyLibrary | undefined>>
|
||||
triggerAuth: boolean
|
||||
setTriggerAuth: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
const JellyfinAuthenticationContextInitializer = () => {
|
||||
const [server, setServer] = useState<JellifyServer | undefined>(Client.server)
|
||||
const [user, setUser] = useState<JellifyUser | undefined>(Client.user)
|
||||
const [library, setLibrary] = useState<JellifyLibrary | undefined>(Client.library)
|
||||
|
||||
const [server, setServer] = useState<JellifyServer | undefined>(Client.server)
|
||||
const [user, setUser] = useState<JellifyUser | undefined>(Client.user)
|
||||
const [library, setLibrary] = useState<JellifyLibrary | undefined>(Client.library);
|
||||
const [triggerAuth, setTriggerAuth] = useState<boolean>(true)
|
||||
|
||||
const [triggerAuth, setTriggerAuth] = useState<boolean>(true);
|
||||
|
||||
return {
|
||||
user,
|
||||
setUser,
|
||||
server,
|
||||
setServer,
|
||||
library,
|
||||
setLibrary,
|
||||
triggerAuth,
|
||||
setTriggerAuth,
|
||||
};
|
||||
return {
|
||||
user,
|
||||
setUser,
|
||||
server,
|
||||
setServer,
|
||||
library,
|
||||
setLibrary,
|
||||
triggerAuth,
|
||||
setTriggerAuth,
|
||||
}
|
||||
}
|
||||
|
||||
const JellyfinAuthenticationContext =
|
||||
createContext<JellyfinAuthenticationContext>({
|
||||
user: undefined,
|
||||
setUser: () => {},
|
||||
server: undefined,
|
||||
setServer: () => {},
|
||||
library: undefined,
|
||||
setLibrary: () => {},
|
||||
triggerAuth: true,
|
||||
setTriggerAuth: () => {},
|
||||
});
|
||||
const JellyfinAuthenticationContext = createContext<JellyfinAuthenticationContext>({
|
||||
user: undefined,
|
||||
setUser: () => {},
|
||||
server: undefined,
|
||||
setServer: () => {},
|
||||
library: undefined,
|
||||
setLibrary: () => {},
|
||||
triggerAuth: true,
|
||||
setTriggerAuth: () => {},
|
||||
})
|
||||
|
||||
export const JellyfinAuthenticationProvider: ({ children }: {
|
||||
children: ReactNode;
|
||||
export const JellyfinAuthenticationProvider: ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) => React.JSX.Element = ({ children }: { children: ReactNode }) => {
|
||||
const { user, setUser, server, setServer, library, setLibrary, triggerAuth, setTriggerAuth } =
|
||||
JellyfinAuthenticationContextInitializer()
|
||||
|
||||
const {
|
||||
user,
|
||||
setUser,
|
||||
server,
|
||||
setServer,
|
||||
library,
|
||||
setLibrary,
|
||||
triggerAuth,
|
||||
setTriggerAuth,
|
||||
} = JellyfinAuthenticationContextInitializer();
|
||||
return (
|
||||
<JellyfinAuthenticationContext.Provider
|
||||
value={{
|
||||
user,
|
||||
setUser,
|
||||
server,
|
||||
setServer,
|
||||
library,
|
||||
setLibrary,
|
||||
triggerAuth,
|
||||
setTriggerAuth,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</JellyfinAuthenticationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<JellyfinAuthenticationContext.Provider value={{
|
||||
user,
|
||||
setUser,
|
||||
server,
|
||||
setServer,
|
||||
library,
|
||||
setLibrary,
|
||||
triggerAuth,
|
||||
setTriggerAuth,
|
||||
}}>
|
||||
{ children }
|
||||
</JellyfinAuthenticationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuthenticationContext = () => useContext(JellyfinAuthenticationContext)
|
||||
export const useAuthenticationContext = () => useContext(JellyfinAuthenticationContext)
|
||||
|
||||
@@ -1,122 +1,119 @@
|
||||
import React, { useState } from "react";
|
||||
import _ from "lodash";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { JellifyServer } from "../../../types/JellifyServer";
|
||||
import { Input, Spinner, XStack, YStack } from "tamagui";
|
||||
import { SwitchWithLabel } from "../../Global/helpers/switch-with-label";
|
||||
import { H2 } from "../../Global/helpers/text";
|
||||
import Button from "../../Global/helpers/button";
|
||||
import { http, https } from "../utils/constants";
|
||||
import { JellyfinInfo } from "../../../api/info";
|
||||
import { Jellyfin } from "@jellyfin/sdk/lib/jellyfin";
|
||||
import { getSystemApi } from "@jellyfin/sdk/lib/utils/api/system-api";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import Client from "../../../api/client";
|
||||
import { useAuthenticationContext } from "../provider";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import React, { useState } from 'react'
|
||||
import _ from 'lodash'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { JellifyServer } from '../../../types/JellifyServer'
|
||||
import { Input, Spinner, XStack, YStack } from 'tamagui'
|
||||
import { SwitchWithLabel } from '../../Global/helpers/switch-with-label'
|
||||
import { H2 } from '../../Global/helpers/text'
|
||||
import Button from '../../Global/helpers/button'
|
||||
import { http, https } from '../utils/constants'
|
||||
import { JellyfinInfo } from '../../../api/info'
|
||||
import { Jellyfin } from '@jellyfin/sdk/lib/jellyfin'
|
||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import Client from '../../../api/client'
|
||||
import { useAuthenticationContext } from '../provider'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
|
||||
import * as Burnt from "burnt";
|
||||
import { Image } from "react-native";
|
||||
import * as Burnt from 'burnt'
|
||||
import { Image } from 'react-native'
|
||||
|
||||
export default function ServerAddress({
|
||||
navigation
|
||||
export default function ServerAddress({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
navigation.setOptions({
|
||||
animationTypeForReplace: 'push',
|
||||
})
|
||||
|
||||
navigation.setOptions({
|
||||
animationTypeForReplace: 'push'
|
||||
})
|
||||
const [useHttps, setUseHttps] = useState<boolean>(true)
|
||||
const [serverAddress, setServerAddress] = useState<string | undefined>(undefined)
|
||||
|
||||
const [useHttps, setUseHttps] = useState<boolean>(true);
|
||||
const [serverAddress, setServerAddress] = useState<string | undefined>(undefined);
|
||||
const { server, setServer } = useAuthenticationContext()
|
||||
|
||||
const { server, setServer } = useAuthenticationContext();
|
||||
const useServerMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
console.debug(`Connecting to ${useHttps ? https : http}${serverAddress}`)
|
||||
|
||||
const useServerMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
const jellyfin = new Jellyfin(JellyfinInfo)
|
||||
|
||||
console.debug(`Connecting to ${useHttps ? https : http}${serverAddress}`);
|
||||
if (!serverAddress) throw new Error('Server address was empty')
|
||||
|
||||
const jellyfin = new Jellyfin(JellyfinInfo);
|
||||
const api = jellyfin.createApi(`${useHttps ? https : http}${serverAddress}`)
|
||||
|
||||
if (!serverAddress)
|
||||
throw new Error("Server address was empty");
|
||||
return getSystemApi(api).getPublicSystemInfo()
|
||||
},
|
||||
onSuccess: (publicSystemInfoResponse) => {
|
||||
if (!publicSystemInfoResponse.data.Version)
|
||||
throw new Error('Jellyfin instance did not respond')
|
||||
|
||||
const api = jellyfin.createApi(`${useHttps ? https : http}${serverAddress}`);
|
||||
console.log(`Connected to Jellyfin ${publicSystemInfoResponse.data.Version!}`)
|
||||
|
||||
return getSystemApi(api).getPublicSystemInfo();
|
||||
},
|
||||
onSuccess: (publicSystemInfoResponse) => {
|
||||
if (!publicSystemInfoResponse.data.Version)
|
||||
throw new Error("Jellyfin instance did not respond");
|
||||
|
||||
console.log(`Connected to Jellyfin ${publicSystemInfoResponse.data.Version!}`);
|
||||
|
||||
const server: JellifyServer = {
|
||||
url: `${useHttps ? https : http}${serverAddress!}`,
|
||||
address: serverAddress!,
|
||||
name: publicSystemInfoResponse.data.ServerName!,
|
||||
version: publicSystemInfoResponse.data.Version!,
|
||||
startUpComplete: publicSystemInfoResponse.data.StartupWizardCompleted!
|
||||
}
|
||||
const server: JellifyServer = {
|
||||
url: `${useHttps ? https : http}${serverAddress!}`,
|
||||
address: serverAddress!,
|
||||
name: publicSystemInfoResponse.data.ServerName!,
|
||||
version: publicSystemInfoResponse.data.Version!,
|
||||
startUpComplete: publicSystemInfoResponse.data.StartupWizardCompleted!,
|
||||
}
|
||||
|
||||
Client.setPublicApiClient(server);
|
||||
setServer(server);
|
||||
Client.setPublicApiClient(server)
|
||||
setServer(server)
|
||||
|
||||
navigation.navigate("ServerAuthentication", { server });
|
||||
},
|
||||
onError: async (error: Error) => {
|
||||
console.error("An error occurred connecting to the Jellyfin instance", error);
|
||||
Client.signOut();
|
||||
setServer(undefined);
|
||||
navigation.navigate('ServerAuthentication', { server })
|
||||
},
|
||||
onError: async (error: Error) => {
|
||||
console.error('An error occurred connecting to the Jellyfin instance', error)
|
||||
Client.signOut()
|
||||
setServer(undefined)
|
||||
|
||||
Burnt.toast({
|
||||
title: "Unable to connect",
|
||||
preset: "error",
|
||||
// message: `Unable to connect to Jellyfin at ${useHttps ? https : http}${serverAddress}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
Burnt.toast({
|
||||
title: 'Unable to connect',
|
||||
preset: 'error',
|
||||
// message: `Unable to connect to Jellyfin at ${useHttps ? https : http}${serverAddress}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{flex:1}}>
|
||||
<YStack maxHeight={"$19"} flex={1} justifyContent="center">
|
||||
<H2 marginHorizontal={"$10"} textAlign="center">
|
||||
Connect to Jellyfin
|
||||
</H2>
|
||||
</YStack>
|
||||
|
||||
<YStack marginHorizontal={"$2"}>
|
||||
<SwitchWithLabel
|
||||
checked={useHttps}
|
||||
onCheckedChange={(checked) => setUseHttps(checked)}
|
||||
label="Use HTTPS"
|
||||
size="$2"
|
||||
width={100}
|
||||
/>
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<YStack maxHeight={'$19'} flex={1} justifyContent='center'>
|
||||
<H2 marginHorizontal={'$10'} textAlign='center'>
|
||||
Connect to Jellyfin
|
||||
</H2>
|
||||
</YStack>
|
||||
|
||||
<Input
|
||||
onChangeText={setServerAddress}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="jellyfin.org"
|
||||
/>
|
||||
|
||||
{ useServerMutation.isPending ? (
|
||||
<Spinner />
|
||||
) :
|
||||
<Button
|
||||
disabled={_.isEmpty(serverAddress)}
|
||||
onPress={() => {
|
||||
useServerMutation.mutate();
|
||||
}}>
|
||||
Connect
|
||||
</Button>}
|
||||
</YStack>
|
||||
|
||||
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
<YStack marginHorizontal={'$2'}>
|
||||
<SwitchWithLabel
|
||||
checked={useHttps}
|
||||
onCheckedChange={(checked) => setUseHttps(checked)}
|
||||
label='Use HTTPS'
|
||||
size='$2'
|
||||
width={100}
|
||||
/>
|
||||
|
||||
<Input
|
||||
onChangeText={setServerAddress}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
placeholder='jellyfin.org'
|
||||
/>
|
||||
|
||||
{useServerMutation.isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Button
|
||||
disabled={_.isEmpty(serverAddress)}
|
||||
onPress={() => {
|
||||
useServerMutation.mutate()
|
||||
}}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
</YStack>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,131 +1,140 @@
|
||||
import React, { useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import _ from "lodash";
|
||||
import { JellyfinCredentials } from "../../../api/types/jellyfin-credentials";
|
||||
import { getToken, Spacer, Spinner, XStack, YStack } from "tamagui";
|
||||
import { useAuthenticationContext } from "../provider";
|
||||
import { H2 } from "../../Global/helpers/text";
|
||||
import Button from "../../Global/helpers/button";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import Client from "../../../api/client";
|
||||
import { JellifyUser } from "../../../types/JellifyUser";
|
||||
import { ServerAuthenticationProps } from "../../../components/types";
|
||||
import Input from "../../../components/Global/helpers/input";
|
||||
import Icon from "../../../components/Global/helpers/icon";
|
||||
import { useToastController } from "@tamagui/toast";
|
||||
import Toast from "../../../components/Global/components/toast";
|
||||
import React, { useState } from 'react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import _ from 'lodash'
|
||||
import { JellyfinCredentials } from '../../../api/types/jellyfin-credentials'
|
||||
import { getToken, Spacer, Spinner, XStack, YStack } from 'tamagui'
|
||||
import { useAuthenticationContext } from '../provider'
|
||||
import { H2 } from '../../Global/helpers/text'
|
||||
import Button from '../../Global/helpers/button'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import Client from '../../../api/client'
|
||||
import { JellifyUser } from '../../../types/JellifyUser'
|
||||
import { ServerAuthenticationProps } from '../../../components/types'
|
||||
import Input from '../../../components/Global/helpers/input'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import { useToastController } from '@tamagui/toast'
|
||||
import Toast from '../../../components/Global/components/toast'
|
||||
|
||||
export default function ServerAuthentication({
|
||||
route,
|
||||
navigation,
|
||||
route,
|
||||
navigation,
|
||||
}: ServerAuthenticationProps): React.JSX.Element {
|
||||
const toast = useToastController()
|
||||
|
||||
const toast = useToastController()
|
||||
const [username, setUsername] = useState<string | undefined>(undefined)
|
||||
const [password, setPassword] = React.useState<string | undefined>(undefined)
|
||||
|
||||
const [username, setUsername] = useState<string | undefined>(undefined);
|
||||
const [password, setPassword] = React.useState<string | undefined>(undefined);
|
||||
const { setUser, setServer } = useAuthenticationContext()
|
||||
|
||||
const { setUser, setServer } = useAuthenticationContext();
|
||||
const useApiMutation = useMutation({
|
||||
mutationFn: async (credentials: JellyfinCredentials) => {
|
||||
return await Client.api!.authenticateUserByName(
|
||||
credentials.username,
|
||||
credentials.password,
|
||||
)
|
||||
},
|
||||
onSuccess: async (authResult) => {
|
||||
console.log(`Received auth response from server`)
|
||||
if (_.isUndefined(authResult))
|
||||
return Promise.reject(new Error('Authentication result was empty'))
|
||||
|
||||
const useApiMutation = useMutation({
|
||||
mutationFn: async (credentials: JellyfinCredentials) => {
|
||||
return await Client.api!.authenticateUserByName(credentials.username, credentials.password);
|
||||
},
|
||||
onSuccess: async (authResult) => {
|
||||
|
||||
console.log(`Received auth response from server`)
|
||||
if (_.isUndefined(authResult))
|
||||
return Promise.reject(new Error("Authentication result was empty"))
|
||||
if (authResult.status >= 400 || _.isEmpty(authResult.data.AccessToken))
|
||||
return Promise.reject(new Error('Invalid credentials'))
|
||||
|
||||
if (authResult.status >= 400 || _.isEmpty(authResult.data.AccessToken))
|
||||
return Promise.reject(new Error("Invalid credentials"))
|
||||
if (_.isUndefined(authResult.data.User))
|
||||
return Promise.reject(new Error('Unable to login'))
|
||||
|
||||
if (_.isUndefined(authResult.data.User))
|
||||
return Promise.reject(new Error("Unable to login"));
|
||||
console.log(`Successfully signed in to server`)
|
||||
|
||||
console.log(`Successfully signed in to server`);
|
||||
const user: JellifyUser = {
|
||||
id: authResult.data.User!.Id!,
|
||||
name: authResult.data.User!.Name!,
|
||||
accessToken: authResult.data.AccessToken as string,
|
||||
}
|
||||
|
||||
const user : JellifyUser = {
|
||||
id: authResult.data.User!.Id!,
|
||||
name: authResult.data.User!.Name!,
|
||||
accessToken: (authResult.data.AccessToken as string)
|
||||
}
|
||||
Client.setUser(user)
|
||||
setUser(user)
|
||||
|
||||
Client.setUser(user);
|
||||
setUser(user);
|
||||
navigation.navigate('LibrarySelection', { user })
|
||||
},
|
||||
onError: async (error: Error) => {
|
||||
console.error('An error occurred connecting to the Jellyfin instance', error)
|
||||
|
||||
navigation.navigate("LibrarySelection", { user });
|
||||
},
|
||||
onError: async (error: Error) => {
|
||||
console.error("An error occurred connecting to the Jellyfin instance", error);
|
||||
toast.show('Sign in failed', {})
|
||||
return Promise.reject(`An error occured signing into ${Client.server!.name}`)
|
||||
},
|
||||
})
|
||||
|
||||
toast.show("Sign in failed", {
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<YStack maxHeight={'$19'} flex={1} justifyContent='center'>
|
||||
<H2 marginHorizontal={'$2'} textAlign='center'>
|
||||
{`Sign in to ${route.params.server.name}`}
|
||||
</H2>
|
||||
</YStack>
|
||||
<YStack marginHorizontal={'$2'}>
|
||||
<Input
|
||||
prependElement={
|
||||
<Icon
|
||||
small
|
||||
name='human-greeting-variant'
|
||||
color={getToken('$color.amethyst')}
|
||||
/>
|
||||
}
|
||||
placeholder='Username'
|
||||
value={username}
|
||||
onChangeText={(value: string | undefined) => setUsername(value)}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
/>
|
||||
|
||||
});
|
||||
return Promise.reject(`An error occured signing into ${Client.server!.name}`);
|
||||
}
|
||||
});
|
||||
<Spacer />
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{flex:1}}>
|
||||
<YStack maxHeight={"$19"} flex={1} justifyContent="center">
|
||||
<H2 marginHorizontal={"$2"} textAlign="center">
|
||||
{ `Sign in to ${route.params.server.name}`}
|
||||
</H2>
|
||||
</YStack>
|
||||
<YStack marginHorizontal={"$2"}>
|
||||
<Input
|
||||
prependElement={(<Icon small name="human-greeting-variant" color={getToken("$color.amethyst")} />)}
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChangeText={(value : string | undefined) => setUsername(value)}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<Input
|
||||
prependElement={
|
||||
<Icon small name='lock-outline' color={getToken('$color.amethyst')} />
|
||||
}
|
||||
placeholder='Password'
|
||||
value={password}
|
||||
onChangeText={(value: string | undefined) => setPassword(value)}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
secureTextEntry
|
||||
/>
|
||||
|
||||
<Spacer />
|
||||
<Spacer />
|
||||
|
||||
<Input
|
||||
prependElement={(<Icon small name="lock-outline" color={getToken("$color.amethyst")} />)}
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChangeText={(value : string | undefined) => setPassword(value)}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
secureTextEntry
|
||||
/>
|
||||
|
||||
<Spacer />
|
||||
|
||||
<XStack justifyContent="space-between">
|
||||
<Button
|
||||
marginVertical={0}
|
||||
icon={() => <Icon name="chevron-left" small />}
|
||||
bordered={0}
|
||||
onPress={() => {
|
||||
Client.switchServer()
|
||||
navigation.push("ServerAddress");
|
||||
}}>
|
||||
Switch Server
|
||||
</Button>
|
||||
{ useApiMutation.isPending ? (
|
||||
<Spinner />
|
||||
) :
|
||||
<Button
|
||||
marginVertical={0}
|
||||
disabled={_.isEmpty(username) || useApiMutation.isPending}
|
||||
onPress={() => {
|
||||
if (!_.isUndefined(username)) {
|
||||
console.log(`Signing in...`);
|
||||
useApiMutation.mutate({ username, password });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</Button>}
|
||||
</XStack>
|
||||
<Toast />
|
||||
</YStack>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
<XStack justifyContent='space-between'>
|
||||
<Button
|
||||
marginVertical={0}
|
||||
icon={() => <Icon name='chevron-left' small />}
|
||||
bordered={0}
|
||||
onPress={() => {
|
||||
Client.switchServer()
|
||||
navigation.push('ServerAddress')
|
||||
}}
|
||||
>
|
||||
Switch Server
|
||||
</Button>
|
||||
{useApiMutation.isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Button
|
||||
marginVertical={0}
|
||||
disabled={_.isEmpty(username) || useApiMutation.isPending}
|
||||
onPress={() => {
|
||||
if (!_.isUndefined(username)) {
|
||||
console.log(`Signing in...`)
|
||||
useApiMutation.mutate({ username, password })
|
||||
}
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
)}
|
||||
</XStack>
|
||||
<Toast />
|
||||
</YStack>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,95 +1,104 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getToken, Spinner, ToggleGroup } from "tamagui";
|
||||
import { useAuthenticationContext } from "../provider";
|
||||
import { H1, H2, Label, Text } from "../../Global/helpers/text";
|
||||
import Button from "../../Global/helpers/button";
|
||||
import _ from "lodash";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import Client from "../../../api/client";
|
||||
import { useJellifyContext } from "../../provider";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { QueryKeys } from "../../../enums/query-keys";
|
||||
import { fetchUserViews } from "../../../api/queries/functions/libraries";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { getToken, Spinner, ToggleGroup } from 'tamagui'
|
||||
import { useAuthenticationContext } from '../provider'
|
||||
import { H1, H2, Label, Text } from '../../Global/helpers/text'
|
||||
import Button from '../../Global/helpers/button'
|
||||
import _ from 'lodash'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import Client from '../../../api/client'
|
||||
import { useJellifyContext } from '../../provider'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchUserViews } from '../../../api/queries/functions/libraries'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
export default function ServerLibrary(): React.JSX.Element {
|
||||
const { setUser } = useAuthenticationContext()
|
||||
|
||||
const { setUser } = useAuthenticationContext();
|
||||
const { setLoggedIn } = useJellifyContext()
|
||||
|
||||
const { setLoggedIn } = useJellifyContext();
|
||||
const [libraryId, setLibraryId] = useState<string | undefined>(undefined)
|
||||
const [playlistLibrary, setPlaylistLibrary] = useState<BaseItemDto | undefined>(undefined)
|
||||
|
||||
const [libraryId, setLibraryId] = useState<string | undefined>(undefined);
|
||||
const [playlistLibrary, setPlaylistLibrary] = useState<BaseItemDto | undefined>(undefined);
|
||||
const {
|
||||
data: libraries,
|
||||
isError,
|
||||
isPending,
|
||||
isSuccess,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.UserViews],
|
||||
queryFn: () => fetchUserViews(),
|
||||
})
|
||||
|
||||
const { data : libraries, isError, isPending, isSuccess, refetch } = useQuery({
|
||||
queryKey: [QueryKeys.UserViews],
|
||||
queryFn: () => fetchUserViews()
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!isPending && isSuccess)
|
||||
setPlaylistLibrary(
|
||||
libraries.filter((library) => library.CollectionType === 'playlists')[0],
|
||||
)
|
||||
}, [isPending, isSuccess])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPending && isSuccess)
|
||||
setPlaylistLibrary(libraries.filter(library => library.CollectionType === 'playlists')[0])
|
||||
}, [
|
||||
isPending,
|
||||
isSuccess
|
||||
])
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<H2>Select Music Library</H2>
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<H2>Select Music Library</H2>
|
||||
{isPending ? (
|
||||
<Spinner size='large' />
|
||||
) : (
|
||||
<ToggleGroup
|
||||
orientation='vertical'
|
||||
type='single'
|
||||
disableDeactivation={true}
|
||||
value={libraryId}
|
||||
onValueChange={setLibraryId}>
|
||||
{libraries!
|
||||
.filter((library) => library.CollectionType === 'music')
|
||||
.map((library) => {
|
||||
return (
|
||||
<ToggleGroup.Item
|
||||
key={library.Id}
|
||||
value={library.Id!}
|
||||
aria-label={library.Name!}
|
||||
backgroundColor={
|
||||
libraryId == library.Id!
|
||||
? getToken('$color.purpleGray')
|
||||
: 'unset'
|
||||
}>
|
||||
<Text>{library.Name ?? 'Unnamed Library'}</Text>
|
||||
</ToggleGroup.Item>
|
||||
)
|
||||
})}
|
||||
</ToggleGroup>
|
||||
)}
|
||||
|
||||
{ isPending ? (
|
||||
<Spinner size="large" />
|
||||
) : (
|
||||
<ToggleGroup
|
||||
orientation="vertical"
|
||||
type="single"
|
||||
disableDeactivation={true}
|
||||
value={libraryId}
|
||||
onValueChange={setLibraryId}
|
||||
>
|
||||
{
|
||||
libraries!.filter(library => library.CollectionType === 'music')
|
||||
.map((library) => {
|
||||
return (
|
||||
<ToggleGroup.Item
|
||||
key={library.Id}
|
||||
value={library.Id!}
|
||||
aria-label={library.Name!}
|
||||
backgroundColor={libraryId == library.Id! ? getToken("$color.purpleGray") : 'unset'}
|
||||
>
|
||||
<Text>{library.Name ?? "Unnamed Library"}</Text>
|
||||
</ToggleGroup.Item>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ToggleGroup>
|
||||
)}
|
||||
{isError && <Text>Unable to load libraries</Text>}
|
||||
|
||||
{ isError && (
|
||||
<Text>Unable to load libraries</Text>
|
||||
)}
|
||||
<Button
|
||||
disabled={!libraryId}
|
||||
onPress={() => {
|
||||
Client.setLibrary({
|
||||
musicLibraryId: libraryId!,
|
||||
musicLibraryName:
|
||||
libraries?.filter((library) => library.Id == libraryId)[0].Name ??
|
||||
'No library name',
|
||||
musicLibraryPrimaryImageId: libraries?.filter(
|
||||
(library) => library.Id == libraryId,
|
||||
)[0].ImageTags!.Primary,
|
||||
playlistLibraryId: playlistLibrary?.Id,
|
||||
playlistLibraryPrimaryImageId: playlistLibrary?.ImageTags!.Primary,
|
||||
})
|
||||
setLoggedIn(true)
|
||||
}}>
|
||||
{`Let's Go!`}
|
||||
</Button>
|
||||
|
||||
<Button disabled={!libraryId}
|
||||
onPress={() => {
|
||||
Client.setLibrary({
|
||||
musicLibraryId: libraryId!,
|
||||
musicLibraryName: libraries?.filter((library) => library.Id == libraryId)[0].Name ?? "No library name",
|
||||
musicLibraryPrimaryImageId: libraries?.filter((library) => library.Id == libraryId)[0].ImageTags!.Primary,
|
||||
playlistLibraryId: playlistLibrary?.Id!,
|
||||
playlistLibraryPrimaryImageId: playlistLibrary?.ImageTags!.Primary,
|
||||
});
|
||||
setLoggedIn(true);
|
||||
}}>
|
||||
Let's Go!
|
||||
</Button>
|
||||
|
||||
<Button onPress={() => {
|
||||
Client.switchUser();
|
||||
setUser(undefined);
|
||||
}}>
|
||||
Switch User
|
||||
</Button>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
onPress={() => {
|
||||
Client.switchUser()
|
||||
setUser(undefined)
|
||||
}}>
|
||||
Switch User
|
||||
</Button>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const http = "http://"
|
||||
export const https = "https://"
|
||||
export const http = 'http://'
|
||||
export const https = 'https://'
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import _ from "lodash"
|
||||
import _ from 'lodash'
|
||||
|
||||
export function validateServerUrl(serverUrl: string | undefined) {
|
||||
if (!_.isEmpty(serverUrl)) {
|
||||
// Parse
|
||||
return true
|
||||
}
|
||||
|
||||
if (!_.isEmpty(serverUrl)) {
|
||||
// Parse
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
1
components/NativeCarPlay.android.ts
Normal file
1
components/NativeCarPlay.android.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const CarPlay = null
|
||||
2
components/NativeCarPlay.ts
Normal file
2
components/NativeCarPlay.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import { CarPlay as CarPlayInterface } from 'react-native-carplay'
|
||||
export const CarPlay = CarPlayInterface
|
||||
@@ -1,19 +1,19 @@
|
||||
import { TextTickerProps } from "react-native-text-ticker";
|
||||
import { TextTickerProps } from 'react-native-text-ticker'
|
||||
|
||||
export const TextTickerConfig : TextTickerProps = {
|
||||
duration: 5000,
|
||||
loop: true,
|
||||
repeatSpacer: 20,
|
||||
marqueeDelay: 1000
|
||||
export const TextTickerConfig: TextTickerProps = {
|
||||
duration: 5000,
|
||||
loop: true,
|
||||
repeatSpacer: 20,
|
||||
marqueeDelay: 1000,
|
||||
}
|
||||
|
||||
/**
|
||||
* RNTP (React Native Track Player) holds a high significant figure
|
||||
* number for the progress.
|
||||
*
|
||||
*
|
||||
* Tamagui Sliders only support whole integers
|
||||
*
|
||||
*
|
||||
* We're going to move the decimal place over so that Tamagui's slider
|
||||
* can be more precise
|
||||
*/
|
||||
export const ProgressMultiplier = 10 ^ 5
|
||||
export const ProgressMultiplier = 10 ^ 5
|
||||
|
||||
@@ -1,52 +1,55 @@
|
||||
import { State } from "react-native-track-player";
|
||||
import { Colors } from "react-native/Libraries/NewAppScreen";
|
||||
import { Spinner, View } from "tamagui";
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
import IconButton from "../../../components/Global/helpers/icon-button";
|
||||
import { State } from 'react-native-track-player'
|
||||
import { Colors } from 'react-native/Libraries/NewAppScreen'
|
||||
import { Spinner, View } from 'tamagui'
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import IconButton from '../../../components/Global/helpers/icon-button'
|
||||
|
||||
export default function PlayPauseButton({ size }: { size?: number | undefined }) : React.JSX.Element {
|
||||
export default function PlayPauseButton({
|
||||
size,
|
||||
}: {
|
||||
size?: number | undefined
|
||||
}): React.JSX.Element {
|
||||
const { playbackState, useTogglePlayback } = usePlayerContext()
|
||||
|
||||
const { playbackState, useTogglePlayback } = usePlayerContext();
|
||||
let button: React.JSX.Element
|
||||
|
||||
let button : React.JSX.Element;
|
||||
switch (playbackState) {
|
||||
case State.Playing: {
|
||||
button = (
|
||||
<IconButton
|
||||
circular
|
||||
largeIcon
|
||||
size={size}
|
||||
name='pause'
|
||||
onPress={() => useTogglePlayback.mutate(undefined)}
|
||||
/>
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
switch (playbackState) {
|
||||
case (State.Playing) : {
|
||||
button = (
|
||||
<IconButton
|
||||
circular
|
||||
largeIcon
|
||||
size={size}
|
||||
name="pause"
|
||||
onPress={() => useTogglePlayback.mutate(undefined)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case (State.Buffering) :
|
||||
case (State.Loading) : {
|
||||
button = <Spinner marginHorizontal={10} size="small" color={Colors.Primary}/>;
|
||||
break;
|
||||
}
|
||||
|
||||
default : {
|
||||
button = (
|
||||
<IconButton
|
||||
circular
|
||||
largeIcon
|
||||
size={size}
|
||||
name="play"
|
||||
onPress={() => useTogglePlayback.mutate(undefined)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
case State.Buffering:
|
||||
case State.Loading: {
|
||||
button = <Spinner marginHorizontal={10} size='small' color={Colors.Primary} />
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<View justifyContent="center" alignItems="center">
|
||||
{ button }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
button = (
|
||||
<IconButton
|
||||
circular
|
||||
largeIcon
|
||||
size={size}
|
||||
name='play'
|
||||
onPress={() => useTogglePlayback.mutate(undefined)}
|
||||
/>
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View justifyContent='center' alignItems='center'>
|
||||
{button}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,64 +1,52 @@
|
||||
import React from "react";
|
||||
import { XStack, getToken } from "tamagui";
|
||||
import PlayPauseButton from "./buttons";
|
||||
import Icon from "../../../components/Global/helpers/icon";
|
||||
import { getProgress, seekBy, skipToNext } from "react-native-track-player/lib/src/trackPlayer";
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import React from 'react'
|
||||
import { XStack, getToken } from 'tamagui'
|
||||
import PlayPauseButton from './buttons'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import { getProgress, seekBy, skipToNext } from 'react-native-track-player/lib/src/trackPlayer'
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
|
||||
export default function Controls(): React.JSX.Element {
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
const {
|
||||
usePrevious,
|
||||
useSeekTo
|
||||
} = usePlayerContext();
|
||||
const { usePrevious, useSeekTo } = usePlayerContext()
|
||||
|
||||
return (
|
||||
return (
|
||||
<XStack alignItems='center' justifyContent='space-evenly' marginVertical={'$2'}>
|
||||
<Icon
|
||||
color={getToken('$color.amethyst')}
|
||||
name='rewind-15'
|
||||
onPress={() => seekBy(-15)}
|
||||
/>
|
||||
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-evenly"
|
||||
marginVertical={"$2"}
|
||||
>
|
||||
<Icon
|
||||
color={getToken("$color.amethyst")}
|
||||
name="rewind-15"
|
||||
onPress={() => seekBy(-15)}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
color={getToken("$color.amethyst")}
|
||||
name="skip-previous"
|
||||
onPress={async () => {
|
||||
<Icon
|
||||
color={getToken('$color.amethyst')}
|
||||
name='skip-previous'
|
||||
onPress={async () => {
|
||||
const progress = await getProgress()
|
||||
if (progress.position < 3) usePrevious.mutate()
|
||||
else {
|
||||
useSeekTo.mutate(0)
|
||||
}
|
||||
}}
|
||||
large
|
||||
/>
|
||||
|
||||
const progress = await getProgress()
|
||||
if (progress.position < 3)
|
||||
usePrevious.mutate()
|
||||
else {
|
||||
useSeekTo.mutate(0);
|
||||
}
|
||||
}}
|
||||
large
|
||||
/>
|
||||
{/* I really wanted a big clunky play button */}
|
||||
<PlayPauseButton size={width / 5} />
|
||||
|
||||
{/* I really wanted a big clunky play button */}
|
||||
<PlayPauseButton size={width / 5} />
|
||||
<Icon
|
||||
color={getToken('$color.amethyst')}
|
||||
name='skip-next'
|
||||
onPress={() => skipToNext()}
|
||||
large
|
||||
/>
|
||||
|
||||
<Icon
|
||||
color={getToken("$color.amethyst")}
|
||||
name="skip-next"
|
||||
onPress={() => skipToNext()}
|
||||
large
|
||||
/>
|
||||
|
||||
<Icon
|
||||
color={getToken("$color.amethyst")}
|
||||
name="fast-forward-15"
|
||||
onPress={() => seekBy(15)}
|
||||
/>
|
||||
</XStack>
|
||||
|
||||
)
|
||||
}
|
||||
<Icon
|
||||
color={getToken('$color.amethyst')}
|
||||
name='fast-forward-15'
|
||||
onPress={() => seekBy(15)}
|
||||
/>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
export function handlePlayerError() {
|
||||
|
||||
}
|
||||
export function handlePlayerError() {}
|
||||
|
||||
@@ -1,116 +1,98 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useProgress } from "react-native-track-player";
|
||||
import { HorizontalSlider } from "../../../components/Global/helpers/slider";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import { trigger } from "react-native-haptic-feedback";
|
||||
import { getToken, XStack, YStack } from "tamagui";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
import { RunTimeSeconds } from "../../../components/Global/helpers/time-codes";
|
||||
import { UPDATE_INTERVAL } from "../../../player/config";
|
||||
import { ProgressMultiplier } from "../component.config";
|
||||
import Icon from "../../../components/Global/helpers/icon";
|
||||
import PlayPauseButton from "./buttons";
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useProgress } from 'react-native-track-player'
|
||||
import { HorizontalSlider } from '../../../components/Global/helpers/slider'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { getToken, XStack, YStack } from 'tamagui'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { RunTimeSeconds } from '../../../components/Global/helpers/time-codes'
|
||||
import { UPDATE_INTERVAL } from '../../../player/config'
|
||||
import { ProgressMultiplier } from '../component.config'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import PlayPauseButton from './buttons'
|
||||
|
||||
const scrubGesture = Gesture.Pan();
|
||||
const scrubGesture = Gesture.Pan()
|
||||
|
||||
export default function Scrubber() : React.JSX.Element {
|
||||
export default function Scrubber(): React.JSX.Element {
|
||||
const { useSeekTo, useSkip, usePrevious } = usePlayerContext()
|
||||
|
||||
const {
|
||||
useSeekTo,
|
||||
useSkip,
|
||||
usePrevious,
|
||||
} = usePlayerContext();
|
||||
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
const [seeking, setSeeking] = useState<boolean>(false)
|
||||
|
||||
const [seeking, setSeeking] = useState<boolean>(false);
|
||||
const progress = useProgress(UPDATE_INTERVAL)
|
||||
|
||||
const progress = useProgress(UPDATE_INTERVAL);
|
||||
|
||||
const [position, setPosition] = useState<number>(progress && progress.position ?
|
||||
Math.floor(progress.position * ProgressMultiplier)
|
||||
: 0
|
||||
);
|
||||
const [position, setPosition] = useState<number>(
|
||||
progress && progress.position ? Math.floor(progress.position * ProgressMultiplier) : 0,
|
||||
)
|
||||
|
||||
/**
|
||||
* Update position in the scrubber if the user isn't interacting
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (
|
||||
!seeking
|
||||
&& !useSkip.isPending
|
||||
&& !usePrevious.isPending
|
||||
&& !useSeekTo.isPending
|
||||
&& progress.position
|
||||
)
|
||||
setPosition(
|
||||
Math.floor(
|
||||
progress.position * ProgressMultiplier
|
||||
)
|
||||
);
|
||||
}, [
|
||||
progress.position
|
||||
]);
|
||||
/**
|
||||
* Update position in the scrubber if the user isn't interacting
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (
|
||||
!seeking &&
|
||||
!useSkip.isPending &&
|
||||
!usePrevious.isPending &&
|
||||
!useSeekTo.isPending &&
|
||||
progress.position
|
||||
)
|
||||
setPosition(Math.floor(progress.position * ProgressMultiplier))
|
||||
}, [progress.position])
|
||||
|
||||
return (
|
||||
<YStack>
|
||||
return (
|
||||
<YStack>
|
||||
<GestureDetector gesture={scrubGesture}>
|
||||
<HorizontalSlider
|
||||
value={position}
|
||||
max={
|
||||
progress && progress.duration > 0
|
||||
? progress.duration * ProgressMultiplier
|
||||
: 1
|
||||
}
|
||||
width={width / 1.125}
|
||||
props={{
|
||||
// If user swipes off of the slider we should seek to the spot
|
||||
onPressOut: () => {
|
||||
trigger('notificationSuccess')
|
||||
useSeekTo.mutate(Math.floor(position / ProgressMultiplier))
|
||||
setSeeking(false)
|
||||
},
|
||||
onSlideStart: (event, value) => {
|
||||
setSeeking(true)
|
||||
trigger('impactLight')
|
||||
setPosition(value)
|
||||
},
|
||||
onSlideMove: (event, value) => {
|
||||
trigger('clockTick')
|
||||
setPosition(value)
|
||||
},
|
||||
onSlideEnd: (event, value) => {
|
||||
trigger('notificationSuccess')
|
||||
setPosition(value)
|
||||
useSeekTo.mutate(Math.floor(value / ProgressMultiplier))
|
||||
setSeeking(false)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</GestureDetector>
|
||||
|
||||
<GestureDetector gesture={scrubGesture}>
|
||||
<HorizontalSlider
|
||||
value={position}
|
||||
max={
|
||||
progress && progress.duration > 0
|
||||
? progress.duration * ProgressMultiplier
|
||||
: 1
|
||||
}
|
||||
width={width / 1.125}
|
||||
props={{
|
||||
// If user swipes off of the slider we should seek to the spot
|
||||
onPressOut: () => {
|
||||
trigger("notificationSuccess")
|
||||
useSeekTo.mutate(Math.floor(position / ProgressMultiplier));
|
||||
setSeeking(false);
|
||||
},
|
||||
onSlideStart: (event, value) => {
|
||||
setSeeking(true);
|
||||
trigger("impactLight");
|
||||
setPosition(value)
|
||||
},
|
||||
onSlideMove: (event, value) => {
|
||||
trigger("clockTick")
|
||||
setPosition(value);
|
||||
},
|
||||
onSlideEnd: (event, value) => {
|
||||
trigger("notificationSuccess")
|
||||
setPosition(value)
|
||||
useSeekTo.mutate(Math.floor(value / ProgressMultiplier));
|
||||
setSeeking(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</GestureDetector>
|
||||
<XStack margin={'$2'} marginTop={'$3'}>
|
||||
<YStack flex={1} alignItems='flex-start'>
|
||||
<RunTimeSeconds>{Math.floor(position / ProgressMultiplier)}</RunTimeSeconds>
|
||||
</YStack>
|
||||
|
||||
<XStack margin={"$2"} marginTop={"$3"}>
|
||||
<YStack flex={1} alignItems="flex-start">
|
||||
<RunTimeSeconds>{Math.floor(position / ProgressMultiplier)}</RunTimeSeconds>
|
||||
</YStack>
|
||||
<YStack flex={1} alignItems='center'>
|
||||
{/** Track metadata can go here */}
|
||||
</YStack>
|
||||
|
||||
<YStack flex={1} alignItems="center">
|
||||
{ /** Track metadata can go here */}
|
||||
</YStack>
|
||||
|
||||
<YStack flex={1} alignItems="flex-end">
|
||||
<RunTimeSeconds>
|
||||
{
|
||||
progress && progress.duration
|
||||
? Math.ceil(progress.duration)
|
||||
: 0
|
||||
}
|
||||
</RunTimeSeconds>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
<YStack flex={1} alignItems='flex-end'>
|
||||
<RunTimeSeconds>
|
||||
{progress && progress.duration ? Math.ceil(progress.duration) : 0}
|
||||
</RunTimeSeconds>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,100 +1,97 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { getToken, getTokens, useTheme, View, XStack, YStack } from "tamagui";
|
||||
import { usePlayerContext } from "../../player/provider";
|
||||
import { BottomTabNavigationEventMap } from "@react-navigation/bottom-tabs";
|
||||
import { NavigationHelpers, ParamListBase } from "@react-navigation/native";
|
||||
import Icon from "../Global/helpers/icon";
|
||||
import { Text } from "../Global/helpers/text";
|
||||
import TextTicker from 'react-native-text-ticker';
|
||||
import PlayPauseButton from "./helpers/buttons";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import { TextTickerConfig } from "./component.config";
|
||||
import { Image } from "expo-image";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from "../../api/client";
|
||||
import React, { useMemo } from 'react'
|
||||
import { getToken, getTokens, useTheme, View, XStack, YStack } from 'tamagui'
|
||||
import { usePlayerContext } from '../../player/provider'
|
||||
import { BottomTabNavigationEventMap } from '@react-navigation/bottom-tabs'
|
||||
import { NavigationHelpers, ParamListBase } from '@react-navigation/native'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import TextTicker from 'react-native-text-ticker'
|
||||
import PlayPauseButton from './helpers/buttons'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { TextTickerConfig } from './component.config'
|
||||
import { Image } from 'expo-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../api/client'
|
||||
|
||||
export function Miniplayer({
|
||||
navigation
|
||||
} : {
|
||||
navigation : NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>
|
||||
}) : React.JSX.Element {
|
||||
export function Miniplayer({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>
|
||||
}): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
|
||||
const theme = useTheme();
|
||||
const { nowPlaying, useSkip } = usePlayerContext()
|
||||
|
||||
const { nowPlaying, useSkip } = usePlayerContext();
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.background.val,
|
||||
borderColor: theme.borderColor.val,
|
||||
}}
|
||||
>
|
||||
{nowPlaying &&
|
||||
useMemo(() => {
|
||||
return (
|
||||
<XStack
|
||||
alignItems='center'
|
||||
margin={0}
|
||||
padding={0}
|
||||
height={'$6'}
|
||||
onPress={() => navigation.navigate('Player')}
|
||||
>
|
||||
<YStack
|
||||
justify='center'
|
||||
alignItems='flex-start'
|
||||
flex={1}
|
||||
minHeight={'$12'}
|
||||
>
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(
|
||||
nowPlaying!.item.AlbumId!,
|
||||
)}
|
||||
placeholder={
|
||||
nowPlaying!.item.ImageBlurHashes?.Primary![0] ?? undefined
|
||||
}
|
||||
style={{
|
||||
width: getToken('$12'),
|
||||
height: getToken('$12'),
|
||||
borderRadius: getToken('$1'),
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
return (
|
||||
<View style={{
|
||||
backgroundColor: theme.background.val,
|
||||
borderColor: theme.borderColor.val
|
||||
}}>
|
||||
{ nowPlaying && (
|
||||
<YStack
|
||||
alignContent='flex-start'
|
||||
marginLeft={'$2'}
|
||||
flex={4}
|
||||
maxWidth={'$20'}
|
||||
>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text bold>{nowPlaying?.title ?? 'Nothing Playing'}</Text>
|
||||
</TextTicker>
|
||||
|
||||
useMemo(() => {
|
||||
return (
|
||||
<XStack
|
||||
alignItems="center"
|
||||
margin={0}
|
||||
padding={0}
|
||||
height={"$6"}
|
||||
onPress={() => navigation.navigate("Player")}
|
||||
>
|
||||
<YStack
|
||||
justify="center"
|
||||
alignItems="flex-start"
|
||||
flex={1}
|
||||
minHeight={"$12"}
|
||||
>
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(nowPlaying!.item.AlbumId!)}
|
||||
placeholder={nowPlaying!.item.ImageBlurHashes?.Primary![0] ?? undefined}
|
||||
style={{
|
||||
width: getToken("$12"),
|
||||
height: getToken("$12"),
|
||||
borderRadius: getToken("$1")
|
||||
}}
|
||||
/>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text color={getTokens().color.telemagenta}>
|
||||
{nowPlaying?.artist ?? ''}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
</YStack>
|
||||
|
||||
</YStack>
|
||||
<XStack justifyContent='flex-end' flex={2}>
|
||||
<PlayPauseButton />
|
||||
|
||||
<YStack
|
||||
alignContent="flex-start"
|
||||
marginLeft={"$2"}
|
||||
flex={4}
|
||||
maxWidth={"$20"}
|
||||
>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text bold>{nowPlaying?.title ?? "Nothing Playing"}</Text>
|
||||
</TextTicker>
|
||||
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text color={getTokens().color.telemagenta}>{nowPlaying?.artist ?? ""}</Text>
|
||||
</TextTicker>
|
||||
|
||||
</YStack>
|
||||
|
||||
<XStack
|
||||
justifyContent="flex-end"
|
||||
flex={2}
|
||||
>
|
||||
<PlayPauseButton />
|
||||
|
||||
<Icon
|
||||
large
|
||||
color={theme.borderColor.val}
|
||||
name="skip-next"
|
||||
onPress={() => useSkip.mutate(undefined)}
|
||||
/>
|
||||
</XStack>
|
||||
</XStack>
|
||||
)
|
||||
}, [
|
||||
nowPlaying
|
||||
])
|
||||
)
|
||||
}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
<Icon
|
||||
large
|
||||
color={theme.borderColor.val}
|
||||
name='skip-next'
|
||||
onPress={() => useSkip.mutate(undefined)}
|
||||
/>
|
||||
</XStack>
|
||||
</XStack>
|
||||
)
|
||||
}, [nowPlaying])}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,209 +1,183 @@
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import React, { useMemo } from "react";
|
||||
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import { YStack, XStack, Spacer, getTokens } from "tamagui";
|
||||
import { Text } from "../../../components/Global/helpers/text";
|
||||
import Icon from "../../../components/Global/helpers/icon";
|
||||
import FavoriteButton from "../../Global/components/favorite-button";
|
||||
import TextTicker from "react-native-text-ticker";
|
||||
import { TextTickerConfig } from "../component.config";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import Scrubber from "../helpers/scrubber";
|
||||
import Controls from "../helpers/controls";
|
||||
import { Image } from "expo-image";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from "../../../api/client";
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import React, { useMemo } from 'react'
|
||||
import { SafeAreaView, useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { YStack, XStack, Spacer, getTokens } from 'tamagui'
|
||||
import { Text } from '../../../components/Global/helpers/text'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import FavoriteButton from '../../Global/components/favorite-button'
|
||||
import TextTicker from 'react-native-text-ticker'
|
||||
import { TextTickerConfig } from '../component.config'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import Scrubber from '../helpers/scrubber'
|
||||
import Controls from '../helpers/controls'
|
||||
import { Image } from 'expo-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../../api/client'
|
||||
|
||||
export default function PlayerScreen({
|
||||
navigation
|
||||
} : {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
export default function PlayerScreen({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { nowPlayingIsFavorite, setNowPlayingIsFavorite, nowPlaying, queue } = usePlayerContext()
|
||||
|
||||
const {
|
||||
nowPlayingIsFavorite,
|
||||
setNowPlayingIsFavorite,
|
||||
nowPlaying,
|
||||
queue
|
||||
} = usePlayerContext();
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
return (
|
||||
<SafeAreaView edges={['right', 'left']}>
|
||||
{nowPlaying && (
|
||||
<>
|
||||
<YStack>
|
||||
{useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<XStack marginBottom={'$2'} marginHorizontal={'$2'}>
|
||||
<YStack
|
||||
alignContent='flex-end'
|
||||
flex={1}
|
||||
justifyContent='center'
|
||||
>
|
||||
<Icon
|
||||
name='chevron-down'
|
||||
onPress={() => {
|
||||
navigation.goBack()
|
||||
}}
|
||||
small
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["right", "left"]}>
|
||||
{ nowPlaying && (
|
||||
<>
|
||||
<YStack>
|
||||
<YStack alignItems='center' alignContent='center' flex={6}>
|
||||
<Text>Playing from</Text>
|
||||
<Text
|
||||
bold
|
||||
numberOfLines={1}
|
||||
lineBreakStrategyIOS='standard'
|
||||
>
|
||||
{
|
||||
// If the Queue is a BaseItemDto, display the name of it
|
||||
typeof queue === 'object'
|
||||
? (queue as BaseItemDto).Name ?? 'Untitled'
|
||||
: queue
|
||||
}
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
{ useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<XStack
|
||||
marginBottom={"$2"}
|
||||
marginHorizontal={"$2"}
|
||||
>
|
||||
<Spacer flex={1} />
|
||||
</XStack>
|
||||
|
||||
<YStack
|
||||
alignContent="flex-end"
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
>
|
||||
<Icon
|
||||
name="chevron-down"
|
||||
onPress={() => {
|
||||
navigation.goBack();
|
||||
}}
|
||||
small
|
||||
/>
|
||||
</YStack>
|
||||
<XStack
|
||||
justifyContent='center'
|
||||
alignContent='center'
|
||||
minHeight={width / 1.1}
|
||||
>
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(
|
||||
nowPlaying!.item.AlbumId!,
|
||||
)}
|
||||
placeholder={
|
||||
nowPlaying!.item.ImageBlurHashes?.Primary![0] ??
|
||||
undefined
|
||||
}
|
||||
style={{
|
||||
borderRadius: 2,
|
||||
width: width / 1.1,
|
||||
}}
|
||||
/>
|
||||
</XStack>
|
||||
</>
|
||||
)
|
||||
}, [nowPlaying, queue])}
|
||||
|
||||
<YStack
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
flex={6}
|
||||
>
|
||||
<XStack marginHorizontal={20} paddingVertical={5}>
|
||||
{/** Memoize TextTickers otherwise they won't animate due to the progress being updated in the PlayerContext */}
|
||||
{useMemo(() => {
|
||||
return (
|
||||
<YStack justifyContent='flex-start' flex={5}>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text bold fontSize={'$6'}>
|
||||
{nowPlaying!.title ?? 'Untitled Track'}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
|
||||
<Text>Playing from</Text>
|
||||
<Text
|
||||
bold
|
||||
numberOfLines={1}
|
||||
lineBreakStrategyIOS="standard"
|
||||
>
|
||||
{
|
||||
// If the Queue is a BaseItemDto, display the name of it
|
||||
typeof(queue) === 'object'
|
||||
? (queue as BaseItemDto).Name ?? "Untitled"
|
||||
: queue
|
||||
}
|
||||
</Text>
|
||||
</YStack>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text
|
||||
fontSize={'$6'}
|
||||
color={getTokens().color.telemagenta}
|
||||
onPress={() => {
|
||||
if (nowPlaying!.item.ArtistItems) {
|
||||
navigation.goBack() // Dismiss player modal
|
||||
navigation.navigate('Tabs', {
|
||||
screen: 'Home',
|
||||
params: {
|
||||
screen: 'Artist',
|
||||
params: {
|
||||
artist: nowPlaying!.item
|
||||
.ArtistItems![0],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{nowPlaying.artist ?? 'Unknown Artist'}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
|
||||
<Spacer flex={1} />
|
||||
</XStack>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text fontSize={'$6'} color={'$borderColor'}>
|
||||
{nowPlaying!.album ?? ''}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
</YStack>
|
||||
)
|
||||
}, [nowPlaying])}
|
||||
|
||||
<XStack
|
||||
justifyContent="center"
|
||||
alignContent="center"
|
||||
minHeight={width / 1.1}
|
||||
>
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(nowPlaying!.item.AlbumId!)}
|
||||
placeholder={nowPlaying!.item.ImageBlurHashes?.Primary![0] ?? undefined}
|
||||
style={{
|
||||
borderRadius: 2,
|
||||
width: width / 1.1
|
||||
}}
|
||||
/>
|
||||
</XStack>
|
||||
</>
|
||||
)
|
||||
}, [
|
||||
nowPlaying,
|
||||
queue
|
||||
])}
|
||||
<XStack justifyContent='flex-end' alignItems='center' flex={2}>
|
||||
{/* Buttons for favorites, song menu go here */}
|
||||
|
||||
<XStack marginHorizontal={20} paddingVertical={5}>
|
||||
<Icon
|
||||
name='dots-horizontal-circle-outline'
|
||||
onPress={() => {
|
||||
navigation.navigate('Details', {
|
||||
item: nowPlaying!.item,
|
||||
isNested: true,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
{/** Memoize TextTickers otherwise they won't animate due to the progress being updated in the PlayerContext */}
|
||||
{ useMemo(() => {
|
||||
return (
|
||||
<YStack justifyContent="flex-start" flex={5}>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text
|
||||
bold
|
||||
fontSize={"$6"}
|
||||
>
|
||||
{nowPlaying!.title ?? "Untitled Track"}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
<Spacer />
|
||||
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text
|
||||
fontSize={"$6"}
|
||||
color={getTokens().color.telemagenta}
|
||||
onPress={() => {
|
||||
if (nowPlaying!.item.ArtistItems) {
|
||||
navigation.goBack(); // Dismiss player modal
|
||||
navigation.navigate('Tabs', {
|
||||
screen: 'Home',
|
||||
params: {
|
||||
screen: 'Artist',
|
||||
params: {
|
||||
artist: nowPlaying!.item.ArtistItems![0],
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{nowPlaying.artist ?? "Unknown Artist"}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
<FavoriteButton
|
||||
item={nowPlaying!.item}
|
||||
onToggle={() => setNowPlayingIsFavorite(!nowPlayingIsFavorite)}
|
||||
/>
|
||||
</XStack>
|
||||
</XStack>
|
||||
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text
|
||||
fontSize={"$6"}
|
||||
color={"$borderColor"}
|
||||
>
|
||||
{ nowPlaying!.album ?? "" }
|
||||
</Text>
|
||||
</TextTicker>
|
||||
</YStack>
|
||||
)}, [
|
||||
nowPlaying
|
||||
])}
|
||||
<XStack justifyContent='center' marginTop={'$3'}>
|
||||
{/* playback progress goes here */}
|
||||
<Scrubber />
|
||||
</XStack>
|
||||
|
||||
<XStack
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
flex={2}
|
||||
>
|
||||
{/* Buttons for favorites, song menu go here */}
|
||||
<Controls />
|
||||
|
||||
<Icon
|
||||
name="dots-horizontal-circle-outline"
|
||||
onPress={() => {
|
||||
navigation.navigate("Details", {
|
||||
item: nowPlaying!.item,
|
||||
isNested: true
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<XStack justifyContent='space-evenly' marginVertical={'$7'}>
|
||||
<Icon name='speaker-multiple' />
|
||||
|
||||
<Spacer />
|
||||
<Spacer />
|
||||
|
||||
<FavoriteButton
|
||||
item={nowPlaying!.item}
|
||||
onToggle={() => setNowPlayingIsFavorite(!nowPlayingIsFavorite)}
|
||||
/>
|
||||
</XStack>
|
||||
</XStack>
|
||||
|
||||
<XStack justifyContent="center" marginTop={"$3"}>
|
||||
{/* playback progress goes here */}
|
||||
<Scrubber />
|
||||
</XStack>
|
||||
|
||||
<Controls />
|
||||
|
||||
<XStack justifyContent="space-evenly" marginVertical={"$7"}>
|
||||
<Icon name="speaker-multiple"
|
||||
/>
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Icon
|
||||
name="playlist-music"
|
||||
onPress={() => {
|
||||
navigation.navigate("Queue");
|
||||
}}
|
||||
/>
|
||||
</XStack>
|
||||
</YStack>
|
||||
</>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
<Icon
|
||||
name='playlist-music'
|
||||
onPress={() => {
|
||||
navigation.navigate('Queue')
|
||||
}}
|
||||
/>
|
||||
</XStack>
|
||||
</YStack>
|
||||
</>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,84 +1,91 @@
|
||||
import Icon from "../../../components/Global/helpers/icon";
|
||||
import Track from "../../../components/Global/components/track";
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import DraggableFlatList from "react-native-draggable-flatlist";
|
||||
import { trigger } from "react-native-haptic-feedback";
|
||||
import { Separator } from "tamagui";
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import Track from '../../../components/Global/components/track'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import DraggableFlatList from 'react-native-draggable-flatlist'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { Separator } from 'tamagui'
|
||||
|
||||
export default function Queue({ navigation }: { navigation: NativeStackNavigationProp<StackParamList>}): React.JSX.Element {
|
||||
export default function Queue({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { width } = useSafeAreaFrame()
|
||||
const {
|
||||
playQueue,
|
||||
queue,
|
||||
useClearQueue,
|
||||
useRemoveFromQueue,
|
||||
useReorderQueue,
|
||||
useSkip,
|
||||
nowPlaying,
|
||||
} = usePlayerContext()
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
const {
|
||||
playQueue,
|
||||
queue,
|
||||
useClearQueue,
|
||||
useRemoveFromQueue,
|
||||
useReorderQueue,
|
||||
useSkip,
|
||||
nowPlaying
|
||||
} = usePlayerContext();
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
return (
|
||||
<Icon
|
||||
name='notification-clear-all'
|
||||
onPress={() => {
|
||||
useClearQueue.mutate()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
return (
|
||||
<Icon name="notification-clear-all" onPress={() => {
|
||||
useClearQueue.mutate()
|
||||
}}/>
|
||||
)
|
||||
}
|
||||
});
|
||||
const scrollIndex = playQueue.findIndex(
|
||||
(queueItem) => queueItem.item.Id! === nowPlaying!.item.Id!,
|
||||
)
|
||||
|
||||
const scrollIndex = playQueue.findIndex(queueItem => queueItem.item.Id! === nowPlaying!.item.Id!)
|
||||
|
||||
return (
|
||||
<DraggableFlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
data={playQueue}
|
||||
dragHitSlop={{ left: -50 }} // https://github.com/computerjazz/react-native-draggable-flatlist/issues/336
|
||||
extraData={nowPlaying}
|
||||
// enableLayoutAnimationExperimental
|
||||
getItemLayout={(data, index) => (
|
||||
{ length: width / 9, offset: width / 9 * index, index}
|
||||
)}
|
||||
initialScrollIndex={scrollIndex !== -1 ? scrollIndex: 0}
|
||||
ItemSeparatorComponent={() => <Separator />}
|
||||
// itemEnteringAnimation={FadeIn}
|
||||
// itemExitingAnimation={FadeOut}
|
||||
// itemLayoutAnimation={SequencedTransition}
|
||||
keyExtractor={({ item }, index) => {
|
||||
return `${index}-${item.Id}`
|
||||
}}
|
||||
numColumns={1}
|
||||
onDragEnd={({ data, from, to}) => {
|
||||
useReorderQueue.mutate({ newOrder: data, from, to });
|
||||
}}
|
||||
renderItem={({ item: queueItem, getIndex, drag, isActive }) =>
|
||||
|
||||
<Track
|
||||
queue={queue}
|
||||
navigation={navigation}
|
||||
track={queueItem.item}
|
||||
index={getIndex()}
|
||||
showArtwork
|
||||
onPress={() => {
|
||||
useSkip.mutate(getIndex());
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactLight');
|
||||
drag();
|
||||
}}
|
||||
isNested
|
||||
showRemove
|
||||
onRemove={() => {
|
||||
if (getIndex())
|
||||
useRemoveFromQueue.mutate(getIndex()!)
|
||||
}}
|
||||
/>
|
||||
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<DraggableFlatList
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
data={playQueue}
|
||||
dragHitSlop={{ left: -50 }} // https://github.com/computerjazz/react-native-draggable-flatlist/issues/336
|
||||
extraData={nowPlaying}
|
||||
// enableLayoutAnimationExperimental
|
||||
getItemLayout={(data, index) => ({
|
||||
length: width / 9,
|
||||
offset: (width / 9) * index,
|
||||
index,
|
||||
})}
|
||||
initialScrollIndex={scrollIndex !== -1 ? scrollIndex : 0}
|
||||
ItemSeparatorComponent={() => <Separator />}
|
||||
// itemEnteringAnimation={FadeIn}
|
||||
// itemExitingAnimation={FadeOut}
|
||||
// itemLayoutAnimation={SequencedTransition}
|
||||
keyExtractor={({ item }, index) => {
|
||||
return `${index}-${item.Id}`
|
||||
}}
|
||||
numColumns={1}
|
||||
onDragEnd={({ data, from, to }) => {
|
||||
useReorderQueue.mutate({ newOrder: data, from, to })
|
||||
}}
|
||||
renderItem={({ item: queueItem, getIndex, drag, isActive }) => (
|
||||
<Track
|
||||
queue={queue}
|
||||
navigation={navigation}
|
||||
track={queueItem.item}
|
||||
index={getIndex()}
|
||||
showArtwork
|
||||
onPress={() => {
|
||||
useSkip.mutate(getIndex())
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger('impactLight')
|
||||
drag()
|
||||
}}
|
||||
isNested
|
||||
showRemove
|
||||
onRemove={() => {
|
||||
if (getIndex()) useRemoveFromQueue.mutate(getIndex()!)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,45 +1,40 @@
|
||||
import React from "react";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../types";
|
||||
import PlayerScreen from "./screens";
|
||||
import Queue from "./screens/queue";
|
||||
import DetailsScreen from "../ItemDetail/screen";
|
||||
import { AlbumScreen } from "../Album";
|
||||
import React from 'react'
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../types'
|
||||
import PlayerScreen from './screens'
|
||||
import Queue from './screens/queue'
|
||||
import DetailsScreen from '../ItemDetail/screen'
|
||||
import { AlbumScreen } from '../Album'
|
||||
|
||||
export const PlayerStack = createNativeStackNavigator<StackParamList>();
|
||||
export const PlayerStack = createNativeStackNavigator<StackParamList>()
|
||||
|
||||
export default function Player() : React.JSX.Element {
|
||||
return (
|
||||
<PlayerStack.Navigator
|
||||
initialRouteName="Player"
|
||||
screenOptions={{}}
|
||||
>
|
||||
<PlayerStack.Screen
|
||||
name="Player"
|
||||
component={PlayerScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
headerTitle: "",
|
||||
}}
|
||||
/>
|
||||
export default function Player(): React.JSX.Element {
|
||||
return (
|
||||
<PlayerStack.Navigator initialRouteName='Player' screenOptions={{}}>
|
||||
<PlayerStack.Screen
|
||||
name='Player'
|
||||
component={PlayerScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
headerTitle: '',
|
||||
}}
|
||||
/>
|
||||
|
||||
<PlayerStack.Screen
|
||||
name="Queue"
|
||||
component={Queue}
|
||||
options={{
|
||||
headerTitle: ""
|
||||
}}
|
||||
/>
|
||||
<PlayerStack.Screen
|
||||
name='Queue'
|
||||
component={Queue}
|
||||
options={{
|
||||
headerTitle: '',
|
||||
}}
|
||||
/>
|
||||
|
||||
<PlayerStack.Screen
|
||||
name="Details"
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerTitle: ""
|
||||
}}
|
||||
/>
|
||||
|
||||
</PlayerStack.Navigator>
|
||||
);
|
||||
<PlayerStack.Screen
|
||||
name='Details'
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerTitle: '',
|
||||
}}
|
||||
/>
|
||||
</PlayerStack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
12
components/Player/types.d.ts
vendored
12
components/Player/types.d.ts
vendored
@@ -1,9 +1,9 @@
|
||||
import { QueuingType } from "../../enums/queuing-type";
|
||||
import { JellifyTrack } from "../../types/JellifyTrack";
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { JellifyTrack } from '../../types/JellifyTrack'
|
||||
|
||||
export type Item = JellifyTrack;
|
||||
export type Item = JellifyTrack
|
||||
|
||||
export type Section = {
|
||||
title: QueuingType,
|
||||
data: Item[]
|
||||
}
|
||||
title: QueuingType
|
||||
data: Item[]
|
||||
}
|
||||
|
||||
@@ -1,211 +1,199 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../types";
|
||||
import { getToken, Separator, Spacer, XStack, YStack } from "tamagui";
|
||||
import { RunTimeTicks } from "../Global/helpers/time-codes";
|
||||
import { H4, H5, Text } from "../Global/helpers/text";
|
||||
import Track from "../Global/components/track";
|
||||
import DraggableFlatList from "react-native-draggable-flatlist";
|
||||
import { removeFromPlaylist, updatePlaylist } from "../../api/mutations/functions/playlists";
|
||||
import { useEffect, useState } from "react";
|
||||
import Icon from "../Global/helpers/icon";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { trigger } from "react-native-haptic-feedback";
|
||||
import { queryClient } from "../../constants/query-client";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { getImageApi, getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from "../../api/client";
|
||||
import { RefreshControl } from "react-native";
|
||||
import { Image } from "expo-image";
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../types'
|
||||
import { getToken, Separator, Spacer, XStack, YStack } from 'tamagui'
|
||||
import { RunTimeTicks } from '../Global/helpers/time-codes'
|
||||
import { H4, H5, Text } from '../Global/helpers/text'
|
||||
import Track from '../Global/components/track'
|
||||
import DraggableFlatList from 'react-native-draggable-flatlist'
|
||||
import { removeFromPlaylist, updatePlaylist } from '../../api/mutations/functions/playlists'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { getImageApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../api/client'
|
||||
import { RefreshControl } from 'react-native'
|
||||
import { Image } from 'expo-image'
|
||||
|
||||
interface PlaylistProps {
|
||||
playlist: BaseItemDto;
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
interface PlaylistProps {
|
||||
playlist: BaseItemDto
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}
|
||||
|
||||
interface PlaylistOrderMutation {
|
||||
playlist: BaseItemDto;
|
||||
track: BaseItemDto;
|
||||
to: number
|
||||
playlist: BaseItemDto
|
||||
track: BaseItemDto
|
||||
to: number
|
||||
}
|
||||
|
||||
interface RemoveFromPlaylistMutation {
|
||||
playlist: BaseItemDto;
|
||||
track: BaseItemDto;
|
||||
index: number;
|
||||
playlist: BaseItemDto
|
||||
track: BaseItemDto
|
||||
index: number
|
||||
}
|
||||
|
||||
export default function Playlist({
|
||||
playlist,
|
||||
navigation
|
||||
}: PlaylistProps): React.JSX.Element {
|
||||
export default function Playlist({ playlist, navigation }: PlaylistProps): React.JSX.Element {
|
||||
const [editing, setEditing] = useState<boolean>(false)
|
||||
const [playlistTracks, setPlaylistTracks] = useState<BaseItemDto[]>([])
|
||||
const {
|
||||
data: tracks,
|
||||
isPending,
|
||||
isSuccess,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, playlist.Id!],
|
||||
queryFn: () => {
|
||||
return getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
parentId: playlist.Id!,
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.Items ? response.data.Items! : []
|
||||
})
|
||||
},
|
||||
staleTime: 1000 * 60 * 1 * 1 * 1, // 1 minute, since these are mutable by nature
|
||||
})
|
||||
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
const [playlistTracks, setPlaylistTracks] = useState<BaseItemDto[]>([]);
|
||||
const { data: tracks, isPending, isSuccess, refetch } = useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, playlist.Id!],
|
||||
queryFn: () => {
|
||||
|
||||
return getItemsApi(Client.api!).getItems({
|
||||
parentId: playlist.Id!,
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.Items ? response.data.Items! : [];
|
||||
})
|
||||
},
|
||||
staleTime: (1000 * 60 * 1 * 1) * 1 // 1 minute, since these are mutable by nature
|
||||
});
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
return (
|
||||
<XStack justifyContent='space-between'>
|
||||
{editing && (
|
||||
<Icon
|
||||
color={getToken('$color.danger')}
|
||||
name='delete-sweep-outline' // otherwise use "delete-circle"
|
||||
onPress={() => navigation.navigate('DeletePlaylist', { playlist })}
|
||||
/>
|
||||
)}
|
||||
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
return (
|
||||
<Spacer />
|
||||
|
||||
<XStack justifyContent="space-between">
|
||||
<Icon
|
||||
color={getToken('$color.amethyst')}
|
||||
name={editing ? 'content-save-outline' : 'pencil'}
|
||||
onPress={() => setEditing(!editing)}
|
||||
/>
|
||||
</XStack>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
{ editing && (
|
||||
<Icon
|
||||
color={getToken("$color.danger")}
|
||||
name="delete-sweep-outline" // otherwise use "delete-circle"
|
||||
onPress={() => navigation.navigate("DeletePlaylist", { playlist })}
|
||||
/>
|
||||
// If we've got the playlist tracks, set our component state
|
||||
useEffect(() => {
|
||||
if (!isPending && isSuccess) setPlaylistTracks(tracks)
|
||||
}, [isPending, isSuccess])
|
||||
|
||||
)}
|
||||
// Refresh playlist tracks if we've finished editing
|
||||
useEffect(() => {
|
||||
if (!editing) refetch()
|
||||
}, [editing])
|
||||
|
||||
<Spacer />
|
||||
const useUpdatePlaylist = useMutation({
|
||||
mutationFn: ({ playlist, tracks }: { playlist: BaseItemDto; tracks: BaseItemDto[] }) => {
|
||||
return updatePlaylist(
|
||||
playlist.Id!,
|
||||
playlist.Name!,
|
||||
tracks.map((track) => track.Id!),
|
||||
)
|
||||
},
|
||||
onSuccess: () => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
<Icon
|
||||
color={getToken("$color.amethyst")}
|
||||
name={editing ? 'content-save-outline' : 'pencil'}
|
||||
onPress={() => setEditing(!editing)}
|
||||
/>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
});
|
||||
// Refresh playlist component data
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.ItemTracks, playlist.Id],
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
trigger('notificationError')
|
||||
|
||||
// If we've got the playlist tracks, set our component state
|
||||
useEffect(() => {
|
||||
if (!isPending && isSuccess)
|
||||
setPlaylistTracks(tracks);
|
||||
}, [
|
||||
isPending,
|
||||
isSuccess
|
||||
]);
|
||||
setPlaylistTracks(tracks ?? [])
|
||||
},
|
||||
})
|
||||
|
||||
// Refresh playlist tracks if we've finished editing
|
||||
useEffect(() => {
|
||||
if (!editing)
|
||||
refetch();
|
||||
}, [
|
||||
editing
|
||||
]);
|
||||
const useRemoveFromPlaylist = useMutation({
|
||||
mutationFn: ({ playlist, track, index }: RemoveFromPlaylistMutation) => {
|
||||
return removeFromPlaylist(track, playlist)
|
||||
},
|
||||
onSuccess: (data, { index }) => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
setPlaylistTracks(
|
||||
playlistTracks
|
||||
.slice(0, index)
|
||||
.concat(playlistTracks.slice(index + 1, playlistTracks.length - 1)),
|
||||
)
|
||||
},
|
||||
onError: () => {
|
||||
trigger('notificationError')
|
||||
},
|
||||
})
|
||||
|
||||
const useUpdatePlaylist = useMutation({
|
||||
mutationFn: ({ playlist, tracks }: { playlist: BaseItemDto, tracks: BaseItemDto[] }) => {
|
||||
return updatePlaylist(playlist.Id!, playlist.Name!, tracks.map(track => track.Id!))
|
||||
},
|
||||
onSuccess: () => {
|
||||
trigger('notificationSuccess');
|
||||
return (
|
||||
<DraggableFlatList
|
||||
refreshControl={<RefreshControl refreshing={isPending} onRefresh={refetch} />}
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
data={playlistTracks}
|
||||
dragHitSlop={{ left: -50 }} // https://github.com/computerjazz/react-native-draggable-flatlist/issues/336
|
||||
keyExtractor={({ Id }, index) => {
|
||||
return `${index}-${Id}`
|
||||
}}
|
||||
ItemSeparatorComponent={() => <Separator />}
|
||||
ListHeaderComponent={
|
||||
<YStack alignItems='center' marginTop={'$4'}>
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(playlist.Id!)}
|
||||
style={{
|
||||
borderRadius: getToken('$5'),
|
||||
width: getToken('$20') + getToken('$15'),
|
||||
height: getToken('$20') + getToken('$15'),
|
||||
}}
|
||||
/>
|
||||
|
||||
// Refresh playlist component data
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.ItemTracks, playlist.Id]
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
trigger('notificationError');
|
||||
<H4>{playlist.Name ?? 'Untitled Playlist'}</H4>
|
||||
<H5>{playlist.ProductionYear?.toString() ?? ''}</H5>
|
||||
</YStack>
|
||||
}
|
||||
numColumns={1}
|
||||
onDragBegin={() => {
|
||||
trigger('impactMedium')
|
||||
}}
|
||||
onDragEnd={({ data, from, to }) => {
|
||||
console.debug(`Moving playlist item from ${from} to ${to}`)
|
||||
|
||||
setPlaylistTracks(tracks ?? []);
|
||||
}
|
||||
});
|
||||
|
||||
const useRemoveFromPlaylist = useMutation({
|
||||
mutationFn: ({ playlist, track, index } : RemoveFromPlaylistMutation) => {
|
||||
return removeFromPlaylist(track, playlist);
|
||||
},
|
||||
onSuccess: (data, { index }) => {
|
||||
trigger("notificationSuccess");
|
||||
|
||||
setPlaylistTracks(playlistTracks.slice(0, index).concat(playlistTracks.slice(index + 1, playlistTracks.length -1)))
|
||||
},
|
||||
onError: () => {
|
||||
trigger("notificationError")
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<DraggableFlatList
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPending}
|
||||
onRefresh={refetch}
|
||||
/>
|
||||
}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
data={playlistTracks}
|
||||
dragHitSlop={{ left: -50 }} // https://github.com/computerjazz/react-native-draggable-flatlist/issues/336
|
||||
keyExtractor={({ Id }, index) => {
|
||||
return `${index}-${Id}`
|
||||
}}
|
||||
ItemSeparatorComponent={() => <Separator />}
|
||||
ListHeaderComponent={(
|
||||
<YStack
|
||||
alignItems="center"
|
||||
marginTop={"$4"}
|
||||
>
|
||||
<Image
|
||||
source={getImageApi(Client.api!).getItemImageUrlById(playlist.Id!)}
|
||||
style={{
|
||||
borderRadius: getToken("$5"),
|
||||
width: getToken("$20") + getToken("$15"),
|
||||
height: getToken("$20") + getToken("$15")
|
||||
}}
|
||||
/>
|
||||
|
||||
<H4>{ playlist.Name ?? "Untitled Playlist" }</H4>
|
||||
<H5>{ playlist.ProductionYear?.toString() ?? "" }</H5>
|
||||
</YStack>
|
||||
)}
|
||||
numColumns={1}
|
||||
onDragBegin={() => {
|
||||
trigger("impactMedium");
|
||||
}}
|
||||
onDragEnd={({ data, from, to }) => {
|
||||
|
||||
console.debug(`Moving playlist item from ${from} to ${to}`);
|
||||
|
||||
setPlaylistTracks(data);
|
||||
useUpdatePlaylist.mutate({
|
||||
playlist,
|
||||
tracks: data
|
||||
});
|
||||
}}
|
||||
refreshing={isPending}
|
||||
renderItem={({ item: track, getIndex, drag }) =>
|
||||
<Track
|
||||
navigation={navigation}
|
||||
track={track}
|
||||
tracklist={tracks!}
|
||||
index={getIndex()}
|
||||
queue={playlist}
|
||||
showArtwork
|
||||
onLongPress={editing ? drag : undefined}
|
||||
showRemove={editing}
|
||||
onRemove={() => useRemoveFromPlaylist.mutate({ playlist, track, index: getIndex()! })}
|
||||
/>
|
||||
}
|
||||
ListFooterComponent={(
|
||||
<XStack justifyContent="flex-end">
|
||||
<Text
|
||||
color={"$borderColor"}
|
||||
style={{ display: "block"}}
|
||||
>
|
||||
Total Runtime:
|
||||
</Text>
|
||||
<RunTimeTicks>{ playlist.RunTimeTicks }</RunTimeTicks>
|
||||
</XStack>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
setPlaylistTracks(data)
|
||||
useUpdatePlaylist.mutate({
|
||||
playlist,
|
||||
tracks: data,
|
||||
})
|
||||
}}
|
||||
refreshing={isPending}
|
||||
renderItem={({ item: track, getIndex, drag }) => (
|
||||
<Track
|
||||
navigation={navigation}
|
||||
track={track}
|
||||
tracklist={tracks!}
|
||||
index={getIndex()}
|
||||
queue={playlist}
|
||||
showArtwork
|
||||
onLongPress={editing ? drag : undefined}
|
||||
showRemove={editing}
|
||||
onRemove={() =>
|
||||
useRemoveFromPlaylist.mutate({ playlist, track, index: getIndex()! })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
ListFooterComponent={
|
||||
<XStack justifyContent='flex-end'>
|
||||
<Text color={'$borderColor'} style={{ display: 'block' }}>
|
||||
Total Runtime:
|
||||
</Text>
|
||||
<RunTimeTicks>{playlist.RunTimeTicks}</RunTimeTicks>
|
||||
</XStack>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import Playlist from "../../../components/Playlist/component";
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import { RouteProp } from "@react-navigation/native";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import React from "react";
|
||||
import Playlist from '../../../components/Playlist/component'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { RouteProp } from '@react-navigation/native'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import React from 'react'
|
||||
|
||||
export function PlaylistScreen({ route, navigation }: {
|
||||
route: RouteProp<StackParamList, "Playlist">,
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
return (
|
||||
<Playlist
|
||||
playlist={route.params.playlist}
|
||||
navigation={navigation}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export function PlaylistScreen({
|
||||
route,
|
||||
navigation,
|
||||
}: {
|
||||
route: RouteProp<StackParamList, 'Playlist'>
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
return <Playlist playlist={route.params.playlist} navigation={navigation} />
|
||||
}
|
||||
|
||||
@@ -1,54 +1,56 @@
|
||||
import { FlatList, RefreshControl } from "react-native-gesture-handler";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import { ItemCard } from "../Global/components/item-card";
|
||||
import { FavoritePlaylistsProps } from "../types";
|
||||
import Icon from "../Global/helpers/icon";
|
||||
import { getToken } from "tamagui";
|
||||
import { fetchFavoritePlaylists } from "../../api/queries/functions/favorites";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { FlatList, RefreshControl } from 'react-native-gesture-handler'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { ItemCard } from '../Global/components/item-card'
|
||||
import { FavoritePlaylistsProps } from '../types'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import { getToken } from 'tamagui'
|
||||
import { fetchFavoritePlaylists } from '../../api/queries/functions/favorites'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
export default function FavoritePlaylists({ navigation }: FavoritePlaylistsProps) : React.JSX.Element {
|
||||
export default function FavoritePlaylists({
|
||||
navigation,
|
||||
}: FavoritePlaylistsProps): React.JSX.Element {
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
return (
|
||||
<Icon
|
||||
name='plus-circle-outline'
|
||||
color={getToken('$color.telemagenta')}
|
||||
onPress={() => navigation.navigate('AddPlaylist')}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
return <Icon
|
||||
name="plus-circle-outline"
|
||||
color={getToken("$color.telemagenta")}
|
||||
onPress={() => navigation.navigate('AddPlaylist')}
|
||||
/>
|
||||
}
|
||||
});
|
||||
const {
|
||||
data: playlists,
|
||||
isPending,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.UserPlaylists],
|
||||
queryFn: () => fetchFavoritePlaylists(),
|
||||
})
|
||||
|
||||
const { data: playlists, isPending, refetch } = useQuery({
|
||||
queryKey: [QueryKeys.UserPlaylists],
|
||||
queryFn: () => fetchFavoritePlaylists()
|
||||
});
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
numColumns={2}
|
||||
data={playlists}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPending}
|
||||
onRefresh={refetch}
|
||||
/>
|
||||
}
|
||||
renderItem={({ index, item: playlist }) =>
|
||||
<ItemCard
|
||||
item={playlist}
|
||||
caption={playlist.Name ?? "Untitled Playlist"}
|
||||
onPress={() => {
|
||||
navigation.navigate("Playlist", { playlist })
|
||||
}}
|
||||
width={width / 2.1}
|
||||
squared
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
numColumns={2}
|
||||
data={playlists}
|
||||
refreshControl={<RefreshControl refreshing={isPending} onRefresh={refetch} />}
|
||||
renderItem={({ index, item: playlist }) => (
|
||||
<ItemCard
|
||||
item={playlist}
|
||||
caption={playlist.Name ?? 'Untitled Playlist'}
|
||||
onPress={() => {
|
||||
navigation.navigate('Playlist', { playlist })
|
||||
}}
|
||||
width={width / 2.1}
|
||||
squared
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { NativeStackScreenProps } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../types";
|
||||
import FavoritePlaylists from "./component";
|
||||
import React from "react";
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../types'
|
||||
import FavoritePlaylists from './component'
|
||||
import React from 'react'
|
||||
|
||||
export default function PlaylistsScreen(
|
||||
props: NativeStackScreenProps<StackParamList, 'Playlists'>
|
||||
) : React.JSX.Element {
|
||||
return (
|
||||
<FavoritePlaylists {...props} />
|
||||
)
|
||||
}
|
||||
props: NativeStackScreenProps<StackParamList, 'Playlists'>,
|
||||
): React.JSX.Element {
|
||||
return <FavoritePlaylists {...props} />
|
||||
}
|
||||
|
||||
@@ -1,114 +1,116 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import Input from "../Global/helpers/input";
|
||||
import Item from "../Global/components/item";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../types";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { fetchSearchResults } from "../../api/queries/functions/search";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { FlatList } from "react-native";
|
||||
import { H3 } from "../Global/helpers/text";
|
||||
import { fetchSearchSuggestions } from "../../api/queries/functions/suggestions";
|
||||
import { Spinner, YStack } from "tamagui";
|
||||
import Suggestions from "./suggestions";
|
||||
import { isEmpty } from "lodash";
|
||||
import HorizontalCardList from "../Global/components/horizontal-list";
|
||||
import { ItemCard } from "../Global/components/item-card";
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import Input from '../Global/helpers/input'
|
||||
import Item from '../Global/components/item'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../types'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { fetchSearchResults } from '../../api/queries/functions/search'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { FlatList } from 'react-native'
|
||||
import { H3 } from '../Global/helpers/text'
|
||||
import { fetchSearchSuggestions } from '../../api/queries/functions/suggestions'
|
||||
import { Spinner, YStack } from 'tamagui'
|
||||
import Suggestions from './suggestions'
|
||||
import { isEmpty } from 'lodash'
|
||||
import HorizontalCardList from '../Global/components/horizontal-list'
|
||||
import { ItemCard } from '../Global/components/item-card'
|
||||
|
||||
export default function Search({
|
||||
navigation
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
export default function Search({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const [searchString, setSearchString] = useState<string | undefined>(undefined)
|
||||
|
||||
const [searchString, setSearchString] = useState<string | undefined>(undefined);
|
||||
const {
|
||||
data: items,
|
||||
refetch,
|
||||
isFetching: fetchingResults,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.Search, searchString],
|
||||
queryFn: () => fetchSearchResults(searchString),
|
||||
})
|
||||
|
||||
const { data: items, refetch, isFetching: fetchingResults } = useQuery({
|
||||
queryKey: [QueryKeys.Search, searchString],
|
||||
queryFn: () => fetchSearchResults(searchString)
|
||||
});
|
||||
const {
|
||||
data: suggestions,
|
||||
isFetching: fetchingSuggestions,
|
||||
refetch: refetchSuggestions,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.SearchSuggestions],
|
||||
queryFn: () => fetchSearchSuggestions(),
|
||||
})
|
||||
|
||||
const { data: suggestions, isFetching: fetchingSuggestions, refetch: refetchSuggestions } = useQuery({
|
||||
queryKey: [QueryKeys.SearchSuggestions],
|
||||
queryFn: () => fetchSearchSuggestions()
|
||||
});
|
||||
const search = useCallback(() => {
|
||||
let timeout: NodeJS.Timeout
|
||||
|
||||
const search = useCallback(() => {
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
refetch()
|
||||
refetchSuggestions()
|
||||
}, 1000)
|
||||
}
|
||||
}, [])
|
||||
|
||||
let timeout : NodeJS.Timeout;
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
refetch();
|
||||
refetchSuggestions();
|
||||
}, 1000)
|
||||
}
|
||||
}, []);
|
||||
const handleSearchStringUpdate = (value: string | undefined) => {
|
||||
setSearchString(value)
|
||||
search()
|
||||
}
|
||||
|
||||
const handleSearchStringUpdate = (value: string | undefined) => {
|
||||
setSearchString(value)
|
||||
search();
|
||||
}
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
progressViewOffset={10}
|
||||
ListHeaderComponent={
|
||||
<YStack>
|
||||
<Input
|
||||
placeholder='Seek and ye shall find'
|
||||
onChangeText={(value) => handleSearchStringUpdate(value)}
|
||||
value={searchString}
|
||||
/>
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
progressViewOffset={10}
|
||||
ListHeaderComponent={(
|
||||
<YStack>
|
||||
<Input
|
||||
placeholder="Seek and ye shall find"
|
||||
onChangeText={(value) => handleSearchStringUpdate(value)}
|
||||
value={searchString}
|
||||
/>
|
||||
{!isEmpty(items) && (
|
||||
<YStack>
|
||||
<H3>Results</H3>
|
||||
|
||||
{ !isEmpty(items) && (
|
||||
<YStack>
|
||||
<H3>Results</H3>
|
||||
|
||||
<HorizontalCardList
|
||||
data={items?.filter(result => result.Type === 'MusicArtist')}
|
||||
renderItem={({ item: artistResult }) => {
|
||||
return (
|
||||
<ItemCard
|
||||
item={artistResult}
|
||||
onPress={() => {
|
||||
navigation.push('Artist', {
|
||||
artist: artistResult
|
||||
})
|
||||
}}
|
||||
size={"$8"}
|
||||
caption={artistResult.Name ?? "Untitled Artist"}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
)}
|
||||
</YStack>
|
||||
)}
|
||||
ListEmptyComponent={() => {
|
||||
return (
|
||||
<YStack
|
||||
alignContent="center"
|
||||
justifyContent="flex-end"
|
||||
marginTop={"$4"}
|
||||
>
|
||||
{ fetchingResults ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Suggestions suggestions={suggestions} navigation={navigation} />
|
||||
)}
|
||||
</YStack>
|
||||
)
|
||||
}}
|
||||
// We're displaying artists separately so we're going to filter them out here
|
||||
data={items?.filter((result) => result.Type !== 'MusicArtist')}
|
||||
refreshing={fetchingResults}
|
||||
renderItem={({ item }) =>
|
||||
<Item item={item} queueName={searchString ?? "Search"} navigation={navigation} />
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<HorizontalCardList
|
||||
data={items?.filter((result) => result.Type === 'MusicArtist')}
|
||||
renderItem={({ item: artistResult }) => {
|
||||
return (
|
||||
<ItemCard
|
||||
item={artistResult}
|
||||
onPress={() => {
|
||||
navigation.push('Artist', {
|
||||
artist: artistResult,
|
||||
})
|
||||
}}
|
||||
size={'$8'}
|
||||
caption={artistResult.Name ?? 'Untitled Artist'}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
)}
|
||||
</YStack>
|
||||
}
|
||||
ListEmptyComponent={() => {
|
||||
return (
|
||||
<YStack alignContent='center' justifyContent='flex-end' marginTop={'$4'}>
|
||||
{fetchingResults ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Suggestions suggestions={suggestions} navigation={navigation} />
|
||||
)}
|
||||
</YStack>
|
||||
)
|
||||
}}
|
||||
// We're displaying artists separately so we're going to filter them out here
|
||||
data={items?.filter((result) => result.Type !== 'MusicArtist')}
|
||||
refreshing={fetchingResults}
|
||||
renderItem={({ item }) => (
|
||||
<Item item={item} queueName={searchString ?? 'Search'} navigation={navigation} />
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { RouteProp } from "@react-navigation/native";
|
||||
import { StackParamList } from "../types";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import React from "react";
|
||||
import Search from "./component";
|
||||
import { RouteProp } from '@react-navigation/native'
|
||||
import { StackParamList } from '../types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import React from 'react'
|
||||
import Search from './component'
|
||||
|
||||
export default function SearchScreen({
|
||||
route,
|
||||
navigation
|
||||
} : {
|
||||
route: RouteProp<StackParamList>,
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
return (
|
||||
<Search navigation={navigation} />
|
||||
)
|
||||
}
|
||||
route,
|
||||
navigation,
|
||||
}: {
|
||||
route: RouteProp<StackParamList>
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
return <Search navigation={navigation} />
|
||||
}
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack"
|
||||
import SearchScreen from "./screen";
|
||||
import { StackParamList } from "../types";
|
||||
import { ArtistScreen } from "../Artist";
|
||||
import { AlbumScreen } from "../Album";
|
||||
import { PlaylistScreen } from "../Playlist/screens";
|
||||
import DetailsScreen from "../ItemDetail/screen";
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||
import SearchScreen from './screen'
|
||||
import { StackParamList } from '../types'
|
||||
import { ArtistScreen } from '../Artist'
|
||||
import { AlbumScreen } from '../Album'
|
||||
import { PlaylistScreen } from '../Playlist/screens'
|
||||
import DetailsScreen from '../ItemDetail/screen'
|
||||
|
||||
const Stack = createNativeStackNavigator<StackParamList>();
|
||||
const Stack = createNativeStackNavigator<StackParamList>()
|
||||
|
||||
export default function SearchStack() : React.JSX.Element {
|
||||
return (
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen
|
||||
name="Search"
|
||||
component={SearchScreen}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
export default function SearchStack(): React.JSX.Element {
|
||||
return (
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen
|
||||
name='Search'
|
||||
component={SearchScreen}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="Artist"
|
||||
component={ArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artist.Name ?? "Unknown Artist",
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Artist'
|
||||
component={ArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artist.Name ?? 'Unknown Artist',
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="Album"
|
||||
component={AlbumScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Album'
|
||||
component={AlbumScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: '',
|
||||
})}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="Playlist"
|
||||
component={PlaylistScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Playlist'
|
||||
component={PlaylistScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: '',
|
||||
})}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="Details"
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "modal"
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
)
|
||||
}
|
||||
<Stack.Screen
|
||||
name='Details'
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: 'modal',
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,55 +1,56 @@
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { FlatList, RefreshControl } from "react-native";
|
||||
import { StackParamList } from "../types";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import Item from "../Global/components/item";
|
||||
import { H3, Text } from "../Global/helpers/text";
|
||||
import { YStack } from "tamagui";
|
||||
import { ItemCard } from "../Global/components/item-card";
|
||||
import HorizontalCardList from "../Global/components/horizontal-list";
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { FlatList, RefreshControl } from 'react-native'
|
||||
import { StackParamList } from '../types'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import Item from '../Global/components/item'
|
||||
import { H3, Text } from '../Global/helpers/text'
|
||||
import { YStack } from 'tamagui'
|
||||
import { ItemCard } from '../Global/components/item-card'
|
||||
import HorizontalCardList from '../Global/components/horizontal-list'
|
||||
|
||||
interface SuggestionsProps {
|
||||
suggestions: BaseItemDto[] | undefined;
|
||||
navigation: NativeStackNavigationProp<StackParamList>;
|
||||
suggestions: BaseItemDto[] | undefined
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}
|
||||
|
||||
export default function Suggestions(
|
||||
props: SuggestionsProps
|
||||
) : React.JSX.Element {
|
||||
export default function Suggestions(props: SuggestionsProps): React.JSX.Element {
|
||||
return (
|
||||
<FlatList
|
||||
// Artists are displayed in the header, so we'll filter them out here
|
||||
data={props.suggestions?.filter((suggestion) => suggestion.Type !== 'MusicArtist')}
|
||||
ListHeaderComponent={
|
||||
<YStack>
|
||||
<H3>Suggestions</H3>
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
// Artists are displayed in the header, so we'll filter them out here
|
||||
data={props.suggestions?.filter(suggestion => suggestion.Type !== 'MusicArtist')}
|
||||
ListHeaderComponent={(
|
||||
<YStack>
|
||||
<H3>Suggestions</H3>
|
||||
|
||||
<HorizontalCardList
|
||||
data={props.suggestions?.filter(suggestion => suggestion.Type === 'MusicArtist')}
|
||||
renderItem={({ item: suggestedArtist }) => {
|
||||
return (
|
||||
<ItemCard
|
||||
item={suggestedArtist}
|
||||
onPress={() => {
|
||||
props.navigation.push('Artist', {
|
||||
artist: suggestedArtist
|
||||
})
|
||||
}}
|
||||
size={"$8"}
|
||||
caption={suggestedArtist.Name ?? "Untitled Artist"}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
)}
|
||||
ListEmptyComponent={(
|
||||
<Text textAlign="center">Wake now, discover that you are the eyes of the world...</Text>
|
||||
)}
|
||||
renderItem={({ item }) => {
|
||||
return <Item item={item} queueName={"Suggestions"} navigation={props.navigation} />
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<HorizontalCardList
|
||||
data={props.suggestions?.filter(
|
||||
(suggestion) => suggestion.Type === 'MusicArtist',
|
||||
)}
|
||||
renderItem={({ item: suggestedArtist }) => {
|
||||
return (
|
||||
<ItemCard
|
||||
item={suggestedArtist}
|
||||
onPress={() => {
|
||||
props.navigation.push('Artist', {
|
||||
artist: suggestedArtist,
|
||||
})
|
||||
}}
|
||||
size={'$8'}
|
||||
caption={suggestedArtist.Name ?? 'Untitled Artist'}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
<Text textAlign='center'>
|
||||
Wake now, discover that you are the eyes of the world...
|
||||
</Text>
|
||||
}
|
||||
renderItem={({ item }) => {
|
||||
return <Item item={item} queueName={'Suggestions'} navigation={props.navigation} />
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user