eslint/prettier

This commit is contained in:
austinried 2021-07-01 11:27:27 +09:00
parent ee7658ccf8
commit 4e98318cd9
44 changed files with 649 additions and 664 deletions

View File

@ -1,4 +1,8 @@
module.exports = { module.exports = {
root: true, root: true,
extends: '@react-native-community', extends: '@react-native-community',
rules: {
'react-native/no-inline-styles': 0,
radix: 0,
},
}; };

View File

@ -1,7 +1,8 @@
module.exports = { module.exports = {
bracketSpacing: false, bracketSpacing: true,
jsxBracketSameLine: true, jsxBracketSameLine: true,
singleQuote: true, singleQuote: true,
trailingComma: 'all', trailingComma: 'all',
arrowParens: 'avoid', arrowParens: 'avoid',
printWidth: 120,
}; };

View File

@ -1 +1 @@
{} {}

View File

@ -1,4 +1,4 @@
{ {
"name": "SubSonify", "name": "SubSonify",
"displayName": "SubSonify" "displayName": "SubSonify"
} }

View File

@ -1,38 +1,38 @@
{ {
"images" : [ "images": [
{ {
"idiom" : "iphone", "idiom": "iphone",
"size" : "29x29", "size": "29x29",
"scale" : "2x" "scale": "2x"
}, },
{ {
"idiom" : "iphone", "idiom": "iphone",
"size" : "29x29", "size": "29x29",
"scale" : "3x" "scale": "3x"
}, },
{ {
"idiom" : "iphone", "idiom": "iphone",
"size" : "40x40", "size": "40x40",
"scale" : "2x" "scale": "2x"
}, },
{ {
"idiom" : "iphone", "idiom": "iphone",
"size" : "40x40", "size": "40x40",
"scale" : "3x" "scale": "3x"
}, },
{ {
"idiom" : "iphone", "idiom": "iphone",
"size" : "60x60", "size": "60x60",
"scale" : "2x" "scale": "2x"
}, },
{ {
"idiom" : "iphone", "idiom": "iphone",
"size" : "60x60", "size": "60x60",
"scale" : "3x" "scale": "3x"
} }
], ],
"info" : { "info": {
"version" : 1, "version": 1,
"author" : "xcode" "author": "xcode"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"info" : { "info": {
"version" : 1, "version": 1,
"author" : "xcode" "author": "xcode"
} }
} }

View File

@ -3,5 +3,5 @@ module.exports = {
ios: {}, ios: {},
android: {}, android: {},
}, },
assets: ['./assets/fonts'] assets: ['./assets/fonts'],
}; };

View File

