mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 01:19:28 +01:00
refactored library lists to enable pagination
This commit is contained in:
parent
b99eee9034
commit
ba2aea0fbe
69
app/hooks/list.ts
Normal file
69
app/hooks/list.ts
Normal 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 }
|
||||||
|
}
|
||||||
@ -18,32 +18,12 @@ export const useSwitchActiveServer = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useActiveListRefresh = (list: unknown[], update: () => void) => {
|
export const useActiveServerRefresh = (refresh: () => 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) => {
|
|
||||||
const activeServer = useStore(selectSettings.activeServer)
|
const activeServer = useStore(selectSettings.activeServer)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeServer) {
|
if (activeServer) {
|
||||||
update()
|
refresh()
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [activeServer, refresh])
|
||||||
}, [activeServer])
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import CoverArt from '@app/components/CoverArt'
|
|||||||
import GradientScrollView from '@app/components/GradientScrollView'
|
import GradientScrollView from '@app/components/GradientScrollView'
|
||||||
import Header from '@app/components/Header'
|
import Header from '@app/components/Header'
|
||||||
import NothingHere from '@app/components/NothingHere'
|
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 { AlbumListItem } from '@app/models/music'
|
||||||
import { selectMusic } from '@app/state/music'
|
import { selectMusic } from '@app/state/music'
|
||||||
import { selectSettings } from '@app/state/settings'
|
import { selectSettings } from '@app/state/settings'
|
||||||
@ -87,7 +87,7 @@ const Home = () => {
|
|||||||
const update = useStore(selectMusic.fetchHomeLists)
|
const update = useStore(selectMusic.fetchHomeLists)
|
||||||
const clear = useStore(selectMusic.clearHomeLists)
|
const clear = useStore(selectMusic.clearHomeLists)
|
||||||
|
|
||||||
useActiveListRefresh2(
|
useActiveServerRefresh(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
clear()
|
clear()
|
||||||
update()
|
update()
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { AlbumContextPressable } from '@app/components/ContextMenu'
|
import { AlbumContextPressable } from '@app/components/ContextMenu'
|
||||||
import CoverArt from '@app/components/CoverArt'
|
import CoverArt from '@app/components/CoverArt'
|
||||||
import GradientFlatList from '@app/components/GradientFlatList'
|
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 { Album, AlbumListItem } from '@app/models/music'
|
||||||
import { selectMusic } from '@app/state/music'
|
import { selectMusic } from '@app/state/music'
|
||||||
import { useStore } from '@app/state/store'
|
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} />
|
}> = ({ item }) => <AlbumItem album={item.album} size={item.size} height={item.height} />
|
||||||
|
|
||||||
const AlbumsList = () => {
|
const AlbumsList = () => {
|
||||||
const list = useStore(selectMusic.albums)
|
const fetchAlbums = useStore(selectMusic.fetchAlbums)
|
||||||
const updating = useStore(selectMusic.albumsUpdating)
|
const { list, refreshing, refresh, fetchNextPage } = useFetchPaginatedList(fetchAlbums, 60)
|
||||||
const updateList = useStore(selectMusic.fetchAlbums)
|
|
||||||
|
|
||||||
useActiveListRefresh2(updateList)
|
|
||||||
|
|
||||||
const layout = useWindowDimensions()
|
const layout = useWindowDimensions()
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
const albumsList = list.map(album => ({ album, size, height }))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<GradientFlatList
|
<GradientFlatList
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
data={albumsList}
|
data={list.map(album => ({ album, size, height }))}
|
||||||
renderItem={AlbumListRenderItem}
|
renderItem={AlbumListRenderItem}
|
||||||
keyExtractor={item => item.album.id}
|
keyExtractor={item => item.album.id}
|
||||||
numColumns={3}
|
numColumns={3}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
refreshing={updating}
|
refreshing={refreshing}
|
||||||
onRefresh={updateList}
|
onRefresh={refresh}
|
||||||
overScrollMode="never"
|
overScrollMode="never"
|
||||||
|
onEndReached={fetchNextPage}
|
||||||
|
onEndReachedThreshold={1}
|
||||||
getItemLayout={(_data, index) => ({
|
getItemLayout={(_data, index) => ({
|
||||||
length: height,
|
length: height,
|
||||||
offset: height * Math.floor(index / 3),
|
offset: height * Math.floor(index / 3),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
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 { useActiveListRefresh2 } from '@app/hooks/server'
|
import { useFetchList } from '@app/hooks/list'
|
||||||
import { Artist } from '@app/models/music'
|
import { Artist } from '@app/models/music'
|
||||||
import { selectMusic } from '@app/state/music'
|
import { selectMusic } from '@app/state/music'
|
||||||
import { useStore } from '@app/state/store'
|
import { useStore } from '@app/state/store'
|
||||||
@ -12,20 +12,17 @@ const ArtistRenderItem: React.FC<{ item: Artist }> = ({ item }) => (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ArtistsList = () => {
|
const ArtistsList = () => {
|
||||||
const artists = useStore(selectMusic.artists)
|
const fetchArtists = useStore(selectMusic.fetchArtists)
|
||||||
const updating = useStore(selectMusic.artistsUpdating)
|
const { list, refreshing, refresh } = useFetchList(fetchArtists)
|
||||||
const updateArtists = useStore(selectMusic.fetchArtists)
|
|
||||||
|
|
||||||
useActiveListRefresh2(updateArtists)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientFlatList
|
<GradientFlatList
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
data={artists}
|
data={list}
|
||||||
renderItem={ArtistRenderItem}
|
renderItem={ArtistRenderItem}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
onRefresh={updateArtists}
|
onRefresh={refresh}
|
||||||
refreshing={updating}
|
refreshing={refreshing}
|
||||||
overScrollMode="never"
|
overScrollMode="never"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
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 { useActiveListRefresh2 } from '@app/hooks/server'
|
import { useFetchList } from '@app/hooks/list'
|
||||||
import { PlaylistListItem } from '@app/models/music'
|
import { PlaylistListItem } from '@app/models/music'
|
||||||
import { selectMusic } from '@app/state/music'
|
import { selectMusic } from '@app/state/music'
|
||||||
import { useStore } from '@app/state/store'
|
import { useStore } from '@app/state/store'
|
||||||
@ -12,20 +12,17 @@ const PlaylistRenderItem: React.FC<{ item: PlaylistListItem }> = ({ item }) => (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const PlaylistsList = () => {
|
const PlaylistsList = () => {
|
||||||
const playlists = useStore(selectMusic.playlists)
|
const fetchPlaylists = useStore(selectMusic.fetchPlaylists)
|
||||||
const updating = useStore(selectMusic.playlistsUpdating)
|
const { list, refreshing, refresh } = useFetchList(fetchPlaylists)
|
||||||
const updatePlaylists = useStore(selectMusic.fetchPlaylists)
|
|
||||||
|
|
||||||
useActiveListRefresh2(updatePlaylists)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientFlatList
|
<GradientFlatList
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
data={playlists}
|
data={list}
|
||||||
renderItem={PlaylistRenderItem}
|
renderItem={PlaylistRenderItem}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
onRefresh={updatePlaylists}
|
onRefresh={refresh}
|
||||||
refreshing={updating}
|
refreshing={refreshing}
|
||||||
overScrollMode="never"
|
overScrollMode="never"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import GradientScrollView from '@app/components/GradientScrollView'
|
|||||||
import Header from '@app/components/Header'
|
import Header from '@app/components/Header'
|
||||||
import ListItem from '@app/components/ListItem'
|
import ListItem from '@app/components/ListItem'
|
||||||
import NothingHere from '@app/components/NothingHere'
|
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 { ListableItem, SearchResults, Song } from '@app/models/music'
|
||||||
import { selectMusic } from '@app/state/music'
|
import { selectMusic } from '@app/state/music'
|
||||||
import { useStore } from '@app/state/store'
|
import { useStore } from '@app/state/store'
|
||||||
@ -68,7 +68,7 @@ const Search = () => {
|
|||||||
const updating = useStore(selectMusic.searchResultsUpdating)
|
const updating = useStore(selectMusic.searchResultsUpdating)
|
||||||
const results = useStore(selectMusic.searchResults)
|
const results = useStore(selectMusic.searchResults)
|
||||||
|
|
||||||
useActiveListRefresh2(
|
useActiveServerRefresh(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
setText('')
|
setText('')
|
||||||
clearSearch()
|
clearSearch()
|
||||||
|
|||||||
@ -199,13 +199,6 @@ const SettingsContent = React.memo(() => {
|
|||||||
onPress={clear}
|
onPress={clear}
|
||||||
buttonStyle="hollow"
|
buttonStyle="hollow"
|
||||||
/>
|
/>
|
||||||
{/* <Button
|
|
||||||
disabled={clearing}
|
|
||||||
style={styles.button}
|
|
||||||
title="Reset everything to default"
|
|
||||||
onPress={() => {}}
|
|
||||||
buttonStyle="hollow"
|
|
||||||
/> */}
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -30,17 +30,9 @@ export type MusicSlice = {
|
|||||||
//
|
//
|
||||||
// lists-style state
|
// lists-style state
|
||||||
//
|
//
|
||||||
artists: Artist[]
|
fetchArtists: (size?: number, offset?: number) => Promise<Artist[]>
|
||||||
artistsUpdating: boolean
|
fetchPlaylists: () => Promise<PlaylistListItem[]>
|
||||||
fetchArtists: () => Promise<void>
|
fetchAlbums: () => Promise<AlbumListItem[]>
|
||||||
|
|
||||||
playlists: PlaylistListItem[]
|
|
||||||
playlistsUpdating: boolean
|
|
||||||
fetchPlaylists: () => Promise<void>
|
|
||||||
|
|
||||||
albums: AlbumListItem[]
|
|
||||||
albumsUpdating: boolean
|
|
||||||
fetchAlbums: (size?: number, offset?: number) => Promise<void>
|
|
||||||
|
|
||||||
searchResults: SearchResults
|
searchResults: SearchResults
|
||||||
searchResultsUpdating: boolean
|
searchResultsUpdating: boolean
|
||||||
@ -71,16 +63,8 @@ export const selectMusic = {
|
|||||||
fetchAlbumWithSongs: (state: Store) => state.fetchAlbumWithSongs,
|
fetchAlbumWithSongs: (state: Store) => state.fetchAlbumWithSongs,
|
||||||
fetchPlaylistWithSongs: (state: Store) => state.fetchPlaylistWithSongs,
|
fetchPlaylistWithSongs: (state: Store) => state.fetchPlaylistWithSongs,
|
||||||
|
|
||||||
artists: (store: MusicSlice) => store.artists,
|
|
||||||
artistsUpdating: (store: MusicSlice) => store.artistsUpdating,
|
|
||||||
fetchArtists: (store: MusicSlice) => store.fetchArtists,
|
fetchArtists: (store: MusicSlice) => store.fetchArtists,
|
||||||
|
|
||||||
playlists: (store: MusicSlice) => store.playlists,
|
|
||||||
playlistsUpdating: (store: MusicSlice) => store.playlistsUpdating,
|
|
||||||
fetchPlaylists: (store: MusicSlice) => store.fetchPlaylists,
|
fetchPlaylists: (store: MusicSlice) => store.fetchPlaylists,
|
||||||
|
|
||||||
albums: (store: MusicSlice) => store.albums,
|
|
||||||
albumsUpdating: (store: MusicSlice) => store.albumsUpdating,
|
|
||||||
fetchAlbums: (store: MusicSlice) => store.fetchAlbums,
|
fetchAlbums: (store: MusicSlice) => store.fetchAlbums,
|
||||||
|
|
||||||
searchResults: (store: MusicSlice) => store.searchResults,
|
searchResults: (store: MusicSlice) => store.searchResults,
|
||||||
@ -194,83 +178,61 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
artists: [],
|
|
||||||
artistsUpdating: false,
|
|
||||||
|
|
||||||
fetchArtists: async () => {
|
fetchArtists: async () => {
|
||||||
const client = get().client
|
const client = get().client
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get().artistsUpdating) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
set({ artistsUpdating: true })
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client.getArtists()
|
const response = await client.getArtists()
|
||||||
const artists = response.data.artists.map(get().mapArtistID3toArtist)
|
const artists = response.data.artists.map(get().mapArtistID3toArtist)
|
||||||
|
|
||||||
set(
|
set(
|
||||||
produce<MusicSlice>(state => {
|
produce<MusicSlice>(state => {
|
||||||
state.artists = artists
|
state.starredArtists = reduceStarred(state.starredArtists, artists)
|
||||||
state.starredArtists = reduceStarred(state.starredArtists, state.artists)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
} finally {
|
|
||||||
set({ artistsUpdating: false })
|
return artists
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
playlists: [],
|
|
||||||
playlistsUpdating: false,
|
|
||||||
|
|
||||||
fetchPlaylists: async () => {
|
fetchPlaylists: async () => {
|
||||||
const client = get().client
|
const client = get().client
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get().playlistsUpdating) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
set({ playlistsUpdating: true })
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client.getPlaylists()
|
const response = await client.getPlaylists()
|
||||||
const playlists = response.data.playlists.map(get().mapPlaylistListItem)
|
return response.data.playlists.map(get().mapPlaylistListItem)
|
||||||
set({ playlists })
|
} catch {
|
||||||
} finally {
|
return []
|
||||||
set({ playlistsUpdating: false })
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
albums: [],
|
|
||||||
albumsUpdating: false,
|
|
||||||
|
|
||||||
fetchAlbums: async (size = 500, offset = 0) => {
|
fetchAlbums: async (size = 500, offset = 0) => {
|
||||||
const client = get().client
|
const client = get().client
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get().albumsUpdating) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
set({ albumsUpdating: true })
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size, offset })
|
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size, offset })
|
||||||
const albums = response.data.albums.map(get().mapAlbumID3toAlbumListItem)
|
const albums = response.data.albums.map(get().mapAlbumID3toAlbumListItem)
|
||||||
|
|
||||||
set(
|
set(
|
||||||
produce<MusicSlice>(state => {
|
produce<MusicSlice>(state => {
|
||||||
state.albums = albums
|
state.starredAlbums = reduceStarred(state.starredAlbums, albums)
|
||||||
state.starredAlbums = reduceStarred(state.starredAlbums, state.albums)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
} finally {
|
|
||||||
set({ albumsUpdating: false })
|
return albums
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user