added settings for min/max buffer

This commit is contained in:
austinried 2021-08-20 16:32:24 +09:00
parent 694d730ebd
commit c83fb43140
6 changed files with 202 additions and 23 deletions

View File

@ -0,0 +1,38 @@
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import React from 'react'
import { TextInput as ReactTextInput, StyleSheet, StyleProp, TextStyle, KeyboardTypeOptions } from 'react-native'
const TextInput = React.memo<{
style?: StyleProp<TextStyle>
value?: string
placeholder?: string
onChangeText?: (text: string) => void
onSubmitEditing?: () => void
keyboardType?: KeyboardTypeOptions
}>(({ style, value, placeholder, onChangeText, onSubmitEditing, keyboardType }) => {
return (
<ReactTextInput
style={[style, styles.textInput]}
placeholder={placeholder}
placeholderTextColor="grey"
selectionColor={colors.text.secondary}
value={value}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
keyboardType={keyboardType}
/>
)
})
const styles = StyleSheet.create({
textInput: {
width: '100%',
backgroundColor: '#515151',
fontFamily: font.regular,
fontSize: 18,
color: colors.text.primary,
},
})
export default TextInput

View File

@ -16,4 +16,6 @@ export interface AppSettings {
estimateContentLength: boolean
maxBitrateWifi: number
maxBitrateMobile: number
minBuffer: number
maxBuffer: number
}

View File

