mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 06:52:43 +01:00
added paginated list/album list
This commit is contained in:
@@ -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 }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 = {}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user