新增数据同步服务端模式已认证设备列表管理

pull/1583/head
lyswhut 2023-08-29 15:40:42 +08:00
parent 5d877d81e3
commit 7d17423e4b
24 changed files with 595 additions and 158 deletions

View File

@ -8,6 +8,7 @@
### 新增
- 新增我的列表名右键菜单-排序歌曲-随机乱序功能,使用它可以对选中列表内歌曲进行随机重排(#1440
- 新增数据同步服务端模式已认证设备列表管理,该功能位置:设置-数据同步-服务端模式-已认证设备列表
### 优化

View File

@ -127,6 +127,8 @@ const modules = {
get_music_url_count: 'get_music_url_count',
sync_action: 'sync_action',
sync_get_server_devices: 'sync_get_server_devices',
sync_remove_server_device: 'sync_remove_server_device',
process_new_desktop_lyric_client: 'process_new_desktop_lyric_client',

View File

@ -31,6 +31,8 @@ declare namespace LX {
| SyncAction<'enable_server', EnableServer>
| SyncAction<'enable_client', EnableClient>
type ServerDevices = ServerKeyInfo[]
interface ServerStatus {
status: boolean
message: string

View File

@ -522,10 +522,16 @@
"setting__sync_server_address": "Synchronization service address: {address}",
"setting__sync_server_auth_code": "Connection code: {code}",
"setting__sync_server_device": "Connected devices: {devices}",
"setting__sync_server_device_list_btn_remove": "Remove",
"setting__sync_server_device_list_noitem": "Nothing here ┗( ▔, ▔ )┛",
"setting__sync_server_device_list_time": "Last connection time: {time}",
"setting__sync_server_device_list_tips": "💡 After the device is removed, you need to re-enter the connection code when reconnecting",
"setting__sync_server_device_list_title": "Certified device",
"setting__sync_server_mode": "Server mode (since the data is transmitted in clear text, please use it under a trusted network)",
"setting__sync_server_port": "Sync port settings",
"setting__sync_server_port_tip": "Please enter the synchronization service port number",
"setting__sync_server_refresh_code": "Refresh the connection code",
"setting__sync_server_show_device_list": "List of certified devices",
"setting__sync_tip": "For how to use it, please see the \"Sync function\" section of the FAQ",
"setting__update": "Update",
"setting__update_checking": "Checking for updates...",
@ -577,7 +583,7 @@
"sync__merge_tip_desc": "Merge the two lists together, the same song will be removed (the song of the merged person is removed), and different songs will be added.",
"sync__other_label": "Other",
"sync__other_tip": "Other: ",
"sync__other_tip_desc": "\"Cancel Sync\" will directly disconnect the two parties.",
"sync__other_tip_desc": "\"Cancel Sync\" will not use list sync.",
"sync__overwrite": "Full coverage",
"sync__overwrite_btn_cancel": "Cancel sync",
"sync__overwrite_btn_local_remote": "Local list Overwrite remote list",

View File

@ -521,10 +521,16 @@
"setting__sync_server_address": "同步服务地址:{address}",
"setting__sync_server_auth_code": "连接码:{code}",
"setting__sync_server_device": "已连接的设备:{devices}",
"setting__sync_server_device_list_btn_remove": "移除",
"setting__sync_server_device_list_noitem": "这里啥也没有 ┗( ▔, ▔ )┛",
"setting__sync_server_device_list_time": "最后连接时间:{time}",
"setting__sync_server_device_list_tips": "💡 设备被移除后,再连接时需要重新输入连接码",
"setting__sync_server_device_list_title": "已认证设备",
"setting__sync_server_mode": "服务端模式(由于数据是明文传输,请在受信任的网络下使用)",
"setting__sync_server_port": "同步端口设置",
"setting__sync_server_port_tip": "请输入同步服务端口号",
"setting__sync_server_refresh_code": "刷新连接码",
"setting__sync_server_show_device_list": "已认证设备列表",
"setting__sync_tip": "使用方式请看常见问题“同步功能”部分",
"setting__update": "软件更新",
"setting__update_checking": "检查更新中...",
@ -576,7 +582,7 @@
"sync__merge_tip_desc": "将两边的列表合并到一起,相同的歌曲将被去掉(去掉的是被合并者的歌曲),不同的歌曲将被添加。",
"sync__other_label": "其他",
"sync__other_tip": "其他:",
"sync__other_tip_desc": "“取消同步”将直接断开双方的连接。",
"sync__other_tip_desc": "“取消同步”将不使用列表同步功能。",
"sync__overwrite": "完全覆盖",
"sync__overwrite_btn_cancel": "取消同步",
"sync__overwrite_btn_local_remote": "本机列表 覆盖 远程列表",

View File

@ -522,10 +522,16 @@
"setting__sync_server_address": "同步服務地址:{address}",
"setting__sync_server_auth_code": "連接碼:{code}",
"setting__sync_server_device": "已連接的設備:{devices}",
"setting__sync_server_device_list_btn_remove": "移除",
"setting__sync_server_device_list_noitem": "這裡啥也沒有 ┗( ▔, ▔ )┛",
"setting__sync_server_device_list_time": "最後連接時間:{time}",
"setting__sync_server_device_list_tips": "💡 設備被移除後,再連接時需要重新輸入連接碼",
"setting__sync_server_device_list_title": "已認證設備",
"setting__sync_server_mode": "服務端模式(由於數據是明文傳輸,請在受信任的網絡下使用)",
"setting__sync_server_port": "同步端口設置",
"setting__sync_server_port_tip": "請輸入同步服務端口號",
"setting__sync_server_refresh_code": "刷新連接碼",
"setting__sync_server_show_device_list": "已認證設備列表",
"setting__sync_tip": "使用方式請看常見問題“同步功能”部分",
"setting__update": "軟件更新",
"setting__update_checking": "檢查更新中...",
@ -576,7 +582,7 @@
"sync__merge_tip_desc": "將兩邊的列表合併到一起,相同的歌曲將被去掉(去掉的是被合併者的歌曲),不同的歌曲將被添加。",
"sync__other_label": "其他",
"sync__other_tip": "其他:",
"sync__other_tip_desc": "“取消同步”將直接斷開雙方的連接。",
"sync__other_tip_desc": "“取消同步”將不使用列表同步功能。",
"sync__overwrite": "完全覆蓋",
"sync__overwrite_btn_cancel": "取消同步",
"sync__overwrite_btn_local_remote": "本機列表 覆蓋 遠程列表",

View File

@ -9,6 +9,8 @@ export {
stopServer,
getStatus as getServerStatus,
generateCode,
getDevices as getServerDevices,
removeDevice as removeServerDevice,
} from './server'
export {

View File

@ -1,14 +1,8 @@
import {
startServer,
stopServer,
getStatus,
generateCode,
} from './server'
export {
startServer,
stopServer,
getStatus,
generateCode,
}
getDevices,
removeDevice,
} from './server'

View File

@ -44,6 +44,10 @@ export class ListManage {
await this.snapshotDataManage.updateDeviceSnapshotKey(clientId, key)
}
removeDevice = async(clientId: string) => {
this.snapshotDataManage.removeSnapshotInfo(clientId)
}
getListData = async(): Promise<LX.Sync.List.ListData> => {
return getLocalListData()
}

View File

@ -68,6 +68,15 @@ export class SnapshotDataManage {
this.saveSnapshotInfoThrottle()
}
removeSnapshotInfo = (clientId: string) => {
let client = this.snapshotInfo.clients[clientId]
if (!client) return
if (client.snapshotKey) this.clientSnapshotKeys.splice(this.clientSnapshotKeys.indexOf(client.snapshotKey), 1)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.snapshotInfo.clients[clientId]
this.saveSnapshotInfoThrottle()
}
getSnapshot = async(name: string) => {
const filePath = path.join(this.snapshotDir, `snapshot_${name}`)
let listData: LX.Sync.List.ListData

View File

@ -1,4 +1,5 @@
// import { SYNC_CLOSE_CODE } from '../../../../constants'
import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
import { getUserSpace, getUserConfig } from '../../../user'
import { getLocalListData, setLocalListData } from '@main/modules/sync/utils'
// import { LIST_IDS } from '@common/constants'
@ -29,9 +30,26 @@ const getRemoteListMD5 = async(socket: LX.Sync.Server.Socket): Promise<string> =
// const getLocalListData = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.List.ListData> => {
// return getUserSpace(socket.userInfo.name).listManage.getListData()
// }
const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.List.SyncMode> => {
return socket.remoteQueueList.list_sync_get_sync_mode()
}
const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.List.SyncMode> => new Promise((resolve, reject) => {
const handleDisconnect = (err: Error) => {
sendCloseSelectMode()
removeSelectModeListener()
reject(err)
}
let removeEventClose = socket.onClose(handleDisconnect)
sendSelectMode(socket.keyInfo.deviceName, (mode) => {
if (mode == null) {
reject(new Error('cancel'))
return
}
resolve(mode)
removeSelectModeListener()
removeEventClose()
})
})
// const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.List.SyncMode> => {
// return socket.remoteQueueList.list_sync_get_sync_mode()
// }
const finishedSync = async(socket: LX.Sync.Server.Socket) => {
await socket.remoteQueueList.list_sync_finished()

View File

@ -1,14 +1,8 @@
import {
startServer,
stopServer,
getStatus,
generateCode,
} from './server'
export {
startServer,
stopServer,
getStatus,
generateCode,
}
getDevices,
removeDevice,
} from './server'

View File

@ -385,3 +385,18 @@ export const generateCode = async() => {
sendServerStatus(status)
return status.code
}
export const getDevices = async() => {
const userSpace = getUserSpace()
return userSpace.getDecices()
}
export const removeDevice = async(clientId: string) => {
if (wss) {
for (const client of wss.clients) {
if (client.keyInfo.clientId == clientId) client.close(SYNC_CLOSE_CODE.normal)
}
}
const userSpace = getUserSpace()
await userSpace.removeDevice(clientId)
}

View File

@ -3,7 +3,7 @@ import path from 'node:path'
import { randomBytes } from 'node:crypto'
import { throttle } from '@common/utils/common'
import { filterFileName, toMD5 } from '../utils'
import { File } from '../../../../../common/constants_sync'
import { File } from '@common/constants_sync'
interface ServerInfo {
@ -105,6 +105,10 @@ export class UserDataManage {
devicesInfo: DevicesInfo
private readonly saveDevicesInfoThrottle: () => void
getAllClientKeyInfo = () => {
return Object.values(this.devicesInfo.clients).sort((a, b) => (b.lastConnectDate ?? 0) - (a.lastConnectDate ?? 0))
}
saveClientKeyInfo = (keyInfo: LX.Sync.ServerKeyInfo) => {
if (this.devicesInfo.clients[keyInfo.clientId] == null && Object.keys(this.devicesInfo.clients).length > 101) throw new Error('max keys')
this.devicesInfo.clients[keyInfo.clientId] = keyInfo
@ -116,6 +120,12 @@ export class UserDataManage {
return this.devicesInfo.clients[clientId] ?? null
}
removeClientKeyInfo = async(clientId: string) => {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.devicesInfo.clients[clientId]
this.saveDevicesInfoThrottle()
}
isIncluedsClient = (clientId: string) => {
return Object.values(this.devicesInfo.clients).some(client => client.clientId == clientId)
}

View File

@ -6,6 +6,8 @@ import {
export interface UserSpace {
dataManage: UserDataManage
listManage: ListManage
getDecices: () => Promise<LX.Sync.ServerKeyInfo[]>
removeDevice: (clientId: string) => Promise<void>
}
const users = new Map<string, UserSpace>()
@ -31,9 +33,17 @@ export const getUserSpace = (userName = 'default') => {
if (!user) {
console.log('new user data manage:', userName)
const dataManage = new UserDataManage(userName)
const listManage = new ListManage(dataManage)
users.set(userName, user = {
dataManage,
listManage: new ListManage(dataManage),
listManage,
async getDecices() {
return this.dataManage.getAllClientKeyInfo()
},
async removeDevice(clientId) {
await listManage.removeDevice(clientId)
await dataManage.removeClientKeyInfo(clientId)
},
})
}
return user

View File

@ -1,6 +1,16 @@
import { mainHandle } from '@common/mainIpc'
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
import { startServer, stopServer, getServerStatus, generateCode, connectServer, disconnectServer, getClientStatus } from '@main/modules/sync'
import {
startServer,
stopServer,
getServerStatus,
generateCode,
connectServer,
disconnectServer,
getClientStatus,
getServerDevices,
removeServerDevice,
} from '@main/modules/sync'
import { sendEvent } from '../main'
let selectModeListenr: ((mode: LX.Sync.List.SyncMode | null) => void) | null = null
@ -27,6 +37,12 @@ export default () => {
break
}
})
mainHandle<never, LX.Sync.ServerDevices>(WIN_MAIN_RENDERER_EVENT_NAME.sync_get_server_devices, async() => {
return getServerDevices()
})
mainHandle<string>(WIN_MAIN_RENDERER_EVENT_NAME.sync_remove_server_device, async({ params: clientId }) => {
await removeServerDevice(clientId)
})
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26" width="208" height="208">
<path d="M8 0C6.34375 0 5 1.34375 5 3L5 23C5 24.65625 6.34375 26 8 26L19 26C20.65625 26 22 24.65625 22 23L22 3C22 1.34375 20.65625 0 19 0 Z M 11.5 1L15.5 1C15.777344 1 16 1.222656 16 1.5C16 1.777344 15.777344 2 15.5 2L11.5 2C11.222656 2 11 1.777344 11 1.5C11 1.222656 11.222656 1 11.5 1 Z M 7.5 3L19.5 3C19.777344 3 20 3.222656 20 3.5L20 20.5C20 20.777344 19.777344 21 19.5 21L7.5 21C7.222656 21 7 20.777344 7 20.5L7 3.5C7 3.222656 7.222656 3 7.5 3 Z M 11.65625 6.71875C11.617188 6.746094 11.59375 6.828125 11.625 6.875L12.15625 7.625C11.320313 8.011719 10.730469 8.761719 10.65625 9.625L16.34375 9.625C16.269531 8.761719 15.679688 8.015625 14.84375 7.625L15.375 6.875C15.40625 6.828125 15.414063 6.746094 15.375 6.71875C15.335938 6.691406 15.25 6.703125 15.21875 6.75L14.6875 7.5625C14.324219 7.421875 13.921875 7.34375 13.5 7.34375C13.078125 7.34375 12.671875 7.421875 12.3125 7.5625L11.78125 6.75C11.75 6.703125 11.695313 6.691406 11.65625 6.71875 Z M 12.3125 8.1875C12.484375 8.1875 12.625 8.324219 12.625 8.5C12.625 8.675781 12.484375 8.84375 12.3125 8.84375C12.140625 8.84375 11.96875 8.675781 11.96875 8.5C11.96875 8.328125 12.140625 8.1875 12.3125 8.1875 Z M 14.75 8.1875C14.925781 8.1875 15.0625 8.324219 15.0625 8.5C15.0625 8.675781 14.925781 8.84375 14.75 8.84375C14.578125 8.84375 14.4375 8.675781 14.4375 8.5C14.4375 8.328125 14.574219 8.1875 14.75 8.1875 Z M 9.625 10C9.273438 10 9 10.296875 9 10.65625L9 13.1875C9 13.546875 9.273438 13.84375 9.625 13.84375C9.980469 13.84375 10.28125 13.546875 10.28125 13.1875L10.28125 10.65625C10.28125 10.296875 9.980469 10 9.625 10 Z M 17.34375 10C16.988281 10 16.71875 10.296875 16.71875 10.65625L16.71875 13.1875C16.71875 13.546875 16.988281 13.84375 17.34375 13.84375C17.699219 13.84375 18 13.546875 18 13.1875L18 10.65625C18 10.296875 17.699219 10 17.34375 10 Z M 10.65625 10.03125L10.65625 14.65625C10.65625 14.933594 10.882813 15.15625 11.15625 15.15625L11.71875 15.15625L11.71875 16.59375C11.71875 16.953125 12.019531 17.21875 12.375 17.21875C12.730469 17.21875 13 16.953125 13 16.59375L13 15.15625L14 15.15625L14 16.59375C14 16.953125 14.304688 17.21875 14.65625 17.21875C15.011719 17.21875 15.28125 16.953125 15.28125 16.59375L15.28125 15.15625L15.875 15.15625C16.152344 15.15625 16.375 14.933594 16.375 14.65625L16.375 10.03125 Z M 8.5 23L10.5 23C10.777344 23 11 23.222656 11 23.5C11 23.777344 10.777344 24 10.5 24L8.5 24C8.222656 24 8 23.777344 8 23.5C8 23.222656 8.222656 23 8.5 23 Z M 12.5 23L14.5 23C14.777344 23 15 23.222656 15 23.5L15 24.5C15 24.777344 14.777344 25 14.5 25L12.5 25C12.222656 25 12 24.777344 12 24.5L12 23.5C12 23.222656 12.222656 23 12.5 23 Z M 16.5 23L18.5 23C18.777344 23 19 23.222656 19 23.5C19 23.777344 18.777344 24 18.5 24L16.5 24C16.222656 24 16 23.777344 16 23.5C16 23.222656 16.222656 23 16.5 23Z" fill="#5B5B5B" />
</svg>

View File

@ -727,6 +727,23 @@ export const sendSyncAction = async(action: LX.Sync.SyncServiceActions) => {
return rendererInvoke<LX.Sync.SyncServiceActions>(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, action)
}
/**
*
* @returns
*/
export const getSyncServerDevices = () => {
return rendererInvoke<LX.Sync.ServerDevices>(WIN_MAIN_RENDERER_EVENT_NAME.sync_get_server_devices)
}
/**
*
* @returns
*/
export const removeSyncServerDevice = (clientId: string) => {
return rendererInvoke<string>(WIN_MAIN_RENDERER_EVENT_NAME.sync_remove_server_device, clientId)
}
// export const refreshSyncCode = async(): Promise<string> => {
// return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.sync_generate_code)
// }

View File

@ -1,130 +0,0 @@
<template lang="pug">
dt#sync
| {{ $t('setting__sync') }}
button(class="help-btn" :aria-label="$t('setting__sync_tip')" @click="openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq/sync')")
svg-icon(name="help-circle-outline")
dd
base-checkbox(id="setting_sync_enable" :model-value="appSetting['sync.enable']" :label="$t('setting__sync_enable')" @update:model-value="updateSetting({ 'sync.enable': $event })")
dd
h3#sync_mode {{ $t('setting__sync_mode') }}
div
base-checkbox.gap-left(id="setting_sync_mode_server" :disabled="sync.enable" :model-value="appSetting['sync.mode']" need value="server" :label="$t('setting__sync_mode_server')" @update:model-value="updateSetting({ 'sync.mode': $event })")
base-checkbox.gap-left(id="setting_sync_mode_client" :disabled="sync.enable" :model-value="appSetting['sync.mode']" need value="client" :label="$t('setting__sync_mode_client')" @update:model-value="updateSetting({ 'sync.mode': $event })")
dd(v-if="sync.mode == 'client'")
h3 {{ $t('setting__sync_client_mode') }}
div
.p.small {{ $t('setting__sync_client_status', { status: clientStatus }) }}
.p.small {{ $t('setting__sync_client_address', { address: sync.client.status.address.join(', ') || '' }) }}
.p
.p.small {{ $t('setting__sync_client_host') }}
div
base-input.gap-left(:class="$style.hostInput" :model-value="appSetting['sync.client.host']" :disabled="sync.enable" :placeholder="$t('setting__sync_client_host_tip')" @update:model-value="setSyncClientHost")
dd(v-else)
h3 {{ syncEnableServerTitle }}
div
.p.small {{ $t('setting__sync_server_auth_code', { code: sync.server.status.code || '' }) }}
.p.small {{ $t('setting__sync_server_address', { address: sync.server.status.address.join(', ') || '' }) }}
.p.small {{ $t('setting__sync_server_device', { devices: syncDevices }) }}
.p
base-btn.btn(min :disabled="!sync.server.status.status" @click="refreshSyncCode") {{ $t('setting__sync_server_refresh_code') }}
.p
.p.small {{ $t('setting__sync_server_port') }}
div
base-input.gap-left(:class="$style.portInput" :model-value="appSetting['sync.server.port']" :disabled="sync.enable" type="number" :placeholder="$t('setting__sync_server_port_tip')" @update:model-value="setSyncServerPort")
</template>
<script>
import { computed } from '@common/utils/vueTools'
import { sync } from '@renderer/store'
import { sendSyncAction } from '@renderer/utils/ipc'
import { openUrl } from '@common/utils/electron'
import { useI18n } from '@renderer/plugins/i18n'
import { appSetting, updateSetting } from '@renderer/store/setting'
import { debounce } from '@common/utils/common'
import { SYNC_CODE } from '@common/constants_sync'
export default {
name: 'SettingSync',
setup() {
const t = useI18n()
const syncEnableServerTitle = computed(() => {
let title = t('setting__sync_server_mode')
if (sync.server.status.message) {
title += ` [${sync.server.status.message}]`
}
// else if (this.sync.server.status.address.length) {
// // title += ` [${this.sync.server.status.address.join(', ')}]`
// }
return title
})
const clientStatus = computed(() => {
let status
switch (sync.client.status.message) {
case SYNC_CODE.msgBlockedIp:
status = t('setting__sync_code_blocked_ip')
break
case SYNC_CODE.authFailed:
status = t('setting__sync_code_fail')
break
default:
status = sync.client.status.message
? sync.client.status.message
: sync.client.status.status
? t('setting_sync_status_enabled')
: t('sync_status_disabled')
break
}
return status
})
const syncDevices = computed(() => {
return sync.server.status.devices.length
? sync.server.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')
: ''
})
const refreshSyncCode = () => {
void sendSyncAction({ action: 'generate_code' })
}
const setSyncServerPort = debounce(port => {
updateSetting({ 'sync.server.port': port.trim() })
}, 500)
const setSyncClientHost = debounce(host => {
updateSetting({ 'sync.client.host': host.trim() })
}, 500)
return {
appSetting,
updateSetting,
sync,
syncEnableServerTitle,
setSyncServerPort,
setSyncClientHost,
syncDevices,
refreshSyncCode,
clientStatus,
openUrl,
}
},
}
</script>
<style lang="less" module>
.portInput[disabled], .hostInput[disabled] {
opacity: .8 !important;
}
.hostInput {
min-width: 380px;
}
</style>

View File

@ -0,0 +1,163 @@
<template lang="pug">
material-modal(:show="modelValue" bg-close teleport="#view" @close="$emit('update:modelValue', false)")
main(:class="$style.main")
h2 {{ $t('setting__sync_server_device_list_title') }}
ul.scroll(v-if="historyDeviceList.length" :class="$style.content")
li(v-for="(device, index) in historyDeviceList" :key="device.id" :class="$style.listItem")
div(:class="$style.listLeft")
span(:class="$style.name")
svg-icon(v-if="device.isMobile" name="phone" style="margin-right: 0.2rem; vertical-align: -0.2em;")
| {{ device.name }}
span(:class="$style.desc") {{ $t('setting__sync_server_device_list_time', { time: device.lastConnectDate }) }}
base-btn(:class="$style.listBtn" outline :aria-label="$t('setting__sync_server_device_list_btn_remove')" @click.stop="handleRemove(index)")
svg(v-once version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" viewBox="0 0 212.982 212.982" space="preserve")
use(xlink:href="#icon-delete")
div(v-else :class="$style.content")
div(:class="$style.noitem") {{ $t('setting__sync_server_device_list_noitem') }}
div(:class="$style.footer")
div(:class="$style.tips") {{ $t('setting__sync_server_device_list_tips') }}
</template>
<script>
import { watch, ref } from '@common/utils/vueTools'
import { sync } from '@renderer/store'
import { getSyncServerDevices, removeSyncServerDevice } from '@renderer/utils/ipc'
import { dateFormat } from '@common/utils/common'
export default {
props: {
modelValue: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
setup(props) {
const historyDeviceList = ref([])
const getList = () => {
void getSyncServerDevices().then((list) => {
historyDeviceList.value = list.map(d => {
return {
id: d.clientId,
name: d.deviceName,
lastConnectDate: d.lastConnectDate ? dateFormat(d.lastConnectDate) : '-',
isMobile: d.isMobile,
}
})
})
}
watch(() => sync.server.status.devices.length, () => {
if (!props.modelValue) return
getList()
})
watch(() => props.modelValue, (val) => {
if (!val) return
getList()
})
const handleRemove = (index) => {
void removeSyncServerDevice(historyDeviceList.value[index].id).then(getList)
}
return {
historyDeviceList,
handleRemove,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
// padding: 15px;
// max-width: 400px;
min-width: 460px;
min-height: 200px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
// min-height: 0;
// max-height: 100%;
// overflow: hidden;
h2 {
margin: 15px;
font-size: 16px;
color: var(--color-font);
line-height: 1.3;
text-align: center;
}
}
.name {
color: var(--color-font);
font-size: 14px;
word-break: break-all;
line-height: 1.2;
}
.desc {
color: var(--color-font-label);
margin-top: 8px;
font-size: 12px;
word-break: break-all;
}
.content {
flex: auto;
min-height: 100px;
max-height: 100%;
}
.listItem {
display: flex;
flex-flow: row nowrap;
align-items: center;
transition: background-color 0.2s ease;
padding: 10px;
// border-radius: @radius-border;
&:hover {
background-color: var(--color-primary-background-hover);
}
}
.noitem {
height: 100px;
font-size: 18px;
color: var(--color-font-label);
display: flex;
justify-content: center;
align-items: center;
}
.listLeft {
flex: auto;
min-width: 0;
display: flex;
flex-flow: column nowrap;
justify-content: center;
}
.listBtn {
flex: none;
height: 30px;
width: 30px;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
svg {
width: 60%;
}
}
// .footer {
// width: @width;
// }
.tips {
padding: 8px 15px;
font-size: 13px;
line-height: 1.25;
color: var(--color-font);
}
</style>

View File

@ -0,0 +1,69 @@
<template lang="pug">
dd
h3 {{ $t('setting__sync_client_mode') }}
div
.p.small {{ $t('setting__sync_client_status', { status: clientStatus }) }}
.p.small {{ $t('setting__sync_client_address', { address: sync.client.status.address.join(', ') || '' }) }}
.p
.p.small {{ $t('setting__sync_client_host') }}
div
base-input.gap-left(:class="$style.hostInput" :model-value="appSetting['sync.client.host']" :disabled="sync.enable" :placeholder="$t('setting__sync_client_host_tip')" @update:model-value="setSyncClientHost")
</template>
<script>
import { computed } from '@common/utils/vueTools'
import { sync } from '@renderer/store'
import { useI18n } from '@renderer/plugins/i18n'
import { appSetting, updateSetting } from '@renderer/store/setting'
import { debounce } from '@common/utils/common'
import { SYNC_CODE } from '@common/constants_sync'
export default {
name: 'SettingSyncClient',
setup() {
const t = useI18n()
const clientStatus = computed(() => {
let status
switch (sync.client.status.message) {
case SYNC_CODE.msgBlockedIp:
status = t('setting__sync_code_blocked_ip')
break
case SYNC_CODE.authFailed:
status = t('setting__sync_code_fail')
break
default:
status = sync.client.status.message
? sync.client.status.message
: sync.client.status.status
? t('setting_sync_status_enabled')
: t('sync_status_disabled')
break
}
return status
})
const setSyncClientHost = debounce(host => {
updateSetting({ 'sync.client.host': host.trim() })
}, 500)
return {
appSetting,
sync,
setSyncClientHost,
clientStatus,
}
},
}
</script>
<style lang="less" module>
.hostInput[disabled] {
opacity: .8 !important;
}
.hostInput {
min-width: 380px;
}
</style>

View File

@ -0,0 +1,175 @@
<template lang="pug">
dd
h3 {{ syncEnableServerTitle }}
div
.p.small {{ $t('setting__sync_server_auth_code', { code: sync.server.status.code || '' }) }}
.p.small {{ $t('setting__sync_server_address', { address: sync.server.status.address.join(', ') || '' }) }}
.p.small {{ $t('setting__sync_server_device', { devices: syncDevices }) }}
.p.gap-top
.p.small {{ $t('setting__sync_server_port') }}
div
base-input.gap-left(:class="$style.portInput" :model-value="appSetting['sync.server.port']" :disabled="sync.enable" type="number" :placeholder="$t('setting__sync_server_port_tip')" @update:model-value="setSyncServerPort")
.p.gap-top
base-btn.btn(min :disabled="!sync.server.status.status" @click="refreshSyncCode") {{ $t('setting__sync_server_refresh_code') }}
base-btn.btn(min @click="isShowDeviceListModal = true") {{ $t('setting__sync_server_show_device_list') }}
ServerDeviceListModal(v-model="isShowDeviceListModal")
</template>
<script>
import { computed, ref } from '@common/utils/vueTools'
import { sync } from '@renderer/store'
import { sendSyncAction } from '@renderer/utils/ipc'
import { useI18n } from '@renderer/plugins/i18n'
import { appSetting, updateSetting } from '@renderer/store/setting'
import { debounce } from '@common/utils/common'
import ServerDeviceListModal from './ServerDeviceListModal.vue'
export default {
name: 'SettingSyncServer',
components: {
ServerDeviceListModal,
},
setup() {
const t = useI18n()
const isShowDeviceListModal = ref(false)
const syncEnableServerTitle = computed(() => {
let title = t('setting__sync_server_mode')
if (sync.server.status.message) {
title += ` [${sync.server.status.message}]`
}
// else if (this.sync.server.status.address.length) {
// // title += ` [${this.sync.server.status.address.join(', ')}]`
// }
return title
})
const syncDevices = computed(() => {
return sync.server.status.devices.length
? sync.server.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')
: ''
})
const refreshSyncCode = () => {
void sendSyncAction({ action: 'generate_code' })
}
const setSyncServerPort = debounce(port => {
updateSetting({ 'sync.server.port': port.trim() })
}, 500)
return {
appSetting,
sync,
syncEnableServerTitle,
setSyncServerPort,
syncDevices,
refreshSyncCode,
isShowDeviceListModal,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.portInput[disabled], .hostInput[disabled] {
opacity: .8 !important;
}
.hostInput {
min-width: 380px;
}
.list {
// background-color: @color-search-form-background;
font-size: 13px;
transition-property: height;
position: relative;
.listItem {
position: relative;
padding: 15px 10px 15px 15px;
transition: .3s ease;
transition-property: background-color, opacity;
line-height: 1.3;
// overflow: hidden;
display: flex;
flex-flow: row nowrap;
align-items: center;
&:hover {
background-color: var(--color-primary-background-hover);
}
// border-radius: 4px;
// &:last-child {
// border-bottom-left-radius: 4px;
// border-bottom-right-radius: 4px;
// }
&.fetching {
opacity: .5;
}
}
}
.listLeft {
flex: auto;
min-width: 0;
display: flex;
flex-flow: column nowrap;
justify-content: center;
}
.text {
flex: auto;
margin-bottom: 2px;
.mixin-ellipsis-1;
}
.label {
flex: none;
font-size: 12px;
opacity: 0.5;
// padding: 0 10px;
// display: flex;
// align-items: center;
// transform: rotate(45deg);
// background-color:
}
.btns {
flex: none;
font-size: 12px;
padding: 0 5px;
display: flex;
align-items: center;
}
.btn {
background-color: transparent;
border: none;
border-radius: @form-radius;
margin-right: 5px;
cursor: pointer;
padding: 4px 7px;
color: var(--color-button-font);
outline: none;
transition: background-color 0.2s ease;
line-height: 0;
&:last-child {
margin-right: 0;
}
svg {
height: 22px;
width: 22px;
}
&:hover {
background-color: var(--color-primary-background-hover);
}
&:active {
background-color: var(--color-primary-font-active);
}
}
</style>

View File

@ -0,0 +1,44 @@
<template lang="pug">
dt#sync
| {{ $t('setting__sync') }}
button(class="help-btn" :aria-label="$t('setting__sync_tip')" @click="openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq/sync')")
svg-icon(name="help-circle-outline")
dd
base-checkbox(id="setting_sync_enable" :model-value="appSetting['sync.enable']" :label="$t('setting__sync_enable')" @update:model-value="updateSetting({ 'sync.enable': $event })")
dd
h3#sync_mode {{ $t('setting__sync_mode') }}
div
base-checkbox.gap-left(id="setting_sync_mode_server" :disabled="sync.enable" :model-value="appSetting['sync.mode']" need value="server" :label="$t('setting__sync_mode_server')" @update:model-value="updateSetting({ 'sync.mode': $event })")
base-checkbox.gap-left(id="setting_sync_mode_client" :disabled="sync.enable" :model-value="appSetting['sync.mode']" need value="client" :label="$t('setting__sync_mode_client')" @update:model-value="updateSetting({ 'sync.mode': $event })")
SyncClient(v-if="sync.mode == 'client'")
SyncServer(v-else)
</template>
<script>
// import { computed } from '@common/utils/vueTools'
import { sync } from '@renderer/store'
import { openUrl } from '@common/utils/electron'
import { appSetting, updateSetting } from '@renderer/store/setting'
import SyncServer from './SyncServer.vue'
import SyncClient from './SyncClient.vue'
export default {
name: 'SettingSync',
components: {
SyncServer,
SyncClient,
},
setup() {
return {
appSetting,
updateSetting,
sync,
openUrl,
}
},
}
</script>

View File

@ -59,7 +59,7 @@ import SettingDesktopLyric from './components/SettingDesktopLyric.vue'
import SettingSearch from './components/SettingSearch.vue'
import SettingList from './components/SettingList.vue'
import SettingDownload from './components/SettingDownload.vue'
import SettingSync from './components/SettingSync.vue'
import SettingSync from './components/SettingSync/index.vue'
import SettingHotKey from './components/SettingHotKey.vue'
import SettingNetwork from './components/SettingNetwork.vue'
import SettingOdc from './components/SettingOdc.vue'