diff --git a/app/components/TextInput.tsx b/app/components/TextInput.tsx new file mode 100644 index 0000000..5572524 --- /dev/null +++ b/app/components/TextInput.tsx @@ -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 + value?: string + placeholder?: string + onChangeText?: (text: string) => void + onSubmitEditing?: () => void + keyboardType?: KeyboardTypeOptions +}>(({ style, value, placeholder, onChangeText, onSubmitEditing, keyboardType }) => { + return ( + + ) +}) + +const styles = StyleSheet.create({ + textInput: { + width: '100%', + backgroundColor: '#515151', + fontFamily: font.regular, + fontSize: 18, + color: colors.text.primary, + }, +}) + +export default TextInput diff --git a/app/models/settings.ts b/app/models/settings.ts index 6f9c78b..a0917b0 100644 --- a/app/models/settings.ts +++ b/app/models/settings.ts @@ -16,4 +16,6 @@ export interface AppSettings { estimateContentLength: boolean maxBitrateWifi: number maxBitrateMobile: number + minBuffer: number + maxBuffer: number } diff --git a/app/screens/Search.tsx b/app/screens/Search.tsx index 450dddb..abde43d 100644 --- a/app/screens/Search.tsx +++ b/app/screens/Search.tsx @@ -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 = () => { - + - +
{title}
@@ -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 ( + <> + + + + + +
{title}
+ + + + + + +
+
+
+
+ + ) +}) + +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 (
Servers
@@ -184,6 +253,20 @@ const SettingsContent = React.memo(() => { value={estimateContentLength} setValue={setEstimateContentLength} /> + +
Music
void setMaxBitrateWifi: (maxBitrateWifi: number) => void setMaxBitrateMobile: (maxBitrateMobile: number) => void + setMinBuffer: (minBuffer: number) => void + setMaxBuffer: (maxBuffer: number) => void pingServer: (server?: Server) => Promise } @@ -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, get: GetState): estimateContentLength: true, maxBitrateWifi: 0, maxBitrateMobile: 192, + minBuffer: 3, + maxBuffer: 60, }, setActiveServer: async (id, force) => { @@ -122,6 +131,7 @@ export const createSettingsSlice = (set: SetState, get: GetState): ) }), ) + if (get().settings.activeServer === server.id) { get().setActiveServer(server.id) } @@ -141,6 +151,7 @@ export const createSettingsSlice = (set: SetState, get: GetState): state.settings.estimateContentLength = estimateContentLength }), ) + get().rebuildQueue() }, @@ -150,6 +161,7 @@ export const createSettingsSlice = (set: SetState, get: GetState): state.settings.maxBitrateWifi = maxBitrateWifi }), ) + if (get().netState === 'wifi') { get().rebuildQueue() } @@ -161,11 +173,40 @@ export const createSettingsSlice = (set: SetState, get: GetState): state.settings.maxBitrateMobile = maxBitrateMobile }), ) + if (get().netState === 'mobile') { get().rebuildQueue() } }, + setMinBuffer: minBuffer => { + if (minBuffer === get().settings.minBuffer) { + return + } + + set( + produce(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(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) { diff --git a/app/state/trackplayer.ts b/app/state/trackplayer.ts index b2ba25a..b7d65b4 100644 --- a/app/state/trackplayer.ts +++ b/app/state/trackplayer.ts @@ -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 buildStreamUri: (id: string) => string resetTrackPlayerState: () => void + + getPlayerOptions: () => PlayerOptions } export const selectTrackPlayer = { @@ -200,7 +202,7 @@ export const createTrackPlayerSlice = (set: SetState, get: GetState { 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, get: GetState, get: GetState { + return { + minBuffer: get().settings.minBuffer, + playBuffer: get().settings.minBuffer / 2, + maxBuffer: get().settings.maxBuffer, + } + }, }) export const getQueue = async (): Promise => {