refactored library lists to enable pagination

This commit is contained in:
austinried 2021-08-18 16:09:42 +09:00
parent b99eee9034
commit ba2aea0fbe
9 changed files with 116 additions and 121 deletions

69
app/hooks/list.ts Normal file
View File

@ -0,0 +1,69 @@
import { useState, useCallback } from 'react'
import { useActiveServerRefresh } from './server'
export const useFetchList = <T>(fetchList: () => Promise<T[]>) => {
const [list, setList] = useState<T[]>([])
const [refreshing, setRefreshing] = useState(false)
const refresh = useCallback(() => {
setRefreshing(true)
fetchList().then(items => {
setList(items)
setRefreshing(false)
})
}, [fetchList])
useActiveServerRefresh(
useCallback(() => {
setList([])
refresh()
}, [refresh]),
)
return { list, refreshing, refresh }
}
export const useFetchPaginatedList = <T>(
fetchList: (size?: number, offset?: number) => Promise<T[]>,
pageSize: number,
) => {
const [list, setList] = useState<T[]>([])
const [refreshing, setRefreshing] = useState(false)
const [offset, setOffset] = useState(0)
const refresh = useCallback(() => {
setOffset(0)
setRefreshing(true)
fetchList(pageSize, 0).then(firstPage => {
setList(firstPage)
setRefreshing(false)
})
}, [fetchList, pageSize])
useActiveServerRefresh(
useCallback(() => {
setList([])
refresh()
}, [refresh]),
)
const fetchNextPage = useCallback(() => {
const newOffset = offset + pageSize
setRefreshing(true)
fetchList(pageSize, newOffset).then(nextPage => {
setRefreshing(false)
if (nextPage.length === 0) {
return
}
setList([...list, ...nextPage])
setOffset(newOffset)
})
}, [offset, pageSize, fetchList, list])
return { list, refreshing, refresh, fetchNextPage }
}

View File

