mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-21 13:30:11 -06:00
Enhancements to Offline (#279)
This commit is contained in:
2
.github/workflows/publish-beta.yml
vendored
2
.github/workflows/publish-beta.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
run: npm run pod:install
|
||||
|
||||
- name: ➕ Version Up
|
||||
run: npx react-native bump-version --type patch
|
||||
run: npx react-native bump-version --type minor
|
||||
|
||||
|
||||
- name: 💬 Echo package.json version to Github ENV
|
||||
|
||||
@@ -1 +1 @@
|
||||
npx lint-staged
|
||||
yarn lint-staged
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import React from 'react'
|
||||
import { getToken, getTokens, Spacer, Theme, useTheme, XStack, YStack } from 'tamagui'
|
||||
import { getToken, getTokens, Theme, useTheme, XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../helpers/text'
|
||||
import { RunTimeTicks } from '../helpers/time-codes'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import Icon from '../helpers/icon'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
@@ -14,10 +13,9 @@ import FavoriteIcon from './favorite-icon'
|
||||
import { Image } from 'expo-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../../api/client'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { getAudioCache } from '../../../components/Network/offlineModeUtils'
|
||||
import { networkStatusTypes } from '../../../components/Network/internetConnectionWatcher'
|
||||
import { useNetworkContext } from '../../../components/Network/provider'
|
||||
|
||||
interface TrackProps {
|
||||
track: BaseItemDto
|
||||
@@ -52,10 +50,11 @@ export default function Track({
|
||||
}: TrackProps): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
const { nowPlaying, playQueue, usePlayNewQueue, usePlayNewQueueOffline } = usePlayerContext()
|
||||
const { data: networkStatus } = useQuery({ queryKey: [QueryKeys.NetworkStatus] })
|
||||
const { downloadedTracks, networkStatus } = useNetworkContext()
|
||||
|
||||
const isPlaying = nowPlaying?.item.Id === track.Id
|
||||
|
||||
const offlineAudio = getAudioCache().find((t) => t.item.Id === track.Id)
|
||||
const offlineAudio = downloadedTracks?.find((t) => t.item.Id === track.Id)
|
||||
const isDownloaded = offlineAudio?.item?.Id
|
||||
|
||||
const isOffline = networkStatus === networkStatusTypes.DISCONNECTED
|
||||
|
||||
@@ -3,6 +3,7 @@ import { StackParamList } from '../../../components/types'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import {
|
||||
Circle,
|
||||
getToken,
|
||||
getTokens,
|
||||
ListItem,
|
||||
@@ -17,7 +18,7 @@ import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import IconButton from '../../../components/Global/helpers/icon-button'
|
||||
import { Text } from '../../../components/Global/helpers/text'
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { AddToPlaylistMutation } from '../types'
|
||||
import { addToPlaylist } from '../../../api/mutations/functions/playlists'
|
||||
@@ -31,8 +32,7 @@ import * as Burnt from 'burnt'
|
||||
import { Image } from 'expo-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../../api/client'
|
||||
import { mapDtoToTrack } from '../../../helpers/mappings'
|
||||
import { getAudioCache, saveAudio } from '../../../components/Network/offlineModeUtils'
|
||||
import { useNetworkContext } from '../../../components/Network/provider'
|
||||
|
||||
interface TrackOptionsProps {
|
||||
track: BaseItemDto
|
||||
@@ -54,22 +54,14 @@ export default function TrackOptions({
|
||||
queryFn: () => fetchItem(track.AlbumId!),
|
||||
})
|
||||
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const jellifyTrack = mapDtoToTrack(track)
|
||||
const { useDownload, useRemoveDownload, downloadedTracks } = useNetworkContext()
|
||||
|
||||
const onDownloadPress = async () => {
|
||||
setIsDownloading(true)
|
||||
await saveAudio(jellifyTrack, queryClient, true)
|
||||
setIsDownloading(false)
|
||||
}
|
||||
|
||||
const isDownloaded = !!getAudioCache().find((t) => t.item.Id === track.Id)?.item?.Id
|
||||
const isDownloaded = downloadedTracks?.find((t) => t.item.Id === track.Id)?.item?.Id
|
||||
|
||||
const {
|
||||
data: playlists,
|
||||
isPending: playlistsFetchPending,
|
||||
isSuccess: playlistsFetchSuccess,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.UserPlaylists],
|
||||
queryFn: () => fetchUserPlaylists(),
|
||||
@@ -169,18 +161,23 @@ export default function TrackOptions({
|
||||
}}
|
||||
size={width / 6}
|
||||
/>
|
||||
|
||||
{useDownload.isPending ? (
|
||||
<Circle size={width / 6} disabled>
|
||||
<Spinner marginHorizontal={10} size='small' color={'$amethyst'} />
|
||||
</Circle>
|
||||
) : (
|
||||
<IconButton
|
||||
disabled={isDownloaded || isDownloading}
|
||||
disabled={!!isDownloaded}
|
||||
circular
|
||||
name={isDownloaded ? 'check' : 'download'}
|
||||
title={
|
||||
isDownloaded ? 'Downloaded' : isDownloading ? 'Downloading...' : 'Download'
|
||||
}
|
||||
name={isDownloaded ? 'delete' : 'download'}
|
||||
title={isDownloaded ? 'Remove Download' : 'Download'}
|
||||
onPress={() => {
|
||||
onDownloadPress()
|
||||
(isDownloaded ? useRemoveDownload : useDownload).mutate(track)
|
||||
}}
|
||||
size={width / 6}
|
||||
/>
|
||||
)}
|
||||
</XStack>
|
||||
|
||||
<Spacer />
|
||||
|
||||
@@ -2,7 +2,7 @@ import NetInfo from '@react-native-community/netinfo'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
import { YStack } from 'tamagui'
|
||||
import { getTokenValue, YStack } from 'tamagui'
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
@@ -35,7 +35,10 @@ const InternetConnectionWatcher = () => {
|
||||
const opacity = useSharedValue(0)
|
||||
|
||||
const animateBannerIn = () => {
|
||||
bannerHeight.value = withTiming(40, { duration: 300, easing: Easing.out(Easing.ease) })
|
||||
bannerHeight.value = withTiming(getTokenValue('$8'), {
|
||||
duration: 300,
|
||||
easing: Easing.out(Easing.ease),
|
||||
})
|
||||
opacity.value = withTiming(1, { duration: 300 })
|
||||
}
|
||||
|
||||
@@ -110,7 +113,7 @@ const InternetConnectionWatcher = () => {
|
||||
return (
|
||||
<Animated.View style={[{ overflow: 'hidden' }, animatedStyle]}>
|
||||
<YStack
|
||||
height={'$4'}
|
||||
height={'$1.5'}
|
||||
justifyContent='center'
|
||||
alignContent='center'
|
||||
backgroundColor={
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { MMKV } from 'react-native-mmkv'
|
||||
|
||||
import RNFS from 'react-native-fs'
|
||||
import { JellifyTrack } from '@/types/JellifyTrack'
|
||||
import { JellifyTrack } from '../../types/JellifyTrack'
|
||||
import axios from 'axios'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { JellifyDownload } from '../../types/JellifyDownload'
|
||||
import DownloadProgress from '../../types/DownloadProgress'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
export async function downloadJellyfinFile(
|
||||
url: string,
|
||||
@@ -29,8 +31,7 @@ export async function downloadJellyfinFile(
|
||||
const fileName = `${name}.${extension}`
|
||||
const downloadDest = `${RNFS.DocumentDirectoryPath}/${fileName}`
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
queryClient.setQueryData(['downloads'], (prev: any = {}) => ({
|
||||
queryClient.setQueryData(['downloads'], (prev: DownloadProgress) => ({
|
||||
...prev,
|
||||
[url]: { progress: 0, name: fileName, songName: songName },
|
||||
}))
|
||||
@@ -47,8 +48,7 @@ export async function downloadJellyfinFile(
|
||||
progress: (data: any) => {
|
||||
const percent = +(data.bytesWritten / data.contentLength).toFixed(2)
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
queryClient.setQueryData(['downloads'], (prev: any = {}) => ({
|
||||
queryClient.setQueryData(['downloads'], (prev: DownloadProgress) => ({
|
||||
...prev,
|
||||
[url]: { progress: percent, name: fileName, songName: songName },
|
||||
}))
|
||||
@@ -59,6 +59,7 @@ export async function downloadJellyfinFile(
|
||||
|
||||
const result = await RNFS.downloadFile(options).promise
|
||||
console.log('Download complete:', result)
|
||||
|
||||
return `file://${downloadDest}`
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error)
|
||||
@@ -125,6 +126,24 @@ export const saveAudio = async (
|
||||
mmkv.set(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE, JSON.stringify(existingArray))
|
||||
}
|
||||
|
||||
export const deleteAudio = async (trackItem: BaseItemDto) => {
|
||||
const downloads = getAudioCache()
|
||||
|
||||
const download = downloads.filter((download) => download.item.Id === trackItem.Id)
|
||||
|
||||
if (download.length === 1) {
|
||||
RNFS.unlink(`${RNFS.DocumentDirectoryPath}/${download[0].item.Id}`)
|
||||
setAudioCache([
|
||||
...downloads.slice(0, downloads.indexOf(download[0])),
|
||||
...downloads.slice(downloads.indexOf(download[0]) + 1, downloads.length - 1),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const setAudioCache = (downloads: JellifyDownload[]) => {
|
||||
mmkv.set(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE, JSON.stringify(downloads))
|
||||
}
|
||||
|
||||
export const getAudioCache = (): JellifyDownload[] => {
|
||||
const existingRaw = mmkv.getString(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE)
|
||||
let existingArray: JellifyDownload[] = []
|
||||
|
||||
141
components/Network/provider.tsx
Normal file
141
components/Network/provider.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React, { createContext, ReactNode, useContext } from 'react'
|
||||
import { JellifyDownload } from '../../types/JellifyDownload'
|
||||
import {
|
||||
QueryObserverResult,
|
||||
RefetchOptions,
|
||||
useMutation,
|
||||
UseMutationResult,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { mapDtoToTrack } from '../../helpers/mappings'
|
||||
import { deleteAudio, getAudioCache, saveAudio } from './offlineModeUtils'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { networkStatusTypes } from './internetConnectionWatcher'
|
||||
import DownloadProgress from '../../types/DownloadProgress'
|
||||
|
||||
interface NetworkContext {
|
||||
useDownload: UseMutationResult<void, Error, BaseItemDto, unknown>
|
||||
useRemoveDownload: UseMutationResult<void, Error, BaseItemDto, unknown>
|
||||
downloadedTracks: JellifyDownload[] | undefined
|
||||
activeDownloads: DownloadProgress[] | undefined
|
||||
networkStatus: networkStatusTypes | undefined
|
||||
}
|
||||
|
||||
const NetworkContextInitializer = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const useDownload = useMutation({
|
||||
mutationFn: (trackItem: BaseItemDto) => {
|
||||
const track = mapDtoToTrack(trackItem)
|
||||
|
||||
return saveAudio(track, queryClient, false)
|
||||
},
|
||||
onSuccess: (data, variables) => {
|
||||
console.debug(`Downloaded ${variables.Id} successfully`)
|
||||
|
||||
refetchDownloadedTracks()
|
||||
return data
|
||||
},
|
||||
})
|
||||
|
||||
const useRemoveDownload = useMutation({
|
||||
mutationFn: (trackItem: BaseItemDto) => deleteAudio(trackItem),
|
||||
onSuccess: (data, { Id }) => {
|
||||
console.debug(`Removed ${Id} from storage`)
|
||||
|
||||
refetchDownloadedTracks()
|
||||
},
|
||||
})
|
||||
|
||||
const { data: networkStatus } = useQuery<networkStatusTypes>({
|
||||
queryKey: [QueryKeys.NetworkStatus],
|
||||
})
|
||||
|
||||
const { data: activeDownloads } = useQuery<DownloadProgress[]>({
|
||||
queryKey: ['downloads'],
|
||||
initialData: [],
|
||||
})
|
||||
|
||||
const { data: downloadedTracks, refetch: refetchDownloadedTracks } = useQuery({
|
||||
queryKey: [QueryKeys.AudioCache],
|
||||
queryFn: getAudioCache,
|
||||
staleTime: 1000 * 60, // 1 minute
|
||||
})
|
||||
|
||||
return {
|
||||
useDownload,
|
||||
useRemoveDownload,
|
||||
activeDownloads,
|
||||
downloadedTracks,
|
||||
networkStatus,
|
||||
}
|
||||
}
|
||||
|
||||
const NetworkContext = createContext<NetworkContext>({
|
||||
useDownload: {
|
||||
mutate: () => {},
|
||||
mutateAsync: async () => {},
|
||||
data: undefined,
|
||||
error: null,
|
||||
variables: undefined,
|
||||
isError: false,
|
||||
isIdle: true,
|
||||
isPaused: false,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
status: 'idle',
|
||||
reset: () => {},
|
||||
context: {},
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
submittedAt: 0,
|
||||
},
|
||||
useRemoveDownload: {
|
||||
mutate: () => {},
|
||||
mutateAsync: async () => {},
|
||||
data: undefined,
|
||||
error: null,
|
||||
variables: undefined,
|
||||
isError: false,
|
||||
isIdle: true,
|
||||
isPaused: false,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
status: 'idle',
|
||||
reset: () => {},
|
||||
context: {},
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
submittedAt: 0,
|
||||
},
|
||||
downloadedTracks: [],
|
||||
activeDownloads: [],
|
||||
networkStatus: networkStatusTypes.ONLINE,
|
||||
})
|
||||
|
||||
export const NetworkContextProvider: ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) => React.JSX.Element = ({ children }: { children: ReactNode }) => {
|
||||
const { useDownload, useRemoveDownload, downloadedTracks, activeDownloads, networkStatus } =
|
||||
NetworkContextInitializer()
|
||||
|
||||
return (
|
||||
<NetworkContext.Provider
|
||||
value={{
|
||||
useDownload,
|
||||
useRemoveDownload,
|
||||
activeDownloads,
|
||||
downloadedTracks,
|
||||
networkStatus,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NetworkContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useNetworkContext = () => useContext(NetworkContext)
|
||||
@@ -1,16 +1,15 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useProgress } from 'react-native-track-player'
|
||||
import { HorizontalSlider } from '../../../components/Global/helpers/slider'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { getToken, XStack, YStack } from 'tamagui'
|
||||
import { XStack, YStack } from 'tamagui'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { RunTimeSeconds } from '../../../components/Global/helpers/time-codes'
|
||||
import { UPDATE_INTERVAL } from '../../../player/config'
|
||||
import { ProgressMultiplier } from '../component.config'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import PlayPauseButton from './buttons'
|
||||
import { useSharedValue } from 'react-native-reanimated'
|
||||
|
||||
const scrubGesture = Gesture.Pan()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { usePlayerContext } from '../../../player/provider'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { SafeAreaView, useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { YStack, XStack, Spacer, getTokens } from 'tamagui'
|
||||
import { Text } from '../../../components/Global/helpers/text'
|
||||
@@ -15,14 +15,6 @@ import Controls from '../helpers/controls'
|
||||
import { Image } from 'expo-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Client from '../../../api/client'
|
||||
import {
|
||||
saveAudio,
|
||||
getAudioCache,
|
||||
purneAudioCache,
|
||||
} from '../../../components/Network/offlineModeUtils'
|
||||
import { useActiveTrack } from 'react-native-track-player'
|
||||
import { ActivityIndicator, Alert } from 'react-native'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
export default function PlayerScreen({
|
||||
navigation,
|
||||
@@ -31,30 +23,8 @@ export default function PlayerScreen({
|
||||
}): React.JSX.Element {
|
||||
const { nowPlayingIsFavorite, setNowPlayingIsFavorite, nowPlaying, queue } = usePlayerContext()
|
||||
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const isDownloaded = getAudioCache().find((item) => item?.item?.Id === nowPlaying?.item.Id)
|
||||
?.item?.Id
|
||||
const activeTrack = useActiveTrack()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const downloadAudio = async (url: string) => {
|
||||
if (!nowPlaying) {
|
||||
return
|
||||
}
|
||||
setIsDownloading(true)
|
||||
await saveAudio(nowPlaying, queryClient)
|
||||
setIsDownloading(false)
|
||||
purneAudioCache()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDownloaded) {
|
||||
downloadAudio(nowPlaying!.url)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={['right', 'left']}>
|
||||
{nowPlaying && (
|
||||
@@ -200,17 +170,8 @@ export default function PlayerScreen({
|
||||
<Icon name='speaker-multiple' />
|
||||
|
||||
<Spacer />
|
||||
{isDownloading ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<Icon
|
||||
name={!isDownloaded ? 'download' : 'check'}
|
||||
onPress={() => {
|
||||
downloadAudio(nowPlaying!.url)
|
||||
}}
|
||||
disabled={isDownloading || !!isDownloaded}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Spacer />
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { View, Text, StyleSheet, Pressable, Alert, FlatList } from 'react-native'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import RNFS from 'react-native-fs'
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons'
|
||||
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
||||
import { deleteAudioCache, getAudioCache } from '../Network/offlineModeUtils'
|
||||
import { deleteAudioCache } from '../Network/offlineModeUtils'
|
||||
import { useNetworkContext } from '../Network/provider'
|
||||
import DownloadProgress from '../../types/DownloadProgress'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
|
||||
// 🔹 Single Download Item with animated progress bar
|
||||
const DownloadItem = ({
|
||||
@@ -41,10 +42,7 @@ export const StorageBar = () => {
|
||||
const [used, setUsed] = useState(0)
|
||||
const [total, setTotal] = useState(1)
|
||||
|
||||
const { data: downloads } = useQuery({
|
||||
queryKey: ['downloads'],
|
||||
initialData: {},
|
||||
})
|
||||
const { downloadedTracks, activeDownloads } = useNetworkContext()
|
||||
|
||||
const usageShared = useSharedValue(0)
|
||||
const percentUsed = used / total
|
||||
@@ -71,8 +69,7 @@ export const StorageBar = () => {
|
||||
}
|
||||
|
||||
const deleteAllDownloads = async () => {
|
||||
const files = getAudioCache()
|
||||
for (const file of files) {
|
||||
for (const file of downloadedTracks ?? []) {
|
||||
await RNFS.unlink(file.url).catch(() => {})
|
||||
}
|
||||
Alert.alert('Deleted', 'All downloads removed.')
|
||||
@@ -84,11 +81,6 @@ export const StorageBar = () => {
|
||||
refreshStats()
|
||||
}, [])
|
||||
|
||||
const downloadList = Object.entries(downloads || {}) as [
|
||||
string,
|
||||
{ name: string; progress: number; songName: string },
|
||||
][]
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Storage Usage */}
|
||||
@@ -101,16 +93,19 @@ export const StorageBar = () => {
|
||||
</View>
|
||||
|
||||
{/* Active Downloads */}
|
||||
{downloadList.length > 0 && (
|
||||
{(activeDownloads ?? []).length > 0 && (
|
||||
<>
|
||||
<Text style={[styles.title, { marginTop: 24 }]}>⬇️ Active Downloads</Text>
|
||||
<FlatList
|
||||
data={downloadList}
|
||||
keyExtractor={([url]) => url}
|
||||
data={activeDownloads}
|
||||
keyExtractor={(download) => download.name}
|
||||
renderItem={({ item }) => {
|
||||
const [url, { name, progress, songName }] = item
|
||||
return (
|
||||
<DownloadItem name={name} progress={progress} fileName={songName} />
|
||||
<DownloadItem
|
||||
name={item.name}
|
||||
progress={item.progress}
|
||||
fileName={item.songName}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
contentContainerStyle={{ paddingBottom: 40 }}
|
||||
@@ -120,7 +115,7 @@ export const StorageBar = () => {
|
||||
|
||||
{/* Delete All Downloads */}
|
||||
<Pressable style={styles.deleteButton} onPress={deleteAllDownloads}>
|
||||
<MaterialIcons name='delete-outline' size={20} color='#ff4d4f' />
|
||||
<Icon name='delete-outline' small color='#ff4d4f' />
|
||||
<Text style={styles.deleteText}> Delete Downloads</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { PortalProvider } from '@tamagui/portal'
|
||||
import { JellifyProvider, useJellifyContext } from './provider'
|
||||
import { ToastProvider } from '@tamagui/toast'
|
||||
import { JellifyUserDataProvider } from './user-data-provider'
|
||||
import { NetworkContextProvider } from './Network/provider'
|
||||
|
||||
export default function Jellify(): React.JSX.Element {
|
||||
return (
|
||||
@@ -28,9 +29,11 @@ function App(): React.JSX.Element {
|
||||
|
||||
return loggedIn ? (
|
||||
<JellifyUserDataProvider>
|
||||
<NetworkContextProvider>
|
||||
<PlayerProvider>
|
||||
<Navigation />
|
||||
</PlayerProvider>
|
||||
</NetworkContextProvider>
|
||||
</JellifyUserDataProvider>
|
||||
) : (
|
||||
<JellyfinAuthenticationProvider>
|
||||
|
||||
@@ -66,4 +66,5 @@ export enum QueryKeys {
|
||||
Audio = 'Audio',
|
||||
RecentlyAdded = 'RecentlyAdded',
|
||||
SimilarItems = 'SimilarItems',
|
||||
AudioCache = 'AudioCache',
|
||||
}
|
||||
|
||||
21787
package-lock.json
generated
21787
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,7 @@ import { markItemPlayed } from '../api/mutations/functions/item'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { SKIP_TO_PREVIOUS_THRESHOLD } from './config'
|
||||
import { useNetworkContext } from '../components/Network/provider'
|
||||
|
||||
interface PlayerContext {
|
||||
initialized: boolean
|
||||
@@ -327,6 +328,7 @@ const PlayerContextInitializer = () => {
|
||||
//#region RNTP Setup
|
||||
|
||||
const { state: playbackState } = usePlaybackState()
|
||||
const { useDownload, downloadedTracks } = useNetworkContext()
|
||||
|
||||
useTrackPlayerEvents(
|
||||
[
|
||||
@@ -364,6 +366,16 @@ const PlayerContextInitializer = () => {
|
||||
nowPlaying!,
|
||||
event,
|
||||
)
|
||||
|
||||
// Cache playing track at 20 seconds if it's not already downloaded
|
||||
if (
|
||||
Math.floor(event.position) === 20 &&
|
||||
downloadedTracks?.filter(
|
||||
(download) => download.item.Id === nowPlaying!.item.Id,
|
||||
).length === 0
|
||||
)
|
||||
useDownload.mutate(nowPlaying!.item)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
5
types/DownloadProgress.ts
Normal file
5
types/DownloadProgress.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default interface DownloadProgress {
|
||||
progress: number
|
||||
name: string
|
||||
songName: string
|
||||
}
|
||||
17
yarn.lock
17
yarn.lock
@@ -4620,14 +4620,6 @@ buffer@^5.4.3, buffer@^5.5.0:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
buffer@^6.0.3:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
|
||||
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
|
||||
dependencies:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.2.1"
|
||||
|
||||
bundle@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bundle/-/bundle-2.1.0.tgz#ab47ccc48eb688f706e6ecd323d01db6854aa25e"
|
||||
@@ -6767,7 +6759,7 @@ iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
ieee754@^1.1.13, ieee754@^1.2.1:
|
||||
ieee754@^1.1.13:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
@@ -9447,11 +9439,6 @@ react-native-draggable-flatlist@^4.0.1:
|
||||
dependencies:
|
||||
"@babel/preset-typescript" "^7.17.12"
|
||||
|
||||
react-native-file-access@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-file-access/-/react-native-file-access-3.1.1.tgz#023fc8a82ccc0e49761a76e87760f674f1695eb0"
|
||||
integrity sha512-4KUpBAsnWJa+AQf1tUbLdHO+1pyiZMTeq3NPf5XOGdz1O5CwIrVkrzl+gkN7ffmUa5JyoYHyXUtwScmA+z0Tlg==
|
||||
|
||||
react-native-fs@^2.20.0:
|
||||
version "2.20.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6"
|
||||
@@ -9489,7 +9476,7 @@ react-native-pager-view@^6.7.0:
|
||||
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.7.0.tgz#88f2520e85f07ce55f3f56c57d6637249a215160"
|
||||
integrity sha512-sutxKiMqBuQrEyt4mLaLNzy8taIC7IuYpxfcwQBXfSYBSSpAa0qE9G1FXlP/iXqTSlFgBXyK7BESsl9umOjECQ==
|
||||
|
||||
react-native-reanimated@^3.17.2:
|
||||
react-native-reanimated@^3.17.4:
|
||||
version "3.17.4"
|
||||
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.17.4.tgz#a8c95ea7c2a089b6ca8f513c7bbff4e450986c7c"
|
||||
integrity sha512-vmkG/N5KZrexHr4v0rZB7ohPVseGVNaCXjGxoRo+NYKgC9+mIZAkg/QIfy9xxfJ73FfTrryO9iYUrxks3ZfKbA==
|
||||
|
||||
Reference in New Issue
Block a user