mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
added settings for min/max buffer
This commit is contained in:
parent
694d730ebd
commit
c83fb43140
38
app/components/TextInput.tsx
Normal file
38
app/components/TextInput.tsx
Normal 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
|
||||
@ -16,4 +16,6 @@ export interface AppSettings {
|
||||
estimateContentLength: boolean
|
||||
maxBitrateWifi: number
|
||||
maxBitrateMobile: number
|
||||
minBuffer: number
|
||||
maxBuffer: number
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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[]> => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user