added paginated list/album list

This commit is contained in:
austinried
2022-03-15 16:00:06 +09:00
parent c45784bcbe
commit 76306f1558
6 changed files with 188 additions and 42 deletions

View File

@@ -28,7 +28,7 @@ export const useFetchList = <T>(fetchList: () => Promise<T[]>) => {
return { list, refreshing, refresh, reset } return { list, refreshing, refresh, reset }
} }
export const useFetchList2 = (fetchList: () => Promise<void>, resetList: () => Promise<void>) => { export const useFetchList2 = (fetchList: () => Promise<void>, resetList: () => void) => {
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
@@ -39,9 +39,9 @@ export const useFetchList2 = (fetchList: () => Promise<void>, resetList: () => P
useActiveServerRefresh( useActiveServerRefresh(
useCallback(async () => { useCallback(async () => {
await resetList() resetList()
await fetchList() await refresh()
}, [fetchList, resetList]), }, [refresh, resetList]),
) )
return { refreshing, refresh } return { refreshing, refresh }
@@ -94,3 +94,32 @@ export const useFetchPaginatedList = <T>(
return { list, refreshing, refresh, reset, fetchNextPage } return { list, refreshing, refresh, reset, fetchNextPage }
} }
export const useFetchPaginatedList2 = (fetchNextListPage: () => Promise<void>, resetList: () => void) => {
const [refreshing, setRefreshing] = useState(false)
const refresh = useCallback(async () => {
setRefreshing(true)
resetList()
await fetchNextListPage()
setRefreshing(false)
}, [fetchNextListPage, resetList])
useActiveServerRefresh(
useCallback(async () => {
await refresh()
}, [refresh]),
)
const fetchNextPage = useCallback(async () => {
setRefreshing(true)
await fetchNextListPage()
setRefreshing(false)
}, [fetchNextListPage])
return { refreshing, refresh, fetchNextPage }
}

View File

@@ -5,7 +5,6 @@ import GradientScrollView from '@app/components/GradientScrollView'
import Header from '@app/components/Header' import Header from '@app/components/Header'
import HeaderBar from '@app/components/HeaderBar' import HeaderBar from '@app/components/HeaderBar'
import ListItem from '@app/components/ListItem' import ListItem from '@app/components/ListItem'
import { useArtistInfo } from '@app/hooks/music'
import { Album, Song } from '@app/models/music' import { Album, Song } from '@app/models/music'
import { useStore } from '@app/state/store' import { useStore } from '@app/state/store'
import { selectTrackPlayer } from '@app/state/trackplayer' import { selectTrackPlayer } from '@app/state/trackplayer'
@@ -15,7 +14,7 @@ import font from '@app/styles/font'
import { useLayout } from '@react-native-community/hooks' import { useLayout } from '@react-native-community/hooks'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import pick from 'lodash.pick' import pick from 'lodash.pick'
import React, { useEffect } from 'react' import React, { useCallback, useEffect } from 'react'
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native' import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
import { useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated' import { useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
@@ -71,10 +70,16 @@ const TopSongs = React.memo<{
const ArtistAlbums = React.memo<{ const ArtistAlbums = React.memo<{
id: string id: string
}>(({ id }) => { }>(({ id }) => {
const albums = useStore(store => { const albums = useStore(
const ids = store.entities.artistAlbums[id] useCallback(
return ids ? pick(store.entities.albums, ids) : undefined store => {
}) const ids = store.entities.artistAlbums[id]
return ids ? pick(store.entities.albums, ids) : undefined
},
[id],
),
)
const fetchArtist = useStore(store => store.fetchLibraryArtist) const fetchArtist = useStore(store => store.fetchLibraryArtist)
const albumsLayout = useLayout() const albumsLayout = useLayout()
@@ -109,10 +114,8 @@ const ArtistViewFallback = React.memo(() => (
)) ))
const ArtistView = React.memo<{ id: string; title: string }>(({ id, title }) => { const ArtistView = React.memo<{ id: string; title: string }>(({ id, title }) => {
// const artist = useArtistInfo(id) const artist = useStore(useCallback(store => store.entities.artists[id], [id]))
const artistInfo = useStore(useCallback(store => store.entities.artistInfo[id], [id]))
const artist = useStore(store => store.entities.artists[id])
const artistInfo = useStore(store => store.entities.artistInfo[id])
const fetchArtist = useStore(store => store.fetchLibraryArtist) const fetchArtist = useStore(store => store.fetchLibraryArtist)
const fetchArtistInfo = useStore(store => store.fetchLibraryArtistInfo) const fetchArtistInfo = useStore(store => store.fetchLibraryArtistInfo)

View File

@@ -2,11 +2,10 @@ import { AlbumContextPressable } from '@app/components/ContextMenu'
import CoverArt from '@app/components/CoverArt' import CoverArt from '@app/components/CoverArt'
import FilterButton, { OptionData } from '@app/components/FilterButton' import FilterButton, { OptionData } from '@app/components/FilterButton'
import GradientFlatList from '@app/components/GradientFlatList' import GradientFlatList from '@app/components/GradientFlatList'
import { useFetchPaginatedList } from '@app/hooks/list' import { useFetchPaginatedList2 } from '@app/hooks/list'
import { Album, AlbumListItem } from '@app/models/music' import { Album, AlbumListItem } from '@app/models/music'
import { selectMusic } from '@app/state/music'
import { selectSettings } from '@app/state/settings' import { selectSettings } from '@app/state/settings'
import { useStore } from '@app/state/store' import { Store, useStore } from '@app/state/store'
import colors from '@app/styles/colors' import colors from '@app/styles/colors'
import font from '@app/styles/font' import font from '@app/styles/font'
import { GetAlbumList2Type } from '@app/subsonic/params' import { GetAlbumList2Type } from '@app/subsonic/params'
@@ -56,9 +55,19 @@ const filterOptions: OptionData[] = [
// { text: 'By Genre...', value: 'byGenre' }, // { text: 'By Genre...', value: 'byGenre' },
] ]
const selectAlbumList = (store: Store) => {
return Object.values(store.entities.albumsList)
.flat()
.map(id => store.entities.albums[id])
}
const AlbumsList = () => { const AlbumsList = () => {
const fetchAlbums = useStore(selectMusic.fetchAlbums) const list = useStore(selectAlbumList)
const { list, refreshing, refresh, reset, fetchNextPage } = useFetchPaginatedList(fetchAlbums, 300)
const fetchAlbumsNextPage = useStore(store => store.fetchLibraryAlbumsNextPage)
const resetAlbumsList = useStore(store => store.resetLibraryAlbumsList)
const { refreshing, refresh, fetchNextPage } = useFetchPaginatedList2(fetchAlbumsNextPage, resetAlbumsList)
const filter = useStore(selectSettings.libraryAlbumFilter) const filter = useStore(selectSettings.libraryAlbumFilter)
const setFilter = useStore(selectSettings.setLibraryAlbumFilter) const setFilter = useStore(selectSettings.setLibraryAlbumFilter)
@@ -67,7 +76,9 @@ const AlbumsList = () => {
const size = layout.width / 3 - styles.itemWrapper.marginHorizontal * 2 const size = layout.width / 3 - styles.itemWrapper.marginHorizontal * 2
const height = size + 36 const height = size + 36
useEffect(() => reset(), [reset, filter]) useEffect(() => {
refresh()
}, [refresh, filter])
return ( return (
<View style={styles.container}> <View style={styles.container}>

View File

@@ -1,13 +1,12 @@
import FilterButton, { OptionData } from '@app/components/FilterButton' import FilterButton, { OptionData } from '@app/components/FilterButton'
import GradientFlatList from '@app/components/GradientFlatList' import GradientFlatList from '@app/components/GradientFlatList'
import ListItem from '@app/components/ListItem' import ListItem from '@app/components/ListItem'
import { useFetchList, useFetchList2 } from '@app/hooks/list' import { useFetchList2 } from '@app/hooks/list'
import { Artist } from '@app/models/music' import { Artist } from '@app/models/music'
import { ArtistFilterType } from '@app/models/settings' import { ArtistFilterType } from '@app/models/settings'
import { selectMusic } from '@app/state/music'
import { selectSettings } from '@app/state/settings' import { selectSettings } from '@app/state/settings'
import { useStore } from '@app/state/store' import { Store, useStore } from '@app/state/store'
import React, { useCallback, useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
const ArtistRenderItem: React.FC<{ item: Artist }> = ({ item }) => ( const ArtistRenderItem: React.FC<{ item: Artist }> = ({ item }) => (
@@ -20,12 +19,14 @@ const filterOptions: OptionData[] = [
{ text: 'Random', value: 'random' }, { text: 'Random', value: 'random' },
] ]
const selectArtists = (store: Store) => store.entities.artists
const ArtistsList = () => { const ArtistsList = () => {
const fetchArtists = useStore(store => store.fetchLibraryArtists) const fetchArtists = useStore(store => store.fetchLibraryArtists)
const resetArtists = useStore(store => store.resetLibraryArtists) const resetArtists = useStore(store => store.resetLibraryArtists)
const { refreshing, refresh } = useFetchList2(fetchArtists, resetArtists) const { refreshing, refresh } = useFetchList2(fetchArtists, resetArtists)
const artists = useStore(store => store.entities.artists) const artists = useStore(selectArtists)
const filter = useStore(selectSettings.libraryArtistFilter) const filter = useStore(selectSettings.libraryArtistFilter)
const setFilter = useStore(selectSettings.setLibraryArtistFiler) const setFilter = useStore(selectSettings.setLibraryArtistFiler)

View File

@@ -1,6 +1,8 @@
import { Store } from '@app/state/store' import { Store } from '@app/state/store'
import { AlbumID3Element, ArtistID3Element, ArtistInfo2Element, ChildElement } from '@app/subsonic/elements' import { AlbumID3Element, ArtistID3Element, ArtistInfo2Element, ChildElement } from '@app/subsonic/elements'
import { GetAlbumList2Params } from '@app/subsonic/params'
import { import {
GetAlbumList2Response,
GetArtistInfo2Response, GetArtistInfo2Response,
GetArtistResponse, GetArtistResponse,
GetArtistsResponse, GetArtistsResponse,
@@ -18,6 +20,15 @@ export interface ById<T> {
export type OneToMany = ById<string[]> export type OneToMany = ById<string[]>
export interface OrderedById<T> {
byId: ById<T>
allIds: string[]
}
export interface PaginatedList {
[offset: number]: string[]
}
export interface Artist { export interface Artist {
itemType: 'artist' itemType: 'artist'
id: string id: string
@@ -111,35 +122,55 @@ function mapSong(song: ChildElement): Song {
export type LibrarySlice = { export type LibrarySlice = {
entities: { entities: {
artists: ById<Artist> artists: ById<Artist>
artistInfo: ById<ArtistInfo>
artistAlbums: OneToMany artistAlbums: OneToMany
artistNameTopSongs: OneToMany
albums: ById<Album> albums: ById<Album>
albumsList: PaginatedList
artistInfo: ById<ArtistInfo> albumsListSize: number
artistNameTopSongs: OneToMany
songs: ById<Song> songs: ById<Song>
} }
resetLibrary: () => void
fetchLibraryArtists: () => Promise<void> fetchLibraryArtists: () => Promise<void>
fetchLibraryArtist: (id: string) => Promise<void> fetchLibraryArtist: (id: string) => Promise<void>
resetLibraryArtists: () => Promise<void>
// fetchAlbums: (artistId: string) => Promise<void>
fetchLibraryArtistInfo: (artistId: string) => Promise<void> fetchLibraryArtistInfo: (artistId: string) => Promise<void>
resetLibraryArtists: () => void
fetchLibraryTopSongs: (artistName: string) => Promise<void> fetchLibraryTopSongs: (artistName: string) => Promise<void>
fetchLibraryAlbumsNextPage: () => Promise<void>
resetLibraryAlbumsList: () => void
} }
function nextOffest(list: PaginatedList): number {
const pages = Object.keys(list).map(k => parseInt(k, 10))
return pages.length > 0 ? pages.sort((a, b) => a - b)[pages.length - 1] : 0
}
const defaultEntities = () => ({
artists: {},
artistAlbums: {},
artistInfo: {},
artistNameTopSongs: {},
albums: {},
albumsList: {},
albumsListSize: 300,
songs: {},
})
export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>): LibrarySlice => ({ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>): LibrarySlice => ({
entities: { entities: defaultEntities(),
artists: {},
artistAlbums: {},
albums: {}, resetLibrary: () => {
set(store => {
artistInfo: {}, store.entities = defaultEntities()
artistNameTopSongs: {}, })
songs: {},
}, },
fetchLibraryArtists: async () => { fetchLibraryArtists: async () => {
@@ -192,12 +223,12 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
produce<LibrarySlice>(state => { produce<LibrarySlice>(state => {
state.entities.artists[id] = artist state.entities.artists[id] = artist
state.entities.artistAlbums[id] = Object.keys(albums) state.entities.artistAlbums[id] = Object.keys(albums)
state.entities.albums = merge(state.entities.albums, albums) merge(state.entities.albums, albums)
}), }),
) )
}, },
resetLibraryArtists: async () => { resetLibraryArtists: () => {
set( set(
produce<LibrarySlice>(state => { produce<LibrarySlice>(state => {
state.entities.artists = {} state.entities.artists = {}
@@ -245,9 +276,78 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
set( set(
produce<LibrarySlice>(state => { produce<LibrarySlice>(state => {
state.entities.songs = merge(state.entities.songs, topSongs) merge(state.entities.songs, topSongs)
state.entities.artistNameTopSongs[artistName] = topSongs.map(s => s.id) state.entities.artistNameTopSongs[artistName] = topSongs.map(s => s.id)
}), }),
) )
}, },
fetchLibraryAlbumsNextPage: async () => {
const client = get().client
if (!client) {
return
}
const filter = get().settings.screens.library.albums
const size = get().entities.albumsListSize
const offset = nextOffest(get().entities.albumsList)
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
}
let response: SubsonicResponse<GetAlbumList2Response>
try {
response = await client.getAlbumList2(params)
} catch {
return
}
const albums = response.data.albums.reduce((acc, value) => {
acc[value.id] = mapAlbum(value)
return acc
}, {} as ById<Album>)
set(
produce<LibrarySlice>(state => {
if (response.data.albums.length <= 0) {
return
}
merge(state.entities.albums, albums)
state.entities.albumsList[offset + size] = response.data.albums.map(a => a.id)
}),
)
},
resetLibraryAlbumsList: () => {
set(
produce<LibrarySlice>(state => {
state.entities.albumsList = {}
}),
)
},
}) })

View File

@@ -117,6 +117,8 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
state.client = new SubsonicApiClient(newActiveServer) state.client = new SubsonicApiClient(newActiveServer)
}), }),
) )
get().resetLibrary()
}, },
getActiveServer: () => get().settings.servers.find(s => s.id === get().settings.activeServer), getActiveServer: () => get().settings.servers.find(s => s.id === get().settings.activeServer),