优化同步逻辑

This commit is contained in:
lyswhut 2023-08-14 15:32:22 +08:00
parent c80d388dc0
commit acb756710a
25 changed files with 1861 additions and 748 deletions

View File

@ -31,6 +31,8 @@
目前本项目的原始发布地址只有**GitHub**及**蓝奏网盘**,其他渠道均为第三方转载发布,与本项目无关!
为了提高使用门槛本软件内的默认设置、UI操作不以新手友好为目标所以使用前建议先根据你的喜好浏览调整一遍软件设置阅读一遍[音乐播放列表机制](https://lyswhut.github.io/lx-music-doc/desktop/faq/playlist)
#### 数据同步服务
从v1.0.0起,我们发布了一个独立版的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme),如果你有服务器,可以将其部署到服务器上作为私人多端同步服务使用,详情看该项目说明

View File

@ -18,10 +18,10 @@
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
tools:targetApi="n">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"

View File

@ -1,5 +1,7 @@
package cn.toside.music.mobile.gzip;
import android.util.Base64;
import com.facebook.common.internal.Throwables;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
@ -18,6 +20,7 @@ import cn.toside.music.mobile.utils.TaskRunner;
// https://github.com/FWC1994/react-native-gzip/blob/main/android/src/main/java/com/reactlibrary/GzipModule.java
// https://www.digitalocean.com/community/tutorials/java-gzip-example-compress-decompress-file
// https://github.com/ammarahm-ed/react-native-gzip/blob/master/android/src/main/java/com/gzip/GzipModule.java
public class GzipModule extends ReactContextBaseJavaModule {
private final ReactApplicationContext reactContext;
@ -31,104 +34,31 @@ public class GzipModule extends ReactContextBaseJavaModule {
return "GzipModule";
}
static class UnGzip implements Callable<String> {
private final String source;
private final String target;
private final Boolean force;
public UnGzip(String source, String target, Boolean force) {
this.source = source;
this.target = target;
this.force = force;
}
@Override
public String call() {
// Log.d("Gzip", "source: " + source + ", target: " + target);
File sourceFile = new File(source);
File targetFile = new File(target);
if(!Utils.checkDir(sourceFile, targetFile, force)){
return "error";
}
FileInputStream fileInputStream;
FileOutputStream fileOutputStream;
@ReactMethod
public void unGzipFromBase64(String base64, Promise promise) {
TaskRunner taskRunner = new TaskRunner();
try {
fileInputStream = new FileInputStream(sourceFile);
fileOutputStream = new FileOutputStream(targetFile);
final GZIPInputStream gzipInputStream = new GZIPInputStream(fileInputStream);
final byte[] buffer = new byte[4096];
int len;
while((len = gzipInputStream.read(buffer)) != -1){
fileOutputStream.write(buffer, 0, len);
}
//close resources
fileOutputStream.close();
gzipInputStream.close();
return "";
} catch (IOException e) {
e.printStackTrace();
return "unGzip error: " + Throwables.getStackTraceAsString(e);
}
}
}
static class Gzip implements Callable<String> {
private final String source;
private final String target;
private final Boolean force;
public Gzip(String source, String target, Boolean force) {
this.source = source;
this.target = target;
this.force = force;
}
@Override
public String call() {
// Log.d("Gzip", "source: " + source + ", target: " + target);
File sourceFile = new File(source);
File targetFile = new File(target);
// Log.d("Gzip", "sourceFile: " + sourceFile.getAbsolutePath() + ", targetFile: " + targetFile.getAbsolutePath());
if(!Utils.checkFile(sourceFile, targetFile, force)){
return "error";
}
FileInputStream fileInputStream;
FileOutputStream fileOutputStream;
try{
fileInputStream = new FileInputStream(sourceFile);
fileOutputStream = new FileOutputStream(targetFile);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(fileOutputStream);
final byte[] buffer = new byte[4096];
int len;
while((len= fileInputStream.read(buffer)) != -1){
gzipOutputStream.write(buffer, 0, len);
}
//close resources
gzipOutputStream.close();
gzipOutputStream.close();
fileInputStream.close();
return "";
} catch (IOException e) {
e.printStackTrace();
return "gzip error: " + source.length() + "\nstack: " + Throwables.getStackTraceAsString(e);
}
taskRunner.executeAsync(new Utils.UnGzip(base64), promise::resolve);
} catch (RuntimeException err) {
promise.reject("-2", err.getMessage());
}
}
@ReactMethod
public void unGzip(String source, String target, Boolean force, Promise promise) {
public void gzipStringToBase64(String data, Promise promise) {
TaskRunner taskRunner = new TaskRunner();
try {
taskRunner.executeAsync(new UnGzip(source, target, force), (String errMessage) -> {
taskRunner.executeAsync(new Utils.Gzip(data), promise::resolve);
} catch (RuntimeException err) {
promise.reject("-2", err.getMessage());
}
}
@ReactMethod
public void unGzipFile(String source, String target, Boolean force, Promise promise) {
TaskRunner taskRunner = new TaskRunner();
try {
taskRunner.executeAsync(new Utils.UnGzipFile(source, target, force), (String errMessage) -> {
if ("".equals(errMessage)) {
promise.resolve(null);
} else promise.reject("-2", errMessage);
@ -139,10 +69,10 @@ public class GzipModule extends ReactContextBaseJavaModule {
}
@ReactMethod
public void gzip(String source, String target, Boolean force, Promise promise) {
public void gzipFile(String source, String target, Boolean force, Promise promise) {
TaskRunner taskRunner = new TaskRunner();
try {
taskRunner.executeAsync(new Gzip(source, target, force), (String errMessage) -> {
taskRunner.executeAsync(new Utils.GzipFile(source, target, force), (String errMessage) -> {
if ("".equals(errMessage)) {
promise.resolve(null);
} else promise.reject("-2", errMessage);

View File

@ -3,7 +3,23 @@ package cn.toside.music.mobile.gzip;
import static cn.toside.music.mobile.utils.Utils.deletePath;
import android.util.Base64;
import android.util.Log;
import com.facebook.common.internal.Throwables;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Callable;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
// https://github.com/FWC1994/react-native-gzip/blob/main/android/src/main/java/com/reactlibrary/GzipModule.java
public class Utils {
@ -37,4 +53,143 @@ public class Utils {
}
return true;
}
static class UnGzipFile implements Callable<String> {
private final String source;
private final String target;
private final Boolean force;
public UnGzipFile(String source, String target, Boolean force) {
this.source = source;
this.target = target;
this.force = force;
}
@Override
public String call() {
// Log.d("Gzip", "source: " + source + ", target: " + target);
File sourceFile = new File(source);
File targetFile = new File(target);
if(!Utils.checkDir(sourceFile, targetFile, force)){
return "error";
}
FileInputStream fileInputStream;
FileOutputStream fileOutputStream;
try{
fileInputStream = new FileInputStream(sourceFile);
fileOutputStream = new FileOutputStream(targetFile);
final GZIPInputStream gzipInputStream = new GZIPInputStream(fileInputStream);
final byte[] buffer = new byte[4096];
int len;
while((len = gzipInputStream.read(buffer)) != -1){
fileOutputStream.write(buffer, 0, len);
}
//close resources
fileOutputStream.close();
gzipInputStream.close();
fileInputStream.close();
return "";
} catch (IOException e) {
e.printStackTrace();
return "unGzip error: " + Throwables.getStackTraceAsString(e);
}
}
}
static class GzipFile implements Callable<String> {
private final String source;
private final String target;
private final Boolean force;
public GzipFile(String source, String target, Boolean force) {
this.source = source;
this.target = target;
this.force = force;
}
@Override
public String call() {
// Log.d("Gzip", "source: " + source + ", target: " + target);
File sourceFile = new File(source);
File targetFile = new File(target);
// Log.d("Gzip", "sourceFile: " + sourceFile.getAbsolutePath() + ", targetFile: " + targetFile.getAbsolutePath());
if(!Utils.checkFile(sourceFile, targetFile, force)){
return "error";
}
FileInputStream fileInputStream;
FileOutputStream fileOutputStream;
try{
fileInputStream = new FileInputStream(sourceFile);
fileOutputStream = new FileOutputStream(targetFile);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(fileOutputStream);
final byte[] buffer = new byte[4096];
int len;
while((len= fileInputStream.read(buffer)) != -1){
gzipOutputStream.write(buffer, 0, len);
}
//close resources
gzipOutputStream.close();
fileInputStream.close();
fileOutputStream.close();
return "";
} catch (IOException e) {
e.printStackTrace();
return "gzip error: " + source.length() + "\nstack: " + Throwables.getStackTraceAsString(e);
}
}
}
static class UnGzip implements Callable<String> {
private final byte[] data;
public UnGzip(String data) {
this.data = Base64.decode(data, Base64.DEFAULT);
}
@Override
public String call() throws IOException {
// Log.d("Gzip", "source: " + source + ", target: " + target);
final int BUFFER_SIZE = 1024;
ByteArrayInputStream is = new ByteArrayInputStream(data);
GZIPInputStream gis = new GZIPInputStream(is, BUFFER_SIZE);
BufferedReader bf = new BufferedReader(new InputStreamReader(gis, StandardCharsets.UTF_8));
final StringBuilder outStr = new StringBuilder();
String line;
while ((line = bf.readLine()) != null) {
outStr.append(line);
}
gis.close();
is.close();
bf.close();
return outStr.toString();
}
}
static class Gzip implements Callable<String> {
private final String data;
public Gzip(String data) {
this.data = data;
}
@Override
public String call() throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream(data.length());
GZIPOutputStream gos = new GZIPOutputStream(os);
gos.write(data.getBytes(StandardCharsets.UTF_8));
gos.close();
byte[] compressed = os.toByteArray();
os.close();
return Base64.encodeToString(compressed, Base64.NO_WRAP);
}
}
}

1736
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "lx-music-mobile",
"version": "1.0.7-beta.9",
"version": "1.0.7-beta.10",
"versionCode": 60,
"private": true,
"scripts": {
@ -43,11 +43,12 @@
"homepage": "https://github.com/lyswhut/lx-music-mobile#readme",
"dependencies": {
"@craftzdog/react-native-buffer": "^6.0.5",
"@react-native-async-storage/async-storage": "^1.19.1",
"@react-native-async-storage/async-storage": "^1.19.2",
"@react-native-clipboard/clipboard": "^1.11.2",
"@react-native-community/slider": "^4.4.2",
"iconv-lite": "^0.6.3",
"lrc-file-parser": "^2.4.1",
"message2call": "^0.1.0",
"pako": "^2.1.0",
"react": "18.2.0",
"react-native": "0.72.3",
@ -62,13 +63,13 @@
"react-native-vector-icons": "^10.0.0"
},
"devDependencies": {
"@babel/core": "^7.22.9",
"@babel/core": "^7.22.10",
"@babel/plugin-proposal-export-namespace-from": "^7.18.9",
"@babel/preset-env": "^7.22.9",
"@babel/runtime": "^7.22.6",
"@react-native/metro-config": "^0.72.9",
"@babel/preset-env": "^7.22.10",
"@babel/runtime": "^7.22.10",
"@react-native/metro-config": "^0.72.11",
"@tsconfig/react-native": "^3.0.2",
"@types/react": "^18.2.18",
"@types/react": "^18.2.20",
"@types/react-native": "^0.70.14",
"@types/react-native-background-timer": "^2.0.0",
"@types/react-native-vector-icons": "^6.4.13",
@ -77,7 +78,7 @@
"eslint-config-standard-with-typescript": "^37.0.0",
"eslint-plugin-react": "^7.33.1",
"eslint-plugin-react-hooks": "^4.6.0",
"metro-react-native-babel-preset": "0.76.7",
"metro-react-native-babel-preset": "0.76.8",
"typescript": "^5.1.6"
}
}

View File

@ -4,11 +4,16 @@
若在升级新版本时提示签名不一致,则表明你手机上的旧版本或者将要安装的新版本中有一方是第三方修改版。
若在升级新版本时提示无法降级安装则表明你使用的是universal通用版安装包安装包大小20M+尝试使用arm64-v8a版安装包或者到GitHub下载其他版本安装包。
### 不兼容性变更
该版本修改了同步协议逻辑需要PC端v2.4.0或移动端v1.7.0版本才能连接使用。
### 优化
- 优化歌单列表歌单封面大小计算方式
- 调整竖屏下的排行榜布局
- 调整桌面歌词主题配色,增强歌词字体阴影(#276
- 优化数据传输逻辑,列表同步指令使用队列机制,保证列表同步操作的顺序
### 修复
@ -19,6 +24,7 @@
- 修复wy源热搜词失效的问题@Folltoshe
- 修复mg歌单搜索歌单播放数量显示问题
- 修复搜索提示功能失效的问题(@Folltoshe
- 修复潜在导致列表数据不同步的问题
### 其他

View File

@ -64,3 +64,20 @@ initNavigation(async() => {
})
})
// const createProxy = () => {
// return new Proxy(function() {}, {
// get: (_target, prop, receiver) => {
// let propName = prop.toString()
// console.log('proxy get', propName)
// return createProxy()
// },
// // eslint-disable-next-line @typescript-eslint/promise-function-async
// apply: (target, thisArg, argumentsList) => {
// console.log('proxy apply')
// return '56'
// },
// })
// }
// const proxy = createProxy()
// console.log(proxy.aaa())

View File

@ -137,7 +137,7 @@ export const DEFAULT_SETTING = {
}
export const SYNC_CODE = {
helloMsg: 'Hello~::^-^::~v3~',
helloMsg: 'Hello~::^-^::~v4~',
idPrefix: 'OjppZDo6',
authMsg: 'lx-music auth::',
authFailed: 'Auth failed',

View File

@ -1,4 +1,4 @@
import TrackPlayer, { Capability, Event, State } from 'react-native-track-player'
import TrackPlayer, { Capability, Event, RepeatMode, State } from 'react-native-track-player'
import BackgroundTimer from 'react-native-background-timer'
import { playMusic as handlePlayMusic } from './playList'
// import { PlayerMusicInfo } from '@/store/modules/player/playInfo'
@ -159,6 +159,8 @@ export const setStop = async() => {
await TrackPlayer.stop()
if (!isEmpty()) await TrackPlayer.skipToNext()
}
export const setLoop = async(loop: boolean) => TrackPlayer.setRepeatMode(loop ? RepeatMode.Off : RepeatMode.Track)
export const setPause = async() => TrackPlayer.pause()
// export const skipToNext = () => TrackPlayer.skipToNext()
export const setCurrentTime = async(time: number) => TrackPlayer.seekTo(time)

View File

@ -4,6 +4,7 @@ import { SYNC_CODE } from '@/config/constant'
import log from '../log'
import { aesDecrypt, aesEncrypt, rsaDecrypt } from '../utils'
import { getDeviceName } from '@/utils/nativeModules/utils'
import { toMD5 } from '@/utils/tools'
const hello = async(urlInfo: LX.Sync.UrlInfo) => request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/hello`)
.then(({ text }) => text == SYNC_CODE.helloMsg)
@ -25,7 +26,7 @@ const getServerId = async(urlInfo: LX.Sync.UrlInfo) => request(`${urlInfo.httpPr
})
const codeAuth = async(urlInfo: LX.Sync.UrlInfo, serverId: string, authCode: string) => {
let key = ''.padStart(16, Buffer.from(authCode).toString('hex'))
let key = toMD5(authCode).substring(0, 16)
// const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
key = Buffer.from(key).toString('base64')
let { publicKey, privateKey } = await generateRsaKey()

View File

@ -1,13 +1,15 @@
import { encryptMsg, decryptMsg } from './utils'
import * as modules from './modules'
import { modules, callObj } from './modules'
// import { action as commonAction } from '@/store/modules/common'
// import { getStore } from '@/store'
import registerSyncListHandler from './syncList'
// import registerSyncListHandler from './syncList'
import log from '../log'
import { SYNC_CLOSE_CODE, SYNC_CODE } from '@/config/constant'
import { aesEncrypt } from '../utils'
import { setSyncStatus } from '@/core/sync'
import { dateFormat } from '@/utils/common'
import { createMsg2call } from 'message2call'
import { toast } from '@/utils/tools'
let status: LX.Sync.Status = {
status: false,
@ -26,8 +28,14 @@ export const sendSyncMessage = (message: string) => {
}
const handleConnection = (socket: LX.Sync.Socket) => {
for (const moduleInit of Object.values(modules)) {
moduleInit(socket)
for (const { registerEvent } of Object.values(modules)) {
registerEvent(socket)
}
}
const handleDisconnection = () => {
for (const { unregisterEvent } of Object.values(modules)) {
unregisterEvent()
}
}
@ -118,47 +126,75 @@ const heartbeatTools = {
let client: LX.Sync.Socket | null
// let listSyncPromise: Promise<void>
export const connect = (urlInfo: LX.Sync.UrlInfo, keyInfo: LX.Sync.KeyInfo) => {
client = new WebSocket(`${urlInfo.wsProtocol}//${urlInfo.hostPath}?i=${encodeURIComponent(keyInfo.clientId)}&t=${encodeURIComponent(aesEncrypt(SYNC_CODE.msgConnect, keyInfo.key))}`) as LX.Sync.Socket
client = new WebSocket(`${urlInfo.wsProtocol}//${urlInfo.hostPath}/socket?i=${encodeURIComponent(keyInfo.clientId)}&t=${encodeURIComponent(aesEncrypt(SYNC_CODE.msgConnect, keyInfo.key))}`) as LX.Sync.Socket
client.data = {
keyInfo,
urlInfo,
}
heartbeatTools.connect(client)
// listSyncPromise = registerSyncListHandler(socket)
let events: Partial<{ [K in keyof LX.Sync.ActionSyncSendType]: Array<(data: LX.Sync.ActionSyncSendType[K]) => (void | Promise<void>)> }> = {}
let closeEvents: Array<(err: Error) => (void | Promise<void>)> = []
const message2read = createMsg2call({
funcsObj: {
...callObj,
list_sync_finished() {
log.info('sync list success')
toast('Sync successfully')
client!.isReady = true
handleConnection(client as LX.Sync.Socket)
sendSyncStatus({
status: true,
message: '',
})
heartbeatTools.failedNum = 0
},
},
timeout: 120 * 1000,
sendMessage(data) {
void encryptMsg(keyInfo, JSON.stringify(data)).then((data) => {
client?.send(data)
}).catch((err) => {
log.error('encrypt msg error: ', err)
client?.close(SYNC_CLOSE_CODE.failed)
})
},
onCallBeforeParams(rawArgs) {
return [client, ...rawArgs]
},
onError(error, path, groupName) {
const name = groupName ?? ''
log.error(`sync call ${name} ${path.join('.')} error:`, error)
if (groupName == null) return
client?.close(SYNC_CLOSE_CODE.failed)
sendSyncStatus({
status: false,
message: error.message,
})
},
})
client.remoteSyncList = message2read.createSyncRemote('list')
client.addEventListener('message', ({ data }) => {
if (data == 'ping') return
if (typeof data === 'string') {
let syncData: LX.Sync.ActionSync
void decryptMsg(keyInfo, data).then((data) => {
let syncData: LX.Sync.ServerActions
try {
syncData = JSON.parse(decryptMsg(keyInfo, data))
} catch {
syncData = JSON.parse(data)
} catch (err) {
log.error('parse msg error: ', err)
client?.close(SYNC_CLOSE_CODE.failed)
return
}
const handlers = events[syncData.action]
if (handlers) {
// @ts-expect-error
for (const handler of handlers) void handler(syncData.data)
}
message2read.onMessage(syncData)
}).catch((error) => {
log.error('decrypt msg error: ', error)
client?.close(SYNC_CLOSE_CODE.failed)
})
}
})
client.onRemoteEvent = function(eventName, handler) {
let eventArr = events[eventName]
if (!eventArr) events[eventName] = eventArr = []
// let eventArr = events.get(eventName)
// if (!eventArr) events.set(eventName, eventArr = [])
eventArr.push(handler)
return () => {
eventArr!.splice(eventArr!.indexOf(handler), 1)
}
}
client.sendData = function(eventName, data, callback) {
client?.send(encryptMsg(keyInfo, JSON.stringify({ action: eventName, data })))
callback?.()
}
client.onClose = function(handler: typeof closeEvents[number]) {
closeEvents.push(handler)
return () => {
@ -166,6 +202,7 @@ export const connect = (urlInfo: LX.Sync.UrlInfo, keyInfo: LX.Sync.KeyInfo) => {
}
}
const initMessage = 'Wait syncing...'
client.addEventListener('open', () => {
log.info('connect')
// const store = getStore()
@ -173,39 +210,14 @@ export const connect = (urlInfo: LX.Sync.UrlInfo, keyInfo: LX.Sync.KeyInfo) => {
client!.isReady = false
sendSyncStatus({
status: false,
message: 'Wait syncing...',
})
void registerSyncListHandler(client as LX.Sync.Socket).then(() => {
log.info('sync list success')
handleConnection(client as LX.Sync.Socket)
log.info('register list sync service success')
client!.isReady = true
sendSyncStatus({
status: true,
message: '',
})
heartbeatTools.failedNum = 0
}).catch(err => {
if (err.message == 'closed') {
sendSyncStatus({
status: false,
message: '',
})
} else {
console.log(err)
log.r_error(err.stack)
sendSyncStatus({
status: false,
message: err.message,
})
}
message: initMessage,
})
})
client.addEventListener('close', ({ code }) => {
const err = new Error('closed')
for (const handler of closeEvents) void handler(err)
handleDisconnection()
closeEvents = []
events = {}
switch (code) {
case SYNC_CLOSE_CODE.normal:
// case SYNC_CLOSE_CODE.failed:
@ -213,6 +225,15 @@ export const connect = (urlInfo: LX.Sync.UrlInfo, keyInfo: LX.Sync.KeyInfo) => {
status: false,
message: '',
})
break
case SYNC_CLOSE_CODE.failed:
if (!status.message || status.message == initMessage) {
sendSyncStatus({
status: false,
message: 'failed',
})
}
break
}
})
client.addEventListener('error', ({ message }) => {

View File

@ -1 +1,10 @@
export { default as list } from './list'
import * as list from './list'
// export * as theme from './theme'
export const callObj = Object.assign({}, list.handler)
export const modules = {
list,
}

View File

@ -0,0 +1,95 @@
import { handleRemoteListAction, getLocalListData, setLocalListData } from '../../../utils'
import log from '../../../log'
// import { SYNC_CLOSE_CODE } from '@/config/constant'
import { removeSyncModeEvent, selectSyncMode } from '@/core/sync'
import { toMD5 } from '@/utils/tools'
const logInfo = (eventName: string, success = false) => {
log.info(`[${eventName}]${eventName.replace('list:sync:list_sync_', '').replace(/_/g, ' ')}${success ? ' success' : ''}`)
}
// const logError = (eventName: string, err: Error) => {
// log.error(`[${eventName}]${eventName.replace('list:sync:list_sync_', '').replace(/_/g, ' ')} error: ${err.message}`)
// }
export const onListSyncAction = async(socket: LX.Sync.Socket, action: LX.Sync.ActionList) => {
if (!socket.isReady) return
await handleRemoteListAction(action)
}
export const list_sync_get_md5 = async(socket: LX.Sync.Socket) => {
logInfo('list:sync:list_sync_get_md5')
return toMD5(JSON.stringify(await getLocalListData()))
}
export const list_sync_get_sync_mode = async(socket: LX.Sync.Socket) => {
logInfo('list:sync:list_sync_get_sync_mode')
const unsubscribe = socket.onClose(() => {
removeSyncModeEvent()
})
return selectSyncMode(socket.data.keyInfo.serverName).finally(unsubscribe)
}
export const list_sync_get_list_data = async(socket: LX.Sync.Socket) => {
logInfo('list:sync:list_sync_get_list_data')
return getLocalListData()
}
export const list_sync_set_list_data = async(socket: LX.Sync.Socket, data: LX.Sync.ListData) => {
logInfo('list:sync:list_sync_set_list_data')
await setLocalListData(data)
}
// export default async(socket: LX.Sync.Socket) => new Promise<void>((resolve, reject) => {
// let listenEvents: Array<() => void> = []
// const unregisterEvents = () => {
// while (listenEvents.length) listenEvents.shift()?.()
// }
// socket.onClose(() => {
// unregisterEvents()
// removeSyncModeEvent()
// reject(new Error('closed'))
// })
// listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_list_data', async() => {
// logInfo('list:sync:list_sync_get_list_data')
// socket?.sendData('list:sync:list_sync_get_list_data', await getLocalListData(), (err) => {
// if (err) {
// logError('list:sync:list_sync_get_list_data', err)
// socket.close(SYNC_CLOSE_CODE.failed)
// return
// }
// logInfo('list:sync:list_sync_get_list_data', true)
// })
// }))
// listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_sync_mode', async() => {
// logInfo('list:sync:list_sync_get_sync_mode')
// let mode: LX.Sync.Mode
// try {
// mode = await selectSyncMode(socket.data.keyInfo.serverName)
// } catch (err: unknown) {
// logError('list:sync:list_sync_get_sync_mode', err as Error)
// socket.close(SYNC_CLOSE_CODE.normal)
// return
// }
// socket?.sendData('list:sync:list_sync_get_sync_mode', mode, (err) => {
// if (err) {
// logError('list:sync:list_sync_get_sync_mode', err)
// socket.close(SYNC_CLOSE_CODE.failed)
// return
// }
// logInfo('list:sync:list_sync_get_sync_mode', true)
// })
// }))
// listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_set_data', async(data) => {
// logInfo('list:sync:list_sync_set_data')
// await setLocalListData(data)
// logInfo('list:sync:list_sync_set_data', true)
// }))
// listenEvents.push(socket.onRemoteEvent('list:sync:finished', async() => {
// unregisterEvents()
// resolve()
// logInfo('list:sync:finished', true)
// toast('Sync successfully')
// }))
// })

View File

@ -1,7 +1,4 @@
import initOn from './on'
import initSend from './send'
export default (socket: LX.Sync.Socket) => {
initOn(socket)
initSend(socket)
}
export * as handler from './handler'
export * from './localEvent'

View File

@ -0,0 +1,20 @@
import { registerListActionEvent } from '../../../utils'
let unregisterLocalListAction: (() => void) | null
export const registerEvent = (socket: LX.Sync.Socket) => {
// socket = _socket
// socket.onClose(() => {
// unregisterLocalListAction?.()
// unregisterLocalListAction = null
// })
unregisterLocalListAction = registerListActionEvent((action) => {
if (!socket?.isReady) return
void socket.remoteSyncList.onListSyncAction(action)
})
}
export const unregisterEvent = () => {
unregisterLocalListAction?.()
unregisterLocalListAction = null
}

View File

@ -1,9 +0,0 @@
import { handleRemoteListAction } from '../../../utils'
export default (socket: LX.Sync.Socket) => {
socket.onRemoteEvent('list:sync:action', (action) => {
if (!socket.isReady) return
void handleRemoteListAction(action)
})
}

View File

@ -1,21 +0,0 @@
import { registerListActionEvent } from '../../../utils'
let socket: LX.Sync.Socket | null
let unregisterLocalListAction: (() => void) | null
const sendListAction = (action: LX.Sync.ActionList) => {
// console.log('sendListAction', action.action)
if (!socket?.isReady) return
socket.sendData('list:sync:action', action)
}
export default (_socket: LX.Sync.Socket) => {
socket = _socket
socket.onClose(() => {
socket = null
unregisterLocalListAction?.()
unregisterLocalListAction = null
})
unregisterLocalListAction = registerListActionEvent(sendListAction)
}

View File

@ -1,79 +0,0 @@
import log from '../log'
import { getLocalListData, setLocalListData } from '../utils'
// import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
import { SYNC_CLOSE_CODE } from '@/config/constant'
import { removeSyncModeEvent, selectSyncMode } from '@/core/sync'
import { toast, toMD5 } from '@/utils/tools'
const logInfo = (eventName: keyof LX.Sync.ActionSyncSendType, success = false) => {
log.info(`[${eventName as string}]${eventName.replace('list:sync:list_sync_', '').replace(/_/g, ' ')}${success ? ' success' : ''}`)
}
const logError = (eventName: keyof LX.Sync.ActionSyncSendType, err: Error) => {
log.error(`[${eventName as string}]${eventName.replace('list:sync:list_sync_', '').replace(/_/g, ' ')} error: ${err.message}`)
}
export default async(socket: LX.Sync.Socket) => new Promise<void>((resolve, reject) => {
let listenEvents: Array<() => void> = []
const unregisterEvents = () => {
while (listenEvents.length) listenEvents.shift()?.()
}
socket.onClose(() => {
unregisterEvents()
removeSyncModeEvent()
reject(new Error('closed'))
})
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_md5', async() => {
logInfo('list:sync:list_sync_get_md5')
const md5 = toMD5(JSON.stringify(await getLocalListData()))
socket?.sendData('list:sync:list_sync_get_md5', md5, (err) => {
if (err) {
logError('list:sync:list_sync_get_md5', err)
socket.close(SYNC_CLOSE_CODE.failed)
return
}
logInfo('list:sync:list_sync_get_md5', true)
})
}))
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_list_data', async() => {
logInfo('list:sync:list_sync_get_list_data')
socket?.sendData('list:sync:list_sync_get_list_data', await getLocalListData(), (err) => {
if (err) {
logError('list:sync:list_sync_get_list_data', err)
socket.close(SYNC_CLOSE_CODE.failed)
return
}
logInfo('list:sync:list_sync_get_list_data', true)
})
}))
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_sync_mode', async() => {
logInfo('list:sync:list_sync_get_sync_mode')
let mode: LX.Sync.Mode
try {
mode = await selectSyncMode(socket.data.keyInfo.serverName)
} catch (err: unknown) {
logError('list:sync:list_sync_get_sync_mode', err as Error)
socket.close(SYNC_CLOSE_CODE.normal)
return
}
socket?.sendData('list:sync:list_sync_get_sync_mode', mode, (err) => {
if (err) {
logError('list:sync:list_sync_get_sync_mode', err)
socket.close(SYNC_CLOSE_CODE.failed)
return
}
logInfo('list:sync:list_sync_get_sync_mode', true)
})
}))
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_set_data', async(data) => {
logInfo('list:sync:list_sync_set_data')
await setLocalListData(data)
logInfo('list:sync:list_sync_set_data', true)
}))
listenEvents.push(socket.onRemoteEvent('list:sync:finished', async() => {
unregisterEvents()
resolve()
logInfo('list:sync:finished', true)
toast('Sync successfully')
}))
})

View File

@ -1,4 +1,5 @@
// import { generateKeyPair } from 'crypto'
import { gzipStringToBase64, unGzipFromBase64 } from '@/utils/nativeModules/gzip'
import BackgroundTimer from 'react-native-background-timer'
export const request = async(url: string, { timeout = 10000, ...options }: RequestInit & { timeout?: number } = {}) => {
@ -68,14 +69,18 @@ export { generateRsaKey } from '@/utils/nativeModules/crypto'
// })
export const encryptMsg = (keyInfo: LX.Sync.KeyInfo, msg: string): string => {
return msg
export const encryptMsg = async(keyInfo: LX.Sync.KeyInfo, msg: string): Promise<string> => {
return msg.length > 1024
? 'cg_' + await gzipStringToBase64(msg)
: msg
// if (!keyInfo) return ''
// return aesEncrypt(msg, keyInfo.key, keyInfo.iv)
}
export const decryptMsg = (keyInfo: LX.Sync.KeyInfo, enMsg: string): string => {
return enMsg
export const decryptMsg = async(keyInfo: LX.Sync.KeyInfo, enMsg: string): Promise<string> => {
return enMsg.substring(0, 3) == 'cg_'
? unGzipFromBase64(enMsg.replace('cg_', ''))
: enMsg
// if (!keyInfo) return ''
// let msg = ''
// try {

70
src/types/sync.d.ts vendored
View File

@ -1,65 +1,6 @@
declare global {
namespace LX {
namespace Sync {
interface Enable {
enable: boolean
port: string
}
interface SyncActionBase <A> {
action: A
}
interface SyncActionData<A, D> extends SyncActionBase<A> {
data: D
}
type SyncAction<A, D = undefined> = D extends undefined ? SyncActionBase<A> : SyncActionData<A, D>
// type SyncMainWindowActions = SyncAction<'select_mode', KeyInfo>
// | SyncAction<'close_select_mode'>
// | SyncAction<'status', Status>
// type SyncServiceActions = SyncAction<'select_mode', Mode>
// | SyncAction<'get_status'>
// | SyncAction<'generate_code'>
// | SyncAction<'enable', Enable>
type ActionList = SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite>
| SyncAction<'list_create', LX.List.ListActionAdd>
| SyncAction<'list_remove', LX.List.ListActionRemove>
| SyncAction<'list_update', LX.List.ListActionUpdate>
| SyncAction<'list_update_position', LX.List.ListActionUpdatePosition>
| SyncAction<'list_music_add', LX.List.ListActionMusicAdd>
| SyncAction<'list_music_move', LX.List.ListActionMusicMove>
| SyncAction<'list_music_remove', LX.List.ListActionMusicRemove>
| SyncAction<'list_music_update', LX.List.ListActionMusicUpdate>
| SyncAction<'list_music_update_position', LX.List.ListActionMusicUpdatePosition>
| SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite>
| SyncAction<'list_music_clear', LX.List.ListActionMusicClear>
type ActionSync = SyncAction<'list:sync:list_sync_get_md5', string>
| SyncAction<'list:sync:list_sync_get_list_data', ListData>
| SyncAction<'list:sync:list_sync_get_sync_mode', Mode>
| SyncAction<'list:sync:action', ActionList>
// | SyncAction<'finished'>
type ActionSyncType = Actions<ActionSync>
type ActionSyncSend = SyncAction<'list:sync:list_sync_get_md5'>
| SyncAction<'list:sync:list_sync_get_list_data'>
| SyncAction<'list:sync:list_sync_get_sync_mode'>
| SyncAction<'list:sync:list_sync_set_data', LX.Sync.ListData>
| SyncAction<'list:sync:action', ActionList>
| SyncAction<'list:sync:finished'>
type ActionSyncSendType = Actions<ActionSyncSend>
interface List {
action: string
data: any
}
interface Status {
status: boolean
message: string
@ -89,18 +30,9 @@ declare global {
urlInfo: UrlInfo
}
onRemoteEvent: <T extends keyof LX.Sync.ActionSyncSendType>(
eventName: T,
handler: (data: LX.Sync.ActionSyncSendType[T]) => (void | Promise<void>)
) => () => void
onClose: (handler: (err: Error) => (void | Promise<void>)) => () => void
sendData: <T extends keyof LX.Sync.ActionSyncType>(
eventName: T,
data?: LX.Sync.ActionSyncType[T],
callback?: (err?: Error) => void
) => void
remoteSyncList: LX.Sync.ServerActions
}

31
src/types/sync_common.d.ts vendored Normal file
View File

@ -0,0 +1,31 @@
declare namespace LX {
namespace Sync {
type ActionList = SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite>
| SyncAction<'list_create', LX.List.ListActionAdd>
| SyncAction<'list_remove', LX.List.ListActionRemove>
| SyncAction<'list_update', LX.List.ListActionUpdate>
| SyncAction<'list_update_position', LX.List.ListActionUpdatePosition>
| SyncAction<'list_music_add', LX.List.ListActionMusicAdd>
| SyncAction<'list_music_move', LX.List.ListActionMusicMove>
| SyncAction<'list_music_remove', LX.List.ListActionMusicRemove>
| SyncAction<'list_music_update', LX.List.ListActionMusicUpdate>
| SyncAction<'list_music_update_position', LX.List.ListActionMusicUpdatePosition>
| SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite>
| SyncAction<'list_music_clear', LX.List.ListActionMusicClear>
type ServerActions = WarpPromiseRecord<{
onListSyncAction: (action: LX.Sync.ActionList) => void
}>
type ClientActions = WarpPromiseRecord<{
onListSyncAction: (action: LX.Sync.ActionList) => void
list_sync_get_md5: () => string
list_sync_get_sync_mode: () => Mode
list_sync_get_list_data: () => ListData
list_sync_set_list_data: (data: ListData) => void
list_sync_finished: () => void
}>
}
}

10
src/types/utils.d.ts vendored
View File

@ -14,3 +14,13 @@ type ForwardRefFn<R> = <P = {}>(p: React.PropsWithChildren<P> & React.RefAttribu
type Actions<T extends { action: string, data?: any }> = {
[U in T as U['action']]: 'data' extends keyof U ? U['data'] : undefined
}
type WarpPromiseValue<T> = T extends ((...args: infer P) => Promise<infer R>)
? ((...args: P) => Promise<R>)
: T extends ((...args: infer P2) => infer R2)
? ((...args: P2) => Promise<R2>)
: Promise<T>
type WarpPromiseRecord<T extends Record<string, any>> = {
[K in keyof T]: WarpPromiseValue<T[K]>
}

View File

@ -2,11 +2,19 @@ import { NativeModules } from 'react-native'
const { GzipModule } = NativeModules
export const gzip = (sourceFilePath: string, targetFilePath: string) => {
export const gzipFile = async(sourceFilePath: string, targetFilePath: string): Promise<string> => {
console.log(sourceFilePath, targetFilePath)
return GzipModule.gzip(sourceFilePath, targetFilePath, true)
return GzipModule.gzipFile(sourceFilePath, targetFilePath, true)
}
export const ungzip = (sourceFilePath: string, targetFilePath: string) => {
return GzipModule.unGzip(sourceFilePath, targetFilePath, true)
export const unGzipFile = async(sourceFilePath: string, targetFilePath: string): Promise<string> => {
return GzipModule.unGzipFile(sourceFilePath, targetFilePath, true)
}
export const gzipStringToBase64 = async(data: string): Promise<string> => {
return GzipModule.gzipStringToBase64(data)
}
export const unGzipFromBase64 = async(data: string): Promise<string> => {
return GzipModule.unGzipFromBase64(data)
}

View File

@ -2,7 +2,7 @@ import { Platform, ToastAndroid, BackHandler, Linking, Dimensions, Alert, Appear
// import ExtraDimensions from 'react-native-extra-dimensions-android'
import Clipboard from '@react-native-clipboard/clipboard'
import { storageDataPrefix } from '@/config/constant'
import { gzip, ungzip } from '@/utils/nativeModules/gzip'
import { gzipFile, unGzipFile } from '@/utils/nativeModules/gzip'
import { temporaryDirectoryPath, unlink } from '@/utils/fs'
import { getSystemLocales, isNotificationsEnabled, openNotificationPermissionActivity, readFile, shareText, writeFile } from '@/utils/nativeModules/utils'
import musicSdk from '@/utils/musicSdk'
@ -146,7 +146,7 @@ export const handleSaveFile = async(path: string, data: any) => {
// const buffer = gzip(data)
const tempFilePath = `${temporaryDirectoryPath}/tempFile.json`
await writeFile(tempFilePath, JSON.stringify(data))
await gzip(tempFilePath, path)
await gzipFile(tempFilePath, path)
await unlink(tempFilePath)
}
export const handleReadFile = async<T = unknown>(path: string): Promise<T> => {
@ -156,7 +156,7 @@ export const handleReadFile = async<T = unknown>(path: string): Promise<T> => {
data = await readFile(path)
} else {
const tempFilePath = `${temporaryDirectoryPath}/tempFile.json`
await ungzip(path, tempFilePath)
await unGzipFile(path, tempFilePath)
data = await readFile(tempFilePath)
await unlink(tempFilePath)
}