diff --git a/package-lock.json b/package-lock.json index c225e72..6f174a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12370,6 +12370,126 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.28", + "resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.28.tgz", + "integrity": "sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.28", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.28.tgz", + "integrity": "sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.28", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.28.tgz", + "integrity": "sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.28", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.28.tgz", + "integrity": "sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.28", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.28.tgz", + "integrity": "sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.28", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.28.tgz", + "integrity": "sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.28", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.28.tgz", + "integrity": "sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.28", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.28.tgz", + "integrity": "sha512-1gCmpvyhz7DkB1srRItJTnmR2UwQPAUXXIg9r0/56g3O8etGmwlX68skKXJOp9EejW3hhv7nSQUJ2raFiz4MoA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/package.json b/package.json index 387054c..6926e78 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@radix-ui/react-tabs": "^1.1.4", "@radix-ui/react-tooltip": "^1.0.7", "@volcengine/openapi": "^1.18.3", + "ahooks": "^3.8.4", "axios": "^1.6.8", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/renderer/components/TaskControls.tsx b/renderer/components/TaskControls.tsx index 9b8fb77..febd131 100644 --- a/renderer/components/TaskControls.tsx +++ b/renderer/components/TaskControls.tsx @@ -1,12 +1,30 @@ -import { useEffect, useState } from 'react'; +import { + useEffect, + useState, + type Dispatch, + type FC, + type SetStateAction, +} from 'react'; import { Button } from './ui/button'; import { toast } from 'sonner'; -import { isSubtitleFile, needsCoreML } from 'lib/utils'; +import { getNewTaskFiles, needsCoreML } from 'lib/utils'; import { useTranslation } from 'next-i18next'; +import type { IFiles } from '../../types'; +import { Switch } from './ui/switch'; +import useLocalStorageState from 'hooks/useLocalStorageState'; +import { useUpdateEffect } from 'ahooks'; -const TaskControls = ({ files, formData }) => { - const [taskStatus, setTaskStatus] = useState('idle'); +type TaskStatus = 'idle' | 'running' | 'paused' | 'cancelled' | 'completed'; + +const TaskControls: FC<{ + files: IFiles[]; + setFiles: Dispatch>; + formData: any; +}> = ({ files, setFiles, formData }) => { + const [taskStatus, setTaskStatus] = useState('idle'); const { t } = useTranslation(['home', 'common']); + const [autoStartNewTaskWhenRunning, setAutoStartNewTaskWhenRunning] = + useLocalStorageState('auto-start-new-task-when-running', false); useEffect(() => { // 获取当前任务状态 @@ -17,7 +35,7 @@ const TaskControls = ({ files, formData }) => { getCurrentTaskStatus(); // 监听任务状态变化 - const cleanup = window?.ipc?.on('taskComplete', (status: string) => { + const cleanup = window?.ipc?.on('taskComplete', (status: TaskStatus) => { setTaskStatus(status); }); @@ -28,30 +46,27 @@ const TaskControls = ({ files, formData }) => { const handleTask = async () => { if (!files?.length) { - toast(t('common:notification'), { - description: t('home:noTask'), - }); - return; + return toast(t('common:notification'), { description: t('home:noTask') }); } - const isAllFilesProcessed = files.every((item) => { - const basicProcessingDone = item.extractAudio && item.extractSubtitle; - - if (formData.translateProvider === '-1') { - return basicProcessingDone; - } - if (isSubtitleFile(item?.filePath)) { - return item.translateSubtitle; - } - return basicProcessingDone && item.translateSubtitle; - }); + // when start task button pressed, persist taskType to IFiles + const needPersist = files.some((f) => !f.taskType); + let updatedFiles = files; + if (needPersist) { + updatedFiles = files.map((f) => { + if (f.taskType) return f; + return { ...f, taskType: formData.taskType }; + }); + setFiles(updatedFiles); + } - if (isAllFilesProcessed) { - toast(t('common:notification'), { + const newTaskFiles = getNewTaskFiles(updatedFiles); + if (!newTaskFiles.length) { + return toast(t('common:notification'), { description: t('home:allFilesProcessed'), }); - return; } + // if(formData.model && needsCoreML(formData.model)){ // const checkMlmodel = await window.ipc.invoke('checkMlmodel', formData.model); // if(!checkMlmodel){ @@ -61,8 +76,15 @@ const TaskControls = ({ files, formData }) => { // return; // } // } + setTaskStatus('running'); - window?.ipc?.send('handleTask', { files, formData }); + setFiles((files) => + files.map((f) => { + if (f.sent) return f; + return { ...f, sent: true }; + }), + ); + window?.ipc?.send('handleTask', { files: newTaskFiles, formData }); }; const handlePause = () => { window?.ipc?.send('pauseTask', null); @@ -78,6 +100,18 @@ const TaskControls = ({ files, formData }) => { window?.ipc?.send('cancelTask', null); setTaskStatus('cancelled'); }; + + useUpdateEffect(() => { + if ( + taskStatus === 'running' && + autoStartNewTaskWhenRunning && + files.length && + files.some((f) => !f.sent) + ) { + handleTask(); + } + }, [files.length]); + return (
{(taskStatus === 'idle' || taskStatus === 'completed') && ( @@ -87,6 +121,19 @@ const TaskControls = ({ files, formData }) => { )} {taskStatus === 'running' && ( <> + + + + diff --git a/renderer/hooks/useIpcCommunication.tsx b/renderer/hooks/useIpcCommunication.tsx index ee8d9f9..1c5cd8a 100644 --- a/renderer/hooks/useIpcCommunication.tsx +++ b/renderer/hooks/useIpcCommunication.tsx @@ -1,7 +1,9 @@ -import { useEffect } from 'react'; -import { IFiles } from '../../types'; +import { useEffect, type Dispatch, type SetStateAction } from 'react'; +import type { IFiles } from '../../types'; -export default function useIpcCommunication(setFiles) { +export default function useIpcCommunication( + setFiles: Dispatch>, +) { useEffect(() => { window?.ipc?.on('file-selected', (res: IFiles[]) => { setFiles((prevFiles) => [...prevFiles, ...res]); diff --git a/renderer/lib/utils.ts b/renderer/lib/utils.ts index 35ac0df..2936fc3 100644 --- a/renderer/lib/utils.ts +++ b/renderer/lib/utils.ts @@ -1,5 +1,6 @@ import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; +import type { IFiles } from '../../types'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -323,3 +324,60 @@ export const filterSupportedFiles = (files: File[]) => { ); }); }; + +function fileStatusGetters(file: IFiles) { + const { taskType, extractAudio, extractSubtitle, translateSubtitle } = file; + + function running(): boolean { + return !ended() && !!(extractAudio || extractSubtitle || translateSubtitle); + } + + function ended(): boolean { + return succeed() || failed(); + } + + function succeed(): boolean { + if (taskType === 'generateOnly') + return extractAudio === 'done' && extractSubtitle === 'done'; + if (taskType === 'translateOnly') return translateSubtitle === 'done'; + if (taskType === 'generateAndTranslate') + return ( + extractAudio === 'done' && + extractSubtitle === 'done' && + translateSubtitle === 'done' + ); + } + + function failed(): boolean { + return ( + extractAudio === 'error' || + extractSubtitle === 'error' || + translateSubtitle === 'error' + ); + } + + return { + get running() { + return running(); + }, + get ended() { + return ended(); + }, + get succeed() { + return succeed(); + }, + get failed() { + return failed(); + }, + }; +} + +export function getNewTaskFiles(files: IFiles[]) { + return files.filter((file) => { + if (!file.sent) return true; // not sent yet + const { running, succeed } = fileStatusGetters(file); + if (running) return false; + if (succeed) return false; + return true; // rest are un-started | failed + }); +} diff --git a/renderer/pages/[locale]/home.tsx b/renderer/pages/[locale]/home.tsx index 8e51fa7..16bb479 100644 --- a/renderer/pages/[locale]/home.tsx +++ b/renderer/pages/[locale]/home.tsx @@ -12,9 +12,10 @@ import TaskConfigForm from '@/components/TaskConfigForm'; import TaskListControl from '@/components/TaskListControl'; import { getStaticPaths, makeStaticProperties } from '../../lib/get-static'; import { filterSupportedFiles } from 'lib/utils'; +import type { IFiles } from '../../../types'; export default function Component() { - const [files, setFiles] = useState([]); + const [files, setFiles] = useState([]); const { systemInfo } = useSystemInfo(); const { form, formData } = useFormConfig(); useIpcCommunication(setFiles); @@ -119,7 +120,7 @@ export default function Component() {
- +
{/* */}
diff --git a/renderer/public/locales/en/home.json b/renderer/public/locales/en/home.json index 471d062..ee0b2e5 100644 --- a/renderer/public/locales/en/home.json +++ b/renderer/public/locales/en/home.json @@ -106,5 +106,6 @@ "total": "Total", "translated": "Translated", "completionRate": "Completion rate", - "originalSubtitle": "Original subtitle" + "originalSubtitle": "Original subtitle", + "autoStartNewTaskWhenRunning": "Auto tart new tasks" } diff --git a/renderer/public/locales/zh/home.json b/renderer/public/locales/zh/home.json index 9d9ba5c..298908f 100644 --- a/renderer/public/locales/zh/home.json +++ b/renderer/public/locales/zh/home.json @@ -112,5 +112,6 @@ "total": "总数", "translated": "已翻译", "completionRate": "完成率", - "originalSubtitle": "原文字幕" + "originalSubtitle": "原文字幕", + "autoStartNewTaskWhenRunning": "自动开始新任务" } diff --git a/types/types.ts b/types/types.ts index 964f211..b6755e7 100644 --- a/types/types.ts +++ b/types/types.ts @@ -4,21 +4,32 @@ export interface ISystemInfo { downloadingModels: string[]; } +export type StepKeyPossibleValues = 'loading' | 'done' | 'error'; + +// 任务类型枚举 +export type ITaskType = + | 'generateAndTranslate' + | 'generateOnly' + | 'translateOnly'; + export interface IFiles { uuid: string; filePath: string; fileName: string; fileExtension: string; directory: string; - extractAudio?: boolean; - extractSubtitle?: boolean; - translateSubtitle?: boolean; + extractAudio?: StepKeyPossibleValues; + extractSubtitle?: StepKeyPossibleValues; + translateSubtitle?: StepKeyPossibleValues; audioFile?: string; srtFile?: string; tempSrtFile?: string; tempAudioFile?: string; translatedSrtFile?: string; tempTranslatedSrtFile?: string; + + sent?: boolean; // 是否已发送 + taskType?: ITaskType; } export interface IFormData { diff --git a/yarn.lock b/yarn.lock index be85859..2d38f66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1059,7 +1059,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0": +"@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0": version "7.27.1" resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.27.1.tgz" integrity sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog== @@ -2272,6 +2272,21 @@ agentkeepalive@^4.2.1: dependencies: humanize-ms "^1.2.1" +ahooks@^3.8.4: + version "3.8.4" + resolved "https://registry.npmmirror.com/ahooks/-/ahooks-3.8.4.tgz#ee2a22d52b6ee57743a1f6ab51c91a7c36bcd7c6" + integrity sha512-39wDEw2ZHvypaT14EpMMk4AzosHWt0z9bulY0BeDsvc9PqJEV+Kjh/4TZfftSsotBMq52iYIOFPd3PR56e0ZJg== + dependencies: + "@babel/runtime" "^7.21.0" + dayjs "^1.9.1" + intersection-observer "^0.12.0" + js-cookie "^3.0.5" + lodash "^4.17.21" + react-fast-compare "^3.2.2" + resize-observer-polyfill "^1.5.1" + screenfull "^5.0.0" + tslib "^2.4.1" + ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-2.1.1.tgz" @@ -3021,7 +3036,7 @@ csstype@^3.0.2: resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== -dayjs@^1.11.5: +dayjs@^1.11.5, dayjs@^1.9.1: version "1.11.13" resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz" integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== @@ -4069,6 +4084,11 @@ int64-buffer@^0.99.1007: resolved "https://registry.npmmirror.com/int64-buffer/-/int64-buffer-0.99.1007.tgz" integrity sha512-XDBEu44oSTqlvCSiOZ/0FoUkpWu/vwjJLGSKDabNISPQNZ5wub1FodGHBljRsrR0IXRPq7SslshZYMuA55CgTQ== +intersection-observer@^0.12.0: + version "0.12.2" + resolved "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz#4a45349cc0cd91916682b1f44c28d7ec737dc375" + integrity sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg== + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz" @@ -4232,6 +4252,11 @@ jiti@^1.21.6: resolved "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz" integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz" @@ -5199,7 +5224,7 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.2" -react-fast-compare@^3.0.1: +react-fast-compare@^3.0.1, react-fast-compare@^3.2.2: version "3.2.2" resolved "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz" integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== @@ -5371,6 +5396,11 @@ require-from-string@^2.0.2: resolved "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-alpn@^1.0.0: version "1.2.1" resolved "https://registry.npmmirror.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz" @@ -5487,6 +5517,11 @@ schema-utils@^4.0.0, schema-utils@^4.3.0: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +screenfull@^5.0.0: + version "5.2.0" + resolved "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba" + integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA== + seek-bzip@^1.0.5: version "1.0.6" resolved "https://registry.npmmirror.com/seek-bzip/-/seek-bzip-1.0.6.tgz" @@ -5988,7 +6023,7 @@ tsconfig-paths@^4.1.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0: +tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1: version "2.8.1" resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==