eslint/prettier

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

View File

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

View File

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

View File

@ -1 +1 @@
{}
{}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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