let's try no semicolons

This commit is contained in:
austinried 2021-07-05 10:27:15 +09:00
parent 8f7b285938
commit 24b443fd70
52 changed files with 1149 additions and 1147 deletions

View File

@ -5,5 +5,6 @@ module.exports = {
'react-native/no-inline-styles': 0,
radix: 0,
'@typescript-eslint/no-unused-vars': ['warn'],
semi: 0,
},
};
}

View File

@ -5,4 +5,5 @@ module.exports = {
trailingComma: 'all',
arrowParens: 'avoid',
printWidth: 120,
};
semi: false,
}

24
App.tsx
View File

@ -1,14 +1,14 @@
import React from 'react';
import { DarkTheme, NavigationContainer } from '@react-navigation/native';
import SplashPage from './src/components/SplashPage';
import RootNavigator from './src/components/navigation/RootNavigator';
import { Provider } from 'jotai';
import { StatusBar, View } from 'react-native';
import colors from './src/styles/colors';
import TrackPlayerState from './src/components/TrackPlayerState';
import React from 'react'
import { DarkTheme, NavigationContainer } from '@react-navigation/native'
import SplashPage from './src/components/SplashPage'
import RootNavigator from './src/components/navigation/RootNavigator'
import { Provider } from 'jotai'
import { StatusBar, View } from 'react-native'
import colors from './src/styles/colors'
import TrackPlayerState from './src/components/TrackPlayerState'
const theme = { ...DarkTheme };
theme.colors.background = colors.gradient.high;
const theme = { ...DarkTheme }
theme.colors.background = colors.gradient.high
const App = () => (
<Provider>
@ -22,6 +22,6 @@ const App = () => (
</SplashPage>
</View>
</Provider>
);
)
export default App;
export default App

View File

@ -2,13 +2,13 @@
* @format
*/
import 'react-native';
import React from 'react';
import App from '../App';
import 'react-native'
import React from 'react'
import App from '../App'
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
import renderer from 'react-test-renderer'
it('renders correctly', () => {
renderer.create(<App />);
});
renderer.create(<App />)
})

View File

@ -4,4 +4,4 @@ module.exports = {
// reanimated has to be listed last in plugins
'react-native-reanimated/plugin',
],
};
}

View File

@ -1,19 +1,19 @@
import 'react-native-gesture-handler';
import 'react-native-get-random-values';
import 'react-native-gesture-handler'
import 'react-native-get-random-values'
import { enableScreens } from 'react-native-screens';
enableScreens();
import { enableScreens } from 'react-native-screens'
enableScreens()
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
import TrackPlayer, { Capability } from 'react-native-track-player';
import { AppRegistry } from 'react-native'
import App from './App'
import { name as appName } from './app.json'
import TrackPlayer, { Capability } from 'react-native-track-player'
AppRegistry.registerComponent(appName, () => App);
TrackPlayer.registerPlaybackService(() => require('./src/playback/service'));
AppRegistry.registerComponent(appName, () => App)
TrackPlayer.registerPlaybackService(() => require('./src/playback/service'))
async function start() {
await TrackPlayer.setupPlayer();
await TrackPlayer.setupPlayer()
await TrackPlayer.updateOptions({
capabilities: [
Capability.Play,
@ -23,6 +23,6 @@ async function start() {
Capability.SkipToPrevious,
],
compactCapabilities: [Capability.Play, Capability.Pause, Capability.SkipToNext, Capability.SkipToPrevious],
});
})
}
start();
start()

View File

@ -14,4 +14,4 @@ module.exports = {
},
}),
},
};
}

View File

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

View File

@ -1,8 +1,8 @@
import React from 'react';
import { FlatList, Text, View } from 'react-native';
import { useAtomValue } from 'jotai/utils';
import { Artist } from '../models/music';
import { artistsAtom } from '../state/music';
import React from 'react'
import { FlatList, Text, View } from 'react-native'
import { useAtomValue } from 'jotai/utils'
import { Artist } from '../models/music'
import { artistsAtom } from '../state/music'
const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => (
<View>
@ -15,15 +15,15 @@ const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => (
{item.name}
</Text>
</View>
);
)
const List = () => {
const artists = useAtomValue(artistsAtom);
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>
@ -31,6 +31,6 @@ const ArtistsList = () => (
<List />
</React.Suspense>
</View>
);
)
export default ArtistsList;
export default ArtistsList

View File

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

View File

@ -1,15 +1,15 @@
import { useAtomValue } from 'jotai/utils';
import React from 'react';
import { Pressable, StatusBar, StyleSheet, Text, useWindowDimensions, View } from 'react-native';
import FastImage from 'react-native-fast-image';
import TrackPlayer, { State } from 'react-native-track-player';
import { currentQueueNameAtom, currentTrackAtom, playerStateAtom } from '../state/trackplayer';
import text from '../styles/text';
import CoverArt from './common/CoverArt';
import ImageGradientBackground from './common/ImageGradientBackground';
import { useAtomValue } from 'jotai/utils'
import React from 'react'
import { Pressable, StatusBar, StyleSheet, Text, useWindowDimensions, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import TrackPlayer, { State } from 'react-native-track-player'
import { currentQueueNameAtom, currentTrackAtom, playerStateAtom } from '../state/trackplayer'
import text from '../styles/text'
import CoverArt from './common/CoverArt'
import ImageGradientBackground from './common/ImageGradientBackground'
const NowPlayingHeader = () => {
const queueName = useAtomValue(currentQueueNameAtom);
const queueName = useAtomValue(currentQueueNameAtom)
return (
<View style={headerStyles.container}>
@ -19,8 +19,8 @@ const NowPlayingHeader = () => {
</Text>
<FastImage source={require('../../res/more_vertical.png')} style={headerStyles.icons} tintColor="white" />
</View>
);
};
)
}
const headerStyles = StyleSheet.create({
container: {
@ -38,13 +38,13 @@ const headerStyles = StyleSheet.create({
queueName: {
...text.paragraph,
},
});
})
const SongCoverArt = () => {
const track = useAtomValue(currentTrackAtom);
const layout = useWindowDimensions();
const track = useAtomValue(currentTrackAtom)
const layout = useWindowDimensions()
const size = layout.width - layout.width / 7;
const size = layout.width - layout.width / 7
return (
<View style={coverArtStyles.container}>
@ -55,8 +55,8 @@ const SongCoverArt = () => {
coverArtUri={track?.artwork as string}
/>
</View>
);
};
)
}
const coverArtStyles = StyleSheet.create({
container: {
@ -64,18 +64,18 @@ const coverArtStyles = StyleSheet.create({
alignItems: 'center',
marginTop: 10,
},
});
})
const SongInfo = () => {
const track = useAtomValue(currentTrackAtom);
const track = useAtomValue(currentTrackAtom)
return (
<View style={infoStyles.container}>
<Text style={infoStyles.title}>{track?.title}</Text>
<Text style={infoStyles.artist}>{track?.artist}</Text>
</View>
);
};
)
}
const infoStyles = StyleSheet.create({
container: {
@ -94,33 +94,33 @@ const infoStyles = StyleSheet.create({
fontSize: 14,
textAlign: 'center',
},
});
})
const PlayerControls = () => {
const state = useAtomValue(playerStateAtom);
const state = useAtomValue(playerStateAtom)
let playPauseIcon: number;
let playPauseStyle: any;
let playPauseAction: () => void;
let playPauseIcon: number
let playPauseStyle: any
let playPauseAction: () => void
switch (state) {
case State.Playing:
case State.Buffering:
case State.Connecting:
playPauseIcon = require('../../res/pause_circle-fill.png');
playPauseStyle = controlsStyles.enabled;
playPauseAction = () => TrackPlayer.pause();
break;
playPauseIcon = require('../../res/pause_circle-fill.png')
playPauseStyle = controlsStyles.enabled
playPauseAction = () => TrackPlayer.pause()
break
case State.Paused:
playPauseIcon = require('../../res/play_circle-fill.png');
playPauseStyle = controlsStyles.enabled;
playPauseAction = () => TrackPlayer.play();
break;
playPauseIcon = require('../../res/play_circle-fill.png')
playPauseStyle = controlsStyles.enabled
playPauseAction = () => TrackPlayer.play()
break
default:
playPauseIcon = require('../../res/play_circle-fill.png');
playPauseStyle = controlsStyles.disabled;
playPauseAction = () => {};
break;
playPauseIcon = require('../../res/play_circle-fill.png')
playPauseStyle = controlsStyles.disabled
playPauseAction = () => {}
break
}
return (
@ -139,8 +139,8 @@ const PlayerControls = () => {
style={{ ...controlsStyles.skip, ...playPauseStyle }}
/>
</View>
);
};
)
}
const controlsStyles = StyleSheet.create({
container: {
@ -165,10 +165,10 @@ const controlsStyles = StyleSheet.create({
disabled: {
opacity: 0.35,
},
});
})
const NowPlayingLayout = () => {
const track = useAtomValue(currentTrackAtom);
const track = useAtomValue(currentTrackAtom)
return (
<View
@ -182,7 +182,7 @@ const NowPlayingLayout = () => {
<SongInfo />
<PlayerControls />
</View>
);
};
)
}
export default NowPlayingLayout;
export default NowPlayingLayout

View File

@ -1,40 +1,40 @@
import { useNavigation } from '@react-navigation/core';
import { useAtom } from 'jotai';
import md5 from 'md5';
import React from 'react';
import { Button, Text, View } from 'react-native';
import { v4 as uuidv4 } from 'uuid';
import { appSettingsAtom } from '../state/settings';
import { getAllKeys, multiRemove } from '../storage/asyncstorage';
import text from '../styles/text';
import { useNavigation } from '@react-navigation/core'
import { useAtom } from 'jotai'
import md5 from 'md5'
import React from 'react'
import { Button, Text, View } from 'react-native'
import { v4 as uuidv4 } from 'uuid'
import { appSettingsAtom } from '../state/settings'
import { getAllKeys, multiRemove } from '../storage/asyncstorage'
import text from '../styles/text'
const TestControls = () => {
const navigation = useNavigation();
const navigation = useNavigation()
const removeAllKeys = async () => {
const allKeys = await getAllKeys();
await multiRemove(allKeys);
};
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')} />
</View>
);
};
)
}
const ServerSettingsView = () => {
const [appSettings, setAppSettings] = useAtom(appSettingsAtom);
const [appSettings, setAppSettings] = useAtom(appSettingsAtom)
const bootstrapServer = () => {
if (appSettings.servers.length !== 0) {
return;
return
}
const id = uuidv4();
const salt = uuidv4();
const address = 'http://demo.subsonic.org';
const id = uuidv4()
const salt = uuidv4()
const address = 'http://demo.subsonic.org'
setAppSettings({
...appSettings,
@ -49,8 +49,8 @@ const ServerSettingsView = () => {
},
],
activeServer: id,
});
};
})
}
return (
<View>
@ -62,8 +62,8 @@ const ServerSettingsView = () => {
</View>
))}
</View>
);
};
)
}
const SettingsView = () => (
<View>
@ -72,6 +72,6 @@ const SettingsView = () => (
<ServerSettingsView />
</React.Suspense>
</View>
);
)
export default SettingsView;
export default SettingsView

View File

@ -1,45 +1,45 @@
import React, { useEffect, useState } from 'react';
import { Text, View } from 'react-native';
import RNFS from 'react-native-fs';
import paths from '../paths';
import React, { useEffect, useState } from 'react'
import { Text, View } from 'react-native'
import RNFS from 'react-native-fs'
import paths from '../paths'
async function mkdir(path: string): Promise<void> {
const exists = await RNFS.exists(path);
const exists = await RNFS.exists(path)
if (exists) {
const isDir = (await RNFS.stat(path)).isDirectory();
const isDir = (await RNFS.stat(path)).isDirectory()
if (!isDir) {
throw new Error(`path exists and is not a directory: ${path}`);
throw new Error(`path exists and is not a directory: ${path}`)
} else {
return;
return
}
}
return await RNFS.mkdir(path);
return await RNFS.mkdir(path)
}
const SplashPage: React.FC<{}> = ({ children }) => {
const [ready, setReady] = useState(false);
const [ready, setReady] = useState(false)
const minSplashTime = new Promise(resolve => setTimeout(resolve, 1));
const minSplashTime = new Promise(resolve => setTimeout(resolve, 1))
const prepare = async () => {
await mkdir(paths.imageCache);
await mkdir(paths.songCache);
await mkdir(paths.songs);
};
await mkdir(paths.imageCache)
await mkdir(paths.songCache)
await mkdir(paths.songs)
}
const promise = Promise.all([prepare(), minSplashTime]);
const promise = Promise.all([prepare(), minSplashTime])
useEffect(() => {
promise.then(() => {
setReady(true);
});
});
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;
export default SplashPage