@ -4,31 +4,26 @@ import { useAtomValue } from 'jotai/utils';
import { Artist } from '../models/music'; import { Artist } from '../models/music';
import { artistsAtom } from '../state/music'; import { artistsAtom } from '../state/music';
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => ( const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => (
<View> <View>
<Text>{item.id}</Text> <Text>{item.id}</Text>
<Text style={{ <Text
fontSize: 60, style={{
paddingBottom: 400, fontSize: 60,
}}>{item.name}</Text> paddingBottom: 400,
}}>
{item.name}
</Text>
</View> </View>
); );
const List = () => { const List = () => {
const artists = useAtomValue(artistsAtom); const artists = useAtomValue(artistsAtom);
const renderItem: React.FC<{ item: Artist }> = ({ item }) => ( const renderItem: React.FC<{ item: Artist }> = ({ item }) => <ArtistItem item={item} />;
<ArtistItem item={item} />
);
return ( return <FlatList data={artists} renderItem={renderItem} keyExtractor={item => item.id} />;
<FlatList };
data={artists}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
);
}
const ArtistsList = () => ( const ArtistsList = () => (
<View> <View>
@ -36,6 +31,6 @@ const ArtistsList = () => (
<List /> <List />
</React.Suspense> </React.Suspense>
</View> </View>
) );
export default ArtistsList; export default ArtistsList;

View File

@ -3,21 +3,21 @@ import { Image, ImageSourcePropType } from 'react-native';
import colors from '../styles/colors'; import colors from '../styles/colors';
export type FocusableIconProps = { export type FocusableIconProps = {
focused: boolean, focused: boolean;
source: ImageSourcePropType; source: ImageSourcePropType;
focusedSource?: ImageSourcePropType; focusedSource?: ImageSourcePropType;
width?: number; width?: number;
height?: number; height?: number;
}; };
const FocusableIcon: React.FC<FocusableIconProps> = (props) => { const FocusableIcon: React.FC<FocusableIconProps> = props => {
props.focusedSource = props.focusedSource || props.source; props.focusedSource = props.focusedSource || props.source;
props.width = props.width || 26; props.width = props.width || 26;
props.height = props.height || 26; props.height = props.height || 26;
return ( return (
<Image <Image
style={{ style={{
height: props.height, height: props.height,
width: props.width, width: props.width,
tintColor: props.focused ? colors.text.primary : colors.text.secondary, tintColor: props.focused ? colors.text.primary : colors.text.secondary,
@ -25,6 +25,6 @@ const FocusableIcon: React.FC<FocusableIconProps> = (props) => {
source={props.focused ? props.focusedSource : props.source} source={props.focused ? props.focusedSource : props.source}
/> />
); );
} };
export default FocusableIcon; export default FocusableIcon;

View File

@ -1,96 +1,103 @@
import React from 'react'; import React from 'react';
import { Image, StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
const NowPlayingLayout = () => { const NowPlayingLayout = () => {
return ( return (
<View style={{ // background <View
backgroundColor: 'darkblue', style={{
flex: 1, // background
}}> backgroundColor: 'darkblue',
flex: 1,
{/* top bar */}
<View style={{
height: 70,
flexDirection: 'row',
}}> }}>
<View style={{ width: 70, height: 70, backgroundColor: 'grey' }}></View> {/* top bar */}
<View style={{ flex: 1, alignItems: 'center', height: 70, }}> <View
<View style={{ flex: 1 }}></View> style={{
height: 70,
flexDirection: 'row',
}}>
<View style={{ width: 70, height: 70, backgroundColor: 'grey' }} />
<View style={{ flex: 1, alignItems: 'center', height: 70 }}>
<View style={{ flex: 1 }} />
<Text style={styles.text}>Playing from Your Library</Text> <Text style={styles.text}>Playing from Your Library</Text>
<Text style={styles.text}>Songs</Text> <Text style={styles.text}>Songs</Text>
<View style={{ flex: 1 }}></View> <View style={{ flex: 1 }} />
</View> </View>
<View style={{ width: 70, height: 70, backgroundColor: 'grey' }}></View> <View style={{ width: 70, height: 70, backgroundColor: 'grey' }} />
</View> </View>
{/* album art */} {/* album art */}
<View style={{ <View
flex: 5, style={{
// backgroundColor: 'darkorange', flex: 5,
alignItems: 'center', // backgroundColor: 'darkorange',
}}> alignItems: 'center',
<View style={{ flex: 1 }}></View> }}>
<View style={{ <View style={{ flex: 1 }} />
width: 320, <View
height: 320, style={{
backgroundColor: 'grey', width: 320,
}}></View> height: 320,
<View style={{ flex: 1 }}></View> backgroundColor: 'grey',
}}
/>
<View style={{ flex: 1 }} />
</View> </View>
{/* song/album/artist title */} {/* song/album/artist title */}
<View style={{ <View
flex: 1, style={{
// backgroundColor: 'green', flex: 1,
alignItems: 'center', // backgroundColor: 'green',
}}> alignItems: 'center',
<Text style={{ ...styles.text, fontSize: 26, }}>Name of the Song</Text> }}>
<Text style={{ ...styles.text, fontSize: 26 }}>Name of the Song</Text>
<Text style={{ ...styles.text, fontSize: 20, fontWeight: 'normal' }}>Cool Artist</Text> <Text style={{ ...styles.text, fontSize: 20, fontWeight: 'normal' }}>Cool Artist</Text>
</View> </View>
{/* seek bar */} {/* seek bar */}
<View style={{ <View
flex: 0.7, style={{
// backgroundColor: 'red', flex: 0.7,
flexDirection: 'row', // backgroundColor: 'red',
alignItems: 'center' flexDirection: 'row',
}}> alignItems: 'center',
<View style={{ width: 20 }}></View>
<View style={{
flex: 1,
}}> }}>
<View style={{ width: 20 }} />
<View
style={{
flex: 1,
}}>
<View> <View>
<View style={{ <View
backgroundColor: 'grey', style={{
height: 3, backgroundColor: 'grey',
marginBottom: 3, height: 3,
// flex: 1, marginBottom: 3,
}} /> // flex: 1,
<View style={{ }}
flexDirection: 'row' />
}}> <View
style={{
flexDirection: 'row',
}}>
<Text style={{ ...styles.text, fontWeight: 'normal' }}>00:00</Text> <Text style={{ ...styles.text, fontWeight: 'normal' }}>00:00</Text>
<View style={{ flex: 1 }} /> <View style={{ flex: 1 }} />
<Text style={{ ...styles.text, fontWeight: 'normal' }}>00:00</Text> <Text style={{ ...styles.text, fontWeight: 'normal' }}>00:00</Text>
</View> </View>
</View> </View>
</View> </View>
<View style={{ width: 20 }}></View> <View style={{ width: 20 }} />
</View> </View>
{/* main player controls */} {/* main player controls */}
<View style={{ <View
height: 90, style={{
// backgroundColor: 'darkorange', height: 90,
flexDirection: 'row', // backgroundColor: 'darkorange',
alignItems: 'center', flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'center',
}}> justifyContent: 'space-between',
}}>
<View style={{ width: 14 }} /> <View style={{ width: 14 }} />
<View style={{ width: 60, height: 60, backgroundColor: 'grey' }} /> <View style={{ width: 60, height: 60, backgroundColor: 'grey' }} />
<View style={{ width: 60, height: 60, backgroundColor: 'grey' }} /> <View style={{ width: 60, height: 60, backgroundColor: 'grey' }} />
@ -100,13 +107,13 @@ const NowPlayingLayout = () => {
<View style={{ width: 14 }} /> <View style={{ width: 14 }} />
</View> </View>
{/* extra controls */} {/* extra controls */}
<View style={{ <View
flex: 1, style={{
// backgroundColor: 'green', flex: 1,
flexDirection: 'row', // backgroundColor: 'green',
}}> flexDirection: 'row',
}}>
<View style={{ width: 14 }} /> <View style={{ width: 14 }} />
<View style={{ width: 60, height: 60, backgroundColor: 'grey' }} /> <View style={{ width: 60, height: 60, backgroundColor: 'grey' }} />
<View style={{ flex: 1 }} /> <View style={{ flex: 1 }} />
@ -115,7 +122,7 @@ const NowPlayingLayout = () => {
</View> </View>
</View> </View>
); );
} };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
text: { text: {

View File

@ -14,21 +14,15 @@ const TestControls = () => {
const removeAllKeys = async () => { const removeAllKeys = async () => {
const allKeys = await getAllKeys(); const allKeys = await getAllKeys();
await multiRemove(allKeys); await multiRemove(allKeys);
} };
return ( return (
<View> <View>
<Button <Button title="Remove all keys" onPress={removeAllKeys} />
title='Remove all keys' <Button title="Now Playing" onPress={() => navigation.navigate('Now Playing')} />
onPress={removeAllKeys}
/>
<Button
title='Now Playing'
onPress={() => navigation.navigate('Now Playing')}
/>
</View> </View>
); );
} };
const ServerSettingsView = () => { const ServerSettingsView = () => {
const [appSettings, setAppSettings] = useAtom(appSettingsAtom); const [appSettings, setAppSettings] = useAtom(appSettingsAtom);
@ -47,7 +41,9 @@ const ServerSettingsView = () => {
servers: [ servers: [
...appSettings.servers, ...appSettings.servers,
{ {
id, salt, address, id,
salt,
address,
username: 'guest', username: 'guest',
token: md5('guest' + salt), token: md5('guest' + salt),
}, },
@ -58,10 +54,7 @@ const ServerSettingsView = () => {
return ( return (
<View> <View>
<Button <Button title="Add default server" onPress={bootstrapServer} />
title='Add default server'
onPress={bootstrapServer}
/>
{appSettings.servers.map(s => ( {appSettings.servers.map(s => (
<View key={s.id}> <View key={s.id}>
<Text style={text.paragraph}>{s.address}</Text> <Text style={text.paragraph}>{s.address}</Text>
@ -70,7 +63,7 @@ const ServerSettingsView = () => {
))} ))}
</View> </View>
); );
} };
const SettingsView = () => ( const SettingsView = () => (
<View> <View>
@ -79,6 +72,6 @@ const SettingsView = () => (
<ServerSettingsView /> <ServerSettingsView />
</React.Suspense> </React.Suspense>
</View> </View>
) );
export default SettingsView; export default SettingsView;

View File

@ -37,12 +37,7 @@ const SplashPage: React.FC<{}> = ({ children }) => {
Capability.SkipToNext, Capability.SkipToNext,
Capability.SkipToPrevious, Capability.SkipToPrevious,
], ],
compactCapabilities: [ compactCapabilities: [Capability.Play, Capability.Pause, Capability.SkipToNext, Capability.SkipToPrevious],
Capability.Play,
Capability.Pause,
Capability.SkipToNext,
Capability.SkipToPrevious,
],
}); });
const castlevania: Track = { const castlevania: Track = {
@ -54,28 +49,24 @@ const SplashPage: React.FC<{}> = ({ children }) => {
artwork: 'https://webgames.host/uploads/2017/03/castlevania-3-draculas-curse.jpg', artwork: 'https://webgames.host/uploads/2017/03/castlevania-3-draculas-curse.jpg',
genre: 'BGM', genre: 'BGM',
date: new Date(1989, 1).toISOString(), date: new Date(1989, 1).toISOString(),
} };
await TrackPlayer.add([castlevania]); await TrackPlayer.add([castlevania]);
// TrackPlayer.play(); // TrackPlayer.play();
} };
const promise = Promise.all([ const promise = Promise.all([prepare(), minSplashTime]);
prepare(), minSplashTime,
]);
useEffect(() => { useEffect(() => {
promise.then(() => { promise.then(() => {
setReady(true); setReady(true);
}); });
}) });
if (!ready) { if (!ready) {
return <Text>Loading THE GOOD SHIT...</Text> return <Text>Loading THE GOOD SHIT...</Text>;
} }
return ( return <View style={{ flex: 1 }}>{children}</View>;
<View style={{ flex: 1 }}>{children}</View> };
);
}
export default SplashPage; export default SplashPage;

View File

@ -17,9 +17,7 @@ const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
const albumArt = useAtomValue(albumArtAtomFamily(id)); const albumArt = useAtomValue(albumArtAtomFamily(id));
const Placeholder = () => ( const Placeholder = () => (
<LinearGradient <LinearGradient colors={[colors.accent, colors.accentLow]}>
colors={[colors.accent, colors.accentLow]}
>
<FastImage <FastImage
source={require('../../../res/record.png')} source={require('../../../res/record.png')}
style={{ height, width }} style={{ height, width }}
@ -36,21 +34,23 @@ const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
coverArtUri={width > 128 ? albumArt?.uri : albumArt?.thumbUri} coverArtUri={width > 128 ? albumArt?.uri : albumArt?.thumbUri}
/> />
); );
} };
const AlbumArtFallback: React.FC<AlbumArtProps> = ({ height, width }) => ( const AlbumArtFallback: React.FC<AlbumArtProps> = ({ height, width }) => (
<View style={{ <View
height, width, style={{
alignItems: 'center', height,
justifyContent: 'center', width,
}}> alignItems: 'center',
<ActivityIndicator size='small' color={colors.accent} /> justifyContent: 'center',
}}>
<ActivityIndicator size="small" color={colors.accent} />
</View> </View>
); );
const AlbumArtLoader: React.FC<AlbumArtProps> = (props) => ( const AlbumArtLoader: React.FC<AlbumArtProps> = props => (
<React.Suspense fallback={<AlbumArtFallback { ...props } />}> <React.Suspense fallback={<AlbumArtFallback {...props} />}>
<AlbumArt { ...props } /> <AlbumArt {...props} />
</React.Suspense> </React.Suspense>
); );

View File

@ -1,7 +1,15 @@
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, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { ActivityIndicator, GestureResponderEvent, Image, Pressable, Text, useWindowDimensions, View } from 'react-native'; import {
ActivityIndicator,
GestureResponderEvent,
Image,
Pressable,
Text,
useWindowDimensions,
View,
} from 'react-native';
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';
@ -13,7 +21,7 @@ import GradientScrollView from './GradientScrollView';
const SongItem: React.FC<{ const SongItem: React.FC<{
id: string; id: string;
title: string title: string;
artist?: string; artist?: string;
onPress: (event: GestureResponderEvent) => void; onPress: (event: GestureResponderEvent) => void;
}> = ({ id, title, artist, onPress }) => { }> = ({ id, title, artist, onPress }) => {
@ -28,8 +36,7 @@ const SongItem: React.FC<{
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
}} }}>
>
<Pressable <Pressable
onPress={onPress} onPress={onPress}
onPressIn={() => setOpacity(0.6)} onPressIn={() => setOpacity(0.6)}
@ -38,19 +45,22 @@ const SongItem: React.FC<{
style={{ style={{
flex: 1, flex: 1,
opacity, opacity,
}} }}>
> <Text
<Text style={{ style={{
...text.songListTitle, ...text.songListTitle,
color: currentTrackId === id ? colors.accent : colors.text.primary, color: currentTrackId === id ? colors.accent : colors.text.primary,
}}>{title}</Text> }}>
{title}
</Text>
<Text style={text.songListSubtitle}>{artist}</Text> <Text style={text.songListSubtitle}>{artist}</Text>
</Pressable> </Pressable>
<View style={{ <View
flexDirection: 'row', style={{
alignItems: 'center', flexDirection: 'row',
marginLeft: 10, alignItems: 'center',
}}> marginLeft: 10,
}}>
{/* <Text style={text.songListSubtitle}>{secondsToTime(duration || 0)}</Text> */} {/* <Text style={text.songListSubtitle}>{secondsToTime(duration || 0)}</Text> */}
<Image <Image
source={require('../../../res/star.png')} source={require('../../../res/star.png')}
@ -74,10 +84,10 @@ const SongItem: React.FC<{
</View> </View>
</View> </View>
); );
} };
const AlbumDetails: React.FC<{ const AlbumDetails: React.FC<{
id: string, id: string;
}> = ({ id }) => { }> = ({ id }) => {
const album = useAtomValue(albumAtomFamily(id)); const album = useAtomValue(albumAtomFamily(id));
const layout = useWindowDimensions(); const layout = useWindowDimensions();
@ -86,9 +96,7 @@ const AlbumDetails: React.FC<{
const coverSize = layout.width - layout.width / 2.5; const coverSize = layout.width - layout.width / 2.5;
if (!album) { if (!album) {
return ( return <Text style={text.paragraph}>No Album</Text>;
<Text style={text.paragraph}>No Album</Text>
);
} }
return ( return (
@ -99,32 +107,36 @@ const AlbumDetails: React.FC<{
contentContainerStyle={{ contentContainerStyle={{
alignItems: 'center', alignItems: 'center',
paddingTop: coverSize / 8, paddingTop: coverSize / 8,
}}
>
<AlbumArt id={album.id} height={coverSize} width={coverSize} />
<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 <AlbumArt id={album.id} height={coverSize} width={coverSize} />
title='Play Album' <Text
onPress={() => setQueue(album.songs, album.songs[0].id)} 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={() => setQueue(album.songs, album.songs[0].id)} />
{/* <View style={{ width: 6, }}></View> {/* <View style={{ width: 6, }}></View>
<Button <Button
title='S' title='S'
@ -132,27 +144,27 @@ const AlbumDetails: React.FC<{
/> */} /> */}
</View> </View>
<View style={{ <View
width: layout.width - (layout.width / 20), style={{
marginTop: 20, width: layout.width - layout.width / 20,
marginBottom: 30, marginTop: 20,
}}> marginBottom: 30,
{album.songs }}>
.sort((a, b) => (a.track as number) - (b.track as number)) {album.songs
.map(s => ( .sort((a, b) => (a.track as number) - (b.track as number))
<SongItem .map(s => (
key={s.id} <SongItem
id={s.id} key={s.id}
title={s.title} id={s.id}
artist={s.artist} title={s.title}
onPress={() => setQueue(album.songs, s.id)} artist={s.artist}
/> onPress={() => setQueue(album.songs, s.id)}
))} />
))}
</View> </View>
</GradientScrollView> </GradientScrollView>
); );
} };
const AlbumViewFallback = () => { const AlbumViewFallback = () => {
const layout = useWindowDimensions(); const layout = useWindowDimensions();
@ -160,17 +172,18 @@ const AlbumViewFallback = () => {
const coverSize = layout.width - layout.width / 2.5; const coverSize = layout.width - layout.width / 2.5;
return ( return (
<GradientBackground style={{ <GradientBackground
alignItems: 'center', style={{
paddingTop: coverSize / 8 + coverSize / 2 - 18, alignItems: 'center',
}}> paddingTop: coverSize / 8 + coverSize / 2 - 18,
<ActivityIndicator size='large' color={colors.accent} /> }}>
<ActivityIndicator size="large" color={colors.accent} />
</GradientBackground> </GradientBackground>
); );
} };
const AlbumView: React.FC<{ const AlbumView: React.FC<{
id: string, id: string;
title: string; title: string;
}> = ({ id, title }) => { }> = ({ id, title }) => {
const navigation = useNavigation(); const navigation = useNavigation();

View File

@ -1,4 +1,3 @@
import { useAtom } from 'jotai';
import { useAtomValue } from 'jotai/utils'; import { useAtomValue } from 'jotai/utils';
import React from 'react'; import React from 'react';
import { ActivityIndicator, View } from 'react-native'; import { ActivityIndicator, View } from 'react-native';
@ -11,7 +10,7 @@ import CoverArt from './CoverArt';
interface ArtistArtSizeProps { interface ArtistArtSizeProps {
height: number; height: number;
width: number; width: number;
}; }
interface ArtistArtXUpProps extends ArtistArtSizeProps { interface ArtistArtXUpProps extends ArtistArtSizeProps {
coverArtUris: string[]; coverArtUris: string[];
@ -25,11 +24,11 @@ const PlaceholderContainer: React.FC<ArtistArtSizeProps> = ({ height, width, chi
<LinearGradient <LinearGradient
colors={[colors.accent, colors.accentLow]} colors={[colors.accent, colors.accentLow]}
style={{ style={{
height, width, height,
width,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}} }}>
>
{children} {children}
</LinearGradient> </LinearGradient>
); );
@ -123,11 +122,7 @@ const TwoUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) =>
const OneUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => { const OneUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
return ( return (
<PlaceholderContainer height={height} width={width}> <PlaceholderContainer height={height} width={width}>
<FastImage <FastImage source={{ uri: coverArtUris[0] }} style={{ height, width }} resizeMode={FastImage.resizeMode.cover} />
source={{ uri: coverArtUris[0] }}
style={{ height, width }}
resizeMode={FastImage.resizeMode.cover}
/>
</PlaceholderContainer> </PlaceholderContainer>
); );
}; };
@ -135,14 +130,14 @@ const OneUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) =>
const NoneUp: React.FC<ArtistArtSizeProps> = ({ height, width }) => { const NoneUp: React.FC<ArtistArtSizeProps> = ({ height, width }) => {
return ( return (
<PlaceholderContainer height={height} width={width}> <PlaceholderContainer height={height} width={width}>
<FastImage <FastImage
source={require('../../../res/mic_on-fill.png')} source={require('../../../res/mic_on-fill.png')}
style={{ style={{
height: height - height / 4, height: height - height / 4,
width: width - width / 4, width: width - width / 4,
}} }}
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
/> />
</PlaceholderContainer> </PlaceholderContainer>
); );
}; };
@ -172,36 +167,34 @@ const ArtistArt: React.FC<ArtistArtProps> = ({ id, height, width }) => {
} }
return none; return none;
} };
return ( return (
<View style={{ <View
borderRadius: height / 2, style={{
overflow: 'hidden', borderRadius: height / 2,
}}> overflow: 'hidden',
<CoverArt }}>
PlaceholderComponent={Placeholder} <CoverArt PlaceholderComponent={Placeholder} height={height} width={width} coverArtUri={artistArt?.uri} />
height={height}
width={width}
coverArtUri={artistArt?.uri}
/>
</View> </View>
); );
} };
const ArtistArtFallback: React.FC<ArtistArtProps> = ({ height, width }) => ( const ArtistArtFallback: React.FC<ArtistArtProps> = ({ height, width }) => (
<View style={{ <View
height, width, style={{
alignItems: 'center', height,
justifyContent: 'center', width,
}}> alignItems: 'center',
<ActivityIndicator size='small' color={colors.accent} /> justifyContent: 'center',
}}>
<ActivityIndicator size="small" color={colors.accent} />
</View> </View>
); );
const ArtistArtLoader: React.FC<ArtistArtProps> = (props) => ( const ArtistArtLoader: React.FC<ArtistArtProps> = props => (
<React.Suspense fallback={<ArtistArtFallback { ...props } />}> <React.Suspense fallback={<ArtistArtFallback {...props} />}>
<ArtistArt { ...props } /> <ArtistArt {...props} />
</React.Suspense> </React.Suspense>
); );

View File

@ -22,16 +22,15 @@ const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
contentContainerStyle={{ contentContainerStyle={{
alignItems: 'center', alignItems: 'center',
// paddingTop: coverSize / 8, // paddingTop: coverSize / 8,
}} }}>
>
<Text style={text.paragraph}>{artist.name}</Text> <Text style={text.paragraph}>{artist.name}</Text>
<ArtistArt id={artist.id} height={200} width={200} /> <ArtistArt id={artist.id} height={200} width={200} />
</GradientScrollView> </GradientScrollView>
) );
} };
const ArtistView: React.FC<{ const ArtistView: React.FC<{
id: string, id: string;
title: string; title: string;
}> = ({ id, title }) => { }> = ({ id, title }) => {
const navigation = useNavigation(); const navigation = useNavigation();

View File

@ -1,12 +1,11 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Text, View, Image, Pressable } from 'react-native'; import { Text, View, Pressable } from 'react-native';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs'; import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import textStyles from '../../styles/text'; import textStyles 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 { useNavigation } from '@react-navigation/native';
const icons: {[key: string]: any} = { const icons: { [key: string]: any } = {
home: { home: {
regular: require('../../../res/home.png'), regular: require('../../../res/home.png'),
fill: require('../../../res/home-fill.png'), fill: require('../../../res/home-fill.png'),
@ -23,14 +22,14 @@ const icons: {[key: string]: any} = {
regular: require('../../../res/settings.png'), regular: require('../../../res/settings.png'),
fill: require('../../../res/settings-fill.png'), fill: require('../../../res/settings-fill.png'),
}, },
} };
const BottomTabButton: React.FC<{ const BottomTabButton: React.FC<{
routeKey: string; routeKey: string;
label: string; label: string;
name: string; name: string;
isFocused: boolean; isFocused: boolean;
img: { regular: number, fill: number }; img: { regular: number; fill: number };
navigation: any; navigation: any;
}> = ({ routeKey, label, name, isFocused, img, navigation }) => { }> = ({ routeKey, label, name, isFocused, img, navigation }) => {
const [opacity, setOpacity] = useState(1); const [opacity, setOpacity] = useState(1);
@ -56,8 +55,7 @@ const BottomTabButton: React.FC<{
alignItems: 'center', alignItems: 'center',
flex: 1, flex: 1,
opacity, opacity,
}} }}>
>
<FastImage <FastImage
source={isFocused ? img.fill : img.regular} source={isFocused ? img.fill : img.regular}
style={{ style={{
@ -66,47 +64,51 @@ const BottomTabButton: React.FC<{
}} }}
tintColor={isFocused ? colors.text.primary : colors.text.secondary} tintColor={isFocused ? colors.text.primary : colors.text.secondary}
/> />
<Text style={{ <Text
...textStyles.xsmall, style={{
color: isFocused ? colors.text.primary : colors.text.secondary, ...textStyles.xsmall,
}}> color: isFocused ? colors.text.primary : colors.text.secondary,
}}>
{label} {label}
</Text> </Text>
</Pressable> </Pressable>
); );
} };
const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigation }) => { const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigation }) => {
return ( return (
<View style={{ <View
height: 54, style={{
backgroundColor: colors.gradient.high, height: 54,
flexDirection: 'row', backgroundColor: colors.gradient.high,
alignItems: 'center', flexDirection: 'row',
justifyContent: 'space-around', alignItems: 'center',
paddingHorizontal: 28, justifyContent: 'space-around',
}}> paddingHorizontal: 28,
}}>
{state.routes.map((route, index) => { {state.routes.map((route, index) => {
const { options } = descriptors[route.key] as any; const { options } = descriptors[route.key] as any;
const label = const label =
options.tabBarLabel !== undefined options.tabBarLabel !== undefined
? options.tabBarLabel as string ? (options.tabBarLabel as string)
: options.title !== undefined : options.title !== undefined
? options.title ? options.title
: route.name; : route.name;
return <BottomTabButton return (
key={route.key} <BottomTabButton
routeKey={route.key} key={route.key}
label={label} routeKey={route.key}
name={route.name} label={label}
isFocused={state.index === index} name={route.name}
img={icons[options.icon]} isFocused={state.index === index}
navigation={navigation} img={icons[options.icon]}
/>; navigation={navigation}
/>
);
})} })}
</View> </View>
); );
} };
export default BottomTabBar; export default BottomTabBar;

