From 0a3d542156f60e4bb0efe7ec70ece5103a1ad23f Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Mon, 9 Aug 2021 18:05:20 +0900 Subject: [PATCH] impl star/unstar starred state --- app/components/ListItem.tsx | 13 +++- app/hooks/music.ts | 15 ++++ app/state/music.ts | 138 +++++++++++++++++++++++++++--------- app/subsonic/api.ts | 11 +++ app/subsonic/params.ts | 6 ++ 5 files changed, 145 insertions(+), 38 deletions(-) diff --git a/app/components/ListItem.tsx b/app/components/ListItem.tsx index 8584a7b..49611bb 100644 --- a/app/components/ListItem.tsx +++ b/app/components/ListItem.tsx @@ -1,10 +1,12 @@ +import { useStarred } from '@app/hooks/music' import { AlbumListItem, Artist, ListableItem, Song } from '@app/models/music' +import { selectMusic } from '@app/state/music' import { useStore } from '@app/state/store' import { selectTrackPlayer } from '@app/state/trackplayer' import colors from '@app/styles/colors' import font from '@app/styles/font' import { useNavigation } from '@react-navigation/native' -import React, { useCallback, useState } from 'react' +import React, { useCallback } from 'react' import { StyleSheet, Text, View } from 'react-native' import FastImage from 'react-native-fast-image' import IconFA from 'react-native-vector-icons/FontAwesome' @@ -47,8 +49,8 @@ const ListItem: React.FC<{ listStyle?: 'big' | 'small' subtitle?: string }> = ({ item, onPress, showArt, showStar, subtitle, listStyle }) => { - const [starred, setStarred] = useState(false) const navigation = useNavigation() + const starred = useStarred(item.id, item.itemType) showStar = showStar === undefined ? true : showStar listStyle = listStyle || 'small' @@ -124,6 +126,11 @@ const ListItem: React.FC<{ PressableComponent = artistPressable } + const starItem = useStore(selectMusic.starItem) + const toggleStarred = useCallback(() => { + starItem(item.id, item.itemType, starred) + }, [item.id, item.itemType, starItem, starred]) + return ( @@ -163,7 +170,7 @@ const ListItem: React.FC<{ {showStar ? ( - setStarred(!starred)} style={styles.controlItem}> + {starred ? ( ) : ( diff --git a/app/hooks/music.ts b/app/hooks/music.ts index 12dd2ec..53d0818 100644 --- a/app/hooks/music.ts +++ b/app/hooks/music.ts @@ -38,6 +38,21 @@ export const usePlaylistWithSongs = (id: string) => { return playlist } +export const useStarred = (id: string, type: string) => { + const starred = useStore( + useCallback( + (state: Store) => { + if (!(type in state.starred)) { + return false + } + return !!state.starred[type][id] + }, + [type, id], + ), + ) + return starred +} + export const useCoverArtUri = () => { const server = useStore(selectSettings.activeServer) diff --git a/app/state/music.ts b/app/state/music.ts index babb31c..34f461b 100644 --- a/app/state/music.ts +++ b/app/state/music.ts @@ -16,7 +16,7 @@ import { SearchResults, } from '@app/models/music' import { Store } from '@app/state/store' -import { GetAlbumList2Type } from '@app/subsonic/params' +import { GetAlbumList2Type, StarParams } from '@app/subsonic/params' import produce from 'immer' import { GetState, SetState } from 'zustand' @@ -24,18 +24,13 @@ export type MusicSlice = { // // family-style state // - cacheSize: number - - artistInfo: { [id: string]: ArtistInfo | undefined } - artistInfoCache: string[] + artistInfo: { [id: string]: ArtistInfo } fetchArtistInfo: (id: string) => Promise - albumsWithSongs: { [id: string]: AlbumWithSongs | undefined } - albumsWithSongsCache: string[] + albumsWithSongs: { [id: string]: AlbumWithSongs } fetchAlbumWithSongs: (id: string) => Promise - playlistsWithSongs: { [id: string]: PlaylistWithSongs | undefined } - playlistsWithSongsCache: string[] + playlistsWithSongs: { [id: string]: PlaylistWithSongs } fetchPlaylistWithSongs: (id: string) => Promise // @@ -62,6 +57,9 @@ export type MusicSlice = { homeListsUpdating: boolean fetchHomeLists: () => Promise clearHomeLists: () => void + + starred: { [type: string]: { [id: string]: boolean } } + starItem: (id: string, type: string, unstar?: boolean) => Promise } export const selectMusic = { @@ -90,13 +88,25 @@ export const selectMusic = { homeListsUpdating: (store: MusicSlice) => store.homeListsUpdating, fetchHomeLists: (store: MusicSlice) => store.fetchHomeLists, clearHomeLists: (store: MusicSlice) => store.clearHomeLists, + + starItem: (store: MusicSlice) => store.starItem, +} + +function reduceStarred( + starredType: { [id: string]: boolean }, + items: { id: string; starred?: Date }[], +): { [id: string]: boolean } { + return { + ...starredType, + ...items.reduce((acc, val) => { + acc[val.id] = !!val.starred + return acc + }, {} as { [id: string]: boolean }), + } } export const createMusicSlice = (set: SetState, get: GetState): MusicSlice => ({ - cacheSize: 100, - artistInfo: {}, - artistInfoCache: [], fetchArtistInfo: async id => { const client = get().client @@ -119,11 +129,10 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu set( produce(state => { - if (state.artistInfoCache.length >= state.cacheSize) { - delete state.albumsWithSongs[state.artistInfoCache.shift() as string] - } state.artistInfo[id] = artistInfo - state.artistInfoCache.push(id) + state.starred.song = reduceStarred(state.starred.song, artistInfo.topSongs) + state.starred.artist = reduceStarred(state.starred.artist, [artistInfo]) + state.starred.album = reduceStarred(state.starred.album, artistInfo.albums) }), ) return artistInfo @@ -133,7 +142,6 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu }, albumsWithSongs: {}, - albumsWithSongsCache: [], fetchAlbumWithSongs: async id => { const client = get().client @@ -147,11 +155,9 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu set( produce(state => { - if (state.albumsWithSongsCache.length >= state.cacheSize) { - delete state.albumsWithSongs[state.albumsWithSongsCache.shift() as string] - } state.albumsWithSongs[id] = album - state.albumsWithSongsCache.push(id) + state.starred.song = reduceStarred(state.starred.song, album.songs) + state.starred.album = reduceStarred(state.starred.album, [album]) }), ) return album @@ -161,7 +167,6 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu }, playlistsWithSongs: {}, - playlistsWithSongsCache: [], fetchPlaylistWithSongs: async id => { const client = get().client @@ -175,11 +180,8 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu set( produce(state => { - if (state.playlistsWithSongsCache.length >= state.cacheSize) { - delete state.playlistsWithSongs[state.playlistsWithSongsCache.shift() as string] - } state.playlistsWithSongs[id] = playlist - state.playlistsWithSongsCache.push(id) + state.starred.song = reduceStarred(state.starred.song, playlist.songs) }), ) return playlist @@ -204,7 +206,12 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu try { const response = await client.getArtists() - set({ artists: response.data.artists.map(mapArtistID3toArtist) }) + set( + produce(state => { + state.artists = response.data.artists.map(mapArtistID3toArtist) + state.starred.artist = reduceStarred(state.starred.artist, state.artists) + }), + ) } finally { set({ artistsUpdating: false }) } @@ -248,7 +255,12 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu try { const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size, offset }) - set({ albums: response.data.albums.map(mapAlbumID3toAlbumListItem) }) + set( + produce(state => { + state.albums = response.data.albums.map(mapAlbumID3toAlbumListItem) + state.starred.albums = reduceStarred(state.starred.albums, state.albums) + }), + ) } finally { set({ albumsUpdating: false }) } @@ -279,13 +291,18 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu try { const response = await client.search3({ query }) - set({ - searchResults: { - artists: response.data.artists.map(mapArtistID3toArtist), - albums: response.data.albums.map(mapAlbumID3toAlbumListItem), - songs: response.data.songs.map(a => mapChildToSong(a, client)), - }, - }) + set( + produce(state => { + state.searchResults = { + artists: response.data.artists.map(mapArtistID3toArtist), + albums: response.data.albums.map(mapAlbumID3toAlbumListItem), + songs: response.data.songs.map(a => mapChildToSong(a, client)), + } + state.starred.song = reduceStarred(state.starred.song, state.searchResults.songs) + state.starred.artist = reduceStarred(state.starred.artist, state.searchResults.artists) + state.starred.album = reduceStarred(state.starred.album, state.searchResults.albums) + }), + ) } finally { set({ searchResultsUpdating: false }) } @@ -324,6 +341,7 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu set( produce(state => { state.homeLists[type] = response.data.albums.map(mapAlbumID3toAlbumListItem) + state.starred.album = reduceStarred(state.starred.album, state.homeLists[type]) }), ) }), @@ -338,4 +356,54 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu clearHomeLists: () => { set({ homeLists: {} }) }, + + starred: { + song: {}, + album: {}, + artist: {}, + }, + + starItem: async (id, type, unstar = false) => { + const client = get().client + if (!client) { + return + } + + let params: StarParams + switch (type) { + case 'song': + params = { id } + break + case 'album': + params = { albumId: id } + break + case 'artist': + params = { artistId: id } + break + default: + return + } + + const setStarred = (starred: boolean) => { + set( + produce(state => { + state.starred[type] = { + ...state.starred[type], + [id]: starred, + } + }), + ) + } + + try { + setStarred(!unstar) + if (unstar) { + await client.unstar(params) + } else { + await client.star(params) + } + } catch { + setStarred(unstar) + } + }, }) diff --git a/app/subsonic/api.ts b/app/subsonic/api.ts index b5da6e5..cfd6797 100644 --- a/app/subsonic/api.ts +++ b/app/subsonic/api.ts @@ -15,6 +15,7 @@ import { GetTopSongsParams, ScrobbleParams, Search3Params, + StarParams, StreamParams, } from '@app/subsonic/params' import { @@ -233,6 +234,16 @@ export class SubsonicApiClient { return new SubsonicResponse(xml, undefined) } + async star(params: StarParams): Promise> { + const xml = await this.apiGetXml('star', params) + return new SubsonicResponse(xml, undefined) + } + + async unstar(params: StarParams): Promise> { + const xml = await this.apiGetXml('unstar', params) + return new SubsonicResponse(xml, undefined) + } + // // Searching // diff --git a/app/subsonic/params.ts b/app/subsonic/params.ts index 0a7a493..6a20cf2 100644 --- a/app/subsonic/params.ts +++ b/app/subsonic/params.ts @@ -110,6 +110,12 @@ export type ScrobbleParams = { submission?: boolean } +export type StarParams = { + id?: string + albumId?: string + artistId?: string +} + // // Searching //