@ -3,6 +3,7 @@ import GradientScrollView from '@app/components/GradientScrollView'
import Header from '@app/components/Header'
import ListItem from '@app/components/ListItem'
import NothingHere from '@app/components/NothingHere'
import TextInput from '@app/components/TextInput'
import { useActiveServerRefresh } from '@app/hooks/server'
import { ListableItem, SearchResults, Song } from '@app/models/music'
import { selectMusic } from '@app/state/music'
@ -13,7 +14,7 @@ import font from '@app/styles/font'
import { useNavigation } from '@react-navigation/native'
import debounce from 'lodash.debounce'
import React, { useCallback, useMemo, useState } from 'react'
import { ActivityIndicator, StatusBar, StyleSheet, TextInput, View } from 'react-native'
import { ActivityIndicator, StatusBar, StyleSheet, View } from 'react-native'
const SongItem = React.memo<{ item: Song }>(({ item }) => {
const setQueue = useStore(selectTrackPlayer.setQueue)
@ -113,14 +114,7 @@ const Search = () => {
<GradientScrollView style={styles.scroll} contentContainerStyle={styles.scrollContentContainer}>
<View style={styles.content}>
<View style={styles.inputBar}>
<TextInput
style={styles.textInput}
placeholder="Search"
placeholderTextColor="grey"
selectionColor={colors.text.secondary}
value={text}
onChangeText={onChangeText}
/>
<TextInput style={styles.textInput} placeholder="Search" value={text} onChangeText={onChangeText} />
<ActivityIndicator
animating={refreshing}
size="small"
@ -154,16 +148,6 @@ const styles = StyleSheet.create({
right: 16,
bottom: 15,
},
textInput: {
width: '100%',
backgroundColor: '#515151',
fontFamily: font.regular,
fontSize: 18,
color: colors.text.primary,
marginTop: 20,
paddingHorizontal: 12,
paddingRight: 46,
},
noResults: {
width: '100%',
},
@ -176,6 +160,11 @@ const styles = StyleSheet.create({
marginTop: 5,
marginBottom: 10,
},
textInput: {
marginTop: 20,
paddingHorizontal: 12,
paddingRight: 46,
},
})
export default Search

View File

@ -4,6 +4,7 @@ import Header from '@app/components/Header'
import PressableOpacity from '@app/components/PressableOpacity'
import SettingsItem from '@app/components/SettingsItem'
import SettingsSwitch from '@app/components/SettingsSwitch'
import TextInput from '@app/components/TextInput'
import { useSwitchActiveServer } from '@app/hooks/server'
import { Server } from '@app/models/settings'
import { selectCache } from '@app/state/cache'
@ -13,7 +14,7 @@ import colors from '@app/styles/colors'
import font from '@app/styles/font'
import { useNavigation } from '@react-navigation/core'
import React, { useCallback, useState } from 'react'
import { Modal, Pressable, StatusBar, StyleSheet, Text, View } from 'react-native'
import { KeyboardTypeOptions, Modal, Pressable, StatusBar, StyleSheet, Text, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
@ -105,7 +106,7 @@ const BitrateModal = React.memo<{
<>
<SettingsItem title={title} subtitle={bitrateString(bitrate)} onPress={toggleModal} />
<Modal animationType="fade" transparent={true} visible={visible} onRequestClose={toggleModal}>
<Pressable style={{ flex: 1 }} onPress={toggleModal}>
<Pressable style={styles.modalBackdrop} onPress={toggleModal}>
<View style={styles.centeredView}>
<Pressable style={styles.modalView}>
<Header style={styles.modalHeader}>{title}</Header>
@ -130,6 +131,66 @@ const BitrateModal = React.memo<{
)
})
const SettingsTextModal = React.memo<{
title: string
value: string
setValue: (text: string) => void
getUnit?: (text: string) => string
keyboardType?: KeyboardTypeOptions
}>(({ title, value, setValue, getUnit, keyboardType }) => {
const [visible, setVisible] = useState(false)
const [inputText, setInputText] = useState(value)
const toggleModal = useCallback(() => setVisible(!visible), [visible])
const submit = useCallback(() => {
setValue(inputText)
toggleModal()
}, [inputText, setValue, toggleModal])
const getSubtitle = useCallback(() => {
if (!getUnit) {
return value
}
return value + ' ' + getUnit(value)
}, [getUnit, value])
return (
<>
<SettingsItem title={title} subtitle={getSubtitle()} onPress={toggleModal} />
<Modal animationType="fade" transparent={true} visible={visible} onRequestClose={toggleModal}>
<Pressable style={styles.modalBackdrop} onPress={toggleModal}>
<View style={styles.centeredView}>
<Pressable style={styles.modalView}>
<Header style={styles.modalHeader}>{title}</Header>
<View style={styles.modalTextInputLine}>
<TextInput
style={styles.modalTextInput}
value={inputText}
onChangeText={setInputText}
onSubmitEditing={submit}
keyboardType={keyboardType}
/>
<PressableOpacity style={styles.modalTextSubmit} onPress={submit} hitSlop={10}>
<Icon name="content-save-edit" color="white" size={32} />
</PressableOpacity>
</View>
</Pressable>
</View>
</Pressable>
</Modal>
</>
)
})
function secondsUnit(seconds: string): string {
const numberValue = parseFloat(seconds)
if (Math.abs(numberValue) !== 1) {
return 'seconds'
}
return 'second'
}
const SettingsContent = React.memo(() => {
const servers = useStore(selectSettings.servers)
const scrobble = useStore(selectSettings.scrobble)
@ -143,6 +204,11 @@ const SettingsContent = React.memo(() => {
const maxBitrateMobile = useStore(selectSettings.maxBitrateMobile)
const setMaxBitrateMobile = useStore(selectSettings.setMaxBitrateMobile)
const minBuffer = useStore(selectSettings.minBuffer)
const setMinBuffer = useStore(selectSettings.setMinBuffer)
const maxBuffer = useStore(selectSettings.maxBuffer)
const setMaxBuffer = useStore(selectSettings.setMaxBuffer)
const clearImageCache = useStore(selectCache.clearImageCache)
const [clearing, setClearing] = useState(false)
@ -163,6 +229,9 @@ const SettingsContent = React.memo(() => {
waitForClear()
}, [clearImageCache])
const setMinBufferText = useCallback((text: string) => setMinBuffer(parseFloat(text)), [setMinBuffer])
const setMaxBufferText = useCallback((text: string) => setMaxBuffer(parseFloat(text)), [setMaxBuffer])
return (
<View style={styles.content}>
<Header>Servers</Header>
@ -184,6 +253,20 @@ const SettingsContent = React.memo(() => {
value={estimateContentLength}
setValue={setEstimateContentLength}
/>
<SettingsTextModal
title="Minimum buffer time"
value={minBuffer.toString()}
setValue={setMinBufferText}
getUnit={secondsUnit}
keyboardType="numeric"
/>
<SettingsTextModal
title="Maximum buffer time"
value={maxBuffer.toString()}
setValue={setMaxBufferText}
getUnit={secondsUnit}
keyboardType="numeric"
/>
<Header style={styles.header}>Music</Header>
<SettingsSwitch
title="Scrobble plays"
@ -234,6 +317,9 @@ const styles = StyleSheet.create({
button: {
marginTop: 16,
},
modalBackdrop: {
flex: 1,
},
centeredView: {
flex: 1,
justifyContent: 'center',
@ -269,6 +355,18 @@ const styles = StyleSheet.create({
modalScroll: {
maxHeight: 475,
},
modalTextInputLine: {
flexDirection: 'row',
margin: 20,
},
modalTextInput: {
flex: 1,
paddingLeft: 12,
},
modalTextSubmit: {
marginLeft: 15,
// backgroundColor: 'green',
},
})
export default Settings

View File

@ -18,6 +18,8 @@ export type SettingsSlice = {
setEstimateContentLength: (estimateContentLength: boolean) => void
setMaxBitrateWifi: (maxBitrateWifi: number) => void
setMaxBitrateMobile: (maxBitrateMobile: number) => void
setMinBuffer: (minBuffer: number) => void
setMaxBuffer: (maxBuffer: number) => void
pingServer: (server?: Server) => Promise<boolean>
}
@ -46,6 +48,11 @@ export const selectSettings = {
maxBitrateMobile: (state: SettingsSlice) => state.settings.maxBitrateMobile,
setMaxBitrateMobile: (state: SettingsSlice) => state.setMaxBitrateMobile,
minBuffer: (state: SettingsSlice) => state.settings.minBuffer,
setMinBuffer: (state: SettingsSlice) => state.setMinBuffer,
maxBuffer: (state: SettingsSlice) => state.settings.maxBuffer,
setMaxBuffer: (state: SettingsSlice) => state.setMaxBuffer,
pingServer: (state: SettingsSlice) => state.pingServer,
}
@ -59,6 +66,8 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
estimateContentLength: true,
maxBitrateWifi: 0,
maxBitrateMobile: 192,
minBuffer: 3,
maxBuffer: 60,
},
setActiveServer: async (id, force) => {
@ -122,6 +131,7 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
)
}),
)
if (get().settings.activeServer === server.id) {
get().setActiveServer(server.id)
}
@ -141,6 +151,7 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
state.settings.estimateContentLength = estimateContentLength
}),
)
get().rebuildQueue()
},
@ -150,6 +161,7 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
state.settings.maxBitrateWifi = maxBitrateWifi
}),
)
if (get().netState === 'wifi') {
get().rebuildQueue()
}
@ -161,11 +173,40 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
state.settings.maxBitrateMobile = maxBitrateMobile
}),
)
if (get().netState === 'mobile') {
get().rebuildQueue()
}
},
setMinBuffer: minBuffer => {
if (minBuffer === get().settings.minBuffer) {
return
}
set(
produce<SettingsSlice>(state => {
state.settings.minBuffer = Math.max(1, Math.min(minBuffer, state.settings.maxBuffer / 2))
}),
)
get().rebuildQueue()
},
setMaxBuffer: maxBuffer => {
if (maxBuffer === get().settings.maxBuffer) {
return
}
set(
produce<SettingsSlice>(state => {
state.settings.maxBuffer = Math.min(5 * 60, Math.max(maxBuffer, state.settings.minBuffer * 2))
}),
)
get().rebuildQueue()
},
pingServer: async server => {
let client: SubsonicApiClient
if (server) {

View File

@ -2,7 +2,7 @@ import { NoClientError } from '@app/models/error'
import { Song } from '@app/models/music'
import PromiseQueue from '@app/util/PromiseQueue'
import produce from 'immer'
import TrackPlayer, { RepeatMode, State, Track } from 'react-native-track-player'
import TrackPlayer, { PlayerOptions, RepeatMode, State, Track } from 'react-native-track-player'
import { GetState, SetState } from 'zustand'
import { Store } from './store'
@ -67,6 +67,8 @@ export type TrackPlayerSlice = {
rebuildQueue: () => Promise<void>
buildStreamUri: (id: string) => string
resetTrackPlayerState: () => void
getPlayerOptions: () => PlayerOptions
}
export const selectTrackPlayer = {
@ -200,7 +202,7 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
return trackPlayerCommands.enqueue(async () => {
const shuffled = shuffle !== undefined ? shuffle : !!get().shuffleOrder
await TrackPlayer.setupPlayer()
await TrackPlayer.setupPlayer(get().getPlayerOptions())
await TrackPlayer.reset()
if (songs.length === 0) {
@ -298,6 +300,7 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
const queueContextType = get().queueContextType
await TrackPlayer.reset()
await TrackPlayer.setupPlayer(get().getPlayerOptions())
try {
for (const track of queue) {
@ -356,6 +359,14 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
progress: { position: 0, duration: 0, buffered: 0 },
})
},
getPlayerOptions: () => {
return {
minBuffer: get().settings.minBuffer,
playBuffer: get().settings.minBuffer / 2,
maxBuffer: get().settings.maxBuffer,
}
},
})
export const getQueue = async (): Promise<TrackExt[]> => {