feat: add extra metadata to the album view
This commit is contained in:
@@ -1,15 +1,56 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useRoute, RouteProp } from '@react-navigation/native';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useRoute, RouteProp, useNavigation } from '@react-navigation/native';
|
||||
import { useAppDispatch, useTypedSelector } from 'store';
|
||||
import TrackListView from './components/TrackListView';
|
||||
import { fetchTracksByAlbum } from 'store/music/actions';
|
||||
import { fetchAlbum, fetchTracksByAlbum } from 'store/music/actions';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { ALBUM_CACHE_AMOUNT_OF_DAYS } from 'CONSTANTS';
|
||||
import { t } from '@localisation';
|
||||
import { StackParams } from 'screens/types';
|
||||
import { NavigationProp, StackParams } from 'screens/types';
|
||||
import { SubHeader, Text } from 'components/Typography';
|
||||
import { ScrollView } from 'react-native-gesture-handler';
|
||||
import { useGetImage } from 'utility/JellyfinApi';
|
||||
import styled from 'styled-components';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { Dimensions, Pressable, useColorScheme } from 'react-native';
|
||||
import { Container } from '@shopify/react-native-skia/lib/typescript/src/renderer/Container';
|
||||
import AlbumImage from './components/AlbumImage';
|
||||
|
||||
type Route = RouteProp<StackParams, 'Album'>;
|
||||
|
||||
const Screen = Dimensions.get('screen');
|
||||
|
||||
const Cover = styled(AlbumImage)`
|
||||
height: ${Screen.width / 2.8};
|
||||
width: ${Screen.width / 2.8};
|
||||
border-radius: 12px;
|
||||
`;
|
||||
|
||||
function SimilarAlbum({ id }: { id: string }) {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const getImage = useGetImage();
|
||||
const album = useTypedSelector((state) => state.music.albums.entities[id]);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
album && navigation.push('Album', { id, album });
|
||||
}, [id, album, navigation]);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => ({
|
||||
opacity: pressed ? 0.5 : 1.0,
|
||||
width: Screen.width / 2.8,
|
||||
marginRight: 12
|
||||
})}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<Cover key={id} source={{ uri: getImage(id) }} />
|
||||
<Text numberOfLines={1} style={{ fontSize: 13 }}>{album?.Name}</Text>
|
||||
<Text numberOfLines={1} style={{ opacity: 0.5, fontSize: 13 }}>{album?.Artists.join(', ')}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const Album: React.FC = () => {
|
||||
const { params: { id } } = useRoute<Route>();
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -19,7 +60,10 @@ const Album: React.FC = () => {
|
||||
const albumTracks = useTypedSelector((state) => state.music.tracks.byAlbum[id]);
|
||||
|
||||
// Define a function for refreshing this entity
|
||||
const refresh = useCallback(() => { dispatch(fetchTracksByAlbum(id)); }, [id, dispatch]);
|
||||
const refresh = useCallback(() => {
|
||||
dispatch(fetchTracksByAlbum(id));
|
||||
dispatch(fetchAlbum(id));
|
||||
}, [id, dispatch]);
|
||||
|
||||
// Auto-fetch the track data periodically
|
||||
useEffect(() => {
|
||||
@@ -39,7 +83,21 @@ const Album: React.FC = () => {
|
||||
shuffleButtonText={t('shuffle-album')}
|
||||
downloadButtonText={t('download-album')}
|
||||
deleteButtonText={t('delete-album')}
|
||||
/>
|
||||
>
|
||||
{album?.Overview && (
|
||||
<Text style={{ opacity: 0.5, lineHeight: 20, fontSize: 12, paddingBottom: 24 }}>{album?.Overview}</Text>
|
||||
)}
|
||||
{album?.Similar && (
|
||||
<>
|
||||
<SubHeader>Similar albums</SubHeader>
|
||||
<ScrollView horizontal style={{ marginLeft: -24, marginTop: 8, marginBottom: 36 }} contentContainerStyle={{ paddingLeft: 24 }} showsHorizontalScrollIndicator={false}>
|
||||
{album.Similar.map((id) => (
|
||||
<SimilarAlbum id={id} key={id} />
|
||||
))}
|
||||
</ScrollView>
|
||||
</>
|
||||
)}
|
||||
</TrackListView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ function AlbumImage(props: FastImageProps) {
|
||||
|
||||
if (!props.source || hasError) {
|
||||
return (
|
||||
<Container source={colorScheme === 'light' ? require('assets/images/empty-album-light.png') : require('assets/images/empty-album-dark.png')} />
|
||||
<Container {...props} source={colorScheme === 'light' ? require('assets/images/empty-album-light.png') : require('assets/images/empty-album-dark.png')} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { PropsWithChildren, useCallback, useMemo } from 'react';
|
||||
import { ScrollView, RefreshControl, StyleSheet, View } from 'react-native';
|
||||
import { useGetImage } from 'utility/JellyfinApi';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
@@ -26,6 +26,7 @@ import { Text } from 'components/Typography';
|
||||
import CoverImage from 'components/CoverImage';
|
||||
import ticksToDuration from 'utility/ticksToDuration';
|
||||
import { useNavigatorPadding } from 'utility/SafeNavigatorView';
|
||||
import { t } from '@localisation';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
index: {
|
||||
@@ -55,7 +56,7 @@ const TrackContainer = styled.View<{ isPlaying: boolean }>`
|
||||
`}
|
||||
`;
|
||||
|
||||
interface TrackListViewProps {
|
||||
export interface TrackListViewProps extends PropsWithChildren<{}> {
|
||||
title?: string;
|
||||
artist?: string;
|
||||
trackIds: EntityId[];
|
||||
@@ -81,6 +82,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
||||
deleteButtonText,
|
||||
listNumberingStyle = 'album',
|
||||
itemDisplayStyle = 'album',
|
||||
children
|
||||
}) => {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const navigatorPadding = useNavigatorPadding();
|
||||
@@ -89,6 +91,11 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
||||
const tracks = useTypedSelector((state) => state.music.tracks.entities);
|
||||
const isLoading = useTypedSelector((state) => state.music.tracks.isLoading);
|
||||
const downloadedTracks = useTypedSelector(selectDownloadedTracks(trackIds));
|
||||
const totalDuration = useMemo(() => (
|
||||
trackIds.reduce<number>((sum, trackId) => (
|
||||
sum + (tracks[trackId]?.RunTimeTicks || 0)
|
||||
), 0)
|
||||
), [trackIds, tracks]);
|
||||
|
||||
// Retrieve helpers
|
||||
const getImage = useGetImage();
|
||||
@@ -157,7 +164,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
||||
? i + 1
|
||||
: tracks[trackId]?.IndexNumber}
|
||||
</Text>
|
||||
<View>
|
||||
<View style={{ flexShrink: 1 }}>
|
||||
<Text
|
||||
style={{
|
||||
...currentTrack?.backendId === trackId && styles.activeText,
|
||||
@@ -197,6 +204,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
||||
</TrackContainer>
|
||||
</TouchableHandler>
|
||||
)}
|
||||
<Text style={{ paddingTop: 24, paddingBottom: 12, textAlign: 'center', opacity: 0.5 }}>{t('total-duration')}: {ticksToDuration(totalDuration)}</Text>
|
||||
<WrappableButtonRow style={{ marginTop: 24 }}>
|
||||
<WrappableButton
|
||||
icon={CloudDownArrow}
|
||||
@@ -214,6 +222,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
||||
/>
|
||||
</WrappableButtonRow>
|
||||
</View>
|
||||
{children}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user