mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
eslint/prettier
This commit is contained in:
parent
ee7658ccf8
commit
4e98318cd9
@ -1,4 +1,8 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native-community',
|
||||
rules: {
|
||||
'react-native/no-inline-styles': 0,
|
||||
radix: 0,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
module.exports = {
|
||||
bracketSpacing: false,
|
||||
bracketSpacing: true,
|
||||
jsxBracketSameLine: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
printWidth: 120,
|
||||
};
|
||||
|
||||
@ -1,38 +1,38 @@
|
||||
{
|
||||
"images" : [
|
||||
"images": [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
"idiom": "iphone",
|
||||
"size": "29x29",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "3x"
|
||||
"idiom": "iphone",
|
||||
"size": "29x29",
|
||||
"scale": "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
"idiom": "iphone",
|
||||
"size": "40x40",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "3x"
|
||||
"idiom": "iphone",
|
||||
"size": "40x40",
|
||||
"scale": "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "2x"
|
||||
"idiom": "iphone",
|
||||
"size": "60x60",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "3x"
|
||||
"idiom": "iphone",
|
||||
"size": "60x60",
|
||||
"scale": "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,5 +3,5 @@ module.exports = {
|
||||
ios: {},
|
||||
android: {},
|
||||
},
|
||||
assets: ['./assets/fonts']
|
||||
assets: ['./assets/fonts'],
|
||||
};
|
||||
|
||||
@ -4,31 +4,26 @@ import { useAtomValue } from 'jotai/utils';
|
||||
import { Artist } from '../models/music';
|
||||
import { artistsAtom } from '../state/music';
|
||||
|
||||
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
|
||||
const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => (
|
||||
<View>
|
||||
<Text>{item.id}</Text>
|
||||
<Text style={{
|
||||
fontSize: 60,
|
||||
paddingBottom: 400,
|
||||
}}>{item.name}</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 60,
|
||||
paddingBottom: 400,
|
||||
}}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const List = () => {
|
||||
const artists = useAtomValue(artistsAtom);
|
||||
|
||||
const renderItem: React.FC<{ item: Artist }> = ({ item }) => (
|
||||
<ArtistItem item={item} />
|
||||
);
|
||||
const renderItem: React.FC<{ item: Artist }> = ({ item }) => <ArtistItem item={item} />;
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={artists}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <FlatList data={artists} renderItem={renderItem} keyExtractor={item => item.id} />;
|
||||
};
|
||||
|
||||
const ArtistsList = () => (
|
||||
<View>
|
||||
@ -36,6 +31,6 @@ const ArtistsList = () => (
|
||||
<List />
|
||||
</React.Suspense>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
|
||||
export default ArtistsList;
|
||||
|
||||
@ -3,14 +3,14 @@ import { Image, ImageSourcePropType } from 'react-native';
|
||||
import colors from '../styles/colors';
|
||||
|
||||
export type FocusableIconProps = {
|
||||
focused: boolean,
|
||||
focused: boolean;
|
||||
source: ImageSourcePropType;
|
||||
focusedSource?: ImageSourcePropType;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
const FocusableIcon: React.FC<FocusableIconProps> = (props) => {
|
||||
const FocusableIcon: React.FC<FocusableIconProps> = props => {
|
||||
props.focusedSource = props.focusedSource || props.source;
|
||||
props.width = props.width || 26;
|
||||
props.height = props.height || 26;
|
||||
@ -25,6 +25,6 @@ const FocusableIcon: React.FC<FocusableIconProps> = (props) => {
|
||||
source={props.focused ? props.focusedSource : props.source}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default FocusableIcon;
|
||||
|
||||
@ -1,96 +1,103 @@
|
||||
import React from 'react';
|
||||
import { Image, StyleSheet, Text, View } from 'react-native';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
const NowPlayingLayout = () => {
|
||||
return (
|
||||
<View style={{ // background
|
||||
backgroundColor: 'darkblue',
|
||||
flex: 1,
|
||||
}}>
|
||||
|
||||
|
||||
{/* top bar */}
|
||||
<View style={{
|
||||
height: 70,
|
||||
flexDirection: 'row',
|
||||
<View
|
||||
style={{
|
||||
// background
|
||||
backgroundColor: 'darkblue',
|
||||
flex: 1,
|
||||
}}>
|
||||
<View style={{ width: 70, height: 70, backgroundColor: 'grey' }}></View>
|
||||
<View style={{ flex: 1, alignItems: 'center', height: 70, }}>
|
||||
<View style={{ flex: 1 }}></View>
|
||||
{/* top bar */}
|
||||
<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}>Songs</Text>
|
||||
<View style={{ flex: 1 }}></View>
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
<View style={{ width: 70, height: 70, backgroundColor: 'grey' }}></View>
|
||||
<View style={{ width: 70, height: 70, backgroundColor: 'grey' }} />
|
||||
</View>
|
||||
|
||||
|
||||
{/* album art */}
|
||||
<View style={{
|
||||
flex: 5,
|
||||
// backgroundColor: 'darkorange',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<View style={{ flex: 1 }}></View>
|
||||
<View style={{
|
||||
width: 320,
|
||||
height: 320,
|
||||
backgroundColor: 'grey',
|
||||
}}></View>
|
||||
<View style={{ flex: 1 }}></View>
|
||||
<View
|
||||
style={{
|
||||
flex: 5,
|
||||
// backgroundColor: 'darkorange',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<View style={{ flex: 1 }} />
|
||||
<View
|
||||
style={{
|
||||
width: 320,
|
||||
height: 320,
|
||||
backgroundColor: 'grey',
|
||||
}}
|
||||
/>
|
||||
<View style={{ flex: 1 }} />
|
||||
</View>
|
||||
|
||||
|
||||
{/* song/album/artist title */}
|
||||
<View style={{
|
||||
flex: 1,
|
||||
// backgroundColor: 'green',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<Text style={{ ...styles.text, fontSize: 26, }}>Name of the Song</Text>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
// backgroundColor: 'green',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<Text style={{ ...styles.text, fontSize: 26 }}>Name of the Song</Text>
|
||||
<Text style={{ ...styles.text, fontSize: 20, fontWeight: 'normal' }}>Cool Artist</Text>
|
||||
</View>
|
||||
|
||||
|
||||
{/* seek bar */}
|
||||
<View style={{
|
||||
flex: 0.7,
|
||||
// backgroundColor: 'red',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<View style={{ width: 20 }}></View>
|
||||
<View style={{
|
||||
flex: 1,
|
||||
<View
|
||||
style={{
|
||||
flex: 0.7,
|
||||
// backgroundColor: 'red',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<View style={{ width: 20 }} />
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
}}>
|
||||
<View>
|
||||
<View style={{
|
||||
backgroundColor: 'grey',
|
||||
height: 3,
|
||||
marginBottom: 3,
|
||||
// flex: 1,
|
||||
}} />
|
||||
<View style={{
|
||||
flexDirection: 'row'
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: 'grey',
|
||||
height: 3,
|
||||
marginBottom: 3,
|
||||
// flex: 1,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<Text style={{ ...styles.text, fontWeight: 'normal' }}>00:00</Text>
|
||||
<View style={{ flex: 1 }} />
|
||||
<Text style={{ ...styles.text, fontWeight: 'normal' }}>00:00</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ width: 20 }}></View>
|
||||
<View style={{ width: 20 }} />
|
||||
</View>
|
||||
|
||||
|
||||
{/* main player controls */}
|
||||
<View style={{
|
||||
height: 90,
|
||||
// backgroundColor: 'darkorange',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
height: 90,
|
||||
// backgroundColor: 'darkorange',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<View style={{ width: 14 }} />
|
||||
<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>
|
||||
|
||||
|
||||
{/* extra controls */}
|
||||
<View style={{
|
||||
flex: 1,
|
||||
// backgroundColor: 'green',
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
// backgroundColor: 'green',
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<View style={{ width: 14 }} />
|
||||
<View style={{ width: 60, height: 60, backgroundColor: 'grey' }} />
|
||||
<View style={{ flex: 1 }} />
|
||||
@ -115,7 +122,7 @@ const NowPlayingLayout = () => {
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
text: {
|
||||
|
||||
@ -14,21 +14,15 @@ const TestControls = () => {
|
||||
const removeAllKeys = async () => {
|
||||
const allKeys = await getAllKeys();
|
||||
await multiRemove(allKeys);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Button
|
||||
title='Remove all keys'
|
||||
onPress={removeAllKeys}
|
||||
/>
|
||||
<Button
|
||||
title='Now Playing'
|
||||
onPress={() => navigation.navigate('Now Playing')}
|
||||
/>
|
||||
<Button title="Remove all keys" onPress={removeAllKeys} />
|
||||
<Button title="Now Playing" onPress={() => navigation.navigate('Now Playing')} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const ServerSettingsView = () => {
|
||||
const [appSettings, setAppSettings] = useAtom(appSettingsAtom);
|
||||
@ -47,7 +41,9 @@ const ServerSettingsView = () => {
|
||||
servers: [
|
||||
...appSettings.servers,
|
||||
{
|
||||
id, salt, address,
|
||||
id,
|
||||
salt,
|
||||
address,
|
||||
username: 'guest',
|
||||
token: md5('guest' + salt),
|
||||
},
|
||||
@ -58,10 +54,7 @@ const ServerSettingsView = () => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Button
|
||||
title='Add default server'
|
||||
onPress={bootstrapServer}
|
||||
/>
|
||||
<Button title="Add default server" onPress={bootstrapServer} />
|
||||
{appSettings.servers.map(s => (
|
||||
<View key={s.id}>
|
||||
<Text style={text.paragraph}>{s.address}</Text>
|
||||
@ -70,7 +63,7 @@ const ServerSettingsView = () => {
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const SettingsView = () => (
|
||||
<View>
|
||||
@ -79,6 +72,6 @@ const SettingsView = () => (
|
||||
<ServerSettingsView />
|
||||
</React.Suspense>
|
||||
</View>
|
||||
)
|
||||
);
|
||||
|
||||
export default SettingsView;
|
||||
|
||||
@ -37,12 +37,7 @@ const SplashPage: React.FC<{}> = ({ children }) => {
|
||||
Capability.SkipToNext,
|
||||
Capability.SkipToPrevious,
|
||||
],
|
||||
compactCapabilities: [
|
||||
Capability.Play,
|
||||
Capability.Pause,
|
||||
Capability.SkipToNext,
|
||||
Capability.SkipToPrevious,
|
||||
],
|
||||
compactCapabilities: [Capability.Play, Capability.Pause, Capability.SkipToNext, Capability.SkipToPrevious],
|
||||
});
|
||||
|
||||
const castlevania: Track = {
|
||||
@ -54,28 +49,24 @@ const SplashPage: React.FC<{}> = ({ children }) => {
|
||||
artwork: 'https://webgames.host/uploads/2017/03/castlevania-3-draculas-curse.jpg',
|
||||
genre: 'BGM',
|
||||
date: new Date(1989, 1).toISOString(),
|
||||
}
|
||||
};
|
||||
|
||||
await TrackPlayer.add([castlevania]);
|
||||
// TrackPlayer.play();
|
||||
}
|
||||
};
|
||||
|
||||
const promise = Promise.all([
|
||||
prepare(), minSplashTime,
|
||||
]);
|
||||
const promise = Promise.all([prepare(), minSplashTime]);
|
||||
|
||||
useEffect(() => {
|
||||
promise.then(() => {
|
||||
setReady(true);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
if (!ready) {
|
||||
return <Text>Loading THE GOOD SHIT...</Text>
|
||||
return <Text>Loading THE GOOD SHIT...</Text>;
|
||||
}
|
||||
return (
|
||||
<View style={{ flex: 1 }}>{children}</View>
|
||||
);
|
||||
}
|
||||
return <View style={{ flex: 1 }}>{children}</View>;
|
||||
};
|
||||
|
||||
export default SplashPage;
|
||||
|
||||
@ -17,9 +17,7 @@ const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
|
||||
const albumArt = useAtomValue(albumArtAtomFamily(id));
|
||||
|
||||
const Placeholder = () => (
|
||||
<LinearGradient
|
||||
colors={[colors.accent, colors.accentLow]}
|
||||
>
|
||||
<LinearGradient colors={[colors.accent, colors.accentLow]}>
|
||||
<FastImage
|
||||
source={require('../../../res/record.png')}
|
||||
style={{ height, width }}
|
||||
@ -36,21 +34,23 @@ const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
|
||||
coverArtUri={width > 128 ? albumArt?.uri : albumArt?.thumbUri}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const AlbumArtFallback: React.FC<AlbumArtProps> = ({ height, width }) => (
|
||||
<View style={{
|
||||
height, width,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<ActivityIndicator size='small' color={colors.accent} />
|
||||
<View
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<ActivityIndicator size="small" color={colors.accent} />
|
||||
</View>
|
||||
);
|
||||
|
||||
const AlbumArtLoader: React.FC<AlbumArtProps> = (props) => (
|
||||
<React.Suspense fallback={<AlbumArtFallback { ...props } />}>
|
||||
<AlbumArt { ...props } />
|
||||
const AlbumArtLoader: React.FC<AlbumArtProps> = props => (
|
||||
<React.Suspense fallback={<AlbumArtFallback {...props} />}>
|
||||
<AlbumArt {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useAtomValue } from 'jotai/utils';
|
||||
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 { albumAtomFamily } from '../../state/music';
|
||||
import colors from '../../styles/colors';
|
||||
@ -13,7 +21,7 @@ import GradientScrollView from './GradientScrollView';
|
||||
|
||||
const SongItem: React.FC<{
|
||||
id: string;
|
||||
title: string
|
||||
title: string;
|
||||
artist?: string;
|
||||
onPress: (event: GestureResponderEvent) => void;
|
||||
}> = ({ id, title, artist, onPress }) => {
|
||||
@ -28,8 +36,7 @@ const SongItem: React.FC<{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
onPressIn={() => setOpacity(0.6)}
|
||||
@ -38,19 +45,22 @@ const SongItem: React.FC<{
|
||||
style={{
|
||||
flex: 1,
|
||||
opacity,
|
||||
}}
|
||||
>
|
||||
<Text style={{
|
||||
...text.songListTitle,
|
||||
color: currentTrackId === id ? colors.accent : colors.text.primary,
|
||||
}}>{title}</Text>
|
||||
}}>
|
||||
<Text
|
||||
style={{
|
||||
...text.songListTitle,
|
||||
color: currentTrackId === id ? colors.accent : colors.text.primary,
|
||||
}}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={text.songListSubtitle}>{artist}</Text>
|
||||
</Pressable>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: 10,
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: 10,
|
||||
}}>
|
||||
{/* <Text style={text.songListSubtitle}>{secondsToTime(duration || 0)}</Text> */}
|
||||
<Image
|
||||
source={require('../../../res/star.png')}
|
||||
@ -74,10 +84,10 @@ const SongItem: React.FC<{
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const AlbumDetails: React.FC<{
|
||||
id: string,
|
||||
id: string;
|
||||
}> = ({ id }) => {
|
||||
const album = useAtomValue(albumAtomFamily(id));
|
||||
const layout = useWindowDimensions();
|
||||
@ -86,9 +96,7 @@ const AlbumDetails: React.FC<{
|
||||
const coverSize = layout.width - layout.width / 2.5;
|
||||
|
||||
if (!album) {
|
||||
return (
|
||||
<Text style={text.paragraph}>No Album</Text>
|
||||
);
|
||||
return <Text style={text.paragraph}>No Album</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -99,32 +107,36 @@ const AlbumDetails: React.FC<{
|
||||
contentContainerStyle={{
|
||||
alignItems: 'center',
|
||||
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
|
||||
title='Play Album'
|
||||
onPress={() => setQueue(album.songs, album.songs[0].id)}
|
||||
/>
|
||||
<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 title="Play Album" onPress={() => setQueue(album.songs, album.songs[0].id)} />
|
||||
{/* <View style={{ width: 6, }}></View>
|
||||
<Button
|
||||
title='S'
|
||||
@ -132,27 +144,27 @@ const AlbumDetails: React.FC<{
|
||||
/> */}
|
||||
</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 => (
|
||||
<SongItem
|
||||
key={s.id}
|
||||
id={s.id}
|
||||
title={s.title}
|
||||
artist={s.artist}
|
||||
onPress={() => setQueue(album.songs, s.id)}
|
||||
/>
|
||||
))}
|
||||
<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 => (
|
||||
<SongItem
|
||||
key={s.id}
|
||||
id={s.id}
|
||||
title={s.title}
|
||||
artist={s.artist}
|
||||
onPress={() => setQueue(album.songs, s.id)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
</GradientScrollView>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const AlbumViewFallback = () => {
|
||||
const layout = useWindowDimensions();
|
||||
@ -160,17 +172,18 @@ const AlbumViewFallback = () => {
|
||||
const coverSize = layout.width - layout.width / 2.5;
|
||||
|
||||
return (
|
||||
<GradientBackground style={{
|
||||
alignItems: 'center',
|
||||
paddingTop: coverSize / 8 + coverSize / 2 - 18,
|
||||
}}>
|
||||
<ActivityIndicator size='large' color={colors.accent} />
|
||||
<GradientBackground
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
paddingTop: coverSize / 8 + coverSize / 2 - 18,
|
||||
}}>
|
||||
<ActivityIndicator size="large" color={colors.accent} />
|
||||
</GradientBackground>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const AlbumView: React.FC<{
|
||||
id: string,
|
||||
id: string;
|
||||
title: string;
|
||||
}> = ({ id, title }) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { useAtom } from 'jotai';
|
||||
import { useAtomValue } from 'jotai/utils';
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, View } from 'react-native';
|
||||
@ -11,7 +10,7 @@ import CoverArt from './CoverArt';
|
||||
interface ArtistArtSizeProps {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ArtistArtXUpProps extends ArtistArtSizeProps {
|
||||
coverArtUris: string[];
|
||||
@ -25,11 +24,11 @@ const PlaceholderContainer: React.FC<ArtistArtSizeProps> = ({ height, width, chi
|
||||
<LinearGradient
|
||||
colors={[colors.accent, colors.accentLow]}
|
||||
style={{
|
||||
height, width,
|
||||
height,
|
||||
width,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{children}
|
||||
</LinearGradient>
|
||||
);
|
||||
@ -123,11 +122,7 @@ const TwoUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) =>
|
||||
const OneUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
||||
return (
|
||||
<PlaceholderContainer height={height} width={width}>
|
||||
<FastImage
|
||||
source={{ uri: coverArtUris[0] }}
|
||||
style={{ height, width }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
<FastImage source={{ uri: coverArtUris[0] }} style={{ height, width }} resizeMode={FastImage.resizeMode.cover} />
|
||||
</PlaceholderContainer>
|
||||
);
|
||||
};
|
||||
@ -135,14 +130,14 @@ const OneUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) =>
|
||||
const NoneUp: React.FC<ArtistArtSizeProps> = ({ height, width }) => {
|
||||
return (
|
||||
<PlaceholderContainer height={height} width={width}>
|
||||
<FastImage
|
||||
source={require('../../../res/mic_on-fill.png')}
|
||||
style={{
|
||||
height: height - height / 4,
|
||||
width: width - width / 4,
|
||||
}}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
<FastImage
|
||||
source={require('../../../res/mic_on-fill.png')}
|
||||
style={{
|
||||
height: height - height / 4,
|
||||
width: width - width / 4,
|
||||
}}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
</PlaceholderContainer>
|
||||
);
|
||||
};
|
||||
@ -172,36 +167,34 @@ const ArtistArt: React.FC<ArtistArtProps> = ({ id, height, width }) => {
|
||||
}
|
||||
|
||||
return none;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{
|
||||
borderRadius: height / 2,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<CoverArt
|
||||
PlaceholderComponent={Placeholder}
|
||||
height={height}
|
||||
width={width}
|
||||
coverArtUri={artistArt?.uri}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: height / 2,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<CoverArt PlaceholderComponent={Placeholder} height={height} width={width} coverArtUri={artistArt?.uri} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const ArtistArtFallback: React.FC<ArtistArtProps> = ({ height, width }) => (
|
||||
<View style={{
|
||||
height, width,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<ActivityIndicator size='small' color={colors.accent} />
|
||||
<View
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<ActivityIndicator size="small" color={colors.accent} />
|
||||
</View>
|
||||
);
|
||||
|
||||
const ArtistArtLoader: React.FC<ArtistArtProps> = (props) => (
|
||||
<React.Suspense fallback={<ArtistArtFallback { ...props } />}>
|
||||
<ArtistArt { ...props } />
|
||||
const ArtistArtLoader: React.FC<ArtistArtProps> = props => (
|
||||
<React.Suspense fallback={<ArtistArtFallback {...props} />}>
|
||||
<ArtistArt {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
|
||||
|
||||
@ -22,16 +22,15 @@ const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
|
||||
contentContainerStyle={{
|
||||
alignItems: 'center',
|
||||
// paddingTop: coverSize / 8,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Text style={text.paragraph}>{artist.name}</Text>
|
||||
<ArtistArt id={artist.id} height={200} width={200} />
|
||||
</GradientScrollView>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const ArtistView: React.FC<{
|
||||
id: string,
|
||||
id: string;
|
||||
title: string;
|
||||
}> = ({ id, title }) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
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 textStyles from '../../styles/text';
|
||||
import colors from '../../styles/colors';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
const icons: {[key: string]: any} = {
|
||||
const icons: { [key: string]: any } = {
|
||||
home: {
|
||||
regular: require('../../../res/home.png'),
|
||||
fill: require('../../../res/home-fill.png'),
|
||||
@ -23,14 +22,14 @@ const icons: {[key: string]: any} = {
|
||||
regular: require('../../../res/settings.png'),
|
||||
fill: require('../../../res/settings-fill.png'),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const BottomTabButton: React.FC<{
|
||||
routeKey: string;
|
||||
label: string;
|
||||
name: string;
|
||||
isFocused: boolean;
|
||||
img: { regular: number, fill: number };
|
||||
img: { regular: number; fill: number };
|
||||
navigation: any;
|
||||
}> = ({ routeKey, label, name, isFocused, img, navigation }) => {
|
||||
const [opacity, setOpacity] = useState(1);
|
||||
@ -56,8 +55,7 @@ const BottomTabButton: React.FC<{
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
opacity,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<FastImage
|
||||
source={isFocused ? img.fill : img.regular}
|
||||
style={{
|
||||
@ -66,47 +64,51 @@ const BottomTabButton: React.FC<{
|
||||
}}
|
||||
tintColor={isFocused ? colors.text.primary : colors.text.secondary}
|
||||
/>
|
||||
<Text style={{
|
||||
...textStyles.xsmall,
|
||||
color: isFocused ? colors.text.primary : colors.text.secondary,
|
||||
}}>
|
||||
<Text
|
||||
style={{
|
||||
...textStyles.xsmall,
|
||||
color: isFocused ? colors.text.primary : colors.text.secondary,
|
||||
}}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigation }) => {
|
||||
return (
|
||||
<View style={{
|
||||
height: 54,
|
||||
backgroundColor: colors.gradient.high,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
paddingHorizontal: 28,
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
height: 54,
|
||||
backgroundColor: colors.gradient.high,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
paddingHorizontal: 28,
|
||||
}}>
|
||||
{state.routes.map((route, index) => {
|
||||
const { options } = descriptors[route.key] as any;
|
||||
const label =
|
||||
options.tabBarLabel !== undefined
|
||||
? options.tabBarLabel as string
|
||||
? (options.tabBarLabel as string)
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: route.name;
|
||||
? options.title
|
||||
: route.name;
|
||||
|
||||
return <BottomTabButton
|
||||
key={route.key}
|
||||
routeKey={route.key}
|
||||
label={label}
|
||||
name={route.name}
|
||||
isFocused={state.index === index}
|
||||
img={icons[options.icon]}
|
||||
navigation={navigation}
|
||||
/>;
|
||||
return (
|
||||
<BottomTabButton
|
||||
key={route.key}
|
||||
routeKey={route.key}
|
||||
label={label}
|
||||
name={route.name}
|
||||
isFocused={state.index === index}
|
||||
img={icons[options.icon]}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default BottomTabBar;
|
||||
|
||||
@ -22,11 +22,10 @@ const Button: React.FC<{
|
||||
justifyContent: 'center',
|
||||
borderRadius: 1000,
|
||||
opacity,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Text style={{ ...text.button }}>{title}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Button;
|
||||
|
||||
@ -4,10 +4,10 @@ import FastImage from 'react-native-fast-image';
|
||||
import colors from '../../styles/colors';
|
||||
|
||||
const CoverArt: React.FC<{
|
||||
PlaceholderComponent: () => JSX.Element,
|
||||
height: number,
|
||||
width: number,
|
||||
coverArtUri?: string
|
||||
PlaceholderComponent: () => JSX.Element;
|
||||
height: number;
|
||||
width: number;
|
||||
coverArtUri?: string;
|
||||
}> = ({ PlaceholderComponent, height, width, coverArtUri }) => {
|
||||
const [placeholderVisible, setPlaceholderVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -16,14 +16,15 @@ const CoverArt: React.FC<{
|
||||
const halfIndicatorHeight = indicatorSize === 'large' ? 18 : 10;
|
||||
|
||||
const Placeholder: React.FC<{ visible: boolean }> = ({ visible }) => (
|
||||
<View style={{
|
||||
opacity: visible ? 100 : 0,
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
opacity: visible ? 100 : 0,
|
||||
}}>
|
||||
<PlaceholderComponent />
|
||||
</View>
|
||||
);
|
||||
|
||||
const CoverArt = () => (
|
||||
const Art = () => (
|
||||
<>
|
||||
<Placeholder visible={placeholderVisible} />
|
||||
<ActivityIndicator
|
||||
@ -37,7 +38,8 @@ const CoverArt: React.FC<{
|
||||
<FastImage
|
||||
source={{ uri: coverArtUri, priority: 'high' }}
|
||||
style={{
|
||||
height, width,
|
||||
height,
|
||||
width,
|
||||
marginTop: -height - halfIndicatorHeight * 2,
|
||||
}}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
@ -50,11 +52,7 @@ const CoverArt: React.FC<{
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={{ height, width }}>
|
||||
{!coverArtUri ? <Placeholder visible={true} /> : <CoverArt />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return <View style={{ height, width }}>{!coverArtUri ? <Placeholder visible={true} /> : <Art />}</View>;
|
||||
};
|
||||
|
||||
export default React.memo(CoverArt);
|
||||
|
||||
@ -14,14 +14,13 @@ const GradientBackground: React.FC<{
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={[colors.gradient.high, colors.gradient.low]}
|
||||
locations={[0.01,0.7]}
|
||||
locations={[0.01, 0.7]}
|
||||
style={{
|
||||
...style,
|
||||
width: width || '100%',
|
||||
height: height || layout.height,
|
||||
position: position || 'absolute',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{children}
|
||||
</LinearGradient>
|
||||
);
|
||||
|
||||
@ -7,13 +7,13 @@ function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) {
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
{ ...props }
|
||||
ListHeaderComponent={() => <GradientBackground position='relative' />}
|
||||
{...props}
|
||||
ListHeaderComponent={() => <GradientBackground position="relative" />}
|
||||
ListHeaderComponentStyle={{
|
||||
marginBottom: -layout.height,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default GradientFlatList;
|
||||
|
||||
@ -2,11 +2,8 @@ import React from 'react';
|
||||
import { ScrollView, ScrollViewProps } from 'react-native';
|
||||
import GradientBackground from './GradientBackground';
|
||||
|
||||
const GradientScrollView: React.FC<ScrollViewProps> = (props) => (
|
||||
<ScrollView
|
||||
overScrollMode='never'
|
||||
{...props}
|
||||
>
|
||||
const GradientScrollView: React.FC<ScrollViewProps> = props => (
|
||||
<ScrollView overScrollMode="never" {...props}>
|
||||
<GradientBackground />
|
||||
{props.children}
|
||||
</ScrollView>
|
||||
|
||||
@ -5,11 +5,10 @@ import colors from '../../styles/colors';
|
||||
const TopTabContainer: React.FC<{}> = ({ children }) => (
|
||||
<LinearGradient
|
||||
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={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{children}
|
||||
</LinearGradient>
|
||||
);
|
||||
|
||||
@ -12,7 +12,7 @@ const AlbumItem: React.FC<{
|
||||
id: string;
|
||||
name: string;
|
||||
artist?: string;
|
||||
}> = ({ id, name, artist, }) => {
|
||||
}> = ({ id, name, artist }) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const size = 125;
|
||||
@ -22,34 +22,30 @@ const AlbumItem: React.FC<{
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
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} />
|
||||
<View style={{
|
||||
flex: 1,
|
||||
width: size,
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
width: size,
|
||||
}}>
|
||||
<Text
|
||||
style={{
|
||||
...textStyles.itemTitle,
|
||||
marginTop: 4,
|
||||
}}
|
||||
numberOfLines={2}
|
||||
>
|
||||
numberOfLines={2}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text
|
||||
style={{ ...textStyles.itemSubtitle }}
|
||||
numberOfLines={1}
|
||||
>
|
||||
<Text style={{ ...textStyles.itemSubtitle }} numberOfLines={1}>
|
||||
{artist}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
};
|
||||
const MemoAlbumItem = React.memo(AlbumItem);
|
||||
|
||||
const AlbumListRenderItem: React.FC<{ item: Album }> = ({ item }) => (
|
||||
@ -79,11 +75,11 @@ const AlbumsList = () => {
|
||||
removeClippedSubviews={true}
|
||||
refreshing={updating}
|
||||
onRefresh={updateAlbums}
|
||||
overScrollMode='never'
|
||||
overScrollMode="never"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const AlbumsTab = () => (
|
||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||
|
||||
@ -2,9 +2,9 @@ import { useNavigation } from '@react-navigation/native';
|
||||
import { useAtomValue } from 'jotai/utils';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Pressable } from 'react-native';
|
||||
import { Image, Text, View } from 'react-native';
|
||||
import { Text } from 'react-native';
|
||||
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 ArtistArt from '../common/ArtistArt';
|
||||
import GradientFlatList from '../common/GradientFlatList';
|
||||
@ -20,20 +20,22 @@ const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => {
|
||||
marginVertical: 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} />
|
||||
<Text style={{
|
||||
...textStyles.paragraph,
|
||||
marginLeft: 12,
|
||||
}}>{item.name}</Text>
|
||||
<Text
|
||||
style={{
|
||||
...textStyles.paragraph,
|
||||
marginLeft: 12,
|
||||
}}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
const ArtistItemLoader: React.FC<{ item: Artist }> = (props) => (
|
||||
const ArtistItemLoader: React.FC<{ item: Artist }> = props => (
|
||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||
<ArtistItem { ...props } />
|
||||
<ArtistItem {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
|
||||
@ -48,9 +50,7 @@ const ArtistsList = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const renderItem: React.FC<{ item: Artist }> = ({ item }) => (
|
||||
<ArtistItemLoader item={item} />
|
||||
);
|
||||
const renderItem: React.FC<{ item: Artist }> = ({ item }) => <ArtistItemLoader item={item} />;
|
||||
|
||||
return (
|
||||
<GradientFlatList
|
||||
@ -59,13 +59,11 @@ const ArtistsList = () => {
|
||||
keyExtractor={item => item.id}
|
||||
onRefresh={updateArtists}
|
||||
refreshing={updating}
|
||||
overScrollMode='never'
|
||||
overScrollMode="never"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const ArtistsTab = () => (
|
||||
<ArtistsList />
|
||||
);
|
||||
const ArtistsTab = () => <ArtistsList />;
|
||||
|
||||
export default ArtistsTab;
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import GradientBackground from '../common/GradientBackground';
|
||||
|
||||
const PlaylistsTab = () => (
|
||||
<GradientBackground />
|
||||
);
|
||||
const PlaylistsTab = () => <GradientBackground />;
|
||||
|
||||
export default PlaylistsTab;
|
||||
|
||||
@ -10,31 +10,13 @@ const Tab = createBottomTabNavigator();
|
||||
|
||||
const BottomTabNavigator = () => {
|
||||
return (
|
||||
<Tab.Navigator
|
||||
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
|
||||
name='Search'
|
||||
component={NowPlayingLayout}
|
||||
options={{ icon: 'search' } as any}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name='Settings'
|
||||
component={SettingsView}
|
||||
options={{ icon: 'settings' } as any}
|
||||
/>
|
||||
<Tab.Navigator 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 name="Search" component={NowPlayingLayout} options={{ icon: 'search' } as any} />
|
||||
<Tab.Screen name="Settings" component={SettingsView} options={{ icon: 'settings' } as any} />
|
||||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default BottomTabNavigator;
|
||||
|
||||
@ -15,48 +15,40 @@ import ArtistView from '../common/ArtistView';
|
||||
const Tab = createMaterialTopTabNavigator();
|
||||
|
||||
const LibraryTopTabNavigator = () => (
|
||||
<Tab.Navigator tabBarOptions={{
|
||||
style: {
|
||||
height: 48,
|
||||
backgroundColor: colors.gradient.high,
|
||||
elevation: 0,
|
||||
},
|
||||
labelStyle: {
|
||||
...text.header,
|
||||
textTransform: null as any,
|
||||
marginTop: 0,
|
||||
marginHorizontal: 2,
|
||||
},
|
||||
indicatorStyle: {
|
||||
backgroundColor: colors.accent,
|
||||
},
|
||||
}}>
|
||||
<Tab.Screen
|
||||
name='Albums'
|
||||
component={AlbumsTab}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name='Artists'
|
||||
component={ArtistsTab}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name='Playlists'
|
||||
component={PlaylistsTab}
|
||||
/>
|
||||
<Tab.Navigator
|
||||
tabBarOptions={{
|
||||
style: {
|
||||
height: 48,
|
||||
backgroundColor: colors.gradient.high,
|
||||
elevation: 0,
|
||||
},
|
||||
labelStyle: {
|
||||
...text.header,
|
||||
textTransform: null as any,
|
||||
marginTop: 0,
|
||||
marginHorizontal: 2,
|
||||
},
|
||||
indicatorStyle: {
|
||||
backgroundColor: colors.accent,
|
||||
},
|
||||
}}>
|
||||
<Tab.Screen name="Albums" component={AlbumsTab} />
|
||||
<Tab.Screen name="Artists" component={ArtistsTab} />
|
||||
<Tab.Screen name="Playlists" component={PlaylistsTab} />
|
||||
</Tab.Navigator>
|
||||
);
|
||||
|
||||
type LibraryStackParamList = {
|
||||
LibraryTopTabs: undefined,
|
||||
AlbumView: { id: string, title: string };
|
||||
ArtistView: { id: string, title: string };
|
||||
}
|
||||
LibraryTopTabs: undefined;
|
||||
AlbumView: { id: string; title: string };
|
||||
ArtistView: { id: string; title: string };
|
||||
};
|
||||
|
||||
type AlbumScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'AlbumView'>;
|
||||
type AlbumScreenRouteProp = RouteProp<LibraryStackParamList, 'AlbumView'>;
|
||||
type AlbumScreenProps = {
|
||||
route: AlbumScreenRouteProp,
|
||||
navigation: AlbumScreenNavigationProp,
|
||||
route: AlbumScreenRouteProp;
|
||||
navigation: AlbumScreenNavigationProp;
|
||||
};
|
||||
|
||||
const AlbumScreen: React.FC<AlbumScreenProps> = ({ route }) => (
|
||||
@ -66,8 +58,8 @@ const AlbumScreen: React.FC<AlbumScreenProps> = ({ route }) => (
|
||||
type ArtistScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'ArtistView'>;
|
||||
type ArtistScreenRouteProp = RouteProp<LibraryStackParamList, 'ArtistView'>;
|
||||
type ArtistScreenProps = {
|
||||
route: ArtistScreenRouteProp,
|
||||
navigation: ArtistScreenNavigationProp,
|
||||
route: ArtistScreenRouteProp;
|
||||
navigation: ArtistScreenNavigationProp;
|
||||
};
|
||||
|
||||
const ArtistScreen: React.FC<ArtistScreenProps> = ({ route }) => (
|
||||
@ -91,31 +83,21 @@ const itemScreenOptions = {
|
||||
headerTitleStyle: {
|
||||
...text.header,
|
||||
},
|
||||
headerBackImage: () => <FastImage
|
||||
source={require('../../../res/arrow_left-fill.png')}
|
||||
tintColor={colors.text.primary}
|
||||
style={{ height: 22, width: 22 }}
|
||||
/>,
|
||||
}
|
||||
headerBackImage: () => (
|
||||
<FastImage
|
||||
source={require('../../../res/arrow_left-fill.png')}
|
||||
tintColor={colors.text.primary}
|
||||
style={{ height: 22, width: 22 }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const LibraryStackNavigator = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen
|
||||
name='LibraryTopTabs'
|
||||
component={LibraryTopTabNavigator}
|
||||
options={{ headerShown: false }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='AlbumView'
|
||||
component={AlbumScreen}
|
||||
options={itemScreenOptions}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='ArtistView'
|
||||
component={ArtistScreen}
|
||||
options={itemScreenOptions}
|
||||
/>
|
||||
<Stack.Screen name="LibraryTopTabs" component={LibraryTopTabNavigator} options={{ headerShown: false }} />
|
||||
<Stack.Screen name="AlbumView" component={AlbumScreen} options={itemScreenOptions} />
|
||||
<Stack.Screen name="ArtistView" component={ArtistScreen} options={itemScreenOptions} />
|
||||
</Stack.Navigator>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -7,16 +7,8 @@ const RootStack = createStackNavigator();
|
||||
|
||||
const RootNavigator = () => (
|
||||
<RootStack.Navigator>
|
||||
<RootStack.Screen
|
||||
name='Main'
|
||||
component={BottomTabNavigator}
|
||||
options={{ headerShown: false }}
|
||||
/>
|
||||
<RootStack.Screen
|
||||
name='Now Playing'
|
||||
component={NowPlayingLayout}
|
||||
options={{ headerShown: false }}
|
||||
/>
|
||||
<RootStack.Screen name="Main" component={BottomTabNavigator} options={{ headerShown: false }} />
|
||||
<RootStack.Screen name="Now Playing" component={NowPlayingLayout} options={{ headerShown: false }} />
|
||||
</RootStack.Navigator>
|
||||
);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import TrackPlayer, { Track, useTrackPlayerEvents, Event, State } from "react-native-track-player";
|
||||
import { Song } from "../models/music";
|
||||
import { useState } from 'react';
|
||||
import TrackPlayer, { Track, useTrackPlayerEvents, Event, State } from 'react-native-track-player';
|
||||
import { Song } from '../models/music';
|
||||
|
||||
function mapSongToTrack(song: Song): Track {
|
||||
return {
|
||||
@ -10,19 +10,15 @@ function mapSongToTrack(song: Song): Track {
|
||||
url: song.streamUri,
|
||||
artwork: song.coverArtUri,
|
||||
duration: song.duration,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const currentTrackEvents = [
|
||||
Event.PlaybackState,
|
||||
Event.PlaybackTrackChanged,
|
||||
Event.RemoteStop,
|
||||
]
|
||||
const currentTrackEvents = [Event.PlaybackState, Event.PlaybackTrackChanged, Event.RemoteStop];
|
||||
|
||||
export const useCurrentTrackId = () => {
|
||||
const [currentTrackId, setCurrentTrackId] = useState<string | null>(null);
|
||||
|
||||
useTrackPlayerEvents(currentTrackEvents, async (event) => {
|
||||
useTrackPlayerEvents(currentTrackEvents, async event => {
|
||||
switch (event.type) {
|
||||
case Event.PlaybackState:
|
||||
switch (event.state) {
|
||||
@ -33,7 +29,7 @@ export const useCurrentTrackId = () => {
|
||||
}
|
||||
break;
|
||||
case Event.PlaybackTrackChanged:
|
||||
const trackIndex = await TrackPlayer.getCurrentTrack()
|
||||
const trackIndex = await TrackPlayer.getCurrentTrack();
|
||||
setCurrentTrackId((await TrackPlayer.getTrack(trackIndex)).id);
|
||||
break;
|
||||
case Event.RemoteStop:
|
||||
@ -45,7 +41,7 @@ export const useCurrentTrackId = () => {
|
||||
});
|
||||
|
||||
return currentTrackId;
|
||||
}
|
||||
};
|
||||
|
||||
export const useSetQueue = () => {
|
||||
return async (songs: Song[], playId?: string) => {
|
||||
@ -70,5 +66,5 @@ export const useSetQueue = () => {
|
||||
const queue = await TrackPlayer.getQueue();
|
||||
console.log(`queue: ${JSON.stringify(queue.map(x => x.title))}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useAtomValue } from "jotai/utils"
|
||||
import { activeServerAtom } from "../state/settings"
|
||||
import { SubsonicApiClient } from "../subsonic/api";
|
||||
import { useAtomValue } from 'jotai/utils';
|
||||
import { activeServerAtom } from '../state/settings';
|
||||
import { SubsonicApiClient } from '../subsonic/api';
|
||||
|
||||
export const useSubsonicApi = () => {
|
||||
const activeServer = useAtomValue(activeServerAtom);
|
||||
@ -10,5 +10,5 @@ export const useSubsonicApi = () => {
|
||||
return undefined;
|
||||
}
|
||||
return new SubsonicApiClient(activeServer);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@ -24,8 +24,8 @@ export interface Album {
|
||||
name: string;
|
||||
starred?: Date;
|
||||
coverArt?: string;
|
||||
coverArtUri?: string,
|
||||
coverArtThumbUri?: string,
|
||||
coverArtUri?: string;
|
||||
coverArtThumbUri?: string;
|
||||
year?: number;
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ export interface Song {
|
||||
starred?: Date;
|
||||
|
||||
streamUri: string;
|
||||
coverArtUri?: string,
|
||||
coverArtUri?: string;
|
||||
}
|
||||
|
||||
export type DownloadedSong = {
|
||||
|
||||
@ -7,6 +7,6 @@ export interface Server {
|
||||
}
|
||||
|
||||
export interface AppSettings {
|
||||
servers: Server[],
|
||||
servers: Server[];
|
||||
activeServer?: string;
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
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.RemotePause, () => TrackPlayer.pause());
|
||||
TrackPlayer.addEventListener(Event.RemoteStop, () => TrackPlayer.destroy());
|
||||
|
||||
TrackPlayer.addEventListener(Event.RemoteDuck, (data) => {
|
||||
TrackPlayer.addEventListener(Event.RemoteDuck, data => {
|
||||
if (data.permanent) {
|
||||
TrackPlayer.stop();
|
||||
return;
|
||||
|
||||
@ -27,14 +27,16 @@ export const useUpdateArtists = () => {
|
||||
const client = new SubsonicApiClient(server);
|
||||
const response = await client.getArtists();
|
||||
|
||||
setArtists(response.data.artists.map(x => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
starred: x.starred,
|
||||
})));
|
||||
setArtists(
|
||||
response.data.artists.map(x => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
starred: x.starred,
|
||||
})),
|
||||
);
|
||||
setUpdating(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const albumsAtom = atom<Record<string, Album>>({});
|
||||
export const albumsUpdatingAtom = atom(false);
|
||||
@ -57,92 +59,102 @@ export const useUpdateAlbums = () => {
|
||||
const client = new SubsonicApiClient(server);
|
||||
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 });
|
||||
|
||||
setAlbums(response.data.albums.reduce((acc, next) => {
|
||||
const album = mapAlbumID3(next, client);
|
||||
acc[album.id] = album;
|
||||
return acc;
|
||||
}, {} as Record<string, Album>));
|
||||
setAlbums(
|
||||
response.data.albums.reduce((acc, next) => {
|
||||
const album = mapAlbumID3(next, client);
|
||||
acc[album.id] = album;
|
||||
return acc;
|
||||
}, {} as Record<string, Album>),
|
||||
);
|
||||
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) => {
|
||||
const server = get(activeServerAtom);
|
||||
if (!server) {
|
||||
return undefined;
|
||||
}
|
||||
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 [artistResponse, artistInfoResponse] = await Promise.all([
|
||||
client.getArtist({ id }),
|
||||
client.getArtistInfo2({ id }),
|
||||
]);
|
||||
return mapArtistInfo(artistResponse.data, artistInfoResponse.data.artistInfo, client);
|
||||
}));
|
||||
const client = new SubsonicApiClient(server);
|
||||
const response = await client.getAlbum({ id });
|
||||
return mapAlbumID3WithSongs(response.data.album, response.data.songs, client);
|
||||
}),
|
||||
);
|
||||
|
||||
export const artistArtAtomFamily = atomFamily((id: string) => atom<ArtistArt | undefined>(async (get) => {
|
||||
const artistInfo = get(artistInfoAtomFamily(id));
|
||||
if (!artistInfo) {
|
||||
return undefined;
|
||||
}
|
||||
export const albumArtAtomFamily = atomFamily((id: string) =>
|
||||
atom<AlbumArt | undefined>(async get => {
|
||||
const server = get(activeServerAtom);
|
||||
if (!server) {
|
||||
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[];
|
||||
const albums = get(albumsAtom);
|
||||
const album = id in albums ? albums[id] : undefined;
|
||||
if (!album) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
coverArtUris,
|
||||
uri: artistInfo.mediumImageUrl,
|
||||
};
|
||||
}));
|
||||
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 => {
|
||||
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(
|
||||
artistResponse: GetArtistResponse,
|
||||
artistInfo: ArtistInfo2Element,
|
||||
client: SubsonicApiClient
|
||||
client: SubsonicApiClient,
|
||||
): ArtistInfo {
|
||||
const info = { ...artistInfo } as any;
|
||||
delete info.similarArtists;
|
||||
|
||||
const { artist, albums } = artistResponse
|
||||
const { artist, albums } = artistResponse;
|
||||
|
||||
const mappedAlbums = albums.map(a => mapAlbumID3(a, client));
|
||||
const coverArtUris = mappedAlbums
|
||||
@ -160,7 +172,7 @@ function mapArtistInfo(
|
||||
...info,
|
||||
albums: mappedAlbums,
|
||||
coverArtUris,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
|
||||
@ -168,7 +180,7 @@ function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
|
||||
...album,
|
||||
coverArtUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt }) : undefined,
|
||||
coverArtThumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
|
||||
@ -176,16 +188,16 @@ function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
|
||||
...child,
|
||||
streamUri: client.streamUri({ id: child.id }),
|
||||
coverArtUri: child.coverArt ? client.getCoverArtUri({ id: child.coverArt }) : undefined,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function mapAlbumID3WithSongs(
|
||||
album: AlbumID3Element,
|
||||
songs: ChildElement[],
|
||||
client: SubsonicApiClient
|
||||
client: SubsonicApiClient,
|
||||
): AlbumWithSongs {
|
||||
return {
|
||||
...mapAlbumID3(album, client),
|
||||
songs: songs.map(s => mapChildToSong(s, client)),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ export const appSettingsAtom = atomWithAsyncStorage<AppSettings>('@appSettings',
|
||||
servers: [],
|
||||
});
|
||||
|
||||
export const activeServerAtom = atom((get) => {
|
||||
export const activeServerAtom = atom(get => {
|
||||
const appSettings = get(appSettingsAtom);
|
||||
return appSettings.servers.find(x => x.id == appSettings.activeServer);
|
||||
return appSettings.servers.find(x => x.id === appSettings.activeServer);
|
||||
});
|
||||
|
||||
@ -15,7 +15,7 @@ export async function multiGet(keys: string[]): Promise<[string, any | null][]>
|
||||
const items = await AsyncStorage.multiGet(keys);
|
||||
return items.map(x => [x[0], x[1] ? JSON.parse(x[1]) : null]);
|
||||
} catch (e) {
|
||||
console.error(`multiGet error`, e);
|
||||
console.error('multiGet error', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -32,7 +32,7 @@ export async function multiSet(items: string[][]): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.multiSet(items.map(x => [x[0], JSON.stringify(x[1])]));
|
||||
} catch (e) {
|
||||
console.error(`multiSet error`, e);
|
||||
console.error('multiSet error', e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export async function getAllKeys(): Promise<string[]> {
|
||||
try {
|
||||
return await AsyncStorage.getAllKeys();
|
||||
} catch (e) {
|
||||
console.error(`getAllKeys error`, e);
|
||||
console.error('getAllKeys error', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -49,6 +49,6 @@ export async function multiRemove(keys: string[]): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.multiRemove(keys);
|
||||
} catch (e) {
|
||||
console.error(`multiRemove error`, e);
|
||||
console.error('multiRemove error', e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ import { getItem, setItem } from './asyncstorage';
|
||||
|
||||
export default <T>(key: string, defaultValue: T) => {
|
||||
return atomWithStorage<T>(key, defaultValue, {
|
||||
getItem: async () => await getItem(key) || defaultValue,
|
||||
getItem: async () => (await getItem(key)) || defaultValue,
|
||||
setItem: setItem,
|
||||
delayInit: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -26,10 +26,13 @@ export async function getDownloadedSongs(): Promise<DownloadedSong[]> {
|
||||
export async function setDownloadedSongs(items: DownloadedSong[]): Promise<void> {
|
||||
await multiSet([
|
||||
[key.downloadedSongKeys, JSON.stringify(items.map(x => x.id))],
|
||||
...items.map(x => [x.id, JSON.stringify({
|
||||
name: x.name,
|
||||
album: x.album,
|
||||
artist: x.artist,
|
||||
})]),
|
||||
...items.map(x => [
|
||||
x.id,
|
||||
JSON.stringify({
|
||||
name: x.name,
|
||||
album: x.album,
|
||||
artist: x.artist,
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -10,4 +10,4 @@ export default {
|
||||
},
|
||||
accent: '#b134db',
|
||||
accentLow: '#511c63',
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,7 +1,29 @@
|
||||
import { DOMParser } from 'xmldom';
|
||||
import RNFS from 'react-native-fs';
|
||||
import { 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 {
|
||||
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 paths from '../paths';
|
||||
|
||||
@ -56,7 +78,7 @@ export class SubsonicApiClient {
|
||||
address: string;
|
||||
username: string;
|
||||
|
||||
private params: URLSearchParams
|
||||
private params: URLSearchParams;
|
||||
|
||||
constructor(server: Server) {
|
||||
this.address = server.address;
|
||||
@ -67,7 +89,7 @@ export class SubsonicApiClient {
|
||||
this.params.append('t', server.token);
|
||||
this.params.append('s', server.salt);
|
||||
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 {
|
||||
|
||||
@ -5,56 +5,65 @@
|
||||
export type GetIndexesParams = {
|
||||
musicFolderId?: string;
|
||||
ifModifiedSince?: number;
|
||||
}
|
||||
};
|
||||
|
||||
export type GetArtistInfoParams = {
|
||||
id: string;
|
||||
count?: number;
|
||||
includeNotPresent?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export type GetArtistInfo2Params = GetArtistInfoParams;
|
||||
|
||||
export type GetMusicDirectoryParams = {
|
||||
id: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type GetAlbumParams = {
|
||||
id: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type GetArtistParams = {
|
||||
id: string;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
//
|
||||
// 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 GetAlbumList2TypeByYear = {
|
||||
type: 'byYear';
|
||||
fromYear: string;
|
||||
toYear: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type GetAlbumList2TypeByGenre = {
|
||||
type: 'byGenre';
|
||||
genre: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type GetAlbumList2Params = {
|
||||
type: GetAlbumList2Type;
|
||||
size?: number;
|
||||
offset?: number;
|
||||
fromYear?: string;
|
||||
toYear?: string;
|
||||
genre?: string;
|
||||
musicFolderId?: string;
|
||||
} | GetAlbumList2TypeByYear | GetAlbumList2TypeByGenre;
|
||||
export type GetAlbumList2Params =
|
||||
| {
|
||||
type: GetAlbumList2Type;
|
||||
size?: number;
|
||||
offset?: number;
|
||||
fromYear?: string;
|
||||
toYear?: string;
|
||||
genre?: string;
|
||||
musicFolderId?: string;
|
||||
}
|
||||
| GetAlbumList2TypeByYear
|
||||
| GetAlbumList2TypeByGenre;
|
||||
|
||||
export type GetAlbumListParams = GetAlbumList2Params;
|
||||
|
||||
@ -65,11 +74,11 @@ export type GetAlbumListParams = GetAlbumList2Params;
|
||||
export type GetCoverArtParams = {
|
||||
id: string;
|
||||
size?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type StreamParams = {
|
||||
id: string;
|
||||
maxBitRate?: number;
|
||||
format?: string;
|
||||
estimateContentLength?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -1,27 +1,26 @@
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"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'. */
|
||||
"lib": ["es2017"], /* Specify library files to be included in the compilation. */
|
||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||
"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'. */,
|
||||
"lib": ["es2017"] /* Specify library files to be included in the compilation. */,
|
||||
"allowJs": true /* Allow javascript files to be compiled. */,
|
||||
// "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. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
// "outDir": "./", /* Redirect output structure to the directory. */
|
||||
// "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. */
|
||||
"noEmit": true, /* Do not emit outputs. */
|
||||
"noEmit": true /* Do not emit outputs. */,
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
// "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'. */
|
||||
"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": 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. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
@ -36,16 +35,16 @@
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* 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. */
|
||||
// "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. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "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. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
"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'. */,
|
||||
// "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 */
|
||||
// "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. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules", "babel.config.js", "metro.config.js", "jest.config.js"
|
||||
]
|
||||
"exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user