diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e5688cc..add70c3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,6 +9,8 @@ + + > 8 & 0xff), + (ipAddress >> 16 & 0xff), (ipAddress >> 24 & 0xff)); + promise.resolve(stringip); + }catch (Exception e) { + promise.resolve(null); + } + } + }).start(); + } + + // https://stackoverflow.com/a/26117646 + @ReactMethod + public void getDeviceName(final Promise promise) { + String manufacturer = Build.MANUFACTURER; + String model = Build.MODEL; + if (model.startsWith(manufacturer)) { + promise.resolve(capitalize(model)); + } else { + promise.resolve(capitalize(manufacturer) + " " + model); + } + } + private String capitalize(String s) { + if (s == null || s.length() == 0) { + return ""; + } + char first = s.charAt(0); + if (Character.isUpperCase(first)) { + return s; + } else { + return Character.toUpperCase(first) + s.substring(1); + } + } } diff --git a/babel.config.js b/babel.config.js index fd2301c..16958d5 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,6 +1,7 @@ module.exports = { presets: ['module:metro-react-native-babel-preset'], plugins: [ + '@babel/plugin-proposal-export-namespace-from', [ 'module-resolver', { diff --git a/index.js b/index.js index 3b8d789..bfa1e71 100644 --- a/index.js +++ b/index.js @@ -20,6 +20,7 @@ import { init as initLyric, toggleTranslation } from '@/plugins/lyric' import { init as initI18n, supportedLngs } from '@/plugins/i18n' import { deviceLanguage, getPlayInfo, toast } from '@/utils/tools' import { LIST_ID_PLAY_TEMP } from '@/config/constant' +import { connect } from '@/plugins/sync' console.log('starting app...') @@ -41,6 +42,7 @@ const init = () => { ]).then(() => { let setting = store.getState().common.setting toggleTranslation(setting.player.isShowTranslation) + if (setting.sync.enable) connect(setting.sync.host, setting.sync.port) let lang = setting.langId let needSetLang = false diff --git a/package-lock.json b/package-lock.json index d926567..1130c0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -777,6 +777,24 @@ "@babel/plugin-syntax-export-default-from": "^7.12.13" } }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.5.tgz", + "integrity": "sha512-g5POA32bXPMmSBu5Dx/iZGLGnKmKPc5AiY7qfZgurzrCYgIztDlHFbznSNCoQuv57YQLnQfaDi7dxCtLDIdXdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true + } + } + }, "@babel/plugin-proposal-nullish-coalescing-operator": { "version": "7.14.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.2.tgz", @@ -859,6 +877,15 @@ "@babel/helper-plugin-utils": "^7.12.13" } }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, "@babel/plugin-syntax-flow": { "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.12.13.tgz", @@ -2667,6 +2694,21 @@ "@types/responselike": "*" } }, + "@types/component-emitter": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", + "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, "@types/debug": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.6.tgz", @@ -3827,11 +3869,21 @@ "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -4465,6 +4517,11 @@ "safe-buffer": "~5.1.1" } }, + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -4496,6 +4553,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cosmiconfig": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", @@ -5169,6 +5235,48 @@ "resolved": "https://registry.npmjs.org/endian-reader/-/endian-reader-0.3.0.tgz", "integrity": "sha1-hOykNrgK7Q0GOcRykTOLky7+UKA=" }, + "engine.io": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.1.1.tgz", + "integrity": "sha512-aMWot7H5aC8L4/T8qMYbLdvKlZOdJTH54FxfdFunTGvhMx1BHkJOntWArsVfgAZVwAO9LC2sryPWRcEeUzCe5w==", + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~4.0.0", + "ws": "~7.4.2" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + } + } + }, + "engine.io-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", + "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", + "requires": { + "base64-arraybuffer": "0.1.4" + } + }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -13430,6 +13538,67 @@ } } }, + "socket.io": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.1.3.tgz", + "integrity": "sha512-tLkaY13RcO4nIRh1K2hT5iuotfTaIQw7cVIe0FUykN3SuQi0cm7ALxuyT5/CtDswOMWUzMGTibxYNx/gU7In+Q==", + "requires": { + "@types/cookie": "^0.4.0", + "@types/cors": "^2.8.10", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.1", + "engine.io": "~5.1.1", + "socket.io-adapter": "~2.3.1", + "socket.io-parser": "~4.0.4" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-adapter": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.1.tgz", + "integrity": "sha512-8cVkRxI8Nt2wadkY6u60Y4rpW3ejA1rxgcK2JuyIhmF+RMNpTy1QRtkHIDUOf3B4HlQwakMsWbKftMv/71VMmw==" + }, + "socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", diff --git a/package.json b/package.json index ed44b1c..10de76f 100644 --- a/package.json +++ b/package.json @@ -69,12 +69,14 @@ "redux-subscriber": "^1.1.0", "redux-thunk": "^2.3.0", "reselect": "^4.0.0", + "socket.io": "^4.1.2", "stream-browserify": "^1.0.0", "url": "~0.10.1", "util": "~0.10.3" }, "devDependencies": { "@babel/core": "^7.14.8", + "@babel/plugin-proposal-export-namespace-from": "^7.14.5", "@babel/runtime": "^7.14.8", "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", diff --git a/src/components/MusicAddModal.js b/src/components/MusicAddModal.js index 189f0ca..83f1067 100644 --- a/src/components/MusicAddModal.js +++ b/src/components/MusicAddModal.js @@ -109,8 +109,8 @@ export default memo(({ visible, hideModal, musicInfo, listId, isMove = false }) const index = list.list.indexOf(musicInfo) if (index > -1) { removeMusicFromList({ - id: list.id, - index, + listId: list.id, + id: musicInfo.songmid, }) toast(t('list_edit_action_tip_remove_success')) } diff --git a/src/components/common/Input.js b/src/components/common/Input.js index 39a9e53..53d9f6c 100644 --- a/src/components/common/Input.js +++ b/src/components/common/Input.js @@ -1,5 +1,5 @@ -import React, { useRef, useImperativeHandle, forwardRef, useCallback, useEffect, useState } from 'react' -import { TextInput, StyleSheet, View, TouchableOpacity, Animated } from 'react-native' +import React, { useRef, useImperativeHandle, forwardRef, useCallback } from 'react' +import { TextInput, StyleSheet, View, TouchableOpacity } from 'react-native' import Icon from './Icon' import { useGetter } from '@/store' diff --git a/src/config/defaultSetting.js b/src/config/defaultSetting.js index 474e65f..bb6ea69 100644 --- a/src/config/defaultSetting.js +++ b/src/config/defaultSetting.js @@ -3,7 +3,7 @@ // const { isMac } = require('./utils') const defaultSetting = { - version: '1.6', + version: '1.7', player: { togglePlayMethod: 'listLoop', highQuality: false, @@ -52,6 +52,9 @@ const defaultSetting = { isShowHistorySearch: false, isFocusSearchBox: false, }, + sync: { + enable: false, + }, // network: { // proxy: { // enable: false, diff --git a/src/config/globalData.js b/src/config/globalData.js index 84db7b6..f3c6035 100644 --- a/src/config/globalData.js +++ b/src/config/globalData.js @@ -17,3 +17,7 @@ global.isScreenKeepAwake = false // 是否播放完后退出应用 global.isPlayedExit = false + + +global.syncKeyInfo = {} +global.isSyncEnableing = false diff --git a/src/config/index.js b/src/config/index.js index 68936a5..8eed688 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -17,6 +17,8 @@ export const storageDataPrefix = { lyric: '@lyric__', musicUrl: '@music_url__', playInfo: '@play_info', + syncAuthKey: '@sync_auth_key', + syncHost: '@sync_host', } export const ITEM_HEIGHT = 60 diff --git a/src/lang/en_us.json b/src/lang/en_us.json index 67063a7..b180038 100644 --- a/src/lang/en_us.json +++ b/src/lang/en_us.json @@ -121,6 +121,18 @@ "setting_play_quality": "Play 320K quality songs first (if supported)", "setting_play_save_play_time": "Remember playback progress", "setting_play_show_translation": "Show lyrics translation", + "setting_sync": "Synchronization [It is recommended to back up the playlist before using it for the first time]", + "setting_sync_address": "Local IP address: {{address}}", + "setting_sync_code_fail": "Invalid connection code", + "setting_sync_code_input_tip": "Please enter the connection code", + "setting_sync_code_label": "You need to enter the connection code for the first connection", + "setting_sync_enbale": "Enable sync", + "setting_sync_host_label": "Synchronization service IP address", + "setting_sync_host_tip": "Please enter the synchronization service IP address", + "setting_sync_port_label": "Synchronization service port number", + "setting_sync_port_tip": "Please enter the synchronization service port number", + "setting_sync_status": "Status: {{status}}", + "setting_sync_status_enabled": "Connected", "setting_version": "Software Update", "setting_version_show_ver_modal": "Open the update window 🚀", "singer": "Artist: {{name}}", @@ -150,6 +162,7 @@ "storage_permission_tip_disagree": "User Disagree", "storage_permission_tip_disagree_ask_again": "This feature cannot be used because you have permanently denied LX access to the phone storage.\nIf you want to continue, you need to go to System Permission Management Set Luo Xue’s storage permission to allow.", "storage_permission_tip_request": "To use this function, you need to allow LX to access the phone storage. Do you agree and continue?", + "sync_status_disabled": "Not connected", "theme_blue": "Blue", "theme_blue2": "Purple Blue", "theme_green": "Green", diff --git a/src/lang/zh_cn.json b/src/lang/zh_cn.json index 4c91e88..05014e5 100644 --- a/src/lang/zh_cn.json +++ b/src/lang/zh_cn.json @@ -20,7 +20,7 @@ "disagree": "我就不", "disagree_tip": "那算了... 🙄", "input_error": "不要乱输好吧😡", - "list_add_btn_title": "把该歌曲添加到 {name}", + "list_add_btn_title": "把该歌曲添加到 {{name}}", "list_add_title_first_add": "添加", "list_add_title_first_move": "移动", "list_add_title_last": "到...", @@ -122,6 +122,18 @@ "setting_play_quality": "优先播放320K品质的歌曲(如果支持)", "setting_play_save_play_time": "记住播放进度", "setting_play_show_translation": "显示歌词翻译", + "setting_sync": "同步 [首次使用前建议先备份一次歌单]", + "setting_sync_address": "本机IP地址:{{address}}", + "setting_sync_code_fail": "连接码无效", + "setting_sync_code_input_tip": "请输入连接码", + "setting_sync_code_label": "首次连接需要输入连接码", + "setting_sync_enbale": "启用同步", + "setting_sync_host_label": "同步服务IP地址", + "setting_sync_host_tip": "请输入同步服务IP地址", + "setting_sync_port_label": "同步服务端口号", + "setting_sync_port_tip": "请输入同步服务端口号", + "setting_sync_status": "状态:{{status}}", + "setting_sync_status_enabled": "已连接", "setting_version": "软件更新", "setting_version_show_ver_modal": "打开更新窗口 🚀", "singer": "艺术家:{{name}}", @@ -151,6 +163,7 @@ "storage_permission_tip_disagree": "你个骗纸,刚刚问你,你都说同意的,最后又拒绝,哼 🥺", "storage_permission_tip_disagree_ask_again": "此功能无法使用,因为你已经永久拒绝洛雪访问手机存储😫。\n若想继续,你需要去👉系统权限管理👈将洛雪的存储权限设置为允许", "storage_permission_tip_request": "使用此功能需要允许洛雪访问手机存储,是否同意并继续?", + "sync_status_disabled": "未连接", "theme_blue": "蓝田生玉", "theme_blue2": "清热版蓝", "theme_green": "绿意盎然", diff --git a/src/plugins/sync/client/auth.js b/src/plugins/sync/client/auth.js new file mode 100644 index 0000000..0270c9b --- /dev/null +++ b/src/plugins/sync/client/auth.js @@ -0,0 +1,71 @@ +import { getSyncAuthKey, setSyncAuthKey } from '@/utils/tools' +import { request, aesEncrypt, aesDecrypt } from './utils' +import { getDeviceName } from '@/utils/utils' +import { SYNC_CODE } from './config' + + +const hello = (host, port) => request(`http://${host}:${port}/hello`) + .then(text => text == SYNC_CODE.helloMsg) + .catch(err => { + console.log(err) + return false + }) + +const getServerId = (host, port) => request(`http://${host}:${port}/id`) + .then(text => { + if (!text.startsWith(SYNC_CODE.idPrefix)) return '' + return text.replace(SYNC_CODE.idPrefix, '') + }) + .catch(err => { + console.log(err) + return false + }) + +const codeAuth = async(host, port, serverId, authCode) => { + let key = ''.padStart(16, Buffer.from(authCode).toString('hex')) + const iv = Buffer.from(key.split('').reverse().join('')).toString('base64') + key = Buffer.from(key).toString('base64') + const msg = aesEncrypt(SYNC_CODE.authMsg + await getDeviceName(), key, iv) + return request(`http://${host}:${port}/ah`, { headers: { m: msg } }).then(text => { + // console.log(text) + let msg + try { + msg = aesDecrypt(text, key, iv) + } catch (err) { + throw new Error(SYNC_CODE.authFailed) + } + if (!msg) return Promise.reject(new Error(SYNC_CODE.authFailed)) + const info = JSON.parse(msg) + setSyncAuthKey(serverId, info) + return info + }) +} + +const keyAuth = async(host, port, keyInfo) => { + const msg = aesEncrypt(SYNC_CODE.authMsg + await getDeviceName(), keyInfo.key, keyInfo.iv) + return request(`http://${host}:${port}/ah`, { headers: { i: keyInfo.clientId, m: msg } }).then(text => { + let msg + try { + msg = aesDecrypt(text, keyInfo.key, keyInfo.iv) + } catch (err) { + throw new Error(SYNC_CODE.authFailed) + } + if (msg != SYNC_CODE.helloMsg) return Promise.reject(new Error(SYNC_CODE.authFailed)) + }) +} + +const auth = async(host, port, serverId, authCode) => { + if (authCode) return codeAuth(host, port, serverId, authCode) + const keyInfo = await getSyncAuthKey(serverId) + if (!keyInfo) throw new Error(SYNC_CODE.missingAuthCode) + await keyAuth(host, port, keyInfo) + return keyInfo +} + +export default async(host, port, authCode) => { + console.log('connect: ', host, port, authCode) + if (!await hello(host, port)) throw new Error(SYNC_CODE.connectServiceFailed) + const serverId = await getServerId(host, port) + if (!serverId) throw new Error(SYNC_CODE.getServiceIdFailed) + return auth(host, port, serverId, authCode) +} diff --git a/src/plugins/sync/client/client.js b/src/plugins/sync/client/client.js new file mode 100644 index 0000000..a0ed987 --- /dev/null +++ b/src/plugins/sync/client/client.js @@ -0,0 +1,93 @@ +import { io } from 'socket.io/client-dist/socket.io' +import { aesEncrypt } from './utils' +import * as modules from '../modules' +import { action as commonAction } from '@/store/modules/common' +import { getStore } from '@/store' +import syncList from './syncList' + +const handleConnection = (socket) => { + for (const module of Object.values(modules)) { + module.registerListHandler(socket) + } +} + +let socket +export const connect = (host, port, keyInfo) => { + socket = io(`ws://${host}:${port}`, { + path: '/sync', + reconnectionAttempts: 5, + transports: ['websocket'], + query: { + i: keyInfo.clientId, + t: aesEncrypt('lx-music connect', keyInfo.key, keyInfo.iv), + }, + }) + + socket.on('connect', async() => { + console.log('connect') + const store = getStore() + global.syncKeyInfo = keyInfo + try { + await syncList(socket) + } catch (err) { + console.log(err) + return + } + handleConnection(socket) + store.dispatch(commonAction.setSyncStatus({ + status: true, + message: '', + })) + }) + socket.on('connect_error', (err) => { + console.log(err.message) + const store = getStore() + store.dispatch(commonAction.setSyncStatus({ + status: false, + message: err.message, + })) + // if (err.message === 'invalid credentials') { + // socket.auth.token = 'efgh' + // socket.connect() + // } + }) + socket.on('disconnect', (reason) => { + console.log('disconnect', reason) + const store = getStore() + store.dispatch(commonAction.setSyncStatus({ + status: false, + message: reason, + })) + // if (reason === 'io server disconnect') { + // // the disconnection was initiated by the server, you need to reconnect manually + // socket.connect() + // } + // else the socket will automatically try to reconnect + }) + + // ws.onopen = () => { + // // connection opened + // ws.send('something') // send a message + // } + + // ws.onmessage = (e) => { + // // a message was received + // console.log(e.data) + // } + + // ws.onerror = (e) => { + // // an error occurred + // console.log(e.message) + // } + + // ws.onclose = (e) => { + // // connection closed + // console.log(e.code, e.reason) + // } +} + +export const disconnect = async() => { + if (!socket) return + await socket.close() + socket = null +} diff --git a/src/plugins/sync/client/config.js b/src/plugins/sync/client/config.js new file mode 100644 index 0000000..043bb2e --- /dev/null +++ b/src/plugins/sync/client/config.js @@ -0,0 +1,10 @@ +export const SYNC_CODE = { + helloMsg: 'Hello~::^-^::', + idPrefix: 'OjppZDo6', + authMsg: 'lx-music auth::', + authFailed: 'Auth failed', + missingAuthCode: 'Missing auth code', + getServiceIdFailed: 'Get service id failed', + connectServiceFailed: 'Connect service failed', + connecting: 'Connecting...', +} diff --git a/src/plugins/sync/client/index.js b/src/plugins/sync/client/index.js new file mode 100644 index 0000000..a418d3b --- /dev/null +++ b/src/plugins/sync/client/index.js @@ -0,0 +1,59 @@ +import handleAuth from './auth' +import { connect as socketConnect, disconnect as socketDisconnect } from './client' +import { getSyncHost } from '@/utils/tools' +import { action as commonAction } from '@/store/modules/common' +import { getStore } from '@/store' +import { SYNC_CODE } from './config' + +const handleConnect = async authCode => { + const hostInfo = await getSyncHost() + if (!hostInfo || !hostInfo.host || !hostInfo.port) throw new Error('Unknown service address') + await disconnect(false) + const keyInfo = await handleAuth(hostInfo.host, hostInfo.port, authCode) + await socketConnect(hostInfo.host, hostInfo.port, keyInfo) +} +const handleDisconnect = async() => { + await socketDisconnect() +} + +const connect = authCode => { + const store = getStore() + store.dispatch(commonAction.setSyncStatus({ + status: false, + message: SYNC_CODE.connecting, + })) + return handleConnect(authCode).then(() => { + const store = getStore() + store.dispatch(commonAction.setSyncStatus({ + status: true, + message: '', + })) + }).catch(err => { + const store = getStore() + store.dispatch(commonAction.setSyncStatus({ + status: false, + message: err.message, + })) + }) +} + +const disconnect = (isResetStatus = true) => handleDisconnect().then(() => { + if (isResetStatus) { + const store = getStore() + store.dispatch(commonAction.setSyncStatus({ + status: false, + message: '', + })) + } +}).catch(err => { + const store = getStore() + store.dispatch(commonAction.setSyncStatus({ + message: err.message, + })) +}) + +export { + connect, + disconnect, + SYNC_CODE, +} diff --git a/src/plugins/sync/client/syncList.js b/src/plugins/sync/client/syncList.js new file mode 100644 index 0000000..55a837c --- /dev/null +++ b/src/plugins/sync/client/syncList.js @@ -0,0 +1,80 @@ +import { getStore } from '@/store' +import { action as commonAction } from '@/store/modules/common' +import { action as listAction } from '@/store/modules/list' + +import { decryptMsg, encryptMsg } from './utils' +let socket +let syncAction + +const wait = () => new Promise((resolve, reject) => { + syncAction = [resolve, reject] +}) + +const sendListData = type => { + const store = getStore() + const state = store.getState() + let listData + switch (type) { + case 'all': + listData = { + defaultList: state.list.defaultList, + loveList: state.list.loveList, + userList: state.list.userList, + } + break + + default: + break + } + // console.log('sendListData') + socket.emit('list:sync', encryptMsg(JSON.stringify({ + action: 'getData', + data: listData, + }))) + // console.log('sendListData', 'encryptMsg') +} + +const saveList = ({ defaultList, loveList, userList }) => { + const store = getStore() + store.dispatch(listAction.setSyncList({ defaultList, loveList, userList })) +} + +const handleListSync = enMsg => { + // console.log('handleListSync', enMsg.length) + const { action, data } = JSON.parse(decryptMsg(enMsg)) + // console.log('handleListSync', action) + switch (action) { + case 'getData': + sendListData(data) + break + case 'setData': + saveList(data) + break + case 'finished': + if (!syncAction) return + syncAction[0]() + syncAction = null + break + default: + break + } +} + +const handleDisconnect = err => { + if (!syncAction) return + syncAction[1](err.message ? err : new Error(err)) + syncAction = null +} + +export default async _socket => { + socket = _socket + socket.on('list:sync', handleListSync) + socket.on('connect_error', handleDisconnect) + socket.on('disconnect', handleDisconnect) + const store = getStore() + store.dispatch(commonAction.setSyncStatus({ + status: false, + message: 'Syncing...', + })) + await wait() +} diff --git a/src/plugins/sync/client/utils.js b/src/plugins/sync/client/utils.js new file mode 100644 index 0000000..118f0dd --- /dev/null +++ b/src/plugins/sync/client/utils.js @@ -0,0 +1,47 @@ +import { createCipheriv, createDecipheriv } from 'crypto' +import BackgroundTimer from 'react-native-background-timer' + +export const request = (url, { timeout = 10000, ...options } = {}) => { + const controller = new global.AbortController() + const id = BackgroundTimer.setTimeout(() => controller.abort(), timeout) + return global.fetch(url, { + ...options, + signal: controller.signal, + }).then(response => { + BackgroundTimer.clearTimeout(id) + return response.text() + }).catch(err => { + // console.log(err, err.code, err.message) + return Promise.reject(err) + }) +} + +export const aesEncrypt = (text, key, iv) => { + const cipher = createCipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64')) + return Buffer.concat([cipher.update(Buffer.from(text)), cipher.final()]).toString('base64') +} + +export const aesDecrypt = (text, key, iv) => { + const decipher = createDecipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64')) + return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString() +} + +export const encryptMsg = msg => { + return msg + // const keyInfo = global.syncKeyInfo + // if (!keyInfo) return '' + // return aesEncrypt(msg, keyInfo.key, keyInfo.iv) +} + +export const decryptMsg = enMsg => { + return enMsg + // const keyInfo = global.syncKeyInfo + // if (!keyInfo) return '' + // let msg = '' + // try { + // msg = aesDecrypt(enMsg, keyInfo.key, keyInfo.iv) + // } catch (err) { + // console.log(err) + // } + // return msg +} diff --git a/src/plugins/sync/index.js b/src/plugins/sync/index.js new file mode 100644 index 0000000..e4bbd22 --- /dev/null +++ b/src/plugins/sync/index.js @@ -0,0 +1,2 @@ +export * from './modules' +export { connect, disconnect, SYNC_CODE } from './client' diff --git a/src/plugins/sync/modules/index.js b/src/plugins/sync/modules/index.js new file mode 100644 index 0000000..cd9e351 --- /dev/null +++ b/src/plugins/sync/modules/index.js @@ -0,0 +1 @@ +export * as list from './list' diff --git a/src/plugins/sync/modules/list/index.js b/src/plugins/sync/modules/list/index.js new file mode 100644 index 0000000..6a73b8a --- /dev/null +++ b/src/plugins/sync/modules/list/index.js @@ -0,0 +1,18 @@ +import { register as registerOn, unregister as unregisterOn } from './on' +import { + register as registerSend, + unregister as unregisterSend, +} from './send' + +export const registerListHandler = _socket => { + unregisterListHandler() + registerOn(_socket) + registerSend(_socket) +} + +export const unregisterListHandler = () => { + unregisterOn() + unregisterSend() +} + +export * from './send' diff --git a/src/plugins/sync/modules/list/on.js b/src/plugins/sync/modules/list/on.js new file mode 100644 index 0000000..b852c0e --- /dev/null +++ b/src/plugins/sync/modules/list/on.js @@ -0,0 +1,96 @@ +import { getStore } from '@/store' +import { decryptMsg } from '../../client/utils' +import { + setList, + listAdd, + listMove, + listAddMultiple, + listMoveMultiple, + listRemove, + listRemoveMultiple, + listClear, + updateMusicInfo, + createUserList, + removeUserList, + setUserListName, + setMusicPosition, + // moveupUserList, + // movedownUserList, + // setUserListPosition, +} from '@/store/modules/list/action' + +const store = getStore() + +let socket + +const handleListAction = enMsg => { + const { action, data } = JSON.parse(decryptMsg(enMsg)) + if (typeof data == 'object') data.isSync = true + console.log(action) + + switch (action) { + // case 'init_list': + // store.dispatch(initList(data)) + // break + case 'set_list': + store.dispatch(setList(data)) + break + case 'list_add': + store.dispatch(listAdd(data)) + break + case 'list_move': + store.dispatch(listMove(data)) + break + case 'list_add_multiple': + store.dispatch(listAddMultiple(data)) + break + case 'list_move_multiple': + store.dispatch(listMoveMultiple(data)) + break + case 'list_remove': + store.dispatch(listRemove(data)) + break + case 'list_remove_multiple': + store.dispatch(listRemoveMultiple(data)) + break + case 'list_clear': + store.dispatch(listClear(data)) + break + case 'update_music_info': + store.dispatch(updateMusicInfo(data)) + break + case 'create_user_list': + store.dispatch(createUserList(data)) + break + case 'remove_user_list': + store.dispatch(removeUserList(data)) + break + case 'set_user_list_name': + store.dispatch(setUserListName(data)) + break + case 'set_music_position': + store.dispatch(setMusicPosition(data)) + break + // case 'moveup_user_list': + // store.dispatch(moveupUserList(data)) + // break + // case 'movedown_user_list': + // store.dispatch(movedownUserList(data)) + // break + default: + break + } +} + +export const register = _socket => { + unregister() + socket = _socket + socket.on('list:action', handleListAction) + // socket.on('list:add', addMusic) +} + +export const unregister = () => { + if (!socket) return + socket.off('list:action', handleListAction) + socket = null +} diff --git a/src/plugins/sync/modules/list/send.js b/src/plugins/sync/modules/list/send.js new file mode 100644 index 0000000..36ac733 --- /dev/null +++ b/src/plugins/sync/modules/list/send.js @@ -0,0 +1,16 @@ +import { encryptMsg } from '../../client/utils' + +let socket + +export const sendListAction = (action, data) => { + if (!socket) return + socket.emit('list:action', encryptMsg(JSON.stringify({ action, data }))) +} + +export const register = _socket => { + socket = _socket +} + +export const unregister = () => { + socket = null +} diff --git a/src/screens/Home/List/components/MyList.js b/src/screens/Home/List/components/MyList.js index b75a0ef..054b1e6 100644 --- a/src/screens/Home/List/components/MyList.js +++ b/src/screens/Home/List/components/MyList.js @@ -72,9 +72,8 @@ const List = memo(({ setVisiblePanel, currentList, activeListIdRef, handleCancel }, [setPrevSelectListId, setVisiblePanel]) const handleRemoveList = useCallback(id => { - if (id == activeListIdRef.current) setPrevSelectListId(userList[0].id) - removeUserList(id) - }, [activeListIdRef, userList, removeUserList, setPrevSelectListId]) + removeUserList({ id }) + }, [removeUserList]) const hideMenu = useCallback(() => { diff --git a/src/screens/Home/List/index.js b/src/screens/Home/List/index.js index 5c1b9cd..0e3848e 100644 --- a/src/screens/Home/List/index.js +++ b/src/screens/Home/List/index.js @@ -212,10 +212,10 @@ const List = () => { break case 'remove': if (selectedListRef.current.length) { - removeListMultiItem({ id: activeListIdRef.current, list: selectedListRef.current }) + removeListMultiItem({ listId: activeListIdRef.current, ids: selectedListRef.current.map(s => s.songmid) }) handleCancelMultiSelect() } else { - removeListItem({ id: activeListIdRef.current, index: selectedDataRef.current.index }) + removeListItem({ listId: activeListIdRef.current, id: selectedDataRef.current.data.songmid }) } break default: diff --git a/src/screens/Home/Setting/Sync/IsEnable.js b/src/screens/Home/Setting/Sync/IsEnable.js new file mode 100644 index 0000000..692c8cd --- /dev/null +++ b/src/screens/Home/Setting/Sync/IsEnable.js @@ -0,0 +1,215 @@ +import React, { memo, useCallback, useState, useEffect, useRef, useMemo } from 'react' +import { View, Text, StyleSheet } from 'react-native' + +import { useGetter, useDispatch } from '@/store' + +import CheckBoxItem from '../components/CheckBoxItem' +import ConfirmAlert from '@/components/common/ConfirmAlert' +import Input from '@/components/common/Input' +import { useTranslation } from '@/plugins/i18n' +import { connect, disconnect, SYNC_CODE } from '@/plugins/sync' +import { getSyncHost, setSyncHost, toast } from '@/utils/tools' +import InputItem from '../components/InputItem' +import { getWIFIIPV4Address } from '@/utils/utils' + +const addressRxp = /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/ +const portRxp = /(\d+)/ + +const HostInput = memo(({ setHost, host, disabled }) => { + const { t } = useTranslation() + + const hostAddress = useMemo(() => { + return addressRxp.test(host) ? RegExp.$1 : '' + }, [host]) + + const setHostAddress = useCallback((value, callback) => { + let hostAddress = addressRxp.test(value) ? RegExp.$1 : '' + callback(hostAddress) + if (host == hostAddress) return + setHost(hostAddress) + }, [host, setHost]) + + return ( + + ) +}) + +const PortInput = memo(({ setPort, port, disabled }) => { + const { t } = useTranslation() + + const portNum = useMemo(() => { + return portRxp.test(port) ? RegExp.$1 : '' + }, [port]) + + const setPortAddress = useCallback((value, callback) => { + let portNum = portRxp.test(value) ? RegExp.$1 : '' + callback(portNum) + if (port == portNum) return + setPort(portNum) + }, [port, setPort]) + + return ( + + ) +}) + + +export default memo(() => { + const { t } = useTranslation() + const setIsEnableSync = useDispatch('common', 'setIsEnableSync') + const syncStatus = useGetter('common', 'syncStatus') + const isEnableSync = useGetter('common', 'isEnableSync') + const [isWaiting, setIsWaiting] = useState(global.isSyncEnableing) + const [hostInfo, setHostInfo] = useState({ host: '', port: '' }) + const isUnmountedRef = useRef(true) + const theme = useGetter('common', 'theme') + const [address, setAddress] = useState('') + const [visibleCodeModal, setVisibleCodeModal] = useState(false) + const [authCode, setAuthCode] = useState('') + + useEffect(() => { + isUnmountedRef.current = false + getSyncHost().then(hostInfo => { + if (isUnmountedRef.current) return + console.log(hostInfo) + setHostInfo(hostInfo) + }) + getWIFIIPV4Address().then(address => { + if (isUnmountedRef.current) return + setAddress(address) + }) + + return () => { + isUnmountedRef.current = true + } + }, []) + + useEffect(() => { + switch (syncStatus.message) { + case SYNC_CODE.authFailed: + toast(t('setting_sync_code_fail')) + case SYNC_CODE.missingAuthCode: + setVisibleCodeModal(true) + break + default: + break + } + }, [syncStatus.message, t]) + + const handleSetEnableSync = useCallback(flag => { + setIsEnableSync(flag) + + global.isSyncEnableing = true + setIsWaiting(true) + ;(flag ? connect() : disconnect()).finally(() => { + global.isSyncEnableing = false + setIsWaiting(false) + }) + }, [setIsEnableSync]) + + + const setHost = useCallback(host => { + const newHostInfo = { ...hostInfo, host } + setSyncHost(newHostInfo) + setHostInfo(newHostInfo) + }, [hostInfo]) + const setPort = useCallback(port => { + const newHostInfo = { ...hostInfo, port } + setSyncHost(newHostInfo) + setHostInfo(newHostInfo) + }, [hostInfo]) + + const host = useMemo(() => hostInfo.host, [hostInfo.host]) + const port = useMemo(() => hostInfo.port, [hostInfo.port]) + + const status = useMemo(() => { + return `${syncStatus.message ? syncStatus.message : syncStatus.status ? t('setting_sync_status_enabled') : t('sync_status_disabled')}` + }, [syncStatus.message, syncStatus.status, t]) + + const handleCancelSetCode = useCallback(() => { + setVisibleCodeModal(false) + }, []) + const handleSetCode = useCallback(() => { + const code = authCode.trim() + if (code.length != 6) return + connect(code) + setAuthCode('') + setVisibleCodeModal(false) + }, [authCode]) + + return ( + <> + + + {t('setting_sync_address', { address })} + {t('setting_sync_status', { status })} + + + + + + + + {t('setting_sync_code_label')} + + + + + ) +}) + + +const styles = StyleSheet.create({ + authCodeContent: { + flexGrow: 1, + flexShrink: 1, + flexDirection: 'column', + }, + authCodeInput: { + flexGrow: 1, + flexShrink: 1, + minWidth: 240, + borderRadius: 4, + paddingTop: 2, + paddingBottom: 2, + fontSize: 12, + }, + + // tagTypeList: { + // flexDirection: 'row', + // flexWrap: 'wrap', + // }, + // tagButton: { + // // marginRight: 10, + // borderRadius: 4, + // marginRight: 10, + // marginBottom: 10, + // }, + // tagButtonText: { + // paddingLeft: 12, + // paddingRight: 12, + // paddingTop: 8, + // paddingBottom: 8, + // }, +}) diff --git a/src/screens/Home/Setting/Sync/index.js b/src/screens/Home/Setting/Sync/index.js new file mode 100644 index 0000000..ecb3d42 --- /dev/null +++ b/src/screens/Home/Setting/Sync/index.js @@ -0,0 +1,17 @@ +import React, { memo } from 'react' + +import Section from '../components/Section' +import IsEnable from './IsEnable' +// import SyncHost from './SyncHost' +import { useTranslation } from '@/plugins/i18n' + +export default memo(() => { + const { t } = useTranslation() + + return ( +
+ + {/* */} +
+ ) +}) diff --git a/src/screens/Home/Setting/components/InputItem.js b/src/screens/Home/Setting/components/InputItem.js index 12fdae2..4744f66 100644 --- a/src/screens/Home/Setting/components/InputItem.js +++ b/src/screens/Home/Setting/components/InputItem.js @@ -18,7 +18,10 @@ export default memo(({ value, label, onChange, ...props }) => { useEffect(() => { isMountRef.current = true return () => isMountRef.current = false - }) + }, []) + useEffect(() => { + if (value != text) setText(String(value)) + }, [value]) return ( {label} @@ -49,5 +52,6 @@ const styles = StyleSheet.create({ paddingTop: 2, paddingBottom: 2, fontSize: 12, + maxWidth: 300, }, }) diff --git a/src/screens/Home/Setting/index.js b/src/screens/Home/Setting/index.js index aa1391f..7345160 100644 --- a/src/screens/Home/Setting/index.js +++ b/src/screens/Home/Setting/index.js @@ -9,6 +9,7 @@ import { import Basic from './Basic' import Player from './Player' import List from './List' +import Sync from './Sync' import Backup from './Backup' import Other from './Other' import Version from './Version' @@ -30,6 +31,7 @@ export default () => { + diff --git a/src/store/modules/common/action.js b/src/store/modules/common/action.js index 20189e9..20899c9 100644 --- a/src/store/modules/common/action.js +++ b/src/store/modules/common/action.js @@ -35,6 +35,8 @@ export const TYPES = { setIsHandleAudioFocus: null, setAddMusicLocationType: null, setIsShowLyricTranslation: null, + setIsEnableSync: null, + setSyncStatus: null, } for (const key of Object.keys(TYPES)) { TYPES[key] = `common__${key}` @@ -291,3 +293,18 @@ export const setIsShowLyricTranslation = flag => async(dispatch, getState) => { await setData(settingKey, common.setting) } +export const setIsEnableSync = flag => async(dispatch, getState) => { + dispatch({ + type: TYPES.setIsEnableSync, + payload: flag, + }) + const { common } = getState() + await setData(settingKey, common.setting) +} + +export const setSyncStatus = statusInfo => async(dispatch, getState) => { + dispatch({ + type: TYPES.setSyncStatus, + payload: statusInfo, + }) +} diff --git a/src/store/modules/common/getter.js b/src/store/modules/common/getter.js index 107f3c3..f2e77f4 100644 --- a/src/store/modules/common/getter.js +++ b/src/store/modules/common/getter.js @@ -48,6 +48,9 @@ export const isShowLyricTranslation = state => state.common.setting.player.isSho export const activeApiSourceId = state => state.common.setting.apiSource +export const isEnableSync = state => state.common.setting.sync.enable +export const syncStatus = state => state.common.syncStatus + const apiSourceListFormated = apiSourceInfo.map(api => ({ id: api.id, name: api.name, diff --git a/src/store/modules/common/reducer.js b/src/store/modules/common/reducer.js index 3002e17..bad79a5 100644 --- a/src/store/modules/common/reducer.js +++ b/src/store/modules/common/reducer.js @@ -39,6 +39,10 @@ const initialState = { desc: '', history: [], }, + syncStatus: { + status: false, + message: '', + }, componentIds: {}, } @@ -310,6 +314,30 @@ const mutations = { return newState }, + [TYPES.setIsEnableSync](state, isEnableSync) { + const newState = { + ...state, + setting: { + ...state.setting, + sync: { + ...state.setting.sync, + enable: isEnableSync, + }, + }, + } + return newState + }, + [TYPES.setSyncStatus](state, { status, message }) { + const newState = { + ...state, + syncStatus: { + ...state.syncStatus, + }, + } + if (status != null) newState.syncStatus.status = status + if (message != null) newState.syncStatus.message = message + return newState + }, } export default (state = initialState, action) => diff --git a/src/store/modules/list/action.js b/src/store/modules/list/action.js index 6de651e..233e07b 100644 --- a/src/store/modules/list/action.js +++ b/src/store/modules/list/action.js @@ -1,4 +1,5 @@ import { action as playerAction } from '@/store/modules/player' +import { action as commonAction } from '@/store/modules/common' import { findMusic } from '@/utils/music' import { getAllListData, @@ -9,6 +10,7 @@ import { saveListScrollPosition, saveListSort, } from '@/utils/tools' +import { list as listSync } from '@/plugins/sync' export const TYPES = { initList: null, @@ -29,6 +31,7 @@ export const TYPES = { setOtherSource: null, clearCache: null, jumpPosition: null, + setSyncList: null, } for (const key of Object.keys(TYPES)) { @@ -57,7 +60,7 @@ export const initList = listData => async(dispatch, getState) => { listPosition = listData.listPosition listSort = listData.listSort } - global.listScrollPosition = listPosition + global.listScrollPosition = listPosition || {} global.listSort = listSort let isNeedSaveSortInfo = false @@ -79,9 +82,38 @@ export const initList = listData => async(dispatch, getState) => { type: TYPES.initList, payload: { defaultList, loveList, userList }, }) + + // if (listData.isSync) { + // const keys = Object.keys(global.allList) + // dispatch(playerAction.checkPlayList(keys)) + // saveList(keys) + // } else { + // listSync.sendListAction('init_list', { defaultList, loveList, userList }) + // } } -export const setList = ({ id, list, name, location, source, sourceListId }) => async(dispatch, getState) => { +export const setSyncList = ({ defaultList, loveList, userList }) => async(dispatch, getState) => { + const state = getState() + const userListIds = userList.map(l => l.id) + const removeUserListIds = state.list.userList.filter(l => !userListIds.includes(l.id)) + if (removeUserListIds.includes(state.common.setting.list.prevSelectListId)) { + dispatch(commonAction.setPrevSelectListId(state.list.defaultList.id)) + } + dispatch({ + type: TYPES.setSyncList, + payload: { defaultList, loveList, userList }, + }) + await removeList(removeUserListIds) + + dispatch(playerAction.checkPlayList([...Object.keys(global.allList), ...removeUserListIds])) + saveList([defaultList, loveList, ...userList]) +} + +export const setList = ({ id, list, name, location, source, sourceListId, isSync }) => async(dispatch, getState) => { + if (!isSync) { + listSync.sendListAction('set_list', { id, list, name, location, source, sourceListId }) + } + const targetList = global.allList[id] if (targetList) { if (name && targetList.name === name) { @@ -89,17 +121,22 @@ export const setList = ({ id, list, name, location, source, sourceListId }) => a type: TYPES.listClear, payload: id, }) - dispatch(listAddMultiple({ id, list })) + dispatch(listAddMultiple({ id, list, isSync: true })) return } id += '_' + Math.random() } - await dispatch(createUserList({ id, list, name, location, source, sourceListId })) + await dispatch(createUserList({ id, list, name, location, source, sourceListId, isSync: true })) } -export const listAdd = ({ musicInfo, id }) => (dispatch, getState) => { - const addMusicLocationType = getState().common.setting.list.addMusicLocationType +export const listAdd = ({ musicInfo, id, addMusicLocationType, isSync }) => (dispatch, getState) => { + if (!addMusicLocationType) addMusicLocationType = getState().common.setting.list.addMusicLocationType + + if (!isSync) { + listSync.sendListAction('list_add', { id, musicInfo, addMusicLocationType }) + } + dispatch({ type: TYPES.listAdd, payload: { @@ -112,18 +149,26 @@ export const listAdd = ({ musicInfo, id }) => (dispatch, getState) => { saveList(global.allList[id]) } -export const listMove = ({ fromId, musicInfo, toId }) => (dispatch, getState) => { - const addMusicLocationType = getState().common.setting.list.addMusicLocationType +export const listMove = ({ fromId, musicInfo, toId, isSync }) => (dispatch, getState) => { + if (!isSync) { + listSync.sendListAction('list_move', { fromId, musicInfo, toId }) + } + dispatch({ type: TYPES.listMove, - payload: { fromId, musicInfo, toId, addMusicLocationType }, + payload: { fromId, musicInfo, toId }, }) - dispatch(playerAction.checkPlayList([fromId, musicInfo])) + dispatch(playerAction.checkPlayList([fromId, toId])) saveList([global.allList[fromId], global.allList[toId]]) } -export const listAddMultiple = ({ id, list }) => (dispatch, getState) => { - const addMusicLocationType = getState().common.setting.list.addMusicLocationType +export const listAddMultiple = ({ id, list, addMusicLocationType, isSync }) => (dispatch, getState) => { + if (!addMusicLocationType) addMusicLocationType = getState().common.setting.list.addMusicLocationType + + if (!isSync) { + listSync.sendListAction('list_add_multiple', { id, list, addMusicLocationType }) + } + dispatch({ type: TYPES.listAddMultiple, payload: { id, list, addMusicLocationType }, @@ -132,10 +177,14 @@ export const listAddMultiple = ({ id, list }) => (dispatch, getState) => { saveList(global.allList[id]) } -export const listMoveMultiple = ({ fromId, toId, list }) => (dispatch, getState) => { +export const listMoveMultiple = ({ fromId, toId, list, isSync }) => (dispatch, getState) => { + if (!isSync) { + listSync.sendListAction('list_move_multiple', { fromId, toId, list }) + } + dispatch({ type: TYPES.listRemoveMultiple, - payload: { id: fromId, list }, + payload: { id: fromId, ids: list.map(s => s.songmid) }, }) dispatch({ type: TYPES.listAddMultiple, @@ -145,25 +194,37 @@ export const listMoveMultiple = ({ fromId, toId, list }) => (dispatch, getState) saveList([global.allList[fromId], global.allList[toId]]) } -export const listRemove = ({ id, index }) => (dispatch, getState) => { +export const listRemove = ({ listId, id, isSync }) => (dispatch, getState) => { + if (!isSync) { + listSync.sendListAction('list_remove', { listId, id }) + } + dispatch({ type: TYPES.listRemove, - payload: { id, index }, + payload: { listId, id }, }) - dispatch(playerAction.checkPlayList([id])) - saveList(global.allList[id]) + dispatch(playerAction.checkPlayList([listId])) + saveList(global.allList[listId]) } -export const listRemoveMultiple = ({ id, list }) => (dispatch, getState) => { +export const listRemoveMultiple = ({ listId, ids, isSync }) => (dispatch, getState) => { + if (!isSync) { + listSync.sendListAction('list_remove_multiple', { listId, ids }) + } + dispatch({ type: TYPES.listRemoveMultiple, - payload: { id, list }, + payload: { listId, ids }, }) - dispatch(playerAction.checkPlayList([id])) - saveList(global.allList[id]) + dispatch(playerAction.checkPlayList([listId])) + saveList(global.allList[listId]) } -export const listClear = id => (dispatch, getState) => { +export const listClear = ({ id, isSync }) => (dispatch, getState) => { + if (!isSync) { + listSync.sendListAction('list_clear', { id }) + } + dispatch({ type: TYPES.listClear, payload: id, @@ -172,35 +233,51 @@ export const listClear = id => (dispatch, getState) => { saveList(global.allList[id]) } -export const updateMusicInfo = ({ id, index, data }) => (dispatch, getState) => { +export const updateMusicInfo = ({ listId, id, data, isSync }) => (dispatch, getState) => { + if (!isSync) { + listSync.sendListAction('update_music_info', { listId, id, data }) + } + dispatch({ type: TYPES.updateMusicInfo, - payload: { id, index, data }, + payload: { listId, id, data }, }) - saveList(global.allList[id]) + saveList(global.allList[listId]) } -export const createUserList = ({ name, id = `userlist_${Date.now()}`, list = [], source, sourceListId }) => async(dispatch, getState) => { +export const createUserList = ({ name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, isSync }) => async(dispatch, getState) => { + if (!isSync) { + listSync.sendListAction('create_user_list', { name, id, list, source, sourceListId }) + } + dispatch({ type: TYPES.createUserList, payload: { name, id, source, sourceListId }, }) - dispatch(listAddMultiple({ id, list })) + dispatch(listAddMultiple({ id, list, isSync: true })) await saveList(global.allList[id]) const state = getState() await saveListSort(id, state.list.userList.length) await saveListScrollPosition(id, 0) } -export const removeUserList = id => async(dispatch, getState) => { - const { list } = getState() +export const removeUserList = ({ id, isSync }) => async(dispatch, getState) => { + if (!isSync) { + listSync.sendListAction('remove_user_list', { id }) + } + + const { list, common } = getState() const index = list.userList.findIndex(l => l.id === id) if (index < 0) return + if (common.setting.list.prevSelectListId == id) { + dispatch(commonAction.setPrevSelectListId(list.defaultList.id)) + } dispatch({ type: TYPES.removeUserList, payload: index, }) await removeList(id) + console.log(common.setting.list.prevSelectListId, id) dispatch(playerAction.checkPlayList([id])) } @@ -220,12 +297,17 @@ export const getOtherSource = ({ musicInfo, id }) => (dispatch, getState) => { }) } -export const setUserListName = ({ id, name }) => async(dispatch, getState) => { +export const setUserListName = ({ id, name, isSync }) => async(dispatch, getState) => { + if (!isSync) { + listSync.sendListAction('set_user_list_name', { id, name }) + } + dispatch({ type: TYPES.setUserListName, payload: { id, name }, }) - await saveList(global.allList[id]) + const targetList = global.allList[id] + await saveList(targetList) } export const setUserListPosition = ({ id, position }) => async(dispatch, getState) => { dispatch({ @@ -234,12 +316,15 @@ export const setUserListPosition = ({ id, position }) => async(dispatch, getStat }) await saveList(global.allList[id]) } -export const setMusicPosition = ({ id, position, list }) => async(dispatch, getState) => { +export const setMusicPosition = ({ id, position, list, isSync }) => async(dispatch, getState) => { + if (!isSync) { + listSync.sendListAction('set_music_position', { id, position, list }) + } // const targetList = global.allList[id] // if (!targetList) return dispatch({ type: TYPES.listRemoveMultiple, - payload: { id, list }, + payload: { listId: id, ids: list.map(m => m.songmid) }, }) dispatch({ type: TYPES.setMusicPosition, diff --git a/src/store/modules/list/reducer.js b/src/store/modules/list/reducer.js index ad8c390..42dc467 100644 --- a/src/store/modules/list/reducer.js +++ b/src/store/modules/list/reducer.js @@ -3,6 +3,9 @@ import { TYPES } from './action' const allList = global.allList = {} const allListInit = (defaultList, loveList, userList) => { + for (const id of Object.keys(allList)) { + delete allList[id] + } allList[defaultList.id] = defaultList allList[loveList.id] = loveList for (const list of userList) allList[list.id] = list @@ -77,13 +80,26 @@ const mutations = { newState.loveList = { ...state.loveList, list: loveList.list, location: loveList.location } ids.push(loveList.id) } - if (userList != null) newState.userList = userList + if (userList != null) { + newState.userList = userList + for (const list of userList) { + ids.push(list.id) + } + } allListInit(newState.defaultList, newState.loveList, newState.userList) newState.isInitedList = true return updateStateList(newState, [ids]) // console.log(allList.default, newState, ids) // return newState }, + [TYPES.setSyncList](state, { defaultList, loveList, userList }) { + const newState = { ...state } + newState.defaultList = defaultList + newState.loveList = loveList + newState.userList = userList + allListInit(newState.defaultList, newState.loveList, newState.userList) + return newState + }, /* [TYPES.initList](state, { defaultList, loveList, userList }) { const newState = { ...state } if (defaultList != null) newState.defaultList = { ...state.defaultList, list: defaultList.list, location: defaultList.location } @@ -154,36 +170,44 @@ const mutations = { const targetList = allList[id] if (!targetList) return state let newList + const map = {} + const ids = [] switch (addMusicLocationType) { case 'top': newList = [...list, ...targetList.list] + for (let i = newList.length - 1; i > -1; i--) { + const item = newList[i] + if (map[item.songmid]) continue + ids.unshift(item.songmid) + map[item.songmid] = item + } break case 'bottom': default: newList = [...targetList.list, ...list] + for (const item of newList) { + if (map[item.songmid]) continue + ids.push(item.songmid) + map[item.songmid] = item + } break } - const map = {} - const ids = [] - for (const item of newList) { - if (map[item.songmid]) continue - ids.push(item.songmid) - map[item.songmid] = item - } targetList.list = ids.map(id => map[id]) return updateStateList({ ...state }, [id]) }, - [TYPES.listRemove](state, { id, index }) { - const targetList = allList[id] + [TYPES.listRemove](state, { listId, id }) { + const targetList = allList[listId] // console.log(targetList, id, index) if (!targetList) return state + const index = targetList.list.findIndex(item => item.songmid == id) + if (index < 0) return state const newTargetList = [...targetList.list] newTargetList.splice(index, 1) targetList.list = newTargetList - return updateStateList({ ...state }, [id]) + return updateStateList({ ...state }, [listId]) }, - [TYPES.listRemoveMultiple](state, { id, list }) { - const targetList = allList[id] + [TYPES.listRemoveMultiple](state, { listId, ids: musicIds }) { + const targetList = allList[listId] if (!targetList) return state const map = {} const ids = [] @@ -191,14 +215,14 @@ const mutations = { ids.push(item.songmid) map[item.songmid] = item } - for (const item of list) { - if (map[item.songmid]) delete map[item.songmid] + for (const songmid of musicIds) { + if (map[songmid]) delete map[songmid] } const newList = [] for (const id of ids) if (map[id]) newList.push(map[id]) targetList.list = newList - return updateStateList({ ...state }, [id]) + return updateStateList({ ...state }, [listId]) }, [TYPES.listClear](state, id) { const targetList = allList[id] @@ -206,13 +230,15 @@ const mutations = { targetList.list = [] return updateStateList({ ...state }, [id]) }, - [TYPES.updateMusicInfo](state, { id, index, data }) { - const targetList = allList[id] + [TYPES.updateMusicInfo](state, { listId, id, data }) { + const targetList = allList[listId] if (!targetList) return state + const targetMusicInfo = targetList.list.find(item => item.songmid == id) + if (!targetMusicInfo) return state const newTargetList = [...targetList.list] - Object.assign(newTargetList[index], data) + Object.assign(targetMusicInfo, data) targetList.list = newTargetList - return updateStateList({ ...state }, [id]) + return updateStateList({ ...state }, [listId]) }, [TYPES.createUserList](state, { name, id, source, sourceListId }) { diff --git a/src/store/modules/player/reducer.js b/src/store/modules/player/reducer.js index 3509589..ba41412 100644 --- a/src/store/modules/player/reducer.js +++ b/src/store/modules/player/reducer.js @@ -46,7 +46,9 @@ const mutations = { [TYPES.setList](state, list) { return { ...state, - listInfo: list, + listInfo: { + ...list, + }, } }, [TYPES.setPlayIndex](state, index) { diff --git a/src/utils/tools.js b/src/utils/tools.js index 3642e86..abf633b 100644 --- a/src/utils/tools.js +++ b/src/utils/tools.js @@ -6,6 +6,8 @@ import { throttle } from './index' const playInfoStorageKey = storageDataPrefix.playInfo const listPositionPrefix = storageDataPrefix.listPosition +const syncAuthKeyPrefix = storageDataPrefix.syncAuthKey +const syncHostPrefix = storageDataPrefix.syncHost const listPrefix = storageDataPrefix.list const listSortPrefix = storageDataPrefix.listSort const defaultListKey = listPrefix + 'default' @@ -209,6 +211,35 @@ export const removeListScrollPosition = async listIds => { handleSaveListScrollPosition(global.listScrollPosition) } +export const getSyncAuthKey = async serverId => { + const keys = await getData(syncAuthKeyPrefix) + if (!keys) return null + return keys[serverId] || null +} + +export const setSyncAuthKey = async(serverId, key) => { + let keys = await getData(syncAuthKeyPrefix) || {} + keys[serverId] = key + await setData(syncAuthKeyPrefix, keys) +} + +let syncHostInfo +export const getSyncHost = async() => { + if (syncHostInfo === undefined) { + syncHostInfo = await getData(syncHostPrefix) || { host: '', port: '' } + } + return { ...syncHostInfo } +} + +export const setSyncHost = async({ host, port }) => { + // let hostInfo = await getData(syncHostPrefix) || {} + // hostInfo.host = host + // hostInfo.port = port + syncHostInfo.host = host + syncHostInfo.port = port + await setData(syncHostPrefix, syncHostInfo) +} + export const exitApp = BackHandler.exitApp export { diff --git a/src/utils/utils.js b/src/utils/utils.js index de77fbd..39736b8 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -20,3 +20,9 @@ export const screenUnkeepAwake = () => { global.isScreenKeepAwake = false UtilsModule.screenUnkeepAwake() } + +export const getWIFIIPV4Address = UtilsModule.getWIFIIPV4Address + +export const getDeviceName = () => { + return UtilsModule.getDeviceName().then(deviceName => deviceName || 'Unknown') +}