mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 09:29:29 +01:00
artist art pull from last.fm or album covers
This commit is contained in:
parent
379779735e
commit
23e6c0caf0
34
src/components/common/AlbumArt.tsx
Normal file
34
src/components/common/AlbumArt.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import FastImage from 'react-native-fast-image';
|
||||||
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
|
import colors from '../../styles/colors';
|
||||||
|
import CoverArt from './CoverArt';
|
||||||
|
|
||||||
|
const AlbumArt: React.FC<{
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
coverArtUri?: string
|
||||||
|
}> = ({ height, width, coverArtUri }) => {
|
||||||
|
const Placeholder = () => (
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.accent, colors.accentLow]}
|
||||||
|
>
|
||||||
|
<FastImage
|
||||||
|
source={require('../../../res/record.png')}
|
||||||
|
style={{ height, width }}
|
||||||
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
|
/>
|
||||||
|
</LinearGradient>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CoverArt
|
||||||
|
PlaceholderComponent={Placeholder}
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
coverArtUri={coverArtUri}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(AlbumArt);
|
||||||
@ -2,56 +2,13 @@ import { useNavigation } from '@react-navigation/native';
|
|||||||
import { useAtomValue } from 'jotai/utils';
|
import { useAtomValue } from 'jotai/utils';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { GestureResponderEvent, Image, Pressable, ScrollView, Text, useWindowDimensions, View } from 'react-native';
|
import { GestureResponderEvent, Image, Pressable, ScrollView, Text, useWindowDimensions, View } from 'react-native';
|
||||||
import { TrackPlayerEvents } from 'react-native-track-player';
|
|
||||||
import { useCurrentTrackId, useSetQueue } from '../../hooks/player';
|
import { useCurrentTrackId, useSetQueue } from '../../hooks/player';
|
||||||
import { albumAtomFamily } from '../../state/music';
|
import { albumAtomFamily } from '../../state/music';
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors';
|
||||||
import text from '../../styles/text';
|
import text from '../../styles/text';
|
||||||
import AlbumCover from './AlbumCover';
|
import AlbumArt from './AlbumArt';
|
||||||
import GradientBackground from './GradientBackground';
|
import Button from './Button';
|
||||||
|
import GradientScrollView from './GradientScrollView';
|
||||||
function secondsToTime(s: number): string {
|
|
||||||
const seconds = s % 60;
|
|
||||||
const minutes = Math.floor(s / 60) % 60;
|
|
||||||
const hours = Math.floor(s / 60 / 60);
|
|
||||||
|
|
||||||
let time = `${minutes.toString().padStart(1, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
||||||
if (hours > 0) {
|
|
||||||
time = `${hours}:${time}`;
|
|
||||||
}
|
|
||||||
return time;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Button: React.FC<{
|
|
||||||
title: string;
|
|
||||||
onPress: (event: GestureResponderEvent) => void;
|
|
||||||
}> = ({ title, onPress }) => {
|
|
||||||
const [opacity, setOpacity] = useState(1);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
onPress={onPress}
|
|
||||||
onPressIn={() => setOpacity(0.6)}
|
|
||||||
onPressOut={() => setOpacity(1)}
|
|
||||||
onLongPress={() => setOpacity(1)}
|
|
||||||
style={{
|
|
||||||
backgroundColor: colors.accent,
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
minHeight: 42,
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderRadius: 1000,
|
|
||||||
opacity,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ ...text.button }}>{title}</Text>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const songEvents = [
|
|
||||||
TrackPlayerEvents.PLAYBACK_STATE,
|
|
||||||
TrackPlayerEvents.PLAYBACK_TRACK_CHANGED,
|
|
||||||
]
|
|
||||||
|
|
||||||
const SongItem: React.FC<{
|
const SongItem: React.FC<{
|
||||||
id: string;
|
id: string;
|
||||||
@ -134,7 +91,7 @@ const AlbumDetails: React.FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<GradientScrollView
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
@ -142,10 +99,8 @@ const AlbumDetails: React.FC<{
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingTop: coverSize / 8,
|
paddingTop: coverSize / 8,
|
||||||
}}
|
}}
|
||||||
overScrollMode='never'
|
|
||||||
>
|
>
|
||||||
<GradientBackground />
|
<AlbumArt
|
||||||
<AlbumCover
|
|
||||||
height={coverSize}
|
height={coverSize}
|
||||||
width={coverSize}
|
width={coverSize}
|
||||||
coverArtUri={album.coverArtUri}
|
coverArtUri={album.coverArtUri}
|
||||||
@ -199,11 +154,10 @@ const AlbumDetails: React.FC<{
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</ScrollView>
|
</GradientScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const AlbumView: React.FC<{
|
const AlbumView: React.FC<{
|
||||||
id: string,
|
id: string,
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
193
src/components/common/ArtistArt.tsx
Normal file
193
src/components/common/ArtistArt.tsx
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
import FastImage from 'react-native-fast-image';
|
||||||
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
|
import colors from '../../styles/colors';
|
||||||
|
import CoverArt from './CoverArt';
|
||||||
|
|
||||||
|
const PlaceholderContainer: React.FC<{
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
}> = ({ height, width, children}) => (
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.accent, colors.accentLow]}
|
||||||
|
style={{
|
||||||
|
height, width,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LinearGradient>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FourUp: React.FC<{
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
coverArtUris: string[];
|
||||||
|
}> = ({ height, width, coverArtUris }) => {
|
||||||
|
const halfHeight = height / 2;
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlaceholderContainer height={height} width={width}>
|
||||||
|
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
|
||||||
|
<FastImage
|
||||||
|
source={{ uri: coverArtUris[0] }}
|
||||||
|
style={{ height: halfHeight, width: halfWidth }}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
/>
|
||||||
|
<FastImage
|
||||||
|
source={{ uri: coverArtUris[1] }}
|
||||||
|
style={{ height: halfHeight, width: halfWidth }}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
|
||||||
|
<FastImage
|
||||||
|
source={{ uri: coverArtUris[2] }}
|
||||||
|
style={{ height: halfHeight, width: halfWidth }}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
/>
|
||||||
|
<FastImage
|
||||||
|
source={{ uri: coverArtUris[3] }}
|
||||||
|
style={{ height: halfHeight, width: halfWidth }}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</PlaceholderContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThreeUp: React.FC<{
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
coverArtUris: string[];
|
||||||
|
}> = ({ height, width, coverArtUris }) => {
|
||||||
|
const halfHeight = height / 2;
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlaceholderContainer height={height} width={width}>
|
||||||
|
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
|
||||||
|
<FastImage
|
||||||
|
source={{ uri: coverArtUris[0] }}
|
||||||
|
style={{ height: halfHeight, width }}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
|
||||||
|
<FastImage
|
||||||
|
source={{ uri: coverArtUris[1] }}
|
||||||
|
style={{ height: halfHeight, width: halfWidth }}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
/>
|
||||||
|
<FastImage
|
||||||
|
source={{ uri: coverArtUris[2] }}
|
||||||
|
style={{ height: halfHeight, width: halfWidth }}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</PlaceholderContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TwoUp: React.FC<{
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
coverArtUris: string[];
|
||||||
|
}> = ({ height, width, coverArtUris }) => {
|
||||||
|
const halfHeight = height / 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PlaceholderContainer height={height} width={width}>
|
||||||
|
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
|
||||||
|
<FastImage
|
||||||
|
source={{ uri: coverArtUris[0] }}
|
||||||
|
style={{ height: halfHeight, width }}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
|
||||||
|
<FastImage
|
||||||
|
source={{ uri: coverArtUris[1] }}
|
||||||
|
style={{ height: halfHeight, width }}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</PlaceholderContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OneUp: React.FC<{
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
coverArtUris: string[];
|
||||||
|
}> = ({ height, width, coverArtUris }) => {
|
||||||
|
return (
|
||||||
|
<PlaceholderContainer height={height} width={width}>
|
||||||
|
<FastImage
|
||||||
|
source={{ uri: coverArtUris[0] }}
|
||||||
|
style={{ height, width }}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
/>
|
||||||
|
</PlaceholderContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NoneUp: React.FC<{
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
}> = ({ height, width }) => {
|
||||||
|
return (
|
||||||
|
<PlaceholderContainer height={height} width={width}>
|
||||||
|
<FastImage
|
||||||
|
source={require('../../../res/mic_on-fill.png')}
|
||||||
|
style={{
|
||||||
|
height: height - height / 4,
|
||||||
|
width: width - width / 4,
|
||||||
|
}}
|
||||||
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
/>
|
||||||
|
</PlaceholderContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArtistArt: React.FC<{
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
mediumImageUrl?: string;
|
||||||
|
coverArtUris?: string[]
|
||||||
|
}> = ({ height, width, mediumImageUrl, coverArtUris }) => {
|
||||||
|
const Placeholder = () => {
|
||||||
|
if (coverArtUris && coverArtUris.length >= 4) {
|
||||||
|
return <FourUp height={height} width={width} coverArtUris={coverArtUris} />;
|
||||||
|
}
|
||||||
|
if (coverArtUris && coverArtUris.length === 3) {
|
||||||
|
return <ThreeUp height={height} width={width} coverArtUris={coverArtUris} />;
|
||||||
|
}
|
||||||
|
if (coverArtUris && coverArtUris.length === 2) {
|
||||||
|
return <TwoUp height={height} width={width} coverArtUris={coverArtUris} />;
|
||||||
|
}
|
||||||
|
if (coverArtUris && coverArtUris.length === 1) {
|
||||||
|
return <OneUp height={height} width={width} coverArtUris={coverArtUris} />;
|
||||||
|
}
|
||||||
|
return <NoneUp height={height} width={width} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{
|
||||||
|
borderRadius: height / 2,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<CoverArt
|
||||||
|
PlaceholderComponent={Placeholder}
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
coverArtUri={mediumImageUrl}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(ArtistArt);
|
||||||
55
src/components/common/ArtistView.tsx
Normal file
55
src/components/common/ArtistView.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { useAtomValue } from 'jotai/utils';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Text } from 'react-native';
|
||||||
|
import { artistInfoAtomFamily } from '../../state/music';
|
||||||
|
import text from '../../styles/text';
|
||||||
|
import ArtistArt from './ArtistArt';
|
||||||
|
import GradientScrollView from './GradientScrollView';
|
||||||
|
|
||||||
|
const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
|
||||||
|
const artist = useAtomValue(artistInfoAtomFamily(id));
|
||||||
|
|
||||||
|
if (!artist) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GradientScrollView
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
contentContainerStyle={{
|
||||||
|
alignItems: 'center',
|
||||||
|
// paddingTop: coverSize / 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={text.paragraph}>{artist.name}</Text>
|
||||||
|
<ArtistArt
|
||||||
|
height={200}
|
||||||
|
width={200}
|
||||||
|
mediumImageUrl={artist.mediumImageUrl}
|
||||||
|
coverArtUris={artist.coverArtUris}
|
||||||
|
/>
|
||||||
|
</GradientScrollView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ArtistView: React.FC<{
|
||||||
|
id: string,
|
||||||
|
title: string;
|
||||||
|
}> = ({ id, title }) => {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
navigation.setOptions({ title });
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||||
|
<ArtistDetails id={id} />
|
||||||
|
</React.Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(ArtistView);
|
||||||
32
src/components/common/Button.tsx
Normal file
32
src/components/common/Button.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { GestureResponderEvent, Pressable, Text } from 'react-native';
|
||||||
|
import colors from '../../styles/colors';
|
||||||
|
import text from '../../styles/text';
|
||||||
|
|
||||||
|
const Button: React.FC<{
|
||||||
|
title: string;
|
||||||
|
onPress: (event: GestureResponderEvent) => void;
|
||||||
|
}> = ({ title, onPress }) => {
|
||||||
|
const [opacity, setOpacity] = useState(1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={onPress}
|
||||||
|
onPressIn={() => setOpacity(0.6)}
|
||||||
|
onPressOut={() => setOpacity(1)}
|
||||||
|
onLongPress={() => setOpacity(1)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.accent,
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
minHeight: 42,
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 1000,
|
||||||
|
opacity,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ ...text.button }}>{title}</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button;
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ActivityIndicator, View } from 'react-native';
|
import { ActivityIndicator, View } from 'react-native';
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image';
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors';
|
||||||
|
|
||||||
const AlbumCover: React.FC<{
|
const CoverArt: React.FC<{
|
||||||
|
PlaceholderComponent: () => JSX.Element,
|
||||||
height: number,
|
height: number,
|
||||||
width: number,
|
width: number,
|
||||||
coverArtUri?: string
|
coverArtUri?: string
|
||||||
}> = ({ height, width, coverArtUri }) => {
|
}> = ({ PlaceholderComponent, height, width, coverArtUri }) => {
|
||||||
const [placeholderVisible, setPlaceholderVisible] = useState(false);
|
const [placeholderVisible, setPlaceholderVisible] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@ -16,23 +16,15 @@ const AlbumCover: React.FC<{
|
|||||||
const halfIndicatorHeight = indicatorSize === 'large' ? 18 : 10;
|
const halfIndicatorHeight = indicatorSize === 'large' ? 18 : 10;
|
||||||
|
|
||||||
const Placeholder: React.FC<{ visible: boolean }> = ({ visible }) => (
|
const Placeholder: React.FC<{ visible: boolean }> = ({ visible }) => (
|
||||||
<LinearGradient
|
<View style={{
|
||||||
colors={[colors.accent, colors.accentLow]}
|
opacity: visible ? 100 : 0,
|
||||||
style={{
|
}}>
|
||||||
height, width,
|
<PlaceholderComponent />
|
||||||
opacity: visible ? 100 : 0,
|
</View>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FastImage
|
|
||||||
source={require('../../../res/record.png')}
|
|
||||||
style={{ height, width }}
|
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
|
||||||
/>
|
|
||||||
</LinearGradient>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const CoverArt = () => (
|
const CoverArt = () => (
|
||||||
<View>
|
<>
|
||||||
<Placeholder visible={placeholderVisible} />
|
<Placeholder visible={placeholderVisible} />
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
animating={loading}
|
animating={loading}
|
||||||
@ -49,17 +41,20 @@ const AlbumCover: React.FC<{
|
|||||||
marginTop: -height - halfIndicatorHeight * 2,
|
marginTop: -height - halfIndicatorHeight * 2,
|
||||||
}}
|
}}
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
onError={() => setPlaceholderVisible(true)}
|
onError={() => {
|
||||||
|
setLoading(false);
|
||||||
|
setPlaceholderVisible(true);
|
||||||
|
}}
|
||||||
onLoadEnd={() => setLoading(false)}
|
onLoadEnd={() => setLoading(false)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ height, width }}>
|
<View style={{ height, width }}>
|
||||||
{!coverArtUri ? <Placeholder visible={placeholderVisible} /> : <CoverArt />}
|
{!coverArtUri ? <Placeholder visible={true} /> : <CoverArt />}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.memo(AlbumCover);
|
export default React.memo(CoverArt);
|
||||||
15
src/components/common/GradientScrollView.tsx
Normal file
15
src/components/common/GradientScrollView.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ScrollView, ScrollViewProps } from 'react-native';
|
||||||
|
import GradientBackground from './GradientBackground';
|
||||||
|
|
||||||
|
const GradientScrollView: React.FC<ScrollViewProps> = (props) => (
|
||||||
|
<ScrollView
|
||||||
|
overScrollMode='never'
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<GradientBackground />
|
||||||
|
{props.children}
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default GradientScrollView;
|
||||||
@ -5,7 +5,7 @@ import { Pressable, Text, View } from 'react-native';
|
|||||||
import { Album } from '../../models/music';
|
import { Album } from '../../models/music';
|
||||||
import { albumsAtom, albumsUpdatingAtom, useUpdateAlbums } from '../../state/music';
|
import { albumsAtom, albumsUpdatingAtom, useUpdateAlbums } from '../../state/music';
|
||||||
import textStyles from '../../styles/text';
|
import textStyles from '../../styles/text';
|
||||||
import AlbumCover from '../common/AlbumCover';
|
import AlbumArt from '../common/AlbumArt';
|
||||||
import GradientFlatList from '../common/GradientFlatList';
|
import GradientFlatList from '../common/GradientFlatList';
|
||||||
|
|
||||||
const AlbumItem: React.FC<{
|
const AlbumItem: React.FC<{
|
||||||
@ -27,7 +27,7 @@ const AlbumItem: React.FC<{
|
|||||||
}}
|
}}
|
||||||
onPress={() => navigation.navigate('AlbumView', { id, title: name })}
|
onPress={() => navigation.navigate('AlbumView', { id, title: name })}
|
||||||
>
|
>
|
||||||
<AlbumCover
|
<AlbumArt
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
coverArtUri={coverArtUri}
|
coverArtUri={coverArtUri}
|
||||||
|
|||||||
@ -1,30 +1,53 @@
|
|||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { useAtomValue } from 'jotai/utils';
|
import { useAtomValue } from 'jotai/utils';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { Pressable } from 'react-native';
|
||||||
import { Image, Text, View } from 'react-native';
|
import { Image, Text, View } from 'react-native';
|
||||||
import { Artist } from '../../models/music';
|
import { Artist } from '../../models/music';
|
||||||
import { artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '../../state/music';
|
import { artistInfoAtomFamily, artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '../../state/music';
|
||||||
import textStyles from '../../styles/text';
|
import textStyles from '../../styles/text';
|
||||||
|
import ArtistArt from '../common/ArtistArt';
|
||||||
import GradientFlatList from '../common/GradientFlatList';
|
import GradientFlatList from '../common/GradientFlatList';
|
||||||
|
|
||||||
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
|
const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => {
|
||||||
<View style={{
|
const navigation = useNavigation();
|
||||||
flexDirection: 'row',
|
const artistInfo = useAtomValue(artistInfoAtomFamily(item.id));
|
||||||
alignItems: 'center',
|
|
||||||
marginVertical: 6,
|
return (
|
||||||
marginLeft: 6,
|
<Pressable
|
||||||
}}>
|
|
||||||
<Image
|
|
||||||
source={item.coverArt ? { uri: 'https://reactnative.dev/img/tiny_logo.png' } : require('../../../res/mic_on-fill.png')}
|
|
||||||
style={{
|
style={{
|
||||||
width: 56,
|
flexDirection: 'row',
|
||||||
height: 56,
|
alignItems: 'center',
|
||||||
|
marginVertical: 6,
|
||||||
|
marginLeft: 6,
|
||||||
}}
|
}}
|
||||||
/>
|
onPress={() => navigation.navigate('ArtistView', { id: item.id, title: item.name })}
|
||||||
<Text style={{
|
>
|
||||||
...textStyles.paragraph,
|
<ArtistArt
|
||||||
marginLeft: 12,
|
width={56}
|
||||||
}}>{item.name}</Text>
|
height={56}
|
||||||
</View>
|
mediumImageUrl={artistInfo?.mediumImageUrl}
|
||||||
|
coverArtUris={artistInfo?.coverArtUris}
|
||||||
|
/>
|
||||||
|
{/* <Image
|
||||||
|
source={{ uri: 'https://reactnative.dev/img/tiny_logo.png' }}
|
||||||
|
style={{
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
<Text style={{
|
||||||
|
...textStyles.paragraph,
|
||||||
|
marginLeft: 12,
|
||||||
|
}}>{item.name}</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArtistItemLoader: React.FC<{ item: Artist }> = (props) => (
|
||||||
|
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||||
|
<ArtistItem { ...props } />
|
||||||
|
</React.Suspense>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ArtistsList = () => {
|
const ArtistsList = () => {
|
||||||
@ -39,7 +62,7 @@ const ArtistsList = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const renderItem: React.FC<{ item: Artist }> = ({ item }) => (
|
const renderItem: React.FC<{ item: Artist }> = ({ item }) => (
|
||||||
<ArtistItem item={item} />
|
<ArtistItemLoader item={item} />
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -10,16 +10,12 @@ import { RouteProp } from '@react-navigation/native';
|
|||||||
import text from '../../styles/text';
|
import text from '../../styles/text';
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors';
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image';
|
||||||
|
import ArtistView from '../common/ArtistView';
|
||||||
|
|
||||||
const Tab = createMaterialTopTabNavigator();
|
const Tab = createMaterialTopTabNavigator();
|
||||||
|
|
||||||
const LibraryTopTabNavigator = () => (
|
const LibraryTopTabNavigator = () => (
|
||||||
<Tab.Navigator tabBarOptions={{
|
<Tab.Navigator tabBarOptions={{
|
||||||
// scrollEnabled: true,
|
|
||||||
tabStyle: {
|
|
||||||
// width: 100,
|
|
||||||
// paddingHorizontal: 0,
|
|
||||||
},
|
|
||||||
style: {
|
style: {
|
||||||
height: 48,
|
height: 48,
|
||||||
backgroundColor: colors.gradient.high,
|
backgroundColor: colors.gradient.high,
|
||||||
@ -53,6 +49,7 @@ const LibraryTopTabNavigator = () => (
|
|||||||
type LibraryStackParamList = {
|
type LibraryStackParamList = {
|
||||||
LibraryTopTabs: undefined,
|
LibraryTopTabs: undefined,
|
||||||
AlbumView: { id: string, title: string };
|
AlbumView: { id: string, title: string };
|
||||||
|
ArtistView: { id: string, title: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlbumScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'AlbumView'>;
|
type AlbumScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'AlbumView'>;
|
||||||
@ -66,8 +63,41 @@ const AlbumScreen: React.FC<AlbumScreenProps> = ({ route }) => (
|
|||||||
<AlbumView id={route.params.id} title={route.params.title} />
|
<AlbumView id={route.params.id} title={route.params.title} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type ArtistScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'ArtistView'>;
|
||||||
|
type ArtistScreenRouteProp = RouteProp<LibraryStackParamList, 'ArtistView'>;
|
||||||
|
type ArtistScreenProps = {
|
||||||
|
route: ArtistScreenRouteProp,
|
||||||
|
navigation: ArtistScreenNavigationProp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArtistScreen: React.FC<ArtistScreenProps> = ({ route }) => (
|
||||||
|
<ArtistView id={route.params.id} title={route.params.title} />
|
||||||
|
);
|
||||||
|
|
||||||
const Stack = createStackNavigator<LibraryStackParamList>();
|
const Stack = createStackNavigator<LibraryStackParamList>();
|
||||||
|
|
||||||
|
const itemScreenOptions = {
|
||||||
|
title: '',
|
||||||
|
headerStyle: {
|
||||||
|
height: 50,
|
||||||
|
backgroundColor: colors.gradient.high,
|
||||||
|
},
|
||||||
|
headerTitleContainerStyle: {
|
||||||
|
marginLeft: -14,
|
||||||
|
},
|
||||||
|
headerLeftContainerStyle: {
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
headerTitleStyle: {
|
||||||
|
...text.header,
|
||||||
|
},
|
||||||
|
headerBackImage: () => <FastImage
|
||||||
|
source={require('../../../res/arrow_left-fill.png')}
|
||||||
|
tintColor={colors.text.primary}
|
||||||
|
style={{ height: 22, width: 22 }}
|
||||||
|
/>,
|
||||||
|
}
|
||||||
|
|
||||||
const LibraryStackNavigator = () => (
|
const LibraryStackNavigator = () => (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Stack.Navigator>
|
<Stack.Navigator>
|
||||||
@ -79,27 +109,12 @@ const LibraryStackNavigator = () => (
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='AlbumView'
|
name='AlbumView'
|
||||||
component={AlbumScreen}
|
component={AlbumScreen}
|
||||||
options={{
|
options={itemScreenOptions}
|
||||||
title: '',
|
/>
|
||||||
headerStyle: {
|
<Stack.Screen
|
||||||
height: 50,
|
name='ArtistView'
|
||||||
backgroundColor: colors.gradient.high,
|
component={ArtistScreen}
|
||||||
},
|
options={itemScreenOptions}
|
||||||
headerTitleContainerStyle: {
|
|
||||||
marginLeft: -14,
|
|
||||||
},
|
|
||||||
headerLeftContainerStyle: {
|
|
||||||
marginLeft: 8,
|
|
||||||
},
|
|
||||||
headerTitleStyle: {
|
|
||||||
...text.header,
|
|
||||||
},
|
|
||||||
headerBackImage: () => <FastImage
|
|
||||||
source={require('../../../res/arrow_left-fill.png')}
|
|
||||||
tintColor={colors.text.primary}
|
|
||||||
style={{ height: 22, width: 22 }}
|
|
||||||
/>,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ function mapSongToTrack(song: Song): Track {
|
|||||||
artist: song.artist || 'Unknown Artist',
|
artist: song.artist || 'Unknown Artist',
|
||||||
url: song.streamUri,
|
url: song.streamUri,
|
||||||
artwork: song.coverArtUri,
|
artwork: song.coverArtUri,
|
||||||
|
duration: song.duration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,14 @@ export interface Artist {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
starred?: Date;
|
starred?: Date;
|
||||||
coverArt?: string;
|
}
|
||||||
coverArtUri?: string,
|
|
||||||
|
export interface ArtistInfo extends Artist {
|
||||||
|
albums: Album[];
|
||||||
|
|
||||||
|
mediumImageUrl?: string;
|
||||||
|
largeImageUrl?: string;
|
||||||
|
coverArtUris: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Album {
|
export interface Album {
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { atom, useAtom } from 'jotai';
|
import { atom, useAtom } from 'jotai';
|
||||||
import { atomFamily, useAtomValue, useUpdateAtom } from 'jotai/utils';
|
import { atomFamily, useAtomValue, useUpdateAtom } from 'jotai/utils';
|
||||||
import { Album as Album, AlbumWithSongs, Artist, Song } from '../models/music';
|
import { Album as Album, AlbumWithSongs, Artist, ArtistInfo, Song } from '../models/music';
|
||||||
import { SubsonicApiClient } from '../subsonic/api';
|
import { SubsonicApiClient } from '../subsonic/api';
|
||||||
import { AlbumID3Element, ChildElement } from '../subsonic/elements';
|
import { AlbumID3Element, ArtistID3Element, ArtistInfo2Element, ChildElement } from '../subsonic/elements';
|
||||||
|
import { GetArtistResponse } from '../subsonic/responses';
|
||||||
import { activeServerAtom } from './settings';
|
import { activeServerAtom } from './settings';
|
||||||
|
|
||||||
export const artistsAtom = atom<Artist[]>([]);
|
export const artistsAtom = atom<Artist[]>([]);
|
||||||
@ -30,8 +31,6 @@ export const useUpdateArtists = () => {
|
|||||||
id: x.id,
|
id: x.id,
|
||||||
name: x.name,
|
name: x.name,
|
||||||
starred: x.starred,
|
starred: x.starred,
|
||||||
coverArt: x.coverArt,
|
|
||||||
coverArtUri: x.coverArt ? client.getCoverArtUri({ id: x.coverArt }) : undefined,
|
|
||||||
})));
|
})));
|
||||||
setUpdating(false);
|
setUpdating(false);
|
||||||
}
|
}
|
||||||
@ -74,6 +73,49 @@ export const albumAtomFamily = atomFamily((id: string) => atom<AlbumWithSongs |
|
|||||||
return mapAlbumID3WithSongs(response.data.album, response.data.songs, client);
|
return mapAlbumID3WithSongs(response.data.album, response.data.songs, client);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const artistInfoAtomFamily = atomFamily((id: string) => atom<ArtistInfo | undefined>(async (get) => {
|
||||||
|
const server = get(activeServerAtom);
|
||||||
|
if (!server) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new SubsonicApiClient(server);
|
||||||
|
const [artistResponse, artistInfoResponse] = await Promise.all([
|
||||||
|
client.getArtist({ id }),
|
||||||
|
client.getArtistInfo2({ id }),
|
||||||
|
]);
|
||||||
|
return mapArtistInfo(artistResponse.data, artistInfoResponse.data.artistInfo, client);
|
||||||
|
}));
|
||||||
|
|
||||||
|
function mapArtistInfo(
|
||||||
|
artistResponse: GetArtistResponse,
|
||||||
|
artistInfo: ArtistInfo2Element,
|
||||||
|
client: SubsonicApiClient
|
||||||
|
): ArtistInfo {
|
||||||
|
const info = { ...artistInfo } as any;
|
||||||
|
delete info.similarArtists;
|
||||||
|
|
||||||
|
const { artist, albums } = artistResponse
|
||||||
|
|
||||||
|
const mappedAlbums = albums.map(a => mapAlbumID3(a, client));
|
||||||
|
const coverArtUris = mappedAlbums
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.year && b.year) {
|
||||||
|
return a.year - b.year;
|
||||||
|
} else {
|
||||||
|
return a.name.localeCompare(b.name) - 9000;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(a => a.coverArtThumbUri);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...artist,
|
||||||
|
...info,
|
||||||
|
albums: mappedAlbums,
|
||||||
|
coverArtUris,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
|
function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
|
||||||
return {
|
return {
|
||||||
...album,
|
...album,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { DOMParser } from 'xmldom';
|
import { DOMParser } from 'xmldom';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
import { GetAlbumList2Params, GetAlbumListParams, GetAlbumParams, GetArtistInfo2Params, GetArtistInfoParams, GetCoverArtParams, GetIndexesParams, GetMusicDirectoryParams, StreamParams } from './params';
|
import { GetAlbumList2Params, GetAlbumListParams, GetAlbumParams, GetArtistInfo2Params, GetArtistInfoParams, GetArtistParams, GetCoverArtParams, GetIndexesParams, GetMusicDirectoryParams, StreamParams } from './params';
|
||||||
import { GetAlbumList2Response, GetAlbumListResponse, GetAlbumResponse, GetArtistInfo2Response, GetArtistInfoResponse, GetArtistsResponse, GetIndexesResponse, GetMusicDirectoryResponse, SubsonicResponse } from './responses';
|
import { GetAlbumList2Response, GetAlbumListResponse, GetAlbumResponse, GetArtistInfo2Response, GetArtistInfoResponse, GetArtistResponse, GetArtistsResponse, GetIndexesResponse, GetMusicDirectoryResponse, SubsonicResponse } from './responses';
|
||||||
import { Server } from '../models/settings';
|
import { Server } from '../models/settings';
|
||||||
import paths from '../paths';
|
import paths from '../paths';
|
||||||
|
|
||||||
@ -167,6 +167,11 @@ export class SubsonicApiClient {
|
|||||||
return new SubsonicResponse<GetArtistInfo2Response>(xml, new GetArtistInfo2Response(xml));
|
return new SubsonicResponse<GetArtistInfo2Response>(xml, new GetArtistInfo2Response(xml));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getArtist(params: GetArtistParams): Promise<SubsonicResponse<GetArtistResponse>> {
|
||||||
|
const xml = await this.apiGetXml('getArtist', params);
|
||||||
|
return new SubsonicResponse<GetArtistResponse>(xml, new GetArtistResponse(xml));
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Album/song lists
|
// Album/song lists
|
||||||
//
|
//
|
||||||
|
|||||||
@ -72,6 +72,53 @@ export class ArtistElement extends BaseArtistElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class BaseArtistInfoElement<T> {
|
||||||
|
similarArtists: T[] = [];
|
||||||
|
biography?: string;
|
||||||
|
musicBrainzId?: string;
|
||||||
|
lastFmUrl?: string;
|
||||||
|
smallImageUrl?: string;
|
||||||
|
mediumImageUrl?: string;
|
||||||
|
largeImageUrl?: string;
|
||||||
|
|
||||||
|
constructor(e: Element, artistType: new (e: Element) => T) {
|
||||||
|
if (e.getElementsByTagName('biography').length > 0) {
|
||||||
|
this.biography = e.getElementsByTagName('biography')[0].textContent as string;
|
||||||
|
}
|
||||||
|
if (e.getElementsByTagName('musicBrainzId').length > 0) {
|
||||||
|
this.musicBrainzId = e.getElementsByTagName('musicBrainzId')[0].textContent as string;
|
||||||
|
}
|
||||||
|
if (e.getElementsByTagName('lastFmUrl').length > 0) {
|
||||||
|
this.lastFmUrl = e.getElementsByTagName('lastFmUrl')[0].textContent as string;
|
||||||
|
}
|
||||||
|
if (e.getElementsByTagName('smallImageUrl').length > 0) {
|
||||||
|
this.smallImageUrl = e.getElementsByTagName('smallImageUrl')[0].textContent as string;
|
||||||
|
}
|
||||||
|
if (e.getElementsByTagName('mediumImageUrl').length > 0) {
|
||||||
|
this.mediumImageUrl = e.getElementsByTagName('mediumImageUrl')[0].textContent as string;
|
||||||
|
}
|
||||||
|
if (e.getElementsByTagName('largeImageUrl').length > 0) {
|
||||||
|
this.largeImageUrl = e.getElementsByTagName('largeImageUrl')[0].textContent as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const similarArtistElements = e.getElementsByTagName('similarArtist');
|
||||||
|
for (let i = 0; i < similarArtistElements.length; i++) {
|
||||||
|
this.similarArtists.push(new artistType(similarArtistElements[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ArtistInfoElement extends BaseArtistInfoElement<ArtistElement> {
|
||||||
|
constructor(e: Element) {
|
||||||
|
super(e, ArtistElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class ArtistInfo2Element extends BaseArtistInfoElement<ArtistID3Element> {
|
||||||
|
constructor(e: Element) {
|
||||||
|
super(e, ArtistID3Element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class DirectoryElement {
|
export class DirectoryElement {
|
||||||
id: string;
|
id: string;
|
||||||
parent?: string;
|
parent?: string;
|
||||||
|
|||||||
@ -23,6 +23,10 @@ export type GetAlbumParams = {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GetArtistParams = {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Album/song lists
|
// Album/song lists
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { AlbumID3Element, ArtistElement, ArtistID3Element, BaseArtistElement, ChildElement, DirectoryElement } from "./elements";
|
import { AlbumID3Element, ArtistElement, ArtistID3Element, ArtistInfo2Element, ArtistInfoElement, BaseArtistElement, BaseArtistInfoElement, ChildElement, DirectoryElement } from "./elements";
|
||||||
|
|
||||||
export type ResponseStatus = 'ok' | 'failed';
|
export type ResponseStatus = 'ok' | 'failed';
|
||||||
|
|
||||||
@ -32,6 +32,20 @@ export class GetArtistsResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class GetArtistResponse {
|
||||||
|
artist: ArtistID3Element;
|
||||||
|
albums: AlbumID3Element[] = [];
|
||||||
|
|
||||||
|
constructor(xml: Document) {
|
||||||
|
this.artist = new ArtistID3Element(xml.getElementsByTagName('artist')[0]);
|
||||||
|
|
||||||
|
const albumElements = xml.getElementsByTagName('album');
|
||||||
|
for (let i = 0; i < albumElements.length; i++) {
|
||||||
|
this.albums.push(new AlbumID3Element(albumElements[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class GetIndexesResponse {
|
export class GetIndexesResponse {
|
||||||
ignoredArticles: string;
|
ignoredArticles: string;
|
||||||
lastModified: number;
|
lastModified: number;
|
||||||
@ -50,51 +64,19 @@ export class GetIndexesResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BaseGetArtistInfoResponse<T extends BaseArtistElement> {
|
export class GetArtistInfoResponse {
|
||||||
similarArtists: T[] = [];
|
artistInfo: ArtistInfoElement;
|
||||||
biography?: string;
|
|
||||||
musicBrainzId?: string;
|
|
||||||
lastFmUrl?: string;
|
|
||||||
smallImageUrl?: string;
|
|
||||||
mediumImageUrl?: string;
|
|
||||||
largeImageUrl?: string;
|
|
||||||
|
|
||||||
constructor(xml: Document, artistType: new (e: Element) => T) {
|
constructor(xml: Document) {
|
||||||
if (xml.getElementsByTagName('biography').length > 0) {
|
this.artistInfo = new ArtistInfoElement(xml.getElementsByTagName('artistInfo')[0]);
|
||||||
this.biography = xml.getElementsByTagName('biography')[0].textContent as string;
|
|
||||||
}
|
|
||||||
if (xml.getElementsByTagName('musicBrainzId').length > 0) {
|
|
||||||
this.musicBrainzId = xml.getElementsByTagName('musicBrainzId')[0].textContent as string;
|
|
||||||
}
|
|
||||||
if (xml.getElementsByTagName('lastFmUrl').length > 0) {
|
|
||||||
this.lastFmUrl = xml.getElementsByTagName('lastFmUrl')[0].textContent as string;
|
|
||||||
}
|
|
||||||
if (xml.getElementsByTagName('smallImageUrl').length > 0) {
|
|
||||||
this.smallImageUrl = xml.getElementsByTagName('smallImageUrl')[0].textContent as string;
|
|
||||||
}
|
|
||||||
if (xml.getElementsByTagName('mediumImageUrl').length > 0) {
|
|
||||||
this.mediumImageUrl = xml.getElementsByTagName('mediumImageUrl')[0].textContent as string;
|
|
||||||
}
|
|
||||||
if (xml.getElementsByTagName('largeImageUrl').length > 0) {
|
|
||||||
this.largeImageUrl = xml.getElementsByTagName('largeImageUrl')[0].textContent as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const similarArtistElements = xml.getElementsByTagName('similarArtist');
|
|
||||||
for (let i = 0; i < similarArtistElements.length; i++) {
|
|
||||||
this.similarArtists.push(new artistType(similarArtistElements[i]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GetArtistInfoResponse extends BaseGetArtistInfoResponse<ArtistElement> {
|
export class GetArtistInfo2Response {
|
||||||
constructor(xml: Document) {
|
artistInfo: ArtistInfo2Element;
|
||||||
super(xml, ArtistElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GetArtistInfo2Response extends BaseGetArtistInfoResponse<ArtistID3Element> {
|
|
||||||
constructor(xml: Document) {
|
constructor(xml: Document) {
|
||||||
super(xml, ArtistID3Element);
|
this.artistInfo = new ArtistInfo2Element(xml.getElementsByTagName('artistInfo2')[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
src/util.ts
Normal file
11
src/util.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export function formatDuration(seconds: number): string {
|
||||||
|
const s = seconds % 60;
|
||||||
|
const m = Math.floor(seconds / 60) % 60;
|
||||||
|
const h = Math.floor(seconds / 60 / 60);
|
||||||
|
|
||||||
|
let time = `${m.toString().padStart(1, '0')}:${s.toString().padStart(2, '0')}`;
|
||||||
|
if (h > 0) {
|
||||||
|
time = `${h}:${time}`;
|
||||||
|
}
|
||||||
|
return time;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user