View File

@ -1,27 +1,27 @@
import React, { useCallback, useEffect } from 'react';
import TrackPlayer, { Event, State, useTrackPlayerEvents } from 'react-native-track-player';
import { useAppState } from '@react-native-community/hooks';
import { useUpdateAtom, useAtomValue } from 'jotai/utils';
import { currentQueueNameAtom, currentTrackAtom, playerStateAtom } from '../state/trackplayer';
import { View } from 'react-native';
import React, { useCallback, useEffect } from 'react'
import TrackPlayer, { Event, State, useTrackPlayerEvents } from 'react-native-track-player'
import { useAppState } from '@react-native-community/hooks'
import { useUpdateAtom, useAtomValue } from 'jotai/utils'
import { currentQueueNameAtom, currentTrackAtom, playerStateAtom } from '../state/trackplayer'
import { View } from 'react-native'
const CurrentTrackState = () => {
const setCurrentTrack = useUpdateAtom(currentTrackAtom);
const appState = useAppState();
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
const appState = useAppState()
const update = useCallback(async () => {
const index = await TrackPlayer.getCurrentTrack();
const index = await TrackPlayer.getCurrentTrack()
if (index !== null && index >= 0) {
const track = await TrackPlayer.getTrack(index);
const track = await TrackPlayer.getTrack(index)
if (track !== null) {
setCurrentTrack(track);
return;
setCurrentTrack(track)
return
}
}
setCurrentTrack(undefined);
}, [setCurrentTrack]);
setCurrentTrack(undefined)
}, [setCurrentTrack])
useTrackPlayerEvents(
[
@ -36,91 +36,91 @@ const CurrentTrackState = () => {
],
event => {
if (event.type === Event.PlaybackQueueEnded && 'track' in event) {
setCurrentTrack(undefined);
return;
setCurrentTrack(undefined)
return
}
update();
update()
},
);
)
useEffect(() => {
if (appState === 'active') {
update();
update()
}
}, [appState, update]);
}, [appState, update])
return <></>;
};
return <></>
}
const CurrentQueueName = () => {
const setCurrentQueueName = useUpdateAtom(currentQueueNameAtom);
const appState = useAppState();
const setCurrentQueueName = useUpdateAtom(currentQueueNameAtom)
const appState = useAppState()
const update = useCallback(async () => {
const queue = await TrackPlayer.getQueue();
const queue = await TrackPlayer.getQueue()
if (queue !== null && queue.length > 0) {
setCurrentQueueName(queue[0].queueName);
return;
setCurrentQueueName(queue[0].queueName)
return
}
setCurrentQueueName(undefined);
}, [setCurrentQueueName]);
setCurrentQueueName(undefined)
}, [setCurrentQueueName])
useTrackPlayerEvents(
[Event.PlaybackState, Event.PlaybackQueueEnded, Event.PlaybackMetadataReceived, Event.RemoteDuck, Event.RemoteStop],
event => {
if (event.type === Event.PlaybackState) {
if (event.state === State.Stopped || event.state === State.None) {
return;
return
}
}
update();
update()
},
);
)
useEffect(() => {
if (appState === 'active') {
update();
update()
}
}, [appState, update]);
}, [appState, update])
return <></>;
};
return <></>
}
const PlayerState = () => {
const setPlayerState = useUpdateAtom(playerStateAtom);
const appState = useAppState();
const setPlayerState = useUpdateAtom(playerStateAtom)
const appState = useAppState()
const update = useCallback(
async (state?: State) => {
setPlayerState(state || (await TrackPlayer.getState()));
setPlayerState(state || (await TrackPlayer.getState()))
},
[setPlayerState],
);
)
useTrackPlayerEvents([Event.PlaybackState], event => {
update(event.state);
});
update(event.state)
})
useEffect(() => {
if (appState === 'active') {
update();
update()
}
}, [appState, update]);
}, [appState, update])
return <></>;
};
return <></>
}
const Debug = () => {
const value = useAtomValue(currentQueueNameAtom);
const value = useAtomValue(currentQueueNameAtom)
useEffect(() => {
console.log(value);
}, [value]);
console.log(value)
}, [value])
return <></>;
};
return <></>
}
const TrackPlayerState = () => (
<View>
@ -129,6 +129,6 @@ const TrackPlayerState = () => (
<PlayerState />
<Debug />
</View>
);
)
export default TrackPlayerState;
export default TrackPlayerState

View File

@ -1,20 +1,20 @@
import { useAtomValue } from 'jotai/utils';
import React from 'react';
import { ActivityIndicator, View } from 'react-native';
import FastImage from 'react-native-fast-image';
import LinearGradient from 'react-native-linear-gradient';
import { albumArtAtomFamily } from '../../state/music';
import colors from '../../styles/colors';
import CoverArt from './CoverArt';
import { useAtomValue } from 'jotai/utils'
import React from 'react'
import { ActivityIndicator, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import LinearGradient from 'react-native-linear-gradient'
import { albumArtAtomFamily } from '../../state/music'
import colors from '../../styles/colors'
import CoverArt from './CoverArt'
interface AlbumArtProps {
id: string;
height: number;
width: number;
id: string
height: number
width: number
}
const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
const albumArt = useAtomValue(albumArtAtomFamily(id));
const albumArt = useAtomValue(albumArtAtomFamily(id))
const Placeholder = () => (
<LinearGradient colors={[colors.accent, colors.accentLow]}>
@ -24,7 +24,7 @@ const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
resizeMode={FastImage.resizeMode.contain}
/>
</LinearGradient>
);
)
return (
<CoverArt
@ -33,8 +33,8 @@ const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
width={width}
coverArtUri={width > 128 ? albumArt?.uri : albumArt?.thumbUri}
/>
);
};
)
}
const AlbumArtFallback: React.FC<AlbumArtProps> = ({ height, width }) => (
<View
@ -46,12 +46,12 @@ const AlbumArtFallback: React.FC<AlbumArtProps> = ({ height, width }) => (
}}>
<ActivityIndicator size="small" color={colors.accent} />
</View>
);
)
const AlbumArtLoader: React.FC<AlbumArtProps> = props => (
<React.Suspense fallback={<AlbumArtFallback {...props} />}>
<AlbumArt {...props} />
</React.Suspense>
);
)
export default React.memo(AlbumArtLoader);
export default React.memo(AlbumArtLoader)

View File

@ -1,6 +1,6 @@
import { useNavigation } from '@react-navigation/native';
import { useAtomValue } from 'jotai/utils';
import React, { useEffect, useState } from 'react';
import { useNavigation } from '@react-navigation/native'
import { useAtomValue } from 'jotai/utils'
import React, { useEffect, useState } from 'react'
import {
ActivityIndicator,
GestureResponderEvent,
@ -9,26 +9,26 @@ import {
Text,
useWindowDimensions,
View,
} from 'react-native';
import { useSetQueue } from '../../hooks/trackplayer';
import { albumAtomFamily } from '../../state/music';
import { currentTrackAtom } from '../../state/trackplayer';
import colors from '../../styles/colors';
import text from '../../styles/text';
import AlbumArt from './AlbumArt';
import Button from './Button';
import GradientBackground from './GradientBackground';
import ImageGradientScrollView from './ImageGradientScrollView';
} from 'react-native'
import { useSetQueue } from '../../hooks/trackplayer'
import { albumAtomFamily } from '../../state/music'
import { currentTrackAtom } from '../../state/trackplayer'
import colors from '../../styles/colors'
import text from '../../styles/text'
import AlbumArt from './AlbumArt'
import Button from './Button'
import GradientBackground from './GradientBackground'
import ImageGradientScrollView from './ImageGradientScrollView'
const SongItem: React.FC<{
id: string;
title: string;
artist?: string;
track?: number;
onPress: (event: GestureResponderEvent) => void;
id: string
title: string
artist?: string
track?: number
onPress: (event: GestureResponderEvent) => void
}> = ({ id, title, artist, onPress }) => {
const [opacity, setOpacity] = useState(1);
const currentTrack = useAtomValue(currentTrackAtom);
const [opacity, setOpacity] = useState(1)
const currentTrack = useAtomValue(currentTrackAtom)
return (
<View
@ -85,20 +85,20 @@ const SongItem: React.FC<{
/>
</View>
</View>
);
};
)
}
const AlbumDetails: React.FC<{
id: string;
id: string
}> = ({ id }) => {
const album = useAtomValue(albumAtomFamily(id));
const layout = useWindowDimensions();
const setQueue = useSetQueue();
const album = useAtomValue(albumAtomFamily(id))
const layout = useWindowDimensions()
const setQueue = useSetQueue()
const coverSize = layout.width - layout.width / 2.5;
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 (
@ -152,9 +152,9 @@ const AlbumDetails: React.FC<{
{album.songs
.sort((a, b) => {
if (b.track && a.track) {
return a.track - b.track;
return a.track - b.track
} else {
return a.title.localeCompare(b.title);
return a.title.localeCompare(b.title)
}
})
.map(s => (
@ -169,13 +169,13 @@ const AlbumDetails: React.FC<{
))}
</View>
</ImageGradientScrollView>
);
};
)
}
const AlbumViewFallback = () => {
const layout = useWindowDimensions();
const layout = useWindowDimensions()
const coverSize = layout.width - layout.width / 2.5;
const coverSize = layout.width - layout.width / 2.5
return (
<GradientBackground
@ -185,24 +185,24 @@ const AlbumViewFallback = () => {
}}>
<ActivityIndicator size="large" color={colors.accent} />
</GradientBackground>
);
};
)
}
const AlbumView: React.FC<{
id: string;
title: string;
id: string
title: string
}> = ({ id, title }) => {
const navigation = useNavigation();
const navigation = useNavigation()
useEffect(() => {
navigation.setOptions({ title });
});
navigation.setOptions({ title })
})
return (
<React.Suspense fallback={<AlbumViewFallback />}>
<AlbumDetails id={id} />
</React.Suspense>
);
};
)
}
export default React.memo(AlbumView);
export default React.memo(AlbumView)

View File

