mirror of
https://github.com/Jellify-Music/App.git
synced 2026-01-26 21:18:45 -06:00
Merge pull request #17 from anultravioletaurora/13-support-favorites-and-build-library-screen
13 support favorites and build library screen
This commit is contained in:
12
App.tsx
12
App.tsx
@@ -1,17 +1,16 @@
|
||||
import './gesture-handler';
|
||||
import "./global.css";
|
||||
import React from 'react';
|
||||
import "react-native-url-polyfill/auto";
|
||||
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
|
||||
import Jellify from './components/jellify';
|
||||
import { TamaguiProvider, Theme } from 'tamagui';
|
||||
import { ToastProvider } from '@tamagui/toast'
|
||||
import { useColorScheme } from 'react-native';
|
||||
import jellifyConfig from './tamagui.config';
|
||||
import { clientPersister } from './constants/storage';
|
||||
import { queryClient } from './constants/query-client';
|
||||
import { CacheManager } from '@georstat/react-native-image-cache';
|
||||
import { Dirs } from "react-native-file-access";
|
||||
import { EventProvider } from "react-native-outside-press";
|
||||
|
||||
CacheManager.config = {
|
||||
baseDir: `${Dirs.CacheDir}/images_cache/`,
|
||||
@@ -35,12 +34,9 @@ export default function App(): React.JSX.Element {
|
||||
}}>
|
||||
<TamaguiProvider config={jellifyConfig}>
|
||||
<Theme name={isDarkMode ? 'dark' : 'light'}>
|
||||
<ToastProvider
|
||||
swipeDirection='down'
|
||||
native={false}
|
||||
>
|
||||
<Jellify />
|
||||
</ToastProvider>
|
||||
<EventProvider>
|
||||
<Jellify />
|
||||
</EventProvider>
|
||||
</Theme>
|
||||
</TamaguiProvider>
|
||||
</PersistQueryClientProvider>
|
||||
|
||||
@@ -36,4 +36,7 @@ Designed to be lightweight, fast, and support ***extremely*** large music librar
|
||||
- Captures anonymous logging if and only if the user opts into it. This can be toggled at anytime
|
||||
|
||||
### Love
|
||||
This is undoubtedly a passion project of [mine](https://github.com/anultravioletaurora), and I've learned a lot from working on it (and the many failed attempts before it). I hope you enjoy using it! Feature requests and bug reports are welcome :)
|
||||
This is undoubtedly a passion project of [mine](https://github.com/anultravioletaurora), and I've learned a lot from working on it (and the many failed attempts before it). I hope you enjoy using it! Feature requests and bug reports are welcome :)
|
||||
|
||||
## Running Locally
|
||||
Clone the repository and run ```npm i``` to install the dependencies
|
||||
189
api/client.ts
189
api/client.ts
@@ -1,36 +1,163 @@
|
||||
import { Api, Jellyfin } from "@jellyfin/sdk";
|
||||
import { getDeviceNameSync, getUniqueIdSync } from "react-native-device-info";
|
||||
import { name, version } from "../package.json"
|
||||
import { capitalize } from "lodash";
|
||||
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";
|
||||
|
||||
/**
|
||||
* Client object that represents Jellify on the Jellyfin server.
|
||||
*/
|
||||
export const jellifyClient: Jellyfin = new Jellyfin({
|
||||
clientInfo: {
|
||||
name: capitalize(name),
|
||||
version: version
|
||||
},
|
||||
deviceInfo: {
|
||||
name: getDeviceNameSync(),
|
||||
id: getUniqueIdSync()
|
||||
export default class Client {
|
||||
static #instance: Client;
|
||||
|
||||
private api : Api | undefined;
|
||||
private user : JellifyUser | undefined;
|
||||
private server : JellifyServer | undefined;
|
||||
private library : JellifyLibrary | undefined;
|
||||
private sessionId : string = uuid.v4();
|
||||
|
||||
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);
|
||||
|
||||
if (user)
|
||||
this.setAndPersistUser(user)
|
||||
else if (userJson)
|
||||
this.user = JSON.parse(userJson)
|
||||
|
||||
if (server)
|
||||
this.setAndPersistServer(server)
|
||||
else if (serverJson)
|
||||
this.server = JSON.parse(serverJson);
|
||||
|
||||
if (library)
|
||||
this.setAndPersistLibrary(library)
|
||||
else if (libraryJson)
|
||||
this.library = JSON.parse(libraryJson)
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Uses the jellifyClient to create a public Jellyfin API instance.
|
||||
* @param serverUrl The URL of the Jellyfin server
|
||||
* @returns
|
||||
*/
|
||||
export function buildPublicApiClient(serverUrl : string) : Api {
|
||||
return jellifyClient.createApi(serverUrl);
|
||||
}
|
||||
public static get instance(): Client {
|
||||
if (!Client.#instance) {
|
||||
Client.#instance = new Client();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param serverUrl The URL of the Jellyfin server
|
||||
* @param accessToken The assigned accessToken for the Jellyfin user
|
||||
*/
|
||||
export function buildAuthenticatedApiClient(serverUrl: string, accessToken: string) : Api {
|
||||
return jellifyClient.createApi(serverUrl, accessToken);
|
||||
return Client.#instance;
|
||||
}
|
||||
|
||||
public static get api(): Api | undefined {
|
||||
return Client.#instance.api;
|
||||
}
|
||||
|
||||
public static get server(): JellifyServer | undefined {
|
||||
return Client.#instance.server;
|
||||
}
|
||||
|
||||
public static get user(): JellifyUser | undefined {
|
||||
return Client.#instance.user;
|
||||
}
|
||||
|
||||
public static get library(): JellifyLibrary | undefined {
|
||||
return Client.#instance.library;
|
||||
}
|
||||
|
||||
public static get sessionId(): string {
|
||||
return Client.#instance.sessionId;
|
||||
}
|
||||
|
||||
public static signOut(): void {
|
||||
Client.#instance.removeCredentials()
|
||||
}
|
||||
|
||||
public static switchServer() : void {
|
||||
Client.#instance.removeServer();
|
||||
}
|
||||
|
||||
public static switchUser(): void {
|
||||
Client.#instance.removeUser();
|
||||
}
|
||||
|
||||
public static setUser(user: JellifyUser): void {
|
||||
Client.#instance.setAndPersistUser(user);
|
||||
}
|
||||
|
||||
private setAndPersistUser(user: JellifyUser) {
|
||||
this.user = user;
|
||||
|
||||
// persist user details
|
||||
storage.set(MMKVStorageKeys.User, JSON.stringify(user));
|
||||
}
|
||||
|
||||
private setAndPersistServer(server : JellifyServer) {
|
||||
this.server = server;
|
||||
|
||||
storage.set(MMKVStorageKeys.Server, JSON.stringify(server));
|
||||
}
|
||||
|
||||
private setAndPersistLibrary(library : JellifyLibrary) {
|
||||
this.library = library;
|
||||
|
||||
storage.set(MMKVStorageKeys.Library, JSON.stringify(library))
|
||||
}
|
||||
|
||||
private removeCredentials() {
|
||||
this.library = undefined;
|
||||
this.library = undefined;
|
||||
this.server = undefined;
|
||||
this.user = undefined;
|
||||
|
||||
storage.delete(MMKVStorageKeys.Server)
|
||||
storage.delete(MMKVStorageKeys.Library)
|
||||
storage.delete(MMKVStorageKeys.User)
|
||||
}
|
||||
|
||||
private removeServer() {
|
||||
this.server = undefined;
|
||||
|
||||
storage.delete(MMKVStorageKeys.Server)
|
||||
}
|
||||
|
||||
private removeUser() {
|
||||
this.user = undefined;
|
||||
|
||||
storage.delete(MMKVStorageKeys.User)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
Client.#instance = new Client(api, undefined, 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);
|
||||
|
||||
Client.#instance = new Client(api, user, server, undefined);
|
||||
}
|
||||
|
||||
public static setLibrary(library : JellifyLibrary) : void {
|
||||
|
||||
Client.#instance = new Client(undefined, undefined, undefined, library);
|
||||
}
|
||||
}
|
||||
18
api/info.ts
Normal file
18
api/info.ts
Normal file
@@ -0,0 +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";
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
});
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { QueryKeys } from "../../enums/query-keys"
|
||||
import { Api } from "@jellyfin/sdk"
|
||||
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 useArtistAlbums = (artistId: string, api: Api) => useQuery({
|
||||
queryKey: [QueryKeys.ArtistAlbums, artistId, api],
|
||||
export const useArtistAlbums = (artistId: string) => useQuery({
|
||||
queryKey: [QueryKeys.ArtistAlbums, artistId],
|
||||
queryFn: ({ queryKey }) => {
|
||||
return getItemsApi(queryKey[2] as Api).getItems({
|
||||
return getItemsApi(Client.api!).getItems({
|
||||
includeItemTypes: [BaseItemKind.MusicAlbum],
|
||||
recursive: true,
|
||||
excludeItemIds: [queryKey[1] as string],
|
||||
@@ -26,10 +26,10 @@ export const useArtistAlbums = (artistId: string, api: Api) => useQuery({
|
||||
})
|
||||
|
||||
|
||||
export const useArtistFeaturedOnAlbums = (artistId: string, api: Api) => useQuery({
|
||||
queryKey: [QueryKeys.ArtistFeaturedAlbums, artistId, api],
|
||||
export const useArtistFeaturedOnAlbums = (artistId: string) => useQuery({
|
||||
queryKey: [QueryKeys.ArtistFeaturedAlbums, artistId],
|
||||
queryFn: ({ queryKey }) => {
|
||||
return getItemsApi(queryKey[2] as Api).getItems({
|
||||
return getItemsApi(Client.api!).getItems({
|
||||
includeItemTypes: [BaseItemKind.MusicAlbum],
|
||||
recursive: true,
|
||||
excludeItemIds: [queryKey[1] as string],
|
||||
|
||||
29
api/queries/favorites.ts
Normal file
29
api/queries/favorites.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchFavoriteAlbums, fetchFavoriteArtists, fetchFavoriteTracks, fetchUserData } from "./functions/favorites";
|
||||
|
||||
export const useFavoriteArtists = () => useQuery({
|
||||
queryKey: [QueryKeys.FavoriteArtists],
|
||||
queryFn: () => {
|
||||
|
||||
return fetchFavoriteArtists()
|
||||
}
|
||||
});
|
||||
|
||||
export const useFavoriteAlbums = () => useQuery({
|
||||
queryKey: [QueryKeys.FavoriteAlbums],
|
||||
queryFn: () => {
|
||||
|
||||
return fetchFavoriteAlbums()
|
||||
}
|
||||
});
|
||||
|
||||
export const useFavoriteTracks = () => useQuery({
|
||||
queryKey: [QueryKeys.FavoriteTracks],
|
||||
queryFn: () => fetchFavoriteTracks()
|
||||
});
|
||||
|
||||
export const useUserData = (itemId: string) => useQuery({
|
||||
queryKey: [QueryKeys.UserData, itemId],
|
||||
queryFn: () => fetchUserData(itemId)
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { jellifyClient } from "../../client";
|
||||
import { JellyfinInfo } from "../../info";
|
||||
import _ from "lodash";
|
||||
|
||||
export function createApi(serverUrl?: string, username?: string, password?: string, accessToken?: string): Promise<Api> {
|
||||
@@ -12,22 +12,22 @@ export function createApi(serverUrl?: string, username?: string, password?: stri
|
||||
|
||||
if (!_.isUndefined(accessToken)) {
|
||||
console.info("Creating API with accessToken")
|
||||
return resolve(jellifyClient.createApi(serverUrl, accessToken));
|
||||
return resolve(JellyfinInfo.createApi(serverUrl, accessToken));
|
||||
}
|
||||
|
||||
|
||||
if (_.isUndefined(username) && _.isUndefined(password)) {
|
||||
|
||||
console.info("Creating public API for server url")
|
||||
return resolve(jellifyClient.createApi(serverUrl));
|
||||
return resolve(JellyfinInfo.createApi(serverUrl));
|
||||
}
|
||||
|
||||
console.log("Signing into Jellyfin")
|
||||
let authResult = await jellifyClient.createApi(serverUrl).authenticateUserByName(username!, password);
|
||||
let authResult = await JellyfinInfo.createApi(serverUrl).authenticateUserByName(username!, password);
|
||||
|
||||
if (authResult.data.AccessToken) {
|
||||
console.info("Signed into Jellyfin successfully")
|
||||
return resolve(jellifyClient.createApi(serverUrl, authResult.data.AccessToken));
|
||||
return resolve(JellyfinInfo.createApi(serverUrl, authResult.data.AccessToken));
|
||||
}
|
||||
|
||||
return reject("Unable to sign in");
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
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`);
|
||||
|
||||
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);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export 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.SortName
|
||||
],
|
||||
sortOrder: [
|
||||
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);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchFavoriteTracks(): Promise<BaseItemDto[]> {
|
||||
console.debug(`Fetching user's favorite tracks`);
|
||||
|
||||
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);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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);
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -1,71 +1,23 @@
|
||||
import { Api } from "@jellyfin/sdk/lib/api"
|
||||
import { ImageFormat, ImageType } from "@jellyfin/sdk/lib/generated-client/models"
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api"
|
||||
import _ from "lodash"
|
||||
import { queryConfig } from "../query.config"
|
||||
import Client from "../../../api/client"
|
||||
import { queryConfig } from "../query.config";
|
||||
|
||||
|
||||
|
||||
|
||||
export function fetchImage(api: Api, itemId: string, imageType?: ImageType) : Promise<string> {
|
||||
return new Promise(async (resolve) => {
|
||||
let imageResponse = await api.axiosInstance
|
||||
.get(getImageApi(api).getItemImageUrlById(
|
||||
itemId,
|
||||
imageType,
|
||||
{
|
||||
format: queryConfig.images.format,
|
||||
fillHeight: queryConfig.images.fillHeight,
|
||||
fillWidth: queryConfig.images.fillWidth
|
||||
}
|
||||
))
|
||||
|
||||
console.debug(convertFileToBase64(imageResponse.data));
|
||||
console.debug(typeof imageResponse.data)
|
||||
resolve(convertFileToBase64(imageResponse.data));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function fetchArtistImage(api: Api, artistId: string, imageType?: ImageType) : Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let response = await getImageApi(api).getArtistImage({
|
||||
name: "",
|
||||
imageIndex: 1,
|
||||
imageType: imageType ? imageType : ImageType.Primary
|
||||
})
|
||||
|
||||
console.log(response.data)
|
||||
|
||||
if (_.isEmpty(response.data))
|
||||
reject(new Error("No image for artist"))
|
||||
|
||||
resolve(convertFileToBase64(response.data));
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchItemImage(api: Api, itemId: string, imageType?: ImageType, width?: number) {
|
||||
export function fetchItemImage(itemId: string, imageType?: ImageType, size?: number) {
|
||||
|
||||
return getImageApi(api).getItemImage({
|
||||
itemId,
|
||||
imageType: imageType ? imageType : ImageType.Primary,
|
||||
format: ImageFormat.Jpg
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(convertFileToBase64(response.data))
|
||||
return convertFileToBase64(response.data);
|
||||
})
|
||||
}
|
||||
|
||||
function base64toJpeg(encode: string) : string {
|
||||
return `data:image/jpeg;base64,${encode}`;
|
||||
}
|
||||
|
||||
function convertFileToBase64(file: any): string {
|
||||
console.debug("Converting file to base64", file)
|
||||
let encode = base64toJpeg(Buffer.from(file, 'binary').toString('base64'));
|
||||
|
||||
console.debug(encode);
|
||||
|
||||
return encode;
|
||||
return getImageApi(Client.api!)
|
||||
.getItemImage({
|
||||
itemId,
|
||||
imageType: imageType ? imageType : ImageType.Primary,
|
||||
format: ImageFormat.Jpg,
|
||||
height: size ?? queryConfig.images.height,
|
||||
width: size ?? queryConfig.images.width
|
||||
}, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response)
|
||||
return URL.createObjectURL(response.data)
|
||||
});
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import Client from "../../client";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api/items-api";
|
||||
import { isUndefined } from "lodash";
|
||||
|
||||
|
||||
export function fetchMusicLibraries(api: Api): Promise<BaseItemDto[]> {
|
||||
export function fetchMusicLibraries(): Promise<BaseItemDto[]> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
console.debug("Fetching music libraries from Jellyfin");
|
||||
|
||||
let libraries = await getItemsApi(api).getItems({
|
||||
let libraries = await getItemsApi(Client.api!).getItems({
|
||||
includeItemTypes: ['CollectionFolder']
|
||||
});
|
||||
|
||||
@@ -24,11 +24,11 @@ export function fetchMusicLibraries(api: Api): Promise<BaseItemDto[]> {
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchPlaylistLibrary(api: Api): Promise<BaseItemDto> {
|
||||
export function fetchPlaylistLibrary(): Promise<BaseItemDto> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
console.debug("Fetching playlist library from Jellyfin");
|
||||
|
||||
let libraries = await getItemsApi(api).getItems({
|
||||
let libraries = await getItemsApi(Client.api!).getItems({
|
||||
includeItemTypes: ['ManualPlaylistsFolder'],
|
||||
excludeItemTypes: ['CollectionFolder']
|
||||
});
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import Client from "../../client";
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { BaseItemDto, ItemSortBy, SortOrder } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
|
||||
export function fetchUserPlaylists(api: Api, userId: string, playlistLibraryId: string): Promise<BaseItemDto[]> {
|
||||
export function fetchUserPlaylists(): Promise<BaseItemDto[]> {
|
||||
console.debug("Fetching user playlists");
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
getItemsApi(api)
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
userId: userId,
|
||||
parentId: playlistLibraryId,
|
||||
userId: Client.user!.id,
|
||||
parentId: Client.library!.playlistLibraryId!,
|
||||
fields: [
|
||||
"Path"
|
||||
],
|
||||
@@ -36,13 +37,13 @@ export function fetchUserPlaylists(api: Api, userId: string, playlistLibraryId:
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchPublicPlaylists(api: Api, playlistLibraryId: string): Promise<BaseItemDto[]> {
|
||||
export function fetchPublicPlaylists(): Promise<BaseItemDto[]> {
|
||||
console.debug("Fetching public playlists");
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
getItemsApi(api)
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
parentId: playlistLibraryId,
|
||||
parentId: Client.library!.playlistLibraryId!,
|
||||
sortBy: [
|
||||
ItemSortBy.IsFolder,
|
||||
ItemSortBy.SortName
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Api } from "@jellyfin/sdk/lib/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";
|
||||
|
||||
export function fetchRecentlyPlayed(api: Api, libraryId: string): Promise<BaseItemDto[]> {
|
||||
export function fetchRecentlyPlayed(): Promise<BaseItemDto[]> {
|
||||
|
||||
console.debug("Fetching recently played items", api);
|
||||
console.debug("Fetching recently played items");
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
getItemsApi(api)
|
||||
getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
includeItemTypes: [
|
||||
BaseItemKind.Audio
|
||||
],
|
||||
limit: queryConfig.limits.recents,
|
||||
parentId: libraryId,
|
||||
parentId: Client.library!.musicLibraryId,
|
||||
recursive: true,
|
||||
sortBy: [
|
||||
ItemSortBy.DatePlayed
|
||||
@@ -37,4 +37,17 @@ export function fetchRecentlyPlayed(api: Api, libraryId: string): Promise<BaseIt
|
||||
reject(error);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchRecentlyPlayedArtists() : Promise<BaseItemDto[]> {
|
||||
return fetchRecentlyPlayed()
|
||||
.then((tracks) => {
|
||||
return getItemsApi(Client.api!)
|
||||
.getItems({
|
||||
ids: tracks.map(track => track.ArtistItems![0].Id!)
|
||||
})
|
||||
.then((recentArtists) => {
|
||||
return recentArtists.data.Items!
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,20 +1,9 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { fetchArtistImage, fetchImage, fetchItemImage } from "./functions/images";
|
||||
import { fetchItemImage } from "./functions/images";
|
||||
import { ImageType } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
|
||||
export const useImage = (api: Api, itemId: string, imageType?: ImageType) => useQuery({
|
||||
queryKey: [QueryKeys.ItemImage, api, itemId, imageType],
|
||||
queryFn: ({ queryKey }) => fetchImage(queryKey[1] as Api, queryKey[2] as string, queryKey[3] as ImageType | undefined)
|
||||
});
|
||||
|
||||
export const useArtistImage = (api: Api, artistName: string, imageType?: ImageType) => useQuery({
|
||||
queryKey: [QueryKeys.ArtistImage, api, artistName, imageType],
|
||||
queryFn: ({ queryKey }) => fetchArtistImage(queryKey[1] as Api, queryKey[2] as string, queryKey[3] as ImageType | undefined)
|
||||
})
|
||||
|
||||
export const useItemImage = (api: Api, itemId: string, imageType?: ImageType, width?: number) => useQuery({
|
||||
queryKey: [QueryKeys.ItemImage, api, itemId, imageType, width],
|
||||
queryFn: ({ queryKey }) => fetchItemImage(queryKey[1] as Api, queryKey[2] as string, queryKey[3] as ImageType | undefined, queryKey[4] as number | undefined)
|
||||
export const useItemImage = (itemId: string, imageType?: ImageType, size?: number) => useQuery({
|
||||
queryKey: [QueryKeys.ItemImage, itemId, imageType, size],
|
||||
queryFn: () => fetchItemImage(itemId, imageType, size)
|
||||
});
|
||||
@@ -1,24 +1,13 @@
|
||||
import { QueryKeys } from "@/enums/query-keys";
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchMusicLibraries, fetchPlaylistLibrary } from "./functions/libraries";
|
||||
|
||||
export const useMusicLibraries = (api: Api) => useQuery({
|
||||
queryKey: [QueryKeys.Libraries, api],
|
||||
queryFn: async ({ queryKey }) => {
|
||||
|
||||
const api : Api = queryKey[1] as Api;
|
||||
|
||||
return await fetchMusicLibraries(api)
|
||||
}
|
||||
export const useMusicLibraries = () => useQuery({
|
||||
queryKey: [QueryKeys.Libraries],
|
||||
queryFn: () => fetchMusicLibraries()
|
||||
});
|
||||
|
||||
export const usePlaylistLibrary = (api: Api) => useQuery({
|
||||
queryKey: [QueryKeys.Playlist, api],
|
||||
queryFn: async ({ queryKey }) => {
|
||||
|
||||
const api : Api = queryKey[1] as Api;
|
||||
|
||||
return await fetchPlaylistLibrary(api)
|
||||
}
|
||||
export const usePlaylistLibrary = () => useQuery({
|
||||
queryKey: [QueryKeys.Playlist],
|
||||
queryFn: () => fetchPlaylistLibrary()
|
||||
});
|
||||
@@ -1,16 +1,9 @@
|
||||
import { QueryKeys } from "@/enums/query-keys";
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchUserPlaylists } from "./functions/playlists";
|
||||
|
||||
export const useUserPlaylists = (api: Api, userId: string, playlistLibraryId: string) => useQuery({
|
||||
queryKey: [QueryKeys.UserPlaylists, api, userId, playlistLibraryId],
|
||||
queryFn: ({ queryKey }) => {
|
||||
const api: Api = queryKey[1] as Api;
|
||||
const userId: string = queryKey[2] as string;
|
||||
const playlistLibraryId: string = queryKey[3] as string;
|
||||
|
||||
return fetchUserPlaylists(api, userId, playlistLibraryId);
|
||||
}
|
||||
})
|
||||
export const useUserPlaylists = () => useQuery({
|
||||
queryKey: [QueryKeys.UserPlaylists],
|
||||
queryFn: () => fetchUserPlaylists()
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ export const queryConfig = {
|
||||
recents: 50 // TODO: Adjust this when we add a list navigator to the end of the recents
|
||||
},
|
||||
images: {
|
||||
fillHeight: 300,
|
||||
fillWidth: 300,
|
||||
height: 300,
|
||||
width: 300,
|
||||
format: ImageFormat.Jpg
|
||||
},
|
||||
banners: {
|
||||
@@ -23,5 +23,6 @@ export const queryConfig = {
|
||||
fillHeight: 1000,
|
||||
fillWidth: 1000,
|
||||
format: ImageFormat.Jpg
|
||||
}
|
||||
},
|
||||
staleTime: 1000 * 60
|
||||
}
|
||||
@@ -1,32 +1,13 @@
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { fetchRecentlyPlayed } from "./functions/recents";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api"
|
||||
import { fetchRecentlyPlayed, fetchRecentlyPlayedArtists } from "./functions/recents";
|
||||
|
||||
export const useRecentlyPlayed = (api: Api, libraryId: string) => useQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayed, api, libraryId],
|
||||
queryFn: ({ queryKey }) => {
|
||||
|
||||
const api : Api = queryKey[1] as Api;
|
||||
const libraryId : string = queryKey[2] as string;
|
||||
|
||||
return fetchRecentlyPlayed(api, libraryId)
|
||||
}
|
||||
export const useRecentlyPlayed = () => useQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayed],
|
||||
queryFn: () => fetchRecentlyPlayed()
|
||||
});
|
||||
|
||||
export const useRecentlyPlayedArtists = (api: Api, libraryId: string) => useQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayedArtists, api, libraryId],
|
||||
queryFn: ({ queryKey }) => {
|
||||
return fetchRecentlyPlayed(queryKey[1] as Api, queryKey[2] as string)
|
||||
.then((tracks) => {
|
||||
return getItemsApi(api)
|
||||
.getItems({
|
||||
ids: tracks.map(track => track.ArtistItems![0].Id!)
|
||||
})
|
||||
.then((recentArtists) => {
|
||||
return recentArtists.data.Items!
|
||||
});
|
||||
});
|
||||
}
|
||||
export const useRecentlyPlayedArtists = () => useQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayedArtists],
|
||||
queryFn: () => fetchRecentlyPlayedArtists()
|
||||
});
|
||||
@@ -1,16 +1,15 @@
|
||||
import { QueryKeys } from "@/enums/query-keys";
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { QueryKeys } from "../../enums/query-keys";
|
||||
import { ItemSortBy } from "@jellyfin/sdk/lib/generated-client/models/item-sort-by";
|
||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api/items-api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { queryConfig } from "./query.config";
|
||||
import Client from "../client";
|
||||
|
||||
export const useItemTracks = (itemId: string, api: Api, sort: boolean = false) => useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, itemId, api, sort],
|
||||
queryFn: ({ queryKey }) => {
|
||||
export const useItemTracks = (itemId: string, sort: boolean = false) => useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, itemId, sort],
|
||||
queryFn: () => {
|
||||
|
||||
const itemId : string = queryKey[1] as string;
|
||||
const api : Api = queryKey[2] as Api;
|
||||
const sort : boolean = queryKey[3] as boolean;
|
||||
console.debug(`Fetching item tracks ${sort ? "sorted" : "unsorted"}`)
|
||||
|
||||
let sortBy: ItemSortBy[] = [];
|
||||
|
||||
@@ -22,12 +21,13 @@ export const useItemTracks = (itemId: string, api: Api, sort: boolean = false) =
|
||||
]
|
||||
}
|
||||
|
||||
return getItemsApi(api).getItems({
|
||||
return getItemsApi(Client.api!).getItems({
|
||||
parentId: itemId,
|
||||
sortBy
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.Items ? response.data.Items! : [];
|
||||
})
|
||||
}
|
||||
},
|
||||
staleTime: queryConfig.staleTime
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
presets: ['babel-preset-expo'],
|
||||
presets: ['module:@react-native/babel-preset'],
|
||||
plugins: [
|
||||
|
||||
// react-native-reanimated/plugin has to be listed last
|
||||
|
||||
@@ -3,7 +3,6 @@ import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { ScrollView, YStack, XStack } from "tamagui";
|
||||
import { CachedImage } from "@georstat/react-native-image-cache";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useApiClientContext } from "../jellyfin-api-provider";
|
||||
import { BaseItemDto, ImageType } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { queryConfig } from "../../api/queries/query.config";
|
||||
import { H4, H5, Text } from "../Global/helpers/text";
|
||||
@@ -11,8 +10,11 @@ import { FlatList } from "react-native";
|
||||
import { usePlayerContext } from "../../player/provider";
|
||||
import { RunTimeTicks } from "../Global/helpers/time-codes";
|
||||
import Track from "../Global/components/track";
|
||||
import { useItemTracks } from "@/api/queries/tracks";
|
||||
import { useItemTracks } from "../../api/queries/tracks";
|
||||
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import FavoriteButton from "../Global/components/favorite-button";
|
||||
import { useEffect } from "react";
|
||||
import Client from "../../api/client";
|
||||
|
||||
interface AlbumProps {
|
||||
album: BaseItemDto,
|
||||
@@ -21,19 +23,32 @@ interface AlbumProps {
|
||||
|
||||
export default function Album(props: AlbumProps): React.JSX.Element {
|
||||
|
||||
const { apiClient } = useApiClientContext();
|
||||
const { nowPlaying } = usePlayerContext();
|
||||
props.navigation.setOptions({
|
||||
headerRight: () => {
|
||||
return (
|
||||
<FavoriteButton item={props.album} />
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const { nowPlaying, nowPlayingIsFavorite } = usePlayerContext();
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
const { data: tracks, isLoading } = useItemTracks(props.album.Id!, apiClient!, true);
|
||||
const { data: tracks, isLoading, refetch } = useItemTracks(props.album.Id!, true);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [
|
||||
nowPlayingIsFavorite
|
||||
])
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["right", "left"]}>
|
||||
<ScrollView contentInsetAdjustmentBehavior="automatic">
|
||||
<YStack alignItems="center" minHeight={width / 1.1}>
|
||||
<CachedImage
|
||||
source={getImageApi(apiClient!)
|
||||
source={getImageApi(Client.api!)
|
||||
.getItemImageUrlById(
|
||||
props.album.Id!,
|
||||
ImageType.Primary,
|
||||
@@ -60,6 +75,7 @@ export default function Album(props: AlbumProps): React.JSX.Element {
|
||||
track={track}
|
||||
tracklist={tracks!}
|
||||
index={index}
|
||||
navigation={props.navigation}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { RouteProp } from "@react-navigation/native";
|
||||
import Album from "../../Album/component";
|
||||
import Album from "../component";
|
||||
import { StackParamList } from "../../types";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
|
||||
|
||||
export function HomeAlbumScreen({ route, navigation } : { route: RouteProp<StackParamList, "Album">, navigation: NativeStackNavigationProp<StackParamList> }): React.JSX.Element {
|
||||
export function AlbumScreen({ route, navigation } : { route: RouteProp<StackParamList, "Album">, navigation: NativeStackNavigationProp<StackParamList> }): React.JSX.Element {
|
||||
return (
|
||||
<Album
|
||||
album={route.params.album }
|
||||
41
components/Albums/component.tsx
Normal file
41
components/Albums/component.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useFavoriteAlbums } from "../../api/queries/favorites";
|
||||
import { AlbumsProps } from "../types";
|
||||
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import { ItemCard } from "../Global/helpers/item-card";
|
||||
import { FlatList, RefreshControl } from "react-native";
|
||||
|
||||
export default function Albums({ navigation }: AlbumsProps) : React.JSX.Element {
|
||||
const { data: albums, refetch, isPending } = useFavoriteAlbums();
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["left", "right"]}>
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
numColumns={2}
|
||||
data={albums}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPending}
|
||||
onRefresh={refetch}
|
||||
/>
|
||||
}
|
||||
renderItem={({ index, item: album}) => {
|
||||
return (
|
||||
<ItemCard
|
||||
itemId={album.Id!}
|
||||
caption={album.Name ?? "Untitled Album"}
|
||||
subCaption={album.ProductionYear?.toString() ?? ""}
|
||||
cornered
|
||||
onPress={() => {
|
||||
navigation.navigate("Album", { album })
|
||||
}}
|
||||
width={width / 2.1}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
17
components/Albums/screen.tsx
Normal file
17
components/Albums/screen.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react"
|
||||
import { StackParamList } from "../types"
|
||||
import { RouteProp } from "@react-navigation/native"
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack"
|
||||
import Albums from "./component"
|
||||
|
||||
export default function AlbumsScreen({
|
||||
route,
|
||||
navigation
|
||||
} : {
|
||||
route: RouteProp<StackParamList, "Albums">,
|
||||
navigation: NativeStackNavigationProp<StackParamList, "Albums", undefined>
|
||||
}) : React.JSX.Element {
|
||||
return (
|
||||
<Albums route={route} navigation={navigation}/>
|
||||
)
|
||||
}
|
||||
@@ -1,80 +1,90 @@
|
||||
import { ScrollView, useWindowDimensions } from "tamagui";
|
||||
import { ScrollView, YStack } from "tamagui";
|
||||
import { useArtistAlbums } from "../../api/queries/artist";
|
||||
import { useApiClientContext } from "../jellyfin-api-provider";
|
||||
import { FlatList } from "react-native";
|
||||
import { Card } from "../Global/helpers/card";
|
||||
import { ItemCard } from "../Global/helpers/item-card";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../types";
|
||||
import { H2 } from "../Global/helpers/text";
|
||||
import { useState } from "react";
|
||||
import { CachedImage } from "@georstat/react-native-image-cache";
|
||||
import { ImageType } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { queryConfig } from "@/api/queries/query.config";
|
||||
import { BaseItemDto, ImageType } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { queryConfig } from "../../api/queries/query.config";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import FavoriteButton from "../Global/components/favorite-button";
|
||||
import Client from "../../api/client";
|
||||
|
||||
interface ArtistProps {
|
||||
artistId: string,
|
||||
artistName: string,
|
||||
artist: BaseItemDto
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}
|
||||
|
||||
export default function Artist(props: ArtistProps): React.JSX.Element {
|
||||
|
||||
props.navigation.setOptions({
|
||||
headerRight: () => {
|
||||
return (
|
||||
<FavoriteButton item={props.artist} />
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
const [columns, setColumns] = useState<number>(2);
|
||||
|
||||
const { apiClient } = useApiClientContext();
|
||||
const { height, width } = useSafeAreaFrame();
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
const bannerHeight = height / 6;
|
||||
|
||||
const { data: albums } = useArtistAlbums(props.artistId, apiClient!);
|
||||
const { data: albums } = useArtistAlbums(props.artist.Id!);
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["top", "right", "left"]}>
|
||||
<SafeAreaView style={{ flex: 1 }} edges={["top", "right", "left"]}>
|
||||
<ScrollView
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
alignContent="center">
|
||||
<CachedImage
|
||||
source={getImageApi(apiClient!)
|
||||
.getItemImageUrlById(
|
||||
props.artistId,
|
||||
ImageType.Primary,
|
||||
{ ...queryConfig.banners})
|
||||
}
|
||||
imageStyle={{
|
||||
width: 500,
|
||||
height: 350,
|
||||
resizeMode: "cover",
|
||||
position: "relative"
|
||||
}}
|
||||
/>
|
||||
<YStack alignContent="center" justifyContent="center" minHeight={bannerHeight}>
|
||||
<CachedImage
|
||||
source={getImageApi(Client.api!)
|
||||
.getItemImageUrlById(
|
||||
props.artist.Id!,
|
||||
ImageType.Primary,
|
||||
{ ...queryConfig.banners})
|
||||
}
|
||||
imageStyle={{
|
||||
width: width,
|
||||
height: bannerHeight,
|
||||
alignSelf: "center",
|
||||
resizeMode: "cover",
|
||||
position: "relative"
|
||||
}}
|
||||
/>
|
||||
</YStack>
|
||||
|
||||
<H2>Albums</H2>
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: "center"
|
||||
}}
|
||||
data={albums}
|
||||
numColumns={columns} // TODO: Make this adjustable
|
||||
renderItem={({ item: album }) => {
|
||||
return (
|
||||
<Card
|
||||
caption={album.Name}
|
||||
subCaption={album.ProductionYear?.toString()}
|
||||
width={(width / 1.1) / columns}
|
||||
cornered
|
||||
itemId={album.Id!}
|
||||
onPress={() => {
|
||||
props.navigation.navigate('Album', {
|
||||
album
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
alignContent: 'center'
|
||||
}}
|
||||
data={albums}
|
||||
numColumns={columns} // TODO: Make this adjustable
|
||||
renderItem={({ item: album }) => {
|
||||
return (
|
||||
<ItemCard
|
||||
caption={album.Name}
|
||||
subCaption={album.ProductionYear?.toString()}
|
||||
width={(width / 1.1) / columns}
|
||||
cornered
|
||||
itemId={album.Id!}
|
||||
onPress={() => {
|
||||
props.navigation.navigate('Album', {
|
||||
album
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { RouteProp } from "@react-navigation/native";
|
||||
import Artist from "../../Artist/component";
|
||||
import Artist from "../component";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../../types";
|
||||
|
||||
export function HomeArtistScreen({ route, navigation } : {
|
||||
export function ArtistScreen({
|
||||
route,
|
||||
navigation
|
||||
} : {
|
||||
route: RouteProp<StackParamList, "Artist">,
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<Artist
|
||||
artistId={route.params.artistId}
|
||||
artistName={route.params.artistName}
|
||||
artist={route.params.artist}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
42
components/Artists/component.tsx
Normal file
42
components/Artists/component.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useFavoriteArtists } from "../../api/queries/favorites";
|
||||
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import React from "react";
|
||||
import { FlatList, RefreshControl } from "react-native";
|
||||
import { ItemCard } from "../Global/helpers/item-card";
|
||||
import { ArtistsProps } from "../types";
|
||||
|
||||
export default function Artists({ navigation }: ArtistsProps): React.JSX.Element {
|
||||
|
||||
const { data: artists, refetch, isPending } = useFavoriteArtists();
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["left", "right"]}>
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
numColumns={2}
|
||||
data={artists}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPending}
|
||||
onRefresh={refetch}
|
||||
/>
|
||||
}
|
||||
renderItem={({ index, item: artist}) => {
|
||||
return (
|
||||
<ItemCard
|
||||
artistName={artist.Name!}
|
||||
itemId={artist.Id!}
|
||||
caption={artist.Name ?? "Unknown Artist"}
|
||||
onPress={() => {
|
||||
navigation.navigate("Artist", { artist })
|
||||
}}
|
||||
width={width / 2.1}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
17
components/Artists/screen.tsx
Normal file
17
components/Artists/screen.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
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}/>
|
||||
)
|
||||
}
|
||||
34
components/CarPlay/NowPlaying.tsx
Normal file
34
components/CarPlay/NowPlaying.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
import {CarPlay, NowPlayingTemplate} from 'react-native-carplay';
|
||||
|
||||
export function NowPlaying() {
|
||||
useEffect(() => {
|
||||
const template = new NowPlayingTemplate({
|
||||
albumArtistButtonEnabled: true,
|
||||
upNextButtonEnabled: false,
|
||||
onUpNextButtonPressed() {
|
||||
console.log('up next was pressed');
|
||||
},
|
||||
onButtonPressed(e) {
|
||||
console.log(e);
|
||||
},
|
||||
});
|
||||
|
||||
CarPlay.enableNowPlaying(true);
|
||||
CarPlay.pushTemplate(template);
|
||||
|
||||
return () => {};
|
||||
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
|
||||
<Text>Now Playing</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
NowPlaying.navigationOptions = {
|
||||
headerTitle: 'Now Playing Template',
|
||||
};
|
||||
105
components/Favorites/component.tsx
Normal file
105
components/Favorites/component.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import React from "react";
|
||||
import { StackParamList } from "../types";
|
||||
import FavoritesScreen from "./screens";
|
||||
import { ArtistScreen } from "../Artist/screens";
|
||||
import { AlbumScreen } from "../Album/screens";
|
||||
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";
|
||||
|
||||
const LibraryStack = createNativeStackNavigator<StackParamList>();
|
||||
|
||||
export default function Library(): React.JSX.Element {
|
||||
return (
|
||||
<LibraryStack.Navigator
|
||||
id="Favorites"
|
||||
initialRouteName="Favorites"
|
||||
>
|
||||
<LibraryStack.Screen
|
||||
name="Favorites"
|
||||
component={FavoritesScreen}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name="Artist"
|
||||
component={ArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artist.Name ?? "Unknown Artist",
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name="Artists"
|
||||
component={ArtistsScreen}
|
||||
options={({ route }) => ({
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name="Album"
|
||||
component={AlbumScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name="Albums"
|
||||
component={AlbumsScreen}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name="Tracks"
|
||||
component={TracksScreen}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name="Playlist"
|
||||
component={PlaylistScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
|
||||
<LibraryStack.Screen
|
||||
name="Details"
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "modal"
|
||||
}}
|
||||
/>
|
||||
</LibraryStack.Navigator>
|
||||
)
|
||||
}
|
||||
15
components/Favorites/screens/categories.ts
Normal file
15
components/Favorites/screens/categories.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
interface CategoryRoute {
|
||||
name: any; // ¯\_(ツ)_/¯
|
||||
iconName: string;
|
||||
};
|
||||
|
||||
const Categories : CategoryRoute[] = [
|
||||
{ name: "Artists", iconName: "microphone-variant" },
|
||||
{ name: "Albums", iconName: "music-box-multiple" },
|
||||
{ name: "Tracks", iconName: "music-note"},
|
||||
{ name: "Playlists", iconName: "playlist-music"},
|
||||
{ name: "Genres", iconName: "guitar-electric"}
|
||||
];
|
||||
|
||||
export default Categories;
|
||||
40
components/Favorites/screens/index.tsx
Normal file
40
components/Favorites/screens/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { FlatList } from "react-native";
|
||||
import { SafeAreaView, 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 FavoritesScreen({
|
||||
route,
|
||||
navigation
|
||||
} : {
|
||||
route: RouteProp<StackParamList, "Favorites">,
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }} edges={["top", "right", "left"]}>
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
data={Categories}
|
||||
numColumns={2}
|
||||
renderItem={({ index, item }) => {
|
||||
return (
|
||||
<IconCard
|
||||
name={item.iconName}
|
||||
caption={item.name}
|
||||
width={width / 2.1}
|
||||
onPress={() => {
|
||||
navigation.navigate(item.name)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
99
components/Global/components/favorite-button.tsx
Normal file
99
components/Global/components/favorite-button.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Icon from "../helpers/icon";
|
||||
import { Colors } from "../../../enums/colors";
|
||||
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { isUndefined } from "lodash";
|
||||
import { useUserData } from "../../../api/queries/favorites";
|
||||
import { Spinner } from "tamagui";
|
||||
import Client from "../../../api/client";
|
||||
import { usePlayerContext } from "../../..//player/provider";
|
||||
|
||||
interface SetFavoriteMutation {
|
||||
item: BaseItemDto,
|
||||
}
|
||||
|
||||
export default function FavoriteButton({
|
||||
item,
|
||||
onToggle
|
||||
}: {
|
||||
item: BaseItemDto;
|
||||
onToggle?: () => void
|
||||
}) : React.JSX.Element {
|
||||
|
||||
const { nowPlaying, nowPlayingIsFavorite } = usePlayerContext();
|
||||
|
||||
const [isFavorite, setIsFavorite] = useState<boolean>(isFavoriteItem(item));
|
||||
|
||||
const { data, isFetching, isFetched, refetch } = useUserData(item.Id!);
|
||||
|
||||
const useSetFavorite = useMutation({
|
||||
mutationFn: async (mutation: SetFavoriteMutation) => {
|
||||
return getUserLibraryApi(Client.api!)
|
||||
.markFavoriteItem({
|
||||
itemId: mutation.item.Id!
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
setIsFavorite(true);
|
||||
onToggle ? onToggle() : {};
|
||||
}
|
||||
})
|
||||
|
||||
const useRemoveFavorite = useMutation({
|
||||
mutationFn: async (mutation: SetFavoriteMutation) => {
|
||||
return getUserLibraryApi(Client.api!)
|
||||
.unmarkFavoriteItem({
|
||||
itemId: mutation.item.Id!
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
setIsFavorite(false);
|
||||
onToggle ? onToggle(): {};
|
||||
}
|
||||
})
|
||||
|
||||
const toggleFavorite = () => {
|
||||
if (isFavorite)
|
||||
useRemoveFavorite.mutate({ item })
|
||||
else
|
||||
useSetFavorite.mutate({ item })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetched
|
||||
&& !isUndefined(data)
|
||||
&& !isUndefined(data.IsFavorite)
|
||||
)
|
||||
setIsFavorite(data.IsFavorite)
|
||||
}, [
|
||||
isFetched,
|
||||
data
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [
|
||||
item
|
||||
]);
|
||||
|
||||
return (
|
||||
isFetching && isUndefined(item.UserData) ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Icon
|
||||
name={isFavorite ? "heart" : "heart-outline"}
|
||||
color={Colors.Primary}
|
||||
onPress={toggleFavorite}
|
||||
/>
|
||||
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function isFavoriteItem(item: BaseItemDto) : boolean {
|
||||
return isUndefined(item.UserData) ? false
|
||||
: isUndefined(item.UserData.IsFavorite) ? false
|
||||
: item.UserData.IsFavorite
|
||||
}
|
||||
@@ -1,27 +1,32 @@
|
||||
import { usePlayerContext } from "@/player/provider";
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
import React from "react";
|
||||
import { Separator, View, XStack, YStack } from "tamagui";
|
||||
import { Separator, Spacer, View, XStack, YStack } from "tamagui";
|
||||
import { Text } from "../helpers/text";
|
||||
import { RunTimeTicks } from "../helpers/time-codes";
|
||||
import { BaseItemDto, ImageType } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { Colors } from "@/enums/colors";
|
||||
import { Colors } from "../../../enums/colors";
|
||||
import { CachedImage } from "@georstat/react-native-image-cache";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api/image-api";
|
||||
import { useApiClientContext } from "@/components/jellyfin-api-provider";
|
||||
import { queryConfig } from "@/api/queries/query.config";
|
||||
import { queryConfig } from "../../../api/queries/query.config";
|
||||
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 Client from "../../../api/client";
|
||||
|
||||
interface TrackProps {
|
||||
track: BaseItemDto;
|
||||
tracklist: BaseItemDto[];
|
||||
index: number;
|
||||
navigation: NativeStackNavigationProp<StackParamList>;
|
||||
index: number | undefined;
|
||||
showArtwork?: boolean | undefined;
|
||||
onPress?: () => void | undefined
|
||||
onPress?: () => void | undefined;
|
||||
}
|
||||
|
||||
export default function Track({
|
||||
track,
|
||||
tracklist,
|
||||
navigation,
|
||||
index,
|
||||
queueName,
|
||||
showArtwork,
|
||||
@@ -29,14 +34,14 @@ export default function Track({
|
||||
} : {
|
||||
track: BaseItemDto,
|
||||
tracklist: BaseItemDto[],
|
||||
index: number,
|
||||
navigation: NativeStackNavigationProp<StackParamList>;
|
||||
index?: number | undefined,
|
||||
queueName?: string | undefined,
|
||||
showArtwork?: boolean | undefined,
|
||||
onPress?: () => void | undefined
|
||||
}) : React.JSX.Element {
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
const { apiClient } = useApiClientContext();
|
||||
const { nowPlaying, usePlayNewQueue } = usePlayerContext();
|
||||
|
||||
const isPlaying = nowPlaying?.item.Id === track.Id;
|
||||
@@ -70,7 +75,7 @@ export default function Track({
|
||||
>
|
||||
{ showArtwork ? (
|
||||
<CachedImage
|
||||
source={getImageApi(apiClient!)
|
||||
source={getImageApi(Client.api!)
|
||||
.getItemImageUrlById(
|
||||
track.AlbumId ?? "",
|
||||
ImageType.Primary,
|
||||
@@ -92,7 +97,7 @@ export default function Track({
|
||||
)}
|
||||
</XStack>
|
||||
|
||||
<YStack alignContent="center" justifyContent="flex-start" flex={6}>
|
||||
<YStack alignContent="center" justifyContent="flex-start" flex={5}>
|
||||
<Text
|
||||
bold
|
||||
color={isPlaying ? Colors.Primary : Colors.White}
|
||||
@@ -108,11 +113,41 @@ export default function Track({
|
||||
</YStack>
|
||||
|
||||
<XStack
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
alignContent="center"
|
||||
flex={1}
|
||||
flex={2}
|
||||
>
|
||||
<RunTimeTicks>{ track.RunTimeTicks }</RunTimeTicks>
|
||||
<YStack
|
||||
alignContent="center"
|
||||
justifyContent="center"
|
||||
minWidth={24}
|
||||
>
|
||||
{ track.UserData?.IsFavorite ? (
|
||||
<Icon small name="heart" color={Colors.Primary} />
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
</YStack>
|
||||
|
||||
<YStack
|
||||
alignContent="center"
|
||||
justifyContent="space-around"
|
||||
>
|
||||
<RunTimeTicks>{ track.RunTimeTicks }</RunTimeTicks>
|
||||
</YStack>
|
||||
|
||||
<YStack
|
||||
alignContent="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Icon small name="dots-vertical" onPress={() => {
|
||||
navigation.push("Details", {
|
||||
item: track
|
||||
})
|
||||
}} />
|
||||
|
||||
</YStack>
|
||||
</XStack>
|
||||
</XStack>
|
||||
</View>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AvatarProps as TamaguiAvatarProps } from "tamagui";
|
||||
import { Avatar as TamaguiAvatar, YStack } from "tamagui"
|
||||
import { Text } from "./text"
|
||||
import { useApiClientContext } from "@/components/jellyfin-api-provider";
|
||||
import { Colors } from "react-native/Libraries/NewAppScreen";
|
||||
import Client from "../../../api/client";
|
||||
|
||||
interface AvatarProps extends TamaguiAvatarProps {
|
||||
itemId: string;
|
||||
@@ -11,8 +11,6 @@ interface AvatarProps extends TamaguiAvatarProps {
|
||||
|
||||
export default function Avatar(props: AvatarProps): React.JSX.Element {
|
||||
|
||||
const { server } = useApiClientContext();
|
||||
|
||||
return (
|
||||
<YStack alignItems="center" marginHorizontal={10}>
|
||||
<TamaguiAvatar
|
||||
@@ -20,7 +18,7 @@ export default function Avatar(props: AvatarProps): React.JSX.Element {
|
||||
borderRadius={!!!props.circular ? 4 : 'unset'}
|
||||
{...props}
|
||||
>
|
||||
<TamaguiAvatar.Image src={`${server!.url}/Items/${props.itemId!}/Images/Primary`} />
|
||||
<TamaguiAvatar.Image src={`${Client.server!.url}/Items/${props.itemId!}/Images/Primary`} />
|
||||
<TamaguiAvatar.Fallback backgroundColor={Colors.Secondary}/>
|
||||
</TamaguiAvatar>
|
||||
{ props.children && (
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Blurhash } from "react-native-blurhash";
|
||||
import { ImageSourcePropType } from "react-native/Libraries/Image/Image";
|
||||
|
||||
const BlurhashLoading = (props: any) => {
|
||||
return (
|
||||
<Blurhash blurhash={props}>
|
||||
|
||||
</Blurhash>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlurhashLoading;
|
||||
40
components/Global/helpers/blurhashed-image.tsx
Normal file
40
components/Global/helpers/blurhashed-image.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { BaseItemDto, ImageType } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useItemImage } from "../../../api/queries/image";
|
||||
import { Blurhash } from "react-native-blurhash";
|
||||
import { Image, View } from "tamagui";
|
||||
import { isEmpty } from "lodash";
|
||||
|
||||
interface BlurhashLoadingProps {
|
||||
item: BaseItemDto;
|
||||
size: number
|
||||
}
|
||||
|
||||
export default function BlurhashedImage({ item, size, type }: { item: BaseItemDto, size: number, type?: ImageType }) : React.JSX.Element {
|
||||
|
||||
const { data: image, isSuccess } = useItemImage(item.Id!, type);
|
||||
|
||||
const blurhash = !isEmpty(item.ImageBlurHashes)
|
||||
&& !isEmpty(item.ImageBlurHashes.Primary)
|
||||
? Object.values(item.ImageBlurHashes.Primary)[0]
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<View minHeight={size} minWidth={size}>
|
||||
|
||||
{ isSuccess ? (
|
||||
<Image
|
||||
source={{
|
||||
uri: image
|
||||
}}
|
||||
style={{
|
||||
height: size,
|
||||
width: size,
|
||||
}}
|
||||
/>
|
||||
) : blurhash && (
|
||||
<Blurhash blurhash={blurhash!} style={{ flex: 1 }} />
|
||||
)
|
||||
}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ interface ButtonProps {
|
||||
children?: Element | string | undefined;
|
||||
onPress?: () => void | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
danger?: boolean | undefined;
|
||||
}
|
||||
|
||||
export default function Button(props: ButtonProps): React.JSX.Element {
|
||||
|
||||
44
components/Global/helpers/icon-card.tsx
Normal file
44
components/Global/helpers/icon-card.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Card, View } from "tamagui";
|
||||
import { H2 } from "./text";
|
||||
import { Colors } from "../../../enums/colors";
|
||||
import Icon from "./icon";
|
||||
|
||||
export default function IconCard({
|
||||
name,
|
||||
onPress,
|
||||
width,
|
||||
caption,
|
||||
}: {
|
||||
name: string,
|
||||
onPress: () => void,
|
||||
width?: number | undefined,
|
||||
caption?: string | undefined,
|
||||
}) : React.JSX.Element {
|
||||
return (
|
||||
<View
|
||||
alignItems="center"
|
||||
margin={5}
|
||||
>
|
||||
<Card
|
||||
elevate
|
||||
borderRadius={"$7"}
|
||||
animation="bouncy"
|
||||
hoverStyle={{ scale: 0.925 }}
|
||||
pressStyle={{ scale: 0.875 }}
|
||||
width={width ? width : 150}
|
||||
height={width ? width : 150}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Card.Header>
|
||||
<Icon color={Colors.Background} name={name} large />
|
||||
</Card.Header>
|
||||
<Card.Footer padded>
|
||||
<H2 color={Colors.Background}>{ caption }</H2>
|
||||
</Card.Footer>
|
||||
<Card.Background backgroundColor={Colors.Primary}>
|
||||
|
||||
</Card.Background>
|
||||
</Card>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +1,24 @@
|
||||
import React from "react"
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"
|
||||
import { Colors } from "../../../enums/colors"
|
||||
import { useColorScheme } from "react-native";
|
||||
|
||||
const smallSize = 24;
|
||||
|
||||
const regularSize = 36;
|
||||
|
||||
const largeSize = 48
|
||||
|
||||
export default function Icon({ name, onPress, large }: { name: string, onPress?: Function, large?: boolean }) : React.JSX.Element {
|
||||
export default function Icon({ name, onPress, small, large, color }: { name: string, onPress?: () => void, small?: boolean, large?: boolean, color?: Colors }) : React.JSX.Element {
|
||||
|
||||
let size = large ? largeSize : regularSize
|
||||
const isDarkMode = useColorScheme() === "dark"
|
||||
let size = large ? largeSize : small ? smallSize : regularSize
|
||||
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
color={Colors.White}
|
||||
color={color ? color : isDarkMode ? Colors.White : Colors.Background}
|
||||
name={name}
|
||||
onPress={() => {
|
||||
if (onPress)
|
||||
onPress();
|
||||
}}
|
||||
onPress={onPress}
|
||||
size={size}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Colors } from '../../../enums/colors';
|
||||
import React, { SetStateAction } from 'react';
|
||||
import { StyleProp } from 'react-native';
|
||||
import { Input as TamaguiInput, TextStyle} from 'tamagui';
|
||||
import { Input as TamaguiInput} from 'tamagui';
|
||||
|
||||
interface InputProps {
|
||||
onChangeText: React.Dispatch<SetStateAction<string | undefined>>,
|
||||
@@ -14,6 +14,8 @@ export default function Input(props: InputProps): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<TamaguiInput
|
||||
backgroundColor={Colors.Background}
|
||||
borderColor={Colors.Borders}
|
||||
placeholder={props.placeholder}
|
||||
onChangeText={props.onChangeText}
|
||||
value={props.value}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { } from "react";
|
||||
import type { CardProps as TamaguiCardProps } from "tamagui"
|
||||
import { H5, Card as TamaguiCard, View } from "tamagui";
|
||||
import { useApiClientContext } from "../../jellyfin-api-provider";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { ImageType } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { CachedImage } from "@georstat/react-native-image-cache";
|
||||
@@ -9,6 +8,7 @@ import invert from "invert-color"
|
||||
import { Blurhash } from "react-native-blurhash"
|
||||
import { queryConfig } from "../../../api/queries/query.config";
|
||||
import { Text } from "./text";
|
||||
import Client from "../../../api/client";
|
||||
|
||||
interface CardProps extends TamaguiCardProps {
|
||||
artistName?: string;
|
||||
@@ -19,16 +19,14 @@ interface CardProps extends TamaguiCardProps {
|
||||
cornered?: boolean;
|
||||
}
|
||||
|
||||
export function Card(props: CardProps) {
|
||||
|
||||
const { apiClient } = useApiClientContext();
|
||||
export function ItemCard(props: CardProps) {
|
||||
|
||||
const dimensions = props.width && typeof(props.width) === "number" ? { width: props.width, height: props.width } : { width: 150, height: 150 };
|
||||
|
||||
const cardTextColor = props.blurhash ? invert(Blurhash.getAverageColor(props.blurhash)!, true) : undefined;
|
||||
|
||||
const logoDimensions = props.width && typeof(props.width) === "number" ? { width: props.width / 2, height: props.width / 2 }: { width: 100, height: 100 };
|
||||
const cardLogoSource = getImageApi(apiClient!).getItemImageUrlById(props.itemId, ImageType.Logo);
|
||||
const cardLogoSource = getImageApi(Client.api!).getItemImageUrlById(props.itemId, ImageType.Logo);
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -50,7 +48,7 @@ export function Card(props: CardProps) {
|
||||
</TamaguiCard.Header>
|
||||
<TamaguiCard.Footer padded>
|
||||
<CachedImage
|
||||
source={getImageApi(apiClient!)
|
||||
source={getImageApi(Client.api!)
|
||||
.getItemImageUrlById(
|
||||
props.itemId,
|
||||
ImageType.Logo,
|
||||
@@ -70,7 +68,7 @@ export function Card(props: CardProps) {
|
||||
</TamaguiCard.Footer>
|
||||
<TamaguiCard.Background>
|
||||
<CachedImage
|
||||
source={getImageApi(apiClient!)
|
||||
source={getImageApi(Client.api!)
|
||||
.getItemImageUrlById(
|
||||
props.itemId,
|
||||
ImageType.Primary,
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Colors } from "@/enums/colors";
|
||||
import { Colors } from "../../../enums/colors";
|
||||
import React from "react";
|
||||
import { SliderProps as TamaguiSliderProps, SliderVerticalProps, Slider as TamaguiSlider, styled, Slider } from "tamagui";
|
||||
|
||||
@@ -11,11 +11,11 @@ interface SliderProps {
|
||||
|
||||
const JellifySliderThumb = styled(Slider.Thumb, {
|
||||
backgroundColor: Colors.Primary,
|
||||
borderColor: Colors.Background
|
||||
borderColor: Colors.Background,
|
||||
})
|
||||
|
||||
const JellifySliderTrack = styled(Slider.Track, {
|
||||
backgroundColor: Colors.Secondary
|
||||
backgroundColor: Colors.Borders
|
||||
});
|
||||
|
||||
const JellifyActiveSliderTrack = styled(Slider.TrackActive, {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SizeTokens, XStack, Separator, Switch, ColorTokens } from "tamagui";
|
||||
import { Label } from "./text";
|
||||
import { Colors } from "react-native/Libraries/NewAppScreen";
|
||||
|
||||
interface SwitchWithLabelProps {
|
||||
onCheckedChange: (value: boolean) => void,
|
||||
@@ -26,7 +27,7 @@ export function SwitchWithLabel(props: SwitchWithLabelProps) {
|
||||
size={props.size}
|
||||
checked={props.checked}
|
||||
onCheckedChange={(checked: boolean) => props.onCheckedChange(checked)}
|
||||
backgroundColor={props.backgroundColor}
|
||||
backgroundColor={props.backgroundColor ?? Colors.Primary}
|
||||
>
|
||||
<Switch.Thumb animation="quicker" />
|
||||
</Switch>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { convertRunTimeTicksToSeconds } from "../../../helpers/runtimeticks";
|
||||
import { Text } from "./text";
|
||||
import { convertRunTimeTicksToSeconds } from "@/helpers/runtimeticks";
|
||||
import React from "react";
|
||||
|
||||
export function RunTimeSeconds({ children }: { children: number }) : React.JSX.Element {
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import { H3, ScrollView, Separator, XStack, YStack } from "tamagui";
|
||||
import _ from "lodash";
|
||||
import RecentlyPlayed from "./helpers/recently-played";
|
||||
import { useApiClientContext } from "../jellyfin-api-provider";
|
||||
import RecentArtists from "./helpers/recent-artists";
|
||||
import { RefreshControl } from "react-native";
|
||||
import { HomeProvider, useHomeContext } from "./provider";
|
||||
import { HomeProvider } from "./provider";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import { StackParamList, ProvidedHomeProps } from "../types";
|
||||
import { HomeArtistScreen } from "./screens/artist";
|
||||
import Avatar from "../Global/helpers/avatar";
|
||||
import { HomeAlbumScreen } from "./screens/album";
|
||||
import Playlists from "./helpers/playlists";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { HomePlaylistScreen } from "./screens/playlist";
|
||||
import { StackParamList } from "../types";
|
||||
import { ArtistScreen } from "../Artist/screens";
|
||||
import { AlbumScreen } from "../Album/screens";
|
||||
import { PlaylistScreen } from "../Playlist/screens";
|
||||
import { ProvidedHome } from "./screens";
|
||||
import DetailsScreen from "../ItemDetail/screen";
|
||||
|
||||
const HomeStack = createNativeStackNavigator<StackParamList>();
|
||||
|
||||
export default function Home(): React.JSX.Element {
|
||||
|
||||
const { user } = useApiClientContext();
|
||||
|
||||
return (
|
||||
<HomeProvider>
|
||||
<HomeStack.Navigator
|
||||
@@ -28,87 +20,61 @@ export default function Home(): React.JSX.Element {
|
||||
screenOptions={{
|
||||
}}
|
||||
>
|
||||
<HomeStack.Screen
|
||||
name="Home"
|
||||
component={ProvidedHome}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<HomeStack.Group>
|
||||
<HomeStack.Screen
|
||||
name="Home"
|
||||
component={ProvidedHome}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<HomeStack.Screen
|
||||
name="Artist"
|
||||
component={HomeArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artistName,
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<HomeStack.Screen
|
||||
name="Artist"
|
||||
component={ArtistScreen}
|
||||
options={({ route }) => ({
|
||||
title: route.params.artist.Name ?? "Unknown Artist",
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
|
||||
<HomeStack.Screen
|
||||
name="Album"
|
||||
component={HomeAlbumScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
<HomeStack.Screen
|
||||
name="Album"
|
||||
component={AlbumScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
|
||||
<HomeStack.Screen
|
||||
name="Playlist"
|
||||
component={HomePlaylistScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
<HomeStack.Screen
|
||||
name="Playlist"
|
||||
component={PlaylistScreen}
|
||||
options={({ route }) => ({
|
||||
headerShown: true,
|
||||
headerTitle: ""
|
||||
})}
|
||||
/>
|
||||
|
||||
</HomeStack.Group>
|
||||
|
||||
<HomeStack.Group screenOptions={{ presentation: "modal"}}>
|
||||
<HomeStack.Screen
|
||||
name="Details"
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
presentation: "modal"
|
||||
}}
|
||||
/>
|
||||
</HomeStack.Group>
|
||||
</HomeStack.Navigator>
|
||||
</HomeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ProvidedHome({ route, navigation }: ProvidedHomeProps): React.JSX.Element {
|
||||
|
||||
const { user } = useApiClientContext();
|
||||
|
||||
const { refreshing: refetching, onRefresh: onRefetch } = useHomeContext()
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["top", "right", "left"]}>
|
||||
<ScrollView
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refetching}
|
||||
onRefresh={onRefetch}
|
||||
/>
|
||||
}>
|
||||
<YStack alignContent='flex-start'>
|
||||
<XStack margin={"$2"}>
|
||||
<H3>{`Hi, ${user!.name}`}</H3>
|
||||
<YStack />
|
||||
<Avatar maxHeight={30} itemId={user!.id} />
|
||||
</XStack>
|
||||
|
||||
<Separator marginVertical={"$2"} />
|
||||
|
||||
<RecentArtists route={route} navigation={navigation} />
|
||||
|
||||
<Separator marginVertical={"$3"} />
|
||||
|
||||
<RecentlyPlayed />
|
||||
|
||||
<Separator marginVertical={"$3"} />
|
||||
|
||||
<Playlists route={route} navigation={navigation}/>
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
import { useUserPlaylists } from "@/api/queries/playlist";
|
||||
import { Card } from "@/components/Global/helpers/card";
|
||||
import { H2 } from "@/components/Global/helpers/text";
|
||||
import { useApiClientContext } from "@/components/jellyfin-api-provider";
|
||||
import { ProvidedHomeProps } from "@/components/types";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { useUserPlaylists } from "../../../api/queries/playlist";
|
||||
import { ItemCard } from "../../../components/Global/helpers/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 } from "tamagui";
|
||||
|
||||
export default function Playlists({ navigation }: ProvidedHomeProps) : React.JSX.Element {
|
||||
export default function Playlists({ navigation }: { navigation: NativeStackNavigationProp<StackParamList>}) : React.JSX.Element {
|
||||
|
||||
const { apiClient, user, library } = useApiClientContext();
|
||||
|
||||
const { data: playlists } = useUserPlaylists(apiClient!, user!.id, library!.playlistLibraryId);
|
||||
const { data: playlists } = useUserPlaylists();
|
||||
|
||||
return (
|
||||
<View>
|
||||
@@ -20,7 +18,7 @@ export default function Playlists({ navigation }: ProvidedHomeProps) : React.JSX
|
||||
data={playlists}
|
||||
renderItem={({ item: playlist }) => {
|
||||
return (
|
||||
<Card
|
||||
<ItemCard
|
||||
itemId={playlist.Id!}
|
||||
caption={playlist.Name ?? "Untitled Playlist"}
|
||||
onPress={() => {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React from "react";
|
||||
import { View } from "tamagui";
|
||||
import { useHomeContext } from "../provider";
|
||||
import { H2 } from "../../Global/helpers/text";
|
||||
import { ProvidedHomeProps } from "../../types";
|
||||
import { StackParamList } from "../../types";
|
||||
import { FlatList } from "react-native";
|
||||
import { Card } from "../../Global/helpers/card";
|
||||
import { ItemCard } from "../../Global/helpers/item-card";
|
||||
import { getPrimaryBlurhashFromDto } from "../../../helpers/blurhash";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
|
||||
export default function RecentArtists({ navigation }: ProvidedHomeProps): React.JSX.Element {
|
||||
export default function RecentArtists({ navigation }: { navigation: NativeStackNavigationProp<StackParamList>}): React.JSX.Element {
|
||||
|
||||
const { recentArtists } = useHomeContext();
|
||||
|
||||
@@ -18,7 +19,7 @@ export default function RecentArtists({ navigation }: ProvidedHomeProps): React.
|
||||
data={recentArtists}
|
||||
renderItem={({ item: recentArtist}) => {
|
||||
return (
|
||||
<Card
|
||||
<ItemCard
|
||||
artistName={recentArtist.Name!}
|
||||
blurhash={getPrimaryBlurhashFromDto(recentArtist)}
|
||||
itemId={recentArtist.Id!}
|
||||
@@ -26,12 +27,11 @@ export default function RecentArtists({ navigation }: ProvidedHomeProps): React.
|
||||
onPress={() => {
|
||||
navigation.navigate('Artist',
|
||||
{
|
||||
artistId: recentArtist.Id!,
|
||||
artistName: recentArtist.Name ?? "Unknown Artist"
|
||||
artist: recentArtist,
|
||||
}
|
||||
)}
|
||||
}>
|
||||
</Card>
|
||||
</ItemCard>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -2,14 +2,19 @@ import React from "react";
|
||||
import { ScrollView, View } from "tamagui";
|
||||
import { useHomeContext } from "../provider";
|
||||
import { H2 } from "../../Global/helpers/text";
|
||||
import { Card } from "../../Global/helpers/card";
|
||||
import { useApiClientContext } from "../../jellyfin-api-provider";
|
||||
import { ItemCard } from "../../Global/helpers/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";
|
||||
|
||||
export default function RecentlyPlayed(): React.JSX.Element {
|
||||
export default function RecentlyPlayed({
|
||||
navigation
|
||||
} : {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
|
||||
const { usePlayNewQueue } = usePlayerContext();
|
||||
const { apiClient, sessionId } = useApiClientContext();
|
||||
const { recentTracks } = useHomeContext();
|
||||
|
||||
return (
|
||||
@@ -18,7 +23,7 @@ export default function RecentlyPlayed(): React.JSX.Element {
|
||||
<ScrollView horizontal>
|
||||
{ recentTracks && recentTracks.map((recentlyPlayedTrack, index) => {
|
||||
return (
|
||||
<Card
|
||||
<ItemCard
|
||||
caption={recentlyPlayedTrack.Name}
|
||||
subCaption={`${recentlyPlayedTrack.Artists?.join(", ")}`}
|
||||
cornered
|
||||
@@ -32,6 +37,12 @@ export default function RecentlyPlayed(): React.JSX.Element {
|
||||
queueName: "Recently Played"
|
||||
});
|
||||
}}
|
||||
onLongPress={() => {
|
||||
trigger("impactLight");
|
||||
navigation.push("Details", {
|
||||
item: recentlyPlayedTrack
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { createContext, ReactNode, useContext, useState } from "react";
|
||||
import { useApiClientContext } from "../jellyfin-api-provider";
|
||||
import { useRecentlyPlayed, useRecentlyPlayedArtists } from "../../api/queries/recently-played";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
|
||||
@@ -13,10 +12,8 @@ interface HomeContext {
|
||||
const HomeContextInitializer = () => {
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false);
|
||||
|
||||
const { apiClient, library } = useApiClientContext();
|
||||
|
||||
const { data : recentTracks, refetch : refetchRecentTracks } = useRecentlyPlayed(apiClient!, library!.musicLibraryId);
|
||||
const { data : recentArtists, refetch : refetchRecentArtists } = useRecentlyPlayedArtists(apiClient!, library!.musicLibraryId);
|
||||
const { data : recentTracks, refetch : refetchRecentTracks } = useRecentlyPlayed();
|
||||
const { data : recentArtists, refetch : refetchRecentArtists } = useRecentlyPlayedArtists();
|
||||
|
||||
const onRefresh = async () => {
|
||||
await Promise.all([
|
||||
|
||||
64
components/Home/screens/index.tsx
Normal file
64
components/Home/screens/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { ProvidedHomeProps, StackParamList } from "../../../components/types";
|
||||
import { ScrollView, RefreshControl } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
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 "../../../components/Global/helpers/text";
|
||||
import Avatar from "../../../components/Global/helpers/avatar";
|
||||
import Client from "../../../api/client";
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
import { useEffect } from "react";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
|
||||
export function ProvidedHome({
|
||||
navigation
|
||||
} : {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
|
||||
const { refreshing: refetching, onRefresh: onRefetch } = useHomeContext()
|
||||
|
||||
const { nowPlayingIsFavorite } = usePlayerContext();
|
||||
|
||||
useEffect(() => {
|
||||
onRefetch()
|
||||
}, [
|
||||
nowPlayingIsFavorite
|
||||
])
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["top", "right", "left"]}>
|
||||
<ScrollView
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refetching}
|
||||
onRefresh={onRefetch}
|
||||
/>
|
||||
}>
|
||||
<YStack alignContent='flex-start'>
|
||||
<XStack margin={"$2"}>
|
||||
<H3>{`Hi, ${Client.user!.name}`}</H3>
|
||||
<YStack />
|
||||
<Avatar maxHeight={30} itemId={Client.user!.id!} />
|
||||
</XStack>
|
||||
|
||||
<Separator marginVertical={"$2"} />
|
||||
|
||||
<RecentArtists navigation={navigation} />
|
||||
|
||||
<Separator marginVertical={"$3"} />
|
||||
|
||||
<RecentlyPlayed navigation={navigation} />
|
||||
|
||||
<Separator marginVertical={"$3"} />
|
||||
|
||||
<Playlists navigation={navigation}/>
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
100
components/ItemDetail/component.tsx
Normal file
100
components/ItemDetail/component.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import { StackParamList } from "../types";
|
||||
import TrackOptions from "./helpers/TrackOptions";
|
||||
import { Spacer, View, XStack, YStack } from "tamagui";
|
||||
import BlurhashedImage from "../Global/helpers/blurhashed-image";
|
||||
import { Text } from "../Global/helpers/text";
|
||||
import { Colors } from "../../enums/colors";
|
||||
import FavoriteButton from "../Global/components/favorite-button";
|
||||
|
||||
export default function ItemDetail({
|
||||
item,
|
||||
navigation
|
||||
} : {
|
||||
item: BaseItemDto,
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
|
||||
let options: React.JSX.Element | undefined = undefined;
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
switch (item.Type) {
|
||||
case "Audio": {
|
||||
options = TrackOptions({ item, navigation });
|
||||
break;
|
||||
}
|
||||
|
||||
case "MusicAlbum" : {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "MusicArtist" : {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Playlist" : {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default : {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["top", "right", "left"]}>
|
||||
<XStack>
|
||||
<BlurhashedImage
|
||||
item={item}
|
||||
size={width / 2}
|
||||
/>
|
||||
|
||||
<YStack
|
||||
marginLeft={"$0.5"}
|
||||
justifyContent="flex-start"
|
||||
alignContent="space-between"
|
||||
>
|
||||
<Text bold fontSize={"$6"}>
|
||||
{ item.Name ?? "Untitled Track" }
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
fontSize={"$6"}
|
||||
color={Colors.Primary}
|
||||
onPress={() => {
|
||||
if (item.ArtistItems) {
|
||||
navigation.goBack(); // Dismiss modal if exists
|
||||
navigation.getParent()!.navigate("Artist", {
|
||||
artist: item.ArtistItems[0]
|
||||
});
|
||||
}
|
||||
}}>
|
||||
{ item.Artists?.join(", ") ?? "Unknown Artist"}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
fontSize={"$6"}
|
||||
color={"$gray10"}
|
||||
>
|
||||
{ item.Album ?? "" }
|
||||
</Text>
|
||||
|
||||
<Spacer />
|
||||
|
||||
<FavoriteButton item={item} />
|
||||
|
||||
<Spacer />
|
||||
|
||||
{ options ?? <View /> }
|
||||
</YStack>
|
||||
|
||||
</XStack>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
22
components/ItemDetail/helpers/TrackOptions.tsx
Normal file
22
components/ItemDetail/helpers/TrackOptions.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import Icon from "../../../components/Global/helpers/icon";
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { XStack } from "tamagui";
|
||||
|
||||
export default function TrackOptions({
|
||||
item,
|
||||
navigation
|
||||
} : {
|
||||
item: BaseItemDto,
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
|
||||
return (
|
||||
<XStack justifyContent="space-evenly">
|
||||
<Icon name="table-column-plus-before" />
|
||||
|
||||
<Icon name="table-column-plus-after" />
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
20
components/ItemDetail/screen.tsx
Normal file
20
components/ItemDetail/screen.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from "react";
|
||||
import { View } from "tamagui";
|
||||
|
||||
export default function Library(): React.JSX.Element {
|
||||
return (
|
||||
<View></View>
|
||||
)
|
||||
}
|
||||
@@ -5,19 +5,16 @@ import { createStackNavigator } from "@react-navigation/stack";
|
||||
import ServerLibrary from "./helpers/server-library";
|
||||
import { useAuthenticationContext } from "./provider";
|
||||
import { useEffect } from "react";
|
||||
import { useApiClientContext } from "../jellyfin-api-provider";
|
||||
|
||||
export default function Login(): React.JSX.Element {
|
||||
|
||||
const { triggerAuth, setTriggerAuth } = useAuthenticationContext();
|
||||
|
||||
const { server, user } = useApiClientContext();
|
||||
const { user, server, triggerAuth, setTriggerAuth } = useAuthenticationContext();
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
useEffect(() => {
|
||||
setTriggerAuth(false);
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
import React, { useState } from "react";
|
||||
import _ from "lodash";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { MMKVStorageKeys } from "../../../enums/mmkv-storage-keys";
|
||||
import { JellifyServer } from "../../../types/JellifyServer";
|
||||
import { useApiClientContext } from "../../jellyfin-api-provider";
|
||||
import { Spacer, Spinner, View, XStack, ZStack } from "tamagui";
|
||||
import { Spacer, Spinner, XStack, ZStack } from "tamagui";
|
||||
import { SwitchWithLabel } from "../../Global/helpers/switch-with-label";
|
||||
import { H1 } from "../../Global/helpers/text";
|
||||
import Input from "../../Global/helpers/input";
|
||||
import Button from "../../Global/helpers/button";
|
||||
import { http, https } from "../utils/constants";
|
||||
import { storage } from "../../../constants/storage";
|
||||
import { jellifyClient } from "../../../api/client";
|
||||
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";
|
||||
|
||||
export default function ServerAddress(): React.JSX.Element {
|
||||
|
||||
const { setServer } = useApiClientContext();
|
||||
|
||||
const [useHttps, setUseHttps] = useState<boolean>(true);
|
||||
const [serverAddress, setServerAddress] = useState<string | undefined>(undefined);
|
||||
|
||||
const { server, setServer } = useAuthenticationContext();
|
||||
|
||||
const useServerMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
let jellyfin = new Jellyfin(jellifyClient);
|
||||
let jellyfin = new Jellyfin(JellyfinInfo);
|
||||
|
||||
if (!!!serverAddress)
|
||||
throw new Error("Server address was empty");
|
||||
|
||||
let api = jellyfin.createApi(`${useHttps ? https : http}${serverAddress}`);
|
||||
let api = jellyfin.createApi(`${useHttps ? https : http}${serverAddress}`);
|
||||
|
||||
return getSystemApi(api).getPublicSystemInfo();
|
||||
},
|
||||
@@ -41,7 +40,7 @@ export default function ServerAddress(): React.JSX.Element {
|
||||
console.debug("REMOVE THIS::onSuccess variable", publicSystemInfoResponse.data);
|
||||
console.log(`Connected to Jellyfin ${publicSystemInfoResponse.data.Version!}`);
|
||||
|
||||
let jellifyServer: JellifyServer = {
|
||||
const server: JellifyServer = {
|
||||
url: `${useHttps ? https : http}${serverAddress!}`,
|
||||
address: serverAddress!,
|
||||
name: publicSystemInfoResponse.data.ServerName!,
|
||||
@@ -49,11 +48,13 @@ export default function ServerAddress(): React.JSX.Element {
|
||||
startUpComplete: publicSystemInfoResponse.data.StartupWizardCompleted!
|
||||
}
|
||||
|
||||
setServer(jellifyServer);
|
||||
Client.setPublicApiClient(server);
|
||||
setServer(server);
|
||||
},
|
||||
onError: async (error: Error) => {
|
||||
console.error("An error occurred connecting to the Jellyfin instance", error);
|
||||
return storage.set(MMKVStorageKeys.Server, "");
|
||||
Client.signOut();
|
||||
setServer(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useApiClientContext } from "../../jellyfin-api-provider";
|
||||
import _ from "lodash";
|
||||
import { JellyfinCredentials } from "../../../api/types/jellyfin-credentials";
|
||||
import { Spinner, View, YStack, ZStack } from "tamagui";
|
||||
import { Spinner, YStack, ZStack } from "tamagui";
|
||||
import { useAuthenticationContext } from "../provider";
|
||||
import { H1 } from "../../Global/helpers/text";
|
||||
import Button from "../../Global/helpers/button";
|
||||
import Input from "../../Global/helpers/input";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import Client from "../../../api/client";
|
||||
import { JellifyUser } from "../../../types/JellifyUser";
|
||||
|
||||
export default function ServerAuthentication(): React.JSX.Element {
|
||||
const { username, setUsername } = useAuthenticationContext();
|
||||
const [password, setPassword] = React.useState<string | undefined>('');
|
||||
|
||||
const { server, setServer, setUser, apiClient } = useApiClientContext();
|
||||
const [username, setUsername] = useState<string | undefined>(undefined);
|
||||
const [password, setPassword] = React.useState<string | undefined>(undefined);
|
||||
|
||||
const { setUser, server, setServer } = useAuthenticationContext();
|
||||
|
||||
const useApiMutation = useMutation({
|
||||
mutationFn: async (credentials: JellyfinCredentials) => {
|
||||
return await apiClient!.authenticateUserByName(credentials.username, credentials.password!);
|
||||
return await Client.api!.authenticateUserByName(credentials.username, credentials.password!);
|
||||
},
|
||||
onSuccess: async (authResult) => {
|
||||
|
||||
@@ -32,16 +34,20 @@ export default function ServerAuthentication(): React.JSX.Element {
|
||||
if (_.isUndefined(authResult.data.User))
|
||||
return Promise.reject(new Error("Unable to login"));
|
||||
|
||||
console.log(`Successfully signed in to server`)
|
||||
return setUser({
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
Client.setUser(user);
|
||||
return setUser(user);
|
||||
},
|
||||
onError: async (error: Error) => {
|
||||
console.error("An error occurred connecting to the Jellyfin instance", error);
|
||||
return Promise.reject(`An error occured signing into ${server!.name}`);
|
||||
return Promise.reject(`An error occured signing into ${Client.server!.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -50,10 +56,10 @@ export default function ServerAuthentication(): React.JSX.Element {
|
||||
<H1>
|
||||
{ `Sign in to ${server?.name ?? "Jellyfin"}`}
|
||||
</H1>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setServer(undefined);
|
||||
}}>
|
||||
<Button onPress={() => {
|
||||
Client.switchServer()
|
||||
setServer(undefined);
|
||||
}}>
|
||||
Switch Server
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useApiClientContext } from "../../jellyfin-api-provider";
|
||||
import { Spinner, Text, ToggleGroup, View } from "tamagui";
|
||||
import React, { useState } from "react";
|
||||
import { Spinner, Text, ToggleGroup } from "tamagui";
|
||||
import { useAuthenticationContext } from "../provider";
|
||||
import { H1, Label } from "../../Global/helpers/text";
|
||||
import Button from "../../Global/helpers/button";
|
||||
import _ from "lodash";
|
||||
import { useMusicLibraries, usePlaylistLibrary } from "@/api/queries/libraries";
|
||||
import { useMusicLibraries, usePlaylistLibrary } from "../../../api/queries/libraries";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import Client from "../../../api/client";
|
||||
import { useJellifyContext } from "../../../components/provider";
|
||||
|
||||
export default function ServerLibrary(): React.JSX.Element {
|
||||
|
||||
const { libraryId, setLibraryId } = useAuthenticationContext();
|
||||
const { apiClient, setUser, setLibrary } = useApiClientContext();
|
||||
const { setLoggedIn } = useJellifyContext()
|
||||
const { setUser } = useAuthenticationContext();
|
||||
|
||||
const { data : libraries, isError, isPending, refetch: refetchMusicLibraries } = useMusicLibraries(apiClient!);
|
||||
const { data : playlistLibrary, refetch: refetchPlaylistLibrary } = usePlaylistLibrary(apiClient!);
|
||||
const [libraryId, setLibraryId] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
refetchMusicLibraries();
|
||||
refetchPlaylistLibrary();
|
||||
}, [
|
||||
apiClient
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(libraries)
|
||||
}, [
|
||||
libraries
|
||||
])
|
||||
const { data : libraries, isError, isPending, refetch: refetchMusicLibraries } = useMusicLibraries();
|
||||
const { data : playlistLibrary, refetch: refetchPlaylistLibrary } = usePlaylistLibrary();
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
@@ -59,19 +49,22 @@ export default function ServerLibrary(): React.JSX.Element {
|
||||
|
||||
<Button disabled={!!!libraryId}
|
||||
onPress={() => {
|
||||
setLibrary({
|
||||
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={() => setUser(undefined)}>
|
||||
<Button onPress={() => {
|
||||
Client.switchUser();
|
||||
setUser(undefined);
|
||||
}}>
|
||||
Switch User
|
||||
</Button>
|
||||
</SafeAreaView>
|
||||
|
||||
@@ -1,44 +1,36 @@
|
||||
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 {
|
||||
username: string | undefined;
|
||||
setUsername: React.Dispatch<SetStateAction<string | undefined>>;
|
||||
serverAddress: string | undefined;
|
||||
setServerAddress: React.Dispatch<SetStateAction<string | undefined>>;
|
||||
libraryName: string | undefined;
|
||||
setLibraryName: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
libraryId: string | undefined;
|
||||
setLibraryId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
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 [username, setUsername] = useState<string | undefined>(undefined);
|
||||
|
||||
const [useHttp, setUseHttp] = useState<boolean>(false);
|
||||
const [useHttps, setUseHttps] = useState<boolean>(true);
|
||||
const [serverAddress, setServerAddress] = useState<string | undefined>(undefined);
|
||||
|
||||
const [libraryName, setLibraryName] = useState<string | undefined>(undefined);
|
||||
const [libraryId, setLibraryId] = useState<string | undefined>(undefined);
|
||||
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);
|
||||
|
||||
return {
|
||||
username,
|
||||
setUsername,
|
||||
useHttp,
|
||||
setUseHttp,
|
||||
useHttps,
|
||||
setUseHttps,
|
||||
serverAddress,
|
||||
setServerAddress,
|
||||
libraryName,
|
||||
setLibraryName,
|
||||
libraryId,
|
||||
setLibraryId,
|
||||
user,
|
||||
setUser,
|
||||
server,
|
||||
setServer,
|
||||
library,
|
||||
setLibrary,
|
||||
triggerAuth,
|
||||
setTriggerAuth,
|
||||
};
|
||||
@@ -46,14 +38,12 @@ const JellyfinAuthenticationContextInitializer = () => {
|
||||
|
||||
const JellyfinAuthenticationContext =
|
||||
createContext<JellyfinAuthenticationContext>({
|
||||
username: undefined,
|
||||
setUsername: () => {},
|
||||
serverAddress: undefined,
|
||||
setServerAddress: () => {},
|
||||
libraryName: undefined,
|
||||
setLibraryName: () => {},
|
||||
libraryId: undefined,
|
||||
setLibraryId: () => {},
|
||||
user: undefined,
|
||||
setUser: () => {},
|
||||
server: undefined,
|
||||
setServer: () => {},
|
||||
library: undefined,
|
||||
setLibrary: () => {},
|
||||
triggerAuth: true,
|
||||
setTriggerAuth: () => {},
|
||||
});
|
||||
@@ -63,28 +53,24 @@ export const JellyfinAuthenticationProvider: ({ children }: {
|
||||
}) => React.JSX.Element = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const {
|
||||
username,
|
||||
setUsername,
|
||||
serverAddress,
|
||||
setServerAddress,
|
||||
libraryName,
|
||||
setLibraryName,
|
||||
libraryId,
|
||||
setLibraryId,
|
||||
user,
|
||||
setUser,
|
||||
server,
|
||||
setServer,
|
||||
library,
|
||||
setLibrary,
|
||||
triggerAuth,
|
||||
setTriggerAuth,
|
||||
} = JellyfinAuthenticationContextInitializer();
|
||||
|
||||
return (
|
||||
<JellyfinAuthenticationContext.Provider value={{
|
||||
username,
|
||||
setUsername,
|
||||
serverAddress,
|
||||
setServerAddress,
|
||||
libraryName,
|
||||
setLibraryName,
|
||||
libraryId,
|
||||
setLibraryId,
|
||||
user,
|
||||
setUser,
|
||||
server,
|
||||
setServer,
|
||||
library,
|
||||
setLibrary,
|
||||
triggerAuth,
|
||||
setTriggerAuth,
|
||||
}}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { State } from "react-native-track-player";
|
||||
import { Colors } from "react-native/Libraries/NewAppScreen";
|
||||
import { Spinner, View } from "tamagui";
|
||||
import Icon from "../../Global/helpers/icon";
|
||||
import { usePlayerContext } from "@/player/provider";
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
|
||||
export default function PlayPauseButton() : React.JSX.Element {
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { } from "react";
|
||||
import { XStack, YStack } from "tamagui";
|
||||
import { 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 { BlurView } from "@react-native-community/blur";
|
||||
import Icon from "../Global/helpers/icon";
|
||||
import { Text } from "../Global/helpers/text";
|
||||
import { Colors } from "../../enums/colors";
|
||||
@@ -11,21 +10,19 @@ import { CachedImage } from "@georstat/react-native-image-cache";
|
||||
import { ImageType } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { queryConfig } from "../../api/queries/query.config";
|
||||
import { useApiClientContext } from "../jellyfin-api-provider";
|
||||
import TextTicker from 'react-native-text-ticker';
|
||||
import PlayPauseButton from "./helpers/buttons";
|
||||
import { useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import Client from "../../api/client";
|
||||
|
||||
export function Miniplayer({ navigation }: { navigation : NavigationHelpers<ParamListBase, BottomTabNavigationEventMap> }) : React.JSX.Element {
|
||||
|
||||
const { nowPlaying, useSkip } = usePlayerContext();
|
||||
|
||||
const { apiClient } = useApiClientContext();
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
return (
|
||||
<BlurView overlayColor={Colors.Background}>
|
||||
<View style={{ backgroundColor: Colors.Background, borderColor: Colors.Borders }}>
|
||||
{ nowPlaying && (
|
||||
|
||||
<XStack
|
||||
@@ -39,7 +36,7 @@ export function Miniplayer({ navigation }: { navigation : NavigationHelpers<Para
|
||||
alignContent="center"
|
||||
flex={1}>
|
||||
<CachedImage
|
||||
source={getImageApi(apiClient!)
|
||||
source={getImageApi(Client.api!)
|
||||
.getItemImageUrlById(
|
||||
nowPlaying!.item.AlbumId ?? "",
|
||||
ImageType.Primary,
|
||||
@@ -57,8 +54,13 @@ export function Miniplayer({ navigation }: { navigation : NavigationHelpers<Para
|
||||
</YStack>
|
||||
|
||||
|
||||
<YStack alignContent="flex-start" flex={4} maxWidth={"$20"}>
|
||||
<TextTicker
|
||||
<YStack
|
||||
alignContent="flex-start"
|
||||
marginLeft={"$0.5"}
|
||||
flex={4}
|
||||
maxWidth={"$20"}
|
||||
>
|
||||
<TextTicker
|
||||
duration={5000}
|
||||
loop
|
||||
repeatSpacer={20}
|
||||
@@ -91,6 +93,6 @@ export function Miniplayer({ navigation }: { navigation : NavigationHelpers<Para
|
||||
</XStack>
|
||||
</XStack>
|
||||
)}
|
||||
</BlurView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +1,37 @@
|
||||
import { queryConfig } from "@/api/queries/query.config";
|
||||
import { HorizontalSlider } from "@/components/Global/helpers/slider";
|
||||
import { RunTimeSeconds } from "@/components/Global/helpers/time-codes";
|
||||
import { useApiClientContext } from "@/components/jellyfin-api-provider";
|
||||
import { StackParamList } from "@/components/types";
|
||||
import { usePlayerContext } from "@/player/provider";
|
||||
import { CachedImage } from "@georstat/react-native-image-cache";
|
||||
import { ImageType } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { HorizontalSlider } from "../../../components/Global/helpers/slider";
|
||||
import { RunTimeSeconds } from "../../../components/Global/helpers/time-codes";
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import { usePlayerContext } from "../../../player/provider";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import { YStack, XStack, Spacer } from "tamagui";
|
||||
import PlayPauseButton from "../helpers/buttons";
|
||||
import { H5, Text } from "@/components/Global/helpers/text";
|
||||
import Icon from "@/components/Global/helpers/icon";
|
||||
import { Colors } from "@/enums/colors";
|
||||
import { State } from "react-native-track-player";
|
||||
import { H5, Text } from "../../../components/Global/helpers/text";
|
||||
import Icon from "../../../components/Global/helpers/icon";
|
||||
import { Colors } from "../../../enums/colors";
|
||||
import FavoriteButton from "../../Global/components/favorite-button";
|
||||
import BlurhashedImage from "../../../components/Global/helpers/blurhashed-image";
|
||||
|
||||
export default function PlayerScreen({ navigation }: { navigation: NativeStackNavigationProp<StackParamList>}): React.JSX.Element {
|
||||
|
||||
|
||||
const { apiClient } = useApiClientContext();
|
||||
const { playbackState, nowPlaying, progress, useSeekTo, useSkip, usePrevious, queueName } = usePlayerContext();
|
||||
const {
|
||||
useTogglePlayback,
|
||||
playbackState,
|
||||
nowPlayingIsFavorite,
|
||||
setNowPlayingIsFavorite,
|
||||
nowPlaying,
|
||||
progress,
|
||||
useSeekTo,
|
||||
useSkip,
|
||||
usePrevious,
|
||||
queueName,
|
||||
} = usePlayerContext();
|
||||
|
||||
const [seeking, setSeeking] = useState<boolean>(false);
|
||||
const [progressState, setProgressState] = useState<number>(progress!.position);
|
||||
|
||||
const { width, height } = useSafeAreaFrame();
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
// Prevent gesture event to close player if we're seeking
|
||||
useEffect(() => {
|
||||
@@ -55,12 +60,19 @@ export default function PlayerScreen({ navigation }: { navigation: NativeStackNa
|
||||
</YStack>
|
||||
|
||||
<XStack
|
||||
animation={"quick"}
|
||||
justifyContent="center"
|
||||
alignContent="center"
|
||||
minHeight={width / 1.1}
|
||||
onPress={() => {
|
||||
useTogglePlayback.mutate(undefined)
|
||||
}}
|
||||
>
|
||||
<CachedImage
|
||||
source={getImageApi(apiClient!)
|
||||
<BlurhashedImage
|
||||
item={nowPlaying!.item}
|
||||
size={width / 1.1}
|
||||
/>
|
||||
{/* <CachedImage
|
||||
source={getImageApi(Client.api!)
|
||||
.getItemImageUrlById(
|
||||
nowPlaying!.item.AlbumId ?? "",
|
||||
ImageType.Primary,
|
||||
@@ -74,7 +86,7 @@ export default function PlayerScreen({ navigation }: { navigation: NativeStackNa
|
||||
height: playbackState === State.Playing ? width / 1.1 : width / 1.4,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
/> */}
|
||||
</XStack>
|
||||
|
||||
<XStack marginHorizontal={20} paddingVertical={5}>
|
||||
@@ -83,18 +95,18 @@ export default function PlayerScreen({ navigation }: { navigation: NativeStackNa
|
||||
bold
|
||||
fontSize={"$6"}
|
||||
>
|
||||
{nowPlaying?.title ?? "Untitled Track"}
|
||||
{nowPlaying!.title ?? "Untitled Track"}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
fontSize={"$6"}
|
||||
color={Colors.Primary}
|
||||
onPress={() => {
|
||||
navigation.goBack(); // Dismiss player modal
|
||||
navigation.push("Artist", {
|
||||
artistName: nowPlaying!.item.ArtistItems![0].Name ?? "Untitled",
|
||||
artistId: nowPlaying!.item.ArtistItems![0].Id!,
|
||||
})
|
||||
if (nowPlaying!.item.ArtistItems) {
|
||||
navigation.navigate("Artist", {
|
||||
artist: nowPlaying!.item.ArtistItems![0],
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{nowPlaying.artist ?? "Unknown Artist"}
|
||||
@@ -108,9 +120,28 @@ export default function PlayerScreen({ navigation }: { navigation: NativeStackNa
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
<XStack alignItems="center" flex={1}>
|
||||
<XStack
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
flex={1}
|
||||
>
|
||||
{/* Buttons for favorites, song menu go here */}
|
||||
|
||||
<Icon
|
||||
name="dots-horizontal-circle-outline"
|
||||
onPress={() => {
|
||||
navigation.navigate("Details", {
|
||||
item: nowPlaying!.item
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Spacer />
|
||||
|
||||
<FavoriteButton
|
||||
item={nowPlaying!.item}
|
||||
onToggle={() => setNowPlayingIsFavorite(!nowPlayingIsFavorite)}
|
||||
/>
|
||||
</XStack>
|
||||
</XStack>
|
||||
|
||||
@@ -159,7 +190,13 @@ export default function PlayerScreen({ navigation }: { navigation: NativeStackNa
|
||||
<XStack justifyContent="space-evenly" marginVertical={"$3"}>
|
||||
<Icon
|
||||
name="rewind-15"
|
||||
onPress={() => useSeekTo.mutate(progress!.position - 15)}
|
||||
onPress={() => {
|
||||
|
||||
setSeeking(true);
|
||||
setProgressState(progressState - 15);
|
||||
useSeekTo.mutate(progress!.position - 15);
|
||||
setSeeking(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
@@ -178,7 +215,12 @@ export default function PlayerScreen({ navigation }: { navigation: NativeStackNa
|
||||
|
||||
<Icon
|
||||
name="fast-forward-15"
|
||||
onPress={() => useSeekTo.mutate(progress!.position + 15)}
|
||||
onPress={() => {
|
||||
setSeeking(true);
|
||||
setProgressState(progressState + 15);
|
||||
useSeekTo.mutate(progress!.position + 15);
|
||||
setSeeking(false);
|
||||
}}
|
||||
/>
|
||||
</XStack>
|
||||
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import Track from "@/components/Global/components/track";
|
||||
import { usePlayerContext } from "@/player/provider";
|
||||
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 { FlatList } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
|
||||
export default function Queue(): React.JSX.Element {
|
||||
export default function Queue({ navigation }: { navigation: NativeStackNavigationProp<StackParamList>}): React.JSX.Element {
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
const { queue, useSkip, nowPlaying } = usePlayerContext();
|
||||
|
||||
const scrollIndex = queue.findIndex(queueItem => queueItem.item.Id! === nowPlaying!.item.Id!)
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["right", "left"]}>
|
||||
<FlatList
|
||||
data={queue}
|
||||
extraData={nowPlaying}
|
||||
initialScrollIndex={queue.indexOf(nowPlaying!)}
|
||||
getItemLayout={(data, index) => (
|
||||
{ length: width / 9, offset: width / 9 * index, index}
|
||||
)}
|
||||
initialScrollIndex={scrollIndex !== -1 ? scrollIndex: 0}
|
||||
numColumns={1}
|
||||
renderItem={({ item: queueItem, index }) => {
|
||||
return (
|
||||
<Track
|
||||
navigation={navigation}
|
||||
track={queueItem.item}
|
||||
tracklist={queue.map((track) => track.item)}
|
||||
index={index}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createNativeStackNavigator, NativeStackNavigationProp } from "@react-na
|
||||
import { StackParamList } from "../types";
|
||||
import PlayerScreen from "./screens";
|
||||
import Queue from "./screens/queue";
|
||||
import DetailsScreen from "../ItemDetail/screen";
|
||||
|
||||
export const PlayerStack = createNativeStackNavigator<StackParamList>();
|
||||
|
||||
@@ -30,6 +31,14 @@ export default function Player({ navigation }: { navigation: NativeStackNavigati
|
||||
}}
|
||||
/>
|
||||
|
||||
<PlayerStack.Screen
|
||||
name="Details"
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerTitle: ""
|
||||
}}
|
||||
/>
|
||||
|
||||
</PlayerStack.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -2,17 +2,18 @@ import { BaseItemDto, ImageType } from "@jellyfin/sdk/lib/generated-client/model
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../types";
|
||||
import { ScrollView, XStack, YStack } from "tamagui";
|
||||
import { useApiClientContext } from "../jellyfin-api-provider";
|
||||
import { usePlayerContext } from "@/player/provider";
|
||||
import { useItemTracks } from "@/api/queries/tracks";
|
||||
import { usePlayerContext } from "../../player/provider";
|
||||
import { useItemTracks } from "../../api/queries/tracks";
|
||||
import { RunTimeTicks } from "../Global/helpers/time-codes";
|
||||
import { H4, H5, Text } from "../Global/helpers/text";
|
||||
import Track from "../Global/components/track";
|
||||
import { FlatList } from "react-native";
|
||||
import { queryConfig } from "@/api/queries/query.config";
|
||||
import { queryConfig } from "../../api/queries/query.config";
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api/image-api";
|
||||
import { CachedImage } from "@georstat/react-native-image-cache";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { useEffect } from "react";
|
||||
import Client from "../../api/client";
|
||||
|
||||
interface PlaylistProps {
|
||||
playlist: BaseItemDto;
|
||||
@@ -21,18 +22,22 @@ interface PlaylistProps {
|
||||
|
||||
export default function Playlist(props: PlaylistProps): React.JSX.Element {
|
||||
|
||||
const { apiClient, sessionId } = useApiClientContext();
|
||||
const { nowPlaying, nowPlayingIsFavorite } = usePlayerContext();
|
||||
|
||||
const { nowPlaying } = usePlayerContext();
|
||||
const { data: tracks, isLoading, refetch } = useItemTracks(props.playlist.Id!);
|
||||
|
||||
const { data: tracks, isLoading } = useItemTracks(props.playlist.Id!, apiClient!);
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [
|
||||
nowPlayingIsFavorite
|
||||
]);
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["right", "left"]}>
|
||||
<ScrollView contentInsetAdjustmentBehavior="automatic">
|
||||
<YStack alignItems="center">
|
||||
<CachedImage
|
||||
source={getImageApi(apiClient!)
|
||||
source={getImageApi(Client.api!)
|
||||
.getItemImageUrlById(
|
||||
props.playlist.Id!,
|
||||
ImageType.Primary,
|
||||
@@ -56,6 +61,7 @@ export default function Playlist(props: PlaylistProps): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<Track
|
||||
navigation={props.navigation}
|
||||
track={track}
|
||||
tracklist={tracks!}
|
||||
index={index}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Playlist from "@/components/Playlist/component";
|
||||
import { StackParamList } from "@/components/types";
|
||||
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 HomePlaylistScreen({ route, navigation }: {
|
||||
export function PlaylistScreen({ route, navigation }: {
|
||||
route: RouteProp<StackParamList, "Playlist">,
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
@@ -1,20 +1,18 @@
|
||||
import { XStack } from "@tamagui/stacks";
|
||||
import React from "react";
|
||||
import { useApiClientContext } from "../../jellyfin-api-provider";
|
||||
import Avatar from "@/components/Global/helpers/avatar";
|
||||
import { Text } from "@/components/Global/helpers/text";
|
||||
import Icon from "@/components/Global/helpers/icon";
|
||||
import Avatar from "../../../components/Global/helpers/avatar";
|
||||
import { Text } from "../../../components/Global/helpers/text";
|
||||
import Icon from "../../../components/Global/helpers/icon";
|
||||
import Client from "../../../api/client";
|
||||
|
||||
export default function AccountDetails(): React.JSX.Element {
|
||||
|
||||
const { user } = useApiClientContext();
|
||||
|
||||
return (
|
||||
|
||||
<XStack alignItems="center">
|
||||
<Icon name="account-music-outline" />
|
||||
<Text>{user!.name}</Text>
|
||||
<Avatar itemId={user!.id} circular />
|
||||
<Text>{Client.user!.name}</Text>
|
||||
<Avatar itemId={Client.user!.id} circular />
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { Text } from "@/components/Global/helpers/text";
|
||||
import { useApiClientContext } from "@/components/jellyfin-api-provider";
|
||||
import Client from "../../../api/client";
|
||||
import { Text } from "../../../components/Global/helpers/text";
|
||||
import React from "react";
|
||||
import { View } from "tamagui";
|
||||
|
||||
export default function LibraryDetails() : React.JSX.Element {
|
||||
|
||||
const { library } = useApiClientContext();
|
||||
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>{ `LibraryID: ${library!.musicLibraryId}` }</Text>
|
||||
<Text>{ `Playlist LibraryID: ${library!.playlistLibraryId}` }</Text>
|
||||
<Text>{ `LibraryID: ${Client.library!.musicLibraryId}` }</Text>
|
||||
<Text>{ `Playlist LibraryID: ${Client.library!.playlistLibraryId}` }</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,25 @@
|
||||
import React from "react";
|
||||
import { useApiClientContext } from "../../jellyfin-api-provider";
|
||||
import { XStack, YStack } from "tamagui";
|
||||
import Icon from "../../Global/helpers/icon";
|
||||
import { H5, Text } from "@/components/Global/helpers/text";
|
||||
import { H5, Text } from "../../../components/Global/helpers/text";
|
||||
import Client from "../../../api/client";
|
||||
|
||||
export default function ServerDetails() : React.JSX.Element {
|
||||
|
||||
const { apiClient } = useApiClientContext();
|
||||
|
||||
|
||||
return (
|
||||
<YStack>
|
||||
<YStack>
|
||||
<H5>Access Token</H5>
|
||||
<XStack>
|
||||
<Icon name="hand-coin-outline" />
|
||||
<Text>{apiClient!.accessToken}</Text>
|
||||
<Text>{Client.api!.accessToken}</Text>
|
||||
</XStack>
|
||||
</YStack>
|
||||
<YStack>
|
||||
<H5>Jellyfin Server</H5>
|
||||
<XStack>
|
||||
<Icon name="server-network" />
|
||||
<Text>{apiClient!.basePath}</Text>
|
||||
<Text>{Client.api!.basePath}</Text>
|
||||
</XStack>
|
||||
</YStack>
|
||||
</YStack>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import React from "react";
|
||||
import Button from "../../Global/helpers/button";
|
||||
import { useApiClientContext } from "../../jellyfin-api-provider";
|
||||
import { stop } from "react-native-track-player/lib/src/trackPlayer";
|
||||
import Client from "../../../api/client";
|
||||
import { useJellifyContext } from "../../../components/provider";
|
||||
|
||||
export default function SignOut(): React.JSX.Element {
|
||||
|
||||
const { signOut } = useApiClientContext();
|
||||
const { setLoggedIn } = useJellifyContext()
|
||||
|
||||
return (
|
||||
<Button onPress={() => {
|
||||
stop();
|
||||
signOut();
|
||||
Client.signOut();
|
||||
setLoggedIn(false);
|
||||
}}>
|
||||
Sign Out
|
||||
</Button>
|
||||
|
||||
13
components/Settings/screens/account-details.tsx
Normal file
13
components/Settings/screens/account-details.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { StackParamList } from "../../../components/types";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
|
||||
export default function AccountDetails({
|
||||
navigation
|
||||
} : {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
|
||||
return (
|
||||
<AccountDetails navigation={navigation} />
|
||||
)
|
||||
}
|
||||
@@ -1,17 +1,40 @@
|
||||
import React from "react";
|
||||
import { SafeAreaView } from "react-native";
|
||||
import { ScrollView, Separator } from "tamagui";
|
||||
import AccountDetails from "../helpers/account-details";
|
||||
import { ListItem, ScrollView, Separator, YGroup } from "tamagui";
|
||||
import SignOut from "../helpers/sign-out";
|
||||
import ServerDetails from "../helpers/server-details";
|
||||
import LibraryDetails from "../helpers/library-details";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import { StackParamList } from "../../../components/types";
|
||||
|
||||
export default function Root({
|
||||
navigation
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
|
||||
export default function Root() : React.JSX.Element {
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<ScrollView contentInsetAdjustmentBehavior="automatic">
|
||||
<AccountDetails />
|
||||
<Separator marginVertical={15} />
|
||||
<YGroup
|
||||
alignSelf="center"
|
||||
bordered
|
||||
width={240}
|
||||
size="$5"
|
||||
>
|
||||
<YGroup.Item>
|
||||
<ListItem
|
||||
hoverTheme
|
||||
pressTheme
|
||||
title="Account Details"
|
||||
subTitle="Everything is about you, man"
|
||||
onPress={() => {
|
||||
navigation.push("AccountDetails")
|
||||
}}
|
||||
/>
|
||||
</YGroup.Item>
|
||||
</YGroup>
|
||||
|
||||
<ServerDetails />
|
||||
<Separator marginVertical={15} />
|
||||
<LibraryDetails />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import Root from "./screens/root";
|
||||
import AccountDetails from "./screens/account-details";
|
||||
|
||||
export const SettingsStack = createNativeStackNavigator();
|
||||
|
||||
@@ -18,6 +19,17 @@ export default function Settings(): React.JSX.Element {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingsStack.Screen
|
||||
name="Account"
|
||||
component={AccountDetails}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SettingsStack.Navigator>
|
||||
)
|
||||
}
|
||||
41
components/Tracks/component.tsx
Normal file
41
components/Tracks/component.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useFavoriteTracks } from "../../api/queries/favorites";
|
||||
import { StackParamList } from "../types";
|
||||
import { SafeAreaView, useSafeAreaFrame } from "react-native-safe-area-context";
|
||||
import { FlatList, RefreshControl } from "react-native";
|
||||
import Track from "../Global/components/track";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
|
||||
export default function Tracks({ navigation }: { navigation: NativeStackNavigationProp<StackParamList> }) : React.JSX.Element {
|
||||
|
||||
const { data: tracks, refetch, isPending } = useFavoriteTracks();
|
||||
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["right", "left"]}>
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
numColumns={1}
|
||||
data={tracks}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPending}
|
||||
onRefresh={refetch}
|
||||
/>
|
||||
}
|
||||
renderItem={({ index, item: track}) => {
|
||||
return (
|
||||
<Track
|
||||
navigation={navigation}
|
||||
showArtwork
|
||||
track={track}
|
||||
tracklist={tracks?.slice(index, index + 50) ?? []}
|
||||
queueName="Favorite Tracks"
|
||||
/>
|
||||
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
17
components/Tracks/screen.tsx
Normal file
17
components/Tracks/screen.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { RouteProp } from "@react-navigation/native";
|
||||
import { StackParamList } from "../types";
|
||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||
import React from "react";
|
||||
import Tracks from "./component";
|
||||
|
||||
export default function TracksScreen({
|
||||
route,
|
||||
navigation
|
||||
} : {
|
||||
route: RouteProp<StackParamList, "Tracks">,
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}) : React.JSX.Element {
|
||||
return (
|
||||
<Tracks navigation={navigation} />
|
||||
)
|
||||
}
|
||||
13
components/carplay.tsx
Normal file
13
components/carplay.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createStackNavigator } from "@react-navigation/stack"
|
||||
import { NowPlaying } from "./CarPlay/NowPlaying";
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
export default function JellifyCarplay(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen name="NowPlaying" component={NowPlaying} />
|
||||
</Stack.Navigator>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import _ from "lodash";
|
||||
import { JellyfinApiClientProvider, useApiClientContext } from "./jellyfin-api-provider";
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import Navigation from "./navigation";
|
||||
import Login from "./Login/component";
|
||||
@@ -8,28 +7,70 @@ import { JellyfinAuthenticationProvider } from "./Login/provider";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { JellifyDarkTheme, JellifyLightTheme } from "./theme";
|
||||
import { PlayerProvider } from "../player/provider";
|
||||
import { useColorScheme } from "react-native";
|
||||
import { Text, useColorScheme, View } from "react-native";
|
||||
import { PortalProvider } from "tamagui";
|
||||
import Client from "../api/client";
|
||||
import { JellifyProvider, useJellifyContext } from "./provider";
|
||||
import { CarPlay } from "react-native-carplay"
|
||||
import JellifyCarplay from "./carplay";
|
||||
|
||||
export default function Jellify(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<JellyfinApiClientProvider>
|
||||
<App />
|
||||
</JellyfinApiClientProvider>
|
||||
<PortalProvider shouldAddRootHost>
|
||||
<JellifyProvider>
|
||||
<App />
|
||||
</JellifyProvider>
|
||||
</PortalProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function App(): React.JSX.Element {
|
||||
|
||||
// If library hasn't been set, we haven't completed the auth flow
|
||||
const { server, library } = useApiClientContext();
|
||||
|
||||
const isDarkMode = useColorScheme() === "dark";
|
||||
|
||||
const { loggedIn } = useJellifyContext();
|
||||
|
||||
const [carPlayConnected, setCarPlayConnected] = useState(CarPlay.connected);
|
||||
|
||||
return (
|
||||
useEffect(() => {
|
||||
console.debug("Client instance changed")
|
||||
}, [
|
||||
Client.instance
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
function onConnect() {
|
||||
setCarPlayConnected(true)
|
||||
}
|
||||
|
||||
function onDisconnect() {
|
||||
setCarPlayConnected(false)
|
||||
}
|
||||
|
||||
CarPlay.registerOnConnect(onConnect);
|
||||
CarPlay.registerOnDisconnect(onDisconnect);
|
||||
return () => {
|
||||
CarPlay.unregisterOnConnect(onConnect)
|
||||
CarPlay.unregisterOnDisconnect(onDisconnect)
|
||||
};
|
||||
});
|
||||
|
||||
return carPlayConnected ? (
|
||||
<NavigationContainer>
|
||||
{ loggedIn ? (
|
||||
<JellifyCarplay />
|
||||
) : (
|
||||
<View>
|
||||
<Text>Please login in the app before using CarPlay</Text>
|
||||
</View>
|
||||
)}
|
||||
</NavigationContainer>
|
||||
) : (
|
||||
<NavigationContainer theme={isDarkMode ? JellifyDarkTheme : JellifyLightTheme}>
|
||||
<SafeAreaProvider>
|
||||
{ server && library ? (
|
||||
{ loggedIn ? (
|
||||
<PlayerProvider>
|
||||
<Navigation />
|
||||
</PlayerProvider>
|
||||
@@ -40,5 +81,5 @@ function App(): React.JSX.Element {
|
||||
)}
|
||||
</SafeAreaProvider>
|
||||
</NavigationContainer>
|
||||
);
|
||||
)
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import React, { createContext, ReactNode, SetStateAction, useContext, useEffect, useState } from 'react';
|
||||
import { useApi } from '../api/queries';
|
||||
import { isUndefined } from 'lodash';
|
||||
import { storage } from '../constants/storage';
|
||||
import { MMKVStorageKeys } from '../enums/mmkv-storage-keys';
|
||||
import { JellifyServer } from '../types/JellifyServer';
|
||||
import { JellifyLibrary } from '../types/JellifyLibrary';
|
||||
import { JellifyUser } from '../types/JellifyUser';
|
||||
import uuid from 'react-native-uuid';
|
||||
import { buildAuthenticatedApiClient, buildPublicApiClient } from '@/api/client';
|
||||
|
||||
interface JellyfinApiClientContext {
|
||||
apiClient: Api | undefined;
|
||||
sessionId: string;
|
||||
server: JellifyServer | undefined;
|
||||
setServer: React.Dispatch<SetStateAction<JellifyServer | undefined>>;
|
||||
user: JellifyUser | undefined;
|
||||
setUser: React.Dispatch<SetStateAction<JellifyUser | undefined>>;
|
||||
library: JellifyLibrary | undefined;
|
||||
setLibrary: React.Dispatch<SetStateAction<JellifyLibrary | undefined>>;
|
||||
signOut: () => void
|
||||
}
|
||||
|
||||
const JellyfinApiClientContextInitializer = () => {
|
||||
|
||||
const userJson = storage.getString(MMKVStorageKeys.User)
|
||||
const serverJson = storage.getString(MMKVStorageKeys.Server);
|
||||
const libraryJson = storage.getString(MMKVStorageKeys.Library);
|
||||
|
||||
const [sessionId, setSessionId] = useState<string>(uuid.v4())
|
||||
const [user, setUser] = useState<JellifyUser | undefined>(userJson ? (JSON.parse(userJson) as JellifyUser) : undefined);
|
||||
const [server, setServer] = useState<JellifyServer | undefined>(serverJson ? (JSON.parse(serverJson) as JellifyServer) : undefined);
|
||||
const [library, setLibrary] = useState<JellifyLibrary | undefined>(libraryJson ? (JSON.parse(libraryJson) as JellifyLibrary) : undefined);
|
||||
|
||||
const [apiClient, setApiClient] = useState<Api | undefined>(!isUndefined(server) && !isUndefined(user) ? buildAuthenticatedApiClient(server!.url, user!.accessToken) : undefined);
|
||||
|
||||
const signOut = () => {
|
||||
console.debug("Signing out of Jellify");
|
||||
setUser(undefined);
|
||||
setServer(undefined);
|
||||
setLibrary(undefined);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (server && user)
|
||||
setApiClient(buildAuthenticatedApiClient(server.url, user.accessToken));
|
||||
else if (server)
|
||||
setApiClient(buildPublicApiClient(server.url));
|
||||
else
|
||||
setApiClient(undefined);
|
||||
}, [
|
||||
server,
|
||||
user
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (server) {
|
||||
console.debug("Storing new server configuration")
|
||||
storage.set(MMKVStorageKeys.Server, JSON.stringify(server))
|
||||
}
|
||||
else {
|
||||
console.debug("Deleting server configuration from storage");
|
||||
storage.delete(MMKVStorageKeys.Server)
|
||||
}
|
||||
}, [
|
||||
server
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
console.debug("Storing new user profile")
|
||||
storage.set(MMKVStorageKeys.User, JSON.stringify(user));
|
||||
}
|
||||
else {
|
||||
console.debug("Deleting access token from storage");
|
||||
storage.delete(MMKVStorageKeys.User);
|
||||
}
|
||||
}, [
|
||||
user
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
console.debug("Library changed")
|
||||
if (library) {
|
||||
console.debug("Setting library");
|
||||
storage.set(MMKVStorageKeys.Library, JSON.stringify(library));
|
||||
} else
|
||||
storage.delete(MMKVStorageKeys.Library)
|
||||
}, [
|
||||
library
|
||||
])
|
||||
|
||||
return {
|
||||
apiClient,
|
||||
sessionId,
|
||||
server,
|
||||
setServer,
|
||||
user,
|
||||
setUser,
|
||||
library,
|
||||
setLibrary,
|
||||
signOut
|
||||
};
|
||||
}
|
||||
|
||||
export const JellyfinApiClientContext =
|
||||
createContext<JellyfinApiClientContext>({
|
||||
apiClient: undefined,
|
||||
sessionId: "",
|
||||
server: undefined,
|
||||
setServer: () => {},
|
||||
user: undefined,
|
||||
setUser: () => {},
|
||||
library: undefined,
|
||||
setLibrary: () => {},
|
||||
signOut: () => {}
|
||||
});
|
||||
|
||||
export const JellyfinApiClientProvider: ({ children }: {
|
||||
children: ReactNode;
|
||||
}) => React.JSX.Element = ({ children }: { children: ReactNode }) => {
|
||||
const {
|
||||
apiClient,
|
||||
sessionId,
|
||||
server,
|
||||
setServer,
|
||||
user,
|
||||
setUser,
|
||||
library,
|
||||
setLibrary,
|
||||
signOut
|
||||
} = JellyfinApiClientContextInitializer();
|
||||
|
||||
// Add your logic to check if credentials are stored and initialize the API client here.
|
||||
|
||||
return (
|
||||
<JellyfinApiClientContext.Provider value={{
|
||||
apiClient,
|
||||
sessionId,
|
||||
server,
|
||||
setServer,
|
||||
user,
|
||||
setUser,
|
||||
library,
|
||||
setLibrary,
|
||||
signOut
|
||||
}}>
|
||||
{children}
|
||||
</JellyfinApiClientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useApiClientContext = () => useContext(JellyfinApiClientContext)
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import Player from "./Player/component";
|
||||
import Player from "./Player/stack";
|
||||
import { Tabs } from "./tabs";
|
||||
import { StackParamList } from "./types";
|
||||
|
||||
|
||||
49
components/provider.tsx
Normal file
49
components/provider.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import Client from "../api/client";
|
||||
import { isUndefined } from "lodash";
|
||||
import { createContext, ReactNode, useContext, useState } from "react";
|
||||
|
||||
interface JellifyContext {
|
||||
loggedIn: boolean;
|
||||
setLoggedIn: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const JellifyContextInitializer = () => {
|
||||
|
||||
const [loggedIn, setLoggedIn] = useState<boolean>(
|
||||
!isUndefined(Client.api) &&
|
||||
!isUndefined(Client.user) &&
|
||||
!isUndefined(Client.server)
|
||||
);
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
setLoggedIn,
|
||||
}
|
||||
}
|
||||
|
||||
const JellifyContext = createContext<JellifyContext>({
|
||||
loggedIn: false,
|
||||
setLoggedIn: () => {}
|
||||
});
|
||||
|
||||
export const JellifyProvider: ({ children }: {
|
||||
children: ReactNode
|
||||
}) => React.JSX.Element = ({ children }: { children: ReactNode }) => {
|
||||
const {
|
||||
loggedIn,
|
||||
setLoggedIn
|
||||
} = JellifyContextInitializer();
|
||||
|
||||
return (
|
||||
<JellifyContext.Provider
|
||||
value={{
|
||||
loggedIn,
|
||||
setLoggedIn
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</JellifyContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useJellifyContext = () => useContext(JellifyContext);
|
||||
@@ -5,8 +5,8 @@ import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityI
|
||||
import { useColorScheme } from "react-native";
|
||||
import { Colors } from "../enums/colors";
|
||||
import Search from "./Search/component";
|
||||
import Library from "./Library/component";
|
||||
import Settings from "./Settings/component";
|
||||
import Favorites from "./Favorites/component";
|
||||
import Settings from "./Settings/stack";
|
||||
import { Discover } from "./Discover/component";
|
||||
import { Miniplayer } from "./Player/mini-player";
|
||||
import { Separator } from "tamagui";
|
||||
@@ -51,8 +51,8 @@ export function Tabs() : React.JSX.Element {
|
||||
/>
|
||||
|
||||
<Tab.Screen
|
||||
name="Library"
|
||||
component={Library}
|
||||
name="Favorites"
|
||||
component={Favorites}
|
||||
options={{
|
||||
headerShown: false,
|
||||
tabBarIcon: ({color, size }) => (
|
||||
|
||||
@@ -5,7 +5,7 @@ export const JellifyDarkTheme = {
|
||||
colors: {
|
||||
...DarkTheme.colors,
|
||||
card: Colors.Background,
|
||||
border: Colors.Secondary,
|
||||
border: Colors.Borders,
|
||||
background: Colors.Background,
|
||||
primary: Colors.Primary,
|
||||
},
|
||||
|
||||
@@ -4,19 +4,37 @@ import { NativeStackScreenProps } from "@react-navigation/native-stack";
|
||||
|
||||
export type StackParamList = {
|
||||
Home: undefined;
|
||||
|
||||
Discover: undefined;
|
||||
Tabs: undefined,
|
||||
Player: undefined,
|
||||
Queue: undefined,
|
||||
|
||||
Favorites: undefined;
|
||||
Artists: undefined;
|
||||
Albums: undefined;
|
||||
Tracks: undefined;
|
||||
Genres: undefined;
|
||||
Playlists: undefined;
|
||||
|
||||
Search: undefined;
|
||||
|
||||
Settings: undefined;
|
||||
AccountDetails: undefined;
|
||||
|
||||
Tabs: undefined;
|
||||
|
||||
Player: undefined;
|
||||
Queue: undefined;
|
||||
|
||||
Artist: {
|
||||
artistId: string,
|
||||
artistName: string
|
||||
artist: BaseItemDto
|
||||
};
|
||||
Album: {
|
||||
album: BaseItemDto
|
||||
};
|
||||
Playlist: {
|
||||
playlist: BaseItemDto
|
||||
};
|
||||
Details: {
|
||||
item: BaseItemDto
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,4 +52,18 @@ export type HomeAlbumProps = NativeStackScreenProps<StackParamList, 'Album'>;
|
||||
|
||||
export type HomePlaylistProps = NativeStackScreenProps<StackParamList, "Playlist">;
|
||||
|
||||
export type QueueProps = NativeStackScreenProps<StackParamList, "Queue">;
|
||||
export type QueueProps = NativeStackScreenProps<StackParamList, "Queue">;
|
||||
|
||||
export type LibraryProps = NativeStackScreenProps<StackParamList, "Favorites">;
|
||||
|
||||
export type ArtistsProps = NativeStackScreenProps<StackParamList, "Artists">;
|
||||
|
||||
export type AlbumsProps = NativeStackScreenProps<StackParamList, "Albums">;
|
||||
|
||||
export type TracksProps = NativeStackScreenProps<StackParamList, "Tracks">;
|
||||
|
||||
export type GenresProps = NativeStackScreenProps<StackParamList, "Genres">;
|
||||
|
||||
export type DetailsProps = NativeStackScreenProps<StackParamList, "Details">;
|
||||
|
||||
export type AccountDetailsProps = NativeStackScreenProps<StackParamList, "AccountDetails">;
|
||||
@@ -1,7 +1,9 @@
|
||||
export enum Colors {
|
||||
White = "#ffffff",
|
||||
Primary = "#cc2f71",
|
||||
Secondary = "#100538",
|
||||
Black = "#000000",
|
||||
Background = "#070217"
|
||||
Primary = "#cc2f71", // Telemagenta
|
||||
Secondary = "#514C63", // English Violet
|
||||
Borders = "#100538", // Russian Violet
|
||||
Background = "#070217", // Rich Black
|
||||
|
||||
White = "#ffffff", // Uh-huh
|
||||
Black = "#000000", // Yep
|
||||
}
|
||||
@@ -30,4 +30,9 @@ export enum QueryKeys {
|
||||
UserPlaylists = "UserPlaylists",
|
||||
ItemTracks = "ItemTracks",
|
||||
RefreshHome = "RefreshHome",
|
||||
FavoriteArtists = "FavoriteArtists",
|
||||
FavoriteAlbums = "FavoriteAlbums",
|
||||
FavoriteTracks = "FavoriteTracks",
|
||||
UserData = "UserData",
|
||||
UpdatePlayerOptions = "UpdatePlayerOptions",
|
||||
}
|
||||
@@ -7,7 +7,7 @@ const aileronFace = {
|
||||
300: { normal: 'Aileron-Light', italic: 'Aileron Light Italic' },
|
||||
400: { normal: 'Aileron-Regular', italic: 'Aileron Italic'} ,
|
||||
500: { normal: 'Aileron-Regular', italic: 'Aileron Italic' },
|
||||
600: { normal: 'Aileron-SemiBold', italic: 'Aileron SemiBold Italic' },
|
||||
600: { normal: 'Aileron SemiBold', italic: 'Aileron SemiBold Italic' },
|
||||
700: { normal: 'Aileron-Bold', italic: 'Aileron Bold Italic' },
|
||||
800: { normal: 'Aileron-Heavy', italic: 'Aileron Heavy Italic' },
|
||||
900: { normal: 'Aileron-Black', italic: 'Aileron-BlackItalic' }
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,17 +1,19 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { JellifyTrack } from "../types/JellifyTrack";
|
||||
import { TrackType } from "react-native-track-player";
|
||||
import { RatingType, TrackType } from "react-native-track-player";
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { QueuingType } from "../enums/queuing-type";
|
||||
import querystring from "querystring"
|
||||
import { getImageApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import Client from "../api/client";
|
||||
import { isUndefined } from "lodash";
|
||||
|
||||
const container = "opus,mp3,aac,m4a,flac,webma,webm,wav,ogg,mpa,wma";
|
||||
|
||||
// TODO: Make this configurable
|
||||
const transcodingContainer = "m4a";
|
||||
|
||||
export function mapDtoToTrack(api: Api, sessionId: string, item: BaseItemDto, queuingType?: QueuingType) {
|
||||
export function mapDtoToTrack(item: BaseItemDto, queuingType?: QueuingType) : JellifyTrack {
|
||||
|
||||
const urlParams = {
|
||||
"Container": container,
|
||||
@@ -19,23 +21,26 @@ export function mapDtoToTrack(api: Api, sessionId: string, item: BaseItemDto, qu
|
||||
"TranscodingProtocol": "hls",
|
||||
"EnableRemoteMedia": true,
|
||||
"EnableRedirection": true,
|
||||
"api_key": api.accessToken,
|
||||
"api_key": Client.api!.accessToken,
|
||||
"StartTimeTicks": 0,
|
||||
"PlaySessionId": sessionId,
|
||||
"PlaySessionId": Client.sessionId,
|
||||
}
|
||||
|
||||
const isFavorite = !isUndefined(item.UserData) && (item.UserData.IsFavorite ?? false);
|
||||
|
||||
return {
|
||||
url: `${api.basePath}/Audio/${item.Id!}/universal?${querystring.stringify(urlParams)}`,
|
||||
url: `${Client.api!.basePath}/Audio/${item.Id!}/universal?${querystring.stringify(urlParams)}`,
|
||||
type: TrackType.HLS,
|
||||
headers: {
|
||||
"X-Emby-Token": api.accessToken
|
||||
"X-Emby-Token": Client.api!.accessToken
|
||||
},
|
||||
title: item.Name,
|
||||
album: item.Album,
|
||||
artist: item.Artists?.join(", "),
|
||||
duration: item.RunTimeTicks,
|
||||
artwork: getImageApi(api).getItemImageUrlById(item.Id!),
|
||||
artwork: getImageApi(Client.api!).getItemImageUrlById(item.Id!),
|
||||
|
||||
rating: isFavorite ? RatingType.Heart : undefined,
|
||||
item,
|
||||
QueuingType: queuingType ?? QueuingType.DirectlyQueued
|
||||
} as JellifyTrack
|
||||
|
||||
4
index.js
4
index.js
@@ -4,6 +4,8 @@ import App from './App';
|
||||
import {name as appName} from './app.json';
|
||||
import { PlaybackService } from './player/service'
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import Client from './api/client';
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
TrackPlayer.registerPlaybackService(() => PlaybackService);
|
||||
TrackPlayer.registerPlaybackService(() => PlaybackService);
|
||||
Client.instance;
|
||||
63
ios/AppDelegate.swift
Normal file
63
ios/AppDelegate.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
// ios/AppDelegate.swift
|
||||
import UIKit
|
||||
import CarPlay
|
||||
import React
|
||||
#if DEBUG
|
||||
#if FB_SONARKIT_ENABLED
|
||||
import FlipperKit
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, RCTBridgeDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
var bridge: RCTBridge?;
|
||||
var rootView: RCTRootView?;
|
||||
|
||||
static var shared: AppDelegate { return UIApplication.shared.delegate as! AppDelegate }
|
||||
|
||||
func sourceURL(for bridge: RCTBridge!) -> URL! {
|
||||
#if DEBUG
|
||||
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index");
|
||||
#else
|
||||
return Bundle.main.url(forResource:"main", withExtension:"jsbundle")
|
||||
#endif
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
initializeFlipper(with: application)
|
||||
self.bridge = RCTBridge.init(delegate: self, launchOptions: launchOptions)
|
||||
self.rootView = RCTRootView.init(bridge: self.bridge!, moduleName: "Jellify", initialProperties: nil)
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
if (connectingSceneSession.role == UISceneSession.Role.carTemplateApplication) {
|
||||
let scene = UISceneConfiguration(name: "CarPlay", sessionRole: connectingSceneSession.role)
|
||||
scene.delegateClass = CarSceneDelegate.self
|
||||
return scene
|
||||
} else {
|
||||
let scene = UISceneConfiguration(name: "Phone", sessionRole: connectingSceneSession.role)
|
||||
scene.delegateClass = PhoneSceneDelegate.self
|
||||
return scene
|
||||
}
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||
}
|
||||
|
||||
private func initializeFlipper(with application: UIApplication) {
|
||||
#if DEBUG
|
||||
#if FB_SONARKIT_ENABLED
|
||||
let client = FlipperClient.shared()
|
||||
let layoutDescriptorMapper = SKDescriptorMapper(defaults: ())
|
||||
client?.add(FlipperKitLayoutPlugin(rootNode: application, with: layoutDescriptorMapper!))
|
||||
client?.add(FKUserDefaultsPlugin(suiteName: nil))
|
||||
client?.add(FlipperKitReactPlugin())
|
||||
client?.add(FlipperKitNetworkPlugin(networkAdapter: SKIOSNetworkAdapter()))
|
||||
client?.start()
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
14
ios/CarScene.swift
Normal file
14
ios/CarScene.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
// ios/CarScene.swift
|
||||
import Foundation
|
||||
import CarPlay
|
||||
|
||||
class CarSceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
|
||||
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
|
||||
didConnect interfaceController: CPInterfaceController) {
|
||||
RNCarPlay.connect(with: interfaceController, window: templateApplicationScene.carWindow);
|
||||
}
|
||||
|
||||
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController) {
|
||||
RNCarPlay.disconnect()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
#import <Expo/Expo.h>
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
#import "RNCarPlay.h"
|
||||
|
||||
#ifdef DEBUG
|
||||
#ifdef FB_SONARKIT_ENABLED
|
||||
#import <FlipperKit/FlipperClient.h>
|
||||
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
|
||||
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
|
||||
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
|
||||
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
|
||||
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
|
||||
#endif
|
||||
#endif
|
||||
@@ -8,14 +8,11 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
00E356F31AD99517003FC87E /* JellifyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* JellifyTests.m */; };
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
172B159DEECEA03B59A47B38 /* Pods_Jellify.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38B09B5C902A2F3CC48BD0A0 /* Pods_Jellify.framework */; };
|
||||
217EBE16A3E8C5FBF476C905 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F757EB73303E0AC21EF34F64 /* PrivacyInfo.xcprivacy */; };
|
||||
65C2C865DA10C69EF1958E92 /* libPods-Jellify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 132610DE1F5A5CBDAB889AF5 /* libPods-Jellify.a */; };
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||
8AC3ECB3A690B29771C29318 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6259C8F5B99EE317280940 /* ExpoModulesProvider.swift */; };
|
||||
9FE4A57E31DB40AA046B375D /* libPods-Jellify-JellifyTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 172599E13E36ADBF5E44D55E /* libPods-Jellify-JellifyTests.a */; };
|
||||
881EBE3FE9362EB8E8594837 /* Pods_Jellify_JellifyTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E68D0EA500FA5AB01EC4D6E6 /* Pods_Jellify_JellifyTests.framework */; };
|
||||
CF620D0C2CF2BB210045E433 /* Aileron-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = CF620CFC2CF2BB1F0045E433 /* Aileron-Italic.otf */; };
|
||||
CF620D0D2CF2BB210045E433 /* Aileron-Thin.otf in Resources */ = {isa = PBXBuildFile; fileRef = CF620CFD2CF2BB1F0045E433 /* Aileron-Thin.otf */; };
|
||||
CF620D0E2CF2BB210045E433 /* Aileron-HeavyItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = CF620CFE2CF2BB1F0045E433 /* Aileron-HeavyItalic.otf */; };
|
||||
@@ -33,7 +30,9 @@
|
||||
CF620D1A2CF2BB210045E433 /* Aileron-ThinItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = CF620D0A2CF2BB200045E433 /* Aileron-ThinItalic.otf */; };
|
||||
CF620D1B2CF2BB210045E433 /* Aileron-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = CF620D0B2CF2BB200045E433 /* Aileron-SemiBold.otf */; };
|
||||
CF71790B2CBC486C0021BCA3 /* dummy-rntp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF71790A2CBC486C0021BCA3 /* dummy-rntp.swift */; };
|
||||
FD44E03C057499699671A391 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2736241344FDDCC9D455D9 /* ExpoModulesProvider.swift */; };
|
||||
CF98CA472D3E99E0003D88B7 /* CarScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF98CA452D3E99DF003D88B7 /* CarScene.swift */; };
|
||||
CF98CA482D3E99E0003D88B7 /* PhoneScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF98CA462D3E99DF003D88B7 /* PhoneScene.swift */; };
|
||||
CF98CA492D3E99E0003D88B7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF98CA442D3E99DF003D88B7 /* AppDelegate.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -50,16 +49,11 @@
|
||||
00E356EE1AD99517003FC87E /* JellifyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JellifyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
00E356F21AD99517003FC87E /* JellifyTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JellifyTests.m; sourceTree = "<group>"; };
|
||||
132610DE1F5A5CBDAB889AF5 /* libPods-Jellify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Jellify.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07F961A680F5B00A75B9A /* Jellify.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Jellify.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Jellify/AppDelegate.h; sourceTree = "<group>"; };
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Jellify/AppDelegate.mm; sourceTree = "<group>"; };
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Jellify/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Jellify/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Jellify/main.m; sourceTree = "<group>"; };
|
||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = Jellify/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
172599E13E36ADBF5E44D55E /* libPods-Jellify-JellifyTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Jellify-JellifyTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1E2736241344FDDCC9D455D9 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Jellify-JellifyTests/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||
38B09B5C902A2F3CC48BD0A0 /* Pods_Jellify.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Jellify.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B6622EAB81CD73F7F18C4A4 /* Pods-Jellify.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.debug.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
47F8F23DBB1E6482816F1E11 /* Pods-Jellify-JellifyTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify-JellifyTests.debug.xcconfig"; path = "Target Support Files/Pods-Jellify-JellifyTests/Pods-Jellify-JellifyTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
61D59F7A2E3DAFA5DF396506 /* Pods-Jellify-JellifyTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify-JellifyTests.release.xcconfig"; path = "Target Support Files/Pods-Jellify-JellifyTests/Pods-Jellify-JellifyTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
@@ -83,8 +77,11 @@
|
||||
CF6588752D25C12400AECE18 /* Jellify.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Jellify.entitlements; path = Jellify/Jellify.entitlements; sourceTree = "<group>"; };
|
||||
CF7179092CBC486C0021BCA3 /* Jellify-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Jellify-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
CF71790A2CBC486C0021BCA3 /* dummy-rntp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "dummy-rntp.swift"; sourceTree = "<group>"; };
|
||||
CF98CA442D3E99DF003D88B7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
CF98CA452D3E99DF003D88B7 /* CarScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarScene.swift; sourceTree = "<group>"; };
|
||||
CF98CA462D3E99DF003D88B7 /* PhoneScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneScene.swift; sourceTree = "<group>"; };
|
||||
D0DF149772F72EE36A16DED9 /* Pods-Jellify.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.release.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.release.xcconfig"; sourceTree = "<group>"; };
|
||||
DA6259C8F5B99EE317280940 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Jellify/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||
E68D0EA500FA5AB01EC4D6E6 /* Pods_Jellify_JellifyTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Jellify_JellifyTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
F757EB73303E0AC21EF34F64 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Jellify/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -94,7 +91,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9FE4A57E31DB40AA046B375D /* libPods-Jellify-JellifyTests.a in Frameworks */,
|
||||
881EBE3FE9362EB8E8594837 /* Pods_Jellify_JellifyTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -102,7 +99,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
65C2C865DA10C69EF1958E92 /* libPods-Jellify.a in Frameworks */,
|
||||
172B159DEECEA03B59A47B38 /* Pods_Jellify.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -129,13 +126,13 @@
|
||||
13B07FAE1A68108700A75B9A /* Jellify */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CF98CA442D3E99DF003D88B7 /* AppDelegate.swift */,
|
||||
CF98CA452D3E99DF003D88B7 /* CarScene.swift */,
|
||||
CF98CA462D3E99DF003D88B7 /* PhoneScene.swift */,
|
||||
CF6588752D25C12400AECE18 /* Jellify.entitlements */,
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */,
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
|
||||
13B07FB71A68108700A75B9A /* main.m */,
|
||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
|
||||
F757EB73303E0AC21EF34F64 /* PrivacyInfo.xcprivacy */,
|
||||
CF71790A2CBC486C0021BCA3 /* dummy-rntp.swift */,
|
||||
@@ -148,8 +145,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
132610DE1F5A5CBDAB889AF5 /* libPods-Jellify.a */,
|
||||
172599E13E36ADBF5E44D55E /* libPods-Jellify-JellifyTests.a */,
|
||||
38B09B5C902A2F3CC48BD0A0 /* Pods_Jellify.framework */,
|
||||
E68D0EA500FA5AB01EC4D6E6 /* Pods_Jellify_JellifyTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -186,7 +183,6 @@
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||
BBD78D7AC51CEA395F1C20DB /* Pods */,
|
||||
FD77E19D77A9C2B467098F57 /* ExpoModulesProviders */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
@@ -202,22 +198,6 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8416CE4219E7CC11004894B4 /* JellifyTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1E2736241344FDDCC9D455D9 /* ExpoModulesProvider.swift */,
|
||||
);
|
||||
name = JellifyTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
89BACC168326BC5320E81D79 /* Jellify */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DA6259C8F5B99EE317280940 /* ExpoModulesProvider.swift */,
|
||||
);
|
||||
name = Jellify;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -229,15 +209,6 @@
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD77E19D77A9C2B467098F57 /* ExpoModulesProviders */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
89BACC168326BC5320E81D79 /* Jellify */,
|
||||
8416CE4219E7CC11004894B4 /* JellifyTests */,
|
||||
);
|
||||
name = ExpoModulesProviders;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -246,7 +217,6 @@
|
||||
buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "JellifyTests" */;
|
||||
buildPhases = (
|
||||
1ECAB9805E862FBBEC2A87E4 /* [CP] Check Pods Manifest.lock */,
|
||||
D7C145763168E210648CCBD4 /* [Expo] Configure project */,
|
||||
00E356EA1AD99517003FC87E /* Sources */,
|
||||
00E356EB1AD99517003FC87E /* Frameworks */,
|
||||
00E356EC1AD99517003FC87E /* Resources */,
|
||||
@@ -268,7 +238,6 @@
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Jellify" */;
|
||||
buildPhases = (
|
||||
307C2C88C87D3ECD7996604B /* [CP] Check Pods Manifest.lock */,
|
||||
9B9571B1B8AF79E561D2815F /* [Expo] Configure project */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
@@ -372,7 +341,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli')\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n";
|
||||
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
|
||||
};
|
||||
1ECAB9805E862FBBEC2A87E4 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@@ -469,44 +438,6 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9B9571B1B8AF79E561D2815F /* [Expo] Configure project */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[Expo] Configure project";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Jellify/expo-configure-project.sh\"\n";
|
||||
};
|
||||
D7C145763168E210648CCBD4 /* [Expo] Configure project */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[Expo] Configure project";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Jellify-JellifyTests/expo-configure-project.sh\"\n";
|
||||
};
|
||||
ED95B28BB20B2A35D6318E80 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -532,7 +463,6 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
00E356F31AD99517003FC87E /* JellifyTests.m in Sources */,
|
||||
FD44E03C057499699671A391 /* ExpoModulesProvider.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -540,10 +470,10 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
CF98CA472D3E99E0003D88B7 /* CarScene.swift in Sources */,
|
||||
CF98CA482D3E99E0003D88B7 /* PhoneScene.swift in Sources */,
|
||||
CF98CA492D3E99E0003D88B7 /* AppDelegate.swift in Sources */,
|
||||
CF71790B2CBC486C0021BCA3 /* dummy-rntp.swift in Sources */,
|
||||
8AC3ECB3A690B29771C29318 /* ExpoModulesProvider.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -635,7 +565,8 @@
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
OTHER_SWIFT_FLAGS = "$(inherited)";
|
||||
"OTHER_SWIFT_FLAGS[arch=*]" = "DEBUG FB_SONARKIT_ENABLED";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cosmonautical.jellify;
|
||||
PRODUCT_NAME = Jellify;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Jellify-Bridging-Header.h";
|
||||
@@ -666,7 +597,7 @@
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
OTHER_SWIFT_FLAGS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cosmonautical.jellify;
|
||||
PRODUCT_NAME = Jellify;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Jellify-Bridging-Header.h";
|
||||
@@ -726,6 +657,17 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
||||
);
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD = "";
|
||||
LDPLUSPLUS = "";
|
||||
@@ -748,7 +690,10 @@
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
@@ -801,6 +746,17 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
||||
);
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD = "";
|
||||
LDPLUSPLUS = "";
|
||||
@@ -822,7 +778,10 @@
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#import <RCTAppDelegate.h>
|
||||
#import <Expo/Expo.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AppDelegate : EXAppDelegateWrapper
|
||||
|
||||
@end
|
||||
@@ -1,31 +0,0 @@
|
||||
#import "AppDelegate.h"
|
||||
|
||||
#import <React/RCTBundleURLProvider.h>
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
self.moduleName = @"Jellify";
|
||||
// You can add your custom initial props in the dictionary below.
|
||||
// They will be passed down to the ViewController used by React Native.
|
||||
self.initialProps = @{};
|
||||
|
||||
return [super application:application didFinishLaunchingWithOptions:launchOptions];
|
||||
}
|
||||
|
||||
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
|
||||
{
|
||||
return [self bundleURL];
|
||||
}
|
||||
|
||||
- (NSURL *)bundleURL
|
||||
{
|
||||
#if DEBUG
|
||||
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
|
||||
#else
|
||||
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
63
ios/Jellify/AppDelegate.swift
Normal file
63
ios/Jellify/AppDelegate.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
// ios/AppDelegate.swift
|
||||
import UIKit
|
||||
import CarPlay
|
||||
import React
|
||||
#if DEBUG
|
||||
#if FB_SONARKIT_ENABLED
|
||||
import FlipperKit
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, RCTBridgeDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
var bridge: RCTBridge?;
|
||||
var rootView: RCTRootView?;
|
||||
|
||||
static var shared: AppDelegate { return UIApplication.shared.delegate as! AppDelegate }
|
||||
|
||||
func sourceURL(for bridge: RCTBridge!) -> URL! {
|
||||
#if DEBUG
|
||||
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index");
|
||||
#else
|
||||
return Bundle.main.url(forResource:"main", withExtension:"jsbundle")
|
||||
#endif
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
initializeFlipper(with: application)
|
||||
self.bridge = RCTBridge.init(delegate: self, launchOptions: launchOptions)
|
||||
self.rootView = RCTRootView.init(bridge: self.bridge!, moduleName: "Jellify", initialProperties: nil)
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
if (connectingSceneSession.role == UISceneSession.Role.carTemplateApplication) {
|
||||
let scene = UISceneConfiguration(name: "CarPlay", sessionRole: connectingSceneSession.role)
|
||||
scene.delegateClass = CarSceneDelegate.self
|
||||
return scene
|
||||
} else {
|
||||
let scene = UISceneConfiguration(name: "Phone", sessionRole: connectingSceneSession.role)
|
||||
scene.delegateClass = PhoneSceneDelegate.self
|
||||
return scene
|
||||
}
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||
}
|
||||
|
||||
private func initializeFlipper(with application: UIApplication) {
|
||||
#if DEBUG
|
||||
#if FB_SONARKIT_ENABLED
|
||||
let client = FlipperClient.shared()
|
||||
let layoutDescriptorMapper = SKDescriptorMapper(defaults: ())
|
||||
client?.add(FlipperKitLayoutPlugin(rootNode: application, with: layoutDescriptorMapper!))
|
||||
client?.add(FKUserDefaultsPlugin(suiteName: nil))
|
||||
client?.add(FlipperKitReactPlugin())
|
||||
client?.add(FlipperKitNetworkPlugin(networkAdapter: SKIOSNetworkAdapter()))
|
||||
client?.start()
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
14
ios/Jellify/CarScene.swift
Normal file
14
ios/Jellify/CarScene.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
// ios/CarScene.swift
|
||||
import Foundation
|
||||
import CarPlay
|
||||
|
||||
class CarSceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
|
||||
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
|
||||
didConnect interfaceController: CPInterfaceController) {
|
||||
RNCarPlay.connect(with: interfaceController, window: templateApplicationScene.carWindow);
|
||||
}
|
||||
|
||||
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController) {
|
||||
RNCarPlay.disconnect()
|
||||
}
|
||||
}
|
||||
@@ -90,5 +90,35 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>CPTemplateApplicationSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>CPTemplateApplicationScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>CarPlay</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).CarSceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Phone</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).PhoneSceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user