View File

@ -22,11 +22,10 @@ const Button: React.FC<{
justifyContent: 'center', justifyContent: 'center',
borderRadius: 1000, borderRadius: 1000,
opacity, opacity,
}} }}>
>
<Text style={{ ...text.button }}>{title}</Text> <Text style={{ ...text.button }}>{title}</Text>
</Pressable> </Pressable>
); );
} };
export default Button; export default Button;

View File

@ -4,10 +4,10 @@ import FastImage from 'react-native-fast-image';
import colors from '../../styles/colors'; import colors from '../../styles/colors';
const CoverArt: React.FC<{ const CoverArt: React.FC<{
PlaceholderComponent: () => JSX.Element, PlaceholderComponent: () => JSX.Element;
height: number, height: number;
width: number, width: number;
coverArtUri?: string coverArtUri?: string;
}> = ({ PlaceholderComponent, 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,14 +16,15 @@ const CoverArt: 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 }) => (
<View style={{ <View
opacity: visible ? 100 : 0, style={{
}}> opacity: visible ? 100 : 0,
}}>
<PlaceholderComponent /> <PlaceholderComponent />
</View> </View>
); );
const CoverArt = () => ( const Art = () => (
<> <>
<Placeholder visible={placeholderVisible} /> <Placeholder visible={placeholderVisible} />
<ActivityIndicator <ActivityIndicator
@ -37,7 +38,8 @@ const CoverArt: React.FC<{
<FastImage <FastImage
source={{ uri: coverArtUri, priority: 'high' }} source={{ uri: coverArtUri, priority: 'high' }}
style={{ style={{
height, width, height,
width,
marginTop: -height - halfIndicatorHeight * 2, marginTop: -height - halfIndicatorHeight * 2,
}} }}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
@ -50,11 +52,7 @@ const CoverArt: React.FC<{
</> </>
); );
return ( return <View style={{ height, width }}>{!coverArtUri ? <Placeholder visible={true} /> : <Art />}</View>;
<View style={{ height, width }}> };
{!coverArtUri ? <Placeholder visible={true} /> : <CoverArt />}
</View>
);
}
export default React.memo(CoverArt); export default React.memo(CoverArt);