@ -1,23 +1,23 @@
import { useAtomValue } from 'jotai/utils';
import React from 'react';
import { ActivityIndicator, View } from 'react-native';
import FastImage from 'react-native-fast-image';
import LinearGradient from 'react-native-linear-gradient';
import { artistArtAtomFamily } from '../../state/music';
import colors from '../../styles/colors';
import CoverArt from './CoverArt';
import { useAtomValue } from 'jotai/utils'
import React from 'react'
import { ActivityIndicator, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import LinearGradient from 'react-native-linear-gradient'
import { artistArtAtomFamily } from '../../state/music'
import colors from '../../styles/colors'
import CoverArt from './CoverArt'
interface ArtistArtSizeProps {
height: number;
width: number;
height: number
width: number
}
interface ArtistArtXUpProps extends ArtistArtSizeProps {
coverArtUris: string[];
coverArtUris: string[]
}
interface ArtistArtProps extends ArtistArtSizeProps {
id: string;
id: string
}
const PlaceholderContainer: React.FC<ArtistArtSizeProps> = ({ height, width, children }) => (
@ -31,11 +31,11 @@ const PlaceholderContainer: React.FC<ArtistArtSizeProps> = ({ height, width, chi
}}>
{children}
</LinearGradient>
);
)
const FourUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
const halfHeight = height / 2;
const halfWidth = width / 2;
const halfHeight = height / 2
const halfWidth = width / 2
return (
<PlaceholderContainer height={height} width={width}>
@ -64,12 +64,12 @@ const FourUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) =>
/>
</View>
</PlaceholderContainer>
);
};
)
}
const ThreeUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
const halfHeight = height / 2;
const halfWidth = width / 2;
const halfHeight = height / 2
const halfWidth = width / 2
return (
<PlaceholderContainer height={height} width={width}>
@ -93,11 +93,11 @@ const ThreeUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) =
/>
</View>
</PlaceholderContainer>
);
};
)
}
const TwoUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
const halfHeight = height / 2;
const halfHeight = height / 2
return (
<PlaceholderContainer height={height} width={width}>
@ -116,16 +116,16 @@ const TwoUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) =>
/>
</View>
</PlaceholderContainer>
);
};
)
}
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} />
</PlaceholderContainer>
);
};
)
}
const NoneUp: React.FC<ArtistArtSizeProps> = ({ height, width }) => {
return (
@ -139,35 +139,35 @@ const NoneUp: React.FC<ArtistArtSizeProps> = ({ height, width }) => {
resizeMode={FastImage.resizeMode.cover}
/>
</PlaceholderContainer>
);
};
)
}
const ArtistArt: React.FC<ArtistArtProps> = ({ id, height, width }) => {
const artistArt = useAtomValue(artistArtAtomFamily(id));
const artistArt = useAtomValue(artistArtAtomFamily(id))
const Placeholder = () => {
const none = <NoneUp height={height} width={width} />;
const none = <NoneUp height={height} width={width} />
if (!artistArt || !artistArt.coverArtUris) {
return none;
return none
}
const { coverArtUris } = artistArt;
const { coverArtUris } = artistArt
if (coverArtUris.length >= 4) {
return <FourUp height={height} width={width} coverArtUris={coverArtUris} />;
return <FourUp height={height} width={width} coverArtUris={coverArtUris} />
}
if (coverArtUris.length === 3) {
return <ThreeUp height={height} width={width} coverArtUris={coverArtUris} />;
return <ThreeUp height={height} width={width} coverArtUris={coverArtUris} />
}
if (coverArtUris.length === 2) {
return <TwoUp height={height} width={width} coverArtUris={coverArtUris} />;
return <TwoUp height={height} width={width} coverArtUris={coverArtUris} />
}
if (coverArtUris.length === 1) {
return <OneUp height={height} width={width} coverArtUris={coverArtUris} />;
return <OneUp height={height} width={width} coverArtUris={coverArtUris} />
}
return none;
};
return none
}
return (
<View
@ -177,8 +177,8 @@ const ArtistArt: React.FC<ArtistArtProps> = ({ id, height, width }) => {
}}>
<CoverArt PlaceholderComponent={Placeholder} height={height} width={width} coverArtUri={artistArt?.uri} />
</View>
);
};
)
}
const ArtistArtFallback: React.FC<ArtistArtProps> = ({ height, width }) => (
<View
@ -190,12 +190,12 @@ const ArtistArtFallback: React.FC<ArtistArtProps> = ({ height, width }) => (
}}>
<ActivityIndicator size="small" color={colors.accent} />
</View>
);
)
const ArtistArtLoader: React.FC<ArtistArtProps> = props => (
<React.Suspense fallback={<ArtistArtFallback {...props} />}>
<ArtistArt {...props} />
</React.Suspense>
);
)
export default React.memo(ArtistArtLoader);
export default React.memo(ArtistArtLoader)

View File

@ -1,17 +1,17 @@
import { useNavigation } from '@react-navigation/native';
import { useAtomValue } from 'jotai/utils';
import React, { useEffect } from 'react';
import { Text } from 'react-native';
import { artistInfoAtomFamily } from '../../state/music';
import text from '../../styles/text';
import ArtistArt from './ArtistArt';
import GradientScrollView from './GradientScrollView';
import { useNavigation } from '@react-navigation/native'
import { useAtomValue } from 'jotai/utils'
import React, { useEffect } from 'react'
import { Text } from 'react-native'
import { artistInfoAtomFamily } from '../../state/music'
import text from '../../styles/text'
import ArtistArt from './ArtistArt'
import GradientScrollView from './GradientScrollView'
const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
const artist = useAtomValue(artistInfoAtomFamily(id));
const artist = useAtomValue(artistInfoAtomFamily(id))
if (!artist) {
return <></>;
return <></>
}
return (
@ -26,24 +26,24 @@ const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
<Text style={text.paragraph}>{artist.name}</Text>
<ArtistArt id={artist.id} height={200} width={200} />
</GradientScrollView>
);
};
)
}
const ArtistView: React.FC<{
id: string;
title: string;
id: string
title: string
}> = ({ id, title }) => {
const navigation = useNavigation();
const navigation = useNavigation()
useEffect(() => {
navigation.setOptions({ title });
});
navigation.setOptions({ title })
})
return (
<React.Suspense fallback={<Text>Loading...</Text>}>
<ArtistDetails id={id} />
</React.Suspense>
);
};
)
}
export default React.memo(ArtistView);
export default React.memo(ArtistView)

View File

@ -1,9 +1,9 @@
import React, { useState } from 'react';
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 React, { useState } from 'react'
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'
const icons: { [key: string]: any } = {
home: {
@ -22,29 +22,29 @@ 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 };
navigation: any;
routeKey: string
label: string
name: string
isFocused: boolean
img: { regular: number; fill: number }
navigation: any
}> = ({ routeKey, label, name, isFocused, img, navigation }) => {
const [opacity, setOpacity] = useState(1);
const [opacity, setOpacity] = useState(1)
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: routeKey,
canPreventDefault: true,
});
})
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(name);
navigation.navigate(name)
}
}
};
return (
<Pressable
@ -72,8 +72,8 @@ const BottomTabButton: React.FC<{
{label}
</Text>
</Pressable>
);
};
)
}
const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigation }) => {
return (
@ -87,13 +87,13 @@ const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigat
paddingHorizontal: 28,
}}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key] as any;
const { options } = descriptors[route.key] as any
const label =
options.tabBarLabel !== undefined
? (options.tabBarLabel as string)
: options.title !== undefined
? options.title
: route.name;
: route.name
return (
<BottomTabButton
@ -105,10 +105,10 @@ const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigat
img={icons[options.icon]}
navigation={navigation}
/>
);
)
})}
</View>
);
};
)
}
export default BottomTabBar;
export default BottomTabBar

View File

@ -1,13 +1,13 @@
import React, { useState } from 'react';
import { GestureResponderEvent, Pressable, Text } from 'react-native';
import colors from '../../styles/colors';
import text from '../../styles/text';
import React, { useState } from 'react'
import { GestureResponderEvent, Pressable, Text } from 'react-native'
import colors from '../../styles/colors'
import text from '../../styles/text'
const Button: React.FC<{
title: string;
onPress: (event: GestureResponderEvent) => void;
title: string
onPress: (event: GestureResponderEvent) => void
}> = ({ title, onPress }) => {
const [opacity, setOpacity] = useState(1);
const [opacity, setOpacity] = useState(1)
return (
<Pressable
@ -25,7 +25,7 @@ const Button: React.FC<{
}}>
<Text style={{ ...text.button }}>{title}</Text>
</Pressable>
);
};
)
}
export default Button;
export default Button

View File

@ -1,19 +1,19 @@
import React, { useState } from 'react';
import { ActivityIndicator, View } from 'react-native';
import FastImage from 'react-native-fast-image';
import colors from '../../styles/colors';
import React, { useState } from 'react'
import { ActivityIndicator, View } from 'react-native'
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);
const [placeholderVisible, setPlaceholderVisible] = useState(false)
const [loading, setLoading] = useState(true)
const indicatorSize = height > 130 ? 'large' : 'small';
const halfIndicatorHeight = indicatorSize === 'large' ? 18 : 10;
const indicatorSize = height > 130 ? 'large' : 'small'
const halfIndicatorHeight = indicatorSize === 'large' ? 18 : 10
const Placeholder: React.FC<{ visible: boolean }> = ({ visible }) => (
<View
@ -22,7 +22,7 @@ const CoverArt: React.FC<{
}}>
<PlaceholderComponent />
</View>
);
)
const Art = () => (
<>
@ -44,15 +44,15 @@ const CoverArt: React.FC<{
}}
resizeMode={FastImage.resizeMode.contain}
onError={() => {
setLoading(false);
setPlaceholderVisible(true);
setLoading(false)
setPlaceholderVisible(true)
}}
onLoadEnd={() => setLoading(false)}
/>
</>
);
)
return <View style={{ height, width }}>{!coverArtUri ? <Placeholder visible={true} /> : <Art />}</View>;
};
return <View style={{ height, width }}>{!coverArtUri ? <Placeholder visible={true} /> : <Art />}</View>
}
export default React.memo(CoverArt);
export default React.memo(CoverArt)

View File

@ -1,17 +1,17 @@
import React from 'react';
import { useWindowDimensions, ViewStyle } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import colorStyles from '../../styles/colors';
import React from 'react'
import { useWindowDimensions, ViewStyle } from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import colorStyles from '../../styles/colors'
const GradientBackground: React.FC<{
height?: number | string;
width?: number | string;
position?: 'relative' | 'absolute';
style?: ViewStyle;
colors?: string[];
locations?: number[];
height?: number | string
width?: number | string
position?: 'relative' | 'absolute'
style?: ViewStyle
colors?: string[]
locations?: number[]
}> = ({ height, width, position, style, colors, locations, children }) => {
const layout = useWindowDimensions();
const layout = useWindowDimensions()
return (
<LinearGradient
@ -25,7 +25,7 @@ const GradientBackground: React.FC<{
}}>
{children}
</LinearGradient>
);
};
)
}
export default GradientBackground;
export default GradientBackground

View File

@ -1,10 +1,10 @@
import React from 'react';
import { FlatList, FlatListProps, useWindowDimensions } from 'react-native';
import colors from '../../styles/colors';
import GradientBackground from './GradientBackground';
import React from 'react'
import { FlatList, FlatListProps, useWindowDimensions } from 'react-native'
import colors from '../../styles/colors'
import GradientBackground from './GradientBackground'
function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) {
const layout = useWindowDimensions();
const layout = useWindowDimensions()
return (
<FlatList
@ -18,7 +18,7 @@ function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) {
marginBottom: -layout.height,
}}
/>
);
)
}
export default GradientFlatList;
export default GradientFlatList

View File

@ -1,18 +1,18 @@
import React from 'react';
import { ScrollView, ScrollViewProps, ViewStyle } from 'react-native';
import colors from '../../styles/colors';
import GradientBackground from './GradientBackground';
import React from 'react'
import { ScrollView, ScrollViewProps, ViewStyle } from 'react-native'
import colors from '../../styles/colors'
import GradientBackground from './GradientBackground'
const GradientScrollView: React.FC<ScrollViewProps> = props => {
props.style = props.style || {};
(props.style as ViewStyle).backgroundColor = colors.gradient.low;
props.style = props.style || {}
;(props.style as ViewStyle).backgroundColor = colors.gradient.low
return (
<ScrollView overScrollMode="never" {...props}>
<GradientBackground />
{props.children}
</ScrollView>
);
};
)
}
export default GradientScrollView;
export default GradientScrollView

View File

@ -1,58 +1,58 @@
import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { ViewStyle } from 'react-native';
import FastImage from 'react-native-fast-image';
import ImageColors from 'react-native-image-colors';
import { AndroidImageColors } from 'react-native-image-colors/lib/typescript/types';
import colors from '../../styles/colors';
import GradientBackground from './GradientBackground';
import { useNavigation } from '@react-navigation/native'
import React, { useEffect, useState } from 'react'
import { ViewStyle } from 'react-native'
import FastImage from 'react-native-fast-image'
import ImageColors from 'react-native-image-colors'
import { AndroidImageColors } from 'react-native-image-colors/lib/typescript/types'
import colors from '../../styles/colors'
import GradientBackground from './GradientBackground'
const ImageGradientBackground: React.FC<{
height?: number | string;
width?: number | string;
position?: 'relative' | 'absolute';
style?: ViewStyle;
imageUri?: string;
imageKey?: string;
height?: number | string
width?: number | string
position?: 'relative' | 'absolute'
style?: ViewStyle
imageUri?: string
imageKey?: string
}> = ({ height, width, position, style, imageUri, imageKey, children }) => {
const [highColor, setHighColor] = useState<string>(colors.gradient.high);
const navigation = useNavigation();
const [highColor, setHighColor] = useState<string>(colors.gradient.high)
const navigation = useNavigation()
useEffect(() => {
async function getColors() {
if (imageUri === undefined) {
return;
return
}
const cachedResult = ImageColors.cache.getItem(imageKey ? imageKey : imageUri);
const cachedResult = ImageColors.cache.getItem(imageKey ? imageKey : imageUri)
let res: AndroidImageColors;
let res: AndroidImageColors
if (cachedResult) {
res = cachedResult as AndroidImageColors;
res = cachedResult as AndroidImageColors
} else {
const path = await FastImage.getCachePath({ uri: imageUri });
const path = await FastImage.getCachePath({ uri: imageUri })
res = (await ImageColors.getColors(path ? `file://${path}` : imageUri, {
cache: true,
key: imageKey ? imageKey : imageUri,
})) as AndroidImageColors;
})) as AndroidImageColors
}
if (res.muted && res.muted !== '#000000') {
setHighColor(res.muted);
setHighColor(res.muted)
} else if (res.darkMuted && res.darkMuted !== '#000000') {
setHighColor(res.darkMuted);
setHighColor(res.darkMuted)
}
}
getColors();
}, [imageUri, imageKey]);
getColors()
}, [imageUri, imageKey])
useEffect(() => {
navigation.setOptions({
headerStyle: {
backgroundColor: highColor,
},
});
}, [navigation, highColor]);
})
}, [navigation, highColor])
return (
<GradientBackground
@ -64,7 +64,7 @@ const ImageGradientBackground: React.FC<{
locations={[0.1, 1.0]}>
{children}
</GradientBackground>
);
};
)
}
export default ImageGradientBackground;
export default ImageGradientBackground

