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 compileSdkVersion = 29
targetSdkVersion = 29 targetSdkVersion = 29
ndkVersion = "20.1.5948944" ndkVersion = "20.1.5948944"
// react-native-async-storage next
kotlinVersion = '1.4.21'
} }
repositories { repositories {
google() google()
@ -16,6 +19,9 @@ buildscript {
classpath("com.android.tools.build:gradle:4.1.0") classpath("com.android.tools.build:gradle:4.1.0")
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // 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 # Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.75.1 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", "name": "subsonify",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^1.15.5",
"@react-native-community/masked-view": "^0.1.11", "@react-native-community/masked-view": "^0.1.11",
"@react-navigation/bottom-tabs": "^5.11.11", "@react-navigation/bottom-tabs": "^5.11.11",
"@react-navigation/material-top-tabs": "^5.3.15", "@react-navigation/material-top-tabs": "^5.3.15",
@ -1768,6 +1769,17 @@
"node": ">=8" "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": { "node_modules/@react-native-community/cli": {
"version": "5.0.1-alpha.2", "version": "5.0.1-alpha.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-5.0.1-alpha.2.tgz", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-5.0.1-alpha.2.tgz",
@ -4491,6 +4503,18 @@
"node": ">=0.10" "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": { "node_modules/deep-is": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@ -6664,6 +6688,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/is-plain-object": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "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": { "@react-native-community/cli": {
"version": "5.0.1-alpha.2", "version": "5.0.1-alpha.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-5.0.1-alpha.2.tgz", "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": { "@react-native-community/masked-view": {
"version": "0.1.11", "version": "0.1.11",
"resolved": "https://registry.npmjs.org/@react-native-community/masked-view/-/masked-view-0.1.11.tgz", "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": { "@react-native/assets": {
"version": "1.0.0", "version": "1.0.0",
@ -15621,7 +15662,8 @@
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
"dev": true "dev": true,
"requires": {}
}, },
"acorn-walk": { "acorn-walk": {
"version": "7.2.0", "version": "7.2.0",
@ -15840,7 +15882,8 @@
"babel-core": { "babel-core": {
"version": "7.0.0-bridge.0", "version": "7.0.0-bridge.0",
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", "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": { "babel-eslint": {
"version": "10.1.0", "version": "10.1.0",
@ -16692,6 +16735,14 @@
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" "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": { "deep-is": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@ -17161,7 +17212,8 @@
"version": "22.4.1", "version": "22.4.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.4.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.4.1.tgz",
"integrity": "sha512-gcLfn6P2PrFAVx3AobaOzlIEevpAEf9chTpFZz7bYfc7pz8XRv7vuKTIE4hxPKZSha6XWKKplDQ0x9Pq8xX2mg==", "integrity": "sha512-gcLfn6P2PrFAVx3AobaOzlIEevpAEf9chTpFZz7bYfc7pz8XRv7vuKTIE4hxPKZSha6XWKKplDQ0x9Pq8xX2mg==",
"dev": true "dev": true,
"requires": {}
}, },
"eslint-plugin-prettier": { "eslint-plugin-prettier": {
"version": "3.1.2", "version": "3.1.2",
@ -17217,7 +17269,8 @@
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz",
"integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==", "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==",
"dev": true "dev": true,
"requires": {}
}, },
"eslint-plugin-react-native": { "eslint-plugin-react-native": {
"version": "3.11.0", "version": "3.11.0",
@ -18314,6 +18367,11 @@
"integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==",
"dev": true "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": { "is-plain-object": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@ -19064,7 +19122,8 @@
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
"integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
"dev": true "dev": true,
"requires": {}
}, },
"jest-regex-util": { "jest-regex-util": {
"version": "26.0.0", "version": "26.0.0",
@ -21277,7 +21336,8 @@
"react-native-fast-image": { "react-native-fast-image": {
"version": "8.3.4", "version": "8.3.4",
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.3.4.tgz", "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": { "react-native-fs": {
"version": "2.18.0", "version": "2.18.0",
@ -21311,12 +21371,14 @@
"react-native-iphone-x-helper": { "react-native-iphone-x-helper": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz", "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": { "react-native-linear-gradient": {
"version": "2.5.6", "version": "2.5.6",
"resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.5.6.tgz", "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": { "react-native-reanimated": {
"version": "2.2.0", "version": "2.2.0",
@ -21332,7 +21394,8 @@
"react-native-safe-area-context": { "react-native-safe-area-context": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-3.2.0.tgz", "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": { "react-native-screens": {
"version": "3.4.0", "version": "3.4.0",
@ -21345,17 +21408,20 @@
"react-native-sqlite-storage": { "react-native-sqlite-storage": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-native-sqlite-storage/-/react-native-sqlite-storage-5.0.0.tgz", "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": { "react-native-tab-view": {
"version": "2.16.0", "version": "2.16.0",
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-2.16.0.tgz", "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": { "react-native-track-player": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/react-native-track-player/-/react-native-track-player-1.2.7.tgz", "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": { "react-refresh": {
"version": "0.4.3", "version": "0.4.3",
@ -23302,7 +23368,8 @@
"ws": { "ws": {
"version": "7.4.6", "version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", "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": { "xcode": {
"version": "2.1.0", "version": "2.1.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; 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 { musicDb } from '../clients';
import { Album, Artist, Song } from '../models/music'; import { Album, Artist, Song } from '../models/music';
import paths from '../paths'; import paths from '../paths';
@ -9,37 +9,84 @@ import RNFS from 'react-native-fs';
export const artistsState = atom<Artist[]>({ export const artistsState = atom<Artist[]>({
key: 'artistsState', key: 'artistsState',
default: selector({ default: [],
key: 'artistsState/default', });
get: () => musicDb.getArtists(),
}), export const artistsUpdatingState = atom<boolean>({
effects_UNSTABLE: [ key: 'artistsUpdatingState',
({ onSet }) => { default: false,
onSet((newValue) => { })
if (!(newValue instanceof DefaultValue)) {
musicDb.updateArtists(newValue); 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[]>({ export const albumsState = atom<Album[]>({
key: 'albumsState', key: 'albumsState',
default: selector({ default: [],
key: 'albumsState/default', });
get: () => musicDb.getAlbums(),
}), export const albumsUpdatingState = atom<boolean>({
effects_UNSTABLE: [ key: 'albumsUpdatingState',
({ onSet }) => { default: false,
onSet((newValue) => { })
if (!(newValue instanceof DefaultValue)) {
musicDb.updateAlbums(newValue); 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[]>({ export const songsState = atom<Song[]>({
key: 'songsState', key: 'songsState',

View File

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

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 { AppSettings } from '../models/settings';
import { DbStorage } from './db'; import { getItem, setItem } from './asyncstorage';
export class SettingsDb extends DbStorage { const appSettingsKey = '@appSettings';
constructor() {
super({ name: 'settings.db', location: 'Library' }); export async function getAppSettings(): Promise<AppSettings> {
const item = await getItem(appSettingsKey);
return item ? JSON.parse(item) : {
servers: [],
};
} }
async createDb(): Promise<void> { export async function setAppSettings(appSettings: AppSettings): Promise<void> {
await this.initDb(tx => { await setItem(appSettingsKey, JSON.stringify(appSettings));
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,
]);
}
} }

View File

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