mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 09:09:29 +01:00
eslint/prettier
This commit is contained in:
parent
ee7658ccf8
commit
4e98318cd9
@ -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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{}
|
{}
|
||||||
|
|||||||
2
app.json
2
app.json
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "SubSonify",
|
"name": "SubSonify",
|
||||||
"displayName": "SubSonify"
|
"displayName": "SubSonify"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"info" : {
|
"info": {
|
||||||
"version" : 1,
|
"version": 1,
|
||||||
"author" : "xcode"
|
"author": "xcode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,5 +3,5 @@ module.exports = {
|
|||||||
ios: {},
|
ios: {},
|
||||||
android: {},
|
android: {},
|
||||||
},
|
},
|
||||||
assets: ['./assets/fonts']
|
assets: ['./assets/fonts'],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>}>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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))}`);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,6 @@ export interface Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
servers: Server[],
|
servers: Server[];
|
||||||
activeServer?: string;
|
activeServer?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(() => {}));
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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)),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,4 +10,4 @@ export default {
|
|||||||
},
|
},
|
||||||
accent: '#b134db',
|
accent: '#b134db',
|
||||||
accentLow: '#511c63',
|
accentLow: '#511c63',
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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}`;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
};
|
||||||
|
|||||||
@ -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[] = [];
|
||||||
|
|||||||
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user