View File

@ -1,17 +1,17 @@
import React, { useState } from 'react';
import { LayoutRectangle, ScrollView, ScrollViewProps } from 'react-native';
import colors from '../../styles/colors';
import ImageGradientBackground from './ImageGradientBackground';
import React, { useState } from 'react'
import { LayoutRectangle, ScrollView, ScrollViewProps } from 'react-native'
import colors from '../../styles/colors'
import ImageGradientBackground from './ImageGradientBackground'
const ImageGradientScrollView: React.FC<ScrollViewProps & { imageUri?: string; imageKey?: string }> = props => {
const [layout, setLayout] = useState<LayoutRectangle | undefined>(undefined);
const [layout, setLayout] = useState<LayoutRectangle | undefined>(undefined)
props.style = props.style || {};
props.style = props.style || {}
if (typeof props.style === 'object' && props.style !== null) {
props.style = {
...props.style,
backgroundColor: colors.gradient.low,
};
}
}
return (
@ -19,12 +19,12 @@ const ImageGradientScrollView: React.FC<ScrollViewProps & { imageUri?: string; i
overScrollMode="never"
{...props}
onLayout={event => {
setLayout(event.nativeEvent.layout);
setLayout(event.nativeEvent.layout)
}}>
<ImageGradientBackground height={layout?.height} imageUri={props.imageUri} imageKey={props.imageKey} />
{props.children}
</ScrollView>
);
};
)
}
export default ImageGradientScrollView;
export default ImageGradientScrollView

View File

@ -1,6 +1,6 @@
import React from 'react';
import LinearGradient from 'react-native-linear-gradient';
import colors from '../../styles/colors';
import React from 'react'
import LinearGradient from 'react-native-linear-gradient'
import colors from '../../styles/colors'
const TopTabContainer: React.FC<{}> = ({ children }) => (
<LinearGradient
@ -11,6 +11,6 @@ const TopTabContainer: React.FC<{}> = ({ children }) => (
}}>
{children}
</LinearGradient>
);
)
export default TopTabContainer;
export default TopTabContainer

View File

@ -1,21 +1,21 @@
import { useNavigation } from '@react-navigation/native';
import { useAtomValue } from 'jotai/utils';
import React, { useEffect } from 'react';
import { Pressable, Text, View } from 'react-native';
import { Album } from '../../models/music';
import { albumsAtom, albumsUpdatingAtom, useUpdateAlbums } from '../../state/music';
import textStyles from '../../styles/text';
import AlbumArt from '../common/AlbumArt';
import GradientFlatList from '../common/GradientFlatList';
import { useNavigation } from '@react-navigation/native'
import { useAtomValue } from 'jotai/utils'
import React, { useEffect } from 'react'
import { Pressable, Text, View } from 'react-native'
import { Album } from '../../models/music'
import { albumsAtom, albumsUpdatingAtom, useUpdateAlbums } from '../../state/music'
import textStyles from '../../styles/text'
import AlbumArt from '../common/AlbumArt'
import GradientFlatList from '../common/GradientFlatList'
const AlbumItem: React.FC<{
id: string;
name: string;
artist?: string;
id: string
name: string
artist?: string
}> = ({ id, name, artist }) => {
const navigation = useNavigation();
const navigation = useNavigation()
const size = 125;
const size = 125
return (
<Pressable
@ -44,26 +44,26 @@ const AlbumItem: React.FC<{
</Text>
</View>
</Pressable>
);
};
const MemoAlbumItem = React.memo(AlbumItem);
)
}
const MemoAlbumItem = React.memo(AlbumItem)
const AlbumListRenderItem: React.FC<{ item: Album }> = ({ item }) => (
<MemoAlbumItem id={item.id} name={item.name} artist={item.artist} />
);
)
const AlbumsList = () => {
const albums = useAtomValue(albumsAtom);
const updating = useAtomValue(albumsUpdatingAtom);
const updateAlbums = useUpdateAlbums();
const albums = useAtomValue(albumsAtom)
const updating = useAtomValue(albumsUpdatingAtom)
const updateAlbums = useUpdateAlbums()
const albumsList = Object.values(albums);
const albumsList = Object.values(albums)
useEffect(() => {
if (albumsList.length === 0) {
updateAlbums();
updateAlbums()
}
});
})
return (
<View style={{ flex: 1 }}>
@ -78,13 +78,13 @@ const AlbumsList = () => {
overScrollMode="never"
/>
</View>
);
};
)
}
const AlbumsTab = () => (
<React.Suspense fallback={<Text>Loading...</Text>}>
<AlbumsList />
</React.Suspense>
);
)
export default React.memo(AlbumsTab);
export default React.memo(AlbumsTab)

View File

@ -1,16 +1,16 @@
import { useNavigation } from '@react-navigation/native';
import { useAtomValue } from 'jotai/utils';
import React, { useEffect } from 'react';
import { Pressable } from 'react-native';
import { Text } from 'react-native';
import { Artist } from '../../models/music';
import { artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '../../state/music';
import textStyles from '../../styles/text';
import ArtistArt from '../common/ArtistArt';
import GradientFlatList from '../common/GradientFlatList';
import { useNavigation } from '@react-navigation/native'
import { useAtomValue } from 'jotai/utils'
import React, { useEffect } from 'react'
import { Pressable } from 'react-native'
import { Text } from 'react-native'
import { Artist } from '../../models/music'
import { artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '../../state/music'
import textStyles from '../../styles/text'
import ArtistArt from '../common/ArtistArt'
import GradientFlatList from '../common/GradientFlatList'
const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => {
const navigation = useNavigation();
const navigation = useNavigation()
return (
<Pressable
@ -30,27 +30,27 @@ const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => {
{item.name}
</Text>
</Pressable>
);
};
)
}
const ArtistItemLoader: React.FC<{ item: Artist }> = props => (
<React.Suspense fallback={<Text>Loading...</Text>}>
<ArtistItem {...props} />
</React.Suspense>
);
)
const ArtistsList = () => {
const artists = useAtomValue(artistsAtom);
const updating = useAtomValue(artistsUpdatingAtom);
const updateArtists = useUpdateArtists();
const artists = useAtomValue(artistsAtom)
const updating = useAtomValue(artistsUpdatingAtom)
const updateArtists = useUpdateArtists()
useEffect(() => {
if (artists.length === 0) {
updateArtists();
updateArtists()
}
});
})
const renderItem: React.FC<{ item: Artist }> = ({ item }) => <ArtistItemLoader item={item} />;
const renderItem: React.FC<{ item: Artist }> = ({ item }) => <ArtistItemLoader item={item} />
return (
<GradientFlatList
@ -61,9 +61,9 @@ const ArtistsList = () => {
refreshing={updating}
overScrollMode="never"
/>
);
};
)
}
const ArtistsTab = () => <ArtistsList />;
const ArtistsTab = () => <ArtistsList />
export default ArtistsTab;
export default ArtistsTab

View File

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

View File

@ -1,12 +1,12 @@
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import SettingsView from '../Settings';
import NowPlayingLayout from '../NowPlayingLayout';
import ArtistsList from '../ArtistsList';
import LibraryTopTabNavigator from './LibraryTopTabNavigator';
import BottomTabBar from '../common/BottomTabBar';
import React from 'react'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import SettingsView from '../Settings'
import NowPlayingLayout from '../NowPlayingLayout'
import ArtistsList from '../ArtistsList'
import LibraryTopTabNavigator from './LibraryTopTabNavigator'
import BottomTabBar from '../common/BottomTabBar'
const Tab = createBottomTabNavigator();
const Tab = createBottomTabNavigator()
const BottomTabNavigator = () => {
return (
@ -16,7 +16,7 @@ const BottomTabNavigator = () => {
<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;
export default BottomTabNavigator

View File

@ -1,17 +1,17 @@
import React from 'react';
import { StatusBar, View } from 'react-native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import AlbumsTab from '../library/AlbumsTab';
import ArtistsTab from '../library/ArtistsTab';
import PlaylistsTab from '../library/PlaylistsTab';
import { createNativeStackNavigator, NativeStackNavigationProp } from 'react-native-screens/native-stack';
import AlbumView from '../common/AlbumView';
import { RouteProp } from '@react-navigation/native';
import text from '../../styles/text';
import colors from '../../styles/colors';
import ArtistView from '../common/ArtistView';
import React from 'react'
import { StatusBar, View } from 'react-native'
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
import AlbumsTab from '../library/AlbumsTab'
import ArtistsTab from '../library/ArtistsTab'
import PlaylistsTab from '../library/PlaylistsTab'
import { createNativeStackNavigator, NativeStackNavigationProp } from 'react-native-screens/native-stack'
import AlbumView from '../common/AlbumView'
import { RouteProp } from '@react-navigation/native'
import text from '../../styles/text'
import colors from '../../styles/colors'
import ArtistView from '../common/ArtistView'
const Tab = createMaterialTopTabNavigator();
const Tab = createMaterialTopTabNavigator()
const LibraryTopTabNavigator = () => (
<Tab.Navigator
@ -36,37 +36,37 @@ const LibraryTopTabNavigator = () => (
<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 = NativeStackNavigationProp<LibraryStackParamList, 'AlbumView'>;
type AlbumScreenRouteProp = RouteProp<LibraryStackParamList, 'AlbumView'>;
type AlbumScreenNavigationProp = NativeStackNavigationProp<LibraryStackParamList, 'AlbumView'>
type AlbumScreenRouteProp = RouteProp<LibraryStackParamList, 'AlbumView'>
type AlbumScreenProps = {
route: AlbumScreenRouteProp;
navigation: AlbumScreenNavigationProp;
};
route: AlbumScreenRouteProp
navigation: AlbumScreenNavigationProp
}
const AlbumScreen: React.FC<AlbumScreenProps> = ({ route }) => (
<AlbumView id={route.params.id} title={route.params.title} />
);
)
type ArtistScreenNavigationProp = NativeStackNavigationProp<LibraryStackParamList, 'ArtistView'>;
type ArtistScreenRouteProp = RouteProp<LibraryStackParamList, 'ArtistView'>;
type ArtistScreenNavigationProp = NativeStackNavigationProp<LibraryStackParamList, 'ArtistView'>
type ArtistScreenRouteProp = RouteProp<LibraryStackParamList, 'ArtistView'>
type ArtistScreenProps = {
route: ArtistScreenRouteProp;
navigation: ArtistScreenNavigationProp;
};
route: ArtistScreenRouteProp
navigation: ArtistScreenNavigationProp
}
const ArtistScreen: React.FC<ArtistScreenProps> = ({ route }) => (
<ArtistView id={route.params.id} title={route.params.title} />
);
)
const Stack = createNativeStackNavigator<LibraryStackParamList>();
const Stack = createNativeStackNavigator<LibraryStackParamList>()
const itemScreenOptions = {
title: '',
@ -78,7 +78,7 @@ const itemScreenOptions = {
headerTitleStyle: {
...text.header,
} as any,
};
}
const LibraryStackNavigator = () => (
<View style={{ flex: 1 }}>
@ -88,6 +88,6 @@ const LibraryStackNavigator = () => (
<Stack.Screen name="ArtistView" component={ArtistScreen} options={itemScreenOptions} />
</Stack.Navigator>
</View>
);
)
export default LibraryStackNavigator;
export default LibraryStackNavigator

View File

@ -1,9 +1,9 @@
import React from 'react';
import { createNativeStackNavigator } from 'react-native-screens/native-stack';
import NowPlayingLayout from '../NowPlayingLayout';
import BottomTabNavigator from './BottomTabNavigator';
import React from 'react'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import NowPlayingLayout from '../NowPlayingLayout'
import BottomTabNavigator from './BottomTabNavigator'
const RootStack = createNativeStackNavigator();
const RootStack = createNativeStackNavigator()
const RootNavigator = () => (
<RootStack.Navigator
@ -13,6 +13,6 @@ const RootNavigator = () => (
<RootStack.Screen name="Main" component={BottomTabNavigator} />
<RootStack.Screen name="Now Playing" component={NowPlayingLayout} />
</RootStack.Navigator>
);
)
export default RootNavigator;
export default RootNavigator

View File

@ -1,14 +1,14 @@
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);
const activeServer = useAtomValue(activeServerAtom)
return () => {
if (!activeServer) {
return undefined;
return undefined
}
return new SubsonicApiClient(activeServer);
};
};
return new SubsonicApiClient(activeServer)
}
}

