diff --git a/resources/i18n/en.yml b/resources/i18n/en.yml index 04bd3fa6..d5200cf4 100644 --- a/resources/i18n/en.yml +++ b/resources/i18n/en.yml @@ -69,3 +69,5 @@ TIPS_UPDATE_DOWNLOADED: The update has been downloaded and will be installed on QUIT: Quit OPERATION_SUCCEED: "Operation Succeed" OPERATION_FAILED: "Operation Failed" +UPLOAD_TASK_COMPLETED: "Upload Task Completed" +UPLOAD_TASK_COMPLETED_BODY: "${completed} succeeded, ${failed} failed" diff --git a/resources/i18n/zh-CN.yml b/resources/i18n/zh-CN.yml index c72e7bbd..0088e5ca 100644 --- a/resources/i18n/zh-CN.yml +++ b/resources/i18n/zh-CN.yml @@ -69,3 +69,5 @@ UPDATE: 更新 QUIT: 退出 OPERATION_SUCCEED: "操作成功" OPERATION_FAILED: "操作失败" +UPLOAD_TASK_COMPLETED: "上传任务完成" +UPLOAD_TASK_COMPLETED_BODY: "${completed} 成功, ${failed} 失败" diff --git a/resources/i18n/zh-TW.yml b/resources/i18n/zh-TW.yml index 803dfbb8..f85f89bc 100644 --- a/resources/i18n/zh-TW.yml +++ b/resources/i18n/zh-TW.yml @@ -69,3 +69,5 @@ UPDATE: 更新 QUIT: 退出 OPERATION_SUCCEED: "操作成功" OPERATION_FAILED: "操作失敗" +UPLOAD_TASK_COMPLETED: "上傳任務完成" +UPLOAD_TASK_COMPLETED_BODY: "${completed} 成功, ${failed} 失敗" diff --git a/src/main/events/rpc/routes/upload/index.ts b/src/main/events/rpc/routes/upload/index.ts index 96748516..733b822d 100644 --- a/src/main/events/rpc/routes/upload/index.ts +++ b/src/main/events/rpc/routes/upload/index.ts @@ -3,6 +3,7 @@ import { uploadChoosedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis import { RPCRouter } from '~/events/rpc/router' import { IRPCActionType, IRPCType } from '~/utils/enum' import getPicBeds from '~/utils/getPicBeds' +import UploadTaskQueueManager from '~/utils/uploadTaskQueue' const uploadRouter = new RPCRouter() @@ -26,6 +27,170 @@ const uploadRoutes = [ return uploadChoosedFiles(evt.sender, args[0]) }, }, + // Upload task queue routes + { + action: IRPCActionType.UPLOAD_TASK_ADD, + handler: async (evt: IIPCEvent, args: [files: IFileWithPath[]]) => { + const manager = UploadTaskQueueManager.getInstance() + manager.setWebContents(evt.sender) + return manager.addTasks(args[0]) + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_START, + handler: async (evt: IIPCEvent, args: [intervalS?: number]) => { + const manager = UploadTaskQueueManager.getInstance() + manager.setWebContents(evt.sender) + await manager.startQueue(args[0]) + return manager.getQueueStatus() + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_PAUSE, + handler: async () => { + const manager = UploadTaskQueueManager.getInstance() + manager.pauseQueue() + return manager.getQueueStatus() + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_RESUME, + handler: async (evt: IIPCEvent) => { + const manager = UploadTaskQueueManager.getInstance() + manager.setWebContents(evt.sender) + await manager.resumeQueue() + return manager.getQueueStatus() + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_CANCEL_ALL, + handler: async () => { + const manager = UploadTaskQueueManager.getInstance() + manager.cancelQueue() + return manager.getQueueStatus() + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_CANCEL_ONE, + handler: async (_: IIPCEvent, args: [taskId: string]) => { + const manager = UploadTaskQueueManager.getInstance() + return manager.cancelTask(args[0]) + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_REMOVE_ONE, + handler: async (_: IIPCEvent, args: [taskId: string]) => { + const manager = UploadTaskQueueManager.getInstance() + return manager.removeTask(args[0]) + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_CLEAR_FINISHED, + handler: async () => { + const manager = UploadTaskQueueManager.getInstance() + manager.clearFinishedTasks() + return manager.getQueueStatus() + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_CLEAR_ALL, + handler: async () => { + const manager = UploadTaskQueueManager.getInstance() + manager.clearAllTasks() + return manager.getQueueStatus() + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_GET_STATUS, + handler: async () => { + const manager = UploadTaskQueueManager.getInstance() + return manager.getQueueStatus() + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_SET_INTERVAL, + handler: async (_: IIPCEvent, args: [intervalS: number]) => { + const manager = UploadTaskQueueManager.getInstance() + manager.setInterval(args[0]) + return manager.getInterval() + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_GET_INTERVAL, + handler: async () => { + const manager = UploadTaskQueueManager.getInstance() + return manager.getInterval() + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_RETRY_ONE, + handler: async (_: IIPCEvent, args: [taskId: string]) => { + const manager = UploadTaskQueueManager.getInstance() + return manager.retryTask(args[0]) + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_RETRY_ALL_FAILED, + handler: async () => { + const manager = UploadTaskQueueManager.getInstance() + return manager.retryAllFailed() + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_MOVE_UP, + handler: async (_: IIPCEvent, args: [taskId: string]) => { + const manager = UploadTaskQueueManager.getInstance() + return manager.moveTaskUp(args[0]) + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_MOVE_DOWN, + handler: async (_: IIPCEvent, args: [taskId: string]) => { + const manager = UploadTaskQueueManager.getInstance() + return manager.moveTaskDown(args[0]) + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_SET_PRIORITY, + handler: async (_: IIPCEvent, args: [taskId: string, priority: number]) => { + const manager = UploadTaskQueueManager.getInstance() + return manager.setTaskPriority(args[0], args[1]) + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_UPDATE_SETTINGS, + handler: async (_: IIPCEvent, args: [settings: any]) => { + const manager = UploadTaskQueueManager.getInstance() + manager.updateSettings(args[0]) + return manager.getSettings() + }, + type: IRPCType.INVOKE, + }, + { + action: IRPCActionType.UPLOAD_TASK_GET_SETTINGS, + handler: async () => { + const manager = UploadTaskQueueManager.getInstance() + return manager.getSettings() + }, + type: IRPCType.INVOKE, + }, ] uploadRouter.addBatch(uploadRoutes) diff --git a/src/main/utils/enum.ts b/src/main/utils/enum.ts index 1b37fab6..b6835238 100644 --- a/src/main/utils/enum.ts +++ b/src/main/utils/enum.ts @@ -139,6 +139,27 @@ export const IRPCActionType = { UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE: 'UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE', UPLOAD_CHOOSED_FILES: 'UPLOAD_CHOOSED_FILES', + // upload task queue rpc + UPLOAD_TASK_ADD: 'UPLOAD_TASK_ADD', + UPLOAD_TASK_START: 'UPLOAD_TASK_START', + UPLOAD_TASK_PAUSE: 'UPLOAD_TASK_PAUSE', + UPLOAD_TASK_RESUME: 'UPLOAD_TASK_RESUME', + UPLOAD_TASK_CANCEL_ALL: 'UPLOAD_TASK_CANCEL_ALL', + UPLOAD_TASK_CANCEL_ONE: 'UPLOAD_TASK_CANCEL_ONE', + UPLOAD_TASK_REMOVE_ONE: 'UPLOAD_TASK_REMOVE_ONE', + UPLOAD_TASK_CLEAR_FINISHED: 'UPLOAD_TASK_CLEAR_FINISHED', + UPLOAD_TASK_CLEAR_ALL: 'UPLOAD_TASK_CLEAR_ALL', + UPLOAD_TASK_GET_STATUS: 'UPLOAD_TASK_GET_STATUS', + UPLOAD_TASK_SET_INTERVAL: 'UPLOAD_TASK_SET_INTERVAL', + UPLOAD_TASK_GET_INTERVAL: 'UPLOAD_TASK_GET_INTERVAL', + UPLOAD_TASK_RETRY_ONE: 'UPLOAD_TASK_RETRY_ONE', + UPLOAD_TASK_RETRY_ALL_FAILED: 'UPLOAD_TASK_RETRY_ALL_FAILED', + UPLOAD_TASK_MOVE_UP: 'UPLOAD_TASK_MOVE_UP', + UPLOAD_TASK_MOVE_DOWN: 'UPLOAD_TASK_MOVE_DOWN', + UPLOAD_TASK_SET_PRIORITY: 'UPLOAD_TASK_SET_PRIORITY', + UPLOAD_TASK_UPDATE_SETTINGS: 'UPLOAD_TASK_UPDATE_SETTINGS', + UPLOAD_TASK_GET_SETTINGS: 'UPLOAD_TASK_GET_SETTINGS', + // gallery rpc GALLERY_PASTE_TEXT: 'GALLERY_PASTE_TEXT', GALLERY_REMOVE_FILES: 'GALLERY_REMOVE_FILES', diff --git a/src/main/utils/uploadTaskQueue.ts b/src/main/utils/uploadTaskQueue.ts new file mode 100644 index 00000000..3fecc92a --- /dev/null +++ b/src/main/utils/uploadTaskQueue.ts @@ -0,0 +1,631 @@ +import path from 'node:path' + +import { GalleryDB } from '@core/datastore' +import db from '@core/datastore' +import picgo from '@core/picgo' +import uploader from 'apis/app/uploader' +import windowManager from 'apis/app/window/windowManager' +import { app, Notification, WebContents } from 'electron' +import fs from 'fs-extra' +import { cloneDeep } from 'lodash-es' +import { v4 as uuid } from 'uuid' + +import { T as $t } from '~/i18n/index' +import { handleCopyUrl, handleUrlEncodeWithSetting } from '~/utils/common' +import { configPaths } from '~/utils/configPaths' +import { IPasteStyle, IWindowList } from '~/utils/enum' +import pasteTemplate from '~/utils/pasteTemplate' + +export const UploadTaskStatus = { + PENDING: 'pending', + UPLOADING: 'uploading', + COMPLETED: 'completed', + FAILED: 'failed', + CANCELLED: 'cancelled', + PAUSED: 'paused', +} + +export const UploadTaskPriority = { + LOW: 0, + NORMAL: 1, + HIGH: 2, +} + +export interface IUploadTaskItem { + id: string + fileName: string + filePath: string + fileSize: number + status: string + progress: number + error?: string + result?: IStringKeyMap + createdAt: number + startedAt?: number + completedAt?: number + retryCount: number + priority: number + uploadSpeed?: number // bytes per second + uploadDuration?: number // seconds +} + +export interface IUploadTaskQueueConfig { + intervalS: number // Interval between uploads in seconds + isRunning: boolean + isPaused: boolean + autoStart: boolean + pauseOnError: boolean + maxRetryCount: number +} + +class UploadTaskQueueManager { + private static instance: UploadTaskQueueManager + + private taskQueue: IUploadTaskItem[] = [] + private config: IUploadTaskQueueConfig = { + intervalS: 1, // Default 1 second interval + isRunning: false, + isPaused: false, + autoStart: false, + pauseOnError: false, + maxRetryCount: 3, + } + + private webContents: WebContents | null = null + private persistPath = path.join(app.getPath('userData'), 'taskQueue.json') + private taskTimer: NodeJS.Timeout | null = null + + private constructor() { + this.restore() + } + + static getInstance(): UploadTaskQueueManager { + if (!UploadTaskQueueManager.instance) { + UploadTaskQueueManager.instance = new UploadTaskQueueManager() + } + return UploadTaskQueueManager.instance + } + + setWebContents(webContents: WebContents): this { + this.webContents = webContents + return this + } + + private getFileSize(filePath: string): number { + try { + if (filePath.startsWith('http://') || filePath.startsWith('https://')) { + return 0 + } + const stats = fs.statSync(filePath) + return stats.size + } catch { + return 0 + } + } + + addTasks(files: IFileWithPath[], priority: number = UploadTaskPriority.NORMAL): IUploadTaskItem[] { + const newTasks: IUploadTaskItem[] = files.map(file => ({ + id: `task_${uuid()}`, + fileName: file.name || path.basename(file.path), + filePath: file.path, + fileSize: this.getFileSize(file.path), + status: UploadTaskStatus.PENDING, + progress: 0, + createdAt: Date.now(), + retryCount: 0, + priority, + })) + + newTasks.forEach(task => { + const insertIndex = this.taskQueue.findIndex( + t => t.status === UploadTaskStatus.PENDING && t.priority < task.priority, + ) + if (insertIndex === -1) { + this.taskQueue.push(task) + } else { + this.taskQueue.splice(insertIndex, 0, task) + } + }) + + this.persist() + this.notifyTaskUpdate() + + if (this.config.autoStart && !this.config.isRunning) { + this.startQueue() + } + + return newTasks + } + + async startQueue(intervalS?: number): Promise { + if (intervalS !== undefined) { + this.config.intervalS = intervalS + } + + if (this.config.isRunning) { + return + } + + this.config.isRunning = true + this.config.isPaused = false + this.persist() + this.notifyTaskUpdate() + + await this.processNextTask() + } + + private async processNextTask(): Promise { + if (!this.config.isRunning || this.config.isPaused) { + return + } + + const pendingTask = this.taskQueue.find(task => task.status === UploadTaskStatus.PENDING) + + if (!pendingTask) { + this.config.isRunning = false + this.persist() + this.notifyTaskUpdate() + this.showCompletionNotification() + return + } + + pendingTask.status = UploadTaskStatus.UPLOADING + pendingTask.startedAt = Date.now() + this.persist() + this.notifyTaskUpdate() + + try { + const result = await this.uploadSingleFile(pendingTask) + + pendingTask.status = UploadTaskStatus.COMPLETED + pendingTask.progress = 100 + pendingTask.completedAt = Date.now() + pendingTask.result = result + + if (pendingTask.startedAt && pendingTask.fileSize > 0) { + pendingTask.uploadDuration = pendingTask.completedAt - pendingTask.startedAt + pendingTask.uploadSpeed = Math.round((pendingTask.fileSize / pendingTask.uploadDuration) * 1000) + } + } catch (error: any) { + pendingTask.error = error.message || 'Upload failed' + pendingTask.completedAt = Date.now() + + if (pendingTask.retryCount < this.config.maxRetryCount) { + pendingTask.retryCount++ + pendingTask.status = UploadTaskStatus.PENDING + pendingTask.startedAt = undefined + pendingTask.completedAt = undefined + pendingTask.error = undefined + } else { + pendingTask.status = UploadTaskStatus.FAILED + + if (this.config.pauseOnError) { + this.config.isPaused = true + this.persist() + this.notifyTaskUpdate() + return + } + } + } + + this.persist() + this.notifyTaskUpdate() + + if (this.config.isRunning && !this.config.isPaused) { + const pendingCount = this.taskQueue.filter(t => t.status === UploadTaskStatus.PENDING).length + if (pendingCount > 0) { + this.taskTimer = setTimeout(() => { + this.processNextTask() + }, this.config.intervalS * 1000) + } else { + this.config.isRunning = false + this.persist() + this.notifyTaskUpdate() + this.showCompletionNotification() + } + } + } + + private async uploadSingleFile(task: IUploadTaskItem): Promise { + const win = windowManager.getAvailableWindow() + const webContents = this.webContents || win?.webContents + + if (!webContents) { + throw new Error('No webContents available for upload') + } + + const input = [task.filePath] + const rawInput = cloneDeep(input) + + let imgs: ImgInfo[] | false = false + imgs = await uploader.setWebContents(webContents).upload(input) + + if (imgs !== false && imgs.length > 0) { + const pasteStyle = db.get(configPaths.settings.pasteStyle) || IPasteStyle.MARKDOWN + const deleteLocalFile = db.get(configPaths.settings.deleteLocalFile) || false + + const img = imgs[0] + + if (deleteLocalFile && !task.filePath.startsWith('http')) { + fs.remove(rawInput[0]) + .then(() => { + picgo.log.info(`delete local file: ${rawInput[0]}`) + }) + .catch((err: Error) => { + picgo.log.error(err) + }) + } + + const [pasteText, shortUrl] = await pasteTemplate(pasteStyle, img, db.get(configPaths.settings.customLink)) + img.shortUrl = shortUrl + + const inserted = await GalleryDB.getInstance().insert(img) + + windowManager.get(IWindowList.TRAY_WINDOW)?.webContents?.send('uploadFiles', [img]) + if (windowManager.has(IWindowList.SETTING_WINDOW)) { + windowManager.get(IWindowList.SETTING_WINDOW)!.webContents?.send('updateGallery') + } + + handleCopyUrl(pasteText) + + return { + url: handleUrlEncodeWithSetting(inserted.imgUrl!), + fullResult: inserted, + } + } + + throw new Error('Upload failed - no result returned') + } + + pauseQueue(): void { + this.config.isPaused = true + if (this.taskTimer) { + clearTimeout(this.taskTimer) + this.taskTimer = null + } + this.persist() + this.notifyTaskUpdate() + } + + async resumeQueue(): Promise { + if (!this.config.isRunning) { + await this.startQueue() + return + } + + this.config.isPaused = false + this.persist() + this.notifyTaskUpdate() + await this.processNextTask() + } + + cancelQueue(): void { + this.config.isRunning = false + this.config.isPaused = false + + if (this.taskTimer) { + clearTimeout(this.taskTimer) + this.taskTimer = null + } + + this.taskQueue.forEach(task => { + if (task.status === UploadTaskStatus.PENDING || task.status === UploadTaskStatus.UPLOADING) { + task.status = UploadTaskStatus.CANCELLED + task.completedAt = Date.now() + } + }) + + this.persist() + this.notifyTaskUpdate() + } + + cancelTask(taskId: string): boolean { + const task = this.taskQueue.find(t => t.id === taskId) + if (task && (task.status === UploadTaskStatus.PENDING || task.status === UploadTaskStatus.UPLOADING)) { + task.status = UploadTaskStatus.CANCELLED + task.completedAt = Date.now() + this.persist() + this.notifyTaskUpdate() + return true + } + return false + } + + removeTask(taskId: string): boolean { + const index = this.taskQueue.findIndex(t => t.id === taskId) + if (index !== -1) { + this.taskQueue.splice(index, 1) + this.persist() + this.notifyTaskUpdate() + return true + } + return false + } + + clearFinishedTasks(): void { + this.taskQueue = this.taskQueue.filter( + task => + task.status === UploadTaskStatus.PENDING || + task.status === UploadTaskStatus.UPLOADING || + task.status === UploadTaskStatus.PAUSED, + ) + this.persist() + this.notifyTaskUpdate() + } + + clearAllTasks(): void { + this.cancelQueue() + this.taskQueue = [] + this.persist() + this.notifyTaskUpdate() + } + + getAllTasks(): IUploadTaskItem[] { + return [...this.taskQueue] + } + + getQueueStatus(): { + tasks: IUploadTaskItem[] + config: IUploadTaskQueueConfig + stats: { + total: number + pending: number + completed: number + failed: number + cancelled: number + uploading: number + totalSize: number + completedSize: number + avgSpeed: number + estimatedTimeMs: number + } + } { + const completedTasks = this.taskQueue.filter(t => t.status === UploadTaskStatus.COMPLETED) + const pendingTasks = this.taskQueue.filter(t => t.status === UploadTaskStatus.PENDING) + const uploadingTasks = this.taskQueue.filter(t => t.status === UploadTaskStatus.UPLOADING) + + const totalSize = this.taskQueue.reduce((sum, t) => sum + (t.fileSize || 0), 0) + const completedSize = completedTasks.reduce((sum, t) => sum + (t.fileSize || 0), 0) + + const tasksWithSpeed = completedTasks.filter(t => t.uploadSpeed && t.uploadSpeed > 0) + const avgSpeed = + tasksWithSpeed.length > 0 + ? Math.round(tasksWithSpeed.reduce((sum, t) => sum + (t.uploadSpeed || 0), 0) / tasksWithSpeed.length) + : 0 + + const remainingSize = + pendingTasks.reduce((sum, t) => sum + (t.fileSize || 0), 0) + + uploadingTasks.reduce((sum, t) => sum + (t.fileSize || 0), 0) + const estimatedTimeMs = + avgSpeed > 0 + ? Math.round((remainingSize / avgSpeed) * 1000) + pendingTasks.length * this.config.intervalS * 1000 + : 0 + + const stats = { + total: this.taskQueue.length, + pending: pendingTasks.length, + completed: completedTasks.length, + failed: this.taskQueue.filter(t => t.status === UploadTaskStatus.FAILED).length, + cancelled: this.taskQueue.filter(t => t.status === UploadTaskStatus.CANCELLED).length, + uploading: uploadingTasks.length, + totalSize, + completedSize, + avgSpeed, + estimatedTimeMs, + } + + return { + tasks: [...this.taskQueue], + config: { ...this.config }, + stats, + } + } + + retryTask(taskId: string): boolean { + const task = this.taskQueue.find(t => t.id === taskId) + if (task && task.status === UploadTaskStatus.FAILED) { + task.status = UploadTaskStatus.PENDING + task.retryCount = 0 + task.error = undefined + task.startedAt = undefined + task.completedAt = undefined + task.progress = 0 + this.persist() + this.notifyTaskUpdate() + return true + } + return false + } + + retryAllFailed(): number { + let count = 0 + this.taskQueue.forEach(task => { + if (task.status === UploadTaskStatus.FAILED) { + task.status = UploadTaskStatus.PENDING + task.retryCount = 0 + task.error = undefined + task.startedAt = undefined + task.completedAt = undefined + task.progress = 0 + count++ + } + }) + if (count > 0) { + this.persist() + this.notifyTaskUpdate() + } + return count + } + + moveTaskUp(taskId: string): boolean { + const index = this.taskQueue.findIndex(t => t.id === taskId) + if (index > 0 && this.taskQueue[index].status === UploadTaskStatus.PENDING) { + let targetIndex = index - 1 + while (targetIndex >= 0 && this.taskQueue[targetIndex].status !== UploadTaskStatus.PENDING) { + targetIndex-- + } + if (targetIndex >= 0) { + const temp = this.taskQueue[index] + this.taskQueue[index] = this.taskQueue[targetIndex] + this.taskQueue[targetIndex] = temp + this.persist() + this.notifyTaskUpdate() + return true + } + } + return false + } + + moveTaskDown(taskId: string): boolean { + const index = this.taskQueue.findIndex(t => t.id === taskId) + if (index < this.taskQueue.length - 1 && this.taskQueue[index].status === UploadTaskStatus.PENDING) { + let targetIndex = index + 1 + while (targetIndex < this.taskQueue.length && this.taskQueue[targetIndex].status !== UploadTaskStatus.PENDING) { + targetIndex++ + } + if (targetIndex < this.taskQueue.length) { + const temp = this.taskQueue[index] + this.taskQueue[index] = this.taskQueue[targetIndex] + this.taskQueue[targetIndex] = temp + this.persist() + this.notifyTaskUpdate() + return true + } + } + return false + } + + setTaskPriority(taskId: string, priority: number): boolean { + const task = this.taskQueue.find(t => t.id === taskId) + if (task && task.status === UploadTaskStatus.PENDING) { + task.priority = priority + this.taskQueue.sort((a, b) => { + if (a.status !== UploadTaskStatus.PENDING && b.status !== UploadTaskStatus.PENDING) return 0 + if (a.status !== UploadTaskStatus.PENDING) return 1 + if (b.status !== UploadTaskStatus.PENDING) return -1 + return b.priority - a.priority + }) + this.persist() + this.notifyTaskUpdate() + return true + } + return false + } + + updateSettings(settings: Partial): void { + if (settings.intervalS !== undefined) { + this.config.intervalS = Math.max(0.1, settings.intervalS) + } + if (settings.autoStart !== undefined) { + this.config.autoStart = settings.autoStart + } + if (settings.pauseOnError !== undefined) { + this.config.pauseOnError = settings.pauseOnError + } + if (settings.maxRetryCount !== undefined) { + this.config.maxRetryCount = Math.max(0, Math.min(10, settings.maxRetryCount)) + } + this.persist() + this.notifyTaskUpdate() + } + + getSettings(): IUploadTaskQueueConfig { + return { ...this.config } + } + + setInterval(intervalS: number): void { + this.config.intervalS = Math.max(0.1, intervalS) // Minimum 0.1 seconds + this.persist() + this.notifyTaskUpdate() + } + + getInterval(): number { + return this.config.intervalS + } + + isRunning(): boolean { + return this.config.isRunning + } + + isPaused(): boolean { + return this.config.isPaused + } + + private showCompletionNotification(): void { + const stats = { + completed: this.taskQueue.filter(t => t.status === UploadTaskStatus.COMPLETED).length, + failed: this.taskQueue.filter(t => t.status === UploadTaskStatus.FAILED).length, + } + + if (stats.completed > 0 || stats.failed > 0) { + const isShowResultNotification = + db.get(configPaths.settings.uploadResultNotification) === undefined + ? true + : !!db.get(configPaths.settings.uploadResultNotification) + + if (isShowResultNotification) { + const notification = new Notification({ + title: $t('UPLOAD_TASK_COMPLETED'), + body: $t('UPLOAD_TASK_COMPLETED_BODY', { completed: stats.completed, failed: stats.failed }), + }) + notification.show() + } + } + } + + private notifyTaskUpdate(): void { + const status = this.getQueueStatus() + windowManager.get(IWindowList.SETTING_WINDOW)?.webContents?.send('uploadTaskQueueUpdate', status) + } + + private persist(): void { + try { + fs.ensureFileSync(this.persistPath) + fs.writeFileSync( + this.persistPath, + JSON.stringify( + { + taskQueue: this.taskQueue, + config: this.config, + }, + null, + 2, + ), + ) + } catch (e) { + console.error('Failed to persist upload task queue:', e) + } + } + + private restore(): void { + try { + if (fs.existsSync(this.persistPath)) { + const data = JSON.parse(fs.readFileSync(this.persistPath, { encoding: 'utf-8' })) + if (data.taskQueue) { + this.taskQueue = data.taskQueue.map((task: IUploadTaskItem) => ({ + ...task, + fileSize: task.fileSize || 0, + retryCount: task.retryCount || 0, + priority: task.priority ?? UploadTaskPriority.NORMAL, + status: task.status === UploadTaskStatus.UPLOADING ? UploadTaskStatus.PENDING : task.status, + })) + } + if (data.config) { + this.config = { + ...this.config, + intervalS: data.config.intervalS || 1, + autoStart: data.config.autoStart || false, + pauseOnError: data.config.pauseOnError || false, + maxRetryCount: data.config.maxRetryCount ?? 3, + isRunning: false, + isPaused: false, + } + } + } + } catch (e) { + console.error('Failed to restore upload task queue:', e) + } + } +} + +export default UploadTaskQueueManager diff --git a/src/renderer/components/ImageProcessSetting.vue b/src/renderer/components/ImageProcessSetting.vue index 6db6ae58..700d91ad 100644 --- a/src/renderer/components/ImageProcessSetting.vue +++ b/src/renderer/components/ImageProcessSetting.vue @@ -1185,11 +1185,11 @@ const waterMarkPositionMap = new Map([ const imageExtList = ['jpg', 'jpeg', 'png', 'webp', 'bmp', 'tiff', 'tif', 'svg', 'ico', 'avif', 'heif', 'heic'] const availableFormat = [ - 'webp', + 'dz', 'png', 'avif', 'jpg', - 'dz', + 'webp', 'fits', 'gif', 'heif', diff --git a/src/renderer/components/css/ImageProcessSetting.css b/src/renderer/components/css/ImageProcessSetting.css index 972d22bb..114d82e9 100644 --- a/src/renderer/components/css/ImageProcessSetting.css +++ b/src/renderer/components/css/ImageProcessSetting.css @@ -1,60 +1,11 @@ /* ==================== Base & Layout ==================== */ .image-process-settings { overflow-y: auto; - padding: 2rem; + padding: 1rem; + height: 100%; min-height: 100vh; color: var(--color-text-primary); - background: linear-gradient(180deg, var(--color-surface) 0%, var(--color-background-secondary) 100%); -} - -/* ==================== Header ==================== */ -.settings-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 2rem; - border: 1px solid var(--color-border); - border-radius: 16px; - padding: 1.75rem 2rem; - background: linear-gradient(135deg, var(--color-surface) 0%, var(--color-background-primary) 100%); - box-shadow: - 0 4px 20px rgb(0 0 0 / 8%), - 0 1px 3px rgb(0 0 0 / 5%); -} - -.header-content { - display: flex; - align-items: center; - gap: 1.25rem; -} - -.header-icon-wrapper { - display: flex; - justify-content: center; - align-items: center; - border-radius: 14px; - width: 56px; - height: 56px; - background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%); - box-shadow: 0 4px 12px rgb(64 158 255 / 30%); -} - -.header-icon { - color: white; -} - -.header-text h1 { - margin: 0 0 0.25rem; - font-size: 1.625rem; - font-weight: 700; - letter-spacing: -0.02em; - color: var(--color-text-primary); -} - -.header-text p { - margin: 0; - font-size: 0.9rem; - color: var(--color-text-secondary); + background: var(--color-background-primary); } /* ==================== Tab Navigation ==================== */ diff --git a/src/renderer/i18n/locales/en.json b/src/renderer/i18n/locales/en.json index 6a7ab1fe..d1c40133 100644 --- a/src/renderer/i18n/locales/en.json +++ b/src/renderer/i18n/locales/en.json @@ -973,6 +973,57 @@ "multipleUrlsHint": "Each URL on a separate line for multiple uploads", "outputFormat": "Output Format", "quickUpload": "Quick Upload", + "taskQueue": { + "addFiles": "Add Files", + "allCancelled": "All tasks cancelled", + "autoStart": "Auto Start", + "cancelAll": "Cancel All", + "cancelTask": "Cancel this task", + "cleared": "Finished tasks cleared", + "clearFinished": "Clear", + "dialogSubtitle": "Add files to upload queue", + "dialogTitle": "Create Upload Task", + "empty": "No tasks in queue", + "emptyHint": "Click 'Add Files' to start uploading files with controlled intervals", + "filesAdded": "{count} files added to queue", + "filterAll": "All", + "filterCompleted": "Done", + "filterFailed": "Failed", + "filterPending": "Pending", + "interval": "Upload Interval", + "intervalHint": "Time between uploads (100-60000ms)", + "manage": "Manage", + "maxRetry": "Max Retry", + "moveDown": "Move down in queue", + "moveUp": "Move up in queue", + "noMatchingTasks": "No matching tasks found", + "overallProgress": "Overall Progress", + "pause": "Pause", + "paused": "Paused", + "pauseOnError": "Pause on Error", + "removeTask": "Remove this task", + "resume": "Resume", + "resumed": "Upload task resumed", + "retriedAllFailed": "{count} failed tasks queued for retry", + "retryAllFailed": "Retry All", + "retryCount": "Retry: {count}", + "retryTask": "Retry this task", + "running": "Running", + "searchPlaceholder": "Search tasks...", + "selectFiles": "Select Files to Upload", + "start": "Start", + "started": "Upload task started", + "stats": "{completed} / {total}", + "statusCancelled": "Cancelled", + "statusCompleted": "Completed", + "statusFailed": "Failed", + "statusPending": "Pending", + "statusUploading": "Uploading...", + "taskRetried": "Task queued for retry", + "title": "Upload Task Queue", + "togglePriority": "Toggle priority" + }, + "taskUpload": "Task Upload", "uploadFailed": "Upload Failed", "uploadHint": "All formats are supported, but images are recommended", "uploadingMultipleUrls": "Uploading {count} URLs...", diff --git a/src/renderer/i18n/locales/zh-CN.json b/src/renderer/i18n/locales/zh-CN.json index 69246f8e..ba08f7ed 100644 --- a/src/renderer/i18n/locales/zh-CN.json +++ b/src/renderer/i18n/locales/zh-CN.json @@ -968,6 +968,57 @@ "multipleUrlsHint": "多个URL请分行输入", "outputFormat": "输出格式", "quickUpload": "快捷上传", + "taskQueue": { + "addFiles": "添加文件", + "allCancelled": "所有任务已取消", + "autoStart": "自动开始", + "cancelAll": "取消全部", + "cancelTask": "取消此任务", + "cleared": "已完成的任务已清除", + "clearFinished": "清除", + "dialogSubtitle": "添加文件到上传队列", + "dialogTitle": "创建上传任务", + "empty": "队列中没有任务", + "emptyHint": "点击添加文件开始以可控间隔上传文件", + "filesAdded": "已添加 {count} 个文件到队列", + "filterAll": "全部", + "filterCompleted": "已完成", + "filterFailed": "失败", + "filterPending": "等待中", + "interval": "上传间隔", + "intervalHint": "两次上传间的等待时间 (100-60000毫秒)", + "manage": "管理", + "maxRetry": "最大重试", + "moveDown": "下移队列", + "moveUp": "上移队列", + "noMatchingTasks": "未找到匹配的任务", + "overallProgress": "总体进度", + "pause": "暂停", + "paused": "已暂停", + "pauseOnError": "出错时暂停", + "removeTask": "移除此任务", + "resume": "继续", + "resumed": "上传任务已继续", + "retriedAllFailed": "{count} 个失败任务已加入重试队列", + "retryAllFailed": "全部重试", + "retryCount": "重试: {count}", + "retryTask": "重试此任务", + "running": "运行中", + "searchPlaceholder": "搜索任务...", + "selectFiles": "选择要上传的文件", + "start": "开始", + "started": "上传任务已开始", + "stats": "{completed} / {total}", + "statusCancelled": "已取消", + "statusCompleted": "已完成", + "statusFailed": "失败", + "statusPending": "等待中", + "statusUploading": "上传中...", + "taskRetried": "任务已加入重试队列", + "title": "上传任务队列", + "togglePriority": "切换优先级" + }, + "taskUpload": "任务上传", "uploadFailed": "上传失败", "uploadHint": "支持所有文件类型,但推荐仅上传图片", "uploadingMultipleUrls": "正在上传 {count} 个URL...", diff --git a/src/renderer/i18n/locales/zh-TW.json b/src/renderer/i18n/locales/zh-TW.json index 280eaf01..7e0f8ea5 100644 --- a/src/renderer/i18n/locales/zh-TW.json +++ b/src/renderer/i18n/locales/zh-TW.json @@ -968,6 +968,57 @@ "multipleUrlsHint": "多個URL請分行輸入", "outputFormat": "輸出格式", "quickUpload": "快捷上傳", + "taskQueue": { + "addFiles": "添加文件", + "allCancelled": "所有任務已取消", + "autoStart": "自動開始", + "cancelAll": "取消全部", + "cancelTask": "取消此任務", + "cleared": "已完成的任務已清除", + "clearFinished": "清除", + "dialogSubtitle": "添加文件到上傳佇列", + "dialogTitle": "建立上傳任務", + "empty": "佇列中沒有任務", + "emptyHint": "點擊「添加文件」開始以可控間隔上傳文件", + "filesAdded": "已添加 {count} 個文件到佇列", + "filterAll": "全部", + "filterCompleted": "已完成", + "filterFailed": "失敗", + "filterPending": "等待中", + "interval": "上傳間隔", + "intervalHint": "兩次上傳間的等待時間 (100-60000毫秒)", + "manage": "管理", + "maxRetry": "最大重試", + "moveDown": "下移佇列", + "moveUp": "上移佇列", + "noMatchingTasks": "未找到匹配的任務", + "overallProgress": "總體進度", + "pause": "暫停", + "paused": "已暫停", + "pauseOnError": "出錯時暫停", + "removeTask": "移除此任務", + "resume": "繼續", + "resumed": "上傳任務已繼續", + "retriedAllFailed": "{count} 個失敗任務已加入重試佇列", + "retryAllFailed": "全部重試", + "retryCount": "重試: {count}", + "retryTask": "重試此任務", + "running": "運行中", + "searchPlaceholder": "搜尋任務...", + "selectFiles": "選擇要上傳的文件", + "start": "開始", + "started": "上傳任務已開始", + "stats": "{completed} / {total}", + "statusCancelled": "已取消", + "statusCompleted": "已完成", + "statusFailed": "失敗", + "statusPending": "等待中", + "statusUploading": "上傳中...", + "taskRetried": "任務已加入重試佇列", + "title": "上傳任務佇列", + "togglePriority": "切換優先級" + }, + "taskUpload": "任務上傳", "uploadFailed": "上傳失敗", "uploadHint": "支持所有文件類型,但推薦僅上傳圖片", "uploadingMultipleUrls": "正在上傳 {count} 個URL...", diff --git a/src/renderer/pages/Upload.vue b/src/renderer/pages/Upload.vue index 76187447..132e763e 100644 --- a/src/renderer/pages/Upload.vue +++ b/src/renderer/pages/Upload.vue @@ -27,7 +27,6 @@ {{ t('pages.upload.imageProcessNameSingle') }} @@ -98,6 +97,17 @@ {{ t('pages.upload.urlUpload') }} + @@ -164,20 +174,379 @@ + + + + diff --git a/src/renderer/pages/css/UploadPage.css b/src/renderer/pages/css/UploadPage.css index 362696df..f125aae8 100644 --- a/src/renderer/pages/css/UploadPage.css +++ b/src/renderer/pages/css/UploadPage.css @@ -326,6 +326,7 @@ html, body { } .quick-action-button { + position: relative; display: flex; align-items: center; border: 1px solid var(--color-border); @@ -353,6 +354,40 @@ html, body { color: var(--color-text-primary); } +.quick-action-button.has-badge { + padding-right: 3rem; +} + +.task-count-badge { + position: absolute; + top: 50%; + right: 0.75rem; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + min-width: 1.5rem; + height: 1.5rem; + padding: 0 0.375rem; + color: white; + font-size: 0.6875rem; + font-weight: 700; + border-radius: 100px; + animation: badge-pulse 2s ease-in-out infinite; +} + +@keyframes badge-pulse { + 0%, 100% { + transform: translateY(-50%) scale(1); + box-shadow: 0 2px 6px rgb(0 122 255 / 40%); + } + + 50% { + transform: translateY(-50%) scale(1.1); + box-shadow: 0 2px 8px rgb(0 122 255 / 60%); + } +} + /* Settings Card */ .settings-content { display: flex; @@ -474,7 +509,7 @@ html, body { border-bottom-right-radius: calc(var(--radius-md) - 1px); } -/* Modal */ +/* Modal - Base Styles (Used by ImageProcess Dialog) */ .modal-overlay { position: fixed; z-index: 1000; @@ -485,6 +520,17 @@ html, body { padding: 2rem; background: rgb(0 0 0 / 50%); inset: 0; + animation: fade-in 0.2s ease-out; +} + +@keyframes fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } } .modal-container { @@ -505,7 +551,8 @@ html, body { justify-content: space-between; align-items: center; border-bottom: 1px solid var(--color-border-secondary); - padding: 0.95rem 1rem; + padding: 1rem 1.25rem; + background: var(--color-surface-elevated); } .modal-title { @@ -516,10 +563,10 @@ html, body { } .modal-subtitle { - margin: 0; + margin: 0.25rem 0 0; font-size: 1.25rem; font-weight: 600; - color: var(--color-text-primary); + color: var(--color-text-secondary); } .modal-close { @@ -533,24 +580,38 @@ html, body { color: var(--color-text-secondary); background: var(--color-surface-elevated); cursor: pointer; + transition: all var(--transition-fast); } .modal-close:hover { border-color: var(--color-danger); - color: var(--color-danger); - background: var(--color-surface); + color: white; + background: var(--color-danger); + transform: scale(1.05); } .modal-content { overflow-y: auto; - padding: 0.2rem; - max-height: calc(90vh - 120px); + max-height: calc(90vh - 90px); scrollbar-width: none; -ms-overflow-style: none; } .modal-content::-webkit-scrollbar { - display: none; + width: 0.5rem; +} + +.modal-content::-webkit-scrollbar-track { + background: var(--color-surface); +} + +.modal-content::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: var(--radius-full); +} + +.modal-content::-webkit-scrollbar-thumb:hover { + background: var(--color-text-tertiary); } /* Transitions */ @@ -710,3 +771,1414 @@ html, body { outline: 2px solid var(--color-accent); outline-offset: 4px; } + +/* ======================================== + Task Queue Components - Consolidated & Optimized + ======================================== */ + +.task-main-info { + display: flex; + align-items: center; + gap: 0.625rem; + flex: 1; + min-width: 0; +} + + + + +.priority-badge.high { + background: linear-gradient(135deg, var(--color-warning), #f39c12); + color: white; + box-shadow: 0 2px 6px rgb(243 156 18 / 35%); +} + +.task-meta { + display: flex; + align-items: center; + gap: 0.625rem; + flex-wrap: wrap; +} + +.task-status { + font-size: 0.75rem; + font-weight: 500; + color: var(--color-text-secondary); + padding: 0.25rem 0.625rem; + background: var(--color-surface); + border-radius: var(--radius-md); +} + +.task-size { + font-size: 0.7rem; + font-weight: 500; + color: var(--color-text-tertiary); + padding: 0.25rem 0.5rem; + background: var(--color-surface); + border-radius: var(--radius-sm); + border: 1px solid var(--color-border-secondary); +} + +.retry-count { + font-size: 0.7rem; + font-weight: 500; + color: var(--color-warning); + padding: 0.25rem 0.5rem; + background: rgb(243 156 18 / 12%); + border-radius: var(--radius-sm); +} + +.task-error { + font-size: 0.7rem; + font-weight: 500; + color: var(--color-danger); + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0.25rem 0.5rem; + background: rgb(231 76 60 / 10%); + border-radius: var(--radius-sm); +} + +.task-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + border-radius: var(--radius-md); + background: var(--color-surface); + color: var(--color-text-secondary); + cursor: pointer; + transition: all 0.2s ease; +} + +.task-btn:hover { + background: var(--color-surface-elevated); + transform: scale(1.1); +} + +.task-btn.cancel:hover { + color: white; + background: var(--color-danger); + box-shadow: 0 2px 8px rgb(231 76 60 / 35%); +} + +.task-btn.remove:hover { + color: white; + background: var(--color-warning); + box-shadow: 0 2px 8px rgb(243 156 18 / 35%); +} + +.task-btn.retry:hover { + color: white; + background: var(--color-accent); + box-shadow: 0 2px 8px rgb(0 122 255 / 35%); +} + +.task-btn.move:hover { + color: white; + background: var(--color-accent); + box-shadow: 0 2px 8px rgb(0 122 255 / 25%); +} + +.task-btn.priority { + color: var(--color-text-tertiary); +} + +.task-btn.priority:hover { + color: white; + background: var(--color-warning); + box-shadow: 0 2px 8px rgb(243 156 18 / 35%); +} + +.task-btn.priority.is-high { + color: white; + background: linear-gradient(135deg, var(--color-warning), #f39c12); + box-shadow: 0 2px 8px rgb(243 156 18 / 35%); +} + +.status-icon { + flex-shrink: 0; + margin-left: 0.375rem; +} + +.status-icon.success { + color: var(--color-success); + filter: drop-shadow(0 2px 4px rgb(46 204 113 / 35%)); +} + +.status-icon.error { + color: var(--color-danger); + filter: drop-shadow(0 2px 4px rgb(231 76 60 / 35%)); +} + +.status-icon.loading { + color: var(--color-accent); + animation: spin 1s linear infinite; + filter: drop-shadow(0 2px 4px rgb(0 122 255 / 35%)); +} + +.status-icon.pending { + color: var(--color-text-tertiary); +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +/* ======================================== + Task List Animations - Enhanced + ======================================== */ +.task-list-enter-active, +.task-list-leave-active { + transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); +} + +.task-list-enter-from { + opacity: 0; + transform: translateX(-30px) scale(0.95); +} + +.task-list-leave-to { + opacity: 0; + transform: translateX(30px) scale(0.95); +} + +.task-list-move { + transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Task item status text colors */ +.task-item.status-completed .task-filename { + color: var(--color-success); +} + +.task-item.status-completed .task-status { + color: var(--color-success); + background: rgb(46 204 113 / 12%); +} + +.task-item.status-failed .task-filename { + color: var(--color-danger); +} + +.task-item.status-failed .task-status { + color: var(--color-danger); + background: rgb(231 76 60 / 12%); +} + +.task-item.status-uploading .task-status { + color: var(--color-accent); + background: rgb(0 122 255 / 12%); +} + +.task-item.status-cancelled .task-filename { + color: var(--color-text-tertiary); + text-decoration: line-through; +} + +.task-item.status-cancelled .task-status { + color: var(--color-text-tertiary); + background: var(--color-surface-elevated); +} + +/* ======================================== + Empty Task List - Modern Design + ======================================== */ +.empty-task-list { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem 2rem; + color: var(--color-text-tertiary); + gap: 1rem; +} + +.empty-task-list svg { + color: var(--color-border); + opacity: 0.6; +} + +.empty-task-list p { + margin: 0; + font-size: 0.9rem; + font-weight: 500; + color: var(--color-text-secondary); +} + +/* ======================================== + Task Modal - Modern UI Design + ======================================== */ +.task-modal { + max-width: 520px; + width: 100%; + height: auto; + max-height: 80vh; + border-radius: var(--radius-2xl); + background: linear-gradient( + 145deg, + var(--color-surface) 0%, + var(--color-background-secondary) 100% + ); + box-shadow: + 0 25px 50px -12px rgb(0 0 0 / 25%), + 0 0 0 1px rgb(255 255 255 / 5%); + backdrop-filter: blur(20px); + overflow: hidden; +} + +.task-modal .modal-header { + padding: 1.5rem 1.75rem; + background: linear-gradient( + 180deg, + rgb(0 122 255 / 8%) 0%, + transparent 100% + ); + border-bottom: 1px solid rgb(0 122 255 / 15%); + position: relative; + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.task-modal .modal-header::before { + content: ''; + position: absolute; + top: 0; + left: 1.75rem; + right: 1.75rem; + height: 3px; + background: linear-gradient(90deg, var(--color-accent), var(--color-success)); + border-radius: 0 0 var(--radius-full) var(--radius-full); +} + +.task-modal .modal-header-text { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.task-modal .modal-title { + display: flex; + align-items: center; + gap: 0.625rem; + font-size: 1.35rem; + font-weight: 700; + background: linear-gradient(135deg, var(--color-text-primary) 0%, var(--color-accent) 100%); + background-clip: text; + -webkit-text-fill-color: transparent; + letter-spacing: -0.025em; +} + +.task-modal .modal-title svg { + color: var(--color-accent); + -webkit-text-fill-color: initial; +} + +.task-modal .modal-subtitle { + display: block; + margin-top: 0.5rem; + font-size: 0.875rem; + font-weight: 400; + color: var(--color-text-secondary); + -webkit-text-fill-color: var(--color-text-secondary); + background: none; +} + +.task-modal .modal-close { + width: 36px; + height: 36px; + border: none; + background: var(--color-surface-elevated); + transition: all 0.2s ease; +} + +.task-modal .modal-close:hover { + background: var(--color-danger); + color: white; + transform: rotate(90deg); +} + +.task-modal .modal-content { + padding: 1.75rem; + max-height: calc(80vh - 100px); + overflow: visible; +} + +.task-dialog-content { + display: flex; + flex-direction: column; + gap: 1.75rem; +} + +.task-dialog-settings { + padding: 1.5rem; + background: var(--color-surface-elevated); + border-radius: var(--radius-xl); + border: 1px solid var(--color-border-secondary); + position: relative; + overflow: hidden; +} + +.task-dialog-settings::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + background: linear-gradient( + 135deg, + rgb(0 122 255 / 3%) 0%, + transparent 50% + ); + pointer-events: none; +} + +.task-dialog-settings .setting-group { + position: relative; + z-index: 1; +} + +.task-dialog-settings .setting-label { + font-size: 0.875rem; + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: 0.75rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.task-dialog-settings .setting-label svg { + color: var(--color-accent); +} + +.task-dialog-settings .setting-label::before { + display: none; +} + +.task-dialog-settings .interval-input-group { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.875rem 1rem; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + transition: all 0.2s ease; +} + +.task-dialog-settings .interval-input-group:focus-within { + border-color: var(--color-accent); + box-shadow: 0 0 0 3px rgb(0 122 255 / 15%); +} + +.task-dialog-settings .interval-input { + width: 100px; + padding: 0; + border: none; + background: transparent; + font-size: 1.25rem; + font-weight: 600; + font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; + color: var(--color-accent); + text-align: left; +} + +.task-dialog-settings .interval-input:focus { + outline: none; +} + +.task-dialog-settings .interval-unit { + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text-tertiary); + padding: 0.25rem 0.625rem; + background: var(--color-surface-elevated); + border-radius: var(--radius-md); +} + +.task-dialog-settings .interval-hint { + display: block; + margin-top: 0.875rem; + padding: 0.75rem 1rem; + font-size: 0.8rem; + color: var(--color-text-secondary); + background: var(--color-surface); + border-radius: var(--radius-md); + border-left: 3px solid var(--color-accent); +} + +.task-dialog-actions { + display: flex; + justify-content: center; + padding-top: 0.5rem; +} + +.task-dialog-actions .action-button { + padding: 1rem 2rem; + font-size: 0.95rem; + font-weight: 600; + border-radius: var(--radius-xl); + background: linear-gradient(135deg, var(--color-accent) 0%, rgb(0 100 220) 100%); + box-shadow: + 0 4px 15px rgb(0 122 255 / 35%), + inset 0 1px 0 rgb(255 255 255 / 15%); + transition: all 0.25s ease; +} + +.task-dialog-actions .action-button:hover { + transform: translateY(-2px); + box-shadow: + 0 8px 25px rgb(0 122 255 / 45%), + inset 0 1px 0 rgb(255 255 255 / 20%); +} + +.task-dialog-actions .action-button:active { + transform: translateY(0); + box-shadow: + 0 2px 10px rgb(0 122 255 / 30%), + inset 0 1px 0 rgb(255 255 255 / 10%); +} + +/* ======================================== + Scrollbar Styling - Modern Design + ======================================== */ +.task-list::-webkit-scrollbar { + width: 8px; +} + +.task-list::-webkit-scrollbar-track { + background: var(--color-surface); + border-radius: var(--radius-full); + margin: 4px 0; +} + +.task-list::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, var(--color-border), var(--color-border-secondary)); + border-radius: var(--radius-full); + border: 2px solid var(--color-surface); +} + +.task-list::-webkit-scrollbar-thumb:hover { + background: linear-gradient(180deg, var(--color-accent), rgb(0 100 220)); +} + +/* ======================================== + Responsive Adjustments - Task Card + ======================================== */ +@media (width <= 640px) { + .task-card::before { + height: 3px; + } + + .task-header { + flex-direction: column; + align-items: flex-start; + padding: 1rem 1.25rem; + } + + .task-header-actions { + width: 100%; + justify-content: flex-start; + } + + .task-action-btn { + padding: 0.5rem 0.75rem; + font-size: 0.75rem; + } + + .task-settings { + flex-direction: column; + align-items: flex-start; + padding: 1rem 1.25rem; + } + + .interval-setting { + width: 100%; + } + + .add-files-btn { + width: 100%; + justify-content: center; + } + + .task-filter-bar { + padding: 0.75rem 1rem; + } + + .search-input-wrapper { + max-width: none; + } + + .filter-buttons { + width: 100%; + justify-content: center; + } + + .task-list { + padding: 0.5rem; + max-height: 350px; + } + + .task-item { + padding: 0.875rem 1rem; + border-radius: var(--radius-lg); + } + + .overall-progress { + padding: 1rem 1.25rem; + } + + .progress-details { + flex-wrap: wrap; + gap: 0.75rem; + } + + .task-settings-panel { + padding: 1rem 1.25rem; + } + + .settings-grid { + gap: 1rem; + } + + .task-modal { + max-width: 100%; + margin: 0.5rem; + max-height: 90vh; + } + + .task-modal .modal-header { + padding: 1.25rem; + } + + .task-modal .modal-content { + padding: 1.25rem; + } + + .task-dialog-settings { + padding: 1rem; + } + + .task-dialog-settings .interval-input { + font-size: 1.125rem; + } +} + +@media (width <= 480px) { + .task-header-left { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .task-action-btn span { + display: none; + } + + .task-action-btn { + padding: 0.5rem; + } + + .task-filename { + font-size: 0.8rem; + } + + .task-meta { + gap: 0.375rem; + } + + .task-status, + .task-size { + font-size: 0.65rem; + padding: 0.125rem 0.375rem; + } + + .progress-bar-container { + height: 10px; + } + + .filter-btn { + padding: 0.375rem 0.625rem; + font-size: 0.7rem; + } +} + +/* ======================================== + Task Queue Modal - Enhanced Design + ======================================== */ +.task-queue-modal { + max-width: 960px; + width: 92vw; + max-height: 88vh; + display: flex; + flex-direction: column; + background: var(--color-surface); + box-shadow: + 0 25px 50px -12px rgb(0 0 0 / 25%), + 0 0 0 1px rgb(0 0 0 / 5%); +} + +.task-queue-content { + display: flex; + flex-direction: column; + gap: 0; + overflow-y: auto; + flex: 1; + min-height: 0; +} + +/* Action Bar - Enhanced */ +.task-action-bar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + padding: 1rem 1.25rem; + background: linear-gradient( + 180deg, + var(--color-surface-elevated) 0%, + var(--color-surface) 100% + ); + border-bottom: 1px solid var(--color-border); + backdrop-filter: blur(10px); +} + +.action-bar-left, +.action-bar-right { + display: flex; + align-items: center; + gap: 0.625rem; + flex-wrap: wrap; +} + +.action-btn { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; + font-weight: 500; + padding: 0.625rem 1rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: white; + color: var(--color-text-primary); + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + white-space: nowrap; + box-shadow: 0 1px 2px rgb(0 0 0 / 5%); +} + +.action-btn:hover { + border-color: var(--color-accent); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgb(0 0 0 / 10%); + background: var(--color-surface-elevated); +} + +.action-btn.primary { + background: var(--color-accent); + border-color: transparent; + color: white; + box-shadow: 0 2px 8px rgb(0 122 255 / 25%); +} + +.action-btn.primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgb(0 122 255 / 40%); +} + +.action-btn.success { + background: var(--color-success); + border-color: transparent; + color: white; + box-shadow: 0 2px 8px rgb(52 199 89 / 25%); +} + +.action-btn.success:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgb(52 199 89 / 40%); +} + +.action-btn.warning { + background: var(--color-danger); + border-color: transparent; + color: white; + box-shadow: 0 2px 8px rgb(243 156 18 / 25%); +} + +.action-btn.warning:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgb(243 156 18 / 40%); +} + +.action-btn.danger { + background: var(--color-danger); + border-color: transparent; + color: white; + box-shadow: 0 2px 8px rgb(255 59 48 / 25%); +} + +.action-btn.danger:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgb(255 59 48 / 40%); +} + +.action-btn.active { + background: var(--color-accent); + border-color: var(--color-accent); + color: white; + box-shadow: 0 2px 8px rgb(0 122 255 / 30%); +} + +/* Overall Progress - Enhanced */ +.overall-progress { + padding: 1.25rem 1.25rem 1rem; + background: var(--color-surface); + border-bottom: 1px solid var(--color-background-primary); +} + +.progress-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.875rem; +} + +.progress-label { + font-size: 0.875rem; + font-weight: 600; + color: var(--color-text-primary); +} + +.progress-percentage { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-accent); + line-height: 1; +} + +.progress-bar-container { + height: 6px; + background: var(--color-surface-elevated); + border-radius: var(--radius-full); + overflow: hidden; +} + +.progress-bar-fill { + height: 100%; + background: linear-gradient(90deg, var(--color-accent), var(--color-success)); + border-radius: var(--radius-full); + transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 0 8px rgb(0 122 255 / 25%); +} + +.progress-details { + display: flex; + justify-content: space-between; + margin-top: 1rem; + gap: 1rem; + flex-wrap: wrap; +} + +.progress-detail-item { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; + color: var(--color-text-secondary); + padding: 0.375rem 0; +} + +.progress-detail-item svg { + color: var(--color-accent); +} + +/* Settings Panel - Enhanced */ +.settings-panel { + padding: 1rem 1.25rem 1.25rem; + background: var(--color-surface); + border-bottom: 1px solid var(--color-background-primary); +} + +.settings-grid { + display: flex; + align-items: center; + gap: 1.25rem; + flex-wrap: nowrap; + overflow-x: auto; +} + +.setting-item { + display: flex; + flex-direction: column; + gap: 0.5rem; + flex: 0 0 auto; + min-width: 220px; +} + +.setting-item.toggle-item { + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0.75rem; + background: transparent; + border-radius: var(--radius-md); + transition: all var(--transition-fast); +} + +.setting-item.toggle-item:hover { + background: var(--color-surface-elevated); +} + +.setting-checkbox { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: var(--color-accent); +} + +.setting-input { + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--color-surface-elevated); + color: var(--color-text-primary); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.setting-input:hover { + border-color: var(--color-accent); + background: var(--color-surface); +} + +.setting-input:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 3px rgb(0 122 255 / 10%); + background: white; +} + +.setting-input:disabled { + opacity: 0.6; + cursor: not-allowed; + background: var(--color-surface); +} + +.input-with-unit { + display: flex; + align-items: center; + gap: 0.625rem; +} + +.input-with-unit .setting-input { + flex: 1; +} + +.input-unit { + font-size: 0.8125rem; + color: var(--color-text-secondary); + font-weight: 500; + padding: 0.5rem 0.625rem; + background: transparent; +} + +.toggle-switch { + position: relative; + width: 44px; + height: 24px; + border: none; + border-radius: var(--radius-full); + background: var(--color-surface); + cursor: pointer; + transition: background var(--transition-fast); +} + +.toggle-switch.active { + background: var(--color-accent); +} + +.toggle-slider { + position: absolute; + top: 2px; + left: 2px; + width: 20px; + height: 20px; + border-radius: 50%; + background: white; + transition: transform var(--transition-fast); + box-shadow: var(--shadow-sm); +} + +.toggle-switch.active .toggle-slider { + transform: translateX(20px); +} + +/* Filter & Search Bar - Enhanced */ +.filter-search-bar { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1.25rem; + background: var(--color-surface-elevated); + border-bottom: 1px solid var(--color-border); +} + +.search-box { + display: flex; + align-items: center; + gap: 0.625rem; + padding: 0.625rem 1rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + background: white; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 1px 2px rgb(0 0 0 / 5%); +} + +.search-box:focus-within { + border-color: var(--color-accent); + box-shadow: 0 0 0 3px rgb(0 122 255 / 10%), 0 1px 2px rgb(0 0 0 / 5%); + background: white; +} + +.search-box svg { + color: var(--color-accent); + flex-shrink: 0; +} + +.search-input { + flex: 1; + border: none; + background: transparent; + font-size: 0.875rem; + color: var(--color-text-primary); + outline: none; +} + +.search-input::placeholder { + color: var(--color-text-tertiary); +} + +.filter-tabs { + display: flex; + gap: 0.625rem; + flex-wrap: wrap; +} + +.filter-tab { + padding: 0.5rem 1rem; + font-size: 0.8125rem; + font-weight: 500; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: white; + color: var(--color-text-secondary); + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + white-space: nowrap; + box-shadow: 0 1px 2px rgb(0 0 0 / 5%); +} + +.filter-tab:hover { + border-color: var(--color-accent); + color: var(--color-text-primary); + transform: translateY(-1px); + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); +} + +.filter-tab.active { + background: linear-gradient(135deg, var(--color-accent) 0%, rgb(0 100 220) 100%); + border-color: transparent; + color: white; + box-shadow: 0 2px 8px rgb(0 122 255 / 30%); +} + +/* Task List Container - Enhanced */ +.task-list-container { + flex: 1; + overflow-y: auto; + background: var(--color-surface); + min-height: 300px; +} + +.task-list { + display: flex; + flex-direction: column; +} + +.task-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--color-border); + background: var(--color-surface); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; +} + +.task-item::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: transparent; + transition: background 0.2s ease; +} + +.task-item:last-child { + border-bottom: none; +} + +.task-item:hover { + background: var(--color-surface-elevated); + box-shadow: 0 2px 8px rgb(0 0 0 / 4%); +} + +.task-item.status-completed { + background: rgb(52 199 89 / 3%); + opacity: 0.85; +} + +.task-item.status-completed::before { + background: var(--color-success); +} + +.task-item.status-failed { + background: rgb(255 59 48 / 3%); +} + +.task-item.status-failed::before { + background: var(--color-error); +} + +.task-item.status-uploading { + background: rgb(0 122 255 / 3%); +} + +.task-item.status-uploading::before { + background: var(--color-accent); + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + + 50% { + opacity: 0.5; + } +} + +.task-item.status-cancelled { + background: rgb(142 142 147 / 3%); + opacity: 0.6; +} + +.task-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.625rem; +} + +.task-header-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.875rem; +} + +.task-name { + display: flex; + align-items: center; + gap: 0.625rem; + min-width: 0; + flex: 1; +} + +.task-filename { + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.priority-badge { + display: flex; + align-items: center; + justify-content: center; + padding: 0.25rem; + background: var(--color-warning); + border-radius: var(--radius-md); + color: white; + flex-shrink: 0; +} + +.task-status-badge { + font-size: 0.6875rem; + font-weight: 600; + padding: 0.25rem 0.625rem; + border-radius: var(--radius-full); + text-transform: uppercase; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.task-status-badge.status-pending { + background: rgb(142 142 147 / 15%); + color: var(--color-text-secondary); +} + +.task-status-badge.status-uploading { + background: rgb(0 122 255 / 15%); + color: var(--color-accent); +} + +.task-status-badge.status-completed { + background: rgb(52 199 89 / 15%); + color: var(--color-success); +} + +.task-status-badge.status-failed { + background: rgb(255 59 48 / 15%); + color: var(--color-error); +} + +.task-status-badge.status-cancelled { + background: rgb(142 142 147 / 15%); + color: var(--color-text-tertiary); +} + +.task-meta-row { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.task-meta-item { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.75rem; + color: var(--color-text-tertiary); +} + +.task-meta-item svg { + color: var(--color-text-secondary); +} + +.task-meta-item.retry { + color: var(--color-warning); +} + +.task-meta-item.error { + color: var(--color-error); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 200px; +} + +.task-actions { + display: flex; + align-items: center; + gap: 0.375rem; +} + +.task-icon-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--color-surface-elevated); + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); +} + +.task-icon-btn:hover { + border-color: var(--color-accent); + background: var(--color-accent); + color: white; + transform: translateY(-1px); +} + +.task-icon-btn.priority.is-high { + background: var(--color-warning); + border-color: var(--color-warning); + color: white; +} + +.task-icon-btn.danger:hover { + background: var(--color-error); + border-color: var(--color-error); + color: white; +} + +.task-status-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; +} + +.task-status-icon .icon-success { + color: var(--color-success); +} + +.task-status-icon .icon-error { + color: var(--color-error); +} + +.task-status-icon .icon-loading { + color: var(--color-accent); +} + +.task-status-icon .icon-pending { + color: var(--color-text-tertiary); +} + +/* Empty State */ +.empty-state { + display: flex; + background: var(--color-surface); + flex-direction: column; + height: 100%; + align-items: center; + justify-content: center; + padding: 3rem 2rem; + text-align: center; + gap: 1rem; +} + +.empty-state svg { + color: var(--color-text-tertiary); + opacity: 0.5; +} + +.empty-state h4 { + font-size: 1.125rem; + font-weight: 600; + color: var(--color-text-primary); + margin: 0; +} + +.empty-state p { + font-size: 0.875rem; + color: var(--color-text-secondary); + margin: 0; + max-width: 400px; +} + +/* Animations */ +.spinning { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +.settings-slide-enter-active, +.settings-slide-leave-active { + transition: all var(--transition-medium); +} + +.settings-slide-enter-from, +.settings-slide-leave-to { + opacity: 0; + transform: translateY(-10px); +} + +.task-enter-active, +.task-leave-active { + transition: all var(--transition-fast); +} + +.task-enter-from { + opacity: 0; + transform: translateX(-20px); +} + +.task-leave-to { + opacity: 0; + transform: translateX(20px); +} + +.task-move { + transition: transform var(--transition-medium); +} + +/* Responsive Design */ +@media (width <= 768px) { + .task-queue-modal { + width: 95vw; + max-height: 90vh; + } + + .task-action-bar { + flex-direction: column; + align-items: stretch; + } + + .action-bar-left, + .action-bar-right { + width: 100%; + justify-content: center; + } + + .filter-search-bar { + gap: 0.5rem; + } + + .task-item { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + } + + .task-actions { + width: 100%; + justify-content: flex-end; + } + + .task-meta-item.error { + max-width: 100%; + } +} diff --git a/src/renderer/utils/enum.ts b/src/renderer/utils/enum.ts index 886ffb11..7c5dbffe 100644 --- a/src/renderer/utils/enum.ts +++ b/src/renderer/utils/enum.ts @@ -85,6 +85,27 @@ export const IRPCActionType = { UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE: 'UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE', UPLOAD_CHOOSED_FILES: 'UPLOAD_CHOOSED_FILES', + // upload task queue rpc + UPLOAD_TASK_ADD: 'UPLOAD_TASK_ADD', + UPLOAD_TASK_START: 'UPLOAD_TASK_START', + UPLOAD_TASK_PAUSE: 'UPLOAD_TASK_PAUSE', + UPLOAD_TASK_RESUME: 'UPLOAD_TASK_RESUME', + UPLOAD_TASK_CANCEL_ALL: 'UPLOAD_TASK_CANCEL_ALL', + UPLOAD_TASK_CANCEL_ONE: 'UPLOAD_TASK_CANCEL_ONE', + UPLOAD_TASK_REMOVE_ONE: 'UPLOAD_TASK_REMOVE_ONE', + UPLOAD_TASK_CLEAR_FINISHED: 'UPLOAD_TASK_CLEAR_FINISHED', + UPLOAD_TASK_CLEAR_ALL: 'UPLOAD_TASK_CLEAR_ALL', + UPLOAD_TASK_GET_STATUS: 'UPLOAD_TASK_GET_STATUS', + UPLOAD_TASK_SET_INTERVAL: 'UPLOAD_TASK_SET_INTERVAL', + UPLOAD_TASK_GET_INTERVAL: 'UPLOAD_TASK_GET_INTERVAL', + UPLOAD_TASK_RETRY_ONE: 'UPLOAD_TASK_RETRY_ONE', + UPLOAD_TASK_RETRY_ALL_FAILED: 'UPLOAD_TASK_RETRY_ALL_FAILED', + UPLOAD_TASK_MOVE_UP: 'UPLOAD_TASK_MOVE_UP', + UPLOAD_TASK_MOVE_DOWN: 'UPLOAD_TASK_MOVE_DOWN', + UPLOAD_TASK_SET_PRIORITY: 'UPLOAD_TASK_SET_PRIORITY', + UPLOAD_TASK_UPDATE_SETTINGS: 'UPLOAD_TASK_UPDATE_SETTINGS', + UPLOAD_TASK_GET_SETTINGS: 'UPLOAD_TASK_GET_SETTINGS', + // gallery rpc GALLERY_PASTE_TEXT: 'GALLERY_PASTE_TEXT', GALLERY_REMOVE_FILES: 'GALLERY_REMOVE_FILES', diff --git a/src/universal/types/i18nt.d.ts b/src/universal/types/i18nt.d.ts index ecb850ad..9509df8f 100644 --- a/src/universal/types/i18nt.d.ts +++ b/src/universal/types/i18nt.d.ts @@ -69,5 +69,7 @@ interface ILocales { QUIT: string OPERATION_SUCCEED: string OPERATION_FAILED: string + UPLOAD_TASK_COMPLETED: string + UPLOAD_TASK_COMPLETED_BODY: string } type ILocalesKey = keyof ILocales