mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-28 01:19:27 +01:00
switching to async storage
also switching to not storing music data from api unless downloaded
This commit is contained in:
parent
50be0a6f85
commit
71e34a6066
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
95
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
98
src/storage/asyncstorage.ts
Normal file
98
src/storage/asyncstorage.ts
Normal 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,
|
||||
})]),
|
||||
]);
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user