View File

@ -1,7 +1,7 @@
import { useUpdateAtom } from 'jotai/utils';
import TrackPlayer, { Track } from 'react-native-track-player';
import { Song } from '../models/music';
import { currentQueueNameAtom, currentTrackAtom } from '../state/trackplayer';
import { useUpdateAtom } from 'jotai/utils'
import TrackPlayer, { Track } from 'react-native-track-player'
import { Song } from '../models/music'
import { currentQueueNameAtom, currentTrackAtom } from '../state/trackplayer'
function mapSongToTrack(song: Song, queueName: string): Track {
return {
@ -13,39 +13,39 @@ function mapSongToTrack(song: Song, queueName: string): Track {
artwork: song.coverArtUri,
artworkThumb: song.coverArtThumbUri,
duration: song.duration,
};
}
}
export const useSetQueue = () => {
const setCurrentTrack = useUpdateAtom(currentTrackAtom);
const setCurrentQueueName = useUpdateAtom(currentQueueNameAtom);
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
const setCurrentQueueName = useUpdateAtom(currentQueueNameAtom)
return async (songs: Song[], name: string, playId?: string) => {
await TrackPlayer.reset();
const tracks = songs.map(s => mapSongToTrack(s, name));
await TrackPlayer.reset()
const tracks = songs.map(s => mapSongToTrack(s, name))
setCurrentQueueName(name);
setCurrentQueueName(name)
if (playId) {
setCurrentTrack(tracks.find(t => t.id === playId));
setCurrentTrack(tracks.find(t => t.id === playId))
}
if (!playId) {
await TrackPlayer.add(tracks);
await TrackPlayer.add(tracks)
} else if (playId === tracks[0].id) {
await TrackPlayer.add(tracks);
await TrackPlayer.play();
await TrackPlayer.add(tracks)
await TrackPlayer.play()
} else {
const playIndex = tracks.findIndex(t => t.id === playId);
const tracks1 = tracks.slice(0, playIndex);
const tracks2 = tracks.slice(playIndex);
const playIndex = tracks.findIndex(t => t.id === playId)
const tracks1 = tracks.slice(0, playIndex)
const tracks2 = tracks.slice(playIndex)
await TrackPlayer.add(tracks2);
await TrackPlayer.play();
await TrackPlayer.add(tracks2)
await TrackPlayer.play()
await TrackPlayer.add(tracks1, 0);
await TrackPlayer.add(tracks1, 0)
// const queue = await TrackPlayer.getQueue();
// console.log(`queue: ${JSON.stringify(queue.map(x => x.title))}`);
}
};
};
}
}

View File

@ -1,95 +1,95 @@
export interface Artist {
id: string;
name: string;
starred?: Date;
id: string
name: string
starred?: Date
}
export interface ArtistInfo extends Artist {
albums: Album[];
albums: Album[]
mediumImageUrl?: string;
largeImageUrl?: string;
coverArtUris: string[];
mediumImageUrl?: string
largeImageUrl?: string
coverArtUris: string[]
}
export interface ArtistArt {
uri?: string;
coverArtUris: string[];
uri?: string
coverArtUris: string[]
}
export interface Album {
id: string;
artistId?: string;
artist?: string;
name: string;
starred?: Date;
coverArt?: string;
coverArtUri?: string;
coverArtThumbUri?: string;
year?: number;
id: string
artistId?: string
artist?: string
name: string
starred?: Date
coverArt?: string
coverArtUri?: string
coverArtThumbUri?: string
year?: number
}
export interface AlbumArt {
uri?: string;
thumbUri?: string;
uri?: string
thumbUri?: string
}
export interface AlbumWithSongs extends Album {
songs: Song[];
songs: Song[]
}
export interface Song {
id: string;
album?: string;
artist?: string;
title: string;
track?: number;
year?: number;
genre?: string;
coverArt?: string;
size?: number;
contentType?: string;
suffix?: string;
duration?: number;
bitRate?: number;
userRating?: number;
averageRating?: number;
playCount?: number;
discNumber?: number;
created?: Date;
starred?: Date;
id: string
album?: string
artist?: string
title: string
track?: number
year?: number
genre?: string
coverArt?: string
size?: number
contentType?: string
suffix?: string
duration?: number
bitRate?: number
userRating?: number
averageRating?: number
playCount?: number
discNumber?: number
created?: Date
starred?: Date
streamUri: string;
coverArtUri?: string;
coverArtThumbUri?: string;
streamUri: string
coverArtUri?: string
coverArtThumbUri?: string
}
export type DownloadedSong = {
id: string;
type: 'song';
name: string;
album: string;
artist: string;
};
id: string
type: 'song'
name: string
album: string
artist: string
}
export type DownloadedAlbum = {
id: string;
type: 'album';
songs: string[];
name: string;
artist: string;
};
id: string
type: 'album'
songs: string[]
name: string
artist: string
}
export type DownloadedArtist = {
id: string;
type: 'artist';
songs: string[];
name: string;
};
id: string
type: 'artist'
songs: string[]
name: string
}
export type DownloadedPlaylist = {
id: string;
type: 'playlist';
songs: string[];
name: string;
};
id: string
type: 'playlist'
songs: string[]
name: string
}

View File

@ -1,12 +1,12 @@
export interface Server {
id: string;
address: string;
username: string;
token: string;
salt: string;
id: string
address: string
username: string
token: string
salt: string
}
export interface AppSettings {
servers: Server[];
activeServer?: string;
servers: Server[]
activeServer?: string
}

View File

@ -1,7 +1,7 @@
import RNFS from 'react-native-fs';
import RNFS from 'react-native-fs'
export default {
imageCache: `${RNFS.DocumentDirectoryPath}/image_cache`,
songCache: `${RNFS.DocumentDirectoryPath}/song_cache`,
songs: `${RNFS.DocumentDirectoryPath}/songs`,
};
}

View File

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

View File

@ -1,31 +1,31 @@
import { atom, useAtom } from 'jotai';
import { atomFamily, useAtomValue, useUpdateAtom } from 'jotai/utils';
import { Album, AlbumArt, AlbumWithSongs, Artist, ArtistArt, ArtistInfo, Song } from '../models/music';
import { SubsonicApiClient } from '../subsonic/api';
import { AlbumID3Element, ArtistInfo2Element, ChildElement } from '../subsonic/elements';
import { GetArtistResponse } from '../subsonic/responses';
import { activeServerAtom } from './settings';
import { atom, useAtom } from 'jotai'
import { atomFamily, useAtomValue, useUpdateAtom } from 'jotai/utils'
import { Album, AlbumArt, AlbumWithSongs, Artist, ArtistArt, ArtistInfo, Song } from '../models/music'
import { SubsonicApiClient } from '../subsonic/api'
import { AlbumID3Element, ArtistInfo2Element, ChildElement } from '../subsonic/elements'
import { GetArtistResponse } from '../subsonic/responses'
import { activeServerAtom } from './settings'
export const artistsAtom = atom<Artist[]>([]);
export const artistsUpdatingAtom = atom(false);
export const artistsAtom = atom<Artist[]>([])
export const artistsUpdatingAtom = atom(false)
export const useUpdateArtists = () => {
const server = useAtomValue(activeServerAtom);
const [updating, setUpdating] = useAtom(artistsUpdatingAtom);
const setArtists = useUpdateAtom(artistsAtom);
const server = useAtomValue(activeServerAtom)
const [updating, setUpdating] = useAtom(artistsUpdatingAtom)
const setArtists = useUpdateAtom(artistsAtom)
if (!server) {
return () => Promise.resolve();
return () => Promise.resolve()
}
return async () => {
if (updating) {
return;
return
}
setUpdating(true);
setUpdating(true)
const client = new SubsonicApiClient(server);
const response = await client.getArtists();
const client = new SubsonicApiClient(server)
const response = await client.getArtists()
setArtists(
response.data.artists.map(x => ({
@ -33,146 +33,146 @@ export const useUpdateArtists = () => {
name: x.name,
starred: x.starred,
})),
);
setUpdating(false);
};
};
)
setUpdating(false)
}
}
export const albumsAtom = atom<Record<string, Album>>({});
export const albumsUpdatingAtom = atom(false);
export const albumsAtom = atom<Record<string, Album>>({})
export const albumsUpdatingAtom = atom(false)
export const useUpdateAlbums = () => {
const server = useAtomValue(activeServerAtom);
const [updating, setUpdating] = useAtom(albumsUpdatingAtom);
const setAlbums = useUpdateAtom(albumsAtom);
const server = useAtomValue(activeServerAtom)
const [updating, setUpdating] = useAtom(albumsUpdatingAtom)
const setAlbums = useUpdateAtom(albumsAtom)
if (!server) {
return () => Promise.resolve();
return () => Promise.resolve()
}
return async () => {
if (updating) {
return;
return
}
setUpdating(true);
setUpdating(true)
const client = new SubsonicApiClient(server);
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 });
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;
const album = mapAlbumID3(next, client)
acc[album.id] = 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);
const server = get(activeServerAtom)
if (!server) {
return undefined;
return undefined
}
const client = new SubsonicApiClient(server);
const response = await client.getAlbum({ id });
return mapAlbumID3WithSongs(response.data.album, response.data.songs, client);
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);
const server = get(activeServerAtom)
if (!server) {
return undefined;
return undefined
}
const albums = get(albumsAtom);
const album = id in albums ? albums[id] : undefined;
const albums = get(albumsAtom)
const album = id in albums ? albums[id] : undefined
if (!album) {
return undefined;
return undefined
}
const client = new SubsonicApiClient(server);
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);
const server = get(activeServerAtom)
if (!server) {
return undefined;
return undefined
}
const client = new SubsonicApiClient(server);
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);
])
return mapArtistInfo(artistResponse.data, artistInfoResponse.data.artistInfo, client)
}),
);
)
export const artistArtAtomFamily = atomFamily((id: string) =>
atom<ArtistArt | undefined>(async get => {
const artistInfo = get(artistInfoAtomFamily(id));
const artistInfo = get(artistInfoAtomFamily(id))
if (!artistInfo) {
return undefined;
return undefined
}
const coverArtUris = artistInfo.albums
.filter(a => a.coverArtThumbUri !== undefined)
.sort((a, b) => {
if (b.year && a.year) {
return b.year - a.year;
return b.year - a.year
} else {
return a.name.localeCompare(b.name);
return a.name.localeCompare(b.name)
}
})
.map(a => a.coverArtThumbUri) as string[];
.map(a => a.coverArtThumbUri) as string[]
return {
coverArtUris,
uri: artistInfo.mediumImageUrl,
};
}
}),
);
)
function mapArtistInfo(
artistResponse: GetArtistResponse,
artistInfo: ArtistInfo2Element,
client: SubsonicApiClient,
): ArtistInfo {
const info = { ...artistInfo } as any;
delete info.similarArtists;
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 mappedAlbums = albums.map(a => mapAlbumID3(a, client))
const coverArtUris = mappedAlbums
.sort((a, b) => {
if (a.year && b.year) {
return a.year - b.year;
return a.year - b.year
} else {
return a.name.localeCompare(b.name) - 9000;
return a.name.localeCompare(b.name) - 9000
}
})
.map(a => a.coverArtThumbUri);
.map(a => a.coverArtThumbUri)
return {
...artist,
...info,
albums: mappedAlbums,
coverArtUris,
};
}
}
function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
@ -180,7 +180,7 @@ function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
...album,
coverArtUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt }) : undefined,
coverArtThumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined,
};
}
}
function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
@ -189,7 +189,7 @@ function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
streamUri: client.streamUri({ id: child.id }),
coverArtUri: child.coverArt ? client.getCoverArtUri({ id: child.coverArt }) : undefined,
coverArtThumbUri: child.coverArt ? client.getCoverArtUri({ id: child.coverArt, size: '256' }) : undefined,
};
}
}
function mapAlbumID3WithSongs(
@ -200,5 +200,5 @@ function mapAlbumID3WithSongs(
return {
...mapAlbumID3(album, client),
songs: songs.map(s => mapChildToSong(s, client)),
};
}
}

