switching to async storage

also switching to not storing music data from api unless downloaded
This commit is contained in:
austinried 2021-06-27 09:50:16 +09:00
parent 50be0a6f85
commit 71e34a6066
16 changed files with 350 additions and 200 deletions

View File

@ -7,6 +7,9 @@ buildscript {
compileSdkVersion = 29
targetSdkVersion = 29
ndkVersion = "20.1.5948944"
// react-native-async-storage next
kotlinVersion = '1.4.21'
}
repositories {
google()
@ -16,6 +19,9 @@ buildscript {
classpath("com.android.tools.build:gradle:4.1.0")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
// react-native-async-storage next
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}

View File

@ -26,3 +26,7 @@ android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.75.1
# react-native-async-storage next
AsyncStorage_useNextStorage=true
AsyncStorage_kotlinVersion=1.4.21

95
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "subsonify",
"version": "0.0.1",
"dependencies": {
"@react-native-async-storage/async-storage": "^1.15.5",
"@react-native-community/masked-view": "^0.1.11",
"@react-navigation/bottom-tabs": "^5.11.11",
"@react-navigation/material-top-tabs": "^5.3.15",
@ -1768,6 +1769,17 @@
"node": ">=8"
}
},
"node_modules/@react-native-async-storage/async-storage": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.15.5.tgz",
"integrity": "sha512-4AYehLH39B9a8UXCMf3ieOK+G61wGMP72ikx6/XSMA0DUnvx0PgaeaT2Wyt06kTrDTy8edewKnbrbeqwaM50TQ==",
"dependencies": {
"deep-assign": "^3.0.0"
},
"peerDependencies": {
"react-native": "^0.60.6 || ^0.61.5 || ^0.62.2 || ^0.63.2 || ^0.64.0 || 1000.0.0"
}
},
"node_modules/@react-native-community/cli": {
"version": "5.0.1-alpha.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-5.0.1-alpha.2.tgz",
@ -4491,6 +4503,18 @@
"node": ">=0.10"
}
},
"node_modules/deep-assign": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-3.0.0.tgz",
"integrity": "sha512-YX2i9XjJ7h5q/aQ/IM9PEwEnDqETAIYbggmdDB3HLTlSgo1CxPsj6pvhPG68rq6SVE0+p+6Ywsm5fTYNrYtBWw==",
"deprecated": "Check out `lodash.merge` or `merge-options` instead.",
"dependencies": {
"is-obj": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/deep-is": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@ -6664,6 +6688,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@ -14558,6 +14590,14 @@
}
}
},
"@react-native-async-storage/async-storage": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.15.5.tgz",
"integrity": "sha512-4AYehLH39B9a8UXCMf3ieOK+G61wGMP72ikx6/XSMA0DUnvx0PgaeaT2Wyt06kTrDTy8edewKnbrbeqwaM50TQ==",
"requires": {
"deep-assign": "^3.0.0"
}
},
"@react-native-community/cli": {
"version": "5.0.1-alpha.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-5.0.1-alpha.2.tgz",
@ -15132,7 +15172,8 @@
"@react-native-community/masked-view": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@react-native-community/masked-view/-/masked-view-0.1.11.tgz",
"integrity": "sha512-rQfMIGSR/1r/SyN87+VD8xHHzDYeHaJq6elOSCAD+0iLagXkSI2pfA0LmSXP21uw5i3em7GkkRjfJ8wpqWXZNw=="
"integrity": "sha512-rQfMIGSR/1r/SyN87+VD8xHHzDYeHaJq6elOSCAD+0iLagXkSI2pfA0LmSXP21uw5i3em7GkkRjfJ8wpqWXZNw==",
"requires": {}
},
"@react-native/assets": {
"version": "1.0.0",
@ -15621,7 +15662,8 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
"dev": true
"dev": true,
"requires": {}
},
"acorn-walk": {
"version": "7.2.0",
@ -15840,7 +15882,8 @@
"babel-core": {
"version": "7.0.0-bridge.0",
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz",
"integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg=="
"integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==",
"requires": {}
},
"babel-eslint": {
"version": "10.1.0",
@ -16692,6 +16735,14 @@
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"deep-assign": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-3.0.0.tgz",
"integrity": "sha512-YX2i9XjJ7h5q/aQ/IM9PEwEnDqETAIYbggmdDB3HLTlSgo1CxPsj6pvhPG68rq6SVE0+p+6Ywsm5fTYNrYtBWw==",
"requires": {
"is-obj": "^1.0.0"
}
},
"deep-is": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@ -17161,7 +17212,8 @@
"version": "22.4.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.4.1.tgz",
"integrity": "sha512-gcLfn6P2PrFAVx3AobaOzlIEevpAEf9chTpFZz7bYfc7pz8XRv7vuKTIE4hxPKZSha6XWKKplDQ0x9Pq8xX2mg==",
"dev": true
"dev": true,
"requires": {}
},
"eslint-plugin-prettier": {
"version": "3.1.2",
@ -17217,7 +17269,8 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz",
"integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==",
"dev": true
"dev": true,
"requires": {}
},
"eslint-plugin-react-native": {
"version": "3.11.0",
@ -18314,6 +18367,11 @@
"integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==",
"dev": true
},
"is-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8="
},
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@ -19064,7 +19122,8 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
"integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
"dev": true
"dev": true,
"requires": {}
},
"jest-regex-util": {
"version": "26.0.0",
@ -21277,7 +21336,8 @@
"react-native-fast-image": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.3.4.tgz",
"integrity": "sha512-LpzAdjUphihUpVEBn5fEv5AILe55rHav0YiZroPZ1rumKDhAl4u2cG01ku2Pb7l8sayjTsNu7FuURAlXUUDsow=="
"integrity": "sha512-LpzAdjUphihUpVEBn5fEv5AILe55rHav0YiZroPZ1rumKDhAl4u2cG01ku2Pb7l8sayjTsNu7FuURAlXUUDsow==",
"requires": {}
},
"react-native-fs": {
"version": "2.18.0",
@ -21311,12 +21371,14 @@
"react-native-iphone-x-helper": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz",
"integrity": "sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg=="
"integrity": "sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==",
"requires": {}
},
"react-native-linear-gradient": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.5.6.tgz",
"integrity": "sha512-HDwEaXcQIuXXCV70O+bK1rizFong3wj+5Q/jSyifKFLg0VWF95xh8XQgfzXwtq0NggL9vNjPKXa016KuFu+VFg=="
"integrity": "sha512-HDwEaXcQIuXXCV70O+bK1rizFong3wj+5Q/jSyifKFLg0VWF95xh8XQgfzXwtq0NggL9vNjPKXa016KuFu+VFg==",
"requires": {}
},
"react-native-reanimated": {
"version": "2.2.0",
@ -21332,7 +21394,8 @@
"react-native-safe-area-context": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-3.2.0.tgz",
"integrity": "sha512-k2Nty4PwSnrg9HwrYeeE+EYqViYJoOFwEy9LxL5RIRfoqxAq/uQXNGwpUg2/u4gnKpBbEPa9eRh15KKMe/VHkA=="
"integrity": "sha512-k2Nty4PwSnrg9HwrYeeE+EYqViYJoOFwEy9LxL5RIRfoqxAq/uQXNGwpUg2/u4gnKpBbEPa9eRh15KKMe/VHkA==",
"requires": {}
},
"react-native-screens": {
"version": "3.4.0",
@ -21345,17 +21408,20 @@
"react-native-sqlite-storage": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-native-sqlite-storage/-/react-native-sqlite-storage-5.0.0.tgz",
"integrity": "sha512-c1Joq3/tO1nmIcP8SkRZNolPSbfvY8uZg5lXse0TmjIPC0qHVbk96IMvWGyly1TmYCIpxpuDRc0/xCffDbYIvg=="
"integrity": "sha512-c1Joq3/tO1nmIcP8SkRZNolPSbfvY8uZg5lXse0TmjIPC0qHVbk96IMvWGyly1TmYCIpxpuDRc0/xCffDbYIvg==",
"requires": {}
},
"react-native-tab-view": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-2.16.0.tgz",
"integrity": "sha512-ac2DmT7+l13wzIFqtbfXn4wwfgtPoKzWjjZyrK1t+T8sdemuUvD4zIt+UImg03fu3s3VD8Wh/fBrIdcqQyZJWg=="
"integrity": "sha512-ac2DmT7+l13wzIFqtbfXn4wwfgtPoKzWjjZyrK1t+T8sdemuUvD4zIt+UImg03fu3s3VD8Wh/fBrIdcqQyZJWg==",
"requires": {}
},
"react-native-track-player": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/react-native-track-player/-/react-native-track-player-1.2.7.tgz",
"integrity": "sha512-U1kA25qm398/kY6BvTojGHt4S1tYuKrJNQpXW+dA+p0B+n4LlPyoidUXfu951YM7aoisg8EmQPsevUFmcXG+cg=="
"integrity": "sha512-U1kA25qm398/kY6BvTojGHt4S1tYuKrJNQpXW+dA+p0B+n4LlPyoidUXfu951YM7aoisg8EmQPsevUFmcXG+cg==",
"requires": {}
},
"react-refresh": {
"version": "0.4.3",
@ -23302,7 +23368,8 @@
"ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"requires": {}
},
"xcode": {
"version": "2.1.0",

View File

@ -10,6 +10,7 @@
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.15.5",
"@react-native-community/masked-view": "^0.1.11",
"@react-navigation/bottom-tabs": "^5.11.11",
"@react-navigation/material-top-tabs": "^5.3.15",

View File

@ -1,6 +1,4 @@
import { MusicDb } from "./storage/music";
import { SettingsDb } from "./storage/settings";
import { SubsonicApiClient } from "./subsonic/api";
export const musicDb = new MusicDb();
export const settingsDb = new SettingsDb();

View File

@ -3,8 +3,8 @@ import { Button, TextInput, View, Text } from 'react-native';
import { useRecoilState } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import md5 from 'md5';
import { musicDb, settingsDb } from '../clients';
import { appSettingsState, serversState } from '../state/settings';
import { musicDb } from '../clients';
import { appSettingsState } from '../state/settings';
import { DbStorage } from '../storage/db';
import { StackScreenProps } from '@react-navigation/stack';
import { useNavigation } from '@react-navigation/core';
@ -36,7 +36,6 @@ const DbControls = () => {
return (
<View>
<RecreateDbButton db={musicDb} title='Music' />
<RecreateDbButton db={settingsDb} title='Settings' />
<Button
title='Now Playing'
onPress={() => navigation.navigate('Now Playing')}
@ -46,11 +45,10 @@ const DbControls = () => {
}
const ServerSettingsView = () => {
const [servers, setServers] = useRecoilState(serversState);
const [appSettings, setAppSettings] = useRecoilState(appSettingsState);
const bootstrapServer = () => {
if (servers.length !== 0) {
if (appSettings.servers.length !== 0) {
return;
}
@ -58,14 +56,17 @@ const ServerSettingsView = () => {
const salt = uuidv4();
const address = 'http://demo.subsonic.org';
setServers([{
id, salt, address,
username: 'guest',
token: md5('guest' + salt),
}]);
setAppSettings({
server: id,
...appSettings,
servers: [
...appSettings.servers,
{
id, salt, address,
username: 'guest',
token: md5('guest' + salt),
},
],
activeServer: id,
});
};
@ -75,7 +76,7 @@ const ServerSettingsView = () => {
title='Add default server'
onPress={bootstrapServer}
/>
{servers.map(s => (
{appSettings.servers.map(s => (
<View key={s.id}>
<Text>{s.address}</Text>
<Text>{s.username}</Text>

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Text, View } from 'react-native';
import RNFS from 'react-native-fs';
import TrackPlayer, { Track } from 'react-native-track-player';
import { musicDb, settingsDb } from '../clients';
import { musicDb } from '../clients';
import paths from '../paths';
async function mkdir(path: string): Promise<void> {
@ -30,14 +30,10 @@ const SplashPage: React.FC<{}> = ({ children }) => {
await mkdir(paths.songs);
await musicDb.openDb();
await settingsDb.openDb();
if (!(await musicDb.dbExists())) {
await musicDb.createDb();
}
if (!(await settingsDb.dbExists())) {
await settingsDb.createDb();
}
await TrackPlayer.setupPlayer();
TrackPlayer.updateOptions({

View File

@ -4,14 +4,16 @@ import FastImage from 'react-native-fast-image';
import LinearGradient from 'react-native-linear-gradient';
import { useRecoilValue } from 'recoil';
import { Album } from '../../models/music';
import { albumsState, useCoverArtUri } from '../../state/music';
import { albumsState, albumsUpdatingState, useCoverArtUri, useUpdateAlbums } from '../../state/music';
import colors from '../../styles/colors';
import textStyles from '../../styles/text';
import TopTabContainer from '../common/TopTabContainer';
const AlbumArt: React.FC<{ height: number, width: number, id?: string }> = ({ height, width, id }) => {
const coverArtUri = useCoverArtUri(id);
const AlbumArt: React.FC<{
height: number,
width: number,
coverArtUri?: string
}> = ({ height, width, coverArtUri }) => {
const Placeholder = (
<LinearGradient
colors={[colors.accent, colors.accentLow]}
@ -39,27 +41,27 @@ const AlbumArt: React.FC<{ height: number, width: number, id?: string }> = ({ he
}
const MemoAlbumArt = React.memo(AlbumArt);
const AlbumItem: React.FC<{ name: string, coverArt?: string } > = ({ name, coverArt }) => {
const AlbumItem: React.FC<{
name: string,
artist?: string,
coverArtUri?: string
} > = ({ name, artist, coverArtUri }) => {
const size = 125;
return (
<View style={{
// flexDirection: 'row',
alignItems: 'center',
marginVertical: 8,
// marginLeft: 6,
// width: size,
flex: 1/3,
}}>
<MemoAlbumArt
width={size}
height={size}
id={coverArt}
coverArtUri={coverArtUri}
/>
<View style={{
flex: 1,
width: size,
// alignItems: 'baseline',
}}>
<Text
style={{
@ -71,13 +73,10 @@ const AlbumItem: React.FC<{ name: string, coverArt?: string } > = ({ name, cover
{name}
</Text>
<Text
style={{
...textStyles.itemSubtitle,
// marginTop: 2,
}}
style={{ ...textStyles.itemSubtitle }}
numberOfLines={1}
>
{name}
{artist}
</Text>
</View>
</View>
@ -86,11 +85,19 @@ const AlbumItem: React.FC<{ name: string, coverArt?: string } > = ({ name, cover
const MemoAlbumItem = React.memo(AlbumItem);
const AlbumListRenderItem: React.FC<{ item: Album }> = ({ item }) => (
<MemoAlbumItem name={item.name} coverArt={item.coverArt} />
<MemoAlbumItem name={item.name} artist={item.artist} coverArtUri={item.coverArtThumbUri} />
);
const AlbumsList = () => {
const albums = useRecoilValue(albumsState);
const updating = useRecoilValue(albumsUpdatingState);
const updateAlbums = useUpdateAlbums();
useEffect(() => {
if (albums.length === 0) {
updateAlbums();
}
});
return (
<View style={{ flex: 1 }}>
@ -100,6 +107,8 @@ const AlbumsList = () => {
keyExtractor={item => item.id}
numColumns={3}
removeClippedSubviews={true}
refreshing={updating}
onRefresh={updateAlbums}
/>
</View>
);

View File

@ -1,10 +1,10 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Text, View, Image, FlatList } from 'react-native';
import { Artist } from '../../models/music';
import { useRecoilValue } from 'recoil';
import { useRecoilValue, useResetRecoilState } from 'recoil';
import textStyles from '../../styles/text';
import TopTabContainer from '../common/TopTabContainer';
import { artistsState } from '../../state/music';
import { artistsState, artistsUpdatingState, useUpdateArtists } from '../../state/music';
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
<View style={{
@ -29,6 +29,14 @@ const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
const ArtistsList = () => {
const artists = useRecoilValue(artistsState);
const updating = useRecoilValue(artistsUpdatingState);
const updateArtists = useUpdateArtists();
useEffect(() => {
if (artists.length === 0) {
updateArtists();
}
});
const renderItem: React.FC<{ item: Artist }> = ({ item }) => (
<ArtistItem item={item} />
@ -39,6 +47,8 @@ const ArtistsList = () => {
data={artists}
renderItem={renderItem}
keyExtractor={item => item.id}
onRefresh={updateArtists}
refreshing={updating}
/>
);
}

View File

@ -3,14 +3,18 @@ export interface Artist {
name: string;
starred?: Date;
coverArt?: string;
coverArtUri?: string,
}
export interface Album {
id: string;
artistId: string;
artistId?: string;
artist?: string;
name: string;
starred?: Date;
coverArt?: string;
coverArtUri?: string,
coverArtThumbUri?: string,
}
export interface Song {

View File

@ -1,4 +1,4 @@
export interface ServerSettings {
export interface Server {
id: string;
address: string;
username: string;
@ -7,5 +7,6 @@ export interface ServerSettings {
}
export interface AppSettings {
server?: string;
servers: Server[],
activeServer?: string;
}

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { atom, DefaultValue, selector, selectorFamily, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { atom, DefaultValue, selector, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { musicDb } from '../clients';
import { Album, Artist, Song } from '../models/music';
import paths from '../paths';
@ -9,38 +9,85 @@ import RNFS from 'react-native-fs';
export const artistsState = atom<Artist[]>({
key: 'artistsState',
default: selector({
key: 'artistsState/default',
get: () => musicDb.getArtists(),
}),
effects_UNSTABLE: [
({ onSet }) => {
onSet((newValue) => {
if (!(newValue instanceof DefaultValue)) {
musicDb.updateArtists(newValue);
}
});
},
],
default: [],
});
export const artistsUpdatingState = atom<boolean>({
key: 'artistsUpdatingState',
default: false,
})
export const useUpdateArtists = () => {
const server = useRecoilValue(activeServer);
if (!server) {
return () => Promise.resolve();
}
const [updating, setUpdating] = useRecoilState(artistsUpdatingState);
const setArtists = useSetRecoilState(artistsState);
return async () => {
if (updating) {
return;
}
setUpdating(true);
const client = new SubsonicApiClient(server);
const response = await client.getArtists();
setArtists(response.data.artists.map(x => ({
id: x.id,
name: x.name,
starred: x.starred,
coverArt: x.coverArt,
coverArtUri: x.coverArt ? client.getCoverArtUri({ id: x.coverArt }) : undefined,
})));
setUpdating(false);
}
}
export const albumsState = atom<Album[]>({
key: 'albumsState',
default: selector({
key: 'albumsState/default',
get: () => musicDb.getAlbums(),
}),
effects_UNSTABLE: [
({ onSet }) => {
onSet((newValue) => {
if (!(newValue instanceof DefaultValue)) {
musicDb.updateAlbums(newValue);
}
});
},
],
default: [],
});
export const albumsUpdatingState = atom<boolean>({
key: 'albumsUpdatingState',
default: false,
})
export const useUpdateAlbums = () => {
const server = useRecoilValue(activeServer);
if (!server) {
return () => Promise.resolve();
}
const [updating, setUpdating] = useRecoilState(albumsUpdatingState);
const setAlbums = useSetRecoilState(albumsState);
return async () => {
if (updating) {
return;
}
setUpdating(true);
const client = new SubsonicApiClient(server);
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 });
setAlbums(response.data.albums.map(x => ({
id: x.id,
artistId: x.artistId,
artist: x.artist,
name: x.name,
starred: x.starred,
coverArt: x.coverArt,
coverArtUri: x.coverArt ? client.getCoverArtUri({ id: x.coverArt }) : undefined,
coverArtThumbUri: x.coverArt ? client.getCoverArtUri({ id: x.coverArt, size: '128' }) : undefined,
})));
setUpdating(false);
}
}
export const songsState = atom<Song[]>({
key: 'songsState',
default: selector({

View File

@ -1,47 +1,28 @@
import { atom, DefaultValue, selector } from 'recoil';
import { settingsDb } from '../clients';
import { AppSettings, ServerSettings } from '../models/settings';
export const serversState = atom<ServerSettings[]>({
key: 'serverState',
default: selector({
key: 'serversState/default',
get: () => settingsDb.getServers(),
}),
effects_UNSTABLE: [
({ onSet }) => {
onSet((newValue) => {
if (!(newValue instanceof DefaultValue)) {
settingsDb.updateServers(newValue);
}
});
}
],
});
import { AppSettings, Server } from '../models/settings';
import { getAppSettings, setAppSettings } from '../storage/settings';
export const appSettingsState = atom<AppSettings>({
key: 'appSettingsState',
default: selector({
key: 'appSettingsState/default',
get: () => settingsDb.getApp(),
get: () => getAppSettings(),
}),
effects_UNSTABLE: [
({ onSet }) => {
onSet((newValue, oldValue) => {
if (!(newValue instanceof DefaultValue)) {
settingsDb.updateApp(newValue);
setAppSettings(newValue);
}
});
}
],
});
export const activeServer = selector<ServerSettings | undefined>({
export const activeServer = selector<Server | undefined>({
key: 'activeServer',
get: ({get}) => {
const appSettings = get(appSettingsState);
const servers = get(serversState);
return servers.find(x => x.id == appSettings.server);
return appSettings.servers.find(x => x.id == appSettings.activeServer);
}
});

View File

@ -0,0 +1,98 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
const key = {
downloadedSongKeys: '@downloadedSongKeys',
downloadedAlbumKeys: '@downloadedAlbumKeys',
downloadedArtistKeys: '@downloadedArtistKeys',
downloadedPlaylistKeys: '@downloadedPlaylistKeys',
};
export async function getItem(key: string): Promise<string | null> {
try {
return await AsyncStorage.getItem(key);
} catch (e) {
console.error(`getItem error (key: ${key})`, e);
return null;
}
}
export async function multiGet(keys: string[]): Promise<[string, string | null][]> {
try {
return await AsyncStorage.multiGet(keys);
} catch (e) {
console.error(`multiGet error`, e);
return [];
}
}
export async function setItem(key: string, item: string): Promise<void> {
try {
await AsyncStorage.setItem(key, item);
} catch (e) {
console.error(`setItem error (key: ${key})`, e);
}
}
export async function multiSet(items: string[][]): Promise<void> {
try {
await AsyncStorage.multiSet(items);
} catch (e) {
console.error(`multiSet error`, e);
}
}
type DownloadedSong = {
id: string;
type: 'song';
name: string;
album: string;
artist: string;
};
type DownloadedAlbum = {
id: string;
type: 'album';
songs: string[];
name: string;
artist: string;
};
type DownloadedArtist = {
id: string;
type: 'artist';
songs: string[];
name: string;
};
type DownloadedPlaylist = {
id: string;
type: 'playlist';
songs: string[];
name: string;
};
export async function getDownloadedSongs(): Promise<DownloadedSong[]> {
const keysItem = await getItem(key.downloadedSongKeys);
const keys: string[] = keysItem ? JSON.parse(keysItem) : [];
const items = await multiGet(keys);
return items.map(x => {
const parsed = JSON.parse(x[1] as string);
return {
id: x[0],
type: 'song',
...parsed,
};
});
}
export async function setDownloadedSongs(items: DownloadedSong[]): Promise<void> {
await multiSet([
[key.downloadedSongKeys, JSON.stringify(items.map(x => x.id))],
...items.map(x => [x.id, JSON.stringify({
name: x.name,
album: x.album,
artist: x.artist,
})]),
]);
}

View File

@ -1,92 +1,15 @@
import { AppSettings, ServerSettings } from '../models/settings';
import { DbStorage } from './db';
import { AppSettings } from '../models/settings';
import { getItem, setItem } from './asyncstorage';
export class SettingsDb extends DbStorage {
constructor() {
super({ name: 'settings.db', location: 'Library' });
}
const appSettingsKey = '@appSettings';
async createDb(): Promise<void> {
await this.initDb(tx => {
tx.executeSql(`
CREATE TABLE servers (
id TEXT PRIMARY KEY NOT NULL,
address TEXT NOT NULL,
username TEXT NOT NULL,
token TEXT NOT NULL,
salt TEXT NOT NULL
);
`);
tx.executeSql(`
CREATE TABLE app (
server TEXT
);
`);
tx.executeSql(`
INSERT INTO app (server)
VALUES (NULL);
`);
});
}
async getServers(): Promise<ServerSettings[]> {
return (await this.executeSql(`
SELECT * FROM servers;
`))[0].rows.raw().map(x => ({
id: x.id,
address: x.address,
username: x.username,
token: x.token,
salt: x.salt,
}));
}
async getServer(id: string): Promise<ServerSettings | undefined> {
return (await this.getServers()).find(x => x.id === id);
}
// async addServer(server: ServerSettings): Promise<void> {
// await this.executeSql(`
// INSERT INTO servers (id, address, username, token, salt)
// VALUES (?, ?, ?, ?, ?);
// `, [server.id, server.address, server.username, server.token, server.salt]);
// }
// async removeServer(id: string): Promise<void> {
// await this.executeSql(`
// DELETE FROM servers
// WHERE id = ?;
// `, [id]);
// }
async updateServers(servers: ServerSettings[]): Promise<void> {
await this.transaction((tx) => {
tx.executeSql(`
DELETE FROM servers
`);
for (const s of servers) {
tx.executeSql(`
INSERT INTO servers (id, address, username, token, salt)
VALUES (?, ?, ?, ?, ?);
`, [s.id, s.address, s.username, s.token, s.salt]);
}
});
}
async getApp(): Promise<AppSettings> {
return (await this.executeSql(`
SELECT * FROM app;
`))[0].rows.raw().map(x => ({
server: x.server || undefined,
}))[0];
}
async updateApp(app: AppSettings): Promise<void> {
await this.executeSql(`
UPDATE app SET
server = ?;
`, [
app.server || null,
]);
}
export async function getAppSettings(): Promise<AppSettings> {
const item = await getItem(appSettingsKey);
return item ? JSON.parse(item) : {
servers: [],
};
}
export async function setAppSettings(appSettings: AppSettings): Promise<void> {
await setItem(appSettingsKey, JSON.stringify(appSettings));
}

View File

@ -2,7 +2,7 @@ import { DOMParser } from 'xmldom';
import RNFS from 'react-native-fs';
import { GetAlbumList2Params, GetAlbumListParams, GetAlbumParams, GetArtistInfo2Params, GetArtistInfoParams, GetCoverArtParams, GetIndexesParams, GetMusicDirectoryParams } from './params';
import { GetAlbumList2Response, GetAlbumListResponse, GetAlbumResponse, GetArtistInfo2Response, GetArtistInfoResponse, GetArtistsResponse, GetIndexesResponse, GetMusicDirectoryResponse, SubsonicResponse } from './responses';
import { ServerSettings } from '../models/settings';
import { Server } from '../models/settings';
import paths from '../paths';
export class SubsonicApiError extends Error {
@ -58,7 +58,7 @@ export class SubsonicApiClient {
private params: URLSearchParams
constructor(server: ServerSettings) {
constructor(server: Server) {
this.address = server.address;
this.username = server.username;
@ -80,7 +80,7 @@ export class SubsonicApiClient {
}
const url = `${this.address}/rest/${method}?${query}`;
console.log(url);
// console.log(url);
return url;
}
@ -189,4 +189,8 @@ export class SubsonicApiClient {
const path = `${paths.songCache}/${params.id}`;
return await this.apiDownload('getCoverArt', path, params);
}
getCoverArtUri(params: GetCoverArtParams): string {
return this.buildUrl('getCoverArt', params);
}
}