diff --git a/README.md b/README.md index 65ec366..8a8b460 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,4 @@ BiliBili ShadowReplay 是一个缓存直播并进行实时编辑投稿的工具 ## 赞助 -donate +donate diff --git a/src-tauri/capabilities/migrated.json b/src-tauri/capabilities/migrated.json index 35e13ed..5142d26 100644 --- a/src-tauri/capabilities/migrated.json +++ b/src-tauri/capabilities/migrated.json @@ -52,6 +52,9 @@ }, { "url": "http://tauri.localhost/*" + }, + { + "url": "http://localhost:8054/*" } ] }, diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index 7f371d1..a491c4e 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main","Live*","Clip*"],"permissions":["core:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists",{"identifier":"fs:scope","allow":["**"]},"core:window:default","core:window:allow-start-dragging","core:window:allow-close","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-set-title","sql:allow-execute","shell:allow-open","dialog:allow-open","dialog:allow-save","dialog:allow-message","dialog:allow-ask","dialog:allow-confirm",{"identifier":"http:default","allow":[{"url":"https://*.hdslb.com/"},{"url":"https://afdian.com/"},{"url":"https://*.afdiancdn.com/"},{"url":"https://*.douyin.com/"},{"url":"https://*.douyinpic.com/"},{"url":"http://tauri.localhost/*"}]},"dialog:default","shell:default","fs:default","http:default","sql:default","os:default","notification:default","dialog:default","fs:default","http:default","shell:default","sql:default","os:default","dialog:default","deep-link:default"]}} \ No newline at end of file +{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main","Live*","Clip*"],"permissions":["core:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists",{"identifier":"fs:scope","allow":["**"]},"core:window:default","core:window:allow-start-dragging","core:window:allow-close","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-set-title","sql:allow-execute","shell:allow-open","dialog:allow-open","dialog:allow-save","dialog:allow-message","dialog:allow-ask","dialog:allow-confirm",{"identifier":"http:default","allow":[{"url":"https://*.hdslb.com/"},{"url":"https://afdian.com/"},{"url":"https://*.afdiancdn.com/"},{"url":"https://*.douyin.com/"},{"url":"https://*.douyinpic.com/"},{"url":"http://tauri.localhost/*"},{"url":"http://localhost:8054/*"}]},"dialog:default","shell:default","fs:default","http:default","sql:default","os:default","notification:default","dialog:default","fs:default","http:default","shell:default","sql:default","os:default","dialog:default","deep-link:default"]}} \ No newline at end of file diff --git a/src-tauri/src/http_server/websocket.rs b/src-tauri/src/http_server/websocket.rs index 39faee9..fcf71df 100644 --- a/src-tauri/src/http_server/websocket.rs +++ b/src-tauri/src/http_server/websocket.rs @@ -29,13 +29,10 @@ pub async fn create_websocket_server(state: State) -> SocketIoLayer { Ok(event) => { let (event_type, message) = match event { Event::ProgressUpdate { id, content } => ( - "progress", + "progress-update", json!({ - "event": "progress-update", - "data": { "id": id, "content": content - } }), ), Event::ProgressFinished { @@ -43,25 +40,19 @@ pub async fn create_websocket_server(state: State) -> SocketIoLayer { success, message, } => ( - "progress", + "progress-finished", json!({ - "event": "progress-finished", - "data": { "id": id, "success": success, "message": message - } }), ), Event::DanmuReceived { room, ts, content } => ( "danmu", json!({ - "event": "danmu-received", - "data": { "room": room, "ts": ts, "content": content - } }), ), }; diff --git a/src-tauri/src/progress/progress_reporter.rs b/src-tauri/src/progress/progress_reporter.rs index 8a39e46..8f6403a 100644 --- a/src-tauri/src/progress/progress_reporter.rs +++ b/src-tauri/src/progress/progress_reporter.rs @@ -76,7 +76,10 @@ impl EventEmitter { match event { Event::ProgressUpdate { id, content } => { self.app_handle - .emit("progress-update", UpdateEvent { id, content }) + .emit( + &format!("progress-update:{}", id), + UpdateEvent { id, content }, + ) .unwrap(); } Event::ProgressFinished { @@ -86,7 +89,7 @@ impl EventEmitter { } => { self.app_handle .emit( - "progress-finished", + &format!("progress-finished:{}", id), FinishEvent { id, success: *success, diff --git a/src/AppLive.svelte b/src/AppLive.svelte index 915fd71..1b03dec 100644 --- a/src/AppLive.svelte +++ b/src/AppLive.svelte @@ -173,30 +173,7 @@ } } - const update_listener = listen(`progress-update`, (e) => { - let event_id = e.payload.id; - if (event_id === current_clip_event_id) { - update_clip_prompt(e.payload.content); - } - }); - const finished_listener = listen( - `progress-finished`, - (e) => { - let event_id = e.payload.id; - if (event_id === current_clip_event_id) { - update_clip_prompt(`生成切片`); - if (!e.payload.success) { - alert("请检查 ffmpeg 是否配置正确:" + e.payload.message); - } - current_clip_event_id = null; - } - } - ); - onDestroy(() => { - update_listener?.then((fn) => fn()); - finished_listener?.then((fn) => fn()); - // 清理滚动定时器 if (scroll_timeout) { clearTimeout(scroll_timeout); @@ -328,6 +305,25 @@ update_clip_prompt(`切片生成中`); let event_id = generateEventId(); current_clip_event_id = event_id; + const clear_update_listener = await listen( + `progress-update:${event_id}`, + (e) => { + update_clip_prompt(e.payload.content); + } + ); + const clear_finished_listener = await listen( + `progress-finished:${event_id}`, + (e) => { + update_clip_prompt(`生成切片`); + if (!e.payload.success) { + alert("请检查 ffmpeg 是否配置正确:" + e.payload.message); + } + current_clip_event_id = null; + + clear_update_listener(); + clear_finished_listener(); + } + ); let new_video = (await clipRange(event_id, { title: archive.title, note: clip_note, diff --git a/src/lib/components/ImportVideoDialog.svelte b/src/lib/components/ImportVideoDialog.svelte index f3c44e1..09b38f1 100644 --- a/src/lib/components/ImportVideoDialog.svelte +++ b/src/lib/components/ImportVideoDialog.svelte @@ -55,47 +55,6 @@ return size.toFixed(decimals) + " " + units[unitIndex]; } - // 进度监听器 - const progressUpdateListener = listen( - "progress-update", - (e) => { - if (e.payload.id === currentImportEventId) { - importProgress = e.payload.content; - - // 从进度文本中提取当前文件索引 - const match = importProgress.match(/正在导入第(\d+)个/); - if (match) { - currentFileIndex = parseInt(match[1]); - } - } - } - ); - - const progressFinishedListener = listen( - "progress-finished", - (e) => { - if (e.payload.id === currentImportEventId) { - if (e.payload.success) { - // 导入成功,关闭对话框并刷新列表 - showDialog = false; - selectedFilePath = null; - selectedFileName = ""; - selectedFileSize = 0; - videoTitle = ""; - resetBatchImportState(); - dispatch("imported"); - } else { - alert("导入失败: " + e.payload.message); - resetBatchImportState(); - } - // 无论成功失败都要重置状态 - importing = false; - currentImportEventId = null; - importProgress = ""; - } - } - ); - // 连接恢复时检查任务状态 async function checkTaskStatus() { if (!currentImportEventId || !importing) return; @@ -114,11 +73,6 @@ } } - onDestroy(() => { - progressUpdateListener?.then((fn) => fn()); - progressFinishedListener?.then((fn) => fn()); - }); - async function handleFileSelect() { if (TAURI_ENV) { // Tauri模式:使用文件对话框,支持多选 @@ -396,13 +350,49 @@ const eventId = "batch_import_" + Date.now(); currentImportEventId = eventId; + const clear_update_listener = await listen( + `progress-update:${eventId}`, + (e) => { + importProgress = e.payload.content; + + // 从进度文本中提取当前文件索引 + const match = importProgress.match(/正在导入第(\d+)个/); + if (match) { + currentFileIndex = parseInt(match[1]); + } + } + ); + const clear_finished_listener = await listen( + `progress-finished:${eventId}`, + (e) => { + if (e.payload.success) { + // 导入成功,关闭对话框并刷新列表 + showDialog = false; + selectedFilePath = null; + selectedFileName = ""; + selectedFileSize = 0; + videoTitle = ""; + resetBatchImportState(); + dispatch("imported"); + } else { + alert("导入失败: " + e.payload.message); + resetBatchImportState(); + } + // 无论成功失败都要重置状态 + importing = false; + currentImportEventId = null; + importProgress = ""; + + clear_update_listener(); + clear_finished_listener(); + } + ); + await invoke("batch_import_external_videos", { eventId: eventId, filePaths: selectedFiles, roomId: roomId || 0, }); - - // 注意:成功处理在 progressFinishedListener 中进行 } catch (error) { console.error("批量导入失败:", error); alert("批量导入失败: " + error); @@ -433,6 +423,44 @@ const eventId = "import_" + Date.now(); currentImportEventId = eventId; + const clear_update_listener = await listen( + `progress-update:${eventId}`, + (e) => { + importProgress = e.payload.content; + + // 从进度文本中提取当前文件索引 + const match = importProgress.match(/正在导入第(\d+)个/); + if (match) { + currentFileIndex = parseInt(match[1]); + } + } + ); + const clear_finished_listener = await listen( + `progress-finished:${eventId}`, + (e) => { + if (e.payload.success) { + // 导入成功,关闭对话框并刷新列表 + showDialog = false; + selectedFilePath = null; + selectedFileName = ""; + selectedFileSize = 0; + videoTitle = ""; + resetBatchImportState(); + dispatch("imported"); + } else { + alert("导入失败: " + e.payload.message); + resetBatchImportState(); + } + // 无论成功失败都要重置状态 + importing = false; + currentImportEventId = null; + importProgress = ""; + + clear_update_listener(); + clear_finished_listener(); + } + ); + await invoke("import_external_video", { eventId: eventId, filePath: selectedFilePath, diff --git a/src/lib/components/Player.svelte b/src/lib/components/Player.svelte index 5d1c1c5..ab7e41b 100644 --- a/src/lib/components/Player.svelte +++ b/src/lib/components/Player.svelte @@ -413,15 +413,11 @@ ${mediaPlaylistUrl}`; } // listen to danmaku event - await listen("danmu", (event: { payload: DanmuEntry }) => { + await listen(`danmu:${room_id}`, (event: { payload: DanmuEntry }) => { if (global_offset == 0) { return; } - if (event.payload.room != room_id) { - return; - } - if (event.payload.ts < global_offset * 1000) { log.error("invalid danmu ts:", event.payload.ts, global_offset); return; diff --git a/src/lib/components/VideoPreview.svelte b/src/lib/components/VideoPreview.svelte index 7817920..70226c7 100644 --- a/src/lib/components/VideoPreview.svelte +++ b/src/lib/components/VideoPreview.svelte @@ -178,63 +178,7 @@ } }); - const update_listener = listen(`progress-update`, (e) => { - let event_id = e.payload.id; - console.log(e.payload); - if (event_id == current_encode_event_id) { - update_encode_prompt(e.payload.content); - } else if (event_id == current_generate_event_id) { - update_generate_prompt(e.payload.content); - } else if (event_id === current_post_event_id) { - update_post_prompt(e.payload.content); - } else if (event_id === current_clip_event_id) { - update_clip_prompt(e.payload.content); - } - }); - - const finish_listener = listen(`progress-finished`, (e) => { - let event_id = e.payload.id; - if (event_id == current_encode_event_id) { - update_encode_prompt(`压制字幕`); - if (!e.payload.success) { - alert("压制失败: " + e.payload.message); - } - current_encode_event_id = null; - } else if (event_id == current_generate_event_id) { - update_generate_prompt(`AI 生成字幕`); - if (!e.payload.success) { - alert("生成字幕失败: " + e.payload.message); - } - current_generate_event_id = null; - } else if (event_id === current_post_event_id) { - update_post_prompt(`投稿`); - if (!e.payload.success) { - alert(e.payload.message); - } - current_post_event_id = null; - } else if (event_id === current_clip_event_id) { - update_clip_prompt(`生成切片`); - if (e.payload.success) { - // 切片生成成功,刷新视频列表 - if (onVideoListUpdate) { - onVideoListUpdate(); - } - // 重置切片设置 - clipStartTime = 0; - clipEndTime = 0; - clipTitle = ""; - clipTimesSet = false; // 重置标记 - } else { - alert("切片生成失败: " + e.payload.message); - } - current_clip_event_id = null; - clipping = false; - } - }); - onDestroy(() => { - update_listener?.then((fn) => fn()); - finish_listener?.then((fn) => fn()); // 清理窗口关闭事件监听器 if (windowCloseUnlisten) { windowCloseUnlisten(); @@ -272,6 +216,28 @@ current_post_event_id = event_id; update_post_prompt(`投稿上传中`); + + const clear_update_listener = await listen( + `progress-update:${event_id}`, + (e) => { + update_post_prompt(e.payload.content); + } + ); + const clear_finished_listener = await listen( + `progress-finished:${event_id}`, + (e) => { + update_post_prompt(`投稿`); + if (!e.payload.success) { + alert(e.payload.message); + } + + current_post_event_id = null; + + clear_update_listener(); + clear_finished_listener(); + } + ); + // update profile in local storage window.localStorage.setItem("profile-" + roomId, JSON.stringify(profile)); invoke("upload_procedure", { @@ -426,6 +392,26 @@ async function generateSubtitles() { if (video?.file) { current_generate_event_id = generateEventId(); + const clear_update_listener = await listen( + `progress-update:${current_generate_event_id}`, + (e) => { + update_generate_prompt(e.payload.content); + } + ); + const clear_finished_listener = await listen( + `progress-finished:${current_generate_event_id}`, + (e) => { + update_generate_prompt(`AI 生成字幕`); + if (!e.payload.success) { + alert("生成字幕失败: " + e.payload.message); + } + + current_generate_event_id = null; + + clear_update_listener(); + clear_finished_listener(); + } + ); const savedSubtitles = (await invoke("generate_video_subtitle", { eventId: current_generate_event_id, id: video.id, @@ -614,6 +600,37 @@ clipping = true; current_clip_event_id = generateEventId(); + const clear_update_listener = await listen( + `progress-update:${current_clip_event_id}`, + (e) => { + update_clip_prompt(e.payload.content); + } + ); + const clear_finished_listener = await listen( + `progress-finished:${current_clip_event_id}`, + (e) => { + update_clip_prompt(`生成切片`); + if (e.payload.success) { + // 切片生成成功,刷新视频列表 + if (onVideoListUpdate) { + onVideoListUpdate(); + } + // 重置切片设置 + clipStartTime = 0; + clipEndTime = 0; + clipTitle = ""; + clipTimesSet = false; // 重置标记 + } else { + alert("切片生成失败: " + e.payload.message); + } + clipping = false; + + current_clip_event_id = null; + + clear_update_listener(); + clear_finished_listener(); + } + ); try { await invoke("clip_video", { @@ -1079,6 +1096,26 @@ await saveSubtitles(); const event_id = generateEventId(); current_encode_event_id = event_id; + const clear_update_listener = await listen( + `progress-update:${event_id}`, + (e) => { + update_encode_prompt(e.payload.content); + } + ); + const clear_finished_listener = await listen( + `progress-finished:${event_id}`, + (e) => { + update_encode_prompt(`压制字幕`); + if (!e.payload.success) { + alert("压制失败: " + e.payload.message); + } + + current_encode_event_id = null; + + clear_update_listener(); + clear_finished_listener(); + } + ); const result = await invoke("encode_video_subtitle", { eventId: event_id, id: video.id, diff --git a/src/lib/invoker.ts b/src/lib/invoker.ts index e7e82a6..f21da6f 100644 --- a/src/lib/invoker.ts +++ b/src/lib/invoker.ts @@ -204,17 +204,15 @@ function createSocket() { }); // 监听服务器发送的事件 - socket.on("progress", (data) => { - const eventType = data.event || "message"; - + socket.on("progress-finished", (data) => { + const eventType = `progress-finished:${data.id}`; // 触发对应的事件监听器 const listeners = eventListeners.get(eventType); if (listeners) { listeners.forEach((callback) => { try { callback({ - type: eventType, - payload: data.data, + payload: data, }); } catch (e) { console.error( @@ -226,15 +224,28 @@ function createSocket() { } }); + socket.on("progress-update", (data) => { + const eventType = `progress-update:${data.id}`; + const listeners = eventListeners.get(eventType); + if (listeners) { + listeners.forEach((callback) => { + callback({ + payload: data, + }); + }); + } + }); + socket.on("danmu", (data) => { // 触发对应的事件监听器 - const listeners = eventListeners.get("danmu"); + console.log("danmu:", data); + const room_id = data.room; + const listeners = eventListeners.get(`danmu:${room_id}`); if (listeners) { listeners.forEach((callback) => { try { callback({ - type: "danmu", - payload: data.data, + payload: data, }); } catch (e) { console.error(`[Socket.IO] Event listener error for danmu:`, e); @@ -249,6 +260,7 @@ if (!TAURI_ENV) { } async function listen(event: string, callback: (data: any) => void) { + log.info("listen to event:", event); if (TAURI_ENV) { return await tauri_listen(event, callback); }