View File

@ -1,12 +1,12 @@
import { atom } from 'jotai';
import { AppSettings } from '../models/settings';
import atomWithAsyncStorage from '../storage/atomWithAsyncStorage';
import { atom } from 'jotai'
import { AppSettings } from '../models/settings'
import atomWithAsyncStorage from '../storage/atomWithAsyncStorage'
export const appSettingsAtom = atomWithAsyncStorage<AppSettings>('@appSettings', {
servers: [],
});
})
export const activeServerAtom = atom(get => {
const appSettings = get(appSettingsAtom);
return appSettings.servers.find(x => x.id === appSettings.activeServer);
});
const appSettings = get(appSettingsAtom)
return appSettings.servers.find(x => x.id === appSettings.activeServer)
})

View File

@ -1,37 +1,37 @@
import { atom } from 'jotai';
import { State, Track } from 'react-native-track-player';
import equal from 'fast-deep-equal';
import { atom } from 'jotai'
import { State, Track } from 'react-native-track-player'
import equal from 'fast-deep-equal'
type OptionalTrack = Track | undefined;
type OptionalTrack = Track | undefined
const currentTrack = atom<OptionalTrack>(undefined);
const currentTrack = atom<OptionalTrack>(undefined)
export const currentTrackAtom = atom<OptionalTrack, OptionalTrack>(
get => get(currentTrack),
(get, set, value) => {
if (!equal(get(currentTrack), value)) {
set(currentTrack, value);
set(currentTrack, value)
}
},
);
)
type OptionalString = string | undefined;
type OptionalString = string | undefined
const currentQueueName = atom<OptionalString>(undefined);
const currentQueueName = atom<OptionalString>(undefined)
export const currentQueueNameAtom = atom<OptionalString, OptionalString>(
get => get(currentQueueName),
(get, set, value) => {
if (get(currentQueueName) !== value) {
set(currentQueueName, value);
set(currentQueueName, value)
}
},
);
)
const playerState = atom<State>(State.None);
const playerState = atom<State>(State.None)
export const playerStateAtom = atom<State, State>(
get => get(playerState),
(get, set, value) => {
if (get(playerState) !== value) {
set(playerState, value);
set(playerState, value)
}
},
);
)

View File

@ -1,54 +1,54 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import AsyncStorage from '@react-native-async-storage/async-storage'
export async function getItem(key: string): Promise<any | null> {
try {
const item = await AsyncStorage.getItem(key);
return item ? JSON.parse(item) : null;
const item = await AsyncStorage.getItem(key)
return item ? JSON.parse(item) : null
} catch (e) {
console.error(`getItem error (key: ${key})`, e);
return null;
console.error(`getItem error (key: ${key})`, e)
return null
}
}
export async function multiGet(keys: string[]): Promise<[string, any | null][]> {
try {
const items = await AsyncStorage.multiGet(keys);
return items.map(x => [x[0], x[1] ? JSON.parse(x[1]) : 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);
return [];
console.error('multiGet error', e)
return []
}
}
export async function setItem(key: string, item: any): Promise<void> {
try {
await AsyncStorage.setItem(key, JSON.stringify(item));
await AsyncStorage.setItem(key, JSON.stringify(item))
} catch (e) {
console.error(`setItem error (key: ${key})`, e);
console.error(`setItem error (key: ${key})`, e)
}
}
export async function multiSet(items: string[][]): Promise<void> {
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) {
console.error('multiSet error', e);
console.error('multiSet error', e)
}
}
export async function getAllKeys(): Promise<string[]> {
try {
return await AsyncStorage.getAllKeys();
return await AsyncStorage.getAllKeys()
} catch (e) {
console.error('getAllKeys error', e);
return [];
console.error('getAllKeys error', e)
return []
}
}
export async function multiRemove(keys: string[]): Promise<void> {
try {
await AsyncStorage.multiRemove(keys);
await AsyncStorage.multiRemove(keys)
} catch (e) {
console.error('multiRemove error', e);
console.error('multiRemove error', e)
}
}

View File

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

View File

@ -1,26 +1,26 @@
import { DownloadedSong } from '../models/music';
import { getItem, multiGet, multiSet } from './asyncstorage';
import { DownloadedSong } from '../models/music'
import { getItem, multiGet, multiSet } from './asyncstorage'
const key = {
downloadedSongKeys: '@downloadedSongKeys',
downloadedAlbumKeys: '@downloadedAlbumKeys',
downloadedArtistKeys: '@downloadedArtistKeys',
downloadedPlaylistKeys: '@downloadedPlaylistKeys',
};
}
export async function getDownloadedSongs(): Promise<DownloadedSong[]> {
const keysItem = await getItem(key.downloadedSongKeys);
const keys: string[] = keysItem ? JSON.parse(keysItem) : [];
const keysItem = await getItem(key.downloadedSongKeys)
const keys: string[] = keysItem ? JSON.parse(keysItem) : []
const items = await multiGet(keys);
const items = await multiGet(keys)
return items.map(x => {
const parsed = JSON.parse(x[1] as string);
const parsed = JSON.parse(x[1] as string)
return {
id: x[0],
type: 'song',
...parsed,
};
});
}
})
}
export async function setDownloadedSongs(items: DownloadedSong[]): Promise<void> {
@ -34,5 +34,5 @@ export async function setDownloadedSongs(items: DownloadedSong[]): Promise<void>
artist: x.artist,
}),
]),
]);
])
}

View File

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

View File

@ -1,62 +1,62 @@
import { TextStyle } from 'react-native';
import colors from './colors';
import { TextStyle } from 'react-native'
import colors from './colors'
const fontRegular = 'Metropolis-Regular';
const fontSemiBold = 'Metropolis-SemiBold';
const fontBold = 'Metropolis-Bold';
const fontRegular = 'Metropolis-Regular'
const fontSemiBold = 'Metropolis-SemiBold'
const fontBold = 'Metropolis-Bold'
const paragraph: TextStyle = {
fontFamily: fontRegular,
fontSize: 16,
color: colors.text.primary,
};
}
const header: TextStyle = {
...paragraph,
fontSize: 18,
fontFamily: fontSemiBold,
};
}
const title: TextStyle = {
...paragraph,
fontSize: 24,
fontFamily: fontBold,
};
}
const itemTitle: TextStyle = {
...paragraph,
fontSize: 13,
fontFamily: fontSemiBold,
};
}
const itemSubtitle: TextStyle = {
...paragraph,
fontSize: 12,
color: colors.text.secondary,
};
}
const songListTitle: TextStyle = {
...paragraph,
fontSize: 16,
fontFamily: fontSemiBold,
};
}
const songListSubtitle: TextStyle = {
...paragraph,
fontSize: 14,
color: colors.text.secondary,
};
}
const xsmall: TextStyle = {
...paragraph,
fontSize: 10,
};
}
const button: TextStyle = {
...paragraph,
fontSize: 15,
fontFamily: fontBold,
};
}
export default {
paragraph,
@ -68,4 +68,4 @@ export default {
songListSubtitle,
xsmall,
button,
};
}

View File

