mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 09:29:29 +01:00
song list for album view
adjusted placeholder to only show when no cover art
This commit is contained in:
parent
f9f016b932
commit
666e1e3e69
BIN
res/more_vertical.png
Normal file
BIN
res/more_vertical.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
res/record.png
Normal file
BIN
res/record.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
res/star-fill.png
Normal file
BIN
res/star-fill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
BIN
res/star.png
Normal file
BIN
res/star.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
65
src/components/common/AlbumCover.tsx
Normal file
65
src/components/common/AlbumCover.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { ActivityIndicator, View } from 'react-native';
|
||||||
|
import FastImage from 'react-native-fast-image';
|
||||||
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
|
import colors from '../../styles/colors';
|
||||||
|
|
||||||
|
const AlbumCover: React.FC<{
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
coverArtUri?: string
|
||||||
|
}> = ({ height, width, coverArtUri }) => {
|
||||||
|
const [placeholderVisible, setPlaceholderVisible] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const indicatorSize = height > 130 ? 'large' : 'small';
|
||||||
|
const halfIndicatorHeight = indicatorSize === 'large' ? 18 : 10;
|
||||||
|
|
||||||
|
const Placeholder: React.FC<{ visible: boolean }> = ({ visible }) => (
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.accent, colors.accentLow]}
|
||||||
|
style={{
|
||||||
|
height, width,
|
||||||
|
opacity: visible ? 100 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FastImage
|
||||||
|
source={require('../../../res/record.png')}
|
||||||
|
style={{ height, width }}
|
||||||
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
|
/>
|
||||||
|
</LinearGradient>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CoverArt = () => (
|
||||||
|
<View>
|
||||||
|
<Placeholder visible={placeholderVisible} />
|
||||||
|
<ActivityIndicator
|
||||||
|
animating={loading}
|
||||||
|
size={indicatorSize}
|
||||||
|
color={colors.accent}
|
||||||
|
style={{
|
||||||
|
top: -height / 2 - halfIndicatorHeight,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FastImage
|
||||||
|
source={{ uri: coverArtUri, priority: 'high' }}
|
||||||
|
style={{
|
||||||
|
height, width,
|
||||||
|
marginTop: -height - halfIndicatorHeight * 2,
|
||||||
|
}}
|
||||||
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
|
onError={() => setPlaceholderVisible(true)}
|
||||||
|
onLoadEnd={() => setLoading(false)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ height, width }}>
|
||||||
|
{!coverArtUri ? <Placeholder visible={placeholderVisible} /> : <CoverArt />}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(AlbumCover);
|
||||||
@ -1,40 +1,172 @@
|
|||||||
import { useNavigation } from '@react-navigation/native';
|
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 { View, Text } from 'react-native';
|
import { ScrollView, Text, useWindowDimensions, View, Image, Pressable, GestureResponderEvent } from 'react-native';
|
||||||
import { albumAtomFamily } from '../../state/music';
|
import { albumAtomFamily } from '../../state/music';
|
||||||
|
import colors from '../../styles/colors';
|
||||||
|
import text from '../../styles/text';
|
||||||
|
import AlbumCover from './AlbumCover';
|
||||||
import TopTabContainer from './TopTabContainer';
|
import TopTabContainer from './TopTabContainer';
|
||||||
|
|
||||||
|
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 }) => {
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={onPress}
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.accent,
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
minHeight: 42,
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 1000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ ...text.button }}>{title}</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const AlbumDetails: React.FC<{
|
const AlbumDetails: React.FC<{
|
||||||
id: string,
|
id: string,
|
||||||
}> = ({ id }) => {
|
}> = ({ id }) => {
|
||||||
const navigation = useNavigation();
|
|
||||||
const album = useAtomValue(albumAtomFamily(id));
|
const album = useAtomValue(albumAtomFamily(id));
|
||||||
|
const layout = useWindowDimensions();
|
||||||
|
|
||||||
useEffect(() => {
|
const coverSize = layout.width - layout.width / 2;
|
||||||
if (!album) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigation.setOptions({ title: album.name });
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ScrollView
|
||||||
<Text>Name: {album?.name}</Text>
|
style={{
|
||||||
<Text>Artist: {album?.artist}</Text>
|
flex: 1,
|
||||||
</>
|
}}
|
||||||
|
contentContainerStyle={{
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: coverSize / 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlbumCover
|
||||||
|
height={coverSize}
|
||||||
|
width={coverSize}
|
||||||
|
coverArtUri={album?.coverArtUri}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={{
|
||||||
|
...text.title,
|
||||||
|
marginTop: 12,
|
||||||
|
width: layout.width - layout.width / 8,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>{album?.name}</Text>
|
||||||
|
|
||||||
|
<Text style={{
|
||||||
|
...text.itemSubtitle,
|
||||||
|
fontSize: 14,
|
||||||
|
marginTop: 4,
|
||||||
|
marginBottom: 20,
|
||||||
|
width: layout.width - layout.width / 8,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>{album?.artist}{album?.year ? ` • ${album.year}` : ''}</Text>
|
||||||
|
|
||||||
|
<View style={{
|
||||||
|
flexDirection: 'row'
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
title='Play Album'
|
||||||
|
onPress={() => null}
|
||||||
|
/>
|
||||||
|
{/* <View style={{ width: 6, }}></View>
|
||||||
|
<Button
|
||||||
|
title='S'
|
||||||
|
onPress={() => null}
|
||||||
|
/> */}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={{
|
||||||
|
width: layout.width - (layout.width / 20),
|
||||||
|
marginTop: 20,
|
||||||
|
marginBottom: 30,
|
||||||
|
}}>
|
||||||
|
{album?.songs
|
||||||
|
.sort((a, b) => (a.track as number) - (b.track as number))
|
||||||
|
.map(s => (
|
||||||
|
<View key={s.id} style={{
|
||||||
|
marginTop: 20,
|
||||||
|
marginLeft: 4,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<View style={{
|
||||||
|
flexShrink: 1,
|
||||||
|
}}>
|
||||||
|
<Text style={text.songListTitle}>{s.title}</Text>
|
||||||
|
<Text style={text.songListSubtitle}>{s.artist}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: 10,
|
||||||
|
}}>
|
||||||
|
{/* <Text style={text.songListSubtitle}>{secondsToTime(s.duration || 0)}</Text> */}
|
||||||
|
<Image
|
||||||
|
source={require('../../../res/star.png')}
|
||||||
|
style={{
|
||||||
|
height: 28,
|
||||||
|
width: 28,
|
||||||
|
tintColor: colors.text.secondary,
|
||||||
|
marginLeft: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
source={require('../../../res/more_vertical.png')}
|
||||||
|
style={{
|
||||||
|
height: 28,
|
||||||
|
width: 28,
|
||||||
|
tintColor: colors.text.secondary,
|
||||||
|
marginLeft: 12,
|
||||||
|
marginRight: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const AlbumView: React.FC<{
|
const AlbumView: React.FC<{
|
||||||
id: string,
|
id: string,
|
||||||
}> = ({ id }) => (
|
title: string;
|
||||||
<TopTabContainer>
|
}> = ({ id, title }) => {
|
||||||
<Text>{id}</Text>
|
const navigation = useNavigation();
|
||||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
|
||||||
<AlbumDetails id={id} />
|
useEffect(() => {
|
||||||
</React.Suspense>
|
navigation.setOptions({ title });
|
||||||
</TopTabContainer>
|
});
|
||||||
);
|
|
||||||
|
return (
|
||||||
|
<TopTabContainer>
|
||||||
|
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||||
|
<AlbumDetails id={id} />
|
||||||
|
</React.Suspense>
|
||||||
|
</TopTabContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default React.memo(AlbumView);
|
export default React.memo(AlbumView);
|
||||||
|
|||||||
@ -6,42 +6,10 @@ import FastImage from 'react-native-fast-image';
|
|||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
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 colors from '../../styles/colors';
|
|
||||||
import textStyles from '../../styles/text';
|
import textStyles from '../../styles/text';
|
||||||
|
import AlbumCover from '../common/AlbumCover';
|
||||||
import TopTabContainer from '../common/TopTabContainer';
|
import TopTabContainer from '../common/TopTabContainer';
|
||||||
|
|
||||||
const AlbumArt: React.FC<{
|
|
||||||
height: number,
|
|
||||||
width: number,
|
|
||||||
coverArtUri?: string
|
|
||||||
}> = ({ height, width, coverArtUri }) => {
|
|
||||||
const Placeholder = (
|
|
||||||
<LinearGradient
|
|
||||||
colors={[colors.accent, colors.accentLow]}
|
|
||||||
style={{ height, width }}
|
|
||||||
>
|
|
||||||
<FastImage
|
|
||||||
source={require('../../../res/record-m.png')}
|
|
||||||
style={{ height, width }}
|
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
|
||||||
/>
|
|
||||||
</LinearGradient>
|
|
||||||
);
|
|
||||||
|
|
||||||
const CoverArt = (
|
|
||||||
<View style={{ height, width }}>
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: coverArtUri }}
|
|
||||||
style={{ height, width }}
|
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
return coverArtUri ? CoverArt : Placeholder;
|
|
||||||
}
|
|
||||||
const MemoAlbumArt = React.memo(AlbumArt);
|
|
||||||
|
|
||||||
const AlbumItem: React.FC<{
|
const AlbumItem: React.FC<{
|
||||||
id: string;
|
id: string;
|
||||||
name: string,
|
name: string,
|
||||||
@ -59,9 +27,9 @@ const AlbumItem: React.FC<{
|
|||||||
marginVertical: 8,
|
marginVertical: 8,
|
||||||
flex: 1/3,
|
flex: 1/3,
|
||||||
}}
|
}}
|
||||||
onPress={() => navigation.navigate('AlbumView', { id })}
|
onPress={() => navigation.navigate('AlbumView', { id, title: name })}
|
||||||
>
|
>
|
||||||
<MemoAlbumArt
|
<AlbumCover
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
coverArtUri={coverArtUri}
|
coverArtUri={coverArtUri}
|
||||||
|
|||||||
@ -52,7 +52,7 @@ const LibraryTopTabNavigator = () => (
|
|||||||
|
|
||||||
type LibraryStackParamList = {
|
type LibraryStackParamList = {
|
||||||
LibraryTopTabs: undefined,
|
LibraryTopTabs: undefined,
|
||||||
AlbumView: { id: string };
|
AlbumView: { id: string, title: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlbumScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'AlbumView'>;
|
type AlbumScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'AlbumView'>;
|
||||||
@ -63,7 +63,7 @@ type AlbumScreenProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AlbumScreen: React.FC<AlbumScreenProps> = ({ route }) => (
|
const AlbumScreen: React.FC<AlbumScreenProps> = ({ route }) => (
|
||||||
<AlbumView id={route.params.id} />
|
<AlbumView id={route.params.id} title={route.params.title} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const Stack = createStackNavigator<LibraryStackParamList>();
|
const Stack = createStackNavigator<LibraryStackParamList>();
|
||||||
@ -79,7 +79,8 @@ const LibraryStackNavigator = () => (
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='AlbumView'
|
name='AlbumView'
|
||||||
component={AlbumScreen}
|
component={AlbumScreen}
|
||||||
options={{ title: 'Album',
|
options={{
|
||||||
|
title: '',
|
||||||
headerStyle: {
|
headerStyle: {
|
||||||
height: 50,
|
height: 50,
|
||||||
backgroundColor: colors.gradient.high,
|
backgroundColor: colors.gradient.high,
|
||||||
|
|||||||
@ -15,13 +15,32 @@ export interface Album {
|
|||||||
coverArt?: string;
|
coverArt?: string;
|
||||||
coverArtUri?: string,
|
coverArtUri?: string,
|
||||||
coverArtThumbUri?: string,
|
coverArtThumbUri?: string,
|
||||||
|
year?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlbumWithSongs extends Album {
|
||||||
|
songs: Song[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Song {
|
export interface Song {
|
||||||
id: string;
|
id: string;
|
||||||
albumId: string;
|
album?: string;
|
||||||
artistId: string;
|
artist?: string;
|
||||||
name: string;
|
title: string;
|
||||||
|
track?: number;
|
||||||
|
year?: number;
|
||||||
|
genre?: string;
|
||||||
|
coverArt?: string;
|
||||||
|
size?: number;
|
||||||
|
contentType?: string;
|
||||||
|
suffix?: string;
|
||||||
|
duration?: number;
|
||||||
|
bitRate?: number;
|
||||||
|
userRating?: number;
|
||||||
|
averageRating?: number;
|
||||||
|
playCount?: number;
|
||||||
|
discNumber?: number;
|
||||||
|
created?: Date;
|
||||||
starred?: Date;
|
starred?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
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, Artist } from '../models/music';
|
import { Album as Album, AlbumWithSongs, Artist, Song } from '../models/music';
|
||||||
import { SubsonicApiClient } from '../subsonic/api';
|
import { SubsonicApiClient } from '../subsonic/api';
|
||||||
|
import { AlbumID3Element, ChildElement } from '../subsonic/elements';
|
||||||
import { activeServerAtom } from './settings';
|
import { activeServerAtom } from './settings';
|
||||||
|
|
||||||
export const artistsAtom = atom<Artist[]>([]);
|
export const artistsAtom = atom<Artist[]>([]);
|
||||||
@ -57,21 +58,12 @@ export const useUpdateAlbums = () => {
|
|||||||
const client = new SubsonicApiClient(server);
|
const client = new SubsonicApiClient(server);
|
||||||
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 });
|
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 });
|
||||||
|
|
||||||
setAlbums(response.data.albums.map(x => ({
|
setAlbums(response.data.albums.map(a => mapAlbumID3(a, client)));
|
||||||
id: x.id,
|
|
||||||
artistId: x.artistId,
|
|
||||||
artist: x.artist,
|
|
||||||
name: x.name,
|
|
||||||
starred: x.starred,
|
|
||||||
coverArt: x.coverArt,
|
|
||||||
coverArtUri: x.coverArt ? client.getCoverArtUri({ id: x.coverArt }) : undefined,
|
|
||||||
coverArtThumbUri: x.coverArt ? client.getCoverArtUri({ id: x.coverArt, size: '128' }) : undefined,
|
|
||||||
})));
|
|
||||||
setUpdating(false);
|
setUpdating(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const albumAtomFamily = atomFamily((id: string) => atom<Album | undefined>(async (get) => {
|
export const albumAtomFamily = atomFamily((id: string) => atom<AlbumWithSongs | undefined>(async (get) => {
|
||||||
const server = get(activeServerAtom);
|
const server = get(activeServerAtom);
|
||||||
if (!server) {
|
if (!server) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -79,10 +71,28 @@ export const albumAtomFamily = atomFamily((id: string) => atom<Album | undefined
|
|||||||
|
|
||||||
const client = new SubsonicApiClient(server);
|
const client = new SubsonicApiClient(server);
|
||||||
const response = await client.getAlbum({ id });
|
const response = await client.getAlbum({ id });
|
||||||
|
return mapAlbumID3WithSongs(response.data.album, response.data.songs, client);
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name: response.data.album.name,
|
|
||||||
artist: response.data.album.artist,
|
|
||||||
};
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
|
||||||
|
return {
|
||||||
|
...album,
|
||||||
|
coverArtUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt }) : undefined,
|
||||||
|
coverArtThumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapChild(child: ChildElement): Song {
|
||||||
|
return { ...child }
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapAlbumID3WithSongs(
|
||||||
|
album: AlbumID3Element,
|
||||||
|
songs: ChildElement[],
|
||||||
|
client: SubsonicApiClient
|
||||||
|
): AlbumWithSongs {
|
||||||
|
return {
|
||||||
|
...mapAlbumID3(album, client),
|
||||||
|
songs: songs.map(s => mapChild(s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { TextStyle } from "react-native";
|
import { TextStyle } from 'react-native';
|
||||||
import colors from './colors';
|
import colors from './colors';
|
||||||
|
|
||||||
const fontRegular = 'Metropolis-Regular';
|
const fontRegular = 'Metropolis-Regular';
|
||||||
const fontSemiBold = 'Metropolis-SemiBold';
|
const fontSemiBold = 'Metropolis-SemiBold';
|
||||||
|
const fontBold = 'Metropolis-Bold';
|
||||||
|
|
||||||
const paragraph: TextStyle = {
|
const paragraph: TextStyle = {
|
||||||
fontFamily: fontRegular,
|
fontFamily: fontRegular,
|
||||||
@ -16,6 +17,12 @@ const header: TextStyle = {
|
|||||||
fontFamily: fontSemiBold,
|
fontFamily: fontSemiBold,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const title: TextStyle = {
|
||||||
|
...paragraph,
|
||||||
|
fontSize: 24,
|
||||||
|
fontFamily: fontBold,
|
||||||
|
};
|
||||||
|
|
||||||
const itemTitle: TextStyle = {
|
const itemTitle: TextStyle = {
|
||||||
...paragraph,
|
...paragraph,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@ -28,15 +35,37 @@ const itemSubtitle: TextStyle = {
|
|||||||
color: colors.text.secondary,
|
color: colors.text.secondary,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const songListTitle: TextStyle = {
|
||||||
|
...paragraph,
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: fontSemiBold,
|
||||||
|
};
|
||||||
|
|
||||||
|
const songListSubtitle: TextStyle = {
|
||||||
|
...paragraph,
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.text.secondary,
|
||||||
|
};
|
||||||
|
|
||||||
const xsmall: TextStyle = {
|
const xsmall: TextStyle = {
|
||||||
...paragraph,
|
...paragraph,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const button: TextStyle = {
|
||||||
|
...paragraph,
|
||||||
|
fontSize: 15,
|
||||||
|
fontFamily: fontBold,
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
paragraph,
|
paragraph,
|
||||||
header,
|
header,
|
||||||
|
title,
|
||||||
itemTitle,
|
itemTitle,
|
||||||
itemSubtitle,
|
itemSubtitle,
|
||||||
xsmall
|
songListTitle,
|
||||||
|
songListSubtitle,
|
||||||
|
xsmall,
|
||||||
|
button,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user