optimize transfer logic
This commit is contained in:
+1
-1
@@ -99,7 +99,7 @@ if (!gotLock) {
|
||||
unregisterMediaShortcuts()
|
||||
destroyTray()
|
||||
stopFileServer()
|
||||
stopMusicServer()
|
||||
stopMusicServer('app-quit')
|
||||
stopDataServer()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export function registerIpc(): void {
|
||||
return true
|
||||
})
|
||||
ipcMain.handle(IPC.MUSIC_SERVER_STOP, () => {
|
||||
stopMusicServer()
|
||||
stopMusicServer('renderer-stop')
|
||||
return true
|
||||
})
|
||||
ipcMain.handle(IPC.MUSIC_SERVER_SEND, (_e, cmd: string, body: string) =>
|
||||
|
||||
@@ -20,12 +20,23 @@ import { cleanIosWavFile } from './wifiTransfer'
|
||||
|
||||
export type MusicServerEvent =
|
||||
| { type: 'connected' }
|
||||
| { type: 'disconnected' }
|
||||
| { type: 'disconnected'; code?: number; reason?: string; wasClean?: boolean }
|
||||
| { type: 'versionMismatch'; remoteVer: string }
|
||||
| { type: 'system'; system: string }
|
||||
| { type: 'portUnavailable'; reason: string }
|
||||
| { type: 'cmd'; cmd: string; body: string }
|
||||
|
||||
function brief(body: string, max = 160): string {
|
||||
const flat = body.replace(/\s+/g, ' ')
|
||||
if (flat.length <= max) return flat
|
||||
return `${flat.slice(0, max)}…(len=${body.length})`
|
||||
}
|
||||
|
||||
function logTransfer(message: string, extra?: unknown): void {
|
||||
if (extra === undefined) console.log(`[transfer] ${message}`)
|
||||
else console.log(`[transfer] ${message}`, extra)
|
||||
}
|
||||
|
||||
/** 握手时确保本地 HTTP 文件服务可用(手机靠此端口拉歌) */
|
||||
async function ensureFileServerPort(): Promise<{ port: number; error?: string }> {
|
||||
const status = getFileServerStatus()
|
||||
@@ -52,48 +63,93 @@ let wss: WebSocketServer | null = null
|
||||
let socket: WebSocket | null = null
|
||||
let phoneSystem = ''
|
||||
let lastReadyList: { musicUId: string; baseUrl: string; musicPath: string }[] = []
|
||||
/** 最近一次业务消息(不含 ping),用于判断 1006 是否因空闲被掐 */
|
||||
let lastActivity = Date.now()
|
||||
|
||||
export function startMusicServer(onEvent: (e: MusicServerEvent) => void): void {
|
||||
stopMusicServer()
|
||||
stopMusicServer('restart')
|
||||
// monkey 并行实例避开开发中的 4388
|
||||
const port = process.env.LLMP_MONKEY === '1' ? PORTS.MUSIC_WS + 100 : PORTS.MUSIC_WS
|
||||
wss = new WebSocketServer({ port, host: '0.0.0.0' })
|
||||
wss.on('error', (err) => console.error('[musicServer]', err.message))
|
||||
logTransfer(`listen 0.0.0.0:${port}`)
|
||||
wss.on('error', (err) => console.error('[transfer] server error', err))
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
wss.on('connection', (ws, req) => {
|
||||
const peer = req.socket.remoteAddress || '?'
|
||||
if (socket && socket !== ws && socket.readyState === WebSocket.OPEN) {
|
||||
logTransfer(`new connection from ${peer}, closing previous socket`)
|
||||
try {
|
||||
socket.close(4000, 'replaced')
|
||||
} catch (e) {
|
||||
console.error('[transfer] close previous socket failed', e)
|
||||
}
|
||||
}
|
||||
socket = ws
|
||||
let verified = false
|
||||
phoneSystem = ''
|
||||
lastReadyList = []
|
||||
lastActivity = Date.now()
|
||||
logTransfer(`connected ${peer}`)
|
||||
onEvent({ type: 'connected' })
|
||||
|
||||
// 传歌时指令通道会空等 HTTP 下载,路由器/手机会按空闲掐掉 TCP(关闭码 1006)
|
||||
const heartbeat = setInterval(() => {
|
||||
if (ws.readyState !== WebSocket.OPEN) return
|
||||
try {
|
||||
ws.ping()
|
||||
} catch (e) {
|
||||
console.error('[transfer] ping failed', peer, e)
|
||||
}
|
||||
}, 10000)
|
||||
ws.on('pong', () => {
|
||||
logTransfer(`pong ${peer} idle=${Date.now() - lastActivity}ms`)
|
||||
})
|
||||
|
||||
ws.on('message', (raw) => {
|
||||
lastActivity = Date.now()
|
||||
const text = raw.toString()
|
||||
let command
|
||||
try {
|
||||
command = decodeFtpCmd(raw.toString())
|
||||
} catch {
|
||||
command = decodeFtpCmd(text)
|
||||
} catch (e) {
|
||||
console.error('[transfer] bad message', brief(text), e)
|
||||
return
|
||||
}
|
||||
logTransfer(`← ${command.cmd} ${brief(command.body)}`)
|
||||
switch (command.cmd) {
|
||||
case MusicCmd.VERSION: {
|
||||
const compatible = isVersionCompatible(command.body)
|
||||
verified = true
|
||||
try {
|
||||
ws.send(encodeFtpCmd(MusicCmd.VERSION, TRANS_VER))
|
||||
logTransfer(`→ ${MusicCmd.VERSION} ${TRANS_VER} compatible=${compatible}`)
|
||||
} catch (e) {
|
||||
console.error('[transfer] send version failed', e)
|
||||
}
|
||||
if (!compatible) onEvent({ type: 'versionMismatch', remoteVer: command.body })
|
||||
break
|
||||
}
|
||||
case MusicCmd.SYSTEM: {
|
||||
if (!verified) {
|
||||
console.error('[transfer] system before version handshake')
|
||||
onEvent({ type: 'versionMismatch', remoteVer: '?' })
|
||||
return
|
||||
}
|
||||
phoneSystem = (command.body || '').toLowerCase()
|
||||
onEvent({ type: 'system', system: command.body })
|
||||
// 下发 HTTP 文件服务端口(未运行则尝试按配置自动启动)
|
||||
void ensureFileServerPort().then(({ port, error }) => {
|
||||
if (ws.readyState !== WebSocket.OPEN) return
|
||||
ws.send(encodeFtpCmd(MusicCmd.PORT, port))
|
||||
if (error || port <= 0) {
|
||||
void ensureFileServerPort().then(({ port: filePort, error }) => {
|
||||
if (ws.readyState !== WebSocket.OPEN) {
|
||||
console.error('[transfer] skip port reply, socket not open', ws.readyState)
|
||||
return
|
||||
}
|
||||
try {
|
||||
ws.send(encodeFtpCmd(MusicCmd.PORT, filePort))
|
||||
logTransfer(`→ ${MusicCmd.PORT} ${filePort}${error ? ` error=${error}` : ''}`)
|
||||
} catch (e) {
|
||||
console.error('[transfer] send port failed', e)
|
||||
}
|
||||
if (error || filePort <= 0) {
|
||||
onEvent({
|
||||
type: 'portUnavailable',
|
||||
reason: error || '本地文件服务端口无效'
|
||||
@@ -105,48 +161,89 @@ export function startMusicServer(onEvent: (e: MusicServerEvent) => void): void {
|
||||
default:
|
||||
if (command.cmd === MusicCmd.DOWNLOAD_SUCCESS && phoneSystem === 'ios') {
|
||||
const hit = lastReadyList.find((m) => m.musicUId === command.body)
|
||||
if (hit) cleanIosWavFile(hit)
|
||||
if (hit) {
|
||||
try {
|
||||
cleanIosWavFile(hit)
|
||||
} catch (e) {
|
||||
console.error('[transfer] clean ios wav failed', command.body, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
onEvent({ type: 'cmd', cmd: command.cmd, body: command.body })
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('close', () => {
|
||||
ws.on('error', (err) => {
|
||||
console.error('[transfer] socket error', peer, err)
|
||||
})
|
||||
|
||||
ws.on('close', (code, reason) => {
|
||||
clearInterval(heartbeat)
|
||||
const why = reason.toString() || '(empty)'
|
||||
const current = socket === ws
|
||||
const idleMs = Date.now() - lastActivity
|
||||
logTransfer(
|
||||
`close code=${code} reason=${why} current=${current} peer=${peer} idleMs=${idleMs}`
|
||||
)
|
||||
if (!current) return
|
||||
socket = null
|
||||
phoneSystem = ''
|
||||
lastReadyList = []
|
||||
onEvent({ type: 'disconnected' })
|
||||
onEvent({
|
||||
type: 'disconnected',
|
||||
code,
|
||||
reason: why,
|
||||
wasClean: code === 1000 || code === 1001
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function sendMusicCmd(cmd: string, body: string): boolean {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) return false
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
console.error(
|
||||
`[transfer] send dropped, socket not open cmd=${cmd} state=${socket?.readyState ?? 'null'} body=${brief(body)}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
if (cmd === MusicCmd.READY) {
|
||||
try {
|
||||
const list = JSON.parse(body) as { musicUId: string; baseUrl: string; musicPath: string }[]
|
||||
lastReadyList = Array.isArray(list) ? list : []
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error('[transfer] ready body is not a list', e)
|
||||
lastReadyList = []
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
socket.send(encodeFtpCmd(cmd, body))
|
||||
lastActivity = Date.now()
|
||||
logTransfer(`→ ${cmd} ${brief(body)}`)
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error(`[transfer] send failed cmd=${cmd}`, e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function stopMusicServer(): void {
|
||||
export function stopMusicServer(reason = 'stop'): void {
|
||||
logTransfer(`stop server reason=${reason} hasSocket=${!!socket} hasServer=${!!wss}`)
|
||||
if (socket) {
|
||||
try {
|
||||
socket.close()
|
||||
} catch {
|
||||
/* ignore */
|
||||
socket.close(1001, reason)
|
||||
} catch (e) {
|
||||
console.error('[transfer] socket.close failed', e)
|
||||
}
|
||||
socket = null
|
||||
}
|
||||
if (wss) {
|
||||
try {
|
||||
wss.close()
|
||||
} catch (e) {
|
||||
console.error('[transfer] server.close failed', e)
|
||||
}
|
||||
wss = null
|
||||
}
|
||||
phoneSystem = ''
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { spawn, ChildProcess } from 'child_process'
|
||||
import { existsSync, unlinkSync } from 'fs'
|
||||
import ffmpegPath from 'ffmpeg-static'
|
||||
|
||||
/**
|
||||
@@ -7,6 +8,8 @@ import ffmpegPath from 'ffmpeg-static'
|
||||
*/
|
||||
|
||||
let current: ChildProcess | null = null
|
||||
/** 终止当前这次转码(杀进程并删掉未完成的 wav) */
|
||||
let cancelCurrent: (() => void) | null = null
|
||||
|
||||
export interface ConvertOptions {
|
||||
input: string
|
||||
@@ -14,6 +17,14 @@ export interface ConvertOptions {
|
||||
onProgress?: (percent: number) => void
|
||||
}
|
||||
|
||||
function removePartial(output: string): void {
|
||||
try {
|
||||
if (existsSync(output)) unlinkSync(output)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function convertFlacToWav(opts: ConvertOptions): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!ffmpegPath) {
|
||||
@@ -21,9 +32,29 @@ export function convertFlacToWav(opts: ConvertOptions): Promise<void> {
|
||||
return
|
||||
}
|
||||
stopConvert()
|
||||
current = spawn(ffmpegPath, ['-y', '-i', opts.input, opts.output])
|
||||
let aborted = false
|
||||
let settled = false
|
||||
const child = spawn(ffmpegPath, ['-y', '-i', opts.input, opts.output])
|
||||
current = child
|
||||
const settle = (err?: Error): void => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
if (current === child) current = null
|
||||
if (cancelCurrent === cancel) cancelCurrent = null
|
||||
if (err) reject(err)
|
||||
else resolve()
|
||||
}
|
||||
const cancel = (): void => {
|
||||
aborted = true
|
||||
try {
|
||||
child.kill('SIGKILL')
|
||||
} catch {
|
||||
/* already exited */
|
||||
}
|
||||
}
|
||||
cancelCurrent = cancel
|
||||
|
||||
current.stderr?.on('data', (chunk: Buffer) => {
|
||||
child.stderr?.on('data', (chunk: Buffer) => {
|
||||
const text = chunk.toString()
|
||||
// 粗略解析进度(ffmpeg 输出 time=xx:xx:xx)
|
||||
const match = text.match(/time=(\d+):(\d+):(\d+)/)
|
||||
@@ -33,18 +64,21 @@ export function convertFlacToWav(opts: ConvertOptions): Promise<void> {
|
||||
}
|
||||
})
|
||||
|
||||
current.on('error', reject)
|
||||
current.on('close', (code) => {
|
||||
current = null
|
||||
if (code === 0) resolve()
|
||||
else reject(new Error(`ffmpeg 退出码 ${code}`))
|
||||
child.on('error', (err) => {
|
||||
if (aborted) removePartial(opts.output)
|
||||
settle(aborted ? new Error('转码已取消') : err)
|
||||
})
|
||||
child.on('close', (code) => {
|
||||
if (code === 0 && !aborted) {
|
||||
settle()
|
||||
return
|
||||
}
|
||||
removePartial(opts.output)
|
||||
settle(new Error(aborted ? '转码已取消' : `ffmpeg 退出码 ${code}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function stopConvert(): void {
|
||||
if (current) {
|
||||
current.kill('SIGKILL')
|
||||
current = null
|
||||
}
|
||||
cancelCurrent?.()
|
||||
}
|
||||
|
||||
@@ -87,25 +87,8 @@ function parseMissingMusicIds(body: string): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
/** 拼本地文件绝对路径(兼容 Windows,供已有 convert.start 使用) */
|
||||
function mediaAbsPath(root: string, baseUrl: string, musicPath: string): string {
|
||||
const parts = [root, baseUrl, musicPath]
|
||||
.map((p) => p.replace(/[/\\]+/g, '/').replace(/^\/+|\/+$/g, ''))
|
||||
.filter(Boolean)
|
||||
const joined = parts.join('/')
|
||||
return root.includes('\\') ? joined.replace(/\//g, '\\') : joined
|
||||
}
|
||||
|
||||
/** iOS:用已有 convert API 在 flac 旁生成 wav(不依赖 preload 新增命名空间) */
|
||||
async function ensureIosWavWithConvert(list: DownloadMusic[]): Promise<void> {
|
||||
const http = await window.api.http.status()
|
||||
if (!http?.root) throw new Error('本地曲库根目录未知,请先启动本地文件服务')
|
||||
for (const m of list) {
|
||||
if (!/\.flac$/i.test(m.musicPath)) continue
|
||||
const src = mediaAbsPath(http.root, m.baseUrl, m.musicPath)
|
||||
const dest = src.replace(/\.flac$/i, '.wav')
|
||||
await window.api.convert.start(src, dest)
|
||||
}
|
||||
function isFlac(musicPath: string): boolean {
|
||||
return /\.flac$/i.test(musicPath)
|
||||
}
|
||||
|
||||
export default function Transfer(): JSX.Element {
|
||||
@@ -116,6 +99,8 @@ export default function Transfer(): JSX.Element {
|
||||
const setIpIndex = useLanStore((s) => s.setIpIndex)
|
||||
const loadLan = useLanStore((s) => s.load)
|
||||
const [connected, setConnected] = useState(false)
|
||||
/** 本轮传输是否已结束(socket 可能仍开着) */
|
||||
const [transferDone, setTransferDone] = useState(false)
|
||||
const [phoneSystem, setPhoneSystem] = useState('')
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||
const [overwrite, setOverwrite] = useState(false)
|
||||
@@ -130,6 +115,21 @@ export default function Transfer(): JSX.Element {
|
||||
const finalListRef = useRef<DownloadMusic[]>([])
|
||||
const overwriteRef = useRef(false)
|
||||
const phoneSystemRef = useRef('')
|
||||
/** 断开 / 离开 / 手机停止时递增,进行中的转码与下发看到代际变化后退出 */
|
||||
const transferGenRef = useRef(0)
|
||||
/** 是否仍有传歌任务(含手机还在拉文件) */
|
||||
const transferringRef = useRef(false)
|
||||
const abortTransferRef = useRef<(notifyPhone: boolean) => void>(() => {})
|
||||
abortTransferRef.current = (notifyPhone: boolean) => {
|
||||
const active = transferringRef.current
|
||||
console.warn(
|
||||
`[transfer] abort ${JSON.stringify({ notifyPhone, active, gen: transferGenRef.current })}`
|
||||
)
|
||||
transferGenRef.current += 1
|
||||
transferringRef.current = false
|
||||
void window.api.convert.stop()
|
||||
if (notifyPhone && active) void window.api.musicServer.send(MusicCmd.STOP, '')
|
||||
}
|
||||
const albumById = useMemo(() => {
|
||||
const map = new Map<string, Album>()
|
||||
for (const a of albums) map.set(a.albumUId, a)
|
||||
@@ -203,13 +203,24 @@ export default function Transfer(): JSX.Element {
|
||||
cmd?: string
|
||||
body?: string
|
||||
reason?: string
|
||||
code?: number
|
||||
wasClean?: boolean
|
||||
}
|
||||
switch (ev.type) {
|
||||
case 'connected':
|
||||
setConnected(true)
|
||||
setTransferDone(false)
|
||||
setConnStatus('设备已连接,正在握手…')
|
||||
break
|
||||
case 'disconnected':
|
||||
console.warn(
|
||||
`[transfer] disconnected ${JSON.stringify({
|
||||
code: ev.code,
|
||||
reason: ev.reason,
|
||||
wasClean: ev.wasClean
|
||||
})}`
|
||||
)
|
||||
abortTransferRef.current(false)
|
||||
setConnected(false)
|
||||
setPhoneSystem('')
|
||||
setConnStatus('等待手机扫码连接…')
|
||||
@@ -224,6 +235,7 @@ export default function Transfer(): JSX.Element {
|
||||
break
|
||||
case 'system':
|
||||
setPhoneSystem(ev.system || '')
|
||||
setTransferDone(false)
|
||||
setConnStatus(`已连接 ${ev.system} 设备,可开始传输`)
|
||||
break
|
||||
case 'portUnavailable':
|
||||
@@ -238,8 +250,9 @@ export default function Transfer(): JSX.Element {
|
||||
|
||||
return () => {
|
||||
disposed = true
|
||||
abortTransferRef.current(true)
|
||||
off()
|
||||
window.api.musicServer.stop()
|
||||
void window.api.musicServer.stop()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
@@ -248,14 +261,61 @@ export default function Transfer(): JSX.Element {
|
||||
window.api.store.set('transMusic', [...ids])
|
||||
}
|
||||
|
||||
const sendDownloadCmds = (list: DownloadMusic[]): void => {
|
||||
list.forEach((m, i) => {
|
||||
const isLast = i === list.length - 1
|
||||
const sendOneDownload = (m: DownloadMusic, isLast: boolean): Promise<boolean> =>
|
||||
window.api.musicServer.send(
|
||||
MusicCmd.DOWNLOAD,
|
||||
`${m.musicUId}${DOWNLOAD_BODY_SEPARATOR}${isLast}`
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* iOS:转码队列与传输队列分开。
|
||||
* 非 flac 立刻进入传输;每首 flac 转完(或已有 wav)就 push 一条 DOWNLOAD,
|
||||
* 手机开始拉这一首时,下一首继续转。isLast 标在最后一条下发上。
|
||||
*/
|
||||
const pipelineIosTransfer = async (list: DownloadMusic[], gen: number): Promise<void> => {
|
||||
const okReady = await window.api.musicServer.send(MusicCmd.READY, JSON.stringify(list))
|
||||
if (gen !== transferGenRef.current) return
|
||||
if (!okReady) {
|
||||
setJobNotice('发送就绪指令失败,请重新连接手机')
|
||||
setProgressMode('idle')
|
||||
return
|
||||
}
|
||||
|
||||
const flacs = list.filter((m) => isFlac(m.musicPath))
|
||||
const readyNow = list.filter((m) => !isFlac(m.musicPath))
|
||||
let sent = 0
|
||||
const push = async (m: DownloadMusic): Promise<boolean> => {
|
||||
sent += 1
|
||||
return sendOneDownload(m, sent === list.length)
|
||||
}
|
||||
|
||||
for (const m of readyNow) {
|
||||
if (gen !== transferGenRef.current) return
|
||||
if (!(await push(m))) {
|
||||
setJobNotice('发送下载指令失败,请重新连接手机')
|
||||
setProgressMode('idle')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < flacs.length; i++) {
|
||||
if (gen !== transferGenRef.current) return
|
||||
const m = flacs[i]
|
||||
setJobNotice(
|
||||
sent > 0
|
||||
? `iOS:转换 ${i + 1}/${flacs.length},已开始传输 ${sent} 首…`
|
||||
: `iOS:转换 ${i + 1}/${flacs.length}…`
|
||||
)
|
||||
await window.api.wifi.ensureIosWav([{ baseUrl: m.baseUrl, musicPath: m.musicPath }])
|
||||
if (gen !== transferGenRef.current) return
|
||||
if (!(await push(m))) {
|
||||
setJobNotice('发送下载指令失败,请重新连接手机')
|
||||
setProgressMode('idle')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (gen === transferGenRef.current) setJobNotice('正在传输…')
|
||||
}
|
||||
|
||||
const handlePhoneCmd = async (cmd: string, body: string): Promise<void> => {
|
||||
@@ -283,18 +343,21 @@ export default function Transfer(): JSX.Element {
|
||||
return
|
||||
}
|
||||
finalListRef.current = list
|
||||
transferringRef.current = true
|
||||
setProgressMode('wifi')
|
||||
setProgress({ current: 0, total: list.length })
|
||||
// iOS 需先把 flac 转成 wav,手机按 .wav URL 下载(用已有 convert.start,避免 preload 未热更新)
|
||||
if (phoneSystemRef.current.toLowerCase() === 'ios') {
|
||||
setJobNotice('iOS:正在转换 flac→wav…')
|
||||
const gen = transferGenRef.current
|
||||
try {
|
||||
await ensureIosWavWithConvert(list)
|
||||
await pipelineIosTransfer(list, gen)
|
||||
} catch (e) {
|
||||
console.error('[transfer] ios pipeline failed', e)
|
||||
if (gen !== transferGenRef.current) return
|
||||
window.api.musicServer.send(MusicCmd.STOP, '')
|
||||
setJobNotice(`iOS 转码失败:${(e as Error).message || '未知错误'}`)
|
||||
setProgressMode('idle')
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
const okReady = await window.api.musicServer.send(MusicCmd.READY, JSON.stringify(list))
|
||||
@@ -303,8 +366,9 @@ export default function Transfer(): JSX.Element {
|
||||
setProgressMode('idle')
|
||||
return
|
||||
}
|
||||
|
||||
sendDownloadCmds(list)
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
void sendOneDownload(list[i], i === list.length - 1)
|
||||
}
|
||||
setJobNotice('正在传输…')
|
||||
break
|
||||
}
|
||||
@@ -316,8 +380,16 @@ export default function Transfer(): JSX.Element {
|
||||
setProgress((p) => ({ ...p, current: Math.min(p.current + 1, p.total) }))
|
||||
break
|
||||
case MusicCmd.FINISH:
|
||||
case MusicCmd.STOP:
|
||||
abortTransferRef.current(false)
|
||||
setTransferDone(true)
|
||||
setJobNotice('传输完成')
|
||||
setConnStatus('传输完成,连接仍保持,可再次传输')
|
||||
setProgressMode('idle')
|
||||
setProgress({ current: 0, total: 0 })
|
||||
break
|
||||
case MusicCmd.STOP:
|
||||
abortTransferRef.current(false)
|
||||
setJobNotice('传输已停止')
|
||||
setProgressMode('idle')
|
||||
setProgress({ current: 0, total: 0 })
|
||||
break
|
||||
@@ -353,7 +425,9 @@ export default function Transfer(): JSX.Element {
|
||||
setProgress({ current: 0, total: 0 })
|
||||
return
|
||||
}
|
||||
setTransferDone(false)
|
||||
setJobNotice('已发送歌单,等待手机确认…')
|
||||
setConnStatus('正在传输…')
|
||||
}
|
||||
|
||||
const exportSongs = async (platform: 'android' | 'ios'): Promise<void> => {
|
||||
@@ -478,7 +552,9 @@ export default function Transfer(): JSX.Element {
|
||||
{connected ? <CheckCircle2 size={16} /> : <Wifi size={16} />}
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-semibold">{connected ? '设备已连接' : '扫码连接手机'}</p>
|
||||
<p className="text-sm font-semibold">
|
||||
{connected ? (transferDone ? '传输完成' : '设备已连接') : '扫码连接手机'}
|
||||
</p>
|
||||
<p className="max-w-[220px] text-xs text-[var(--text-secondary)]">{connStatus}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user