mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 09:09:29 +01:00
let's try no semicolons
This commit is contained in:
parent
8f7b285938
commit
24b443fd70
@ -5,5 +5,6 @@ module.exports = {
|
|||||||
'react-native/no-inline-styles': 0,
|
'react-native/no-inline-styles': 0,
|
||||||
radix: 0,
|
radix: 0,
|
||||||
'@typescript-eslint/no-unused-vars': ['warn'],
|
'@typescript-eslint/no-unused-vars': ['warn'],
|
||||||
|
semi: 0,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|||||||
@ -5,4 +5,5 @@ module.exports = {
|
|||||||
trailingComma: 'all',
|
trailingComma: 'all',
|
||||||
arrowParens: 'avoid',
|
arrowParens: 'avoid',
|
||||||
printWidth: 120,
|
printWidth: 120,
|
||||||
};
|
semi: false,
|
||||||
|
}
|
||||||
|
|||||||
24
App.tsx
24
App.tsx
@ -1,14 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { DarkTheme, NavigationContainer } from '@react-navigation/native';
|
import { DarkTheme, NavigationContainer } from '@react-navigation/native'
|
||||||
import SplashPage from './src/components/SplashPage';
|
import SplashPage from './src/components/SplashPage'
|
||||||
import RootNavigator from './src/components/navigation/RootNavigator';
|
import RootNavigator from './src/components/navigation/RootNavigator'
|
||||||
import { Provider } from 'jotai';
|
import { Provider } from 'jotai'
|
||||||
import { StatusBar, View } from 'react-native';
|
import { StatusBar, View } from 'react-native'
|
||||||
import colors from './src/styles/colors';
|
import colors from './src/styles/colors'
|
||||||
import TrackPlayerState from './src/components/TrackPlayerState';
|
import TrackPlayerState from './src/components/TrackPlayerState'
|
||||||
|
|
||||||
const theme = { ...DarkTheme };
|
const theme = { ...DarkTheme }
|
||||||
theme.colors.background = colors.gradient.high;
|
theme.colors.background = colors.gradient.high
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<Provider>
|
<Provider>
|
||||||
@ -22,6 +22,6 @@ const App = () => (
|
|||||||
</SplashPage>
|
</SplashPage>
|
||||||
</View>
|
</View>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default App;
|
export default App
|
||||||
|
|||||||
@ -2,13 +2,13 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'react-native';
|
import 'react-native'
|
||||||
import React from 'react';
|
import React from 'react'
|
||||||
import App from '../App';
|
import App from '../App'
|
||||||
|
|
||||||
// Note: test renderer must be required after react-native.
|
// Note: test renderer must be required after react-native.
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer'
|
||||||
|
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
renderer.create(<App />);
|
renderer.create(<App />)
|
||||||
});
|
})
|
||||||
|
|||||||
@ -4,4 +4,4 @@ module.exports = {
|
|||||||
// reanimated has to be listed last in plugins
|
// reanimated has to be listed last in plugins
|
||||||
'react-native-reanimated/plugin',
|
'react-native-reanimated/plugin',
|
||||||
],
|
],
|
||||||
};
|
}
|
||||||
|
|||||||
26
index.js
26
index.js
@ -1,19 +1,19 @@
|
|||||||
import 'react-native-gesture-handler';
|
import 'react-native-gesture-handler'
|
||||||
import 'react-native-get-random-values';
|
import 'react-native-get-random-values'
|
||||||
|
|
||||||
import { enableScreens } from 'react-native-screens';
|
import { enableScreens } from 'react-native-screens'
|
||||||
enableScreens();
|
enableScreens()
|
||||||
|
|
||||||
import { AppRegistry } from 'react-native';
|
import { AppRegistry } from 'react-native'
|
||||||
import App from './App';
|
import App from './App'
|
||||||
import { name as appName } from './app.json';
|
import { name as appName } from './app.json'
|
||||||
import TrackPlayer, { Capability } from 'react-native-track-player';
|
import TrackPlayer, { Capability } from 'react-native-track-player'
|
||||||
|
|
||||||
AppRegistry.registerComponent(appName, () => App);
|
AppRegistry.registerComponent(appName, () => App)
|
||||||
TrackPlayer.registerPlaybackService(() => require('./src/playback/service'));
|
TrackPlayer.registerPlaybackService(() => require('./src/playback/service'))
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
await TrackPlayer.setupPlayer();
|
await TrackPlayer.setupPlayer()
|
||||||
await TrackPlayer.updateOptions({
|
await TrackPlayer.updateOptions({
|
||||||
capabilities: [
|
capabilities: [
|
||||||
Capability.Play,
|
Capability.Play,
|
||||||
@ -23,6 +23,6 @@ async function start() {
|
|||||||
Capability.SkipToPrevious,
|
Capability.SkipToPrevious,
|
||||||
],
|
],
|
||||||
compactCapabilities: [Capability.Play, Capability.Pause, Capability.SkipToNext, Capability.SkipToPrevious],
|
compactCapabilities: [Capability.Play, Capability.Pause, Capability.SkipToNext, Capability.SkipToPrevious],
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
start();
|
start()
|
||||||
|
|||||||
@ -14,4 +14,4 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|||||||
@ -4,4 +4,4 @@ module.exports = {
|
|||||||
android: {},
|
android: {},
|
||||||
},
|
},
|
||||||
assets: ['./assets/fonts'],
|
assets: ['./assets/fonts'],
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { FlatList, Text, View } from 'react-native';
|
import { FlatList, Text, View } from 'react-native'
|
||||||
import { useAtomValue } from 'jotai/utils';
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import { Artist } from '../models/music';
|
import { Artist } from '../models/music'
|
||||||
import { artistsAtom } from '../state/music';
|
import { artistsAtom } from '../state/music'
|
||||||
|
|
||||||
const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => (
|
const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => (
|
||||||
<View>
|
<View>
|
||||||
@ -15,15 +15,15 @@ const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => (
|
|||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
|
|
||||||
const List = () => {
|
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 = () => (
|
const ArtistsList = () => (
|
||||||
<View>
|
<View>
|
||||||
@ -31,6 +31,6 @@ const ArtistsList = () => (
|
|||||||
<List />
|
<List />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default ArtistsList;
|
export default ArtistsList
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { Image, ImageSourcePropType } from 'react-native';
|
import { Image, ImageSourcePropType } from 'react-native'
|
||||||
import colors from '../styles/colors';
|
import colors from '../styles/colors'
|
||||||
|
|
||||||
export type FocusableIconProps = {
|
export type FocusableIconProps = {
|
||||||
focused: boolean;
|
focused: boolean
|
||||||
source: ImageSourcePropType;
|
source: ImageSourcePropType
|
||||||
focusedSource?: ImageSourcePropType;
|
focusedSource?: ImageSourcePropType
|
||||||
width?: number;
|
width?: number
|
||||||
height?: number;
|
height?: number
|
||||||
};
|
}
|
||||||
|
|
||||||
const FocusableIcon: React.FC<FocusableIconProps> = props => {
|
const FocusableIcon: React.FC<FocusableIconProps> = props => {
|
||||||
props.focusedSource = props.focusedSource || props.source;
|
props.focusedSource = props.focusedSource || props.source
|
||||||
props.width = props.width || 26;
|
props.width = props.width || 26
|
||||||
props.height = props.height || 26;
|
props.height = props.height || 26
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
@ -24,7 +24,7 @@ const FocusableIcon: React.FC<FocusableIconProps> = props => {
|
|||||||
}}
|
}}
|
||||||
source={props.focused ? props.focusedSource : props.source}
|
source={props.focused ? props.focusedSource : props.source}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FocusableIcon;
|
export default FocusableIcon
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { useAtomValue } from 'jotai/utils';
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { Pressable, StatusBar, StyleSheet, Text, useWindowDimensions, View } from 'react-native';
|
import { Pressable, StatusBar, StyleSheet, Text, useWindowDimensions, View } from 'react-native'
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image'
|
||||||
import TrackPlayer, { State } from 'react-native-track-player';
|
import TrackPlayer, { State } from 'react-native-track-player'
|
||||||
import { currentQueueNameAtom, currentTrackAtom, playerStateAtom } from '../state/trackplayer';
|
import { currentQueueNameAtom, currentTrackAtom, playerStateAtom } from '../state/trackplayer'
|
||||||
import text from '../styles/text';
|
import text from '../styles/text'
|
||||||
import CoverArt from './common/CoverArt';
|
import CoverArt from './common/CoverArt'
|
||||||
import ImageGradientBackground from './common/ImageGradientBackground';
|
import ImageGradientBackground from './common/ImageGradientBackground'
|
||||||
|
|
||||||
const NowPlayingHeader = () => {
|
const NowPlayingHeader = () => {
|
||||||
const queueName = useAtomValue(currentQueueNameAtom);
|
const queueName = useAtomValue(currentQueueNameAtom)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={headerStyles.container}>
|
<View style={headerStyles.container}>
|
||||||
@ -19,8 +19,8 @@ const NowPlayingHeader = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
<FastImage source={require('../../res/more_vertical.png')} style={headerStyles.icons} tintColor="white" />
|
<FastImage source={require('../../res/more_vertical.png')} style={headerStyles.icons} tintColor="white" />
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const headerStyles = StyleSheet.create({
|
const headerStyles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
@ -38,13 +38,13 @@ const headerStyles = StyleSheet.create({
|
|||||||
queueName: {
|
queueName: {
|
||||||
...text.paragraph,
|
...text.paragraph,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const SongCoverArt = () => {
|
const SongCoverArt = () => {
|
||||||
const track = useAtomValue(currentTrackAtom);
|
const track = useAtomValue(currentTrackAtom)
|
||||||
const layout = useWindowDimensions();
|
const layout = useWindowDimensions()
|
||||||
|
|
||||||
const size = layout.width - layout.width / 7;
|
const size = layout.width - layout.width / 7
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={coverArtStyles.container}>
|
<View style={coverArtStyles.container}>
|
||||||
@ -55,8 +55,8 @@ const SongCoverArt = () => {
|
|||||||
coverArtUri={track?.artwork as string}
|
coverArtUri={track?.artwork as string}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const coverArtStyles = StyleSheet.create({
|
const coverArtStyles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
@ -64,18 +64,18 @@ const coverArtStyles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginTop: 10,
|
marginTop: 10,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const SongInfo = () => {
|
const SongInfo = () => {
|
||||||
const track = useAtomValue(currentTrackAtom);
|
const track = useAtomValue(currentTrackAtom)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={infoStyles.container}>
|
<View style={infoStyles.container}>
|
||||||
<Text style={infoStyles.title}>{track?.title}</Text>
|
<Text style={infoStyles.title}>{track?.title}</Text>
|
||||||
<Text style={infoStyles.artist}>{track?.artist}</Text>
|
<Text style={infoStyles.artist}>{track?.artist}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const infoStyles = StyleSheet.create({
|
const infoStyles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
@ -94,33 +94,33 @@ const infoStyles = StyleSheet.create({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const PlayerControls = () => {
|
const PlayerControls = () => {
|
||||||
const state = useAtomValue(playerStateAtom);
|
const state = useAtomValue(playerStateAtom)
|
||||||
|
|
||||||
let playPauseIcon: number;
|
let playPauseIcon: number
|
||||||
let playPauseStyle: any;
|
let playPauseStyle: any
|
||||||
let playPauseAction: () => void;
|
let playPauseAction: () => void
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case State.Playing:
|
case State.Playing:
|
||||||
case State.Buffering:
|
case State.Buffering:
|
||||||
case State.Connecting:
|
case State.Connecting:
|
||||||
playPauseIcon = require('../../res/pause_circle-fill.png');
|
playPauseIcon = require('../../res/pause_circle-fill.png')
|
||||||
playPauseStyle = controlsStyles.enabled;
|
playPauseStyle = controlsStyles.enabled
|
||||||
playPauseAction = () => TrackPlayer.pause();
|
playPauseAction = () => TrackPlayer.pause()
|
||||||
break;
|
break
|
||||||
case State.Paused:
|
case State.Paused:
|
||||||
playPauseIcon = require('../../res/play_circle-fill.png');
|
playPauseIcon = require('../../res/play_circle-fill.png')
|
||||||
playPauseStyle = controlsStyles.enabled;
|
playPauseStyle = controlsStyles.enabled
|
||||||
playPauseAction = () => TrackPlayer.play();
|
playPauseAction = () => TrackPlayer.play()
|
||||||
break;
|
break
|
||||||
default:
|
default:
|
||||||
playPauseIcon = require('../../res/play_circle-fill.png');
|
playPauseIcon = require('../../res/play_circle-fill.png')
|
||||||
playPauseStyle = controlsStyles.disabled;
|
playPauseStyle = controlsStyles.disabled
|
||||||
playPauseAction = () => {};
|
playPauseAction = () => {}
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -139,8 +139,8 @@ const PlayerControls = () => {
|
|||||||
style={{ ...controlsStyles.skip, ...playPauseStyle }}
|
style={{ ...controlsStyles.skip, ...playPauseStyle }}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const controlsStyles = StyleSheet.create({
|
const controlsStyles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
@ -165,10 +165,10 @@ const controlsStyles = StyleSheet.create({
|
|||||||
disabled: {
|
disabled: {
|
||||||
opacity: 0.35,
|
opacity: 0.35,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const NowPlayingLayout = () => {
|
const NowPlayingLayout = () => {
|
||||||
const track = useAtomValue(currentTrackAtom);
|
const track = useAtomValue(currentTrackAtom)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@ -182,7 +182,7 @@ const NowPlayingLayout = () => {
|
|||||||
<SongInfo />
|
<SongInfo />
|
||||||
<PlayerControls />
|
<PlayerControls />
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default NowPlayingLayout;
|
export default NowPlayingLayout
|
||||||
|
|||||||
@ -1,40 +1,40 @@
|
|||||||
import { useNavigation } from '@react-navigation/core';
|
import { useNavigation } from '@react-navigation/core'
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai'
|
||||||
import md5 from 'md5';
|
import md5 from 'md5'
|
||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { Button, Text, View } from 'react-native';
|
import { Button, Text, View } from 'react-native'
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { appSettingsAtom } from '../state/settings';
|
import { appSettingsAtom } from '../state/settings'
|
||||||
import { getAllKeys, multiRemove } from '../storage/asyncstorage';
|
import { getAllKeys, multiRemove } from '../storage/asyncstorage'
|
||||||
import text from '../styles/text';
|
import text from '../styles/text'
|
||||||
|
|
||||||
const TestControls = () => {
|
const TestControls = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation()
|
||||||
|
|
||||||
const removeAllKeys = async () => {
|
const removeAllKeys = async () => {
|
||||||
const allKeys = await getAllKeys();
|
const allKeys = await getAllKeys()
|
||||||
await multiRemove(allKeys);
|
await multiRemove(allKeys)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Button title="Remove all keys" onPress={removeAllKeys} />
|
<Button title="Remove all keys" onPress={removeAllKeys} />
|
||||||
<Button title="Now Playing" onPress={() => navigation.navigate('Now Playing')} />
|
<Button title="Now Playing" onPress={() => navigation.navigate('Now Playing')} />
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const ServerSettingsView = () => {
|
const ServerSettingsView = () => {
|
||||||
const [appSettings, setAppSettings] = useAtom(appSettingsAtom);
|
const [appSettings, setAppSettings] = useAtom(appSettingsAtom)
|
||||||
|
|
||||||
const bootstrapServer = () => {
|
const bootstrapServer = () => {
|
||||||
if (appSettings.servers.length !== 0) {
|
if (appSettings.servers.length !== 0) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = uuidv4();
|
const id = uuidv4()
|
||||||
const salt = uuidv4();
|
const salt = uuidv4()
|
||||||
const address = 'http://demo.subsonic.org';
|
const address = 'http://demo.subsonic.org'
|
||||||
|
|
||||||
setAppSettings({
|
setAppSettings({
|
||||||
...appSettings,
|
...appSettings,
|
||||||
@ -49,8 +49,8 @@ const ServerSettingsView = () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
activeServer: id,
|
activeServer: id,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
@ -62,8 +62,8 @@ const ServerSettingsView = () => {
|
|||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const SettingsView = () => (
|
const SettingsView = () => (
|
||||||
<View>
|
<View>
|
||||||
@ -72,6 +72,6 @@ const SettingsView = () => (
|
|||||||
<ServerSettingsView />
|
<ServerSettingsView />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default SettingsView;
|
export default SettingsView
|
||||||
|
|||||||
@ -1,45 +1,45 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Text, View } from 'react-native';
|
import { Text, View } from 'react-native'
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs'
|
||||||
import paths from '../paths';
|
import paths from '../paths'
|
||||||
|
|
||||||
async function mkdir(path: string): Promise<void> {
|
async function mkdir(path: string): Promise<void> {
|
||||||
const exists = await RNFS.exists(path);
|
const exists = await RNFS.exists(path)
|
||||||
if (exists) {
|
if (exists) {
|
||||||
const isDir = (await RNFS.stat(path)).isDirectory();
|
const isDir = (await RNFS.stat(path)).isDirectory()
|
||||||
if (!isDir) {
|
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 {
|
} else {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await RNFS.mkdir(path);
|
return await RNFS.mkdir(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SplashPage: React.FC<{}> = ({ children }) => {
|
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 () => {
|
const prepare = async () => {
|
||||||
await mkdir(paths.imageCache);
|
await mkdir(paths.imageCache)
|
||||||
await mkdir(paths.songCache);
|
await mkdir(paths.songCache)
|
||||||
await mkdir(paths.songs);
|
await mkdir(paths.songs)
|
||||||
};
|
}
|
||||||
|
|
||||||
const promise = Promise.all([prepare(), minSplashTime]);
|
const promise = Promise.all([prepare(), minSplashTime])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
promise.then(() => {
|
promise.then(() => {
|
||||||
setReady(true);
|
setReady(true)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
return <Text>Loading THE GOOD SHIT...</Text>;
|
return <Text>Loading THE GOOD SHIT...</Text>
|
||||||
}
|
}
|
||||||
return <View style={{ flex: 1 }}>{children}</View>;
|
return <View style={{ flex: 1 }}>{children}</View>
|
||||||
};
|
}
|
||||||
|
|
||||||
export default SplashPage;
|
export default SplashPage
|
||||||
|
|||||||
@ -1,27 +1,27 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react'
|
||||||
import TrackPlayer, { Event, State, useTrackPlayerEvents } from 'react-native-track-player';
|
import TrackPlayer, { Event, State, useTrackPlayerEvents } from 'react-native-track-player'
|
||||||
import { useAppState } from '@react-native-community/hooks';
|
import { useAppState } from '@react-native-community/hooks'
|
||||||
import { useUpdateAtom, useAtomValue } from 'jotai/utils';
|
import { useUpdateAtom, useAtomValue } from 'jotai/utils'
|
||||||
import { currentQueueNameAtom, currentTrackAtom, playerStateAtom } from '../state/trackplayer';
|
import { currentQueueNameAtom, currentTrackAtom, playerStateAtom } from '../state/trackplayer'
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native'
|
||||||
|
|
||||||
const CurrentTrackState = () => {
|
const CurrentTrackState = () => {
|
||||||
const setCurrentTrack = useUpdateAtom(currentTrackAtom);
|
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
||||||
const appState = useAppState();
|
const appState = useAppState()
|
||||||
|
|
||||||
const update = useCallback(async () => {
|
const update = useCallback(async () => {
|
||||||
const index = await TrackPlayer.getCurrentTrack();
|
const index = await TrackPlayer.getCurrentTrack()
|
||||||
|
|
||||||
if (index !== null && index >= 0) {
|
if (index !== null && index >= 0) {
|
||||||
const track = await TrackPlayer.getTrack(index);
|
const track = await TrackPlayer.getTrack(index)
|
||||||
if (track !== null) {
|
if (track !== null) {
|
||||||
setCurrentTrack(track);
|
setCurrentTrack(track)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentTrack(undefined);
|
setCurrentTrack(undefined)
|
||||||
}, [setCurrentTrack]);
|
}, [setCurrentTrack])
|
||||||
|
|
||||||
useTrackPlayerEvents(
|
useTrackPlayerEvents(
|
||||||
[
|
[
|
||||||
@ -36,91 +36,91 @@ const CurrentTrackState = () => {
|
|||||||
],
|
],
|
||||||
event => {
|
event => {
|
||||||
if (event.type === Event.PlaybackQueueEnded && 'track' in event) {
|
if (event.type === Event.PlaybackQueueEnded && 'track' in event) {
|
||||||
setCurrentTrack(undefined);
|
setCurrentTrack(undefined)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
update();
|
update()
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appState === 'active') {
|
if (appState === 'active') {
|
||||||
update();
|
update()
|
||||||
}
|
}
|
||||||
}, [appState, update]);
|
}, [appState, update])
|
||||||
|
|
||||||
return <></>;
|
return <></>
|
||||||
};
|
}
|
||||||
|
|
||||||
const CurrentQueueName = () => {
|
const CurrentQueueName = () => {
|
||||||
const setCurrentQueueName = useUpdateAtom(currentQueueNameAtom);
|
const setCurrentQueueName = useUpdateAtom(currentQueueNameAtom)
|
||||||
const appState = useAppState();
|
const appState = useAppState()
|
||||||
|
|
||||||
const update = useCallback(async () => {
|
const update = useCallback(async () => {
|
||||||
const queue = await TrackPlayer.getQueue();
|
const queue = await TrackPlayer.getQueue()
|
||||||
|
|
||||||
if (queue !== null && queue.length > 0) {
|
if (queue !== null && queue.length > 0) {
|
||||||
setCurrentQueueName(queue[0].queueName);
|
setCurrentQueueName(queue[0].queueName)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentQueueName(undefined);
|
setCurrentQueueName(undefined)
|
||||||
}, [setCurrentQueueName]);
|
}, [setCurrentQueueName])
|
||||||
|
|
||||||
useTrackPlayerEvents(
|
useTrackPlayerEvents(
|
||||||
[Event.PlaybackState, Event.PlaybackQueueEnded, Event.PlaybackMetadataReceived, Event.RemoteDuck, Event.RemoteStop],
|
[Event.PlaybackState, Event.PlaybackQueueEnded, Event.PlaybackMetadataReceived, Event.RemoteDuck, Event.RemoteStop],
|
||||||
event => {
|
event => {
|
||||||
if (event.type === Event.PlaybackState) {
|
if (event.type === Event.PlaybackState) {
|
||||||
if (event.state === State.Stopped || event.state === State.None) {
|
if (event.state === State.Stopped || event.state === State.None) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
update();
|
update()
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appState === 'active') {
|
if (appState === 'active') {
|
||||||
update();
|
update()
|
||||||
}
|
}
|
||||||
}, [appState, update]);
|
}, [appState, update])
|
||||||
|
|
||||||
return <></>;
|
return <></>
|
||||||
};
|
}
|
||||||
|
|
||||||
const PlayerState = () => {
|
const PlayerState = () => {
|
||||||
const setPlayerState = useUpdateAtom(playerStateAtom);
|
const setPlayerState = useUpdateAtom(playerStateAtom)
|
||||||
const appState = useAppState();
|
const appState = useAppState()
|
||||||
|
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
async (state?: State) => {
|
async (state?: State) => {
|
||||||
setPlayerState(state || (await TrackPlayer.getState()));
|
setPlayerState(state || (await TrackPlayer.getState()))
|
||||||
},
|
},
|
||||||
[setPlayerState],
|
[setPlayerState],
|
||||||
);
|
)
|
||||||
|
|
||||||
useTrackPlayerEvents([Event.PlaybackState], event => {
|
useTrackPlayerEvents([Event.PlaybackState], event => {
|
||||||
update(event.state);
|
update(event.state)
|
||||||
});
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appState === 'active') {
|
if (appState === 'active') {
|
||||||
update();
|
update()
|
||||||
}
|
}
|
||||||
}, [appState, update]);
|
}, [appState, update])
|
||||||
|
|
||||||
return <></>;
|
return <></>
|
||||||
};
|
}
|
||||||
|
|
||||||
const Debug = () => {
|
const Debug = () => {
|
||||||
const value = useAtomValue(currentQueueNameAtom);
|
const value = useAtomValue(currentQueueNameAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(value);
|
console.log(value)
|
||||||
}, [value]);
|
}, [value])
|
||||||
|
|
||||||
return <></>;
|
return <></>
|
||||||
};
|
}
|
||||||
|
|
||||||
const TrackPlayerState = () => (
|
const TrackPlayerState = () => (
|
||||||
<View>
|
<View>
|
||||||
@ -129,6 +129,6 @@ const TrackPlayerState = () => (
|
|||||||
<PlayerState />
|
<PlayerState />
|
||||||
<Debug />
|
<Debug />
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default TrackPlayerState;
|
export default TrackPlayerState
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
import { useAtomValue } from 'jotai/utils';
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { ActivityIndicator, View } from 'react-native';
|
import { ActivityIndicator, View } from 'react-native'
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image'
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import { albumArtAtomFamily } from '../../state/music';
|
import { albumArtAtomFamily } from '../../state/music'
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors'
|
||||||
import CoverArt from './CoverArt';
|
import CoverArt from './CoverArt'
|
||||||
|
|
||||||
interface AlbumArtProps {
|
interface AlbumArtProps {
|
||||||
id: string;
|
id: string
|
||||||
height: number;
|
height: number
|
||||||
width: number;
|
width: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
|
const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
|
||||||
const albumArt = useAtomValue(albumArtAtomFamily(id));
|
const albumArt = useAtomValue(albumArtAtomFamily(id))
|
||||||
|
|
||||||
const Placeholder = () => (
|
const Placeholder = () => (
|
||||||
<LinearGradient colors={[colors.accent, colors.accentLow]}>
|
<LinearGradient colors={[colors.accent, colors.accentLow]}>
|
||||||
@ -24,7 +24,7 @@ const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
|
|||||||
resizeMode={FastImage.resizeMode.contain}
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
/>
|
/>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CoverArt
|
<CoverArt
|
||||||
@ -33,8 +33,8 @@ const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
|
|||||||
width={width}
|
width={width}
|
||||||
coverArtUri={width > 128 ? albumArt?.uri : albumArt?.thumbUri}
|
coverArtUri={width > 128 ? albumArt?.uri : albumArt?.thumbUri}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const AlbumArtFallback: React.FC<AlbumArtProps> = ({ height, width }) => (
|
const AlbumArtFallback: React.FC<AlbumArtProps> = ({ height, width }) => (
|
||||||
<View
|
<View
|
||||||
@ -46,12 +46,12 @@ const AlbumArtFallback: React.FC<AlbumArtProps> = ({ height, width }) => (
|
|||||||
}}>
|
}}>
|
||||||
<ActivityIndicator size="small" color={colors.accent} />
|
<ActivityIndicator size="small" color={colors.accent} />
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
|
|
||||||
const AlbumArtLoader: React.FC<AlbumArtProps> = props => (
|
const AlbumArtLoader: React.FC<AlbumArtProps> = props => (
|
||||||
<React.Suspense fallback={<AlbumArtFallback {...props} />}>
|
<React.Suspense fallback={<AlbumArtFallback {...props} />}>
|
||||||
<AlbumArt {...props} />
|
<AlbumArt {...props} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default React.memo(AlbumArtLoader);
|
export default React.memo(AlbumArtLoader)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { useAtomValue } from 'jotai/utils';
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
GestureResponderEvent,
|
GestureResponderEvent,
|
||||||
@ -9,26 +9,26 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native'
|
||||||
import { useSetQueue } from '../../hooks/trackplayer';
|
import { useSetQueue } from '../../hooks/trackplayer'
|
||||||
import { albumAtomFamily } from '../../state/music';
|
import { albumAtomFamily } from '../../state/music'
|
||||||
import { currentTrackAtom } from '../../state/trackplayer';
|
import { currentTrackAtom } from '../../state/trackplayer'
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors'
|
||||||
import text from '../../styles/text';
|
import text from '../../styles/text'
|
||||||
import AlbumArt from './AlbumArt';
|
import AlbumArt from './AlbumArt'
|
||||||
import Button from './Button';
|
import Button from './Button'
|
||||||
import GradientBackground from './GradientBackground';
|
import GradientBackground from './GradientBackground'
|
||||||
import ImageGradientScrollView from './ImageGradientScrollView';
|
import ImageGradientScrollView from './ImageGradientScrollView'
|
||||||
|
|
||||||
const SongItem: React.FC<{
|
const SongItem: React.FC<{
|
||||||
id: string;
|
id: string
|
||||||
title: string;
|
title: string
|
||||||
artist?: string;
|
artist?: string
|
||||||
track?: number;
|
track?: number
|
||||||
onPress: (event: GestureResponderEvent) => void;
|
onPress: (event: GestureResponderEvent) => void
|
||||||
}> = ({ id, title, artist, onPress }) => {
|
}> = ({ id, title, artist, onPress }) => {
|
||||||
const [opacity, setOpacity] = useState(1);
|
const [opacity, setOpacity] = useState(1)
|
||||||
const currentTrack = useAtomValue(currentTrackAtom);
|
const currentTrack = useAtomValue(currentTrackAtom)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@ -85,20 +85,20 @@ const SongItem: React.FC<{
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const AlbumDetails: React.FC<{
|
const AlbumDetails: React.FC<{
|
||||||
id: string;
|
id: string
|
||||||
}> = ({ id }) => {
|
}> = ({ id }) => {
|
||||||
const album = useAtomValue(albumAtomFamily(id));
|
const album = useAtomValue(albumAtomFamily(id))
|
||||||
const layout = useWindowDimensions();
|
const layout = useWindowDimensions()
|
||||||
const setQueue = useSetQueue();
|
const setQueue = useSetQueue()
|
||||||
|
|
||||||
const coverSize = layout.width - layout.width / 2.5;
|
const coverSize = layout.width - layout.width / 2.5
|
||||||
|
|
||||||
if (!album) {
|
if (!album) {
|
||||||
return <Text style={text.paragraph}>No Album</Text>;
|
return <Text style={text.paragraph}>No Album</Text>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -152,9 +152,9 @@ const AlbumDetails: React.FC<{
|
|||||||
{album.songs
|
{album.songs
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (b.track && a.track) {
|
if (b.track && a.track) {
|
||||||
return a.track - b.track;
|
return a.track - b.track
|
||||||
} else {
|
} else {
|
||||||
return a.title.localeCompare(b.title);
|
return a.title.localeCompare(b.title)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map(s => (
|
.map(s => (
|
||||||
@ -169,13 +169,13 @@ const AlbumDetails: React.FC<{
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</ImageGradientScrollView>
|
</ImageGradientScrollView>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const AlbumViewFallback = () => {
|
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 (
|
return (
|
||||||
<GradientBackground
|
<GradientBackground
|
||||||
@ -185,24 +185,24 @@ const AlbumViewFallback = () => {
|
|||||||
}}>
|
}}>
|
||||||
<ActivityIndicator size="large" color={colors.accent} />
|
<ActivityIndicator size="large" color={colors.accent} />
|
||||||
</GradientBackground>
|
</GradientBackground>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const AlbumView: React.FC<{
|
const AlbumView: React.FC<{
|
||||||
id: string;
|
id: string
|
||||||
title: string;
|
title: string
|
||||||
}> = ({ id, title }) => {
|
}> = ({ id, title }) => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.setOptions({ title });
|
navigation.setOptions({ title })
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Suspense fallback={<AlbumViewFallback />}>
|
<React.Suspense fallback={<AlbumViewFallback />}>
|
||||||
<AlbumDetails id={id} />
|
<AlbumDetails id={id} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default React.memo(AlbumView);
|
export default React.memo(AlbumView)
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
import { useAtomValue } from 'jotai/utils';
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { ActivityIndicator, View } from 'react-native';
|
import { ActivityIndicator, View } from 'react-native'
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image'
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import { artistArtAtomFamily } from '../../state/music';
|
import { artistArtAtomFamily } from '../../state/music'
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors'
|
||||||
import CoverArt from './CoverArt';
|
import CoverArt from './CoverArt'
|
||||||
|
|
||||||
interface ArtistArtSizeProps {
|
interface ArtistArtSizeProps {
|
||||||
height: number;
|
height: number
|
||||||
width: number;
|
width: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArtistArtXUpProps extends ArtistArtSizeProps {
|
interface ArtistArtXUpProps extends ArtistArtSizeProps {
|
||||||
coverArtUris: string[];
|
coverArtUris: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArtistArtProps extends ArtistArtSizeProps {
|
interface ArtistArtProps extends ArtistArtSizeProps {
|
||||||
id: string;
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlaceholderContainer: React.FC<ArtistArtSizeProps> = ({ height, width, children }) => (
|
const PlaceholderContainer: React.FC<ArtistArtSizeProps> = ({ height, width, children }) => (
|
||||||
@ -31,11 +31,11 @@ const PlaceholderContainer: React.FC<ArtistArtSizeProps> = ({ height, width, chi
|
|||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
);
|
)
|
||||||
|
|
||||||
const FourUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
const FourUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
||||||
const halfHeight = height / 2;
|
const halfHeight = height / 2
|
||||||
const halfWidth = width / 2;
|
const halfWidth = width / 2
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlaceholderContainer height={height} width={width}>
|
<PlaceholderContainer height={height} width={width}>
|
||||||
@ -64,12 +64,12 @@ const FourUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) =>
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</PlaceholderContainer>
|
</PlaceholderContainer>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const ThreeUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
const ThreeUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
||||||
const halfHeight = height / 2;
|
const halfHeight = height / 2
|
||||||
const halfWidth = width / 2;
|
const halfWidth = width / 2
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlaceholderContainer height={height} width={width}>
|
<PlaceholderContainer height={height} width={width}>
|
||||||
@ -93,11 +93,11 @@ const ThreeUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) =
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</PlaceholderContainer>
|
</PlaceholderContainer>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const TwoUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
const TwoUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
||||||
const halfHeight = height / 2;
|
const halfHeight = height / 2
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlaceholderContainer height={height} width={width}>
|
<PlaceholderContainer height={height} width={width}>
|
||||||
@ -116,16 +116,16 @@ const TwoUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) =>
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</PlaceholderContainer>
|
</PlaceholderContainer>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const OneUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
const OneUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
||||||
return (
|
return (
|
||||||
<PlaceholderContainer height={height} width={width}>
|
<PlaceholderContainer height={height} width={width}>
|
||||||
<FastImage source={{ uri: coverArtUris[0] }} style={{ height, width }} resizeMode={FastImage.resizeMode.cover} />
|
<FastImage source={{ uri: coverArtUris[0] }} style={{ height, width }} resizeMode={FastImage.resizeMode.cover} />
|
||||||
</PlaceholderContainer>
|
</PlaceholderContainer>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const NoneUp: React.FC<ArtistArtSizeProps> = ({ height, width }) => {
|
const NoneUp: React.FC<ArtistArtSizeProps> = ({ height, width }) => {
|
||||||
return (
|
return (
|
||||||
@ -139,35 +139,35 @@ const NoneUp: React.FC<ArtistArtSizeProps> = ({ height, width }) => {
|
|||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
/>
|
/>
|
||||||
</PlaceholderContainer>
|
</PlaceholderContainer>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const ArtistArt: React.FC<ArtistArtProps> = ({ id, height, width }) => {
|
const ArtistArt: React.FC<ArtistArtProps> = ({ id, height, width }) => {
|
||||||
const artistArt = useAtomValue(artistArtAtomFamily(id));
|
const artistArt = useAtomValue(artistArtAtomFamily(id))
|
||||||
|
|
||||||
const Placeholder = () => {
|
const Placeholder = () => {
|
||||||
const none = <NoneUp height={height} width={width} />;
|
const none = <NoneUp height={height} width={width} />
|
||||||
|
|
||||||
if (!artistArt || !artistArt.coverArtUris) {
|
if (!artistArt || !artistArt.coverArtUris) {
|
||||||
return none;
|
return none
|
||||||
}
|
}
|
||||||
const { coverArtUris } = artistArt;
|
const { coverArtUris } = artistArt
|
||||||
|
|
||||||
if (coverArtUris.length >= 4) {
|
if (coverArtUris.length >= 4) {
|
||||||
return <FourUp height={height} width={width} coverArtUris={coverArtUris} />;
|
return <FourUp height={height} width={width} coverArtUris={coverArtUris} />
|
||||||
}
|
}
|
||||||
if (coverArtUris.length === 3) {
|
if (coverArtUris.length === 3) {
|
||||||
return <ThreeUp height={height} width={width} coverArtUris={coverArtUris} />;
|
return <ThreeUp height={height} width={width} coverArtUris={coverArtUris} />
|
||||||
}
|
}
|
||||||
if (coverArtUris.length === 2) {
|
if (coverArtUris.length === 2) {
|
||||||
return <TwoUp height={height} width={width} coverArtUris={coverArtUris} />;
|
return <TwoUp height={height} width={width} coverArtUris={coverArtUris} />
|
||||||
}
|
}
|
||||||
if (coverArtUris.length === 1) {
|
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 (
|
return (
|
||||||
<View
|
<View
|
||||||
@ -177,8 +177,8 @@ const ArtistArt: React.FC<ArtistArtProps> = ({ id, height, width }) => {
|
|||||||
}}>
|
}}>
|
||||||
<CoverArt PlaceholderComponent={Placeholder} height={height} width={width} coverArtUri={artistArt?.uri} />
|
<CoverArt PlaceholderComponent={Placeholder} height={height} width={width} coverArtUri={artistArt?.uri} />
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const ArtistArtFallback: React.FC<ArtistArtProps> = ({ height, width }) => (
|
const ArtistArtFallback: React.FC<ArtistArtProps> = ({ height, width }) => (
|
||||||
<View
|
<View
|
||||||
@ -190,12 +190,12 @@ const ArtistArtFallback: React.FC<ArtistArtProps> = ({ height, width }) => (
|
|||||||
}}>
|
}}>
|
||||||
<ActivityIndicator size="small" color={colors.accent} />
|
<ActivityIndicator size="small" color={colors.accent} />
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
|
|
||||||
const ArtistArtLoader: React.FC<ArtistArtProps> = props => (
|
const ArtistArtLoader: React.FC<ArtistArtProps> = props => (
|
||||||
<React.Suspense fallback={<ArtistArtFallback {...props} />}>
|
<React.Suspense fallback={<ArtistArtFallback {...props} />}>
|
||||||
<ArtistArt {...props} />
|
<ArtistArt {...props} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default React.memo(ArtistArtLoader);
|
export default React.memo(ArtistArtLoader)
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { useAtomValue } from 'jotai/utils';
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react'
|
||||||
import { Text } from 'react-native';
|
import { Text } from 'react-native'
|
||||||
import { artistInfoAtomFamily } from '../../state/music';
|
import { artistInfoAtomFamily } from '../../state/music'
|
||||||
import text from '../../styles/text';
|
import text from '../../styles/text'
|
||||||
import ArtistArt from './ArtistArt';
|
import ArtistArt from './ArtistArt'
|
||||||
import GradientScrollView from './GradientScrollView';
|
import GradientScrollView from './GradientScrollView'
|
||||||
|
|
||||||
const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
|
const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
|
||||||
const artist = useAtomValue(artistInfoAtomFamily(id));
|
const artist = useAtomValue(artistInfoAtomFamily(id))
|
||||||
|
|
||||||
if (!artist) {
|
if (!artist) {
|
||||||
return <></>;
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -26,24 +26,24 @@ const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
|
|||||||
<Text style={text.paragraph}>{artist.name}</Text>
|
<Text style={text.paragraph}>{artist.name}</Text>
|
||||||
<ArtistArt id={artist.id} height={200} width={200} />
|
<ArtistArt id={artist.id} height={200} width={200} />
|
||||||
</GradientScrollView>
|
</GradientScrollView>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const ArtistView: React.FC<{
|
const ArtistView: React.FC<{
|
||||||
id: string;
|
id: string
|
||||||
title: string;
|
title: string
|
||||||
}> = ({ id, title }) => {
|
}> = ({ id, title }) => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.setOptions({ title });
|
navigation.setOptions({ title })
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||||
<ArtistDetails id={id} />
|
<ArtistDetails id={id} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default React.memo(ArtistView);
|
export default React.memo(ArtistView)
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react'
|
||||||
import { Text, View, Pressable } from 'react-native';
|
import { Text, View, Pressable } from 'react-native'
|
||||||
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
import { BottomTabBarProps } from '@react-navigation/bottom-tabs'
|
||||||
import textStyles from '../../styles/text';
|
import textStyles from '../../styles/text'
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors'
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image'
|
||||||
|
|
||||||
const icons: { [key: string]: any } = {
|
const icons: { [key: string]: any } = {
|
||||||
home: {
|
home: {
|
||||||
@ -22,29 +22,29 @@ const icons: { [key: string]: any } = {
|
|||||||
regular: require('../../../res/settings.png'),
|
regular: require('../../../res/settings.png'),
|
||||||
fill: require('../../../res/settings-fill.png'),
|
fill: require('../../../res/settings-fill.png'),
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
const BottomTabButton: React.FC<{
|
const BottomTabButton: React.FC<{
|
||||||
routeKey: string;
|
routeKey: string
|
||||||
label: string;
|
label: string
|
||||||
name: string;
|
name: string
|
||||||
isFocused: boolean;
|
isFocused: boolean
|
||||||
img: { regular: number; fill: number };
|
img: { regular: number; fill: number }
|
||||||
navigation: any;
|
navigation: any
|
||||||
}> = ({ routeKey, label, name, isFocused, img, navigation }) => {
|
}> = ({ routeKey, label, name, isFocused, img, navigation }) => {
|
||||||
const [opacity, setOpacity] = useState(1);
|
const [opacity, setOpacity] = useState(1)
|
||||||
|
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
const event = navigation.emit({
|
const event = navigation.emit({
|
||||||
type: 'tabPress',
|
type: 'tabPress',
|
||||||
target: routeKey,
|
target: routeKey,
|
||||||
canPreventDefault: true,
|
canPreventDefault: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!isFocused && !event.defaultPrevented) {
|
if (!isFocused && !event.defaultPrevented) {
|
||||||
navigation.navigate(name);
|
navigation.navigate(name)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -72,8 +72,8 @@ const BottomTabButton: React.FC<{
|
|||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigation }) => {
|
const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigation }) => {
|
||||||
return (
|
return (
|
||||||
@ -87,13 +87,13 @@ const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigat
|
|||||||
paddingHorizontal: 28,
|
paddingHorizontal: 28,
|
||||||
}}>
|
}}>
|
||||||
{state.routes.map((route, index) => {
|
{state.routes.map((route, index) => {
|
||||||
const { options } = descriptors[route.key] as any;
|
const { options } = descriptors[route.key] as any
|
||||||
const label =
|
const label =
|
||||||
options.tabBarLabel !== undefined
|
options.tabBarLabel !== undefined
|
||||||
? (options.tabBarLabel as string)
|
? (options.tabBarLabel as string)
|
||||||
: options.title !== undefined
|
: options.title !== undefined
|
||||||
? options.title
|
? options.title
|
||||||
: route.name;
|
: route.name
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomTabButton
|
<BottomTabButton
|
||||||
@ -105,10 +105,10 @@ const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigat
|
|||||||
img={icons[options.icon]}
|
img={icons[options.icon]}
|
||||||
navigation={navigation}
|
navigation={navigation}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default BottomTabBar;
|
export default BottomTabBar
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react'
|
||||||
import { GestureResponderEvent, Pressable, Text } from 'react-native';
|
import { GestureResponderEvent, Pressable, Text } from 'react-native'
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors'
|
||||||
import text from '../../styles/text';
|
import text from '../../styles/text'
|
||||||
|
|
||||||
const Button: React.FC<{
|
const Button: React.FC<{
|
||||||
title: string;
|
title: string
|
||||||
onPress: (event: GestureResponderEvent) => void;
|
onPress: (event: GestureResponderEvent) => void
|
||||||
}> = ({ title, onPress }) => {
|
}> = ({ title, onPress }) => {
|
||||||
const [opacity, setOpacity] = useState(1);
|
const [opacity, setOpacity] = useState(1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -25,7 +25,7 @@ const Button: React.FC<{
|
|||||||
}}>
|
}}>
|
||||||
<Text style={{ ...text.button }}>{title}</Text>
|
<Text style={{ ...text.button }}>{title}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Button;
|
export default Button
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react'
|
||||||
import { ActivityIndicator, View } from 'react-native';
|
import { ActivityIndicator, View } from 'react-native'
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image'
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors'
|
||||||
|
|
||||||
const CoverArt: React.FC<{
|
const CoverArt: React.FC<{
|
||||||
PlaceholderComponent: () => JSX.Element;
|
PlaceholderComponent: () => JSX.Element
|
||||||
height: number;
|
height: number
|
||||||
width: number;
|
width: number
|
||||||
coverArtUri?: string;
|
coverArtUri?: string
|
||||||
}> = ({ PlaceholderComponent, height, width, coverArtUri }) => {
|
}> = ({ PlaceholderComponent, height, width, coverArtUri }) => {
|
||||||
const [placeholderVisible, setPlaceholderVisible] = useState(false);
|
const [placeholderVisible, setPlaceholderVisible] = useState(false)
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
const indicatorSize = height > 130 ? 'large' : 'small';
|
const indicatorSize = height > 130 ? 'large' : 'small'
|
||||||
const halfIndicatorHeight = indicatorSize === 'large' ? 18 : 10;
|
const halfIndicatorHeight = indicatorSize === 'large' ? 18 : 10
|
||||||
|
|
||||||
const Placeholder: React.FC<{ visible: boolean }> = ({ visible }) => (
|
const Placeholder: React.FC<{ visible: boolean }> = ({ visible }) => (
|
||||||
<View
|
<View
|
||||||
@ -22,7 +22,7 @@ const CoverArt: React.FC<{
|
|||||||
}}>
|
}}>
|
||||||
<PlaceholderComponent />
|
<PlaceholderComponent />
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
|
|
||||||
const Art = () => (
|
const Art = () => (
|
||||||
<>
|
<>
|
||||||
@ -44,15 +44,15 @@ const CoverArt: React.FC<{
|
|||||||
}}
|
}}
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
setPlaceholderVisible(true);
|
setPlaceholderVisible(true)
|
||||||
}}
|
}}
|
||||||
onLoadEnd={() => setLoading(false)}
|
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)
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { useWindowDimensions, ViewStyle } from 'react-native';
|
import { useWindowDimensions, ViewStyle } from 'react-native'
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import colorStyles from '../../styles/colors';
|
import colorStyles from '../../styles/colors'
|
||||||
|
|
||||||
const GradientBackground: React.FC<{
|
const GradientBackground: React.FC<{
|
||||||
height?: number | string;
|
height?: number | string
|
||||||
width?: number | string;
|
width?: number | string
|
||||||
position?: 'relative' | 'absolute';
|
position?: 'relative' | 'absolute'
|
||||||
style?: ViewStyle;
|
style?: ViewStyle
|
||||||
colors?: string[];
|
colors?: string[]
|
||||||
locations?: number[];
|
locations?: number[]
|
||||||
}> = ({ height, width, position, style, colors, locations, children }) => {
|
}> = ({ height, width, position, style, colors, locations, children }) => {
|
||||||
const layout = useWindowDimensions();
|
const layout = useWindowDimensions()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
@ -25,7 +25,7 @@ const GradientBackground: React.FC<{
|
|||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default GradientBackground;
|
export default GradientBackground
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { FlatList, FlatListProps, useWindowDimensions } from 'react-native';
|
import { FlatList, FlatListProps, useWindowDimensions } from 'react-native'
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors'
|
||||||
import GradientBackground from './GradientBackground';
|
import GradientBackground from './GradientBackground'
|
||||||
|
|
||||||
function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) {
|
function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) {
|
||||||
const layout = useWindowDimensions();
|
const layout = useWindowDimensions()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
@ -18,7 +18,7 @@ function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) {
|
|||||||
marginBottom: -layout.height,
|
marginBottom: -layout.height,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GradientFlatList;
|
export default GradientFlatList
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { ScrollView, ScrollViewProps, ViewStyle } from 'react-native';
|
import { ScrollView, ScrollViewProps, ViewStyle } from 'react-native'
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors'
|
||||||
import GradientBackground from './GradientBackground';
|
import GradientBackground from './GradientBackground'
|
||||||
|
|
||||||
const GradientScrollView: React.FC<ScrollViewProps> = props => {
|
const GradientScrollView: React.FC<ScrollViewProps> = props => {
|
||||||
props.style = props.style || {};
|
props.style = props.style || {}
|
||||||
(props.style as ViewStyle).backgroundColor = colors.gradient.low;
|
;(props.style as ViewStyle).backgroundColor = colors.gradient.low
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView overScrollMode="never" {...props}>
|
<ScrollView overScrollMode="never" {...props}>
|
||||||
<GradientBackground />
|
<GradientBackground />
|
||||||
{props.children}
|
{props.children}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default GradientScrollView;
|
export default GradientScrollView
|
||||||
|
|||||||
@ -1,58 +1,58 @@
|
|||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react'
|
||||||
import { ViewStyle } from 'react-native';
|
import { ViewStyle } from 'react-native'
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image'
|
||||||
import ImageColors from 'react-native-image-colors';
|
import ImageColors from 'react-native-image-colors'
|
||||||
import { AndroidImageColors } from 'react-native-image-colors/lib/typescript/types';
|
import { AndroidImageColors } from 'react-native-image-colors/lib/typescript/types'
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors'
|
||||||
import GradientBackground from './GradientBackground';
|
import GradientBackground from './GradientBackground'
|
||||||
|
|
||||||
const ImageGradientBackground: React.FC<{
|
const ImageGradientBackground: React.FC<{
|
||||||
height?: number | string;
|
height?: number | string
|
||||||
width?: number | string;
|
width?: number | string
|
||||||
position?: 'relative' | 'absolute';
|
position?: 'relative' | 'absolute'
|
||||||
style?: ViewStyle;
|
style?: ViewStyle
|
||||||
imageUri?: string;
|
imageUri?: string
|
||||||
imageKey?: string;
|
imageKey?: string
|
||||||
}> = ({ height, width, position, style, imageUri, imageKey, children }) => {
|
}> = ({ height, width, position, style, imageUri, imageKey, children }) => {
|
||||||
const [highColor, setHighColor] = useState<string>(colors.gradient.high);
|
const [highColor, setHighColor] = useState<string>(colors.gradient.high)
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getColors() {
|
async function getColors() {
|
||||||
if (imageUri === undefined) {
|
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) {
|
if (cachedResult) {
|
||||||
res = cachedResult as AndroidImageColors;
|
res = cachedResult as AndroidImageColors
|
||||||
} else {
|
} else {
|
||||||
const path = await FastImage.getCachePath({ uri: imageUri });
|
const path = await FastImage.getCachePath({ uri: imageUri })
|
||||||
res = (await ImageColors.getColors(path ? `file://${path}` : imageUri, {
|
res = (await ImageColors.getColors(path ? `file://${path}` : imageUri, {
|
||||||
cache: true,
|
cache: true,
|
||||||
key: imageKey ? imageKey : imageUri,
|
key: imageKey ? imageKey : imageUri,
|
||||||
})) as AndroidImageColors;
|
})) as AndroidImageColors
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.muted && res.muted !== '#000000') {
|
if (res.muted && res.muted !== '#000000') {
|
||||||
setHighColor(res.muted);
|
setHighColor(res.muted)
|
||||||
} else if (res.darkMuted && res.darkMuted !== '#000000') {
|
} else if (res.darkMuted && res.darkMuted !== '#000000') {
|
||||||
setHighColor(res.darkMuted);
|
setHighColor(res.darkMuted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getColors();
|
getColors()
|
||||||
}, [imageUri, imageKey]);
|
}, [imageUri, imageKey])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerStyle: {
|
headerStyle: {
|
||||||
backgroundColor: highColor,
|
backgroundColor: highColor,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
}, [navigation, highColor]);
|
}, [navigation, highColor])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientBackground
|
<GradientBackground
|
||||||
@ -64,7 +64,7 @@ const ImageGradientBackground: React.FC<{
|
|||||||
locations={[0.1, 1.0]}>
|
locations={[0.1, 1.0]}>
|
||||||
{children}
|
{children}
|
||||||
</GradientBackground>
|
</GradientBackground>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default ImageGradientBackground;
|
export default ImageGradientBackground
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react'
|
||||||
import { LayoutRectangle, ScrollView, ScrollViewProps } from 'react-native';
|
import { LayoutRectangle, ScrollView, ScrollViewProps } from 'react-native'
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors'
|
||||||
import ImageGradientBackground from './ImageGradientBackground';
|
import ImageGradientBackground from './ImageGradientBackground'
|
||||||
|
|
||||||
const ImageGradientScrollView: React.FC<ScrollViewProps & { imageUri?: string; imageKey?: string }> = props => {
|
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) {
|
if (typeof props.style === 'object' && props.style !== null) {
|
||||||
props.style = {
|
props.style = {
|
||||||
...props.style,
|
...props.style,
|
||||||
backgroundColor: colors.gradient.low,
|
backgroundColor: colors.gradient.low,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -19,12 +19,12 @@ const ImageGradientScrollView: React.FC<ScrollViewProps & { imageUri?: string; i
|
|||||||
overScrollMode="never"
|
overScrollMode="never"
|
||||||
{...props}
|
{...props}
|
||||||
onLayout={event => {
|
onLayout={event => {
|
||||||
setLayout(event.nativeEvent.layout);
|
setLayout(event.nativeEvent.layout)
|
||||||
}}>
|
}}>
|
||||||
<ImageGradientBackground height={layout?.height} imageUri={props.imageUri} imageKey={props.imageKey} />
|
<ImageGradientBackground height={layout?.height} imageUri={props.imageUri} imageKey={props.imageKey} />
|
||||||
{props.children}
|
{props.children}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default ImageGradientScrollView;
|
export default ImageGradientScrollView
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors'
|
||||||
|
|
||||||
const TopTabContainer: React.FC<{}> = ({ children }) => (
|
const TopTabContainer: React.FC<{}> = ({ children }) => (
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
@ -11,6 +11,6 @@ const TopTabContainer: React.FC<{}> = ({ children }) => (
|
|||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default TopTabContainer;
|
export default TopTabContainer
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { useAtomValue } from 'jotai/utils';
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react'
|
||||||
import { Pressable, Text, View } from 'react-native';
|
import { Pressable, Text, View } from 'react-native'
|
||||||
import { Album } from '../../models/music';
|
import { Album } from '../../models/music'
|
||||||
import { albumsAtom, albumsUpdatingAtom, useUpdateAlbums } from '../../state/music';
|
import { albumsAtom, albumsUpdatingAtom, useUpdateAlbums } from '../../state/music'
|
||||||
import textStyles from '../../styles/text';
|
import textStyles from '../../styles/text'
|
||||||
import AlbumArt from '../common/AlbumArt';
|
import AlbumArt from '../common/AlbumArt'
|
||||||
import GradientFlatList from '../common/GradientFlatList';
|
import GradientFlatList from '../common/GradientFlatList'
|
||||||
|
|
||||||
const AlbumItem: React.FC<{
|
const AlbumItem: React.FC<{
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
artist?: string;
|
artist?: string
|
||||||
}> = ({ id, name, artist }) => {
|
}> = ({ id, name, artist }) => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation()
|
||||||
|
|
||||||
const size = 125;
|
const size = 125
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -44,26 +44,26 @@ const AlbumItem: React.FC<{
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
const MemoAlbumItem = React.memo(AlbumItem);
|
const MemoAlbumItem = React.memo(AlbumItem)
|
||||||
|
|
||||||
const AlbumListRenderItem: React.FC<{ item: Album }> = ({ item }) => (
|
const AlbumListRenderItem: React.FC<{ item: Album }> = ({ item }) => (
|
||||||
<MemoAlbumItem id={item.id} name={item.name} artist={item.artist} />
|
<MemoAlbumItem id={item.id} name={item.name} artist={item.artist} />
|
||||||
);
|
)
|
||||||
|
|
||||||
const AlbumsList = () => {
|
const AlbumsList = () => {
|
||||||
const albums = useAtomValue(albumsAtom);
|
const albums = useAtomValue(albumsAtom)
|
||||||
const updating = useAtomValue(albumsUpdatingAtom);
|
const updating = useAtomValue(albumsUpdatingAtom)
|
||||||
const updateAlbums = useUpdateAlbums();
|
const updateAlbums = useUpdateAlbums()
|
||||||
|
|
||||||
const albumsList = Object.values(albums);
|
const albumsList = Object.values(albums)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (albumsList.length === 0) {
|
if (albumsList.length === 0) {
|
||||||
updateAlbums();
|
updateAlbums()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
@ -78,13 +78,13 @@ const AlbumsList = () => {
|
|||||||
overScrollMode="never"
|
overScrollMode="never"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const AlbumsTab = () => (
|
const AlbumsTab = () => (
|
||||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||||
<AlbumsList />
|
<AlbumsList />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default React.memo(AlbumsTab);
|
export default React.memo(AlbumsTab)
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { useAtomValue } from 'jotai/utils';
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react'
|
||||||
import { Pressable } from 'react-native';
|
import { Pressable } from 'react-native'
|
||||||
import { Text } from 'react-native';
|
import { Text } from 'react-native'
|
||||||
import { Artist } from '../../models/music';
|
import { Artist } from '../../models/music'
|
||||||
import { artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '../../state/music';
|
import { artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '../../state/music'
|
||||||
import textStyles from '../../styles/text';
|
import textStyles from '../../styles/text'
|
||||||
import ArtistArt from '../common/ArtistArt';
|
import ArtistArt from '../common/ArtistArt'
|
||||||
import GradientFlatList from '../common/GradientFlatList';
|
import GradientFlatList from '../common/GradientFlatList'
|
||||||
|
|
||||||
const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => {
|
const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -30,27 +30,27 @@ const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => {
|
|||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const ArtistItemLoader: React.FC<{ item: Artist }> = props => (
|
const ArtistItemLoader: React.FC<{ item: Artist }> = props => (
|
||||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||||
<ArtistItem {...props} />
|
<ArtistItem {...props} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
);
|
)
|
||||||
|
|
||||||
const ArtistsList = () => {
|
const ArtistsList = () => {
|
||||||
const artists = useAtomValue(artistsAtom);
|
const artists = useAtomValue(artistsAtom)
|
||||||
const updating = useAtomValue(artistsUpdatingAtom);
|
const updating = useAtomValue(artistsUpdatingAtom)
|
||||||
const updateArtists = useUpdateArtists();
|
const updateArtists = useUpdateArtists()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (artists.length === 0) {
|
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 (
|
return (
|
||||||
<GradientFlatList
|
<GradientFlatList
|
||||||
@ -61,9 +61,9 @@ const ArtistsList = () => {
|
|||||||
refreshing={updating}
|
refreshing={updating}
|
||||||
overScrollMode="never"
|
overScrollMode="never"
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const ArtistsTab = () => <ArtistsList />;
|
const ArtistsTab = () => <ArtistsList />
|
||||||
|
|
||||||
export default ArtistsTab;
|
export default ArtistsTab
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import GradientBackground from '../common/GradientBackground';
|
import GradientBackground from '../common/GradientBackground'
|
||||||
|
|
||||||
const PlaylistsTab = () => <GradientBackground />;
|
const PlaylistsTab = () => <GradientBackground />
|
||||||
|
|
||||||
export default PlaylistsTab;
|
export default PlaylistsTab
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
||||||
import SettingsView from '../Settings';
|
import SettingsView from '../Settings'
|
||||||
import NowPlayingLayout from '../NowPlayingLayout';
|
import NowPlayingLayout from '../NowPlayingLayout'
|
||||||
import ArtistsList from '../ArtistsList';
|
import ArtistsList from '../ArtistsList'
|
||||||
import LibraryTopTabNavigator from './LibraryTopTabNavigator';
|
import LibraryTopTabNavigator from './LibraryTopTabNavigator'
|
||||||
import BottomTabBar from '../common/BottomTabBar';
|
import BottomTabBar from '../common/BottomTabBar'
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator();
|
const Tab = createBottomTabNavigator()
|
||||||
|
|
||||||
const BottomTabNavigator = () => {
|
const BottomTabNavigator = () => {
|
||||||
return (
|
return (
|
||||||
@ -16,7 +16,7 @@ const BottomTabNavigator = () => {
|
|||||||
<Tab.Screen name="Search" component={NowPlayingLayout} options={{ icon: 'search' } as any} />
|
<Tab.Screen name="Search" component={NowPlayingLayout} options={{ icon: 'search' } as any} />
|
||||||
<Tab.Screen name="Settings" component={SettingsView} options={{ icon: 'settings' } as any} />
|
<Tab.Screen name="Settings" component={SettingsView} options={{ icon: 'settings' } as any} />
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default BottomTabNavigator;
|
export default BottomTabNavigator
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { StatusBar, View } from 'react-native';
|
import { StatusBar, View } from 'react-native'
|
||||||
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
|
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
|
||||||
import AlbumsTab from '../library/AlbumsTab';
|
import AlbumsTab from '../library/AlbumsTab'
|
||||||
import ArtistsTab from '../library/ArtistsTab';
|
import ArtistsTab from '../library/ArtistsTab'
|
||||||
import PlaylistsTab from '../library/PlaylistsTab';
|
import PlaylistsTab from '../library/PlaylistsTab'
|
||||||
import { createNativeStackNavigator, NativeStackNavigationProp } from 'react-native-screens/native-stack';
|
import { createNativeStackNavigator, NativeStackNavigationProp } from 'react-native-screens/native-stack'
|
||||||
import AlbumView from '../common/AlbumView';
|
import AlbumView from '../common/AlbumView'
|
||||||
import { RouteProp } from '@react-navigation/native';
|
import { RouteProp } from '@react-navigation/native'
|
||||||
import text from '../../styles/text';
|
import text from '../../styles/text'
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors'
|
||||||
import ArtistView from '../common/ArtistView';
|
import ArtistView from '../common/ArtistView'
|
||||||
|
|
||||||
const Tab = createMaterialTopTabNavigator();
|
const Tab = createMaterialTopTabNavigator()
|
||||||
|
|
||||||
const LibraryTopTabNavigator = () => (
|
const LibraryTopTabNavigator = () => (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
@ -36,37 +36,37 @@ const LibraryTopTabNavigator = () => (
|
|||||||
<Tab.Screen name="Artists" component={ArtistsTab} />
|
<Tab.Screen name="Artists" component={ArtistsTab} />
|
||||||
<Tab.Screen name="Playlists" component={PlaylistsTab} />
|
<Tab.Screen name="Playlists" component={PlaylistsTab} />
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
);
|
)
|
||||||
|
|
||||||
type LibraryStackParamList = {
|
type LibraryStackParamList = {
|
||||||
LibraryTopTabs: undefined;
|
LibraryTopTabs: undefined
|
||||||
AlbumView: { id: string; title: string };
|
AlbumView: { id: string; title: string }
|
||||||
ArtistView: { id: string; title: string };
|
ArtistView: { id: string; title: string }
|
||||||
};
|
}
|
||||||
|
|
||||||
type AlbumScreenNavigationProp = NativeStackNavigationProp<LibraryStackParamList, 'AlbumView'>;
|
type AlbumScreenNavigationProp = NativeStackNavigationProp<LibraryStackParamList, 'AlbumView'>
|
||||||
type AlbumScreenRouteProp = RouteProp<LibraryStackParamList, 'AlbumView'>;
|
type AlbumScreenRouteProp = RouteProp<LibraryStackParamList, 'AlbumView'>
|
||||||
type AlbumScreenProps = {
|
type AlbumScreenProps = {
|
||||||
route: AlbumScreenRouteProp;
|
route: AlbumScreenRouteProp
|
||||||
navigation: AlbumScreenNavigationProp;
|
navigation: AlbumScreenNavigationProp
|
||||||
};
|
}
|
||||||
|
|
||||||
const AlbumScreen: React.FC<AlbumScreenProps> = ({ route }) => (
|
const AlbumScreen: React.FC<AlbumScreenProps> = ({ route }) => (
|
||||||
<AlbumView id={route.params.id} title={route.params.title} />
|
<AlbumView id={route.params.id} title={route.params.title} />
|
||||||
);
|
)
|
||||||
|
|
||||||
type ArtistScreenNavigationProp = NativeStackNavigationProp<LibraryStackParamList, 'ArtistView'>;
|
type ArtistScreenNavigationProp = NativeStackNavigationProp<LibraryStackParamList, 'ArtistView'>
|
||||||
type ArtistScreenRouteProp = RouteProp<LibraryStackParamList, 'ArtistView'>;
|
type ArtistScreenRouteProp = RouteProp<LibraryStackParamList, 'ArtistView'>
|
||||||
type ArtistScreenProps = {
|
type ArtistScreenProps = {
|
||||||
route: ArtistScreenRouteProp;
|
route: ArtistScreenRouteProp
|
||||||
navigation: ArtistScreenNavigationProp;
|
navigation: ArtistScreenNavigationProp
|
||||||
};
|
}
|
||||||
|
|
||||||
const ArtistScreen: React.FC<ArtistScreenProps> = ({ route }) => (
|
const ArtistScreen: React.FC<ArtistScreenProps> = ({ route }) => (
|
||||||
<ArtistView id={route.params.id} title={route.params.title} />
|
<ArtistView id={route.params.id} title={route.params.title} />
|
||||||
);
|
)
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<LibraryStackParamList>();
|
const Stack = createNativeStackNavigator<LibraryStackParamList>()
|
||||||
|
|
||||||
const itemScreenOptions = {
|
const itemScreenOptions = {
|
||||||
title: '',
|
title: '',
|
||||||
@ -78,7 +78,7 @@ const itemScreenOptions = {
|
|||||||
headerTitleStyle: {
|
headerTitleStyle: {
|
||||||
...text.header,
|
...text.header,
|
||||||
} as any,
|
} as any,
|
||||||
};
|
}
|
||||||
|
|
||||||
const LibraryStackNavigator = () => (
|
const LibraryStackNavigator = () => (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
@ -88,6 +88,6 @@ const LibraryStackNavigator = () => (
|
|||||||
<Stack.Screen name="ArtistView" component={ArtistScreen} options={itemScreenOptions} />
|
<Stack.Screen name="ArtistView" component={ArtistScreen} options={itemScreenOptions} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default LibraryStackNavigator;
|
export default LibraryStackNavigator
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack';
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
import NowPlayingLayout from '../NowPlayingLayout';
|
import NowPlayingLayout from '../NowPlayingLayout'
|
||||||
import BottomTabNavigator from './BottomTabNavigator';
|
import BottomTabNavigator from './BottomTabNavigator'
|
||||||
|
|
||||||
const RootStack = createNativeStackNavigator();
|
const RootStack = createNativeStackNavigator()
|
||||||
|
|
||||||
const RootNavigator = () => (
|
const RootNavigator = () => (
|
||||||
<RootStack.Navigator
|
<RootStack.Navigator
|
||||||
@ -13,6 +13,6 @@ const RootNavigator = () => (
|
|||||||
<RootStack.Screen name="Main" component={BottomTabNavigator} />
|
<RootStack.Screen name="Main" component={BottomTabNavigator} />
|
||||||
<RootStack.Screen name="Now Playing" component={NowPlayingLayout} />
|
<RootStack.Screen name="Now Playing" component={NowPlayingLayout} />
|
||||||
</RootStack.Navigator>
|
</RootStack.Navigator>
|
||||||
);
|
)
|
||||||
|
|
||||||
export default RootNavigator;
|
export default RootNavigator
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { useAtomValue } from 'jotai/utils';
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import { activeServerAtom } from '../state/settings';
|
import { activeServerAtom } from '../state/settings'
|
||||||
import { SubsonicApiClient } from '../subsonic/api';
|
import { SubsonicApiClient } from '../subsonic/api'
|
||||||
|
|
||||||
export const useSubsonicApi = () => {
|
export const useSubsonicApi = () => {
|
||||||
const activeServer = useAtomValue(activeServerAtom);
|
const activeServer = useAtomValue(activeServerAtom)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!activeServer) {
|
if (!activeServer) {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
return new SubsonicApiClient(activeServer);
|
return new SubsonicApiClient(activeServer)
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useUpdateAtom } from 'jotai/utils';
|
import { useUpdateAtom } from 'jotai/utils'
|
||||||
import TrackPlayer, { Track } from 'react-native-track-player';
|
import TrackPlayer, { Track } from 'react-native-track-player'
|
||||||
import { Song } from '../models/music';
|
import { Song } from '../models/music'
|
||||||
import { currentQueueNameAtom, currentTrackAtom } from '../state/trackplayer';
|
import { currentQueueNameAtom, currentTrackAtom } from '../state/trackplayer'
|
||||||
|
|
||||||
function mapSongToTrack(song: Song, queueName: string): Track {
|
function mapSongToTrack(song: Song, queueName: string): Track {
|
||||||
return {
|
return {
|
||||||
@ -13,39 +13,39 @@ function mapSongToTrack(song: Song, queueName: string): Track {
|
|||||||
artwork: song.coverArtUri,
|
artwork: song.coverArtUri,
|
||||||
artworkThumb: song.coverArtThumbUri,
|
artworkThumb: song.coverArtThumbUri,
|
||||||
duration: song.duration,
|
duration: song.duration,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSetQueue = () => {
|
export const useSetQueue = () => {
|
||||||
const setCurrentTrack = useUpdateAtom(currentTrackAtom);
|
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
||||||
const setCurrentQueueName = useUpdateAtom(currentQueueNameAtom);
|
const setCurrentQueueName = useUpdateAtom(currentQueueNameAtom)
|
||||||
|
|
||||||
return async (songs: Song[], name: string, playId?: string) => {
|
return async (songs: Song[], name: string, playId?: string) => {
|
||||||
await TrackPlayer.reset();
|
await TrackPlayer.reset()
|
||||||
const tracks = songs.map(s => mapSongToTrack(s, name));
|
const tracks = songs.map(s => mapSongToTrack(s, name))
|
||||||
|
|
||||||
setCurrentQueueName(name);
|
setCurrentQueueName(name)
|
||||||
if (playId) {
|
if (playId) {
|
||||||
setCurrentTrack(tracks.find(t => t.id === playId));
|
setCurrentTrack(tracks.find(t => t.id === playId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!playId) {
|
if (!playId) {
|
||||||
await TrackPlayer.add(tracks);
|
await TrackPlayer.add(tracks)
|
||||||
} else if (playId === tracks[0].id) {
|
} else if (playId === tracks[0].id) {
|
||||||
await TrackPlayer.add(tracks);
|
await TrackPlayer.add(tracks)
|
||||||
await TrackPlayer.play();
|
await TrackPlayer.play()
|
||||||
} else {
|
} else {
|
||||||
const playIndex = tracks.findIndex(t => t.id === playId);
|
const playIndex = tracks.findIndex(t => t.id === playId)
|
||||||
const tracks1 = tracks.slice(0, playIndex);
|
const tracks1 = tracks.slice(0, playIndex)
|
||||||
const tracks2 = tracks.slice(playIndex);
|
const tracks2 = tracks.slice(playIndex)
|
||||||
|
|
||||||
await TrackPlayer.add(tracks2);
|
await TrackPlayer.add(tracks2)
|
||||||
await TrackPlayer.play();
|
await TrackPlayer.play()
|
||||||
|
|
||||||
await TrackPlayer.add(tracks1, 0);
|
await TrackPlayer.add(tracks1, 0)
|
||||||
|
|
||||||
// const queue = await TrackPlayer.getQueue();
|
// const queue = await TrackPlayer.getQueue();
|
||||||
// console.log(`queue: ${JSON.stringify(queue.map(x => x.title))}`);
|
// console.log(`queue: ${JSON.stringify(queue.map(x => x.title))}`);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,95 +1,95 @@
|
|||||||
export interface Artist {
|
export interface Artist {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
starred?: Date;
|
starred?: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArtistInfo extends Artist {
|
export interface ArtistInfo extends Artist {
|
||||||
albums: Album[];
|
albums: Album[]
|
||||||
|
|
||||||
mediumImageUrl?: string;
|
mediumImageUrl?: string
|
||||||
largeImageUrl?: string;
|
largeImageUrl?: string
|
||||||
coverArtUris: string[];
|
coverArtUris: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArtistArt {
|
export interface ArtistArt {
|
||||||
uri?: string;
|
uri?: string
|
||||||
coverArtUris: string[];
|
coverArtUris: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Album {
|
export interface Album {
|
||||||
id: string;
|
id: string
|
||||||
artistId?: string;
|
artistId?: string
|
||||||
artist?: string;
|
artist?: string
|
||||||
name: string;
|
name: string
|
||||||
starred?: Date;
|
starred?: Date
|
||||||
coverArt?: string;
|
coverArt?: string
|
||||||
coverArtUri?: string;
|
coverArtUri?: string
|
||||||
coverArtThumbUri?: string;
|
coverArtThumbUri?: string
|
||||||
year?: number;
|
year?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlbumArt {
|
export interface AlbumArt {
|
||||||
uri?: string;
|
uri?: string
|
||||||
thumbUri?: string;
|
thumbUri?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlbumWithSongs extends Album {
|
export interface AlbumWithSongs extends Album {
|
||||||
songs: Song[];
|
songs: Song[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Song {
|
export interface Song {
|
||||||
id: string;
|
id: string
|
||||||
album?: string;
|
album?: string
|
||||||
artist?: string;
|
artist?: string
|
||||||
title: string;
|
title: string
|
||||||
track?: number;
|
track?: number
|
||||||
year?: number;
|
year?: number
|
||||||
genre?: string;
|
genre?: string
|
||||||
coverArt?: string;
|
coverArt?: string
|
||||||
size?: number;
|
size?: number
|
||||||
contentType?: string;
|
contentType?: string
|
||||||
suffix?: string;
|
suffix?: string
|
||||||
duration?: number;
|
duration?: number
|
||||||
bitRate?: number;
|
bitRate?: number
|
||||||
userRating?: number;
|
userRating?: number
|
||||||
averageRating?: number;
|
averageRating?: number
|
||||||
playCount?: number;
|
playCount?: number
|
||||||
discNumber?: number;
|
discNumber?: number
|
||||||
created?: Date;
|
created?: Date
|
||||||
starred?: Date;
|
starred?: Date
|
||||||
|
|
||||||
streamUri: string;
|
streamUri: string
|
||||||
coverArtUri?: string;
|
coverArtUri?: string
|
||||||
coverArtThumbUri?: string;
|
coverArtThumbUri?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DownloadedSong = {
|
export type DownloadedSong = {
|
||||||
id: string;
|
id: string
|
||||||
type: 'song';
|
type: 'song'
|
||||||
name: string;
|
name: string
|
||||||
album: string;
|
album: string
|
||||||
artist: string;
|
artist: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type DownloadedAlbum = {
|
export type DownloadedAlbum = {
|
||||||
id: string;
|
id: string
|
||||||
type: 'album';
|
type: 'album'
|
||||||
songs: string[];
|
songs: string[]
|
||||||
name: string;
|
name: string
|
||||||
artist: string;
|
artist: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type DownloadedArtist = {
|
export type DownloadedArtist = {
|
||||||
id: string;
|
id: string
|
||||||
type: 'artist';
|
type: 'artist'
|
||||||
songs: string[];
|
songs: string[]
|
||||||
name: string;
|
name: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type DownloadedPlaylist = {
|
export type DownloadedPlaylist = {
|
||||||
id: string;
|
id: string
|
||||||
type: 'playlist';
|
type: 'playlist'
|
||||||
songs: string[];
|
songs: string[]
|
||||||
name: string;
|
name: string
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
export interface Server {
|
export interface Server {
|
||||||
id: string;
|
id: string
|
||||||
address: string;
|
address: string
|
||||||
username: string;
|
username: string
|
||||||
token: string;
|
token: string
|
||||||
salt: string;
|
salt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
servers: Server[];
|
servers: Server[]
|
||||||
activeServer?: string;
|
activeServer?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
imageCache: `${RNFS.DocumentDirectoryPath}/image_cache`,
|
imageCache: `${RNFS.DocumentDirectoryPath}/image_cache`,
|
||||||
songCache: `${RNFS.DocumentDirectoryPath}/song_cache`,
|
songCache: `${RNFS.DocumentDirectoryPath}/song_cache`,
|
||||||
songs: `${RNFS.DocumentDirectoryPath}/songs`,
|
songs: `${RNFS.DocumentDirectoryPath}/songs`,
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
import TrackPlayer, { Event } from 'react-native-track-player';
|
import TrackPlayer, { Event } from 'react-native-track-player'
|
||||||
|
|
||||||
module.exports = async function () {
|
module.exports = async function () {
|
||||||
TrackPlayer.addEventListener(Event.RemotePlay, () => TrackPlayer.play());
|
TrackPlayer.addEventListener(Event.RemotePlay, () => TrackPlayer.play())
|
||||||
TrackPlayer.addEventListener(Event.RemotePause, () => TrackPlayer.pause());
|
TrackPlayer.addEventListener(Event.RemotePause, () => TrackPlayer.pause())
|
||||||
TrackPlayer.addEventListener(Event.RemoteStop, () => TrackPlayer.destroy());
|
TrackPlayer.addEventListener(Event.RemoteStop, () => TrackPlayer.destroy())
|
||||||
|
|
||||||
TrackPlayer.addEventListener(Event.RemoteDuck, data => {
|
TrackPlayer.addEventListener(Event.RemoteDuck, data => {
|
||||||
if (data.permanent) {
|
if (data.permanent) {
|
||||||
TrackPlayer.stop();
|
TrackPlayer.stop()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.paused) {
|
if (data.paused) {
|
||||||
TrackPlayer.pause();
|
TrackPlayer.pause()
|
||||||
} else {
|
} else {
|
||||||
TrackPlayer.play();
|
TrackPlayer.play()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
TrackPlayer.addEventListener(Event.RemoteNext, () => TrackPlayer.skipToNext().catch(() => {}));
|
TrackPlayer.addEventListener(Event.RemoteNext, () => TrackPlayer.skipToNext().catch(() => {}))
|
||||||
TrackPlayer.addEventListener(Event.RemotePrevious, () => TrackPlayer.skipToPrevious().catch(() => {}));
|
TrackPlayer.addEventListener(Event.RemotePrevious, () => TrackPlayer.skipToPrevious().catch(() => {}))
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,31 +1,31 @@
|
|||||||
import { atom, useAtom } from 'jotai';
|
import { atom, useAtom } from 'jotai'
|
||||||
import { atomFamily, useAtomValue, useUpdateAtom } from 'jotai/utils';
|
import { atomFamily, useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||||
import { Album, AlbumArt, AlbumWithSongs, Artist, ArtistArt, ArtistInfo, Song } from '../models/music';
|
import { Album, AlbumArt, AlbumWithSongs, Artist, ArtistArt, ArtistInfo, Song } from '../models/music'
|
||||||
import { SubsonicApiClient } from '../subsonic/api';
|
import { SubsonicApiClient } from '../subsonic/api'
|
||||||
import { AlbumID3Element, ArtistInfo2Element, ChildElement } from '../subsonic/elements';
|
import { AlbumID3Element, ArtistInfo2Element, ChildElement } from '../subsonic/elements'
|
||||||
import { GetArtistResponse } from '../subsonic/responses';
|
import { GetArtistResponse } from '../subsonic/responses'
|
||||||
import { activeServerAtom } from './settings';
|
import { activeServerAtom } from './settings'
|
||||||
|
|
||||||
export const artistsAtom = atom<Artist[]>([]);
|
export const artistsAtom = atom<Artist[]>([])
|
||||||
export const artistsUpdatingAtom = atom(false);
|
export const artistsUpdatingAtom = atom(false)
|
||||||
|
|
||||||
export const useUpdateArtists = () => {
|
export const useUpdateArtists = () => {
|
||||||
const server = useAtomValue(activeServerAtom);
|
const server = useAtomValue(activeServerAtom)
|
||||||
const [updating, setUpdating] = useAtom(artistsUpdatingAtom);
|
const [updating, setUpdating] = useAtom(artistsUpdatingAtom)
|
||||||
const setArtists = useUpdateAtom(artistsAtom);
|
const setArtists = useUpdateAtom(artistsAtom)
|
||||||
|
|
||||||
if (!server) {
|
if (!server) {
|
||||||
return () => Promise.resolve();
|
return () => Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
if (updating) {
|
if (updating) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
setUpdating(true);
|
setUpdating(true)
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server);
|
const client = new SubsonicApiClient(server)
|
||||||
const response = await client.getArtists();
|
const response = await client.getArtists()
|
||||||
|
|
||||||
setArtists(
|
setArtists(
|
||||||
response.data.artists.map(x => ({
|
response.data.artists.map(x => ({
|
||||||
@ -33,146 +33,146 @@ export const useUpdateArtists = () => {
|
|||||||
name: x.name,
|
name: x.name,
|
||||||
starred: x.starred,
|
starred: x.starred,
|
||||||
})),
|
})),
|
||||||
);
|
)
|
||||||
setUpdating(false);
|
setUpdating(false)
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const albumsAtom = atom<Record<string, Album>>({});
|
export const albumsAtom = atom<Record<string, Album>>({})
|
||||||
export const albumsUpdatingAtom = atom(false);
|
export const albumsUpdatingAtom = atom(false)
|
||||||
|
|
||||||
export const useUpdateAlbums = () => {
|
export const useUpdateAlbums = () => {
|
||||||
const server = useAtomValue(activeServerAtom);
|
const server = useAtomValue(activeServerAtom)
|
||||||
const [updating, setUpdating] = useAtom(albumsUpdatingAtom);
|
const [updating, setUpdating] = useAtom(albumsUpdatingAtom)
|
||||||
const setAlbums = useUpdateAtom(albumsAtom);
|
const setAlbums = useUpdateAtom(albumsAtom)
|
||||||
|
|
||||||
if (!server) {
|
if (!server) {
|
||||||
return () => Promise.resolve();
|
return () => Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
if (updating) {
|
if (updating) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
setUpdating(true);
|
setUpdating(true)
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server);
|
const client = new SubsonicApiClient(server)
|
||||||
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 });
|
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 })
|
||||||
|
|
||||||
setAlbums(
|
setAlbums(
|
||||||
response.data.albums.reduce((acc, next) => {
|
response.data.albums.reduce((acc, next) => {
|
||||||
const album = mapAlbumID3(next, client);
|
const album = mapAlbumID3(next, client)
|
||||||
acc[album.id] = album;
|
acc[album.id] = album
|
||||||
return acc;
|
return acc
|
||||||
}, {} as Record<string, Album>),
|
}, {} as Record<string, Album>),
|
||||||
);
|
)
|
||||||
setUpdating(false);
|
setUpdating(false)
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const albumAtomFamily = atomFamily((id: string) =>
|
export const albumAtomFamily = atomFamily((id: string) =>
|
||||||
atom<AlbumWithSongs | undefined>(async get => {
|
atom<AlbumWithSongs | undefined>(async get => {
|
||||||
const server = get(activeServerAtom);
|
const server = get(activeServerAtom)
|
||||||
if (!server) {
|
if (!server) {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server);
|
const client = new SubsonicApiClient(server)
|
||||||
const response = await client.getAlbum({ id });
|
const response = await client.getAlbum({ id })
|
||||||
return mapAlbumID3WithSongs(response.data.album, response.data.songs, client);
|
return mapAlbumID3WithSongs(response.data.album, response.data.songs, client)
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
export const albumArtAtomFamily = atomFamily((id: string) =>
|
export const albumArtAtomFamily = atomFamily((id: string) =>
|
||||||
atom<AlbumArt | undefined>(async get => {
|
atom<AlbumArt | undefined>(async get => {
|
||||||
const server = get(activeServerAtom);
|
const server = get(activeServerAtom)
|
||||||
if (!server) {
|
if (!server) {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const albums = get(albumsAtom);
|
const albums = get(albumsAtom)
|
||||||
const album = id in albums ? albums[id] : undefined;
|
const album = id in albums ? albums[id] : undefined
|
||||||
if (!album) {
|
if (!album) {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server);
|
const client = new SubsonicApiClient(server)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt }) : undefined,
|
uri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt }) : undefined,
|
||||||
thumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined,
|
thumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined,
|
||||||
};
|
}
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
export const artistInfoAtomFamily = atomFamily((id: string) =>
|
export const artistInfoAtomFamily = atomFamily((id: string) =>
|
||||||
atom<ArtistInfo | undefined>(async get => {
|
atom<ArtistInfo | undefined>(async get => {
|
||||||
const server = get(activeServerAtom);
|
const server = get(activeServerAtom)
|
||||||
if (!server) {
|
if (!server) {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server);
|
const client = new SubsonicApiClient(server)
|
||||||
const [artistResponse, artistInfoResponse] = await Promise.all([
|
const [artistResponse, artistInfoResponse] = await Promise.all([
|
||||||
client.getArtist({ id }),
|
client.getArtist({ id }),
|
||||||
client.getArtistInfo2({ 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) =>
|
export const artistArtAtomFamily = atomFamily((id: string) =>
|
||||||
atom<ArtistArt | undefined>(async get => {
|
atom<ArtistArt | undefined>(async get => {
|
||||||
const artistInfo = get(artistInfoAtomFamily(id));
|
const artistInfo = get(artistInfoAtomFamily(id))
|
||||||
if (!artistInfo) {
|
if (!artistInfo) {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const coverArtUris = artistInfo.albums
|
const coverArtUris = artistInfo.albums
|
||||||
.filter(a => a.coverArtThumbUri !== undefined)
|
.filter(a => a.coverArtThumbUri !== undefined)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (b.year && a.year) {
|
if (b.year && a.year) {
|
||||||
return b.year - a.year;
|
return b.year - a.year
|
||||||
} else {
|
} 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 {
|
return {
|
||||||
coverArtUris,
|
coverArtUris,
|
||||||
uri: artistInfo.mediumImageUrl,
|
uri: artistInfo.mediumImageUrl,
|
||||||
};
|
}
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
function mapArtistInfo(
|
function mapArtistInfo(
|
||||||
artistResponse: GetArtistResponse,
|
artistResponse: GetArtistResponse,
|
||||||
artistInfo: ArtistInfo2Element,
|
artistInfo: ArtistInfo2Element,
|
||||||
client: SubsonicApiClient,
|
client: SubsonicApiClient,
|
||||||
): ArtistInfo {
|
): ArtistInfo {
|
||||||
const info = { ...artistInfo } as any;
|
const info = { ...artistInfo } as any
|
||||||
delete info.similarArtists;
|
delete info.similarArtists
|
||||||
|
|
||||||
const { artist, albums } = artistResponse;
|
const { artist, albums } = artistResponse
|
||||||
|
|
||||||
const mappedAlbums = albums.map(a => mapAlbumID3(a, client));
|
const mappedAlbums = albums.map(a => mapAlbumID3(a, client))
|
||||||
const coverArtUris = mappedAlbums
|
const coverArtUris = mappedAlbums
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.year && b.year) {
|
if (a.year && b.year) {
|
||||||
return a.year - b.year;
|
return a.year - b.year
|
||||||
} else {
|
} else {
|
||||||
return a.name.localeCompare(b.name) - 9000;
|
return a.name.localeCompare(b.name) - 9000
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map(a => a.coverArtThumbUri);
|
.map(a => a.coverArtThumbUri)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...artist,
|
...artist,
|
||||||
...info,
|
...info,
|
||||||
albums: mappedAlbums,
|
albums: mappedAlbums,
|
||||||
coverArtUris,
|
coverArtUris,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
|
function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
|
||||||
@ -180,7 +180,7 @@ function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
|
|||||||
...album,
|
...album,
|
||||||
coverArtUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt }) : undefined,
|
coverArtUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt }) : undefined,
|
||||||
coverArtThumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined,
|
coverArtThumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
|
function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
|
||||||
@ -189,7 +189,7 @@ function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
|
|||||||
streamUri: client.streamUri({ id: child.id }),
|
streamUri: client.streamUri({ id: child.id }),
|
||||||
coverArtUri: child.coverArt ? client.getCoverArtUri({ id: child.coverArt }) : undefined,
|
coverArtUri: child.coverArt ? client.getCoverArtUri({ id: child.coverArt }) : undefined,
|
||||||
coverArtThumbUri: child.coverArt ? client.getCoverArtUri({ id: child.coverArt, size: '256' }) : undefined,
|
coverArtThumbUri: child.coverArt ? client.getCoverArtUri({ id: child.coverArt, size: '256' }) : undefined,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapAlbumID3WithSongs(
|
function mapAlbumID3WithSongs(
|
||||||
@ -200,5 +200,5 @@ function mapAlbumID3WithSongs(
|
|||||||
return {
|
return {
|
||||||
...mapAlbumID3(album, client),
|
...mapAlbumID3(album, client),
|
||||||
songs: songs.map(s => mapChildToSong(s, client)),
|
songs: songs.map(s => mapChildToSong(s, client)),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai'
|
||||||
import { AppSettings } from '../models/settings';
|
import { AppSettings } from '../models/settings'
|
||||||
import atomWithAsyncStorage from '../storage/atomWithAsyncStorage';
|
import atomWithAsyncStorage from '../storage/atomWithAsyncStorage'
|
||||||
|
|
||||||
export const appSettingsAtom = atomWithAsyncStorage<AppSettings>('@appSettings', {
|
export const appSettingsAtom = atomWithAsyncStorage<AppSettings>('@appSettings', {
|
||||||
servers: [],
|
servers: [],
|
||||||
});
|
})
|
||||||
|
|
||||||
export const activeServerAtom = atom(get => {
|
export const activeServerAtom = atom(get => {
|
||||||
const appSettings = get(appSettingsAtom);
|
const appSettings = get(appSettingsAtom)
|
||||||
return appSettings.servers.find(x => x.id === appSettings.activeServer);
|
return appSettings.servers.find(x => x.id === appSettings.activeServer)
|
||||||
});
|
})
|
||||||
|
|||||||
@ -1,37 +1,37 @@
|
|||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai'
|
||||||
import { State, Track } from 'react-native-track-player';
|
import { State, Track } from 'react-native-track-player'
|
||||||
import equal from 'fast-deep-equal';
|
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>(
|
export const currentTrackAtom = atom<OptionalTrack, OptionalTrack>(
|
||||||
get => get(currentTrack),
|
get => get(currentTrack),
|
||||||
(get, set, value) => {
|
(get, set, value) => {
|
||||||
if (!equal(get(currentTrack), 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>(
|
export const currentQueueNameAtom = atom<OptionalString, OptionalString>(
|
||||||
get => get(currentQueueName),
|
get => get(currentQueueName),
|
||||||
(get, set, value) => {
|
(get, set, value) => {
|
||||||
if (get(currentQueueName) !== 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>(
|
export const playerStateAtom = atom<State, State>(
|
||||||
get => get(playerState),
|
get => get(playerState),
|
||||||
(get, set, value) => {
|
(get, set, value) => {
|
||||||
if (get(playerState) !== value) {
|
if (get(playerState) !== value) {
|
||||||
set(playerState, value);
|
set(playerState, value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|||||||
@ -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> {
|
export async function getItem(key: string): Promise<any | null> {
|
||||||
try {
|
try {
|
||||||
const item = await AsyncStorage.getItem(key);
|
const item = await AsyncStorage.getItem(key)
|
||||||
return item ? JSON.parse(item) : null;
|
return item ? JSON.parse(item) : null
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`getItem error (key: ${key})`, e);
|
console.error(`getItem error (key: ${key})`, e)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function multiGet(keys: string[]): Promise<[string, any | null][]> {
|
export async function multiGet(keys: string[]): Promise<[string, any | null][]> {
|
||||||
try {
|
try {
|
||||||
const items = await AsyncStorage.multiGet(keys);
|
const items = await AsyncStorage.multiGet(keys)
|
||||||
return items.map(x => [x[0], x[1] ? JSON.parse(x[1]) : null]);
|
return items.map(x => [x[0], x[1] ? JSON.parse(x[1]) : null])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('multiGet error', e);
|
console.error('multiGet error', e)
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setItem(key: string, item: any): Promise<void> {
|
export async function setItem(key: string, item: any): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem(key, JSON.stringify(item));
|
await AsyncStorage.setItem(key, JSON.stringify(item))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`setItem error (key: ${key})`, e);
|
console.error(`setItem error (key: ${key})`, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function multiSet(items: string[][]): Promise<void> {
|
export async function multiSet(items: string[][]): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.multiSet(items.map(x => [x[0], JSON.stringify(x[1])]));
|
await AsyncStorage.multiSet(items.map(x => [x[0], JSON.stringify(x[1])]))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('multiSet error', e);
|
console.error('multiSet error', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllKeys(): Promise<string[]> {
|
export async function getAllKeys(): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
return await AsyncStorage.getAllKeys();
|
return await AsyncStorage.getAllKeys()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('getAllKeys error', e);
|
console.error('getAllKeys error', e)
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function multiRemove(keys: string[]): Promise<void> {
|
export async function multiRemove(keys: string[]): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.multiRemove(keys);
|
await AsyncStorage.multiRemove(keys)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('multiRemove error', e);
|
console.error('multiRemove error', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils'
|
||||||
import { getItem, setItem } from './asyncstorage';
|
import { getItem, setItem } from './asyncstorage'
|
||||||
|
|
||||||
export default <T>(key: string, defaultValue: T) => {
|
export default <T>(key: string, defaultValue: T) => {
|
||||||
return atomWithStorage<T>(key, defaultValue, {
|
return atomWithStorage<T>(key, defaultValue, {
|
||||||
getItem: async () => (await getItem(key)) || defaultValue,
|
getItem: async () => (await getItem(key)) || defaultValue,
|
||||||
setItem: setItem,
|
setItem: setItem,
|
||||||
delayInit: true,
|
delayInit: true,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,26 +1,26 @@
|
|||||||
import { DownloadedSong } from '../models/music';
|
import { DownloadedSong } from '../models/music'
|
||||||
import { getItem, multiGet, multiSet } from './asyncstorage';
|
import { getItem, multiGet, multiSet } from './asyncstorage'
|
||||||
|
|
||||||
const key = {
|
const key = {
|
||||||
downloadedSongKeys: '@downloadedSongKeys',
|
downloadedSongKeys: '@downloadedSongKeys',
|
||||||
downloadedAlbumKeys: '@downloadedAlbumKeys',
|
downloadedAlbumKeys: '@downloadedAlbumKeys',
|
||||||
downloadedArtistKeys: '@downloadedArtistKeys',
|
downloadedArtistKeys: '@downloadedArtistKeys',
|
||||||
downloadedPlaylistKeys: '@downloadedPlaylistKeys',
|
downloadedPlaylistKeys: '@downloadedPlaylistKeys',
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function getDownloadedSongs(): Promise<DownloadedSong[]> {
|
export async function getDownloadedSongs(): Promise<DownloadedSong[]> {
|
||||||
const keysItem = await getItem(key.downloadedSongKeys);
|
const keysItem = await getItem(key.downloadedSongKeys)
|
||||||
const keys: string[] = keysItem ? JSON.parse(keysItem) : [];
|
const keys: string[] = keysItem ? JSON.parse(keysItem) : []
|
||||||
|
|
||||||
const items = await multiGet(keys);
|
const items = await multiGet(keys)
|
||||||
return items.map(x => {
|
return items.map(x => {
|
||||||
const parsed = JSON.parse(x[1] as string);
|
const parsed = JSON.parse(x[1] as string)
|
||||||
return {
|
return {
|
||||||
id: x[0],
|
id: x[0],
|
||||||
type: 'song',
|
type: 'song',
|
||||||
...parsed,
|
...parsed,
|
||||||
};
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setDownloadedSongs(items: DownloadedSong[]): Promise<void> {
|
export async function setDownloadedSongs(items: DownloadedSong[]): Promise<void> {
|
||||||
@ -34,5 +34,5 @@ export async function setDownloadedSongs(items: DownloadedSong[]): Promise<void>
|
|||||||
artist: x.artist,
|
artist: x.artist,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
]);
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,4 +10,4 @@ export default {
|
|||||||
},
|
},
|
||||||
accent: '#b134db',
|
accent: '#b134db',
|
||||||
accentLow: '#511c63',
|
accentLow: '#511c63',
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,62 +1,62 @@
|
|||||||
import { TextStyle } from 'react-native';
|
import { TextStyle } from 'react-native'
|
||||||
import colors from './colors';
|
import colors from './colors'
|
||||||
|
|
||||||
const fontRegular = 'Metropolis-Regular';
|
const fontRegular = 'Metropolis-Regular'
|
||||||
const fontSemiBold = 'Metropolis-SemiBold';
|
const fontSemiBold = 'Metropolis-SemiBold'
|
||||||
const fontBold = 'Metropolis-Bold';
|
const fontBold = 'Metropolis-Bold'
|
||||||
|
|
||||||
const paragraph: TextStyle = {
|
const paragraph: TextStyle = {
|
||||||
fontFamily: fontRegular,
|
fontFamily: fontRegular,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: colors.text.primary,
|
color: colors.text.primary,
|
||||||
};
|
}
|
||||||
|
|
||||||
const header: TextStyle = {
|
const header: TextStyle = {
|
||||||
...paragraph,
|
...paragraph,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontFamily: fontSemiBold,
|
fontFamily: fontSemiBold,
|
||||||
};
|
}
|
||||||
|
|
||||||
const title: TextStyle = {
|
const title: TextStyle = {
|
||||||
...paragraph,
|
...paragraph,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontFamily: fontBold,
|
fontFamily: fontBold,
|
||||||
};
|
}
|
||||||
|
|
||||||
const itemTitle: TextStyle = {
|
const itemTitle: TextStyle = {
|
||||||
...paragraph,
|
...paragraph,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: fontSemiBold,
|
fontFamily: fontSemiBold,
|
||||||
};
|
}
|
||||||
|
|
||||||
const itemSubtitle: TextStyle = {
|
const itemSubtitle: TextStyle = {
|
||||||
...paragraph,
|
...paragraph,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: colors.text.secondary,
|
color: colors.text.secondary,
|
||||||
};
|
}
|
||||||
|
|
||||||
const songListTitle: TextStyle = {
|
const songListTitle: TextStyle = {
|
||||||
...paragraph,
|
...paragraph,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontFamily: fontSemiBold,
|
fontFamily: fontSemiBold,
|
||||||
};
|
}
|
||||||
|
|
||||||
const songListSubtitle: TextStyle = {
|
const songListSubtitle: TextStyle = {
|
||||||
...paragraph,
|
...paragraph,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: colors.text.secondary,
|
color: colors.text.secondary,
|
||||||
};
|
}
|
||||||
|
|
||||||
const xsmall: TextStyle = {
|
const xsmall: TextStyle = {
|
||||||
...paragraph,
|
...paragraph,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
};
|
}
|
||||||
|
|
||||||
const button: TextStyle = {
|
const button: TextStyle = {
|
||||||
...paragraph,
|
...paragraph,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontFamily: fontBold,
|
fontFamily: fontBold,
|
||||||
};
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
paragraph,
|
paragraph,
|
||||||
@ -68,4 +68,4 @@ export default {
|
|||||||
songListSubtitle,
|
songListSubtitle,
|
||||||
xsmall,
|
xsmall,
|
||||||
button,
|
button,
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { DOMParser } from 'xmldom';
|
import { DOMParser } from 'xmldom'
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs'
|
||||||
import {
|
import {
|
||||||
GetAlbumList2Params,
|
GetAlbumList2Params,
|
||||||
GetAlbumListParams,
|
GetAlbumListParams,
|
||||||
@ -11,7 +11,7 @@ import {
|
|||||||
GetIndexesParams,
|
GetIndexesParams,
|
||||||
GetMusicDirectoryParams,
|
GetMusicDirectoryParams,
|
||||||
StreamParams,
|
StreamParams,
|
||||||
} from './params';
|
} from './params'
|
||||||
import {
|
import {
|
||||||
GetAlbumList2Response,
|
GetAlbumList2Response,
|
||||||
GetAlbumListResponse,
|
GetAlbumListResponse,
|
||||||
@ -23,127 +23,127 @@ import {
|
|||||||
GetIndexesResponse,
|
GetIndexesResponse,
|
||||||
GetMusicDirectoryResponse,
|
GetMusicDirectoryResponse,
|
||||||
SubsonicResponse,
|
SubsonicResponse,
|
||||||
} from './responses';
|
} from './responses'
|
||||||
import { Server } from '../models/settings';
|
import { Server } from '../models/settings'
|
||||||
import paths from '../paths';
|
import paths from '../paths'
|
||||||
|
|
||||||
export class SubsonicApiError extends Error {
|
export class SubsonicApiError extends Error {
|
||||||
method: string;
|
method: string
|
||||||
code: string;
|
code: string
|
||||||
|
|
||||||
constructor(method: string, xml: Document) {
|
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.name = method
|
||||||
this.method = method;
|
this.method = method
|
||||||
this.code = errorElement.getAttribute('code') as string;
|
this.code = errorElement.getAttribute('code') as string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueuePromise = () => Promise<any>;
|
type QueuePromise = () => Promise<any>
|
||||||
|
|
||||||
class Queue {
|
class Queue {
|
||||||
maxSimultaneously: number;
|
maxSimultaneously: number
|
||||||
|
|
||||||
private active = 0;
|
private active = 0
|
||||||
private queue: QueuePromise[] = [];
|
private queue: QueuePromise[] = []
|
||||||
|
|
||||||
constructor(maxSimultaneously = 1) {
|
constructor(maxSimultaneously = 1) {
|
||||||
this.maxSimultaneously = maxSimultaneously;
|
this.maxSimultaneously = maxSimultaneously
|
||||||
}
|
}
|
||||||
|
|
||||||
async enqueue(func: QueuePromise) {
|
async enqueue(func: QueuePromise) {
|
||||||
if (++this.active > this.maxSimultaneously) {
|
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 {
|
try {
|
||||||
return await func();
|
return await func()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err
|
||||||
} finally {
|
} finally {
|
||||||
this.active--;
|
this.active--
|
||||||
if (this.queue.length) {
|
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 {
|
export class SubsonicApiClient {
|
||||||
address: string;
|
address: string
|
||||||
username: string;
|
username: string
|
||||||
|
|
||||||
private params: URLSearchParams;
|
private params: URLSearchParams
|
||||||
|
|
||||||
constructor(server: Server) {
|
constructor(server: Server) {
|
||||||
this.address = server.address;
|
this.address = server.address
|
||||||
this.username = server.username;
|
this.username = server.username
|
||||||
|
|
||||||
this.params = new URLSearchParams();
|
this.params = new URLSearchParams()
|
||||||
this.params.append('u', server.username);
|
this.params.append('u', server.username)
|
||||||
this.params.append('t', server.token);
|
this.params.append('t', server.token)
|
||||||
this.params.append('s', server.salt);
|
this.params.append('s', server.salt)
|
||||||
this.params.append('v', '1.15.0');
|
this.params.append('v', '1.15.0')
|
||||||
this.params.append('c', 'subsonify-cool-unique-app-string');
|
this.params.append('c', 'subsonify-cool-unique-app-string')
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildUrl(method: string, params?: { [key: string]: any }): string {
|
private buildUrl(method: string, params?: { [key: string]: any }): string {
|
||||||
let query = this.params.toString();
|
let query = this.params.toString()
|
||||||
if (params) {
|
if (params) {
|
||||||
const urlParams = this.obj2Params(params);
|
const urlParams = this.obj2Params(params)
|
||||||
if (urlParams) {
|
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);
|
// console.log(url);
|
||||||
return url;
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
private async apiDownload(method: string, path: string, params?: { [key: string]: any }): Promise<string> {
|
private async apiDownload(method: string, path: string, params?: { [key: string]: any }): Promise<string> {
|
||||||
const download = RNFS.downloadFile({
|
const download = RNFS.downloadFile({
|
||||||
fromUrl: this.buildUrl(method, params),
|
fromUrl: this.buildUrl(method, params),
|
||||||
toFile: path,
|
toFile: path,
|
||||||
}).promise;
|
}).promise
|
||||||
|
|
||||||
await downloadQueue.enqueue(() => download);
|
await downloadQueue.enqueue(() => download)
|
||||||
await downloadQueue.enqueue(() => new Promise(resolve => setTimeout(resolve, 100)));
|
await downloadQueue.enqueue(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||||
|
|
||||||
return path;
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
private async apiGetXml(method: string, params?: { [key: string]: any }): Promise<Document> {
|
private async apiGetXml(method: string, params?: { [key: string]: any }): Promise<Document> {
|
||||||
const response = await fetch(this.buildUrl(method, params));
|
const response = await fetch(this.buildUrl(method, params))
|
||||||
const text = await response.text();
|
const text = await response.text()
|
||||||
|
|
||||||
// console.log(text);
|
// console.log(text);
|
||||||
|
|
||||||
const xml = new DOMParser().parseFromString(text);
|
const xml = new DOMParser().parseFromString(text)
|
||||||
if (xml.documentElement.getAttribute('status') !== 'ok') {
|
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 {
|
private obj2Params(obj: { [key: string]: any }): URLSearchParams | undefined {
|
||||||
const keys = Object.keys(obj);
|
const keys = Object.keys(obj)
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams()
|
||||||
for (const key of keys) {
|
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>> {
|
async ping(): Promise<SubsonicResponse<null>> {
|
||||||
const xml = await this.apiGetXml('ping');
|
const xml = await this.apiGetXml('ping')
|
||||||
return new SubsonicResponse<null>(xml, null);
|
return new SubsonicResponse<null>(xml, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -160,38 +160,38 @@ export class SubsonicApiClient {
|
|||||||
//
|
//
|
||||||
|
|
||||||
async getArtists(): Promise<SubsonicResponse<GetArtistsResponse>> {
|
async getArtists(): Promise<SubsonicResponse<GetArtistsResponse>> {
|
||||||
const xml = await this.apiGetXml('getArtists');
|
const xml = await this.apiGetXml('getArtists')
|
||||||
return new SubsonicResponse<GetArtistsResponse>(xml, new GetArtistsResponse(xml));
|
return new SubsonicResponse<GetArtistsResponse>(xml, new GetArtistsResponse(xml))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getIndexes(params?: GetIndexesParams): Promise<SubsonicResponse<GetIndexesResponse>> {
|
async getIndexes(params?: GetIndexesParams): Promise<SubsonicResponse<GetIndexesResponse>> {
|
||||||
const xml = await this.apiGetXml('getIndexes', params);
|
const xml = await this.apiGetXml('getIndexes', params)
|
||||||
return new SubsonicResponse<GetIndexesResponse>(xml, new GetIndexesResponse(xml));
|
return new SubsonicResponse<GetIndexesResponse>(xml, new GetIndexesResponse(xml))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMusicDirectory(params: GetMusicDirectoryParams): Promise<SubsonicResponse<GetMusicDirectoryResponse>> {
|
async getMusicDirectory(params: GetMusicDirectoryParams): Promise<SubsonicResponse<GetMusicDirectoryResponse>> {
|
||||||
const xml = await this.apiGetXml('getMusicDirectory', params);
|
const xml = await this.apiGetXml('getMusicDirectory', params)
|
||||||
return new SubsonicResponse<GetMusicDirectoryResponse>(xml, new GetMusicDirectoryResponse(xml));
|
return new SubsonicResponse<GetMusicDirectoryResponse>(xml, new GetMusicDirectoryResponse(xml))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAlbum(params: GetAlbumParams): Promise<SubsonicResponse<GetAlbumResponse>> {
|
async getAlbum(params: GetAlbumParams): Promise<SubsonicResponse<GetAlbumResponse>> {
|
||||||
const xml = await this.apiGetXml('getAlbum', params);
|
const xml = await this.apiGetXml('getAlbum', params)
|
||||||
return new SubsonicResponse<GetAlbumResponse>(xml, new GetAlbumResponse(xml));
|
return new SubsonicResponse<GetAlbumResponse>(xml, new GetAlbumResponse(xml))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getArtistInfo(params: GetArtistInfoParams): Promise<SubsonicResponse<GetArtistInfoResponse>> {
|
async getArtistInfo(params: GetArtistInfoParams): Promise<SubsonicResponse<GetArtistInfoResponse>> {
|
||||||
const xml = await this.apiGetXml('getArtistInfo', params);
|
const xml = await this.apiGetXml('getArtistInfo', params)
|
||||||
return new SubsonicResponse<GetArtistInfoResponse>(xml, new GetArtistInfoResponse(xml));
|
return new SubsonicResponse<GetArtistInfoResponse>(xml, new GetArtistInfoResponse(xml))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getArtistInfo2(params: GetArtistInfo2Params): Promise<SubsonicResponse<GetArtistInfo2Response>> {
|
async getArtistInfo2(params: GetArtistInfo2Params): Promise<SubsonicResponse<GetArtistInfo2Response>> {
|
||||||
const xml = await this.apiGetXml('getArtistInfo2', params);
|
const xml = await this.apiGetXml('getArtistInfo2', params)
|
||||||
return new SubsonicResponse<GetArtistInfo2Response>(xml, new GetArtistInfo2Response(xml));
|
return new SubsonicResponse<GetArtistInfo2Response>(xml, new GetArtistInfo2Response(xml))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getArtist(params: GetArtistParams): Promise<SubsonicResponse<GetArtistResponse>> {
|
async getArtist(params: GetArtistParams): Promise<SubsonicResponse<GetArtistResponse>> {
|
||||||
const xml = await this.apiGetXml('getArtist', params);
|
const xml = await this.apiGetXml('getArtist', params)
|
||||||
return new SubsonicResponse<GetArtistResponse>(xml, new GetArtistResponse(xml));
|
return new SubsonicResponse<GetArtistResponse>(xml, new GetArtistResponse(xml))
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -199,13 +199,13 @@ export class SubsonicApiClient {
|
|||||||
//
|
//
|
||||||
|
|
||||||
async getAlbumList(params: GetAlbumListParams): Promise<SubsonicResponse<GetAlbumListResponse>> {
|
async getAlbumList(params: GetAlbumListParams): Promise<SubsonicResponse<GetAlbumListResponse>> {
|
||||||
const xml = await this.apiGetXml('getAlbumList', params);
|
const xml = await this.apiGetXml('getAlbumList', params)
|
||||||
return new SubsonicResponse<GetAlbumListResponse>(xml, new GetAlbumListResponse(xml));
|
return new SubsonicResponse<GetAlbumListResponse>(xml, new GetAlbumListResponse(xml))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAlbumList2(params: GetAlbumList2Params): Promise<SubsonicResponse<GetAlbumList2Response>> {
|
async getAlbumList2(params: GetAlbumList2Params): Promise<SubsonicResponse<GetAlbumList2Response>> {
|
||||||
const xml = await this.apiGetXml('getAlbumList2', params);
|
const xml = await this.apiGetXml('getAlbumList2', params)
|
||||||
return new SubsonicResponse<GetAlbumList2Response>(xml, new GetAlbumList2Response(xml));
|
return new SubsonicResponse<GetAlbumList2Response>(xml, new GetAlbumList2Response(xml))
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -213,15 +213,15 @@ export class SubsonicApiClient {
|
|||||||
//
|
//
|
||||||
|
|
||||||
async getCoverArt(params: GetCoverArtParams): Promise<string> {
|
async getCoverArt(params: GetCoverArtParams): Promise<string> {
|
||||||
const path = `${paths.songCache}/${params.id}`;
|
const path = `${paths.songCache}/${params.id}`
|
||||||
return await this.apiDownload('getCoverArt', path, params);
|
return await this.apiDownload('getCoverArt', path, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCoverArtUri(params: GetCoverArtParams): string {
|
getCoverArtUri(params: GetCoverArtParams): string {
|
||||||
return this.buildUrl('getCoverArt', params);
|
return this.buildUrl('getCoverArt', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
streamUri(params: StreamParams): string {
|
streamUri(params: StreamParams): string {
|
||||||
return this.buildUrl('stream', params);
|
return this.buildUrl('stream', params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,237 +1,237 @@
|
|||||||
function requiredString(e: Element, name: string): string {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
export class BaseArtistElement {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
starred?: Date;
|
starred?: Date
|
||||||
|
|
||||||
constructor(e: Element) {
|
constructor(e: Element) {
|
||||||
this.id = requiredString(e, 'id');
|
this.id = requiredString(e, 'id')
|
||||||
this.name = requiredString(e, 'name');
|
this.name = requiredString(e, 'name')
|
||||||
this.starred = optionalDate(e, 'starred');
|
this.starred = optionalDate(e, 'starred')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ArtistID3Element extends BaseArtistElement {
|
export class ArtistID3Element extends BaseArtistElement {
|
||||||
coverArt?: string;
|
coverArt?: string
|
||||||
albumCount?: number;
|
albumCount?: number
|
||||||
|
|
||||||
constructor(e: Element) {
|
constructor(e: Element) {
|
||||||
super(e);
|
super(e)
|
||||||
this.coverArt = optionalString(e, 'coverArt');
|
this.coverArt = optionalString(e, 'coverArt')
|
||||||
this.albumCount = optionalInt(e, 'albumCount');
|
this.albumCount = optionalInt(e, 'albumCount')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ArtistElement extends BaseArtistElement {
|
export class ArtistElement extends BaseArtistElement {
|
||||||
userRating?: number;
|
userRating?: number
|
||||||
averageRating?: number;
|
averageRating?: number
|
||||||
|
|
||||||
constructor(e: Element) {
|
constructor(e: Element) {
|
||||||
super(e);
|
super(e)
|
||||||
this.userRating = optionalInt(e, 'userRating');
|
this.userRating = optionalInt(e, 'userRating')
|
||||||
this.averageRating = optionalFloat(e, 'averageRating');
|
this.averageRating = optionalFloat(e, 'averageRating')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BaseArtistInfoElement<T> {
|
export class BaseArtistInfoElement<T> {
|
||||||
similarArtists: T[] = [];
|
similarArtists: T[] = []
|
||||||
biography?: string;
|
biography?: string
|
||||||
musicBrainzId?: string;
|
musicBrainzId?: string
|
||||||
lastFmUrl?: string;
|
lastFmUrl?: string
|
||||||
smallImageUrl?: string;
|
smallImageUrl?: string
|
||||||
mediumImageUrl?: string;
|
mediumImageUrl?: string
|
||||||
largeImageUrl?: string;
|
largeImageUrl?: string
|
||||||
|
|
||||||
constructor(e: Element, artistType: new (e: Element) => T) {
|
constructor(e: Element, artistType: new (e: Element) => T) {
|
||||||
if (e.getElementsByTagName('biography').length > 0) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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++) {
|
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> {
|
export class ArtistInfoElement extends BaseArtistInfoElement<ArtistElement> {
|
||||||
constructor(e: Element) {
|
constructor(e: Element) {
|
||||||
super(e, ArtistElement);
|
super(e, ArtistElement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class ArtistInfo2Element extends BaseArtistInfoElement<ArtistID3Element> {
|
export class ArtistInfo2Element extends BaseArtistInfoElement<ArtistID3Element> {
|
||||||
constructor(e: Element) {
|
constructor(e: Element) {
|
||||||
super(e, ArtistID3Element);
|
super(e, ArtistID3Element)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DirectoryElement {
|
export class DirectoryElement {
|
||||||
id: string;
|
id: string
|
||||||
parent?: string;
|
parent?: string
|
||||||
name: string;
|
name: string
|
||||||
starred?: Date;
|
starred?: Date
|
||||||
userRating?: number;
|
userRating?: number
|
||||||
averageRating?: number;
|
averageRating?: number
|
||||||
playCount?: number;
|
playCount?: number
|
||||||
|
|
||||||
constructor(e: Element) {
|
constructor(e: Element) {
|
||||||
this.id = requiredString(e, 'id');
|
this.id = requiredString(e, 'id')
|
||||||
this.parent = optionalString(e, 'parent');
|
this.parent = optionalString(e, 'parent')
|
||||||
this.name = requiredString(e, 'name');
|
this.name = requiredString(e, 'name')
|
||||||
this.starred = optionalDate(e, 'starred');
|
this.starred = optionalDate(e, 'starred')
|
||||||
this.userRating = optionalInt(e, 'userRating');
|
this.userRating = optionalInt(e, 'userRating')
|
||||||
this.averageRating = optionalFloat(e, 'averageRating');
|
this.averageRating = optionalFloat(e, 'averageRating')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChildElement {
|
export class ChildElement {
|
||||||
id: string;
|
id: string
|
||||||
parent?: string;
|
parent?: string
|
||||||
isDir: boolean;
|
isDir: boolean
|
||||||
title: string;
|
title: string
|
||||||
album?: string;
|
album?: string
|
||||||
artist?: string;
|
artist?: string
|
||||||
track?: number;
|
track?: number
|
||||||
year?: number;
|
year?: number
|
||||||
genre?: string;
|
genre?: string
|
||||||
coverArt?: string;
|
coverArt?: string
|
||||||
size?: number;
|
size?: number
|
||||||
contentType?: string;
|
contentType?: string
|
||||||
suffix?: string;
|
suffix?: string
|
||||||
transcodedContentType?: string;
|
transcodedContentType?: string
|
||||||
transcodedSuffix?: string;
|
transcodedSuffix?: string
|
||||||
duration?: number;
|
duration?: number
|
||||||
bitRate?: number;
|
bitRate?: number
|
||||||
path?: string;
|
path?: string
|
||||||
isVideo?: boolean;
|
isVideo?: boolean
|
||||||
userRating?: number;
|
userRating?: number
|
||||||
averageRating?: number;
|
averageRating?: number
|
||||||
playCount?: number;
|
playCount?: number
|
||||||
discNumber?: number;
|
discNumber?: number
|
||||||
created?: Date;
|
created?: Date
|
||||||
starred?: Date;
|
starred?: Date
|
||||||
albumId?: string;
|
albumId?: string
|
||||||
artistId?: string;
|
artistId?: string
|
||||||
type?: string;
|
type?: string
|
||||||
bookmarkPosition?: number;
|
bookmarkPosition?: number
|
||||||
originalWidth?: number;
|
originalWidth?: number
|
||||||
originalHeight?: number;
|
originalHeight?: number
|
||||||
|
|
||||||
constructor(e: Element) {
|
constructor(e: Element) {
|
||||||
this.id = requiredString(e, 'id');
|
this.id = requiredString(e, 'id')
|
||||||
this.parent = optionalString(e, 'parent');
|
this.parent = optionalString(e, 'parent')
|
||||||
this.isDir = requiredBoolean(e, 'isDir');
|
this.isDir = requiredBoolean(e, 'isDir')
|
||||||
this.title = requiredString(e, 'title');
|
this.title = requiredString(e, 'title')
|
||||||
this.album = optionalString(e, 'album');
|
this.album = optionalString(e, 'album')
|
||||||
this.artist = optionalString(e, 'artist');
|
this.artist = optionalString(e, 'artist')
|
||||||
this.track = optionalInt(e, 'track');
|
this.track = optionalInt(e, 'track')
|
||||||
this.year = optionalInt(e, 'year');
|
this.year = optionalInt(e, 'year')
|
||||||
this.genre = optionalString(e, 'genre');
|
this.genre = optionalString(e, 'genre')
|
||||||
this.coverArt = optionalString(e, 'coverArt');
|
this.coverArt = optionalString(e, 'coverArt')
|
||||||
this.size = optionalInt(e, 'size');
|
this.size = optionalInt(e, 'size')
|
||||||
this.contentType = optionalString(e, 'contentType');
|
this.contentType = optionalString(e, 'contentType')
|
||||||
this.suffix = optionalString(e, 'suffix');
|
this.suffix = optionalString(e, 'suffix')
|
||||||
this.transcodedContentType = optionalString(e, 'transcodedContentType');
|
this.transcodedContentType = optionalString(e, 'transcodedContentType')
|
||||||
this.transcodedSuffix = optionalString(e, 'transcodedSuffix');
|
this.transcodedSuffix = optionalString(e, 'transcodedSuffix')
|
||||||
this.duration = optionalInt(e, 'duration');
|
this.duration = optionalInt(e, 'duration')
|
||||||
this.bitRate = optionalInt(e, 'bitRate');
|
this.bitRate = optionalInt(e, 'bitRate')
|
||||||
this.path = optionalString(e, 'path');
|
this.path = optionalString(e, 'path')
|
||||||
this.isVideo = optionalBoolean(e, 'isVideo');
|
this.isVideo = optionalBoolean(e, 'isVideo')
|
||||||
this.userRating = optionalInt(e, 'userRating');
|
this.userRating = optionalInt(e, 'userRating')
|
||||||
this.averageRating = optionalFloat(e, 'averageRating');
|
this.averageRating = optionalFloat(e, 'averageRating')
|
||||||
this.playCount = optionalInt(e, 'playCount');
|
this.playCount = optionalInt(e, 'playCount')
|
||||||
this.discNumber = optionalInt(e, 'discNumber');
|
this.discNumber = optionalInt(e, 'discNumber')
|
||||||
this.created = optionalDate(e, 'created');
|
this.created = optionalDate(e, 'created')
|
||||||
this.starred = optionalDate(e, 'starred');
|
this.starred = optionalDate(e, 'starred')
|
||||||
this.albumId = optionalString(e, 'albumId');
|
this.albumId = optionalString(e, 'albumId')
|
||||||
this.artistId = optionalString(e, 'artistId');
|
this.artistId = optionalString(e, 'artistId')
|
||||||
this.type = optionalString(e, 'type');
|
this.type = optionalString(e, 'type')
|
||||||
this.bookmarkPosition = optionalInt(e, 'bookmarkPosition');
|
this.bookmarkPosition = optionalInt(e, 'bookmarkPosition')
|
||||||
this.originalWidth = optionalInt(e, 'originalWidth');
|
this.originalWidth = optionalInt(e, 'originalWidth')
|
||||||
this.originalHeight = optionalInt(e, 'originalHeight');
|
this.originalHeight = optionalInt(e, 'originalHeight')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AlbumID3Element {
|
export class AlbumID3Element {
|
||||||
id: string;
|
id: string
|
||||||
name: string;
|
name: string
|
||||||
artist?: string;
|
artist?: string
|
||||||
artistId?: string;
|
artistId?: string
|
||||||
coverArt?: string;
|
coverArt?: string
|
||||||
songCount: number;
|
songCount: number
|
||||||
duration: number;
|
duration: number
|
||||||
playCount?: number;
|
playCount?: number
|
||||||
created: Date;
|
created: Date
|
||||||
starred?: Date;
|
starred?: Date
|
||||||
year?: number;
|
year?: number
|
||||||
genre?: string;
|
genre?: string
|
||||||
|
|
||||||
constructor(e: Element) {
|
constructor(e: Element) {
|
||||||
this.id = requiredString(e, 'id');
|
this.id = requiredString(e, 'id')
|
||||||
this.name = requiredString(e, 'name');
|
this.name = requiredString(e, 'name')
|
||||||
this.artist = optionalString(e, 'artist');
|
this.artist = optionalString(e, 'artist')
|
||||||
this.artistId = optionalString(e, 'artistId');
|
this.artistId = optionalString(e, 'artistId')
|
||||||
this.coverArt = optionalString(e, 'coverArt');
|
this.coverArt = optionalString(e, 'coverArt')
|
||||||
this.songCount = requiredInt(e, 'songCount');
|
this.songCount = requiredInt(e, 'songCount')
|
||||||
this.duration = requiredInt(e, 'duration');
|
this.duration = requiredInt(e, 'duration')
|
||||||
this.playCount = optionalInt(e, 'playCount');
|
this.playCount = optionalInt(e, 'playCount')
|
||||||
this.created = requiredDate(e, 'created');
|
this.created = requiredDate(e, 'created')
|
||||||
this.starred = optionalDate(e, 'starred');
|
this.starred = optionalDate(e, 'starred')
|
||||||
this.year = optionalInt(e, 'year');
|
this.year = optionalInt(e, 'year')
|
||||||
this.genre = optionalString(e, 'genre');
|
this.genre = optionalString(e, 'genre')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,29 +3,29 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
export type GetIndexesParams = {
|
export type GetIndexesParams = {
|
||||||
musicFolderId?: string;
|
musicFolderId?: string
|
||||||
ifModifiedSince?: number;
|
ifModifiedSince?: number
|
||||||
};
|
}
|
||||||
|
|
||||||
export type GetArtistInfoParams = {
|
export type GetArtistInfoParams = {
|
||||||
id: string;
|
id: string
|
||||||
count?: number;
|
count?: number
|
||||||
includeNotPresent?: boolean;
|
includeNotPresent?: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
export type GetArtistInfo2Params = GetArtistInfoParams;
|
export type GetArtistInfo2Params = GetArtistInfoParams
|
||||||
|
|
||||||
export type GetMusicDirectoryParams = {
|
export type GetMusicDirectoryParams = {
|
||||||
id: string;
|
id: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type GetAlbumParams = {
|
export type GetAlbumParams = {
|
||||||
id: string;
|
id: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type GetArtistParams = {
|
export type GetArtistParams = {
|
||||||
id: string;
|
id: string
|
||||||
};
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Album/song lists
|
// Album/song lists
|
||||||
@ -38,47 +38,47 @@ export type GetAlbumList2Type =
|
|||||||
| 'recent'
|
| 'recent'
|
||||||
| 'starred'
|
| 'starred'
|
||||||
| 'alphabeticalByName'
|
| 'alphabeticalByName'
|
||||||
| 'alphabeticalByArtist';
|
| 'alphabeticalByArtist'
|
||||||
export type GetAlbumListType = GetAlbumList2Type | ' highest';
|
export type GetAlbumListType = GetAlbumList2Type | ' highest'
|
||||||
|
|
||||||
export type GetAlbumList2TypeByYear = {
|
export type GetAlbumList2TypeByYear = {
|
||||||
type: 'byYear';
|
type: 'byYear'
|
||||||
fromYear: string;
|
fromYear: string
|
||||||
toYear: string;
|
toYear: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type GetAlbumList2TypeByGenre = {
|
export type GetAlbumList2TypeByGenre = {
|
||||||
type: 'byGenre';
|
type: 'byGenre'
|
||||||
genre: string;
|
genre: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type GetAlbumList2Params =
|
export type GetAlbumList2Params =
|
||||||
| {
|
| {
|
||||||
type: GetAlbumList2Type;
|
type: GetAlbumList2Type
|
||||||
size?: number;
|
size?: number
|
||||||
offset?: number;
|
offset?: number
|
||||||
fromYear?: string;
|
fromYear?: string
|
||||||
toYear?: string;
|
toYear?: string
|
||||||
genre?: string;
|
genre?: string
|
||||||
musicFolderId?: string;
|
musicFolderId?: string
|
||||||
}
|
}
|
||||||
| GetAlbumList2TypeByYear
|
| GetAlbumList2TypeByYear
|
||||||
| GetAlbumList2TypeByGenre;
|
| GetAlbumList2TypeByGenre
|
||||||
|
|
||||||
export type GetAlbumListParams = GetAlbumList2Params;
|
export type GetAlbumListParams = GetAlbumList2Params
|
||||||
|
|
||||||
//
|
//
|
||||||
// Media retrieval
|
// Media retrieval
|
||||||
//
|
//
|
||||||
|
|
||||||
export type GetCoverArtParams = {
|
export type GetCoverArtParams = {
|
||||||
id: string;
|
id: string
|
||||||
size?: string;
|
size?: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type StreamParams = {
|
export type StreamParams = {
|
||||||
id: string;
|
id: string
|
||||||
maxBitRate?: number;
|
maxBitRate?: number
|
||||||
format?: string;
|
format?: string
|
||||||
estimateContentLength?: boolean;
|
estimateContentLength?: boolean
|
||||||
};
|
}
|
||||||
|
|||||||
@ -6,19 +6,19 @@ import {
|
|||||||
ArtistInfoElement,
|
ArtistInfoElement,
|
||||||
ChildElement,
|
ChildElement,
|
||||||
DirectoryElement,
|
DirectoryElement,
|
||||||
} from './elements';
|
} from './elements'
|
||||||
|
|
||||||
export type ResponseStatus = 'ok' | 'failed';
|
export type ResponseStatus = 'ok' | 'failed'
|
||||||
|
|
||||||
export class SubsonicResponse<T> {
|
export class SubsonicResponse<T> {
|
||||||
status: ResponseStatus;
|
status: ResponseStatus
|
||||||
version: string;
|
version: string
|
||||||
data: T;
|
data: T
|
||||||
|
|
||||||
constructor(xml: Document, data: T) {
|
constructor(xml: Document, data: T) {
|
||||||
this.data = data;
|
this.data = data
|
||||||
this.status = xml.documentElement.getAttribute('status') as ResponseStatus;
|
this.status = xml.documentElement.getAttribute('status') as ResponseStatus
|
||||||
this.version = xml.documentElement.getAttribute('version') as string;
|
this.version = xml.documentElement.getAttribute('version') as string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,91 +27,91 @@ export class SubsonicResponse<T> {
|
|||||||
//
|
//
|
||||||
|
|
||||||
export class GetArtistsResponse {
|
export class GetArtistsResponse {
|
||||||
ignoredArticles: string;
|
ignoredArticles: string
|
||||||
artists: ArtistID3Element[] = [];
|
artists: ArtistID3Element[] = []
|
||||||
|
|
||||||
constructor(xml: Document) {
|
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++) {
|
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 {
|
export class GetArtistResponse {
|
||||||
artist: ArtistID3Element;
|
artist: ArtistID3Element
|
||||||
albums: AlbumID3Element[] = [];
|
albums: AlbumID3Element[] = []
|
||||||
|
|
||||||
constructor(xml: Document) {
|
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++) {
|
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 {
|
export class GetIndexesResponse {
|
||||||
ignoredArticles: string;
|
ignoredArticles: string
|
||||||
lastModified: number;
|
lastModified: number
|
||||||
artists: ArtistElement[] = [];
|
artists: ArtistElement[] = []
|
||||||
|
|
||||||
constructor(xml: Document) {
|
constructor(xml: Document) {
|
||||||
const indexesElement = xml.getElementsByTagName('indexes')[0];
|
const indexesElement = xml.getElementsByTagName('indexes')[0]
|
||||||
|
|
||||||
this.ignoredArticles = indexesElement.getAttribute('ignoredArticles') as string;
|
this.ignoredArticles = indexesElement.getAttribute('ignoredArticles') as string
|
||||||
this.lastModified = parseInt(indexesElement.getAttribute('lastModified') 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++) {
|
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 {
|
export class GetArtistInfoResponse {
|
||||||
artistInfo: ArtistInfoElement;
|
artistInfo: ArtistInfoElement
|
||||||
|
|
||||||
constructor(xml: Document) {
|
constructor(xml: Document) {
|
||||||
this.artistInfo = new ArtistInfoElement(xml.getElementsByTagName('artistInfo')[0]);
|
this.artistInfo = new ArtistInfoElement(xml.getElementsByTagName('artistInfo')[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GetArtistInfo2Response {
|
export class GetArtistInfo2Response {
|
||||||
artistInfo: ArtistInfo2Element;
|
artistInfo: ArtistInfo2Element
|
||||||
|
|
||||||
constructor(xml: Document) {
|
constructor(xml: Document) {
|
||||||
this.artistInfo = new ArtistInfo2Element(xml.getElementsByTagName('artistInfo2')[0]);
|
this.artistInfo = new ArtistInfo2Element(xml.getElementsByTagName('artistInfo2')[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GetMusicDirectoryResponse {
|
export class GetMusicDirectoryResponse {
|
||||||
directory: DirectoryElement;
|
directory: DirectoryElement
|
||||||
children: ChildElement[] = [];
|
children: ChildElement[] = []
|
||||||
|
|
||||||
constructor(xml: Document) {
|
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++) {
|
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 {
|
export class GetAlbumResponse {
|
||||||
album: AlbumID3Element;
|
album: AlbumID3Element
|
||||||
songs: ChildElement[] = [];
|
songs: ChildElement[] = []
|
||||||
|
|
||||||
constructor(xml: Document) {
|
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++) {
|
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> {
|
class BaseGetAlbumListResponse<T> {
|
||||||
albums: T[] = [];
|
albums: T[] = []
|
||||||
|
|
||||||
constructor(xml: Document, albumType: new (e: Element) => 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++) {
|
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> {
|
export class GetAlbumListResponse extends BaseGetAlbumListResponse<ChildElement> {
|
||||||
constructor(xml: Document) {
|
constructor(xml: Document) {
|
||||||
super(xml, ChildElement);
|
super(xml, ChildElement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GetAlbumList2Response extends BaseGetAlbumListResponse<AlbumID3Element> {
|
export class GetAlbumList2Response extends BaseGetAlbumListResponse<AlbumID3Element> {
|
||||||
constructor(xml: Document) {
|
constructor(xml: Document) {
|
||||||
super(xml, AlbumID3Element);
|
super(xml, AlbumID3Element)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/util.ts
12
src/util.ts
@ -1,11 +1,11 @@
|
|||||||
export function formatDuration(seconds: number): string {
|
export function formatDuration(seconds: number): string {
|
||||||
const s = seconds % 60;
|
const s = seconds % 60
|
||||||
const m = Math.floor(seconds / 60) % 60;
|
const m = Math.floor(seconds / 60) % 60
|
||||||
const h = 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) {
|
if (h > 0) {
|
||||||
time = `${h}:${time}`;
|
time = `${h}:${time}`
|
||||||
}
|
}
|
||||||
return time;
|
return time
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user