完善同步功能

This commit is contained in:
lyswhut 2021-08-01 12:21:22 +08:00
parent 1fe5656eda
commit 5933825f9b
38 changed files with 1255 additions and 67 deletions

View File

@ -9,6 +9,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<application
android:name=".MainApplication"
android:usesCleartextTraffic="true"

View File

@ -1,8 +1,11 @@
package com.lxmusicmobile.utils;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.util.Log;
import android.view.WindowManager;
@ -128,5 +131,51 @@ public class UtilsModule extends ReactContextBaseJavaModule {
});
}
}
/**
Gets the device's WiFi interface IP address
@return device's WiFi IP if connected to WiFi, else '0.0.0.0'
*/
@ReactMethod
public void getWIFIIPV4Address(final Promise promise) throws Exception {
// https://github.com/pusherman/react-native-network-info/blob/master/android/src/main/java/com/pusherman/networkinfo/RNNetworkInfo.java
WifiManager wifi = (WifiManager) reactContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
new Thread(new Runnable() {
public void run() {
try {
WifiInfo info = wifi.getConnectionInfo();
int ipAddress = info.getIpAddress();
String stringip = String.format("%d.%d.%d.%d", (ipAddress & 0xff), (ipAddress >> 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);
}
}
}

View File

@ -1,6 +1,7 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: [
'@babel/plugin-proposal-export-namespace-from',
[
'module-resolver',
{

View File

@ -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

169
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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'))
}

View File

@ -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'

View File

@ -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,

View File

@ -17,3 +17,7 @@ global.isScreenKeepAwake = false
// 是否播放完后退出应用
global.isPlayedExit = false
global.syncKeyInfo = {}
global.isSyncEnableing = false

View File

@ -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

View File

@ -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 Xues 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",

View File

@ -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": "绿意盎然",

View File

@ -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)
}

View File

@ -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
}

View File

@ -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...',
}

View File

@ -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,
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -0,0 +1,2 @@
export * from './modules'
export { connect, disconnect, SYNC_CODE } from './client'

View File

@ -0,0 +1 @@
export * as list from './list'

View File

@ -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'

View File

@ -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
}

View File

@ -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
}

View File

@ -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(() => {

View File

@ -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:

View File

@ -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 (
<InputItem
editable={!disabled}
value={hostAddress}
label={t('setting_sync_host_label')}
onChange={setHostAddress}
keyboardType="number-pad"
placeholder={t('setting_sync_host_tip')} />
)
})
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 (
<InputItem
editable={!disabled}
value={portNum}
label={t('setting_sync_port_label')}
onChange={setPortAddress}
keyboardType="number-pad"
placeholder={t('setting_sync_port_tip')} />
)
})
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 (
<>
<View style={{ marginTop: 5 }}>
<CheckBoxItem disabled={isWaiting || !port || !host} check={isEnableSync} label={t('setting_sync_enbale')} onChange={handleSetEnableSync} />
<Text style={{ color: theme.normal, marginLeft: 25, marginTop: 10, fontSize: 12 }}>{t('setting_sync_address', { address })}</Text>
<Text style={{ color: theme.normal, marginLeft: 25, fontSize: 12 }}>{t('setting_sync_status', { status })}</Text>
</View>
<View style={{ marginTop: 10 }} >
<HostInput setHost={setHost} host={host} disabled={isWaiting || isEnableSync} />
<PortInput setPort={setPort} port={port} disabled={isWaiting || isEnableSync} />
</View>
<ConfirmAlert
visible={visibleCodeModal}
onHide={handleCancelSetCode}
onConfirm={handleSetCode}
>
<View style={styles.authCodeContent}>
<Text style={{ color: theme.normal, marginBottom: 5 }}>{t('setting_sync_code_label')}</Text>
<Input
placeholder={t('setting_sync_code_input_tip')}
value={authCode}
onChangeText={setAuthCode}
style={{ ...styles.authCodeInput, backgroundColor: theme.secondary40 }}
/>
</View>
</ConfirmAlert>
</>
)
})
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,
// },
})

View File

@ -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 (
<Section title={t('setting_sync')}>
<IsEnable />
{/* <SyncHost /> */}
</Section>
)
})

View File

@ -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 (
<View style={styles.container}>
<Text style={{ ...styles.label, color: theme.normal }}>{label}</Text>
@ -49,5 +52,6 @@ const styles = StyleSheet.create({
paddingTop: 2,
paddingBottom: 2,
fontSize: 12,
maxWidth: 300,
},
})

View File

@ -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 () => {
<Basic />
<Player />
<List />
<Sync />
<Backup />
<Other />
<Version />

View File

@ -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,
})
}

View File

@ -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,

View File

@ -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) =>

View File

@ -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,

View File

@ -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 }) {

View File

@ -46,7 +46,9 @@ const mutations = {
[TYPES.setList](state, list) {
return {
...state,
listInfo: list,
listInfo: {
...list,
},
}
},
[TYPES.setPlayIndex](state, index) {

View File

@ -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 {

View File

@ -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')
}