mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 15:02:42 +01:00
start of music store refactor
moving stuff into a state cache better separate it from view logic
This commit is contained in:
@@ -28,6 +28,25 @@ 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>) => {
|
||||||
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
setRefreshing(true)
|
||||||
|
await fetchList()
|
||||||
|
setRefreshing(false)
|
||||||
|
}, [fetchList])
|
||||||
|
|
||||||
|
useActiveServerRefresh(
|
||||||
|
useCallback(async () => {
|
||||||
|
await resetList()
|
||||||
|
await fetchList()
|
||||||
|
}, [fetchList, resetList]),
|
||||||
|
)
|
||||||
|
|
||||||
|
return { refreshing, refresh }
|
||||||
|
}
|
||||||
|
|
||||||
export const useFetchPaginatedList = <T>(
|
export const useFetchPaginatedList = <T>(
|
||||||
fetchList: (size?: number, offset?: number) => Promise<T[]>,
|
fetchList: (size?: number, offset?: number) => Promise<T[]>,
|
||||||
pageSize: number,
|
pageSize: number,
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import dimensions from '@app/styles/dimensions'
|
|||||||
import font from '@app/styles/font'
|
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 React from 'react'
|
import pick from 'lodash.pick'
|
||||||
|
import React, { 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'
|
||||||
|
|
||||||
@@ -67,6 +68,40 @@ 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 fetchArtist = useStore(store => store.fetchLibraryArtist)
|
||||||
|
const albumsLayout = useLayout()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!albums) {
|
||||||
|
fetchArtist(id)
|
||||||
|
}
|
||||||
|
}, [albums, fetchArtist, id])
|
||||||
|
|
||||||
|
const sortedAlbums = (albums ? Object.values(albums) : [])
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.sort((a, b) => (b.year || 0) - (a.year || 0))
|
||||||
|
|
||||||
|
const albumSize = albumsLayout.width / 2 - styles.contentContainer.paddingHorizontal / 2
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header>Albums</Header>
|
||||||
|
<View style={styles.albums} onLayout={albumsLayout.onLayout}>
|
||||||
|
{sortedAlbums.map(a => (
|
||||||
|
<AlbumItem key={a.id} album={a} height={albumSize} width={albumSize} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const ArtistViewFallback = React.memo(() => (
|
const ArtistViewFallback = React.memo(() => (
|
||||||
<GradientBackground style={styles.fallback}>
|
<GradientBackground style={styles.fallback}>
|
||||||
<ActivityIndicator size="large" color={colors.accent} />
|
<ActivityIndicator size="large" color={colors.accent} />
|
||||||
@@ -74,8 +109,14 @@ 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 = useArtistInfo(id)
|
||||||
const albumsLayout = useLayout()
|
|
||||||
|
const artist = useStore(store => store.entities.artists[id])
|
||||||
|
const artistInfo = useStore(store => store.entities.artistInfo[id])
|
||||||
|
|
||||||
|
const fetchArtist = useStore(store => store.fetchLibraryArtist)
|
||||||
|
const fetchArtistInfo = useStore(store => store.fetchLibraryArtistInfo)
|
||||||
|
|
||||||
const coverLayout = useLayout()
|
const coverLayout = useLayout()
|
||||||
const headerOpacity = useSharedValue(0)
|
const headerOpacity = useSharedValue(0)
|
||||||
|
|
||||||
@@ -91,16 +132,22 @@ const ArtistView = React.memo<{ id: string; title: string }>(({ id, title }) =>
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const albumSize = albumsLayout.width / 2 - styles.contentContainer.paddingHorizontal / 2
|
useEffect(() => {
|
||||||
|
if (!artist) {
|
||||||
|
fetchArtist(id)
|
||||||
|
}
|
||||||
|
}, [artist, fetchArtist, id])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!artistInfo) {
|
||||||
|
fetchArtistInfo(id)
|
||||||
|
}
|
||||||
|
}, [artistInfo, fetchArtistInfo, id])
|
||||||
|
|
||||||
if (!artist) {
|
if (!artist) {
|
||||||
return <ArtistViewFallback />
|
return <ArtistViewFallback />
|
||||||
}
|
}
|
||||||
|
|
||||||
const _albums = [...artist.albums]
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
.sort((a, b) => (b.year || 0) - (a.year || 0))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<HeaderBar title={title} headerStyle={[styles.header, animatedOpacity]} />
|
<HeaderBar title={title} headerStyle={[styles.header, animatedOpacity]} />
|
||||||
@@ -115,17 +162,12 @@ const ArtistView = React.memo<{ id: string; title: string }>(({ id, title }) =>
|
|||||||
<Text style={styles.title}>{artist.name}</Text>
|
<Text style={styles.title}>{artist.name}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.contentContainer}>
|
<View style={styles.contentContainer}>
|
||||||
{artist.topSongs.length > 0 ? (
|
{/* {artist.topSongs.length > 0 ? (
|
||||||
<TopSongs songs={artist.topSongs} name={artist.name} artistId={artist.id} />
|
<TopSongs songs={artist.topSongs} name={artist.name} artistId={artist.id} />
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)} */}
|
||||||
<Header>Albums</Header>
|
<ArtistAlbums id={id} />
|
||||||
<View style={styles.albums} onLayout={albumsLayout.onLayout}>
|
|
||||||
{_albums.map(a => (
|
|
||||||
<AlbumItem key={a.id} album={a} height={albumSize} width={albumSize} />
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</GradientScrollView>
|
</GradientScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
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 } from '@app/hooks/list'
|
import { useFetchList, 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 { selectMusic } from '@app/state/music'
|
||||||
import { selectSettings } from '@app/state/settings'
|
import { selectSettings } from '@app/state/settings'
|
||||||
import { useStore } from '@app/state/store'
|
import { useStore } from '@app/state/store'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useCallback, 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 }) => (
|
||||||
@@ -21,13 +21,18 @@ const filterOptions: OptionData[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const ArtistsList = () => {
|
const ArtistsList = () => {
|
||||||
const fetchArtists = useStore(selectMusic.fetchArtists)
|
const fetchArtists = useStore(store => store.fetchLibraryArtists)
|
||||||
const { list, refreshing, refresh } = useFetchList(fetchArtists)
|
const resetArtists = useStore(store => store.resetLibraryArtists)
|
||||||
|
|
||||||
|
const { refreshing, refresh } = useFetchList2(fetchArtists, resetArtists)
|
||||||
|
const artists = useStore(store => store.entities.artists)
|
||||||
|
|
||||||
const filter = useStore(selectSettings.libraryArtistFilter)
|
const filter = useStore(selectSettings.libraryArtistFilter)
|
||||||
const setFilter = useStore(selectSettings.setLibraryArtistFiler)
|
const setFilter = useStore(selectSettings.setLibraryArtistFiler)
|
||||||
const [sortedList, setSortedList] = useState<Artist[]>([])
|
const [sortedList, setSortedList] = useState<Artist[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const list = Object.values(artists)
|
||||||
switch (filter.type) {
|
switch (filter.type) {
|
||||||
case 'random':
|
case 'random':
|
||||||
setSortedList([...list].sort(() => Math.random() - 0.5))
|
setSortedList([...list].sort(() => Math.random() - 0.5))
|
||||||
@@ -39,7 +44,7 @@ const ArtistsList = () => {
|
|||||||
setSortedList([...list])
|
setSortedList([...list])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, [list, filter])
|
}, [filter.type, artists])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
|||||||
253
app/state/library.ts
Normal file
253
app/state/library.ts
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
import { Store } from '@app/state/store'
|
||||||
|
import { AlbumID3Element, ArtistID3Element, ArtistInfo2Element, ChildElement } from '@app/subsonic/elements'
|
||||||
|
import {
|
||||||
|
GetArtistInfo2Response,
|
||||||
|
GetArtistResponse,
|
||||||
|
GetArtistsResponse,
|
||||||
|
GetTopSongsResponse,
|
||||||
|
SubsonicResponse,
|
||||||
|
} from '@app/subsonic/responses'
|
||||||
|
import produce from 'immer'
|
||||||
|
import merge from 'lodash.merge'
|
||||||
|
import pick from 'lodash.pick'
|
||||||
|
import { GetState, SetState } from 'zustand'
|
||||||
|
|
||||||
|
export interface ById<T> {
|
||||||
|
[id: string]: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OneToMany = ById<string[]>
|
||||||
|
|
||||||
|
export interface Artist {
|
||||||
|
itemType: 'artist'
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
starred?: Date
|
||||||
|
coverArt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtistInfo {
|
||||||
|
id: string
|
||||||
|
smallImageUrl?: string
|
||||||
|
largeImageUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Album {
|
||||||
|
itemType: 'album'
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
artist?: string
|
||||||
|
artistId?: string
|
||||||
|
starred?: Date
|
||||||
|
coverArt?: string
|
||||||
|
year?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Song {
|
||||||
|
itemType: 'song'
|
||||||
|
id: string
|
||||||
|
album?: string
|
||||||
|
albumId?: string
|
||||||
|
artist?: string
|
||||||
|
artistId?: string
|
||||||
|
title: string
|
||||||
|
track?: number
|
||||||
|
discNumber?: number
|
||||||
|
duration?: number
|
||||||
|
starred?: Date
|
||||||
|
|
||||||
|
// streamUri: string
|
||||||
|
coverArt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapArtist(artist: ArtistID3Element): Artist {
|
||||||
|
return {
|
||||||
|
itemType: 'artist',
|
||||||
|
id: artist.id,
|
||||||
|
name: artist.name,
|
||||||
|
starred: artist.starred,
|
||||||
|
coverArt: artist.coverArt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapArtistInfo(id: string, info: ArtistInfo2Element): ArtistInfo {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
smallImageUrl: info.smallImageUrl,
|
||||||
|
largeImageUrl: info.largeImageUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapAlbum(album: AlbumID3Element): Album {
|
||||||
|
return {
|
||||||
|
itemType: 'album',
|
||||||
|
id: album.id,
|
||||||
|
name: album.name,
|
||||||
|
artist: album.artist,
|
||||||
|
artistId: album.artist,
|
||||||
|
starred: album.starred,
|
||||||
|
coverArt: album.coverArt,
|
||||||
|
year: album.year,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapSong(song: ChildElement): Song {
|
||||||
|
return {
|
||||||
|
itemType: 'song',
|
||||||
|
id: song.id,
|
||||||
|
album: song.album,
|
||||||
|
albumId: song.albumId,
|
||||||
|
artist: song.artist,
|
||||||
|
artistId: song.artistId,
|
||||||
|
title: song.title,
|
||||||
|
track: song.track,
|
||||||
|
discNumber: song.discNumber,
|
||||||
|
duration: song.duration,
|
||||||
|
starred: song.starred,
|
||||||
|
coverArt: song.coverArt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LibrarySlice = {
|
||||||
|
entities: {
|
||||||
|
artists: ById<Artist>
|
||||||
|
artistAlbums: OneToMany
|
||||||
|
|
||||||
|
albums: ById<Album>
|
||||||
|
|
||||||
|
artistInfo: ById<ArtistInfo>
|
||||||
|
artistNameTopSongs: OneToMany
|
||||||
|
|
||||||
|
songs: ById<Song>
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchLibraryArtists: () => Promise<void>
|
||||||
|
fetchLibraryArtist: (id: string) => Promise<void>
|
||||||
|
resetLibraryArtists: () => Promise<void>
|
||||||
|
// fetchAlbums: (artistId: string) => Promise<void>
|
||||||
|
fetchLibraryArtistInfo: (artistId: string) => Promise<void>
|
||||||
|
fetchLibraryTopSongs: (artistName: string) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>): LibrarySlice => ({
|
||||||
|
entities: {
|
||||||
|
artists: {},
|
||||||
|
artistAlbums: {},
|
||||||
|
|
||||||
|
albums: {},
|
||||||
|
|
||||||
|
artistInfo: {},
|
||||||
|
artistNameTopSongs: {},
|
||||||
|
|
||||||
|
songs: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchLibraryArtists: async () => {
|
||||||
|
const client = get().client
|
||||||
|
if (!client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: SubsonicResponse<GetArtistsResponse>
|
||||||
|
try {
|
||||||
|
response = await client.getArtists()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const artists = response.data.artists.reduce((acc, value) => {
|
||||||
|
acc[value.id] = mapArtist(value)
|
||||||
|
return acc
|
||||||
|
}, {} as ById<Artist>)
|
||||||
|
|
||||||
|
set(
|
||||||
|
produce<LibrarySlice>(state => {
|
||||||
|
state.entities.artists = artists
|
||||||
|
state.entities.artistAlbums = pick(state.entities.artistAlbums, Object.keys(artists))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchLibraryArtist: async id => {
|
||||||
|
const client = get().client
|
||||||
|
if (!client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: SubsonicResponse<GetArtistResponse>
|
||||||
|
try {
|
||||||
|
response = await client.getArtist({ id })
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const albums = response.data.albums.reduce((acc, value) => {
|
||||||
|
acc[value.id] = mapAlbum(value)
|
||||||
|
return acc
|
||||||
|
}, {} as ById<Album>)
|
||||||
|
|
||||||
|
const artist = mapArtist(response.data.artist)
|
||||||
|
|
||||||
|
set(
|
||||||
|
produce<LibrarySlice>(state => {
|
||||||
|
state.entities.artists[id] = artist
|
||||||
|
state.entities.artistAlbums[id] = Object.keys(albums)
|
||||||
|
state.entities.albums = merge(state.entities.albums, albums)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
resetLibraryArtists: async () => {
|
||||||
|
set(
|
||||||
|
produce<LibrarySlice>(state => {
|
||||||
|
state.entities.artists = {}
|
||||||
|
state.entities.artistAlbums = {}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchLibraryArtistInfo: async id => {
|
||||||
|
const client = get().client
|
||||||
|
if (!client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: SubsonicResponse<GetArtistInfo2Response>
|
||||||
|
try {
|
||||||
|
response = await client.getArtistInfo2({ id })
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = mapArtistInfo(id, response.data.artistInfo)
|
||||||
|
|
||||||
|
set(
|
||||||
|
produce<LibrarySlice>(state => {
|
||||||
|
state.entities.artistInfo[id] = info
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchLibraryTopSongs: async artistName => {
|
||||||
|
const client = get().client
|
||||||
|
if (!client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: SubsonicResponse<GetTopSongsResponse>
|
||||||
|
try {
|
||||||
|
response = await client.getTopSongs({ artist: artistName, count: 50 })
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const topSongs = response.data.songs.map(mapSong)
|
||||||
|
|
||||||
|
set(
|
||||||
|
produce<LibrarySlice>(state => {
|
||||||
|
state.entities.songs = merge(state.entities.songs, topSongs)
|
||||||
|
state.entities.artistNameTopSongs[artistName] = topSongs.map(s => s.id)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -5,6 +5,7 @@ import create from 'zustand'
|
|||||||
import { persist, StateStorage } from 'zustand/middleware'
|
import { persist, StateStorage } from 'zustand/middleware'
|
||||||
import { CacheSlice, createCacheSlice } from './cache'
|
import { CacheSlice, createCacheSlice } from './cache'
|
||||||
import migrations from './migrations'
|
import migrations from './migrations'
|
||||||
|
import { createLibrarySlice, LibrarySlice } from './library'
|
||||||
import { createMusicMapSlice, MusicMapSlice } from './musicmap'
|
import { createMusicMapSlice, MusicMapSlice } from './musicmap'
|
||||||
import { createTrackPlayerSlice, TrackPlayerSlice } from './trackplayer'
|
import { createTrackPlayerSlice, TrackPlayerSlice } from './trackplayer'
|
||||||
import { createTrackPlayerMapSlice, TrackPlayerMapSlice } from './trackplayermap'
|
import { createTrackPlayerMapSlice, TrackPlayerMapSlice } from './trackplayermap'
|
||||||
@@ -13,6 +14,7 @@ const DB_VERSION = migrations.length
|
|||||||
|
|
||||||
export type Store = SettingsSlice &
|
export type Store = SettingsSlice &
|
||||||
MusicSlice &
|
MusicSlice &
|
||||||
|
LibrarySlice &
|
||||||
MusicMapSlice &
|
MusicMapSlice &
|
||||||
TrackPlayerSlice &
|
TrackPlayerSlice &
|
||||||
TrackPlayerMapSlice &
|
TrackPlayerMapSlice &
|
||||||
@@ -44,6 +46,7 @@ export const useStore = create<Store>(
|
|||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
...createSettingsSlice(set, get),
|
...createSettingsSlice(set, get),
|
||||||
...createMusicSlice(set, get),
|
...createMusicSlice(set, get),
|
||||||
|
...createLibrarySlice(set, get),
|
||||||
...createMusicMapSlice(set, get),
|
...createMusicMapSlice(set, get),
|
||||||
...createTrackPlayerSlice(set, get),
|
...createTrackPlayerSlice(set, get),
|
||||||
...createTrackPlayerMapSlice(set, get),
|
...createTrackPlayerMapSlice(set, get),
|
||||||
|
|||||||
@@ -31,6 +31,8 @@
|
|||||||
"@xmldom/xmldom": "^0.7.0",
|
"@xmldom/xmldom": "^0.7.0",
|
||||||
"immer": "^9.0.6",
|
"immer": "^9.0.6",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
|
"lodash.pick": "^4.4.0",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-native": "0.67.1",
|
"react-native": "0.67.1",
|
||||||
@@ -56,6 +58,8 @@
|
|||||||
"@react-native-community/eslint-config": "^2.0.0",
|
"@react-native-community/eslint-config": "^2.0.0",
|
||||||
"@types/jest": "^26.0.23",
|
"@types/jest": "^26.0.23",
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
|
"@types/lodash.merge": "^4.6.6",
|
||||||
|
"@types/lodash.pick": "^4.4.6",
|
||||||
"@types/md5": "^2.3.0",
|
"@types/md5": "^2.3.0",
|
||||||
"@types/react-native-vector-icons": "^6.4.7",
|
"@types/react-native-vector-icons": "^6.4.7",
|
||||||
"@types/react-test-renderer": "^16.9.2",
|
"@types/react-test-renderer": "^16.9.2",
|
||||||
|
|||||||
14
yarn.lock
14
yarn.lock
@@ -1492,6 +1492,20 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/lodash" "*"
|
"@types/lodash" "*"
|
||||||
|
|
||||||
|
"@types/lodash.merge@^4.6.6":
|
||||||
|
version "4.6.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash.merge/-/lodash.merge-4.6.6.tgz#b84b403c1d31bc42d51772d1cd5557fa008cd3d6"
|
||||||
|
integrity sha512-IB90krzMf7YpfgP3u/EvZEdXVvm4e3gJbUvh5ieuI+o+XqiNEt6fCzqNRaiLlPVScLI59RxIGZMQ3+Ko/DJ8vQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/lodash" "*"
|
||||||
|
|
||||||
|
"@types/lodash.pick@^4.4.6":
|
||||||
|
version "4.4.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash.pick/-/lodash.pick-4.4.6.tgz#ae4e8f109e982786313bb6aac4b1a73aefa6e9be"
|
||||||
|
integrity sha512-u8bzA16qQ+8dY280z3aK7PoWb3fzX5ATJ0rJB6F+uqchOX2VYF02Aqa+8aYiHiHgPzQiITqCgeimlyKFy4OA6g==
|
||||||
|
dependencies:
|
||||||
|
"@types/lodash" "*"
|
||||||
|
|
||||||
"@types/lodash@*", "@types/lodash@^4.14.53":
|
"@types/lodash@*", "@types/lodash@^4.14.53":
|
||||||
version "4.14.178"
|
version "4.14.178"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8"
|
||||||
|
|||||||
Reference in New Issue
Block a user