From a15159014ce7c4cb94f94f081ac3aef0c872de8f Mon Sep 17 00:00:00 2001
From: austinried <4966622+austinried@users.noreply.github.com>
Date: Sun, 20 Mar 2022 09:33:15 +0900
Subject: [PATCH] refactor star
---
app/components/ContextMenu.tsx | 12 ++---
app/components/ListItem.tsx | 19 ++------
app/components/Star.tsx | 20 +++++++-
app/hooks/music.ts | 74 ++++++++++++++++++++---------
app/screens/NowPlayingView.tsx | 12 +----
app/state/library.ts | 25 ++++++++++
app/state/music.ts | 87 ----------------------------------
app/subsonic/api.ts | 7 +++
app/subsonic/params.ts | 4 ++
app/subsonic/responses.ts | 8 ++++
10 files changed, 122 insertions(+), 146 deletions(-)
diff --git a/app/components/ContextMenu.tsx b/app/components/ContextMenu.tsx
index 633d6be..913780b 100644
--- a/app/components/ContextMenu.tsx
+++ b/app/components/ContextMenu.tsx
@@ -1,8 +1,6 @@
import PressableOpacity from '@app/components/PressableOpacity'
-import { useStarred } from '@app/hooks/music'
+import { useStar } from '@app/hooks/music'
import { AlbumListItem, Artist, Song, StarrableItemType } from '@app/models/music'
-import { selectMusic } from '@app/state/music'
-import { useStore } from '@app/state/store'
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import { NavigationProp, useNavigation } from '@react-navigation/native'
@@ -12,9 +10,8 @@ import { ScrollView, StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-
import { Menu, MenuOption, MenuOptions, MenuTrigger, renderers } from 'react-native-popup-menu'
import IconFA from 'react-native-vector-icons/FontAwesome'
import IconFA5 from 'react-native-vector-icons/FontAwesome5'
-// import IconMat from 'react-native-vector-icons/MaterialIcons'
import CoverArt from './CoverArt'
-import Star from './Star'
+import { Star } from './Star'
const { SlideInMenu } = renderers
@@ -144,14 +141,13 @@ const OptionStar = React.memo<{
type: StarrableItemType
additionalText?: string
}>(({ id, type, additionalText: text }) => {
- const starred = useStarred(id, type)
- const setStarred = useStore(selectMusic.starItem)
+ const { starred, toggleStar } = useStar(id, type)
return (
}
text={(starred ? 'Unstar' : 'Star') + (text ? ` ${text}` : '')}
- onSelect={() => setStarred(id, type, starred)}
+ onSelect={toggleStar}
/>
)
})
diff --git a/app/components/ListItem.tsx b/app/components/ListItem.tsx
index 0e36e1a..329feef 100644
--- a/app/components/ListItem.tsx
+++ b/app/components/ListItem.tsx
@@ -1,8 +1,5 @@
-import { useStarred } from '@app/hooks/music'
import { useIsPlaying } from '@app/hooks/trackplayer'
import { AlbumListItem, Artist, ListableItem, Song } from '@app/models/music'
-import { selectMusic } from '@app/state/music'
-import { useStore } from '@app/state/store'
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import { useNavigation } from '@react-navigation/native'
@@ -13,7 +10,7 @@ import IconMat from 'react-native-vector-icons/MaterialIcons'
import { AlbumContextPressable, ArtistContextPressable, SongContextPressable } from './ContextMenu'
import CoverArt from './CoverArt'
import PressableOpacity from './PressableOpacity'
-import Star from './Star'
+import { PressableStar } from './Star'
const TitleTextSong = React.memo<{
contextId?: string
@@ -58,7 +55,6 @@ const ListItem: React.FC<{
style?: StyleProp
}> = ({ item, contextId, queueId, onPress, showArt, showStar, subtitle, listStyle, style }) => {
const navigation = useNavigation()
- const starred = useStarred(item.id, item.itemType)
showStar = showStar === undefined ? true : showStar
listStyle = listStyle || 'small'
@@ -133,13 +129,6 @@ const ListItem: React.FC<{
PressableComponent = artistPressable
}
- const starItem = useStore(selectMusic.starItem)
- const toggleStarred = useCallback(() => {
- if (item.itemType !== 'playlist') {
- starItem(item.id, item.itemType, starred)
- }
- }, [item.id, item.itemType, starItem, starred])
-
let title = <>>
if (item.itemType === 'song' && queueId !== undefined) {
title =
@@ -178,10 +167,8 @@ const ListItem: React.FC<{
- {showStar && (
-
-
-
+ {showStar && item.itemType !== 'playlist' && (
+
)}
diff --git a/app/components/Star.tsx b/app/components/Star.tsx
index c58d5e9..75a70fd 100644
--- a/app/components/Star.tsx
+++ b/app/components/Star.tsx
@@ -1,8 +1,11 @@
+import { useStar } from '@app/hooks/music'
import colors from '@app/styles/colors'
import React from 'react'
+import { PressableStateCallbackType, StyleProp, ViewStyle } from 'react-native'
import IconFA from 'react-native-vector-icons/FontAwesome'
+import PressableOpacity from './PressableOpacity'
-const Star = React.memo<{
+export const Star = React.memo<{
starred: boolean
size: number
}>(({ starred, size }) => {
@@ -11,4 +14,17 @@ const Star = React.memo<{
)
})
-export default Star
+export const PressableStar = React.memo<{
+ id: string
+ type: 'album' | 'artist' | 'song'
+ size: number
+ style?: StyleProp | ((state: PressableStateCallbackType) => StyleProp) | undefined
+}>(({ id, type, size, style }) => {
+ const { starred, toggleStar } = useStar(id, type)
+
+ return (
+
+
+
+ )
+})
diff --git a/app/hooks/music.ts b/app/hooks/music.ts
index 4d3eae5..7f321e4 100644
--- a/app/hooks/music.ts
+++ b/app/hooks/music.ts
@@ -1,35 +1,63 @@
-import { Store, useStore, useStoreDeep } from '@app/state/store'
+import { useStore } from '@app/state/store'
+import { StarParams } from '@app/subsonic/params'
import { useCallback, useEffect } from 'react'
-export const useArtistInfo = (id: string) => {
- const artistInfo = useStoreDeep(useCallback(store => store.entities.artistInfo[id], [id]))
- const fetchArtistInfo = useStore(store => store.fetchLibraryArtistInfo)
+type StarrableItem = 'album' | 'artist' | 'song'
- useEffect(() => {
- if (!artistInfo) {
- fetchArtistInfo(id)
- }
- }, [artistInfo, fetchArtistInfo, id])
+function starParams(id: string, type: StarrableItem): StarParams {
+ const params: StarParams = {}
+ if (type === 'album') {
+ params.albumId = id
+ } else if (type === 'artist') {
+ params.artistId = id
+ } else {
+ params.id = id
+ }
- return artistInfo
+ return params
}
-export const useStarred = (id: string, type: string) => {
- return useStore(
+export const useStar = (id: string, type: StarrableItem) => {
+ const fetchAlbum = useStore(store => store.fetchLibraryAlbum)
+ const fetchArtist = useStore(store => store.fetchLibraryArtist)
+ const fetchSong = useStore(store => store.fetchLibrarySong)
+
+ const _starred = useStore(
useCallback(
- (state: Store) => {
- switch (type) {
- case 'song':
- return state.starredSongs[id]
- case 'album':
- return state.starredAlbums[id]
- case 'artist':
- return state.starredArtists[id]
- default:
- return false
+ store => {
+ if (type === 'album') {
+ return store.entities.albums[id] ? !!store.entities.albums[id].starred : null
+ } else if (type === 'artist') {
+ return store.entities.artists[id] ? !!store.entities.artists[id].starred : null
+ } else {
+ return store.entities.songs[id] ? !!store.entities.songs[id].starred : null
}
},
- [type, id],
+ [id, type],
),
)
+
+ useEffect(() => {
+ if (_starred === null) {
+ if (type === 'album') {
+ fetchAlbum(id)
+ } else if (type === 'artist') {
+ fetchArtist(id)
+ } else {
+ fetchSong(id)
+ }
+ }
+ }, [fetchAlbum, fetchArtist, fetchSong, id, _starred, type])
+
+ const starred = !!_starred
+
+ const _star = useStore(store => store.star)
+ const _unstar = useStore(store => store.unstar)
+
+ const star = useCallback(() => _star(starParams(id, type)), [_star, id, type])
+ const unstar = useCallback(() => _unstar(starParams(id, type)), [_unstar, id, type])
+
+ const toggleStar = useCallback(() => (starred ? unstar() : star()), [star, starred, unstar])
+
+ return { star, unstar, toggleStar, starred }
}
diff --git a/app/screens/NowPlayingView.tsx b/app/screens/NowPlayingView.tsx
index 696a713..0fe6288 100644
--- a/app/screens/NowPlayingView.tsx
+++ b/app/screens/NowPlayingView.tsx
@@ -2,10 +2,8 @@ import CoverArt from '@app/components/CoverArt'
import HeaderBar from '@app/components/HeaderBar'
import ImageGradientBackground from '@app/components/ImageGradientBackground'
import PressableOpacity from '@app/components/PressableOpacity'
-import Star from '@app/components/Star'
-import { useStarred } from '@app/hooks/music'
+import { PressableStar } from '@app/components/Star'
import { useNext, usePause, usePlay, usePrevious, useSeekTo } from '@app/hooks/trackplayer'
-import { selectMusic } from '@app/state/music'
import { useStore } from '@app/state/store'
import { QueueContextType, selectTrackPlayer, TrackExt } from '@app/state/trackplayer'
import { selectTrackPlayerMap } from '@app/state/trackplayermap'
@@ -118,10 +116,6 @@ const coverArtStyles = StyleSheet.create({
const SongInfo = () => {
const track = useStore(selectTrackPlayer.currentTrack)
- const id = track?.id || '-1'
- const type = 'song'
- const starred = useStarred(id, type)
- const setStarred = useStore(selectMusic.starItem)
return (
@@ -134,9 +128,7 @@ const SongInfo = () => {
- setStarred(id, type, starred)}>
-
-
+
)
diff --git a/app/state/library.ts b/app/state/library.ts
index d7dfdfd..4021bee 100644
--- a/app/state/library.ts
+++ b/app/state/library.ts
@@ -15,6 +15,7 @@ import {
GetArtistsResponse,
GetPlaylistResponse,
GetPlaylistsResponse,
+ GetSongResponse,
GetTopSongsResponse,
Search3Response,
SubsonicResponse,
@@ -184,6 +185,8 @@ export type LibrarySlice = {
fetchLibraryPlaylists: () => Promise
fetchLibraryPlaylist: (id: string) => Promise
+ fetchLibrarySong: (id: string) => Promise
+
fetchLibraryAlbumList: (params: GetAlbumList2Params) => Promise
fetchLibrarySearchResults: (params: Search3Params) => Promise
star: (params: StarParams) => Promise
@@ -405,6 +408,28 @@ export const createLibrarySlice = (set: SetState, get: GetState):
)
},
+ fetchLibrarySong: async id => {
+ const client = get().client
+ if (!client) {
+ return
+ }
+
+ let response: SubsonicResponse
+ try {
+ response = await client.getSong({ id })
+ } catch {
+ return
+ }
+
+ const song = mapSong(response.data.song)
+
+ set(
+ produce(state => {
+ state.entities.songs[id] = song
+ }),
+ )
+ },
+
fetchLibraryAlbumList: async params => {
const client = get().client
if (!client) {
diff --git a/app/state/music.ts b/app/state/music.ts
index 55fbf4c..d2cc658 100644
--- a/app/state/music.ts
+++ b/app/state/music.ts
@@ -1,102 +1,15 @@
-import { StarrableItemType } from '@app/models/music'
import { Store } from '@app/state/store'
-import { StarParams } from '@app/subsonic/params'
import produce from 'immer'
import { GetState, SetState } from 'zustand'
export type MusicSlice = {
- //
- // actions, etc.
- //
- starredSongs: { [id: string]: boolean }
- starredAlbums: { [id: string]: boolean }
- starredArtists: { [id: string]: boolean }
- starItem: (id: string, type: StarrableItemType, unstar?: boolean) => Promise
-
albumIdCoverArt: { [id: string]: string | undefined }
albumIdCoverArtRequests: { [id: string]: Promise }
fetchAlbumCoverArt: (id: string) => Promise
getAlbumCoverArt: (id: string | undefined) => Promise
}
-export const selectMusic = {
- starItem: (store: MusicSlice) => store.starItem,
-}
-
-function reduceStarred(
- starredType: { [id: string]: boolean },
- items: { id: string; starred?: Date | boolean }[],
-): { [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 => ({
- starredSongs: {},
- starredAlbums: {},
- starredArtists: {},
-
- starItem: async (id, type, unstar = false) => {
- const client = get().client
- if (!client) {
- return
- }
-
- let params: StarParams
- let setStarred: (starred: boolean) => void
-
- switch (type) {
- case 'song':
- params = { id }
- setStarred = starred => {
- set(
- produce(state => {
- state.starredSongs = reduceStarred(state.starredSongs, [{ id, starred }])
- }),
- )
- }
- break
- case 'album':
- params = { albumId: id }
- setStarred = starred => {
- set(
- produce(state => {
- state.starredAlbums = reduceStarred(state.starredAlbums, [{ id, starred }])
- }),
- )
- }
- break
- case 'artist':
- params = { artistId: id }
- setStarred = starred => {
- set(
- produce(state => {
- state.starredArtists = reduceStarred(state.starredArtists, [{ id, starred }])
- }),
- )
- }
- break
- default:
- return
- }
-
- try {
- setStarred(!unstar)
- if (unstar) {
- await client.unstar(params)
- } else {
- await client.star(params)
- }
- } catch {
- setStarred(unstar)
- }
- },
-
albumIdCoverArt: {},
albumIdCoverArtRequests: {},
diff --git a/app/subsonic/api.ts b/app/subsonic/api.ts
index 9921b5c..3552a72 100644
--- a/app/subsonic/api.ts
+++ b/app/subsonic/api.ts
@@ -11,6 +11,7 @@ import {
GetMusicDirectoryParams,
GetPlaylistParams,
GetPlaylistsParams,
+ GetSongParams,
GetTopSongsParams,
ScrobbleParams,
Search3Params,
@@ -29,6 +30,7 @@ import {
GetMusicDirectoryResponse,
GetPlaylistResponse,
GetPlaylistsResponse,
+ GetSongResponse,
GetTopSongsResponse,
Search3Response,
SubsonicResponse,
@@ -180,6 +182,11 @@ export class SubsonicApiClient {
return new SubsonicResponse(xml, new GetTopSongsResponse(xml))
}
+ async getSong(params: GetSongParams): Promise> {
+ const xml = await this.apiGetXml('getSong', params)
+ return new SubsonicResponse(xml, new GetSongResponse(xml))
+ }
+
//
// Album/song lists
//
diff --git a/app/subsonic/params.ts b/app/subsonic/params.ts
index ed2d773..116f4bd 100644
--- a/app/subsonic/params.ts
+++ b/app/subsonic/params.ts
@@ -27,6 +27,10 @@ export type GetArtistParams = {
id: string
}
+export type GetSongParams = {
+ id: string
+}
+
export type GetTopSongsParams = {
artist: string
count?: number
diff --git a/app/subsonic/responses.ts b/app/subsonic/responses.ts
index 0614ec7..41944ba 100644
--- a/app/subsonic/responses.ts
+++ b/app/subsonic/responses.ts
@@ -129,6 +129,14 @@ export class GetTopSongsResponse {
}
}
+export class GetSongResponse {
+ song: ChildElement
+
+ constructor(xml: Document) {
+ this.song = new ChildElement(xml.getElementsByTagName('song')[0])
+ }
+}
+
//
// Album/song lists
//