From d245d4b786a8323eb8ec27d0d08b72fc27ee8a87 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Fri, 9 Jul 2021 14:34:15 +0900 Subject: [PATCH] home screen first pass --- app/components/AlbumArt.tsx | 2 +- app/components/GradientScrollView.tsx | 18 ++-- app/components/PressableOpacity.tsx | 1 + app/models/music.ts | 8 ++ app/navigation/BottomTabNavigator.tsx | 4 +- app/screens/Home.tsx | 115 ++++++++++++++++++++++++ app/screens/LibraryAlbums.tsx | 52 ++++++++--- app/state/music.ts | 121 +++++++++++++++++++++++++- 8 files changed, 296 insertions(+), 25 deletions(-) create mode 100644 app/screens/Home.tsx diff --git a/app/components/AlbumArt.tsx b/app/components/AlbumArt.tsx index dbabc66..90cd225 100644 --- a/app/components/AlbumArt.tsx +++ b/app/components/AlbumArt.tsx @@ -31,7 +31,7 @@ const AlbumArt: React.FC = ({ id, height, width }) => { PlaceholderComponent={Placeholder} height={height} width={width} - coverArtUri={width > 128 ? albumArt?.uri : albumArt?.thumbUri} + coverArtUri={width > 200 ? albumArt?.uri : albumArt?.thumbUri} /> ) } diff --git a/app/components/GradientScrollView.tsx b/app/components/GradientScrollView.tsx index b564697..bf6816d 100644 --- a/app/components/GradientScrollView.tsx +++ b/app/components/GradientScrollView.tsx @@ -1,14 +1,20 @@ -import React from 'react' -import { ScrollView, ScrollViewProps, ViewStyle } from 'react-native' -import colors from '@app/styles/colors' import GradientBackground from '@app/components/GradientBackground' +import colors from '@app/styles/colors' +import dimensions from '@app/styles/dimensions' +import React from 'react' +import { ScrollView, ScrollViewProps, useWindowDimensions } from 'react-native' const GradientScrollView: React.FC = props => { - props.style = props.style || {} - ;(props.style as ViewStyle).backgroundColor = colors.gradient.low + const layout = useWindowDimensions() + + const minHeight = layout.height - (dimensions.top() + dimensions.bottom()) return ( - + {props.children} diff --git a/app/components/PressableOpacity.tsx b/app/components/PressableOpacity.tsx index 10e4d4e..a442312 100644 --- a/app/components/PressableOpacity.tsx +++ b/app/components/PressableOpacity.tsx @@ -4,6 +4,7 @@ import { LayoutRectangle, Pressable, PressableProps } from 'react-native' type PressableOpacityProps = PressableProps & { ripple?: boolean rippleColor?: string + unstable_pressDelay?: number } const PressableOpacity: React.FC = props => { diff --git a/app/models/music.ts b/app/models/music.ts index 9d9d258..f7ff3c9 100644 --- a/app/models/music.ts +++ b/app/models/music.ts @@ -29,6 +29,14 @@ export interface Album { year?: number } +export interface AlbumListItem { + id: string + name: string + artist?: string + starred?: Date + coverArtThumbUri?: string +} + export interface AlbumArt { uri?: string thumbUri?: string diff --git a/app/navigation/BottomTabNavigator.tsx b/app/navigation/BottomTabNavigator.tsx index 495dfbc..c2c6f4b 100644 --- a/app/navigation/BottomTabNavigator.tsx +++ b/app/navigation/BottomTabNavigator.tsx @@ -2,16 +2,16 @@ import React from 'react' import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' import SettingsView from '@app/screens/Settings' import NowPlayingLayout from '@app/screens/NowPlayingLayout' -import ArtistsList from '@app/screens/ArtistsList' import LibraryTopTabNavigator from '@app/navigation/LibraryTopTabNavigator' import BottomTabBar from '@app/navigation/BottomTabBar' +import Home from '@app/screens/Home' const Tab = createBottomTabNavigator() const BottomTabNavigator = () => { return ( - + diff --git a/app/screens/Home.tsx b/app/screens/Home.tsx new file mode 100644 index 0000000..1b66db1 --- /dev/null +++ b/app/screens/Home.tsx @@ -0,0 +1,115 @@ +import CoverArt from '@app/components/CoverArt' +import GradientScrollView from '@app/components/GradientScrollView' +import PressableOpacity from '@app/components/PressableOpacity' +import { albumLists } from '@app/state/music' +import colors from '@app/styles/colors' +import font from '@app/styles/font' +import { useNavigation } from '@react-navigation/native' +import { useAtomValue } from 'jotai/utils' +import React from 'react' +import { ScrollView, StatusBar, StyleSheet, Text, View } from 'react-native' + +const Category: React.FC<{ + name: string + stateKey: string +}> = ({ name, stateKey }) => { + const navigation = useNavigation() + + const state = albumLists[stateKey] + const list = useAtomValue(state.listAtom) + const updating = useAtomValue(state.updatingAtom) + const updateList = state.useUpdateList() + + return ( + + + {name} + + + {list.map(album => ( + navigation.navigate('AlbumView', { id: album.id, title: album.name })} + unstable_pressDelay={50} + key={album.id} + style={styles.item}> + <>} + coverArtUri={album.coverArtThumbUri} + height={styles.item.width} + width={styles.item.width} + /> + + {album.name} + + + {album.artist} + + + ))} + + + ) +} + +const Home = () => ( + + + + + + + + + +) + +const styles = StyleSheet.create({ + scroll: { + flex: 1, + }, + scrollContentContainer: { + paddingTop: StatusBar.currentHeight, + }, + content: { + paddingBottom: 20, + }, + category: { + marginTop: 12, + }, + header: { + fontFamily: font.bold, + fontSize: 24, + color: colors.text.primary, + paddingHorizontal: 20, + marginTop: 4, + }, + artScroll: { + marginTop: 10, + height: 190, + }, + artScrollContent: { + paddingLeft: 20, + }, + item: { + marginRight: 10, + width: 150, + alignItems: 'flex-start', + }, + title: { + fontFamily: font.semiBold, + fontSize: 13, + color: colors.text.primary, + marginTop: 4, + }, + subtitle: { + fontFamily: font.regular, + fontSize: 12, + color: colors.text.secondary, + }, +}) + +export default Home diff --git a/app/screens/LibraryAlbums.tsx b/app/screens/LibraryAlbums.tsx index ea9a673..03c3ce8 100644 --- a/app/screens/LibraryAlbums.tsx +++ b/app/screens/LibraryAlbums.tsx @@ -1,7 +1,7 @@ import { useNavigation } from '@react-navigation/native' import { useAtomValue } from 'jotai/utils' import React, { useEffect } from 'react' -import { Pressable, StyleSheet, Text, View } from 'react-native' +import { Pressable, StyleSheet, Text, useWindowDimensions, View } from 'react-native' import { Album } from '@app/models/music' import { albumsAtom, albumsUpdatingAtom, useUpdateAlbums } from '@app/state/music' import font from '@app/styles/font' @@ -12,15 +12,19 @@ import colors from '@app/styles/colors' const AlbumItem: React.FC<{ id: string name: string + size: number + height: number artist?: string -}> = ({ id, name, artist }) => { +}> = ({ id, name, artist, size, height }) => { const navigation = useNavigation() return ( - navigation.navigate('AlbumView', { id, title: name })}> - + navigation.navigate('AlbumView', { id, title: name })}> + - + {name} @@ -32,16 +36,28 @@ const AlbumItem: React.FC<{ } const MemoAlbumItem = React.memo(AlbumItem) -const AlbumListRenderItem: React.FC<{ item: Album }> = ({ item }) => ( - +const AlbumListRenderItem: React.FC<{ + item: { album: Album; size: number; height: number } +}> = ({ item }) => ( + ) const AlbumsList = () => { const albums = useAtomValue(albumsAtom) const updating = useAtomValue(albumsUpdatingAtom) const updateAlbums = useUpdateAlbums() + const layout = useWindowDimensions() - const albumsList = Object.values(albums) + const size = layout.width / 3 - styles.item.marginHorizontal * 2 + const height = size + 44 + + const albumsList = Object.values(albums).map(album => ({ album, size, height })) useEffect(() => { if (albumsList.length === 0) { @@ -54,12 +70,17 @@ const AlbumsList = () => { item.id} + keyExtractor={item => item.album.id} numColumns={3} removeClippedSubviews={true} refreshing={updating} onRefresh={updateAlbums} overScrollMode="never" + getItemLayout={(_data, index) => ({ + length: height, + offset: height * Math.floor(index / 3), + index, + })} /> ) @@ -77,24 +98,27 @@ const styles = StyleSheet.create({ }, item: { alignItems: 'center', - marginVertical: 8, + marginVertical: 4, + marginHorizontal: 2, flex: 1 / 3, + // backgroundColor: 'green', }, art: { - height: 125, + // height: 125, }, itemDetails: { flex: 1, - width: 125, + width: '100%', + // width: 125, }, title: { - fontSize: 13, + fontSize: 12, fontFamily: font.semiBold, color: colors.text.primary, marginTop: 4, }, subtitle: { - fontSize: 12, + fontSize: 11, fontFamily: font.regular, color: colors.text.secondary, }, diff --git a/app/state/music.ts b/app/state/music.ts index 56e3f2e..1cdd4df 100644 --- a/app/state/music.ts +++ b/app/state/music.ts @@ -1,10 +1,11 @@ -import { atom, useAtom } from 'jotai' +import { Atom, atom, useAtom, WritableAtom } from 'jotai' import { atomFamily, useAtomValue, useUpdateAtom } from 'jotai/utils' -import { Album, AlbumArt, AlbumWithSongs, Artist, ArtistArt, ArtistInfo, Song } from '@app/models/music' +import { Album, AlbumArt, AlbumListItem, AlbumWithSongs, Artist, ArtistArt, ArtistInfo, Song } from '@app/models/music' import { SubsonicApiClient } from '@app/subsonic/api' import { AlbumID3Element, ArtistInfo2Element, ChildElement } from '@app/subsonic/elements' import { GetArtistResponse } from '@app/subsonic/responses' import { activeServerAtom } from '@app/state/settings' +import { GetAlbumList2Type } from '@app/subsonic/params' export const artistsAtom = atom([]) export const artistsUpdatingAtom = atom(false) @@ -70,6 +71,112 @@ export const useUpdateAlbums = () => { } } +const useUpdateAlbumListBase = ( + type: GetAlbumList2Type, + albumListAtom: WritableAtom, + updatingAtom: WritableAtom, +) => { + const server = useAtomValue(activeServerAtom) + const setAlbumList = useUpdateAtom(albumListAtom) + const [updating, setUpdating] = useAtom(updatingAtom) + + if (!server) { + return async () => {} + } + + return async () => { + if (updating) { + return + } + setUpdating(true) + + const client = new SubsonicApiClient(server) + const response = await client.getAlbumList2({ type, size: 20 }) + + setAlbumList(response.data.albums.map(a => mapAlbumID3toAlbumListItem(a, client))) + setUpdating(false) + } +} + +function createAlbumList(type: GetAlbumList2Type) { + const listAtom = atom([]) + const listReadAtom = atom(get => get(listAtom)) + const updatingAtom = atom(false) + const updatingReadAtom = atom(get => get(updatingAtom)) + const useUpdateAlbumList = () => useUpdateAlbumListBase(type, listAtom, updatingAtom) + + return { listAtom, listReadAtom, updatingAtom, updatingReadAtom, useUpdateAlbumList } +} + +type ListState = { + listAtom: Atom + updatingAtom: Atom + useUpdateList: () => () => Promise +} + +const recent = createAlbumList('recent') +const starred = createAlbumList('starred') +const frequent = createAlbumList('frequent') +const random = createAlbumList('random') +const newest = createAlbumList('newest') + +export const albumLists: { [key: string]: ListState } = { + recent: { + listAtom: recent.listReadAtom, + updatingAtom: recent.updatingReadAtom, + useUpdateList: recent.useUpdateAlbumList, + }, + starred: { + listAtom: starred.listReadAtom, + updatingAtom: starred.updatingReadAtom, + useUpdateList: starred.useUpdateAlbumList, + }, + frequent: { + listAtom: frequent.listReadAtom, + updatingAtom: frequent.updatingReadAtom, + useUpdateList: frequent.useUpdateAlbumList, + }, + random: { + listAtom: random.listReadAtom, + updatingAtom: random.updatingReadAtom, + useUpdateList: random.useUpdateAlbumList, + }, + newest: { + listAtom: newest.listReadAtom, + updatingAtom: newest.updatingReadAtom, + useUpdateList: newest.useUpdateAlbumList, + }, +} + +export const useRecentAlbums = () => { + const server = useAtomValue(activeServerAtom) + const [updating, setUpdating] = useAtom(albumsUpdatingAtom) + const setAlbums = useUpdateAtom(albumsAtom) + + if (!server) { + return async () => {} + } + + return async () => { + if (updating) { + return + } + setUpdating(true) + + const client = new SubsonicApiClient(server) + const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 }) + + setAlbums( + response.data.albums.reduce((acc, next) => { + const album = mapAlbumID3(next, client) + acc[album.id] = album + return acc + }, {} as Record), + ) + setUpdating(false) + } +} + export const albumAtomFamily = atomFamily((id: string) => atom(async get => { const server = get(activeServerAtom) @@ -183,6 +290,16 @@ function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album { } } +function mapAlbumID3toAlbumListItem(album: AlbumID3Element, client: SubsonicApiClient): AlbumListItem { + return { + id: album.id, + name: album.name, + artist: album.artist, + starred: album.starred, + coverArtThumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined, + } +} + function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song { return { ...child,