@ -1,5 +1,5 @@
import { DOMParser } from 'xmldom';
import RNFS from 'react-native-fs';
import { DOMParser } from 'xmldom'
import RNFS from 'react-native-fs'
import {
GetAlbumList2Params,
GetAlbumListParams,
@ -11,7 +11,7 @@ import {
GetIndexesParams,
GetMusicDirectoryParams,
StreamParams,
} from './params';
} from './params'
import {
GetAlbumList2Response,
GetAlbumListResponse,
@ -23,127 +23,127 @@ import {
GetIndexesResponse,
GetMusicDirectoryResponse,
SubsonicResponse,
} from './responses';
import { Server } from '../models/settings';
import paths from '../paths';
} from './responses'
import { Server } from '../models/settings'
import paths from '../paths'
export class SubsonicApiError extends Error {
method: string;
code: string;
method: string
code: string
constructor(method: string, xml: Document) {
const errorElement = xml.getElementsByTagName('error')[0];
const errorElement = xml.getElementsByTagName('error')[0]
super(errorElement.getAttribute('message') as string);
super(errorElement.getAttribute('message') as string)
this.name = method;
this.method = method;
this.code = errorElement.getAttribute('code') as string;
this.name = method
this.method = method
this.code = errorElement.getAttribute('code') as string
}
}
type QueuePromise = () => Promise<any>;
type QueuePromise = () => Promise<any>
class Queue {
maxSimultaneously: number;
maxSimultaneously: number
private active = 0;
private queue: QueuePromise[] = [];
private active = 0
private queue: QueuePromise[] = []
constructor(maxSimultaneously = 1) {
this.maxSimultaneously = maxSimultaneously;
this.maxSimultaneously = maxSimultaneously
}
async enqueue(func: QueuePromise) {
if (++this.active > this.maxSimultaneously) {
await new Promise(resolve => this.queue.push(resolve as QueuePromise));
await new Promise(resolve => this.queue.push(resolve as QueuePromise))
}
try {
return await func();
return await func()
} catch (err) {
throw err;
throw err
} finally {
this.active--;
this.active--
if (this.queue.length) {
(this.queue.shift() as QueuePromise)();
this.queue.shift()?.()
}
}
}
}
const downloadQueue = new Queue(1);
const downloadQueue = new Queue(1)
export class SubsonicApiClient {
address: string;
username: string;
address: string
username: string
private params: URLSearchParams;
private params: URLSearchParams
constructor(server: Server) {
this.address = server.address;
this.username = server.username;
this.address = server.address
this.username = server.username
this.params = new URLSearchParams();
this.params.append('u', server.username);
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 = new URLSearchParams()
this.params.append('u', server.username)
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')
}
private buildUrl(method: string, params?: { [key: string]: any }): string {
let query = this.params.toString();
let query = this.params.toString()
if (params) {
const urlParams = this.obj2Params(params);
const urlParams = this.obj2Params(params)
if (urlParams) {
query += '&' + urlParams.toString();
query += '&' + urlParams.toString()
}
}
const url = `${this.address}/rest/${method}?${query}`;
const url = `${this.address}/rest/${method}?${query}`
// console.log(url);
return url;
return url
}
private async apiDownload(method: string, path: string, params?: { [key: string]: any }): Promise<string> {
const download = RNFS.downloadFile({
fromUrl: this.buildUrl(method, params),
toFile: path,
}).promise;
}).promise
await downloadQueue.enqueue(() => download);
await downloadQueue.enqueue(() => new Promise(resolve => setTimeout(resolve, 100)));
await downloadQueue.enqueue(() => download)
await downloadQueue.enqueue(() => new Promise(resolve => setTimeout(resolve, 100)))
return path;
return path
}
private async apiGetXml(method: string, params?: { [key: string]: any }): Promise<Document> {
const response = await fetch(this.buildUrl(method, params));
const text = await response.text();
const response = await fetch(this.buildUrl(method, params))
const text = await response.text()
// console.log(text);
const xml = new DOMParser().parseFromString(text);
const xml = new DOMParser().parseFromString(text)
if (xml.documentElement.getAttribute('status') !== 'ok') {
throw new SubsonicApiError(method, xml);
throw new SubsonicApiError(method, xml)
}
return xml;
return xml
}
private obj2Params(obj: { [key: string]: any }): URLSearchParams | undefined {
const keys = Object.keys(obj);
const keys = Object.keys(obj)
if (keys.length === 0) {
return undefined;
return undefined
}
const params = new URLSearchParams();
const params = new URLSearchParams()
for (const key of keys) {
params.append(key, String(obj[key]));
params.append(key, String(obj[key]))
}
return params;
return params
}
//
@ -151,8 +151,8 @@ export class SubsonicApiClient {
//
async ping(): Promise<SubsonicResponse<null>> {
const xml = await this.apiGetXml('ping');
return new SubsonicResponse<null>(xml, null);
const xml = await this.apiGetXml('ping')
return new SubsonicResponse<null>(xml, null)
}
//
@ -160,38 +160,38 @@ export class SubsonicApiClient {
//
async getArtists(): Promise<SubsonicResponse<GetArtistsResponse>> {
const xml = await this.apiGetXml('getArtists');
return new SubsonicResponse<GetArtistsResponse>(xml, new GetArtistsResponse(xml));
const xml = await this.apiGetXml('getArtists')
return new SubsonicResponse<GetArtistsResponse>(xml, new GetArtistsResponse(xml))
}
async getIndexes(params?: GetIndexesParams): Promise<SubsonicResponse<GetIndexesResponse>> {
const xml = await this.apiGetXml('getIndexes', params);
return new SubsonicResponse<GetIndexesResponse>(xml, new GetIndexesResponse(xml));
const xml = await this.apiGetXml('getIndexes', params)
return new SubsonicResponse<GetIndexesResponse>(xml, new GetIndexesResponse(xml))
}
async getMusicDirectory(params: GetMusicDirectoryParams): Promise<SubsonicResponse<GetMusicDirectoryResponse>> {
const xml = await this.apiGetXml('getMusicDirectory', params);
return new SubsonicResponse<GetMusicDirectoryResponse>(xml, new GetMusicDirectoryResponse(xml));
const xml = await this.apiGetXml('getMusicDirectory', params)
return new SubsonicResponse<GetMusicDirectoryResponse>(xml, new GetMusicDirectoryResponse(xml))
}
async getAlbum(params: GetAlbumParams): Promise<SubsonicResponse<GetAlbumResponse>> {
const xml = await this.apiGetXml('getAlbum', params);
return new SubsonicResponse<GetAlbumResponse>(xml, new GetAlbumResponse(xml));
const xml = await this.apiGetXml('getAlbum', params)
return new SubsonicResponse<GetAlbumResponse>(xml, new GetAlbumResponse(xml))
}
async getArtistInfo(params: GetArtistInfoParams): Promise<SubsonicResponse<GetArtistInfoResponse>> {
const xml = await this.apiGetXml('getArtistInfo', params);
return new SubsonicResponse<GetArtistInfoResponse>(xml, new GetArtistInfoResponse(xml));
const xml = await this.apiGetXml('getArtistInfo', params)
return new SubsonicResponse<GetArtistInfoResponse>(xml, new GetArtistInfoResponse(xml))
}
async getArtistInfo2(params: GetArtistInfo2Params): Promise<SubsonicResponse<GetArtistInfo2Response>> {
const xml = await this.apiGetXml('getArtistInfo2', params);
return new SubsonicResponse<GetArtistInfo2Response>(xml, new GetArtistInfo2Response(xml));
const xml = await this.apiGetXml('getArtistInfo2', params)
return new SubsonicResponse<GetArtistInfo2Response>(xml, new GetArtistInfo2Response(xml))
}
async getArtist(params: GetArtistParams): Promise<SubsonicResponse<GetArtistResponse>> {
const xml = await this.apiGetXml('getArtist', params);
return new SubsonicResponse<GetArtistResponse>(xml, new GetArtistResponse(xml));
const xml = await this.apiGetXml('getArtist', params)
return new SubsonicResponse<GetArtistResponse>(xml, new GetArtistResponse(xml))
}
//
@ -199,13 +199,13 @@ export class SubsonicApiClient {
//
async getAlbumList(params: GetAlbumListParams): Promise<SubsonicResponse<GetAlbumListResponse>> {
const xml = await this.apiGetXml('getAlbumList', params);
return new SubsonicResponse<GetAlbumListResponse>(xml, new GetAlbumListResponse(xml));
const xml = await this.apiGetXml('getAlbumList', params)
return new SubsonicResponse<GetAlbumListResponse>(xml, new GetAlbumListResponse(xml))
}
async getAlbumList2(params: GetAlbumList2Params): Promise<SubsonicResponse<GetAlbumList2Response>> {
const xml = await this.apiGetXml('getAlbumList2', params);
return new SubsonicResponse<GetAlbumList2Response>(xml, new GetAlbumList2Response(xml));
const xml = await this.apiGetXml('getAlbumList2', params)
return new SubsonicResponse<GetAlbumList2Response>(xml, new GetAlbumList2Response(xml))
}
//
@ -213,15 +213,15 @@ export class SubsonicApiClient {
//
async getCoverArt(params: GetCoverArtParams): Promise<string> {
const path = `${paths.songCache}/${params.id}`;
return await this.apiDownload('getCoverArt', path, params);
const path = `${paths.songCache}/${params.id}`
return await this.apiDownload('getCoverArt', path, params)
}
getCoverArtUri(params: GetCoverArtParams): string {
return this.buildUrl('getCoverArt', params);
return this.buildUrl('getCoverArt', params)
}
streamUri(params: StreamParams): string {
return this.buildUrl('stream', params);
return this.buildUrl('stream', params)
}
}

View File

@ -1,237 +1,237 @@
function requiredString(e: Element, name: string): string {
return e.getAttribute(name) as string;
return e.getAttribute(name) as string
}
function optionalString(e: Element, name: string): string | undefined {
return e.hasAttribute(name) ? requiredString(e, name) : undefined;
return e.hasAttribute(name) ? requiredString(e, name) : undefined
}
function requiredBoolean(e: Element, name: string): boolean {
return (e.getAttribute(name) as string).toLowerCase() === 'true';
return (e.getAttribute(name) as string).toLowerCase() === 'true'
}
function optionalBoolean(e: Element, name: string): boolean | undefined {
return e.hasAttribute(name) ? requiredBoolean(e, name) : undefined;
return e.hasAttribute(name) ? requiredBoolean(e, name) : undefined
}
function requiredInt(e: Element, name: string): number {
return parseInt(e.getAttribute(name) as string);
return parseInt(e.getAttribute(name) as string)
}
function optionalInt(e: Element, name: string): number | undefined {
return e.hasAttribute(name) ? requiredInt(e, name) : undefined;
return e.hasAttribute(name) ? requiredInt(e, name) : undefined
}
function requiredFloat(e: Element, name: string): number {
return parseFloat(e.getAttribute(name) as string);
return parseFloat(e.getAttribute(name) as string)
}
function optionalFloat(e: Element, name: string): number | undefined {
return e.hasAttribute(name) ? requiredFloat(e, name) : undefined;
return e.hasAttribute(name) ? requiredFloat(e, name) : undefined
}
function requiredDate(e: Element, name: string): Date {
return new Date(e.getAttribute(name) as string);
return new Date(e.getAttribute(name) as string)
}
function optionalDate(e: Element, name: string): Date | undefined {
return e.hasAttribute(name) ? requiredDate(e, name) : undefined;
return e.hasAttribute(name) ? requiredDate(e, name) : undefined
}
export class BaseArtistElement {
id: string;
name: string;
starred?: Date;
id: string
name: string
starred?: Date
constructor(e: Element) {
this.id = requiredString(e, 'id');
this.name = requiredString(e, 'name');
this.starred = optionalDate(e, 'starred');
this.id = requiredString(e, 'id')
this.name = requiredString(e, 'name')
this.starred = optionalDate(e, 'starred')
}
}
export class ArtistID3Element extends BaseArtistElement {
coverArt?: string;
albumCount?: number;
coverArt?: string
albumCount?: number
constructor(e: Element) {
super(e);
this.coverArt = optionalString(e, 'coverArt');
this.albumCount = optionalInt(e, 'albumCount');
super(e)
this.coverArt = optionalString(e, 'coverArt')
this.albumCount = optionalInt(e, 'albumCount')
}
}
export class ArtistElement extends BaseArtistElement {
userRating?: number;
averageRating?: number;
userRating?: number
averageRating?: number
constructor(e: Element) {
super(e);
this.userRating = optionalInt(e, 'userRating');
this.averageRating = optionalFloat(e, 'averageRating');
super(e)
this.userRating = optionalInt(e, 'userRating')
this.averageRating = optionalFloat(e, 'averageRating')
}
}
export class BaseArtistInfoElement<T> {
similarArtists: T[] = [];
biography?: string;
musicBrainzId?: string;
lastFmUrl?: string;
smallImageUrl?: string;
mediumImageUrl?: string;
largeImageUrl?: string;
similarArtists: T[] = []
biography?: string
musicBrainzId?: string
lastFmUrl?: string
smallImageUrl?: string
mediumImageUrl?: string
largeImageUrl?: string
constructor(e: Element, artistType: new (e: Element) => T) {
if (e.getElementsByTagName('biography').length > 0) {
this.biography = e.getElementsByTagName('biography')[0].textContent as string;
this.biography = e.getElementsByTagName('biography')[0].textContent as string
}
if (e.getElementsByTagName('musicBrainzId').length > 0) {
this.musicBrainzId = e.getElementsByTagName('musicBrainzId')[0].textContent as string;
this.musicBrainzId = e.getElementsByTagName('musicBrainzId')[0].textContent as string
}
if (e.getElementsByTagName('lastFmUrl').length > 0) {
this.lastFmUrl = e.getElementsByTagName('lastFmUrl')[0].textContent as string;
this.lastFmUrl = e.getElementsByTagName('lastFmUrl')[0].textContent as string
}
if (e.getElementsByTagName('smallImageUrl').length > 0) {
this.smallImageUrl = e.getElementsByTagName('smallImageUrl')[0].textContent as string;
this.smallImageUrl = e.getElementsByTagName('smallImageUrl')[0].textContent as string
}
if (e.getElementsByTagName('mediumImageUrl').length > 0) {
this.mediumImageUrl = e.getElementsByTagName('mediumImageUrl')[0].textContent as string;
this.mediumImageUrl = e.getElementsByTagName('mediumImageUrl')[0].textContent as string
}
if (e.getElementsByTagName('largeImageUrl').length > 0) {
this.largeImageUrl = e.getElementsByTagName('largeImageUrl')[0].textContent as string;
this.largeImageUrl = e.getElementsByTagName('largeImageUrl')[0].textContent as string
}
const similarArtistElements = e.getElementsByTagName('similarArtist');
const similarArtistElements = e.getElementsByTagName('similarArtist')
for (let i = 0; i < similarArtistElements.length; i++) {
this.similarArtists.push(new artistType(similarArtistElements[i]));
this.similarArtists.push(new artistType(similarArtistElements[i]))
}
}
}
export class ArtistInfoElement extends BaseArtistInfoElement<ArtistElement> {
constructor(e: Element) {
super(e, ArtistElement);
super(e, ArtistElement)
}
}
export class ArtistInfo2Element extends BaseArtistInfoElement<ArtistID3Element> {
constructor(e: Element) {
super(e, ArtistID3Element);
super(e, ArtistID3Element)
}
}
export class DirectoryElement {
id: string;
parent?: string;
name: string;
starred?: Date;
userRating?: number;
averageRating?: number;
playCount?: number;
id: string
parent?: string
name: string
starred?: Date
userRating?: number
averageRating?: number
playCount?: number
constructor(e: Element) {
this.id = requiredString(e, 'id');
this.parent = optionalString(e, 'parent');
this.name = requiredString(e, 'name');
this.starred = optionalDate(e, 'starred');
this.userRating = optionalInt(e, 'userRating');
this.averageRating = optionalFloat(e, 'averageRating');
this.id = requiredString(e, 'id')
this.parent = optionalString(e, 'parent')
this.name = requiredString(e, 'name')
this.starred = optionalDate(e, 'starred')
this.userRating = optionalInt(e, 'userRating')
this.averageRating = optionalFloat(e, 'averageRating')
}
}
export class ChildElement {
id: string;
parent?: string;
isDir: boolean;
title: string;
album?: string;
artist?: string;
track?: number;
year?: number;
genre?: string;
coverArt?: string;
size?: number;
contentType?: string;
suffix?: string;
transcodedContentType?: string;
transcodedSuffix?: string;
duration?: number;
bitRate?: number;
path?: string;
isVideo?: boolean;
userRating?: number;
averageRating?: number;
playCount?: number;
discNumber?: number;
created?: Date;
starred?: Date;
albumId?: string;
artistId?: string;
type?: string;
bookmarkPosition?: number;
originalWidth?: number;
originalHeight?: number;
id: string
parent?: string
isDir: boolean
title: string
album?: string
artist?: string
track?: number
year?: number
genre?: string
coverArt?: string
size?: number
contentType?: string
suffix?: string
transcodedContentType?: string
transcodedSuffix?: string
duration?: number
bitRate?: number
path?: string
isVideo?: boolean
userRating?: number
averageRating?: number
playCount?: number
discNumber?: number
created?: Date
starred?: Date
albumId?: string
artistId?: string
type?: string
bookmarkPosition?: number
originalWidth?: number
originalHeight?: number
constructor(e: Element) {
this.id = requiredString(e, 'id');
this.parent = optionalString(e, 'parent');
this.isDir = requiredBoolean(e, 'isDir');
this.title = requiredString(e, 'title');
this.album = optionalString(e, 'album');
this.artist = optionalString(e, 'artist');
this.track = optionalInt(e, 'track');
this.year = optionalInt(e, 'year');
this.genre = optionalString(e, 'genre');
this.coverArt = optionalString(e, 'coverArt');
this.size = optionalInt(e, 'size');
this.contentType = optionalString(e, 'contentType');
this.suffix = optionalString(e, 'suffix');
this.transcodedContentType = optionalString(e, 'transcodedContentType');
this.transcodedSuffix = optionalString(e, 'transcodedSuffix');
this.duration = optionalInt(e, 'duration');
this.bitRate = optionalInt(e, 'bitRate');
this.path = optionalString(e, 'path');
this.isVideo = optionalBoolean(e, 'isVideo');
this.userRating = optionalInt(e, 'userRating');
this.averageRating = optionalFloat(e, 'averageRating');
this.playCount = optionalInt(e, 'playCount');
this.discNumber = optionalInt(e, 'discNumber');
this.created = optionalDate(e, 'created');
this.starred = optionalDate(e, 'starred');
this.albumId = optionalString(e, 'albumId');
this.artistId = optionalString(e, 'artistId');
this.type = optionalString(e, 'type');
this.bookmarkPosition = optionalInt(e, 'bookmarkPosition');
this.originalWidth = optionalInt(e, 'originalWidth');
this.originalHeight = optionalInt(e, 'originalHeight');
this.id = requiredString(e, 'id')
this.parent = optionalString(e, 'parent')
this.isDir = requiredBoolean(e, 'isDir')
this.title = requiredString(e, 'title')
this.album = optionalString(e, 'album')
this.artist = optionalString(e, 'artist')
this.track = optionalInt(e, 'track')
this.year = optionalInt(e, 'year')
this.genre = optionalString(e, 'genre')
this.coverArt = optionalString(e, 'coverArt')
this.size = optionalInt(e, 'size')
this.contentType = optionalString(e, 'contentType')
this.suffix = optionalString(e, 'suffix')
this.transcodedContentType = optionalString(e, 'transcodedContentType')
this.transcodedSuffix = optionalString(e, 'transcodedSuffix')
this.duration = optionalInt(e, 'duration')
this.bitRate = optionalInt(e, 'bitRate')
this.path = optionalString(e, 'path')
this.isVideo = optionalBoolean(e, 'isVideo')
this.userRating = optionalInt(e, 'userRating')
this.averageRating = optionalFloat(e, 'averageRating')
this.playCount = optionalInt(e, 'playCount')
this.discNumber = optionalInt(e, 'discNumber')
this.created = optionalDate(e, 'created')
this.starred = optionalDate(e, 'starred')
this.albumId = optionalString(e, 'albumId')
this.artistId = optionalString(e, 'artistId')
this.type = optionalString(e, 'type')
this.bookmarkPosition = optionalInt(e, 'bookmarkPosition')
this.originalWidth = optionalInt(e, 'originalWidth')
this.originalHeight = optionalInt(e, 'originalHeight')
}
}
export class AlbumID3Element {
id: string;
name: string;
artist?: string;
artistId?: string;
coverArt?: string;
songCount: number;
duration: number;
playCount?: number;
created: Date;
starred?: Date;
year?: number;
genre?: string;
id: string
name: string
artist?: string
artistId?: string
coverArt?: string
songCount: number
duration: number
playCount?: number
created: Date
starred?: Date
year?: number
genre?: string
constructor(e: Element) {
this.id = requiredString(e, 'id');
this.name = requiredString(e, 'name');
this.artist = optionalString(e, 'artist');
this.artistId = optionalString(e, 'artistId');
this.coverArt = optionalString(e, 'coverArt');
this.songCount = requiredInt(e, 'songCount');
this.duration = requiredInt(e, 'duration');
this.playCount = optionalInt(e, 'playCount');
this.created = requiredDate(e, 'created');
this.starred = optionalDate(e, 'starred');
this.year = optionalInt(e, 'year');
this.genre = optionalString(e, 'genre');
this.id = requiredString(e, 'id')
this.name = requiredString(e, 'name')
this.artist = optionalString(e, 'artist')
this.artistId = optionalString(e, 'artistId')
this.coverArt = optionalString(e, 'coverArt')
this.songCount = requiredInt(e, 'songCount')
this.duration = requiredInt(e, 'duration')
this.playCount = optionalInt(e, 'playCount')
this.created = requiredDate(e, 'created')
this.starred = optionalDate(e, 'starred')
this.year = optionalInt(e, 'year')
this.genre = optionalString(e, 'genre')
}
}

View File

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

View File

@ -6,19 +6,19 @@ import {
ArtistInfoElement,
ChildElement,
DirectoryElement,
} from './elements';
} from './elements'
export type ResponseStatus = 'ok' | 'failed';
export type ResponseStatus = 'ok' | 'failed'
export class SubsonicResponse<T> {
status: ResponseStatus;
version: string;
data: T;
status: ResponseStatus
version: string
data: T
constructor(xml: Document, data: T) {
this.data = data;
this.status = xml.documentElement.getAttribute('status') as ResponseStatus;
this.version = xml.documentElement.getAttribute('version') as string;
this.data = data
this.status = xml.documentElement.getAttribute('status') as ResponseStatus
this.version = xml.documentElement.getAttribute('version') as string
}
}
@ -27,91 +27,91 @@ export class SubsonicResponse<T> {
//
export class GetArtistsResponse {
ignoredArticles: string;
artists: ArtistID3Element[] = [];
ignoredArticles: string
artists: ArtistID3Element[] = []
constructor(xml: Document) {
this.ignoredArticles = xml.getElementsByTagName('artists')[0].getAttribute('ignoredArticles') as string;
this.ignoredArticles = xml.getElementsByTagName('artists')[0].getAttribute('ignoredArticles') as string
const artistElements = xml.getElementsByTagName('artist');
const artistElements = xml.getElementsByTagName('artist')
for (let i = 0; i < artistElements.length; i++) {
this.artists.push(new ArtistID3Element(artistElements[i]));
this.artists.push(new ArtistID3Element(artistElements[i]))
}
}
}
export class GetArtistResponse {
artist: ArtistID3Element;
albums: AlbumID3Element[] = [];
artist: ArtistID3Element
albums: AlbumID3Element[] = []
constructor(xml: Document) {
this.artist = new ArtistID3Element(xml.getElementsByTagName('artist')[0]);
this.artist = new ArtistID3Element(xml.getElementsByTagName('artist')[0])
const albumElements = xml.getElementsByTagName('album');
const albumElements = xml.getElementsByTagName('album')
for (let i = 0; i < albumElements.length; i++) {
this.albums.push(new AlbumID3Element(albumElements[i]));
this.albums.push(new AlbumID3Element(albumElements[i]))
}
}
}
export class GetIndexesResponse {
ignoredArticles: string;
lastModified: number;
artists: ArtistElement[] = [];
ignoredArticles: string
lastModified: number
artists: ArtistElement[] = []
constructor(xml: Document) {
const indexesElement = xml.getElementsByTagName('indexes')[0];
const indexesElement = xml.getElementsByTagName('indexes')[0]
this.ignoredArticles = indexesElement.getAttribute('ignoredArticles') as string;
this.lastModified = parseInt(indexesElement.getAttribute('lastModified') as string);
this.ignoredArticles = indexesElement.getAttribute('ignoredArticles') as string
this.lastModified = parseInt(indexesElement.getAttribute('lastModified') as string)
const artistElements = xml.getElementsByTagName('artist');
const artistElements = xml.getElementsByTagName('artist')
for (let i = 0; i < artistElements.length; i++) {
this.artists.push(new ArtistElement(artistElements[i]));
this.artists.push(new ArtistElement(artistElements[i]))
}
}
}
export class GetArtistInfoResponse {
artistInfo: ArtistInfoElement;
artistInfo: ArtistInfoElement
constructor(xml: Document) {
this.artistInfo = new ArtistInfoElement(xml.getElementsByTagName('artistInfo')[0]);
this.artistInfo = new ArtistInfoElement(xml.getElementsByTagName('artistInfo')[0])
}
}
export class GetArtistInfo2Response {
artistInfo: ArtistInfo2Element;
artistInfo: ArtistInfo2Element
constructor(xml: Document) {
this.artistInfo = new ArtistInfo2Element(xml.getElementsByTagName('artistInfo2')[0]);
this.artistInfo = new ArtistInfo2Element(xml.getElementsByTagName('artistInfo2')[0])
}
}
export class GetMusicDirectoryResponse {
directory: DirectoryElement;
children: ChildElement[] = [];
directory: DirectoryElement
children: ChildElement[] = []
constructor(xml: Document) {
this.directory = new DirectoryElement(xml.getElementsByTagName('directory')[0]);
this.directory = new DirectoryElement(xml.getElementsByTagName('directory')[0])
const childElements = xml.getElementsByTagName('child');
const childElements = xml.getElementsByTagName('child')
for (let i = 0; i < childElements.length; i++) {
this.children.push(new ChildElement(childElements[i]));
this.children.push(new ChildElement(childElements[i]))
}
}
}
export class GetAlbumResponse {
album: AlbumID3Element;
songs: ChildElement[] = [];
album: AlbumID3Element
songs: ChildElement[] = []
constructor(xml: Document) {
this.album = new AlbumID3Element(xml.getElementsByTagName('album')[0]);
this.album = new AlbumID3Element(xml.getElementsByTagName('album')[0])
const childElements = xml.getElementsByTagName('song');
const childElements = xml.getElementsByTagName('song')
for (let i = 0; i < childElements.length; i++) {
this.songs.push(new ChildElement(childElements[i]));
this.songs.push(new ChildElement(childElements[i]))
}
}
}
@ -121,24 +121,24 @@ export class GetAlbumResponse {
//
class BaseGetAlbumListResponse<T> {
albums: T[] = [];
albums: T[] = []
constructor(xml: Document, albumType: new (e: Element) => T) {
const albumElements = xml.getElementsByTagName('album');
const albumElements = xml.getElementsByTagName('album')
for (let i = 0; i < albumElements.length; i++) {
this.albums.push(new albumType(albumElements[i]));
this.albums.push(new albumType(albumElements[i]))
}
}
}
export class GetAlbumListResponse extends BaseGetAlbumListResponse<ChildElement> {
constructor(xml: Document) {
super(xml, ChildElement);
super(xml, ChildElement)
}
}
export class GetAlbumList2Response extends BaseGetAlbumListResponse<AlbumID3Element> {
constructor(xml: Document) {
super(xml, AlbumID3Element);
super(xml, AlbumID3Element)
}
}

View File

@ -1,11 +1,11 @@
export function formatDuration(seconds: number): string {
const s = seconds % 60;
const m = Math.floor(seconds / 60) % 60;
const h = Math.floor(seconds / 60 / 60);
const s = seconds % 60
const m = Math.floor(seconds / 60) % 60
const h = Math.floor(seconds / 60 / 60)
let time = `${m.toString().padStart(1, '0')}:${s.toString().padStart(2, '0')}`;
let time = `${m.toString().padStart(1, '0')}:${s.toString().padStart(2, '0')}`
if (h > 0) {
time = `${h}:${time}`;
time = `${h}:${time}`
}
return time;
return time
}