View File

@ -14,14 +14,13 @@ const GradientBackground: React.FC<{
return ( return (
<LinearGradient <LinearGradient
colors={[colors.gradient.high, colors.gradient.low]} colors={[colors.gradient.high, colors.gradient.low]}
locations={[0.01,0.7]} locations={[0.01, 0.7]}
style={{ style={{
...style, ...style,
width: width || '100%', width: width || '100%',
height: height || layout.height, height: height || layout.height,
position: position || 'absolute', position: position || 'absolute',
}} }}>
>
{children} {children}
</LinearGradient> </LinearGradient>
); );

View File

@ -7,13 +7,13 @@ function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) {
return ( return (
<FlatList <FlatList
{ ...props } {...props}
ListHeaderComponent={() => <GradientBackground position='relative' />} ListHeaderComponent={() => <GradientBackground position="relative" />}
ListHeaderComponentStyle={{ ListHeaderComponentStyle={{
marginBottom: -layout.height, marginBottom: -layout.height,
}} }}
/> />
); );
}; }
export default GradientFlatList; export default GradientFlatList;

View File

@ -2,11 +2,8 @@ import React from 'react';
import { ScrollView, ScrollViewProps } from 'react-native'; import { ScrollView, ScrollViewProps } from 'react-native';
import GradientBackground from './GradientBackground'; import GradientBackground from './GradientBackground';
const GradientScrollView: React.FC<ScrollViewProps> = (props) => ( const GradientScrollView: React.FC<ScrollViewProps> = props => (
<ScrollView <ScrollView overScrollMode="never" {...props}>
overScrollMode='never'
{...props}
>
<GradientBackground /> <GradientBackground />
{props.children} {props.children}
</ScrollView> </ScrollView>

View File

@ -5,11 +5,10 @@ import colors from '../../styles/colors';
const TopTabContainer: React.FC<{}> = ({ children }) => ( const TopTabContainer: React.FC<{}> = ({ children }) => (
<LinearGradient <LinearGradient
colors={[colors.gradient.high, colors.gradient.mid, colors.gradient.low]} colors={[colors.gradient.high, colors.gradient.mid, colors.gradient.low]}
locations={[0.03,0.3,0.7]} locations={[0.03, 0.3, 0.7]}
style={{ style={{
flex: 1, flex: 1,
}} }}>
>
{children} {children}
</LinearGradient> </LinearGradient>
); );

View File

@ -12,7 +12,7 @@ const AlbumItem: React.FC<{
id: string; id: string;
name: string; name: string;
artist?: string; artist?: string;
}> = ({ id, name, artist, }) => { }> = ({ id, name, artist }) => {
const navigation = useNavigation(); const navigation = useNavigation();
const size = 125; const size = 125;
@ -22,34 +22,30 @@ const AlbumItem: React.FC<{
style={{ style={{
alignItems: 'center', alignItems: 'center',
marginVertical: 8, marginVertical: 8,
flex: 1/3, flex: 1 / 3,
}} }}
onPress={() => navigation.navigate('AlbumView', { id, title: name })} onPress={() => navigation.navigate('AlbumView', { id, title: name })}>
>
<AlbumArt id={id} height={size} width={size} /> <AlbumArt id={id} height={size} width={size} />
<View style={{ <View
flex: 1, style={{
width: size, flex: 1,
}}> width: size,
}}>
<Text <Text
style={{ style={{
...textStyles.itemTitle, ...textStyles.itemTitle,
marginTop: 4, marginTop: 4,
}} }}
numberOfLines={2} numberOfLines={2}>
>
{name} {name}
</Text> </Text>
<Text <Text style={{ ...textStyles.itemSubtitle }} numberOfLines={1}>
style={{ ...textStyles.itemSubtitle }}
numberOfLines={1}
>
{artist} {artist}
</Text> </Text>
</View> </View>
</Pressable> </Pressable>
); );
} };
const MemoAlbumItem = React.memo(AlbumItem); const MemoAlbumItem = React.memo(AlbumItem);
const AlbumListRenderItem: React.FC<{ item: Album }> = ({ item }) => ( const AlbumListRenderItem: React.FC<{ item: Album }> = ({ item }) => (
@ -79,11 +75,11 @@ const AlbumsList = () => {
removeClippedSubviews={true} removeClippedSubviews={true}
refreshing={updating} refreshing={updating}
onRefresh={updateAlbums} onRefresh={updateAlbums}
overScrollMode='never' overScrollMode="never"
/> />
</View> </View>
); );
} };
const AlbumsTab = () => ( const AlbumsTab = () => (
<React.Suspense fallback={<Text>Loading...</Text>}> <React.Suspense fallback={<Text>Loading...</Text>}>

View File

@ -2,9 +2,9 @@ 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 { Pressable } from 'react-native';
import { Image, Text, View } from 'react-native'; import { Text } from 'react-native';
import { Artist } from '../../models/music'; import { Artist } from '../../models/music';
import { artistInfoAtomFamily, artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '../../state/music'; import { artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '../../state/music';
import textStyles from '../../styles/text'; import textStyles from '../../styles/text';
import ArtistArt from '../common/ArtistArt'; import ArtistArt from '../common/ArtistArt';
import GradientFlatList from '../common/GradientFlatList'; import GradientFlatList from '../common/GradientFlatList';
@ -20,20 +20,22 @@ const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => {
marginVertical: 6, marginVertical: 6,
marginLeft: 6, marginLeft: 6,
}} }}
onPress={() => navigation.navigate('ArtistView', { id: item.id, title: item.name })} onPress={() => navigation.navigate('ArtistView', { id: item.id, title: item.name })}>
>
<ArtistArt id={item.id} width={56} height={56} /> <ArtistArt id={item.id} width={56} height={56} />
<Text style={{ <Text
...textStyles.paragraph, style={{
marginLeft: 12, ...textStyles.paragraph,
}}>{item.name}</Text> marginLeft: 12,
}}>
{item.name}
</Text>
</Pressable> </Pressable>
); );
}; };
const ArtistItemLoader: React.FC<{ item: Artist }> = (props) => ( const ArtistItemLoader: React.FC<{ item: Artist }> = props => (
<React.Suspense fallback={<Text>Loading...</Text>}> <React.Suspense fallback={<Text>Loading...</Text>}>
<ArtistItem { ...props } /> <ArtistItem {...props} />
</React.Suspense> </React.Suspense>
); );
@ -48,9 +50,7 @@ const ArtistsList = () => {
} }
}); });
const renderItem: React.FC<{ item: Artist }> = ({ item }) => ( const renderItem: React.FC<{ item: Artist }> = ({ item }) => <ArtistItemLoader item={item} />;
<ArtistItemLoader item={item} />
);
return ( return (
<GradientFlatList <GradientFlatList
@ -59,13 +59,11 @@ const ArtistsList = () => {
keyExtractor={item => item.id} keyExtractor={item => item.id}
onRefresh={updateArtists} onRefresh={updateArtists}
refreshing={updating} refreshing={updating}
overScrollMode='never' overScrollMode="never"
/> />
); );
} };
const ArtistsTab = () => ( const ArtistsTab = () => <ArtistsList />;
<ArtistsList />
);
export default ArtistsTab; export default ArtistsTab;

