mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 23:02:43 +01:00
Library store refactor (#76)
* start of music store refactor moving stuff into a state cache better separate it from view logic * added paginated list/album list * reworked fetchAlbumList to remove ui state refactored home screen to use new method i broke playing songs somehow, JS thread goes into a loop * don't reset parts manually, do it all at once * fixed perf issue related to too many rerenders rerenders were caused by strict equality check on object/array picks switched artistInfo to new store updated zustand and fixed deprecation warnings * update typescript and use workspace tsc version for vscode * remove old artistInfo * switched to new playlist w/songs removed more unused stuff * remove unused + (slightly) rework search * refactor star * use only original/large imges for covers/artist fix view artist from context menu add loading indicators to song list and artist views (show info we have right away) * set starred/unstar assuming it works and correct state on error * reorg, remove old music slice files * added back fix for song cover art * sort artists by localCompare name * update licenses * fix now playing background grey bar * update react-native-gesture-handler for node-fetch security alert * fix another gradient height grey bar issue * update licenses again * remove thumbnail cache * rename to remove "Library" from methods * Revert "remove thumbnail cache" This reverts commite0db4931f1. * use ids for lists, pull state later * Revert "use only original/large imges for covers/artist" This reverts commitc9aea9065c. * deep equal ListItem props for now this needs a bigger refactor * use immer as middleware * refactor api client to use string method hoping to use this for requestKey/deduping next * use thumbnails in list items * Revert "refactor api client to use string method" This reverts commit234326135b. * rename/cleanup * store servers by id * get rid of settings selectors * renames for clarity remove unused estimateContentLength setting * remove trackplayer selectors * fix migration for library filter settings * fixed shuffle order reporting wrong track/queue * removed the other selectors * don't actually need es6/react for our state * fix slow artist sort on star localeCompare is too slow for large lists
This commit is contained in:
@@ -5,16 +5,15 @@ import GradientScrollView from '@app/components/GradientScrollView'
|
||||
import Header from '@app/components/Header'
|
||||
import HeaderBar from '@app/components/HeaderBar'
|
||||
import ListItem from '@app/components/ListItem'
|
||||
import { useArtistInfo } from '@app/hooks/music'
|
||||
import { Album, Song } from '@app/models/music'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { selectTrackPlayer } from '@app/state/trackplayer'
|
||||
import { Album, Song } from '@app/models/library'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
import dimensions from '@app/styles/dimensions'
|
||||
import font from '@app/styles/font'
|
||||
import { mapById } from '@app/util/state'
|
||||
import { useLayout } from '@react-native-community/hooks'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import React from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
|
||||
import { useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
|
||||
|
||||
@@ -47,12 +46,12 @@ const TopSongs = React.memo<{
|
||||
name: string
|
||||
artistId: string
|
||||
}>(({ songs, name, artistId }) => {
|
||||
const setQueue = useStore(selectTrackPlayer.setQueue)
|
||||
const setQueue = useStore(store => store.setQueue)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>Top Songs</Header>
|
||||
{songs.map((s, i) => (
|
||||
{songs.slice(0, 5).map((s, i) => (
|
||||
<ListItem
|
||||
key={i}
|
||||
item={s}
|
||||
@@ -67,6 +66,29 @@ const TopSongs = React.memo<{
|
||||
)
|
||||
})
|
||||
|
||||
const ArtistAlbums = React.memo<{
|
||||
albums: Album[]
|
||||
}>(({ albums }) => {
|
||||
const albumsLayout = useLayout()
|
||||
|
||||
const sortedAlbums = [...albums]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.sort((a, b) => (b.year || 0) - (a.year || 0))
|
||||
|
||||
const albumSize = albumsLayout.width / 2 - styles.contentContainer.paddingHorizontal / 2
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header>Albums</Header>
|
||||
<View style={styles.albums} onLayout={albumsLayout.onLayout}>
|
||||
{sortedAlbums.map(a => (
|
||||
<AlbumItem key={a.id} album={a} height={albumSize} width={albumSize} />
|
||||
))}
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
const ArtistViewFallback = React.memo(() => (
|
||||
<GradientBackground style={styles.fallback}>
|
||||
<ActivityIndicator size="large" color={colors.accent} />
|
||||
@@ -74,8 +96,19 @@ const ArtistViewFallback = React.memo(() => (
|
||||
))
|
||||
|
||||
const ArtistView = React.memo<{ id: string; title: string }>(({ id, title }) => {
|
||||
const artist = useArtistInfo(id)
|
||||
const albumsLayout = useLayout()
|
||||
const artist = useStoreDeep(useCallback(store => store.library.artists[id], [id]))
|
||||
const topSongIds = useStoreDeep(useCallback(store => store.library.artistNameTopSongs[artist?.name], [artist?.name]))
|
||||
const topSongs = useStoreDeep(
|
||||
useCallback(store => (topSongIds ? mapById(store.library.songs, topSongIds) : undefined), [topSongIds]),
|
||||
)
|
||||
const albumIds = useStoreDeep(useCallback(store => store.library.artistAlbums[id], [id]))
|
||||
const albums = useStoreDeep(
|
||||
useCallback(store => (albumIds ? mapById(store.library.albums, albumIds) : undefined), [albumIds]),
|
||||
)
|
||||
|
||||
const fetchArtist = useStore(store => store.fetchArtist)
|
||||
const fetchTopSongs = useStore(store => store.fetchArtistTopSongs)
|
||||
|
||||
const coverLayout = useLayout()
|
||||
const headerOpacity = useSharedValue(0)
|
||||
|
||||
@@ -91,16 +124,22 @@ const ArtistView = React.memo<{ id: string; title: string }>(({ id, title }) =>
|
||||
}
|
||||
})
|
||||
|
||||
const albumSize = albumsLayout.width / 2 - styles.contentContainer.paddingHorizontal / 2
|
||||
useEffect(() => {
|
||||
if (!artist || !albumIds) {
|
||||
fetchArtist(id)
|
||||
}
|
||||
}, [artist, albumIds, fetchArtist, id])
|
||||
|
||||
useEffect(() => {
|
||||
if (artist && !topSongIds) {
|
||||
fetchTopSongs(artist.name)
|
||||
}
|
||||
}, [artist, fetchTopSongs, topSongIds])
|
||||
|
||||
if (!artist) {
|
||||
return <ArtistViewFallback />
|
||||
}
|
||||
|
||||
const _albums = [...artist.albums]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.sort((a, b) => (b.year || 0) - (a.year || 0))
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<HeaderBar title={title} headerStyle={[styles.header, animatedOpacity]} />
|
||||
@@ -115,17 +154,18 @@ const ArtistView = React.memo<{ id: string; title: string }>(({ id, title }) =>
|
||||
<Text style={styles.title}>{artist.name}</Text>
|
||||
</View>
|
||||
<View style={styles.contentContainer}>
|
||||
{artist.topSongs.length > 0 ? (
|
||||
<TopSongs songs={artist.topSongs} name={artist.name} artistId={artist.id} />
|
||||
{topSongs && albums ? (
|
||||
topSongs.length > 0 ? (
|
||||
<>
|
||||
<TopSongs songs={topSongs} name={artist.name} artistId={artist.id} />
|
||||
<ArtistAlbums albums={albums} />
|
||||
</>
|
||||
) : (
|
||||
<ArtistAlbums albums={albums} />
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
<ActivityIndicator size="large" color={colors.accent} style={styles.loading} />
|
||||
)}
|
||||
<Header>Albums</Header>
|
||||
<View style={styles.albums} onLayout={albumsLayout.onLayout}>
|
||||
{_albums.map(a => (
|
||||
<AlbumItem key={a.id} album={a} height={albumSize} width={albumSize} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</GradientScrollView>
|
||||
</View>
|
||||
@@ -200,6 +240,9 @@ const styles = StyleSheet.create({
|
||||
fontFamily: font.regular,
|
||||
textAlign: 'center',
|
||||
},
|
||||
loading: {
|
||||
marginTop: 30,
|
||||
},
|
||||
})
|
||||
|
||||
export default ArtistView
|
||||
|
||||
@@ -3,17 +3,17 @@ import CoverArt from '@app/components/CoverArt'
|
||||
import GradientScrollView from '@app/components/GradientScrollView'
|
||||
import Header from '@app/components/Header'
|
||||
import NothingHere from '@app/components/NothingHere'
|
||||
import { useActiveServerRefresh } from '@app/hooks/server'
|
||||
import { AlbumListItem } from '@app/models/music'
|
||||
import { selectMusic } from '@app/state/music'
|
||||
import { selectSettings } from '@app/state/settings'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { useActiveServerRefresh } from '@app/hooks/settings'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
import font from '@app/styles/font'
|
||||
import { GetAlbumListType } from '@app/subsonic/params'
|
||||
import { GetAlbumList2TypeBase, GetAlbumListType } from '@app/subsonic/params'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import React, { useCallback } from 'react'
|
||||
import equal from 'fast-deep-equal/es6/react'
|
||||
import produce from 'immer'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { RefreshControl, ScrollView, StatusBar, StyleSheet, Text, View } from 'react-native'
|
||||
import create, { StateSelector } from 'zustand'
|
||||
|
||||
const titles: { [key in GetAlbumListType]?: string } = {
|
||||
recent: 'Recently Played',
|
||||
@@ -23,9 +23,14 @@ const titles: { [key in GetAlbumListType]?: string } = {
|
||||
}
|
||||
|
||||
const AlbumItem = React.memo<{
|
||||
album: AlbumListItem
|
||||
}>(({ album }) => {
|
||||
id: string
|
||||
}>(({ id }) => {
|
||||
const navigation = useNavigation()
|
||||
const album = useStoreDeep(useCallback(store => store.library.albums[id], [id]))
|
||||
|
||||
if (!album) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<AlbumContextPressable
|
||||
@@ -49,9 +54,10 @@ const AlbumItem = React.memo<{
|
||||
})
|
||||
|
||||
const Category = React.memo<{
|
||||
name?: string
|
||||
data: AlbumListItem[]
|
||||
}>(({ name, data }) => {
|
||||
type: string
|
||||
}>(({ type }) => {
|
||||
const list = useHomeStoreDeep(useCallback(store => store.lists[type] || [], [type]))
|
||||
|
||||
const Albums = () => (
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
@@ -59,8 +65,8 @@ const Category = React.memo<{
|
||||
overScrollMode={'never'}
|
||||
style={styles.artScroll}
|
||||
contentContainerStyle={styles.artScrollContent}>
|
||||
{data.map(album => (
|
||||
<AlbumItem key={album.id} album={album} />
|
||||
{list.map(id => (
|
||||
<AlbumItem key={id} id={id} />
|
||||
))}
|
||||
</ScrollView>
|
||||
)
|
||||
@@ -73,24 +79,57 @@ const Category = React.memo<{
|
||||
|
||||
return (
|
||||
<View style={styles.category}>
|
||||
<Header style={styles.header}>{name}</Header>
|
||||
{data.length > 0 ? <Albums /> : <Nothing />}
|
||||
<Header style={styles.header}>{titles[type as GetAlbumListType] || ''}</Header>
|
||||
{list.length > 0 ? <Albums /> : <Nothing />}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
interface HomeState {
|
||||
lists: { [type: string]: string[] }
|
||||
setList: (type: string, list: string[]) => void
|
||||
}
|
||||
|
||||
const useHomeStore = create<HomeState>(set => ({
|
||||
lists: {},
|
||||
|
||||
setList: (type, list) => {
|
||||
set(
|
||||
produce<HomeState>(state => {
|
||||
state.lists[type] = list
|
||||
}),
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
function useHomeStoreDeep<U>(stateSelector: StateSelector<HomeState, U>) {
|
||||
return useHomeStore(stateSelector, equal)
|
||||
}
|
||||
|
||||
const Home = () => {
|
||||
const types = useStore(selectSettings.homeLists)
|
||||
const lists = useStore(selectMusic.homeLists)
|
||||
const updating = useStore(selectMusic.homeListsUpdating)
|
||||
const update = useStore(selectMusic.fetchHomeLists)
|
||||
const clear = useStore(selectMusic.clearHomeLists)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const types = useStoreDeep(store => store.settings.screens.home.listTypes)
|
||||
const fetchAlbumList = useStore(store => store.fetchAlbumList)
|
||||
const setList = useHomeStore(store => store.setList)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setRefreshing(true)
|
||||
|
||||
await Promise.all(
|
||||
types.map(async type => {
|
||||
const ids = await fetchAlbumList({ type: type as GetAlbumList2TypeBase, size: 20, offset: 0 })
|
||||
setList(type, ids)
|
||||
}),
|
||||
)
|
||||
|
||||
setRefreshing(false)
|
||||
}, [fetchAlbumList, setList, types])
|
||||
|
||||
useActiveServerRefresh(
|
||||
useCallback(() => {
|
||||
clear()
|
||||
update()
|
||||
}, [clear, update]),
|
||||
types.forEach(type => setList(type, []))
|
||||
refresh()
|
||||
}, [refresh, setList, types]),
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -99,15 +138,15 @@ const Home = () => {
|
||||
contentContainerStyle={styles.scrollContentContainer}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={updating}
|
||||
onRefresh={update}
|
||||
refreshing={refreshing}
|
||||
onRefresh={refresh}
|
||||
colors={[colors.accent, colors.accentLow]}
|
||||
progressViewOffset={StatusBar.currentHeight}
|
||||
/>
|
||||
}>
|
||||
<View style={styles.content}>
|
||||
{types.map(type => (
|
||||
<Category key={type} name={titles[type as GetAlbumListType]} data={type in lists ? lists[type] : []} />
|
||||
<Category key={type} type={type} />
|
||||
))}
|
||||
</View>
|
||||
</GradientScrollView>
|
||||
|
||||
@@ -3,24 +3,26 @@ import CoverArt from '@app/components/CoverArt'
|
||||
import FilterButton, { OptionData } from '@app/components/FilterButton'
|
||||
import GradientFlatList from '@app/components/GradientFlatList'
|
||||
import { useFetchPaginatedList } from '@app/hooks/list'
|
||||
import { Album, AlbumListItem } from '@app/models/music'
|
||||
import { selectMusic } from '@app/state/music'
|
||||
import { selectSettings } from '@app/state/settings'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
import font from '@app/styles/font'
|
||||
import { GetAlbumList2Type } from '@app/subsonic/params'
|
||||
import { GetAlbumList2Params, GetAlbumList2Type } from '@app/subsonic/params'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native'
|
||||
|
||||
const AlbumItem = React.memo<{
|
||||
album: AlbumListItem
|
||||
id: string
|
||||
size: number
|
||||
height: number
|
||||
}>(({ album, size, height }) => {
|
||||
}>(({ id, size, height }) => {
|
||||
const album = useStoreDeep(useCallback(store => store.library.albums[id], [id]))
|
||||
const navigation = useNavigation()
|
||||
|
||||
if (!album) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<AlbumContextPressable
|
||||
album={album}
|
||||
@@ -41,8 +43,8 @@ const AlbumItem = React.memo<{
|
||||
})
|
||||
|
||||
const AlbumListRenderItem: React.FC<{
|
||||
item: { album: Album; size: number; height: number }
|
||||
}> = ({ item }) => <AlbumItem album={item.album} size={item.size} height={item.height} />
|
||||
item: { id: string; size: number; height: number }
|
||||
}> = ({ item }) => <AlbumItem id={item.id} size={item.size} height={item.height} />
|
||||
|
||||
const filterOptions: OptionData[] = [
|
||||
{ text: 'By Name', value: 'alphabeticalByName' },
|
||||
@@ -57,24 +59,57 @@ const filterOptions: OptionData[] = [
|
||||
]
|
||||
|
||||
const AlbumsList = () => {
|
||||
const fetchAlbums = useStore(selectMusic.fetchAlbums)
|
||||
const { list, refreshing, refresh, reset, fetchNextPage } = useFetchPaginatedList(fetchAlbums, 300)
|
||||
const filter = useStore(selectSettings.libraryAlbumFilter)
|
||||
const setFilter = useStore(selectSettings.setLibraryAlbumFilter)
|
||||
const filter = useStoreDeep(store => store.settings.screens.library.albumsFilter)
|
||||
const setFilter = useStore(store => store.setLibraryAlbumFilter)
|
||||
|
||||
const fetchAlbumList = useStore(store => store.fetchAlbumList)
|
||||
const fetchPage = useCallback(
|
||||
(size: number, offset: number) => {
|
||||
let params: GetAlbumList2Params
|
||||
switch (filter.type) {
|
||||
case 'byYear':
|
||||
params = {
|
||||
size,
|
||||
offset,
|
||||
type: filter.type,
|
||||
fromYear: filter.fromYear,
|
||||
toYear: filter.toYear,
|
||||
}
|
||||
break
|
||||
case 'byGenre':
|
||||
params = {
|
||||
size,
|
||||
offset,
|
||||
type: filter.type,
|
||||
genre: filter.genre,
|
||||
}
|
||||
break
|
||||
default:
|
||||
params = {
|
||||
size,
|
||||
offset,
|
||||
type: filter.type,
|
||||
}
|
||||
break
|
||||
}
|
||||
return fetchAlbumList(params)
|
||||
},
|
||||
[fetchAlbumList, filter.fromYear, filter.genre, filter.toYear, filter.type],
|
||||
)
|
||||
|
||||
const { list, refreshing, refresh, fetchNextPage } = useFetchPaginatedList(fetchPage, 300)
|
||||
|
||||
const layout = useWindowDimensions()
|
||||
|
||||
const size = layout.width / 3 - styles.itemWrapper.marginHorizontal * 2
|
||||
const height = size + 36
|
||||
|
||||
useEffect(() => reset(), [reset, filter])
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<GradientFlatList
|
||||
data={list.map(album => ({ album, size, height }))}
|
||||
data={list.map(id => ({ id, size, height }))}
|
||||
renderItem={AlbumListRenderItem}
|
||||
keyExtractor={item => item.album.id}
|
||||
keyExtractor={item => item.id}
|
||||
numColumns={3}
|
||||
removeClippedSubviews={true}
|
||||
refreshing={refreshing}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import FilterButton, { OptionData } from '@app/components/FilterButton'
|
||||
import GradientFlatList from '@app/components/GradientFlatList'
|
||||
import ListItem from '@app/components/ListItem'
|
||||
import { useFetchList } from '@app/hooks/list'
|
||||
import { Artist } from '@app/models/music'
|
||||
import { useFetchList2 } from '@app/hooks/list'
|
||||
import { Artist } from '@app/models/library'
|
||||
import { ArtistFilterType } from '@app/models/settings'
|
||||
import { selectMusic } from '@app/state/music'
|
||||
import { selectSettings } from '@app/state/settings'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
|
||||
@@ -21,13 +19,17 @@ const filterOptions: OptionData[] = [
|
||||
]
|
||||
|
||||
const ArtistsList = () => {
|
||||
const fetchArtists = useStore(selectMusic.fetchArtists)
|
||||
const { list, refreshing, refresh } = useFetchList(fetchArtists)
|
||||
const filter = useStore(selectSettings.libraryArtistFilter)
|
||||
const setFilter = useStore(selectSettings.setLibraryArtistFiler)
|
||||
const fetchArtists = useStore(store => store.fetchArtists)
|
||||
const { refreshing, refresh } = useFetchList2(fetchArtists)
|
||||
const artists = useStoreDeep(store => store.library.artists)
|
||||
const artistOrder = useStoreDeep(store => store.library.artistOrder)
|
||||
|
||||
const filter = useStoreDeep(store => store.settings.screens.library.artistsFilter)
|
||||
const setFilter = useStore(store => store.setLibraryArtistFiler)
|
||||
const [sortedList, setSortedList] = useState<Artist[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const list = Object.values(artists)
|
||||
switch (filter.type) {
|
||||
case 'random':
|
||||
setSortedList([...list].sort(() => Math.random() - 0.5))
|
||||
@@ -35,11 +37,14 @@ const ArtistsList = () => {
|
||||
case 'starred':
|
||||
setSortedList([...list].filter(a => a.starred))
|
||||
break
|
||||
case 'alphabeticalByName':
|
||||
setSortedList(artistOrder.map(id => artists[id]))
|
||||
break
|
||||
default:
|
||||
setSortedList([...list])
|
||||
break
|
||||
}
|
||||
}, [list, filter])
|
||||
}, [filter.type, artists, artistOrder])
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import GradientFlatList from '@app/components/GradientFlatList'
|
||||
import ListItem from '@app/components/ListItem'
|
||||
import { useFetchList } from '@app/hooks/list'
|
||||
import { PlaylistListItem } from '@app/models/music'
|
||||
import { selectMusic } from '@app/state/music'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { useFetchList2 } from '@app/hooks/list'
|
||||
import { Playlist } from '@app/models/library'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import React from 'react'
|
||||
import { StyleSheet } from 'react-native'
|
||||
|
||||
const PlaylistRenderItem: React.FC<{ item: PlaylistListItem }> = ({ item }) => (
|
||||
const PlaylistRenderItem: React.FC<{ item: Playlist }> = ({ item }) => (
|
||||
<ListItem item={item} showArt={true} showStar={false} listStyle="big" style={styles.listItem} />
|
||||
)
|
||||
|
||||
const PlaylistsList = () => {
|
||||
const fetchPlaylists = useStore(selectMusic.fetchPlaylists)
|
||||
const { list, refreshing, refresh } = useFetchList(fetchPlaylists)
|
||||
const fetchPlaylists = useStore(store => store.fetchPlaylists)
|
||||
const { refreshing, refresh } = useFetchList2(fetchPlaylists)
|
||||
const playlists = useStoreDeep(store => store.library.playlists)
|
||||
|
||||
return (
|
||||
<GradientFlatList
|
||||
data={list}
|
||||
data={Object.values(playlists)}
|
||||
renderItem={PlaylistRenderItem}
|
||||
keyExtractor={item => item.id}
|
||||
onRefresh={refresh}
|
||||
|
||||
@@ -2,10 +2,8 @@ import GradientFlatList from '@app/components/GradientFlatList'
|
||||
import ListItem from '@app/components/ListItem'
|
||||
import NowPlayingBar from '@app/components/NowPlayingBar'
|
||||
import { useSkipTo } from '@app/hooks/trackplayer'
|
||||
import { Song } from '@app/models/music'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { selectTrackPlayer } from '@app/state/trackplayer'
|
||||
import { selectTrackPlayerMap } from '@app/state/trackplayermap'
|
||||
import { Song } from '@app/models/library'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import React from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
|
||||
@@ -27,8 +25,8 @@ const SongRenderItem: React.FC<{
|
||||
)
|
||||
|
||||
const NowPlayingQueue = React.memo<{}>(() => {
|
||||
const queue = useStore(selectTrackPlayer.queue)
|
||||
const mapTrackExtToSong = useStore(selectTrackPlayerMap.mapTrackExtToSong)
|
||||
const queue = useStoreDeep(store => store.queue)
|
||||
const mapTrackExtToSong = useStore(store => store.mapTrackExtToSong)
|
||||
const skipTo = useSkipTo()
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,13 +2,10 @@ import CoverArt from '@app/components/CoverArt'
|
||||
import HeaderBar from '@app/components/HeaderBar'
|
||||
import ImageGradientBackground from '@app/components/ImageGradientBackground'
|
||||
import PressableOpacity from '@app/components/PressableOpacity'
|
||||
import Star from '@app/components/Star'
|
||||
import { useStarred } from '@app/hooks/music'
|
||||
import { PressableStar } from '@app/components/Star'
|
||||
import { useNext, usePause, usePlay, usePrevious, useSeekTo } from '@app/hooks/trackplayer'
|
||||
import { selectMusic } from '@app/state/music'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { QueueContextType, selectTrackPlayer, TrackExt } from '@app/state/trackplayer'
|
||||
import { selectTrackPlayerMap } from '@app/state/trackplayermap'
|
||||
import { QueueContextType, TrackExt } from '@app/models/trackplayer'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
import font from '@app/styles/font'
|
||||
import formatDuration from '@app/util/formatDuration'
|
||||
@@ -41,9 +38,9 @@ function getContextName(type?: QueueContextType) {
|
||||
const NowPlayingHeader = React.memo<{
|
||||
track?: TrackExt
|
||||
}>(({ track }) => {
|
||||
const queueName = useStore(selectTrackPlayer.queueName)
|
||||
const queueContextType = useStore(selectTrackPlayer.queueContextType)
|
||||
const mapTrackExtToSong = useStore(selectTrackPlayerMap.mapTrackExtToSong)
|
||||
const queueName = useStore(store => store.queueName)
|
||||
const queueContextType = useStore(store => store.queueContextType)
|
||||
const mapTrackExtToSong = useStore(store => store.mapTrackExtToSong)
|
||||
|
||||
if (!track) {
|
||||
return <></>
|
||||
@@ -94,11 +91,11 @@ const headerStyles = StyleSheet.create({
|
||||
})
|
||||
|
||||
const SongCoverArt = () => {
|
||||
const track = useStore(selectTrackPlayer.currentTrack)
|
||||
const coverArt = useStore(store => store.currentTrack?.coverArt)
|
||||
|
||||
return (
|
||||
<View style={coverArtStyles.container}>
|
||||
<CoverArt type="cover" size="original" coverArt={track?.coverArt} style={coverArtStyles.image} />
|
||||
<CoverArt type="cover" size="original" coverArt={coverArt} style={coverArtStyles.image} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -117,26 +114,22 @@ const coverArtStyles = StyleSheet.create({
|
||||
})
|
||||
|
||||
const SongInfo = () => {
|
||||
const track = useStore(selectTrackPlayer.currentTrack)
|
||||
const id = track?.id || '-1'
|
||||
const type = 'song'
|
||||
const starred = useStarred(id, type)
|
||||
const setStarred = useStore(selectMusic.starItem)
|
||||
const id = useStore(store => store.currentTrack?.id)
|
||||
const artist = useStore(store => store.currentTrack?.artist)
|
||||
const title = useStore(store => store.currentTrack?.title)
|
||||
|
||||
return (
|
||||
<View style={infoStyles.container}>
|
||||
<View style={infoStyles.details}>
|
||||
<Text numberOfLines={1} style={infoStyles.title}>
|
||||
{track?.title}
|
||||
{title}
|
||||
</Text>
|
||||
<Text numberOfLines={1} style={infoStyles.artist}>
|
||||
{track?.artist}
|
||||
{artist}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={infoStyles.controls}>
|
||||
<PressableOpacity onPress={() => setStarred(id, type, starred)}>
|
||||
<Star size={32} starred={starred} />
|
||||
</PressableOpacity>
|
||||
<PressableStar id={id || '-1'} type={'song'} size={32} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -170,7 +163,8 @@ const infoStyles = StyleSheet.create({
|
||||
})
|
||||
|
||||
const SeekBar = () => {
|
||||
const { position, duration } = useStore(selectTrackPlayer.progress)
|
||||
const position = useStore(store => store.progress.position)
|
||||
const duration = useStore(store => store.progress.duration)
|
||||
const seekTo = useSeekTo()
|
||||
const [value, setValue] = useState(0)
|
||||
const [sliding, setSliding] = useState(false)
|
||||
@@ -262,15 +256,15 @@ const seekStyles = StyleSheet.create({
|
||||
})
|
||||
|
||||
const PlayerControls = () => {
|
||||
const state = useStore(selectTrackPlayer.playerState)
|
||||
const state = useStore(store => store.playerState)
|
||||
const play = usePlay()
|
||||
const pause = usePause()
|
||||
const next = useNext()
|
||||
const previous = usePrevious()
|
||||
const shuffled = useStore(selectTrackPlayer.shuffled)
|
||||
const toggleShuffle = useStore(selectTrackPlayer.toggleShuffle)
|
||||
const repeatMode = useStore(selectTrackPlayer.repeatMode)
|
||||
const toggleRepeat = useStore(selectTrackPlayer.toggleRepeatMode)
|
||||
const shuffled = useStore(store => !!store.shuffleOrder)
|
||||
const toggleShuffle = useStore(store => store.toggleShuffle)
|
||||
const repeatMode = useStore(store => store.repeatMode)
|
||||
const toggleRepeat = useStore(store => store.toggleRepeatMode)
|
||||
const navigation = useNavigation()
|
||||
|
||||
let playPauseIcon: string
|
||||
@@ -392,7 +386,7 @@ type RootStackParamList = {
|
||||
type NowPlayingProps = NativeStackScreenProps<RootStackParamList, 'main'>
|
||||
|
||||
const NowPlayingView: React.FC<NowPlayingProps> = ({ navigation }) => {
|
||||
const track = useStore(selectTrackPlayer.currentTrack)
|
||||
const track = useStoreDeep(store => store.currentTrack)
|
||||
|
||||
useEffect(() => {
|
||||
if (!track) {
|
||||
@@ -404,7 +398,7 @@ const NowPlayingView: React.FC<NowPlayingProps> = ({ navigation }) => {
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ImageGradientBackground imagePath={imagePath} />
|
||||
<ImageGradientBackground imagePath={imagePath} height={'100%'} />
|
||||
<NowPlayingHeader track={track} />
|
||||
<View style={styles.content}>
|
||||
<SongCoverArt />
|
||||
|
||||
@@ -4,13 +4,12 @@ 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'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { selectTrackPlayer } from '@app/state/trackplayer'
|
||||
import { useActiveServerRefresh } from '@app/hooks/settings'
|
||||
import { Song, Album, Artist, SearchResults } from '@app/models/library'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
import font from '@app/styles/font'
|
||||
import { mapById } from '@app/util/state'
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native'
|
||||
import debounce from 'lodash.debounce'
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||
@@ -25,7 +24,7 @@ import {
|
||||
} from 'react-native'
|
||||
|
||||
const SongItem = React.memo<{ item: Song }>(({ item }) => {
|
||||
const setQueue = useStore(selectTrackPlayer.setQueue)
|
||||
const setQueue = useStore(store => store.setQueue)
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@@ -42,8 +41,27 @@ const SongItem = React.memo<{ item: Song }>(({ item }) => {
|
||||
const ResultsCategory = React.memo<{
|
||||
name: string
|
||||
query: string
|
||||
items: ListableItem[]
|
||||
}>(({ name, query, items }) => {
|
||||
ids: string[]
|
||||
type: 'artist' | 'album' | 'song'
|
||||
}>(({ name, query, type, ids }) => {
|
||||
const items: (Album | Artist | Song)[] = useStoreDeep(
|
||||
useCallback(
|
||||
store => {
|
||||
switch (type) {
|
||||
case 'album':
|
||||
return mapById(store.library.albums, ids)
|
||||
case 'artist':
|
||||
return mapById(store.library.artists, ids)
|
||||
case 'song':
|
||||
return mapById(store.library.songs, ids)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
},
|
||||
[ids, type],
|
||||
),
|
||||
)
|
||||
|
||||
const navigation = useNavigation()
|
||||
|
||||
if (items.length === 0) {
|
||||
@@ -54,8 +72,8 @@ const ResultsCategory = React.memo<{
|
||||
<>
|
||||
<Header>{name}</Header>
|
||||
{items.map(a =>
|
||||
a.itemType === 'song' ? (
|
||||
<SongItem key={a.id} item={a} />
|
||||
type === 'song' ? (
|
||||
<SongItem key={a.id} item={a as Song} />
|
||||
) : (
|
||||
<ListItem key={a.id} item={a} showArt={true} showStar={false} />
|
||||
),
|
||||
@@ -78,15 +96,15 @@ const Results = React.memo<{
|
||||
}>(({ results, query }) => {
|
||||
return (
|
||||
<>
|
||||
<ResultsCategory name="Artists" query={query} items={results.artists} />
|
||||
<ResultsCategory name="Albums" query={query} items={results.albums} />
|
||||
<ResultsCategory name="Songs" query={query} items={results.songs} />
|
||||
<ResultsCategory name="Artists" query={query} type={'artist'} ids={results.artists} />
|
||||
<ResultsCategory name="Albums" query={query} type={'album'} ids={results.albums} />
|
||||
<ResultsCategory name="Songs" query={query} type={'song'} ids={results.songs} />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
const Search = () => {
|
||||
const fetchSearchResults = useStore(selectMusic.fetchSearchResults)
|
||||
const fetchSearchResults = useStore(store => store.fetchSearchResults)
|
||||
const [results, setResults] = useState<SearchResults>({ artists: [], albums: [], songs: [] })
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [text, setText] = useState('')
|
||||
@@ -118,7 +136,7 @@ const Search = () => {
|
||||
() =>
|
||||
debounce(async (query: string) => {
|
||||
setRefreshing(true)
|
||||
setResults(await fetchSearchResults(query))
|
||||
setResults(await fetchSearchResults({ query, albumCount: 5, artistCount: 5, songCount: 5 }))
|
||||
setRefreshing(false)
|
||||
}, 400),
|
||||
[fetchSearchResults],
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import GradientFlatList from '@app/components/GradientFlatList'
|
||||
import ListItem from '@app/components/ListItem'
|
||||
import { useFetchPaginatedList } from '@app/hooks/list'
|
||||
import { AlbumListItem, Artist, Song } from '@app/models/music'
|
||||
import { selectMusic } from '@app/state/music'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { selectTrackPlayer } from '@app/state/trackplayer'
|
||||
import { Album, Artist, Song } from '@app/models/library'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import { Search3Params } from '@app/subsonic/params'
|
||||
import { mapById } from '@app/util/state'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { StyleSheet } from 'react-native'
|
||||
|
||||
type SearchListItemType = AlbumListItem | Song | Artist
|
||||
type SearchListItemType = Album | Song | Artist
|
||||
|
||||
const ResultsListItem: React.FC<{ item: SearchListItemType }> = ({ item }) => {
|
||||
const setQueue = useStore(selectTrackPlayer.setQueue)
|
||||
const setQueue = useStore(store => store.setQueue)
|
||||
|
||||
let onPress
|
||||
if (item.itemType === 'song') {
|
||||
@@ -40,27 +40,62 @@ const SearchResultsView: React.FC<{
|
||||
type: 'album' | 'artist' | 'song'
|
||||
}> = ({ query, type }) => {
|
||||
const navigation = useNavigation()
|
||||
const fetchSearchResults = useStore(selectMusic.fetchSearchResults)
|
||||
const { list, refreshing, refresh, fetchNextPage } = useFetchPaginatedList<SearchListItemType>(
|
||||
const fetchSearchResults = useStore(store => store.fetchSearchResults)
|
||||
const { list, refreshing, refresh, fetchNextPage } = useFetchPaginatedList(
|
||||
useCallback(
|
||||
(size, offset) =>
|
||||
fetchSearchResults(query, type, size, offset).then(results => {
|
||||
switch (type) {
|
||||
case 'album':
|
||||
return results.albums
|
||||
case 'artist':
|
||||
return results.artists
|
||||
case 'song':
|
||||
return results.songs
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}),
|
||||
async (size, offset) => {
|
||||
const params: Search3Params = { query }
|
||||
if (type === 'album') {
|
||||
params.albumCount = size
|
||||
params.albumOffset = offset
|
||||
} else if (type === 'artist') {
|
||||
params.artistCount = size
|
||||
params.artistOffset = offset
|
||||
} else if (type === 'song') {
|
||||
params.songCount = size
|
||||
params.songOffset = offset
|
||||
} else {
|
||||
params.albumCount = 5
|
||||
params.artistCount = 5
|
||||
params.songCount = 5
|
||||
}
|
||||
|
||||
const results = await fetchSearchResults(params)
|
||||
|
||||
switch (type) {
|
||||
case 'album':
|
||||
return results.albums
|
||||
case 'artist':
|
||||
return results.artists
|
||||
case 'song':
|
||||
return results.songs
|
||||
default:
|
||||
return []
|
||||
}
|
||||
},
|
||||
[fetchSearchResults, query, type],
|
||||
),
|
||||
100,
|
||||
)
|
||||
|
||||
const items: SearchListItemType[] = useStoreDeep(
|
||||
useCallback(
|
||||
store => {
|
||||
switch (type) {
|
||||
case 'album':
|
||||
return mapById(store.library.albums, list)
|
||||
case 'artist':
|
||||
return mapById(store.library.artists, list)
|
||||
case 'song':
|
||||
return mapById(store.library.songs, list)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
},
|
||||
[list, type],
|
||||
),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
title: `Search: "${query}"`,
|
||||
@@ -70,7 +105,7 @@ const SearchResultsView: React.FC<{
|
||||
|
||||
return (
|
||||
<GradientFlatList
|
||||
data={list}
|
||||
data={items}
|
||||
renderItem={SearchResultsRenderItem}
|
||||
keyExtractor={(item, i) => i.toString()}
|
||||
onRefresh={refresh}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Button from '@app/components/Button'
|
||||
import GradientScrollView from '@app/components/GradientScrollView'
|
||||
import { Server } from '@app/models/settings'
|
||||
import { selectSettings } from '@app/state/settings'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
import font from '@app/styles/font'
|
||||
import toast from '@app/util/toast'
|
||||
@@ -19,13 +18,13 @@ const ServerView: React.FC<{
|
||||
id?: string
|
||||
}> = ({ id }) => {
|
||||
const navigation = useNavigation()
|
||||
const activeServer = useStore(selectSettings.activeServer)
|
||||
const servers = useStore(selectSettings.servers)
|
||||
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 pingServer = useStore(selectSettings.pingServer)
|
||||
const activeServerId = useStore(store => store.settings.activeServerId)
|
||||
const servers = useStoreDeep(store => store.settings.servers)
|
||||
const addServer = useStore(store => store.addServer)
|
||||
const updateServer = useStore(store => store.updateServer)
|
||||
const removeServer = useStore(store => store.removeServer)
|
||||
const server = id ? servers[id] : undefined
|
||||
const pingServer = useStore(store => store.pingServer)
|
||||
|
||||
const [address, setAddress] = useState(server?.address || '')
|
||||
const [username, setUsername] = useState(server?.username || '')
|
||||
@@ -44,8 +43,8 @@ const ServerView: React.FC<{
|
||||
}, [address, username, password])
|
||||
|
||||
const canRemove = useCallback(() => {
|
||||
return id && servers.length > 1 && activeServer?.id !== id
|
||||
}, [id, servers, activeServer])
|
||||
return id && Object.keys(servers).length > 1 && activeServerId !== id
|
||||
}, [id, servers, activeServerId])
|
||||
|
||||
const exit = useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
|
||||
@@ -5,11 +5,9 @@ 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 { useSwitchActiveServer } from '@app/hooks/settings'
|
||||
import { Server } from '@app/models/settings'
|
||||
import { selectCache } from '@app/state/cache'
|
||||
import { selectSettings } from '@app/state/settings'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
import font from '@app/styles/font'
|
||||
import { useNavigation } from '@react-navigation/core'
|
||||
@@ -22,7 +20,7 @@ import { version } from '../../package.json'
|
||||
const ServerItem = React.memo<{
|
||||
server: Server
|
||||
}>(({ server }) => {
|
||||
const activeServer = useStore(selectSettings.activeServer)
|
||||
const activeServerId = useStore(store => store.settings.activeServerId)
|
||||
const switchActiveServer = useSwitchActiveServer()
|
||||
const navigation = useNavigation()
|
||||
|
||||
@@ -36,7 +34,7 @@ const ServerItem = React.memo<{
|
||||
subtitle={server.username}
|
||||
onPress={() => navigation.navigate('server', { id: server.id })}>
|
||||
<PressableOpacity style={styles.serverActive} onPress={setActive}>
|
||||
{activeServer && activeServer.id === server.id ? (
|
||||
{activeServerId === server.id ? (
|
||||
<Icon name="checkbox-marked-circle" size={30} color={colors.accent} />
|
||||
) : (
|
||||
<Icon name="checkbox-blank-circle-outline" size={30} color={colors.text.secondary} />
|
||||
@@ -193,27 +191,22 @@ function secondsUnit(seconds: string): string {
|
||||
}
|
||||
|
||||
const SettingsContent = React.memo(() => {
|
||||
const servers = useStore(selectSettings.servers)
|
||||
const scrobble = useStore(selectSettings.scrobble)
|
||||
const setScrobble = useStore(selectSettings.setScrobble)
|
||||
const servers = useStoreDeep(store => store.settings.servers)
|
||||
const scrobble = useStore(store => store.settings.scrobble)
|
||||
const setScrobble = useStore(store => store.setScrobble)
|
||||
|
||||
// doesn't seem to ever be a case where we want this off
|
||||
// will remove later if there isn't a use case for disabling
|
||||
// const estimateContentLength = useStore(selectSettings.estimateContentLength)
|
||||
// const setEstimateContentLength = useStore(selectSettings.setEstimateContentLength)
|
||||
const maxBitrateWifi = useStore(store => store.settings.maxBitrateWifi)
|
||||
const setMaxBitrateWifi = useStore(store => store.setMaxBitrateWifi)
|
||||
|
||||
const maxBitrateWifi = useStore(selectSettings.maxBitrateWifi)
|
||||
const setMaxBitrateWifi = useStore(selectSettings.setMaxBitrateWifi)
|
||||
const maxBitrateMobile = useStore(store => store.settings.maxBitrateMobile)
|
||||
const setMaxBitrateMobile = useStore(store => store.setMaxBitrateMobile)
|
||||
|
||||
const maxBitrateMobile = useStore(selectSettings.maxBitrateMobile)
|
||||
const setMaxBitrateMobile = useStore(selectSettings.setMaxBitrateMobile)
|
||||
const minBuffer = useStore(store => store.settings.minBuffer)
|
||||
const setMinBuffer = useStore(store => store.setMinBuffer)
|
||||
const maxBuffer = useStore(store => store.settings.maxBuffer)
|
||||
const setMaxBuffer = useStore(store => store.setMaxBuffer)
|
||||
|
||||
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 clearImageCache = useStore(store => store.clearImageCache)
|
||||
const [clearing, setClearing] = useState(false)
|
||||
|
||||
const navigation = useNavigation()
|
||||
@@ -239,7 +232,7 @@ const SettingsContent = React.memo(() => {
|
||||
return (
|
||||
<View style={styles.content}>
|
||||
<Header>Servers</Header>
|
||||
{servers.map(s => (
|
||||
{Object.values(servers).map(s => (
|
||||
<ServerItem key={s.id} server={s} />
|
||||
))}
|
||||
<Button
|
||||
@@ -251,12 +244,6 @@ const SettingsContent = React.memo(() => {
|
||||
<Header style={styles.header}>Network</Header>
|
||||
<BitrateModal title="Maximum bitrate (Wi-Fi)" bitrate={maxBitrateWifi} setBitrate={setMaxBitrateWifi} />
|
||||
<BitrateModal title="Maximum bitrate (mobile)" bitrate={maxBitrateMobile} setBitrate={setMaxBitrateMobile} />
|
||||
{/* <SettingsSwitch
|
||||
title="Estimate content length"
|
||||
subtitle='Send the "estimateContentLength" flag when streaming. Helps fix issues with seeking when the server is transcoding songs.'
|
||||
value={estimateContentLength}
|
||||
setValue={setEstimateContentLength}
|
||||
/> */}
|
||||
<SettingsTextModal
|
||||
title="Minimum buffer time"
|
||||
value={minBuffer.toString()}
|
||||
|
||||
@@ -4,14 +4,13 @@ import HeaderBar from '@app/components/HeaderBar'
|
||||
import ImageGradientFlatList from '@app/components/ImageGradientFlatList'
|
||||
import ListItem from '@app/components/ListItem'
|
||||
import ListPlayerControls from '@app/components/ListPlayerControls'
|
||||
import NothingHere from '@app/components/NothingHere'
|
||||
import { useCoverArtFile } from '@app/hooks/cache'
|
||||
import { useAlbumWithSongs, usePlaylistWithSongs } from '@app/hooks/music'
|
||||
import { AlbumWithSongs, PlaylistWithSongs, Song } from '@app/models/music'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { selectTrackPlayer } from '@app/state/trackplayer'
|
||||
import { Song, Album, Playlist } from '@app/models/library'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
import font from '@app/styles/font'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
|
||||
|
||||
type SongListType = 'album' | 'playlist'
|
||||
@@ -46,18 +45,19 @@ const SongRenderItem: React.FC<{
|
||||
const SongListDetails = React.memo<{
|
||||
title: string
|
||||
type: SongListType
|
||||
songList?: AlbumWithSongs | PlaylistWithSongs
|
||||
songList?: Album | Playlist
|
||||
songs?: Song[]
|
||||
subtitle?: string
|
||||
}>(({ title, songList, subtitle, type }) => {
|
||||
}>(({ title, songList, songs, subtitle, type }) => {
|
||||
const coverArtFile = useCoverArtFile(songList?.coverArt, 'thumbnail')
|
||||
const [headerColor, setHeaderColor] = useState<string | undefined>(undefined)
|
||||
const setQueue = useStore(selectTrackPlayer.setQueue)
|
||||
const setQueue = useStore(store => store.setQueue)
|
||||
|
||||
if (!songList) {
|
||||
return <SongListDetailsFallback />
|
||||
}
|
||||
|
||||
const _songs = [...songList.songs]
|
||||
const _songs = [...(songs || [])]
|
||||
let typeName = ''
|
||||
|
||||
if (type === 'album') {
|
||||
@@ -101,21 +101,26 @@ const SongListDetails = React.memo<{
|
||||
overScrollMode="never"
|
||||
windowSize={7}
|
||||
contentMarginTop={26}
|
||||
ListEmptyComponent={
|
||||
songs ? (
|
||||
<NothingHere style={styles.nothing} />
|
||||
) : (
|
||||
<ActivityIndicator size="large" color={colors.accent} style={styles.listLoading} />
|
||||
)
|
||||
}
|
||||
ListHeaderComponent={
|
||||
<View style={styles.content}>
|
||||
<CoverArt type="cover" size="original" coverArt={songList.coverArt} style={styles.cover} />
|
||||
<Text style={styles.title}>{songList.name}</Text>
|
||||
{subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : <></>}
|
||||
{songList.songs.length > 0 && (
|
||||
<ListPlayerControls
|
||||
style={styles.controls}
|
||||
songs={_songs}
|
||||
typeName={typeName}
|
||||
queueName={songList.name}
|
||||
queueContextId={songList.id}
|
||||
queueContextType={type}
|
||||
/>
|
||||
)}
|
||||
<ListPlayerControls
|
||||
style={styles.controls}
|
||||
songs={_songs}
|
||||
typeName={typeName}
|
||||
queueName={songList.name}
|
||||
queueContextId={songList.id}
|
||||
queueContextType={type}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
@@ -127,19 +132,58 @@ const PlaylistView = React.memo<{
|
||||
id: string
|
||||
title: string
|
||||
}>(({ id, title }) => {
|
||||
const playlist = usePlaylistWithSongs(id)
|
||||
return <SongListDetails title={title} songList={playlist} subtitle={playlist?.comment} type="playlist" />
|
||||
const playlist = useStoreDeep(useCallback(store => store.library.playlists[id], [id]))
|
||||
const songs = useStoreDeep(
|
||||
useCallback(
|
||||
store => {
|
||||
const ids = store.library.playlistSongs[id]
|
||||
return ids ? ids.map(i => store.library.songs[i]) : undefined
|
||||
},
|
||||
[id],
|
||||
),
|
||||
)
|
||||
|
||||
const fetchPlaylist = useStore(store => store.fetchPlaylist)
|
||||
|
||||
useEffect(() => {
|
||||
if (!playlist || !songs) {
|
||||
fetchPlaylist(id)
|
||||
}
|
||||
}, [playlist, fetchPlaylist, id, songs])
|
||||
|
||||
return (
|
||||
<SongListDetails title={title} songList={playlist} songs={songs} subtitle={playlist?.comment} type="playlist" />
|
||||
)
|
||||
})
|
||||
|
||||
const AlbumView = React.memo<{
|
||||
id: string
|
||||
title: string
|
||||
}>(({ id, title }) => {
|
||||
const album = useAlbumWithSongs(id)
|
||||
const album = useStoreDeep(useCallback(store => store.library.albums[id], [id]))
|
||||
const songs = useStoreDeep(
|
||||
useCallback(
|
||||
store => {
|
||||
const ids = store.library.albumSongs[id]
|
||||
return ids ? ids.map(i => store.library.songs[i]) : undefined
|
||||
},
|
||||
[id],
|
||||
),
|
||||
)
|
||||
|
||||
const fetchAlbum = useStore(store => store.fetchAlbum)
|
||||
|
||||
useEffect(() => {
|
||||
if (!album || !songs) {
|
||||
fetchAlbum(id)
|
||||
}
|
||||
}, [album, fetchAlbum, id, songs])
|
||||
|
||||
return (
|
||||
<SongListDetails
|
||||
title={title}
|
||||
songList={album}
|
||||
songs={songs}
|
||||
subtitle={(album?.artist || '') + (album?.year ? ' • ' + album?.year : '')}
|
||||
type="album"
|
||||
/>
|
||||
@@ -196,6 +240,12 @@ const styles = StyleSheet.create({
|
||||
listItem: {
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
nothing: {
|
||||
width: '100%',
|
||||
},
|
||||
listLoading: {
|
||||
marginTop: 10,
|
||||
},
|
||||
})
|
||||
|
||||
export default SongListView
|
||||
|
||||
@@ -45,7 +45,7 @@ const SplashPage: React.FC<{}> = ({ children }) => {
|
||||
|
||||
const splash = (
|
||||
<Animated.View style={[styles.splashContainer, animatedStyles]} pointerEvents="none">
|
||||
<GradientBackground style={styles.background}>
|
||||
<GradientBackground style={styles.background} height="100%">
|
||||
<View style={styles.logoContainer}>
|
||||
<Image style={styles.image} source={require('@res/casette.png')} fadeDuration={0} />
|
||||
<Text style={styles.text}>subtracks</Text>
|
||||
|
||||
Reference in New Issue
Block a user