@ -18,32 +18,12 @@ export const useSwitchActiveServer = () => {
}
}
export const useActiveListRefresh = (list: unknown[], update: () => void) => {
const activeServer = useStore(selectSettings.activeServer)
useEffect(() => {
if (list.length === 0) {
update()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeServer])
}
export const useActiveListRefresh2 = (update: () => void) => {
const activeServer = useStore(selectSettings.activeServer)
useEffect(() => {
update()
}, [activeServer, update])
}
export const useActiveServerRefresh = (update: () => void) => {
export const useActiveServerRefresh = (refresh: () => void) => {
const activeServer = useStore(selectSettings.activeServer)
useEffect(() => {
if (activeServer) {
update()
refresh()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeServer])
}, [activeServer, refresh])
}

View File

@ -3,7 +3,7 @@ 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 { useActiveListRefresh2 } from '@app/hooks/server'
import { useActiveServerRefresh } from '@app/hooks/server'
import { AlbumListItem } from '@app/models/music'
import { selectMusic } from '@app/state/music'
import { selectSettings } from '@app/state/settings'
@ -87,7 +87,7 @@ const Home = () => {
const update = useStore(selectMusic.fetchHomeLists)
const clear = useStore(selectMusic.clearHomeLists)
useActiveListRefresh2(
useActiveServerRefresh(
useCallback(() => {
clear()
update()

View File

@ -1,7 +1,7 @@
import { AlbumContextPressable } from '@app/components/ContextMenu'
import CoverArt from '@app/components/CoverArt'
import GradientFlatList from '@app/components/GradientFlatList'
import { useActiveListRefresh2 } from '@app/hooks/server'
import { useFetchPaginatedList } from '@app/hooks/list'
import { Album, AlbumListItem } from '@app/models/music'
import { selectMusic } from '@app/state/music'
import { useStore } from '@app/state/store'
@ -48,31 +48,28 @@ const AlbumListRenderItem: React.FC<{
}> = ({ item }) => <AlbumItem album={item.album} size={item.size} height={item.height} />
const AlbumsList = () => {
const list = useStore(selectMusic.albums)
const updating = useStore(selectMusic.albumsUpdating)
const updateList = useStore(selectMusic.fetchAlbums)
useActiveListRefresh2(updateList)
const fetchAlbums = useStore(selectMusic.fetchAlbums)
const { list, refreshing, refresh, fetchNextPage } = useFetchPaginatedList(fetchAlbums, 60)
const layout = useWindowDimensions()
const size = layout.width / 3 - styles.itemWrapper.marginHorizontal * 2
const height = size + 36
const albumsList = list.map(album => ({ album, size, height }))
return (
<View style={styles.container}>
<GradientFlatList
contentContainerStyle={styles.listContent}
data={albumsList}
data={list.map(album => ({ album, size, height }))}
renderItem={AlbumListRenderItem}
keyExtractor={item => item.album.id}
numColumns={3}
removeClippedSubviews={true}
refreshing={updating}
onRefresh={updateList}
refreshing={refreshing}
onRefresh={refresh}
overScrollMode="never"
onEndReached={fetchNextPage}
onEndReachedThreshold={1}
getItemLayout={(_data, index) => ({
length: height,
offset: height * Math.floor(index / 3),

View File

@ -1,6 +1,6 @@
import GradientFlatList from '@app/components/GradientFlatList'
import ListItem from '@app/components/ListItem'
import { useActiveListRefresh2 } from '@app/hooks/server'
import { useFetchList } from '@app/hooks/list'
import { Artist } from '@app/models/music'
import { selectMusic } from '@app/state/music'
import { useStore } from '@app/state/store'
@ -12,20 +12,17 @@ const ArtistRenderItem: React.FC<{ item: Artist }> = ({ item }) => (
)
const ArtistsList = () => {
const artists = useStore(selectMusic.artists)
const updating = useStore(selectMusic.artistsUpdating)
const updateArtists = useStore(selectMusic.fetchArtists)
useActiveListRefresh2(updateArtists)
const fetchArtists = useStore(selectMusic.fetchArtists)
const { list, refreshing, refresh } = useFetchList(fetchArtists)
return (
<GradientFlatList
contentContainerStyle={styles.listContent}
data={artists}
data={list}
renderItem={ArtistRenderItem}
keyExtractor={item => item.id}
onRefresh={updateArtists}
refreshing={updating}
onRefresh={refresh}
refreshing={refreshing}
overScrollMode="never"
/>
)

View File

@ -1,6 +1,6 @@
import GradientFlatList from '@app/components/GradientFlatList'
import ListItem from '@app/components/ListItem'
import { useActiveListRefresh2 } from '@app/hooks/server'
import { useFetchList } from '@app/hooks/list'
import { PlaylistListItem } from '@app/models/music'
import { selectMusic } from '@app/state/music'
import { useStore } from '@app/state/store'
@ -12,20 +12,17 @@ const PlaylistRenderItem: React.FC<{ item: PlaylistListItem }> = ({ item }) => (
)
const PlaylistsList = () => {
const playlists = useStore(selectMusic.playlists)
const updating = useStore(selectMusic.playlistsUpdating)
const updatePlaylists = useStore(selectMusic.fetchPlaylists)
useActiveListRefresh2(updatePlaylists)
const fetchPlaylists = useStore(selectMusic.fetchPlaylists)
const { list, refreshing, refresh } = useFetchList(fetchPlaylists)
return (
<GradientFlatList
contentContainerStyle={styles.listContent}
data={playlists}
data={list}
renderItem={PlaylistRenderItem}
keyExtractor={item => item.id}
onRefresh={updatePlaylists}
refreshing={updating}
onRefresh={refresh}
refreshing={refreshing}
overScrollMode="never"
/>
)

View File

@ -2,7 +2,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 { useActiveListRefresh2 } from '@app/hooks/server'
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'
@ -68,7 +68,7 @@ const Search = () => {
const updating = useStore(selectMusic.searchResultsUpdating)
const results = useStore(selectMusic.searchResults)
useActiveListRefresh2(
useActiveServerRefresh(
useCallback(() => {
setText('')
clearSearch()

View File

@ -199,13 +199,6 @@ const SettingsContent = React.memo(() => {
onPress={clear}
buttonStyle="hollow"
/>
{/* <Button
disabled={clearing}
style={styles.button}
title="Reset everything to default"
onPress={() => {}}
buttonStyle="hollow"
/> */}
</View>
)
})

View File

@ -30,17 +30,9 @@ export type MusicSlice = {
//
// lists-style state
//
artists: Artist[]
artistsUpdating: boolean
fetchArtists: () => Promise<void>
playlists: PlaylistListItem[]
playlistsUpdating: boolean
fetchPlaylists: () => Promise<void>
albums: AlbumListItem[]
albumsUpdating: boolean
fetchAlbums: (size?: number, offset?: number) => Promise<void>
fetchArtists: (size?: number, offset?: number) => Promise<Artist[]>
fetchPlaylists: () => Promise<PlaylistListItem[]>
fetchAlbums: () => Promise<AlbumListItem[]>
searchResults: SearchResults
searchResultsUpdating: boolean
@ -71,16 +63,8 @@ export const selectMusic = {
fetchAlbumWithSongs: (state: Store) => state.fetchAlbumWithSongs,
fetchPlaylistWithSongs: (state: Store) => state.fetchPlaylistWithSongs,
artists: (store: MusicSlice) => store.artists,
artistsUpdating: (store: MusicSlice) => store.artistsUpdating,
fetchArtists: (store: MusicSlice) => store.fetchArtists,
playlists: (store: MusicSlice) => store.playlists,
playlistsUpdating: (store: MusicSlice) => store.playlistsUpdating,
fetchPlaylists: (store: MusicSlice) => store.fetchPlaylists,
albums: (store: MusicSlice) => store.albums,
albumsUpdating: (store: MusicSlice) => store.albumsUpdating,
fetchAlbums: (store: MusicSlice) => store.fetchAlbums,
searchResults: (store: MusicSlice) => store.searchResults,
@ -194,83 +178,61 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
}
},
artists: [],
artistsUpdating: false,
fetchArtists: async () => {
const client = get().client
if (!client) {
return
return []
}
if (get().artistsUpdating) {
return
}
set({ artistsUpdating: true })
try {
const response = await client.getArtists()
const artists = response.data.artists.map(get().mapArtistID3toArtist)
set(
produce<MusicSlice>(state => {
state.artists = artists
state.starredArtists = reduceStarred(state.starredArtists, state.artists)
state.starredArtists = reduceStarred(state.starredArtists, artists)
}),
)
} finally {
set({ artistsUpdating: false })
return artists
} catch {
return []
}
},
playlists: [],
playlistsUpdating: false,
fetchPlaylists: async () => {
const client = get().client
if (!client) {
return
return []
}
if (get().playlistsUpdating) {
return
}
set({ playlistsUpdating: true })
try {
const response = await client.getPlaylists()
const playlists = response.data.playlists.map(get().mapPlaylistListItem)
set({ playlists })
} finally {
set({ playlistsUpdating: false })
return response.data.playlists.map(get().mapPlaylistListItem)
} catch {
return []
}
},
albums: [],
albumsUpdating: false,
fetchAlbums: async (size = 500, offset = 0) => {
const client = get().client
if (!client) {
return
return []
}
if (get().albumsUpdating) {
return
}
set({ albumsUpdating: true })
try {
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size, offset })
const albums = response.data.albums.map(get().mapAlbumID3toAlbumListItem)
set(
produce<MusicSlice>(state => {
state.albums = albums
state.starredAlbums = reduceStarred(state.starredAlbums, state.albums)
state.starredAlbums = reduceStarred(state.starredAlbums, albums)
}),
)
} finally {
set({ albumsUpdating: false })
return albums
} catch {
return []
}
},