View File

@ -1,8 +1,6 @@
import React from 'react'; import React from 'react';
import GradientBackground from '../common/GradientBackground'; import GradientBackground from '../common/GradientBackground';
const PlaylistsTab = () => ( const PlaylistsTab = () => <GradientBackground />;
<GradientBackground />
);
export default PlaylistsTab; export default PlaylistsTab;

View File

@ -10,31 +10,13 @@ const Tab = createBottomTabNavigator();
const BottomTabNavigator = () => { const BottomTabNavigator = () => {
return ( return (
<Tab.Navigator <Tab.Navigator tabBar={BottomTabBar}>
tabBar={BottomTabBar} <Tab.Screen name="Home" component={ArtistsList} options={{ icon: 'home' } as any} />
> <Tab.Screen name="Library" component={LibraryTopTabNavigator} options={{ icon: 'library' } as any} />
<Tab.Screen <Tab.Screen name="Search" component={NowPlayingLayout} options={{ icon: 'search' } as any} />
name='Home' <Tab.Screen name="Settings" component={SettingsView} options={{ icon: 'settings' } as any} />
component={ArtistsList}
options={{ icon: 'home' } as any}
/>
<Tab.Screen
name='Library'
component={LibraryTopTabNavigator}
options={{ icon: 'library' } as any}
/>
<Tab.Screen
name='Search'
component={NowPlayingLayout}
options={{ icon: 'search' } as any}
/>
<Tab.Screen
name='Settings'
component={SettingsView}
options={{ icon: 'settings' } as any}
/>
</Tab.Navigator> </Tab.Navigator>
); );
} };
export default BottomTabNavigator; export default BottomTabNavigator;

View File

