From 76306f15582d1275fbba82f7537ba6d6cf22d5f9 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Tue, 15 Mar 2022 16:00:06 +0900 Subject: [PATCH] added paginated list/album list --- app/hooks/list.ts | 37 ++++++++- app/screens/ArtistView.tsx | 23 +++--- app/screens/LibraryAlbums.tsx | 23 ++++-- app/screens/LibraryArtists.tsx | 11 +-- app/state/library.ts | 134 ++++++++++++++++++++++++++++----- app/state/settings.ts | 2 + 6 files changed, 188 insertions(+), 42 deletions(-) diff --git a/app/hooks/list.ts b/app/hooks/list.ts index 48c8cef..7c95e0c 100644 --- a/app/hooks/list.ts +++ b/app/hooks/list.ts @@ -28,7 +28,7 @@ export const useFetchList = (fetchList: () => Promise) => { return { list, refreshing, refresh, reset } } -export const useFetchList2 = (fetchList: () => Promise, resetList: () => Promise) => { +export const useFetchList2 = (fetchList: () => Promise, resetList: () => void) => { const [refreshing, setRefreshing] = useState(false) const refresh = useCallback(async () => { @@ -39,9 +39,9 @@ export const useFetchList2 = (fetchList: () => Promise, resetList: () => P useActiveServerRefresh( useCallback(async () => { - await resetList() - await fetchList() - }, [fetchList, resetList]), + resetList() + await refresh() + }, [refresh, resetList]), ) return { refreshing, refresh } @@ -94,3 +94,32 @@ export const useFetchPaginatedList = ( return { list, refreshing, refresh, reset, fetchNextPage } } + +export const useFetchPaginatedList2 = (fetchNextListPage: () => Promise, 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 } +} diff --git a/app/screens/ArtistView.tsx b/app/screens/ArtistView.tsx index a0fa421..516e227 100644 --- a/app/screens/ArtistView.tsx +++ b/app/screens/ArtistView.tsx @@ -5,7 +5,6 @@ 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' @@ -15,7 +14,7 @@ import font from '@app/styles/font' import { useLayout } from '@react-native-community/hooks' import { useNavigation } from '@react-navigation/native' 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 { useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated' @@ -71,10 +70,16 @@ const TopSongs = React.memo<{ const ArtistAlbums = React.memo<{ id: string }>(({ id }) => { - const albums = useStore(store => { - const ids = store.entities.artistAlbums[id] - return ids ? pick(store.entities.albums, ids) : undefined - }) + const albums = useStore( + useCallback( + store => { + const ids = store.entities.artistAlbums[id] + return ids ? pick(store.entities.albums, ids) : undefined + }, + [id], + ), + ) + const fetchArtist = useStore(store => store.fetchLibraryArtist) const albumsLayout = useLayout() @@ -109,10 +114,8 @@ const ArtistViewFallback = React.memo(() => ( )) const ArtistView = React.memo<{ id: string; title: string }>(({ id, title }) => { - // const artist = useArtistInfo(id) - - const artist = useStore(store => store.entities.artists[id]) - const artistInfo = useStore(store => store.entities.artistInfo[id]) + const artist = useStore(useCallback(store => store.entities.artists[id], [id])) + const artistInfo = useStore(useCallback(store => store.entities.artistInfo[id], [id])) const fetchArtist = useStore(store => store.fetchLibraryArtist) const fetchArtistInfo = useStore(store => store.fetchLibraryArtistInfo) diff --git a/app/screens/LibraryAlbums.tsx b/app/screens/LibraryAlbums.tsx index 351a40d..842c00a 100644 --- a/app/screens/LibraryAlbums.tsx +++ b/app/screens/LibraryAlbums.tsx @@ -2,11 +2,10 @@ import { AlbumContextPressable } from '@app/components/ContextMenu' 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 { useFetchPaginatedList2 } 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 { Store, useStore } from '@app/state/store' import colors from '@app/styles/colors' import font from '@app/styles/font' import { GetAlbumList2Type } from '@app/subsonic/params' @@ -56,9 +55,19 @@ const filterOptions: OptionData[] = [ // { 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 fetchAlbums = useStore(selectMusic.fetchAlbums) - const { list, refreshing, refresh, reset, fetchNextPage } = useFetchPaginatedList(fetchAlbums, 300) + const list = useStore(selectAlbumList) + + 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 setFilter = useStore(selectSettings.setLibraryAlbumFilter) @@ -67,7 +76,9 @@ const AlbumsList = () => { const size = layout.width / 3 - styles.itemWrapper.marginHorizontal * 2 const height = size + 36 - useEffect(() => reset(), [reset, filter]) + useEffect(() => { + refresh() + }, [refresh, filter]) return ( diff --git a/app/screens/LibraryArtists.tsx b/app/screens/LibraryArtists.tsx index 2f6ef5a..6a0350c 100644 --- a/app/screens/LibraryArtists.tsx +++ b/app/screens/LibraryArtists.tsx @@ -1,13 +1,12 @@ import FilterButton, { OptionData } from '@app/components/FilterButton' import GradientFlatList from '@app/components/GradientFlatList' 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 { ArtistFilterType } from '@app/models/settings' -import { selectMusic } from '@app/state/music' import { selectSettings } from '@app/state/settings' -import { useStore } from '@app/state/store' -import React, { useCallback, useEffect, useState } from 'react' +import { Store, useStore } from '@app/state/store' +import React, { useEffect, useState } from 'react' import { StyleSheet, View } from 'react-native' const ArtistRenderItem: React.FC<{ item: Artist }> = ({ item }) => ( @@ -20,12 +19,14 @@ const filterOptions: OptionData[] = [ { text: 'Random', value: 'random' }, ] +const selectArtists = (store: Store) => store.entities.artists + const ArtistsList = () => { const fetchArtists = useStore(store => store.fetchLibraryArtists) const resetArtists = useStore(store => store.resetLibraryArtists) const { refreshing, refresh } = useFetchList2(fetchArtists, resetArtists) - const artists = useStore(store => store.entities.artists) + const artists = useStore(selectArtists) const filter = useStore(selectSettings.libraryArtistFilter) const setFilter = useStore(selectSettings.setLibraryArtistFiler) diff --git a/app/state/library.ts b/app/state/library.ts index dde8042..cd8a087 100644 --- a/app/state/library.ts +++ b/app/state/library.ts @@ -1,6 +1,8 @@ import { Store } from '@app/state/store' import { AlbumID3Element, ArtistID3Element, ArtistInfo2Element, ChildElement } from '@app/subsonic/elements' +import { GetAlbumList2Params } from '@app/subsonic/params' import { + GetAlbumList2Response, GetArtistInfo2Response, GetArtistResponse, GetArtistsResponse, @@ -18,6 +20,15 @@ export interface ById { export type OneToMany = ById +export interface OrderedById { + byId: ById + allIds: string[] +} + +export interface PaginatedList { + [offset: number]: string[] +} + export interface Artist { itemType: 'artist' id: string @@ -111,35 +122,55 @@ function mapSong(song: ChildElement): Song { export type LibrarySlice = { entities: { artists: ById + artistInfo: ById artistAlbums: OneToMany + artistNameTopSongs: OneToMany albums: ById - - artistInfo: ById - artistNameTopSongs: OneToMany + albumsList: PaginatedList + albumsListSize: number songs: ById } + resetLibrary: () => void + fetchLibraryArtists: () => Promise fetchLibraryArtist: (id: string) => Promise - resetLibraryArtists: () => Promise - // fetchAlbums: (artistId: string) => Promise fetchLibraryArtistInfo: (artistId: string) => Promise + resetLibraryArtists: () => void + fetchLibraryTopSongs: (artistName: string) => Promise + + fetchLibraryAlbumsNextPage: () => Promise + 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, get: GetState): LibrarySlice => ({ - entities: { - artists: {}, - artistAlbums: {}, + entities: defaultEntities(), - albums: {}, - - artistInfo: {}, - artistNameTopSongs: {}, - - songs: {}, + resetLibrary: () => { + set(store => { + store.entities = defaultEntities() + }) }, fetchLibraryArtists: async () => { @@ -192,12 +223,12 @@ export const createLibrarySlice = (set: SetState, get: GetState): produce(state => { state.entities.artists[id] = artist state.entities.artistAlbums[id] = Object.keys(albums) - state.entities.albums = merge(state.entities.albums, albums) + merge(state.entities.albums, albums) }), ) }, - resetLibraryArtists: async () => { + resetLibraryArtists: () => { set( produce(state => { state.entities.artists = {} @@ -245,9 +276,78 @@ export const createLibrarySlice = (set: SetState, get: GetState): set( produce(state => { - state.entities.songs = merge(state.entities.songs, topSongs) + merge(state.entities.songs, topSongs) 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 + 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) + + set( + produce(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(state => { + state.entities.albumsList = {} + }), + ) + }, }) diff --git a/app/state/settings.ts b/app/state/settings.ts index d43dc4a..ae3daf1 100644 --- a/app/state/settings.ts +++ b/app/state/settings.ts @@ -117,6 +117,8 @@ export const createSettingsSlice = (set: SetState, get: GetState): state.client = new SubsonicApiClient(newActiveServer) }), ) + + get().resetLibrary() }, getActiveServer: () => get().settings.servers.find(s => s.id === get().settings.activeServer),