mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 09:09:29 +01:00
impl cache create/delete with server create/delete
also impl test connection
This commit is contained in:
parent
52223e6979
commit
b7a05ca955
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { LayoutRectangle, Pressable, PressableProps } from 'react-native'
|
import { GestureResponderEvent, LayoutRectangle, Pressable, PressableProps, ViewStyle } from 'react-native'
|
||||||
|
|
||||||
type PressableOpacityProps = PressableProps & {
|
type PressableOpacityProps = PressableProps & {
|
||||||
ripple?: boolean
|
ripple?: boolean
|
||||||
@ -9,10 +9,11 @@ type PressableOpacityProps = PressableProps & {
|
|||||||
|
|
||||||
const PressableOpacity: React.FC<PressableOpacityProps> = props => {
|
const PressableOpacity: React.FC<PressableOpacityProps> = props => {
|
||||||
const [opacity, setOpacity] = useState(1)
|
const [opacity, setOpacity] = useState(1)
|
||||||
|
const [disabledStyle, setDisabledStyle] = useState<ViewStyle>({})
|
||||||
const [dimensions, setDimensions] = useState<LayoutRectangle | undefined>(undefined)
|
const [dimensions, setDimensions] = useState<LayoutRectangle | undefined>(undefined)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
props.disabled === true ? setOpacity(0.3) : setOpacity(1)
|
props.disabled === true ? setDisabledStyle({ opacity: 0.3 }) : setDisabledStyle({})
|
||||||
}, [props.disabled])
|
}, [props.disabled])
|
||||||
|
|
||||||
props = {
|
props = {
|
||||||
@ -20,10 +21,41 @@ const PressableOpacity: React.FC<PressableOpacityProps> = props => {
|
|||||||
unstable_pressDelay: props.unstable_pressDelay === undefined ? 60 : props.unstable_pressDelay,
|
unstable_pressDelay: props.unstable_pressDelay === undefined ? 60 : props.unstable_pressDelay,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onPressIn = useCallback<(event: GestureResponderEvent) => void>(
|
||||||
|
data => {
|
||||||
|
if (props.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOpacity(0.4)
|
||||||
|
props.onPressIn ? props.onPressIn(data) : null
|
||||||
|
},
|
||||||
|
[props],
|
||||||
|
)
|
||||||
|
const onPressOut = useCallback<(event: GestureResponderEvent) => void>(
|
||||||
|
data => {
|
||||||
|
if (props.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOpacity(1)
|
||||||
|
props.onPressOut ? props.onPressOut(data) : null
|
||||||
|
},
|
||||||
|
[props],
|
||||||
|
)
|
||||||
|
const onLongPress = useCallback<(event: GestureResponderEvent) => void>(
|
||||||
|
data => {
|
||||||
|
if (props.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOpacity(1)
|
||||||
|
props.onLongPress ? props.onLongPress(data) : null
|
||||||
|
},
|
||||||
|
[props],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
{...props}
|
{...props}
|
||||||
style={[{ justifyContent: 'center', alignItems: 'center' }, props.style as any, { opacity }]}
|
style={[{ justifyContent: 'center', alignItems: 'center' }, props.style as any, { opacity }, disabledStyle]}
|
||||||
android_ripple={
|
android_ripple={
|
||||||
props.ripple
|
props.ripple
|
||||||
? {
|
? {
|
||||||
@ -34,22 +66,9 @@ const PressableOpacity: React.FC<PressableOpacityProps> = props => {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onLayout={event => setDimensions(event.nativeEvent.layout)}
|
onLayout={event => setDimensions(event.nativeEvent.layout)}
|
||||||
onPressIn={() => {
|
onPressIn={onPressIn}
|
||||||
if (!props.disabled) {
|
onPressOut={onPressOut}
|
||||||
setOpacity(0.4)
|
onLongPress={onLongPress}>
|
||||||
}
|
|
||||||
}}
|
|
||||||
onPressOut={() => {
|
|
||||||
if (!props.disabled) {
|
|
||||||
setOpacity(1)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onLongPress={data => {
|
|
||||||
if (!props.disabled) {
|
|
||||||
setOpacity(1)
|
|
||||||
props.onLongPress ? props.onLongPress(data) : null
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -42,7 +42,7 @@ const styles = StyleSheet.create({
|
|||||||
itemSubtitle: {
|
itemSubtitle: {
|
||||||
fontFamily: font.regular,
|
fontFamily: font.regular,
|
||||||
color: colors.text.secondary,
|
color: colors.text.secondary,
|
||||||
fontSize: 15,
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -72,9 +72,9 @@ export type HomeLists = { [key: string]: AlbumListItem[] }
|
|||||||
export type StarrableItemType = 'song' | 'album' | 'artist'
|
export type StarrableItemType = 'song' | 'album' | 'artist'
|
||||||
|
|
||||||
export enum CacheItemType {
|
export enum CacheItemType {
|
||||||
coverArt,
|
coverArt = 'coverArt',
|
||||||
artistArt,
|
artistArt = 'artistArt',
|
||||||
song,
|
song = 'song',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CacheItemTypeKey = keyof typeof CacheItemType
|
export type CacheItemTypeKey = keyof typeof CacheItemType
|
||||||
|
|||||||
@ -8,28 +8,27 @@ import font from '@app/styles/font'
|
|||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import md5 from 'md5'
|
import md5 from 'md5'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { StyleSheet, Text, TextInput, View } from 'react-native'
|
import { StyleSheet, Text, TextInput, ToastAndroid, View } from 'react-native'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
function replaceIndex<T>(array: T[], index: number, replacement: T): T[] {
|
|
||||||
const start = array.slice(0, index)
|
|
||||||
const end = array.slice(index + 1)
|
|
||||||
return [...start, replacement, ...end]
|
|
||||||
}
|
|
||||||
|
|
||||||
const ServerView: React.FC<{
|
const ServerView: React.FC<{
|
||||||
id?: string
|
id?: string
|
||||||
}> = ({ id }) => {
|
}> = ({ id }) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const activeServer = useStore(selectSettings.activeServer)
|
const activeServer = useStore(selectSettings.activeServer)
|
||||||
const setActiveServer = useStore(selectSettings.setActiveServer)
|
|
||||||
const servers = useStore(selectSettings.servers)
|
const servers = useStore(selectSettings.servers)
|
||||||
const setServers = useStore(selectSettings.setServers)
|
const addServer = useStore(selectSettings.addServer)
|
||||||
|
const updateServer = useStore(selectSettings.updateServer)
|
||||||
|
const removeServer = useStore(selectSettings.removeServer)
|
||||||
const server = id ? servers.find(s => s.id === id) : undefined
|
const server = id ? servers.find(s => s.id === id) : undefined
|
||||||
|
const pingServer = useStore(selectSettings.pingServer)
|
||||||
|
|
||||||
const [address, setAddress] = useState(server?.address || '')
|
const [address, setAddress] = useState(server?.address || '')
|
||||||
const [username, setUsername] = useState(server?.username || '')
|
const [username, setUsername] = useState(server?.username || '')
|
||||||
const [password, setPassword] = useState(server?.token ? 'password' : '')
|
const [password, setPassword] = useState(server?.token ? 'password' : '')
|
||||||
|
const [testing, setTesting] = useState(false)
|
||||||
|
const [removing, setRemoving] = useState(false)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
const validate = useCallback(() => {
|
const validate = useCallback(() => {
|
||||||
return !!address && !!username && !!password
|
return !!address && !!username && !!password
|
||||||
@ -47,11 +46,7 @@ const ServerView: React.FC<{
|
|||||||
}
|
}
|
||||||
}, [navigation])
|
}, [navigation])
|
||||||
|
|
||||||
const save = useCallback(() => {
|
const createServer = useCallback<() => Server>(() => {
|
||||||
if (!validate()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const salt = server?.salt || uuidv4()
|
const salt = server?.salt || uuidv4()
|
||||||
let token: string
|
let token: string
|
||||||
if (password === 'password' && server?.token) {
|
if (password === 'password' && server?.token) {
|
||||||
@ -60,47 +55,76 @@ const ServerView: React.FC<{
|
|||||||
token = md5(password + salt)
|
token = md5(password + salt)
|
||||||
}
|
}
|
||||||
|
|
||||||
const update: Server = {
|
return {
|
||||||
id: server?.id || uuidv4(),
|
id: server?.id || uuidv4(),
|
||||||
address,
|
address,
|
||||||
username,
|
username,
|
||||||
salt,
|
salt,
|
||||||
token,
|
token,
|
||||||
}
|
}
|
||||||
|
}, [address, password, server?.id, server?.salt, server?.token, username])
|
||||||
|
|
||||||
if (server) {
|
const save = useCallback(() => {
|
||||||
setServers(
|
if (!validate()) {
|
||||||
replaceIndex(
|
return
|
||||||
servers,
|
|
||||||
servers.findIndex(s => s.id === id),
|
|
||||||
update,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setServers([...servers, update])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!activeServer) {
|
setSaving(true)
|
||||||
setActiveServer(update.id)
|
const update = createServer()
|
||||||
}
|
|
||||||
|
|
||||||
exit()
|
const waitForSave = async () => {
|
||||||
}, [activeServer, address, exit, id, password, server, servers, setActiveServer, setServers, username, validate])
|
try {
|
||||||
|
if (id) {
|
||||||
|
updateServer(update)
|
||||||
|
} else {
|
||||||
|
await addServer(update)
|
||||||
|
}
|
||||||
|
exit()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
waitForSave()
|
||||||
|
}, [addServer, createServer, exit, id, updateServer, validate])
|
||||||
|
|
||||||
const remove = useCallback(() => {
|
const remove = useCallback(() => {
|
||||||
if (!canRemove()) {
|
if (!canRemove()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const update = [...servers]
|
setRemoving(true)
|
||||||
update.splice(
|
const waitForRemove = async () => {
|
||||||
update.findIndex(s => s.id === id),
|
try {
|
||||||
1,
|
await removeServer(id as string)
|
||||||
)
|
exit()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
setRemoving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
waitForRemove()
|
||||||
|
}, [canRemove, exit, id, removeServer])
|
||||||
|
|
||||||
setServers(update)
|
const test = useCallback(() => {
|
||||||
exit()
|
setTesting(true)
|
||||||
}, [canRemove, exit, id, servers, setServers])
|
const potential = createServer()
|
||||||
|
|
||||||
|
const ping = async () => {
|
||||||
|
const res = await pingServer(potential)
|
||||||
|
if (res) {
|
||||||
|
ToastAndroid.show(`Connection to ${potential.address} OK!`, ToastAndroid.SHORT)
|
||||||
|
} else {
|
||||||
|
ToastAndroid.show(`Connection to ${potential.address} failed, check settings or server`, ToastAndroid.SHORT)
|
||||||
|
}
|
||||||
|
setTesting(false)
|
||||||
|
}
|
||||||
|
ping()
|
||||||
|
}, [createServer, pingServer, setTesting])
|
||||||
|
|
||||||
|
const disableControls = useCallback(() => {
|
||||||
|
return !validate() || testing || removing || saving
|
||||||
|
}, [validate, testing, removing, saving])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientScrollView style={styles.scroll} contentContainerStyle={styles.scrollContentContainer}>
|
<GradientScrollView style={styles.scroll} contentContainerStyle={styles.scrollContentContainer}>
|
||||||
@ -139,18 +163,19 @@ const ServerView: React.FC<{
|
|||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={!validate()}
|
disabled={disableControls()}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
title="Test Connection"
|
title="Test Connection"
|
||||||
buttonStyle="hollow"
|
buttonStyle="hollow"
|
||||||
onPress={() => {}}
|
onPress={test}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
disabled={disableControls()}
|
||||||
style={[styles.button, styles.delete, { display: canRemove() ? 'flex' : 'none' }]}
|
style={[styles.button, styles.delete, { display: canRemove() ? 'flex' : 'none' }]}
|
||||||
title="Delete"
|
title="Delete"
|
||||||
onPress={remove}
|
onPress={remove}
|
||||||
/>
|
/>
|
||||||
<Button disabled={!validate()} style={styles.button} title="Save" onPress={save} />
|
<Button disabled={disableControls()} style={styles.button} title="Save" onPress={save} />
|
||||||
</View>
|
</View>
|
||||||
</GradientScrollView>
|
</GradientScrollView>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,23 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Text, View } from 'react-native'
|
import { Text, View } from 'react-native'
|
||||||
import RNFS from 'react-native-fs'
|
|
||||||
import paths from '@app/util/paths'
|
|
||||||
import { Store, useStore } from '@app/state/store'
|
import { Store, useStore } from '@app/state/store'
|
||||||
|
|
||||||
async function mkdir(path: string): Promise<void> {
|
|
||||||
const exists = await RNFS.exists(path)
|
|
||||||
if (exists) {
|
|
||||||
const isDir = (await RNFS.stat(path)).isDirectory()
|
|
||||||
if (!isDir) {
|
|
||||||
throw new Error(`path exists and is not a directory: ${path}`)
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await RNFS.mkdir(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectHydrated = (store: Store) => store.hydrated
|
const selectHydrated = (store: Store) => store.hydrated
|
||||||
|
|
||||||
const SplashPage: React.FC<{}> = ({ children }) => {
|
const SplashPage: React.FC<{}> = ({ children }) => {
|
||||||
@ -27,9 +11,7 @@ const SplashPage: React.FC<{}> = ({ children }) => {
|
|||||||
const minSplashTime = new Promise(resolve => setTimeout(resolve, 1))
|
const minSplashTime = new Promise(resolve => setTimeout(resolve, 1))
|
||||||
|
|
||||||
const prepare = async () => {
|
const prepare = async () => {
|
||||||
await mkdir(paths.imageCache)
|
return
|
||||||
await mkdir(paths.songCache)
|
|
||||||
await mkdir(paths.songs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const promise = Promise.all([prepare(), minSplashTime])
|
const promise = Promise.all([prepare(), minSplashTime])
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { CacheFile, CacheItemTypeKey, CacheRequest } from '@app/models/music'
|
import { CacheFile, CacheItemType, CacheItemTypeKey, CacheRequest } from '@app/models/music'
|
||||||
|
import { mkdir, rmdir } from '@app/util/fs'
|
||||||
import PromiseQueue from '@app/util/PromiseQueue'
|
import PromiseQueue from '@app/util/PromiseQueue'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
import RNFS from 'react-native-fs'
|
import RNFS from 'react-native-fs'
|
||||||
@ -33,6 +34,11 @@ export type CacheSlice = {
|
|||||||
cacheRequests: CacheRequestsByServer
|
cacheRequests: CacheRequestsByServer
|
||||||
|
|
||||||
fetchCoverArtFilePath: (coverArt: string) => Promise<string | undefined>
|
fetchCoverArtFilePath: (coverArt: string) => Promise<string | undefined>
|
||||||
|
|
||||||
|
createCache: (serverId: string) => Promise<void>
|
||||||
|
prepareCache: (serverId: string) => void
|
||||||
|
pendingRemoval: Record<string, boolean>
|
||||||
|
removeCache: (serverId: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectCache = {
|
export const selectCache = {
|
||||||
@ -66,6 +72,10 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): Ca
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (get().pendingRemoval[activeServerId]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const inProgress = get().cacheRequests[activeServerId][key][itemId]
|
const inProgress = get().cacheRequests[activeServerId][key][itemId]
|
||||||
if (inProgress && inProgress.promise !== undefined) {
|
if (inProgress && inProgress.promise !== undefined) {
|
||||||
return await inProgress.promise
|
return await inProgress.promise
|
||||||
@ -154,4 +164,82 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): Ca
|
|||||||
await get().cacheItem('coverArt', coverArt, () => client.getCoverArtUri({ id: coverArt }))
|
await get().cacheItem('coverArt', coverArt, () => client.getCoverArtUri({ id: coverArt }))
|
||||||
return `file://${get().cacheFiles[activeServerId].coverArt[coverArt].path}`
|
return `file://${get().cacheFiles[activeServerId].coverArt[coverArt].path}`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createCache: async serverId => {
|
||||||
|
for (const type in CacheItemType) {
|
||||||
|
await mkdir(`${RNFS.DocumentDirectoryPath}/servers/${serverId}/${type}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
set(
|
||||||
|
produce<CacheSlice>(state => {
|
||||||
|
state.cacheFiles[serverId] = {
|
||||||
|
song: {},
|
||||||
|
coverArt: {},
|
||||||
|
artistArt: {},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
get().prepareCache(serverId)
|
||||||
|
},
|
||||||
|
|
||||||
|
prepareCache: serverId => {
|
||||||
|
set(
|
||||||
|
produce<CacheSlice>(state => {
|
||||||
|
if (!state.cacheDirs[serverId]) {
|
||||||
|
state.cacheDirs[serverId] = {
|
||||||
|
song: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/song`,
|
||||||
|
coverArt: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/coverArt`,
|
||||||
|
artistArt: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/artistArt`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!state.cacheRequests[serverId]) {
|
||||||
|
state.cacheRequests[serverId] = {
|
||||||
|
song: {},
|
||||||
|
coverArt: {},
|
||||||
|
artistArt: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
pendingRemoval: {},
|
||||||
|
|
||||||
|
removeCache: async serverId => {
|
||||||
|
set(
|
||||||
|
produce<CacheSlice>(state => {
|
||||||
|
state.pendingRemoval[serverId] = true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const cacheRequests = get().cacheRequests[serverId]
|
||||||
|
const pendingRequests: Promise<void>[] = []
|
||||||
|
|
||||||
|
for (const type in CacheItemType) {
|
||||||
|
const requests = Object.values(cacheRequests[type as CacheItemTypeKey])
|
||||||
|
.filter(r => r.promise !== undefined)
|
||||||
|
.map(r => r.promise) as Promise<void>[]
|
||||||
|
pendingRequests.push(...requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(pendingRequests)
|
||||||
|
await rmdir(`${RNFS.DocumentDirectoryPath}/servers/${serverId}`)
|
||||||
|
|
||||||
|
set(
|
||||||
|
produce<CacheSlice>(state => {
|
||||||
|
delete state.pendingRemoval[serverId]
|
||||||
|
|
||||||
|
if (state.cacheDirs[serverId]) {
|
||||||
|
delete state.cacheDirs[serverId]
|
||||||
|
}
|
||||||
|
if (state.cacheFiles[serverId]) {
|
||||||
|
delete state.cacheFiles[serverId]
|
||||||
|
}
|
||||||
|
if (state.cacheRequests[serverId]) {
|
||||||
|
delete state.cacheRequests[serverId]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,37 +1,25 @@
|
|||||||
import { CacheItemType } from '@app/models/music'
|
|
||||||
import { AppSettings, Server } from '@app/models/settings'
|
import { AppSettings, Server } from '@app/models/settings'
|
||||||
import { Store } from '@app/state/store'
|
import { Store } from '@app/state/store'
|
||||||
import { SubsonicApiClient } from '@app/subsonic/api'
|
import { SubsonicApiClient } from '@app/subsonic/api'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
import RNFS from 'react-native-fs'
|
|
||||||
import { GetState, SetState } from 'zustand'
|
import { GetState, SetState } from 'zustand'
|
||||||
|
|
||||||
async function mkdir(path: string): Promise<void> {
|
|
||||||
const exists = await RNFS.exists(path)
|
|
||||||
if (exists) {
|
|
||||||
const isDir = (await RNFS.stat(path)).isDirectory()
|
|
||||||
if (!isDir) {
|
|
||||||
throw new Error(`path exists and is not a directory: ${path}`)
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await RNFS.mkdir(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SettingsSlice = {
|
export type SettingsSlice = {
|
||||||
settings: AppSettings
|
settings: AppSettings
|
||||||
client?: SubsonicApiClient
|
client?: SubsonicApiClient
|
||||||
|
|
||||||
setActiveServer: (id: string | undefined, force?: boolean) => Promise<void>
|
setActiveServer: (id: string | undefined, force?: boolean) => Promise<void>
|
||||||
getActiveServer: () => Server | undefined
|
getActiveServer: () => Server | undefined
|
||||||
setServers: (servers: Server[]) => void
|
addServer: (server: Server) => Promise<void>
|
||||||
|
removeServer: (id: string) => Promise<void>
|
||||||
|
updateServer: (server: Server) => void
|
||||||
|
|
||||||
setScrobble: (scrobble: boolean) => void
|
setScrobble: (scrobble: boolean) => void
|
||||||
setEstimateContentLength: (estimateContentLength: boolean) => void
|
setEstimateContentLength: (estimateContentLength: boolean) => void
|
||||||
setMaxBitrateWifi: (maxBitrateWifi: number) => void
|
setMaxBitrateWifi: (maxBitrateWifi: number) => void
|
||||||
setMaxBitrateMobile: (maxBitrateMobile: number) => void
|
setMaxBitrateMobile: (maxBitrateMobile: number) => void
|
||||||
|
|
||||||
|
pingServer: (server?: Server) => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectSettings = {
|
export const selectSettings = {
|
||||||
@ -41,7 +29,9 @@ export const selectSettings = {
|
|||||||
setActiveServer: (state: SettingsSlice) => state.setActiveServer,
|
setActiveServer: (state: SettingsSlice) => state.setActiveServer,
|
||||||
|
|
||||||
servers: (state: SettingsSlice) => state.settings.servers,
|
servers: (state: SettingsSlice) => state.settings.servers,
|
||||||
setServers: (state: SettingsSlice) => state.setServers,
|
addServer: (state: SettingsSlice) => state.addServer,
|
||||||
|
removeServer: (state: SettingsSlice) => state.removeServer,
|
||||||
|
updateServer: (state: SettingsSlice) => state.updateServer,
|
||||||
|
|
||||||
homeLists: (state: SettingsSlice) => state.settings.home.lists,
|
homeLists: (state: SettingsSlice) => state.settings.home.lists,
|
||||||
|
|
||||||
@ -55,6 +45,8 @@ export const selectSettings = {
|
|||||||
setMaxBitrateWifi: (state: SettingsSlice) => state.setMaxBitrateWifi,
|
setMaxBitrateWifi: (state: SettingsSlice) => state.setMaxBitrateWifi,
|
||||||
maxBitrateMobile: (state: SettingsSlice) => state.settings.maxBitrateMobile,
|
maxBitrateMobile: (state: SettingsSlice) => state.settings.maxBitrateMobile,
|
||||||
setMaxBitrateMobile: (state: SettingsSlice) => state.setMaxBitrateMobile,
|
setMaxBitrateMobile: (state: SettingsSlice) => state.setMaxBitrateMobile,
|
||||||
|
|
||||||
|
pingServer: (state: SettingsSlice) => state.pingServer,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>): SettingsSlice => ({
|
export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>): SettingsSlice => ({
|
||||||
@ -84,50 +76,55 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const type in CacheItemType) {
|
get().prepareCache(newActiveServer.id)
|
||||||
await mkdir(`${RNFS.DocumentDirectoryPath}/servers/${id}/${type}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
set(
|
set(
|
||||||
produce<Store>(state => {
|
produce<Store>(state => {
|
||||||
state.settings.activeServer = newActiveServer.id
|
state.settings.activeServer = newActiveServer.id
|
||||||
state.client = new SubsonicApiClient(newActiveServer)
|
state.client = new SubsonicApiClient(newActiveServer)
|
||||||
|
|
||||||
if (!state.cacheDirs[newActiveServer.id]) {
|
|
||||||
state.cacheDirs[newActiveServer.id] = {
|
|
||||||
song: `${RNFS.DocumentDirectoryPath}/servers/${id}/song`,
|
|
||||||
coverArt: `${RNFS.DocumentDirectoryPath}/servers/${id}/coverArt`,
|
|
||||||
artistArt: `${RNFS.DocumentDirectoryPath}/servers/${id}/artistArt`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!state.cacheFiles[newActiveServer.id]) {
|
|
||||||
state.cacheFiles[newActiveServer.id] = {
|
|
||||||
song: {},
|
|
||||||
coverArt: {},
|
|
||||||
artistArt: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!state.cacheRequests[newActiveServer.id]) {
|
|
||||||
state.cacheRequests[newActiveServer.id] = {
|
|
||||||
song: {},
|
|
||||||
coverArt: {},
|
|
||||||
artistArt: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
getActiveServer: () => get().settings.servers.find(s => s.id === get().settings.activeServer),
|
getActiveServer: () => get().settings.servers.find(s => s.id === get().settings.activeServer),
|
||||||
|
|
||||||
setServers: servers => {
|
addServer: async server => {
|
||||||
|
await get().createCache(server.id)
|
||||||
|
|
||||||
set(
|
set(
|
||||||
produce<SettingsSlice>(state => {
|
produce<SettingsSlice>(state => {
|
||||||
state.settings.servers = servers
|
state.settings.servers.push(server)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const activeServer = servers.find(s => s.id === get().settings.activeServer)
|
|
||||||
get().setActiveServer(activeServer?.id)
|
if (get().settings.servers.length === 1) {
|
||||||
|
get().setActiveServer(server.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeServer: async id => {
|
||||||
|
await get().removeCache(id)
|
||||||
|
|
||||||
|
set(
|
||||||
|
produce<SettingsSlice>(state => {
|
||||||
|
state.settings.servers = state.settings.servers.filter(s => s.id !== id)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
updateServer: server => {
|
||||||
|
set(
|
||||||
|
produce<SettingsSlice>(state => {
|
||||||
|
state.settings.servers = replaceIndex(
|
||||||
|
state.settings.servers,
|
||||||
|
state.settings.servers.findIndex(s => s.id === server.id),
|
||||||
|
server,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if (get().settings.activeServer === server.id) {
|
||||||
|
get().setActiveServer(server.id)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setScrobble: scrobble => {
|
setScrobble: scrobble => {
|
||||||
@ -168,4 +165,30 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
|
|||||||
get().rebuildQueue()
|
get().rebuildQueue()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pingServer: async server => {
|
||||||
|
let client: SubsonicApiClient
|
||||||
|
if (server) {
|
||||||
|
client = new SubsonicApiClient(server)
|
||||||
|
} else {
|
||||||
|
const currentClient = get().client
|
||||||
|
if (!currentClient) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
client = currentClient
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.ping()
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function replaceIndex<T>(array: T[], index: number, replacement: T): T[] {
|
||||||
|
const start = array.slice(0, index)
|
||||||
|
const end = array.slice(index + 1)
|
||||||
|
return [...start, replacement, ...end]
|
||||||
|
}
|
||||||
|
|||||||
@ -35,7 +35,6 @@ import {
|
|||||||
SubsonicResponse,
|
SubsonicResponse,
|
||||||
} from '@app/subsonic/responses'
|
} from '@app/subsonic/responses'
|
||||||
import { Server } from '@app/models/settings'
|
import { Server } from '@app/models/settings'
|
||||||
import paths from '@app/util/paths'
|
|
||||||
import PromiseQueue from '@app/util/PromiseQueue'
|
import PromiseQueue from '@app/util/PromiseQueue'
|
||||||
|
|
||||||
export class SubsonicApiError extends Error {
|
export class SubsonicApiError extends Error {
|
||||||
@ -212,11 +211,6 @@ export class SubsonicApiClient {
|
|||||||
// Media retrieval
|
// Media retrieval
|
||||||
//
|
//
|
||||||
|
|
||||||
async getCoverArt(params: GetCoverArtParams): Promise<string> {
|
|
||||||
const path = `${paths.songCache}/${params.id}`
|
|
||||||
return await this.apiDownload('getCoverArt', path, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
getCoverArtUri(params?: GetCoverArtParams): string {
|
getCoverArtUri(params?: GetCoverArtParams): string {
|
||||||
return this.buildUrl('getCoverArt', params)
|
return this.buildUrl('getCoverArt', params)
|
||||||
}
|
}
|
||||||
|
|||||||
19
app/util/fs.ts
Normal file
19
app/util/fs.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import RNFS from 'react-native-fs'
|
||||||
|
|
||||||
|
export async function mkdir(path: string): Promise<void> {
|
||||||
|
const exists = await RNFS.exists(path)
|
||||||
|
if (exists) {
|
||||||
|
const isDir = (await RNFS.stat(path)).isDirectory()
|
||||||
|
if (!isDir) {
|
||||||
|
throw new Error(`path exists and is not a directory: ${path}`)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await RNFS.mkdir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rmdir(path: string): Promise<void> {
|
||||||
|
return RNFS.unlink(path)
|
||||||
|
}
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import RNFS from 'react-native-fs'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
imageCache: `${RNFS.DocumentDirectoryPath}/image_cache`,
|
|
||||||
songCache: `${RNFS.DocumentDirectoryPath}/song_cache`,
|
|
||||||
songs: `${RNFS.DocumentDirectoryPath}/songs`,
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user