@ -15,48 +15,40 @@ import ArtistView from '../common/ArtistView';
const Tab = createMaterialTopTabNavigator(); const Tab = createMaterialTopTabNavigator();
const LibraryTopTabNavigator = () => ( const LibraryTopTabNavigator = () => (
<Tab.Navigator tabBarOptions={{ <Tab.Navigator
style: { tabBarOptions={{
height: 48, style: {
backgroundColor: colors.gradient.high, height: 48,
elevation: 0, backgroundColor: colors.gradient.high,
}, elevation: 0,
labelStyle: { },
...text.header, labelStyle: {
textTransform: null as any, ...text.header,
marginTop: 0, textTransform: null as any,
marginHorizontal: 2, marginTop: 0,
}, marginHorizontal: 2,
indicatorStyle: { },
backgroundColor: colors.accent, indicatorStyle: {
}, backgroundColor: colors.accent,
}}> },
<Tab.Screen }}>
name='Albums' <Tab.Screen name="Albums" component={AlbumsTab} />
component={AlbumsTab} <Tab.Screen name="Artists" component={ArtistsTab} />
/> <Tab.Screen name="Playlists" component={PlaylistsTab} />
<Tab.Screen
name='Artists'
component={ArtistsTab}
/>
<Tab.Screen
name='Playlists'
component={PlaylistsTab}
/>
</Tab.Navigator> </Tab.Navigator>
); );
type LibraryStackParamList = { type LibraryStackParamList = {
LibraryTopTabs: undefined, LibraryTopTabs: undefined;
AlbumView: { id: string, title: string }; AlbumView: { id: string; title: string };
ArtistView: { id: string, title: string }; ArtistView: { id: string; title: string };
} };
type AlbumScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'AlbumView'>; type AlbumScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'AlbumView'>;
type AlbumScreenRouteProp = RouteProp<LibraryStackParamList, 'AlbumView'>; type AlbumScreenRouteProp = RouteProp<LibraryStackParamList, 'AlbumView'>;
type AlbumScreenProps = { type AlbumScreenProps = {
route: AlbumScreenRouteProp, route: AlbumScreenRouteProp;
navigation: AlbumScreenNavigationProp, navigation: AlbumScreenNavigationProp;
}; };
const AlbumScreen: React.FC<AlbumScreenProps> = ({ route }) => ( const AlbumScreen: React.FC<AlbumScreenProps> = ({ route }) => (
@ -66,8 +58,8 @@ const AlbumScreen: React.FC<AlbumScreenProps> = ({ route }) => (
type ArtistScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'ArtistView'>; type ArtistScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'ArtistView'>;
type ArtistScreenRouteProp = RouteProp<LibraryStackParamList, 'ArtistView'>; type ArtistScreenRouteProp = RouteProp<LibraryStackParamList, 'ArtistView'>;
type ArtistScreenProps = { type ArtistScreenProps = {
route: ArtistScreenRouteProp, route: ArtistScreenRouteProp;
navigation: ArtistScreenNavigationProp, navigation: ArtistScreenNavigationProp;
}; };
const ArtistScreen: React.FC<ArtistScreenProps> = ({ route }) => ( const ArtistScreen: React.FC<ArtistScreenProps> = ({ route }) => (
@ -78,10 +70,10 @@ const Stack = createStackNavigator<LibraryStackParamList>();
const itemScreenOptions = { const itemScreenOptions = {
title: '', title: '',
headerStyle: { headerStyle: {
height: 50, height: 50,
backgroundColor: colors.gradient.high, backgroundColor: colors.gradient.high,
}, },
headerTitleContainerStyle: { headerTitleContainerStyle: {
marginLeft: -14, marginLeft: -14,
}, },
@ -91,31 +83,21 @@ const itemScreenOptions = {
headerTitleStyle: { headerTitleStyle: {
...text.header, ...text.header,
}, },
headerBackImage: () => <FastImage headerBackImage: () => (
source={require('../../../res/arrow_left-fill.png')} <FastImage
tintColor={colors.text.primary} source={require('../../../res/arrow_left-fill.png')}
style={{ height: 22, width: 22 }} 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>
<Stack.Screen <Stack.Screen name="LibraryTopTabs" component={LibraryTopTabNavigator} options={{ headerShown: false }} />
name='LibraryTopTabs' <Stack.Screen name="AlbumView" component={AlbumScreen} options={itemScreenOptions} />
component={LibraryTopTabNavigator} <Stack.Screen name="ArtistView" component={ArtistScreen} options={itemScreenOptions} />
options={{ headerShown: false }}
/>
<Stack.Screen
name='AlbumView'
component={AlbumScreen}
options={itemScreenOptions}
/>
<Stack.Screen
name='ArtistView'
component={ArtistScreen}
options={itemScreenOptions}
/>
</Stack.Navigator> </Stack.Navigator>
</View> </View>
); );

View File

@ -7,16 +7,8 @@ const RootStack = createStackNavigator();
const RootNavigator = () => ( const RootNavigator = () => (
<RootStack.Navigator> <RootStack.Navigator>
<RootStack.Screen <RootStack.Screen name="Main" component={BottomTabNavigator} options={{ headerShown: false }} />
name='Main' <RootStack.Screen name="Now Playing" component={NowPlayingLayout} options={{ headerShown: false }} />
component={BottomTabNavigator}
options={{ headerShown: false }}
/>
<RootStack.Screen
name='Now Playing'
component={NowPlayingLayout}
options={{ headerShown: false }}
/>
</RootStack.Navigator> </RootStack.Navigator>
); );

View File

@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from 'react';
import TrackPlayer, { Track, useTrackPlayerEvents, Event, State } from "react-native-track-player"; import TrackPlayer, { Track, useTrackPlayerEvents, Event, State } from 'react-native-track-player';
import { Song } from "../models/music"; import { Song } from '../models/music';
function mapSongToTrack(song: Song): Track { function mapSongToTrack(song: Song): Track {
return { return {
@ -10,19 +10,15 @@ function mapSongToTrack(song: Song): Track {
url: song.streamUri, url: song.streamUri,
artwork: song.coverArtUri, artwork: song.coverArtUri,
duration: song.duration, duration: song.duration,
} };
} }
const currentTrackEvents = [ const currentTrackEvents = [Event.PlaybackState, Event.PlaybackTrackChanged, Event.RemoteStop];
Event.PlaybackState,
Event.PlaybackTrackChanged,
Event.RemoteStop,
]
export const useCurrentTrackId = () => { export const useCurrentTrackId = () => {
const [currentTrackId, setCurrentTrackId] = useState<string | null>(null); const [currentTrackId, setCurrentTrackId] = useState<string | null>(null);
useTrackPlayerEvents(currentTrackEvents, async (event) => { useTrackPlayerEvents(currentTrackEvents, async event => {
switch (event.type) { switch (event.type) {
case Event.PlaybackState: case Event.PlaybackState:
switch (event.state) { switch (event.state) {
@ -33,7 +29,7 @@ export const useCurrentTrackId = () => {
} }
break; break;
case Event.PlaybackTrackChanged: case Event.PlaybackTrackChanged:
const trackIndex = await TrackPlayer.getCurrentTrack() const trackIndex = await TrackPlayer.getCurrentTrack();
setCurrentTrackId((await TrackPlayer.getTrack(trackIndex)).id); setCurrentTrackId((await TrackPlayer.getTrack(trackIndex)).id);
break; break;
case Event.RemoteStop: case Event.RemoteStop:
@ -45,7 +41,7 @@ export const useCurrentTrackId = () => {
}); });
return currentTrackId; return currentTrackId;
} };
export const useSetQueue = () => { export const useSetQueue = () => {
return async (songs: Song[], playId?: string) => { return async (songs: Song[], playId?: string) => {
@ -70,5 +66,5 @@ export const useSetQueue = () => {
const queue = await TrackPlayer.getQueue(); const queue = await TrackPlayer.getQueue();
console.log(`queue: ${JSON.stringify(queue.map(x => x.title))}`); console.log(`queue: ${JSON.stringify(queue.map(x => x.title))}`);
} }
} };
} };

View File

@ -1,6 +1,6 @@
import { useAtomValue } from "jotai/utils" import { useAtomValue } from 'jotai/utils';
import { activeServerAtom } from "../state/settings" import { activeServerAtom } from '../state/settings';
import { SubsonicApiClient } from "../subsonic/api"; import { SubsonicApiClient } from '../subsonic/api';
export const useSubsonicApi = () => { export const useSubsonicApi = () => {
const activeServer = useAtomValue(activeServerAtom); const activeServer = useAtomValue(activeServerAtom);
@ -10,5 +10,5 @@ export const useSubsonicApi = () => {
return undefined; return undefined;
} }
return new SubsonicApiClient(activeServer); return new SubsonicApiClient(activeServer);
} };
} };

View File

@ -24,8 +24,8 @@ export interface Album {
name: string; name: string;
starred?: Date; starred?: Date;
coverArt?: string; coverArt?: string;
coverArtUri?: string, coverArtUri?: string;
coverArtThumbUri?: string, coverArtThumbUri?: string;
year?: number; year?: number;
} }
@ -60,7 +60,7 @@ export interface Song {
starred?: Date; starred?: Date;
streamUri: string; streamUri: string;
coverArtUri?: string, coverArtUri?: string;
} }
export type DownloadedSong = { export type DownloadedSong = {
@ -91,4 +91,4 @@ export type DownloadedPlaylist = {
type: 'playlist'; type: 'playlist';
songs: string[]; songs: string[];
name: string; name: string;
}; };

View File

@ -7,6 +7,6 @@ export interface Server {
} }
export interface AppSettings { export interface AppSettings {
servers: Server[], servers: Server[];
activeServer?: string; activeServer?: string;
} }

View File

@ -1,23 +1,23 @@
import TrackPlayer, { Event } from 'react-native-track-player'; import TrackPlayer, { Event } from 'react-native-track-player';
module.exports = async function() { module.exports = async function () {
TrackPlayer.addEventListener(Event.RemotePlay, () => TrackPlayer.play()); TrackPlayer.addEventListener(Event.RemotePlay, () => TrackPlayer.play());
TrackPlayer.addEventListener(Event.RemotePause, () => TrackPlayer.pause()); TrackPlayer.addEventListener(Event.RemotePause, () => TrackPlayer.pause());
TrackPlayer.addEventListener(Event.RemoteStop, () => TrackPlayer.destroy()); TrackPlayer.addEventListener(Event.RemoteStop, () => TrackPlayer.destroy());
TrackPlayer.addEventListener(Event.RemoteDuck, (data) => { TrackPlayer.addEventListener(Event.RemoteDuck, data => {
if (data.permanent) { if (data.permanent) {
TrackPlayer.stop(); TrackPlayer.stop();
return; return;
} }
if (data.paused) { if (data.paused) {
TrackPlayer.pause(); TrackPlayer.pause();
} else { } else {
TrackPlayer.play(); TrackPlayer.play();
} }
}); });
TrackPlayer.addEventListener(Event.RemoteNext, () => TrackPlayer.skipToNext().catch(() => {})); TrackPlayer.addEventListener(Event.RemoteNext, () => TrackPlayer.skipToNext().catch(() => {}));
TrackPlayer.addEventListener(Event.RemotePrevious, () => TrackPlayer.skipToPrevious().catch(() => {})); TrackPlayer.addEventListener(Event.RemotePrevious, () => TrackPlayer.skipToPrevious().catch(() => {}));
}; };

View File

@ -27,14 +27,16 @@ export const useUpdateArtists = () => {
const client = new SubsonicApiClient(server); const client = new SubsonicApiClient(server);
const response = await client.getArtists(); const response = await client.getArtists();
setArtists(response.data.artists.map(x => ({ setArtists(
id: x.id, response.data.artists.map(x => ({
name: x.name, id: x.id,
starred: x.starred, name: x.name,
}))); starred: x.starred,
})),
);
setUpdating(false); setUpdating(false);
} };
} };
export const albumsAtom = atom<Record<string, Album>>({}); export const albumsAtom = atom<Record<string, Album>>({});
export const albumsUpdatingAtom = atom(false); export const albumsUpdatingAtom = atom(false);
@ -57,92 +59,102 @@ 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.reduce((acc, next) => { setAlbums(
const album = mapAlbumID3(next, client); response.data.albums.reduce((acc, next) => {
acc[album.id] = album; const album = mapAlbumID3(next, client);
return acc; acc[album.id] = album;
}, {} as Record<string, Album>)); return acc;
}, {} as Record<string, Album>),
);
setUpdating(false); setUpdating(false);
}
}
export const albumAtomFamily = atomFamily((id: string) => atom<AlbumWithSongs | undefined>(async (get) => {
const server = get(activeServerAtom);
if (!server) {
return undefined;
}
const client = new SubsonicApiClient(server);
const response = await client.getAlbum({ id });
return mapAlbumID3WithSongs(response.data.album, response.data.songs, client);
}));
export const albumArtAtomFamily = atomFamily((id: string) => atom<AlbumArt | undefined>(async (get) => {
const server = get(activeServerAtom);
if (!server) {
return undefined;
}
const albums = get(albumsAtom);
const album = id in albums ? albums[id] : undefined;
if (!album) {
return undefined;
}
const client = new SubsonicApiClient(server);
return {
uri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt }) : undefined,
thumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined,
}; };
})); };
export const artistInfoAtomFamily = atomFamily((id: string) => atom<ArtistInfo | undefined>(async (get) => { export const albumAtomFamily = atomFamily((id: string) =>
const server = get(activeServerAtom); atom<AlbumWithSongs | undefined>(async get => {
if (!server) { const server = get(activeServerAtom);
return undefined; if (!server) {
} return undefined;
}
const client = new SubsonicApiClient(server); const client = new SubsonicApiClient(server);
const [artistResponse, artistInfoResponse] = await Promise.all([ const response = await client.getAlbum({ id });
client.getArtist({ id }), return mapAlbumID3WithSongs(response.data.album, response.data.songs, client);
client.getArtistInfo2({ id }), }),
]); );
return mapArtistInfo(artistResponse.data, artistInfoResponse.data.artistInfo, client);
}));
export const artistArtAtomFamily = atomFamily((id: string) => atom<ArtistArt | undefined>(async (get) => { export const albumArtAtomFamily = atomFamily((id: string) =>
const artistInfo = get(artistInfoAtomFamily(id)); atom<AlbumArt | undefined>(async get => {
if (!artistInfo) { const server = get(activeServerAtom);
return undefined; if (!server) {
} return undefined;
}
const coverArtUris = artistInfo.albums const albums = get(albumsAtom);
.filter(a => a.coverArtThumbUri !== undefined) const album = id in albums ? albums[id] : undefined;
.sort((a, b) => { if (!album) {
if (b.year && a.year) { return undefined;
return b.year - a.year; }
} else {
return a.name.localeCompare(b.name) - 9000;
}
})
.map(a => a.coverArtThumbUri) as string[];
return { const client = new SubsonicApiClient(server);
coverArtUris,
uri: artistInfo.mediumImageUrl, return {
}; uri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt }) : undefined,
})); thumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined,
};
}),
);
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);
}),
);
export const artistArtAtomFamily = atomFamily((id: string) =>
atom<ArtistArt | undefined>(async get => {
const artistInfo = get(artistInfoAtomFamily(id));
if (!artistInfo) {
return undefined;
}
const coverArtUris = artistInfo.albums
.filter(a => a.coverArtThumbUri !== undefined)
.sort((a, b) => {
if (b.year && a.year) {
return b.year - a.year;
} else {
return a.name.localeCompare(b.name) - 9000;
}
})
.map(a => a.coverArtThumbUri) as string[];
return {
coverArtUris,
uri: artistInfo.mediumImageUrl,
};
}),
);
function mapArtistInfo( function mapArtistInfo(
artistResponse: GetArtistResponse, artistResponse: GetArtistResponse,
artistInfo: ArtistInfo2Element, artistInfo: ArtistInfo2Element,
client: SubsonicApiClient client: SubsonicApiClient,
): ArtistInfo { ): ArtistInfo {
const info = { ...artistInfo } as any; const info = { ...artistInfo } as any;
delete info.similarArtists; delete info.similarArtists;
const { artist, albums } = artistResponse const { artist, albums } = artistResponse;
const mappedAlbums = albums.map(a => mapAlbumID3(a, client)); const mappedAlbums = albums.map(a => mapAlbumID3(a, client));
const coverArtUris = mappedAlbums const coverArtUris = mappedAlbums
@ -160,15 +172,15 @@ function mapArtistInfo(
...info, ...info,
albums: mappedAlbums, albums: mappedAlbums,
coverArtUris, coverArtUris,
} };
} }
function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album { function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
return { return {
...album, ...album,
coverArtUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt }) : undefined, coverArtUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt }) : undefined,
coverArtThumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined, coverArtThumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined,
} };
} }
function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song { function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
@ -176,16 +188,16 @@ function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
...child, ...child,
streamUri: client.streamUri({ id: child.id }), streamUri: client.streamUri({ id: child.id }),
coverArtUri: child.coverArt ? client.getCoverArtUri({ id: child.coverArt }) : undefined, coverArtUri: child.coverArt ? client.getCoverArtUri({ id: child.coverArt }) : undefined,
} };
} }
function mapAlbumID3WithSongs( function mapAlbumID3WithSongs(
album: AlbumID3Element, album: AlbumID3Element,
songs: ChildElement[], songs: ChildElement[],
client: SubsonicApiClient client: SubsonicApiClient,
): AlbumWithSongs { ): AlbumWithSongs {
return { return {
...mapAlbumID3(album, client), ...mapAlbumID3(album, client),
songs: songs.map(s => mapChildToSong(s, client)), songs: songs.map(s => mapChildToSong(s, client)),
} };
} }

View File

@ -6,7 +6,7 @@ export const appSettingsAtom = atomWithAsyncStorage<AppSettings>('@appSettings',
servers: [], servers: [],
}); });
export const activeServerAtom = atom((get) => { export const activeServerAtom = atom(get => {
const appSettings = get(appSettingsAtom); const appSettings = get(appSettingsAtom);
return appSettings.servers.find(x => x.id == appSettings.activeServer); return appSettings.servers.find(x => x.id === appSettings.activeServer);
}); });

View File

@ -15,7 +15,7 @@ export async function multiGet(keys: string[]): Promise<[string, any | null][]>
const items = await AsyncStorage.multiGet(keys); const items = await AsyncStorage.multiGet(keys);
return items.map(x => [x[0], x[1] ? JSON.parse(x[1]) : null]); return items.map(x => [x[0], x[1] ? JSON.parse(x[1]) : null]);
} catch (e) { } catch (e) {
console.error(`multiGet error`, e); console.error('multiGet error', e);
return []; return [];
} }
} }
@ -32,7 +32,7 @@ export async function multiSet(items: string[][]): Promise<void> {
try { try {
await AsyncStorage.multiSet(items.map(x => [x[0], JSON.stringify(x[1])])); await AsyncStorage.multiSet(items.map(x => [x[0], JSON.stringify(x[1])]));
} catch (e) { } catch (e) {
console.error(`multiSet error`, e); console.error('multiSet error', e);
} }
} }
@ -40,7 +40,7 @@ export async function getAllKeys(): Promise<string[]> {
try { try {
return await AsyncStorage.getAllKeys(); return await AsyncStorage.getAllKeys();
} catch (e) { } catch (e) {
console.error(`getAllKeys error`, e); console.error('getAllKeys error', e);
return []; return [];
} }
} }
@ -49,6 +49,6 @@ export async function multiRemove(keys: string[]): Promise<void> {
try { try {
await AsyncStorage.multiRemove(keys); await AsyncStorage.multiRemove(keys);
} catch (e) { } catch (e) {
console.error(`multiRemove error`, e); console.error('multiRemove error', e);
} }
} }

View File

@ -3,8 +3,8 @@ import { getItem, setItem } from './asyncstorage';
export default <T>(key: string, defaultValue: T) => { export default <T>(key: string, defaultValue: T) => {
return atomWithStorage<T>(key, defaultValue, { return atomWithStorage<T>(key, defaultValue, {
getItem: async () => await getItem(key) || defaultValue, getItem: async () => (await getItem(key)) || defaultValue,
setItem: setItem, setItem: setItem,
delayInit: true, delayInit: true,
}); });
} };

View File

@ -26,10 +26,13 @@ export async function getDownloadedSongs(): Promise<DownloadedSong[]> {
export async function setDownloadedSongs(items: DownloadedSong[]): Promise<void> { export async function setDownloadedSongs(items: DownloadedSong[]): Promise<void> {
await multiSet([ await multiSet([
[key.downloadedSongKeys, JSON.stringify(items.map(x => x.id))], [key.downloadedSongKeys, JSON.stringify(items.map(x => x.id))],
...items.map(x => [x.id, JSON.stringify({ ...items.map(x => [
name: x.name, x.id,
album: x.album, JSON.stringify({
artist: x.artist, name: x.name,
})]), album: x.album,
artist: x.artist,
}),
]),
]); ]);
} }

View File

@ -10,4 +10,4 @@ export default {
}, },
accent: '#b134db', accent: '#b134db',
accentLow: '#511c63', accentLow: '#511c63',
} };

View File

@ -1,7 +1,29 @@
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, GetArtistParams, GetCoverArtParams, GetIndexesParams, GetMusicDirectoryParams, StreamParams } from './params'; import {
import { GetAlbumList2Response, GetAlbumListResponse, GetAlbumResponse, GetArtistInfo2Response, GetArtistInfoResponse, GetArtistResponse, GetArtistsResponse, GetIndexesResponse, GetMusicDirectoryResponse, SubsonicResponse } from './responses'; GetAlbumList2Params,
GetAlbumListParams,
GetAlbumParams,
GetArtistInfo2Params,
GetArtistInfoParams,
GetArtistParams,
GetCoverArtParams,
GetIndexesParams,
GetMusicDirectoryParams,
StreamParams,
} from './params';
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';
@ -56,7 +78,7 @@ export class SubsonicApiClient {
address: string; address: string;
username: string; username: string;
private params: URLSearchParams private params: URLSearchParams;
constructor(server: Server) { constructor(server: Server) {
this.address = server.address; this.address = server.address;
@ -67,7 +89,7 @@ export class SubsonicApiClient {
this.params.append('t', server.token); this.params.append('t', server.token);
this.params.append('s', server.salt); this.params.append('s', server.salt);
this.params.append('v', '1.15.0'); this.params.append('v', '1.15.0');
this.params.append('c', 'subsonify-cool-unique-app-string') this.params.append('c', 'subsonify-cool-unique-app-string');
} }
private buildUrl(method: string, params?: { [key: string]: any }): string { private buildUrl(method: string, params?: { [key: string]: any }): string {
@ -124,18 +146,18 @@ export class SubsonicApiClient {
return params; return params;
} }
// //
// System // System
// //
async ping(): Promise<SubsonicResponse<null>> { async ping(): Promise<SubsonicResponse<null>> {
const xml = await this.apiGetXml('ping'); const xml = await this.apiGetXml('ping');
return new SubsonicResponse<null>(xml, null); return new SubsonicResponse<null>(xml, null);
} }
// //
// Browsing // Browsing
// //
async getArtists(): Promise<SubsonicResponse<GetArtistsResponse>> { async getArtists(): Promise<SubsonicResponse<GetArtistsResponse>> {
const xml = await this.apiGetXml('getArtists'); const xml = await this.apiGetXml('getArtists');
@ -172,9 +194,9 @@ export class SubsonicApiClient {
return new SubsonicResponse<GetArtistResponse>(xml, new GetArtistResponse(xml)); return new SubsonicResponse<GetArtistResponse>(xml, new GetArtistResponse(xml));
} }
// //
// Album/song lists // Album/song lists
// //
async getAlbumList(params: GetAlbumListParams): Promise<SubsonicResponse<GetAlbumListResponse>> { async getAlbumList(params: GetAlbumListParams): Promise<SubsonicResponse<GetAlbumListResponse>> {
const xml = await this.apiGetXml('getAlbumList', params); const xml = await this.apiGetXml('getAlbumList', params);
@ -186,9 +208,9 @@ export class SubsonicApiClient {
return new SubsonicResponse<GetAlbumList2Response>(xml, new GetAlbumList2Response(xml)); return new SubsonicResponse<GetAlbumList2Response>(xml, new GetAlbumList2Response(xml));
} }
// //
// Media retrieval // Media retrieval
// //
async getCoverArt(params: GetCoverArtParams): Promise<string> { async getCoverArt(params: GetCoverArtParams): Promise<string> {
const path = `${paths.songCache}/${params.id}`; const path = `${paths.songCache}/${params.id}`;

View File

@ -1,75 +1,84 @@
// //
// Browsing // Browsing
// //
export type GetIndexesParams = { export type GetIndexesParams = {
musicFolderId?: string; musicFolderId?: string;
ifModifiedSince?: number; ifModifiedSince?: number;
} };
export type GetArtistInfoParams = { export type GetArtistInfoParams = {
id: string; id: string;
count?: number; count?: number;
includeNotPresent?: boolean; includeNotPresent?: boolean;
} };
export type GetArtistInfo2Params = GetArtistInfoParams; export type GetArtistInfo2Params = GetArtistInfoParams;
export type GetMusicDirectoryParams = { export type GetMusicDirectoryParams = {
id: string; id: string;
} };
export type GetAlbumParams = { export type GetAlbumParams = {
id: string; id: string;
} };
export type GetArtistParams = { export type GetArtistParams = {
id: string; id: string;
} };
//
//
// Album/song lists // Album/song lists
// //
export type GetAlbumList2Type = 'random' | 'newest' | 'frequent' | 'recent' | 'starred' | 'alphabeticalByName' | 'alphabeticalByArtist'; export type GetAlbumList2Type =
| 'random'
| 'newest'
| 'frequent'
| 'recent'
| 'starred'
| 'alphabeticalByName'
| 'alphabeticalByArtist';
export type GetAlbumListType = GetAlbumList2Type | ' highest'; export type GetAlbumListType = GetAlbumList2Type | ' highest';
export type GetAlbumList2TypeByYear = { export type GetAlbumList2TypeByYear = {
type: 'byYear'; type: 'byYear';
fromYear: string; fromYear: string;
toYear: string; toYear: string;
} };
export type GetAlbumList2TypeByGenre = { export type GetAlbumList2TypeByGenre = {
type: 'byGenre'; type: 'byGenre';
genre: string; genre: string;
} };
export type GetAlbumList2Params = { export type GetAlbumList2Params =
type: GetAlbumList2Type; | {
size?: number; type: GetAlbumList2Type;
offset?: number; size?: number;
fromYear?: string; offset?: number;
toYear?: string; fromYear?: string;
genre?: string; toYear?: string;
musicFolderId?: string; genre?: string;
} | GetAlbumList2TypeByYear | GetAlbumList2TypeByGenre; musicFolderId?: string;
}
| GetAlbumList2TypeByYear
| GetAlbumList2TypeByGenre;
export type GetAlbumListParams = GetAlbumList2Params; export type GetAlbumListParams = GetAlbumList2Params;
// //
// Media retrieval // Media retrieval
// //
export type GetCoverArtParams = { export type GetCoverArtParams = {
id: string; id: string;
size?: string; size?: string;
} };
export type StreamParams = { export type StreamParams = {
id: string; id: string;
maxBitRate?: number; maxBitRate?: number;
format?: string; format?: string;
estimateContentLength?: boolean; estimateContentLength?: boolean;
} };

View File

@ -1,4 +1,12 @@
import { AlbumID3Element, ArtistElement, ArtistID3Element, ArtistInfo2Element, ArtistInfoElement, BaseArtistElement, BaseArtistInfoElement, ChildElement, DirectoryElement } from "./elements"; import {
AlbumID3Element,
ArtistElement,
ArtistID3Element,
ArtistInfo2Element,
ArtistInfoElement,
ChildElement,
DirectoryElement,
} from './elements';
export type ResponseStatus = 'ok' | 'failed'; export type ResponseStatus = 'ok' | 'failed';
@ -14,9 +22,9 @@ export class SubsonicResponse<T> {
} }
} }
// //
// Browsing // Browsing
// //
export class GetArtistsResponse { export class GetArtistsResponse {
ignoredArticles: string; ignoredArticles: string;
@ -108,9 +116,9 @@ export class GetAlbumResponse {
} }
} }
// //
// Album/song lists // Album/song lists
// //
class BaseGetAlbumListResponse<T> { class BaseGetAlbumListResponse<T> {
albums: T[] = []; albums: T[] = [];

View File

@ -1,27 +1,26 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Basic Options */ /* Basic Options */
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"lib": ["es2017"], /* Specify library files to be included in the compilation. */ "lib": ["es2017"] /* Specify library files to be included in the compilation. */,
"allowJs": true, /* Allow javascript files to be compiled. */ "allowJs": true /* Allow javascript files to be compiled. */,
// "checkJs": true, /* Report errors in .js files. */ // "checkJs": true, /* Report errors in .js files. */
"jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "jsx": "react-native" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
// "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */ // "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */ // "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "removeComments": true, /* Do not emit comments to output. */ // "removeComments": true, /* Do not emit comments to output. */
"noEmit": true, /* Do not emit outputs. */ "noEmit": true /* Do not emit outputs. */,
// "incremental": true, /* Enable incremental compilation */ // "incremental": true, /* Enable incremental compilation */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
"isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */,
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */ "strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */ // "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
@ -36,16 +35,16 @@
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */ // "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */ // "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
"skipLibCheck": false /* Skip type checking of declaration files. */ "skipLibCheck": false /* Skip type checking of declaration files. */
/* Source Map Options */ /* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
@ -57,7 +56,5 @@
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}, },
"exclude": [ "exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js"]
"node_modules", "babel.config.js", "metro.config.js", "jest.config.js"
]
} }