mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-07-13 00:21:25 +08:00
✨ Feature(custom): add upload task system
This commit is contained in:
@@ -69,3 +69,5 @@ TIPS_UPDATE_DOWNLOADED: The update has been downloaded and will be installed on
|
|||||||
QUIT: Quit
|
QUIT: Quit
|
||||||
OPERATION_SUCCEED: "Operation Succeed"
|
OPERATION_SUCCEED: "Operation Succeed"
|
||||||
OPERATION_FAILED: "Operation Failed"
|
OPERATION_FAILED: "Operation Failed"
|
||||||
|
UPLOAD_TASK_COMPLETED: "Upload Task Completed"
|
||||||
|
UPLOAD_TASK_COMPLETED_BODY: "${completed} succeeded, ${failed} failed"
|
||||||
|
|||||||
@@ -69,3 +69,5 @@ UPDATE: 更新
|
|||||||
QUIT: 退出
|
QUIT: 退出
|
||||||
OPERATION_SUCCEED: "操作成功"
|
OPERATION_SUCCEED: "操作成功"
|
||||||
OPERATION_FAILED: "操作失败"
|
OPERATION_FAILED: "操作失败"
|
||||||
|
UPLOAD_TASK_COMPLETED: "上传任务完成"
|
||||||
|
UPLOAD_TASK_COMPLETED_BODY: "${completed} 成功, ${failed} 失败"
|
||||||
|
|||||||
@@ -69,3 +69,5 @@ UPDATE: 更新
|
|||||||
QUIT: 退出
|
QUIT: 退出
|
||||||
OPERATION_SUCCEED: "操作成功"
|
OPERATION_SUCCEED: "操作成功"
|
||||||
OPERATION_FAILED: "操作失敗"
|
OPERATION_FAILED: "操作失敗"
|
||||||
|
UPLOAD_TASK_COMPLETED: "上傳任務完成"
|
||||||
|
UPLOAD_TASK_COMPLETED_BODY: "${completed} 成功, ${failed} 失敗"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { uploadChoosedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis
|
|||||||
import { RPCRouter } from '~/events/rpc/router'
|
import { RPCRouter } from '~/events/rpc/router'
|
||||||
import { IRPCActionType, IRPCType } from '~/utils/enum'
|
import { IRPCActionType, IRPCType } from '~/utils/enum'
|
||||||
import getPicBeds from '~/utils/getPicBeds'
|
import getPicBeds from '~/utils/getPicBeds'
|
||||||
|
import UploadTaskQueueManager from '~/utils/uploadTaskQueue'
|
||||||
|
|
||||||
const uploadRouter = new RPCRouter()
|
const uploadRouter = new RPCRouter()
|
||||||
|
|
||||||
@@ -26,6 +27,170 @@ const uploadRoutes = [
|
|||||||
return uploadChoosedFiles(evt.sender, args[0])
|
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)
|
uploadRouter.addBatch(uploadRoutes)
|
||||||
|
|||||||
@@ -139,6 +139,27 @@ export const IRPCActionType = {
|
|||||||
UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE: 'UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE',
|
UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE: 'UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE',
|
||||||
UPLOAD_CHOOSED_FILES: 'UPLOAD_CHOOSED_FILES',
|
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 rpc
|
||||||
GALLERY_PASTE_TEXT: 'GALLERY_PASTE_TEXT',
|
GALLERY_PASTE_TEXT: 'GALLERY_PASTE_TEXT',
|
||||||
GALLERY_REMOVE_FILES: 'GALLERY_REMOVE_FILES',
|
GALLERY_REMOVE_FILES: 'GALLERY_REMOVE_FILES',
|
||||||
|
|||||||
631
src/main/utils/uploadTaskQueue.ts
Normal file
631
src/main/utils/uploadTaskQueue.ts
Normal file
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<IStringKeyMap> {
|
||||||
|
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<void> {
|
||||||
|
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<IUploadTaskQueueConfig>): 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
|
||||||
@@ -1185,11 +1185,11 @@ const waterMarkPositionMap = new Map([
|
|||||||
|
|
||||||
const imageExtList = ['jpg', 'jpeg', 'png', 'webp', 'bmp', 'tiff', 'tif', 'svg', 'ico', 'avif', 'heif', 'heic']
|
const imageExtList = ['jpg', 'jpeg', 'png', 'webp', 'bmp', 'tiff', 'tif', 'svg', 'ico', 'avif', 'heif', 'heic']
|
||||||
const availableFormat = [
|
const availableFormat = [
|
||||||
'webp',
|
'dz',
|
||||||
'png',
|
'png',
|
||||||
'avif',
|
'avif',
|
||||||
'jpg',
|
'jpg',
|
||||||
'dz',
|
'webp',
|
||||||
'fits',
|
'fits',
|
||||||
'gif',
|
'gif',
|
||||||
'heif',
|
'heif',
|
||||||
|
|||||||
@@ -1,60 +1,11 @@
|
|||||||
/* ==================== Base & Layout ==================== */
|
/* ==================== Base & Layout ==================== */
|
||||||
.image-process-settings {
|
.image-process-settings {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 2rem;
|
padding: 1rem;
|
||||||
|
height: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
background: linear-gradient(180deg, var(--color-surface) 0%, var(--color-background-secondary) 100%);
|
background: var(--color-background-primary);
|
||||||
}
|
|
||||||
|
|
||||||
/* ==================== 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== Tab Navigation ==================== */
|
/* ==================== Tab Navigation ==================== */
|
||||||
|
|||||||
@@ -973,6 +973,57 @@
|
|||||||
"multipleUrlsHint": "Each URL on a separate line for multiple uploads",
|
"multipleUrlsHint": "Each URL on a separate line for multiple uploads",
|
||||||
"outputFormat": "Output Format",
|
"outputFormat": "Output Format",
|
||||||
"quickUpload": "Quick Upload",
|
"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",
|
"uploadFailed": "Upload Failed",
|
||||||
"uploadHint": "All formats are supported, but images are recommended",
|
"uploadHint": "All formats are supported, but images are recommended",
|
||||||
"uploadingMultipleUrls": "Uploading {count} URLs...",
|
"uploadingMultipleUrls": "Uploading {count} URLs...",
|
||||||
|
|||||||
@@ -968,6 +968,57 @@
|
|||||||
"multipleUrlsHint": "多个URL请分行输入",
|
"multipleUrlsHint": "多个URL请分行输入",
|
||||||
"outputFormat": "输出格式",
|
"outputFormat": "输出格式",
|
||||||
"quickUpload": "快捷上传",
|
"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": "上传失败",
|
"uploadFailed": "上传失败",
|
||||||
"uploadHint": "支持所有文件类型,但推荐仅上传图片",
|
"uploadHint": "支持所有文件类型,但推荐仅上传图片",
|
||||||
"uploadingMultipleUrls": "正在上传 {count} 个URL...",
|
"uploadingMultipleUrls": "正在上传 {count} 个URL...",
|
||||||
|
|||||||
@@ -968,6 +968,57 @@
|
|||||||
"multipleUrlsHint": "多個URL請分行輸入",
|
"multipleUrlsHint": "多個URL請分行輸入",
|
||||||
"outputFormat": "輸出格式",
|
"outputFormat": "輸出格式",
|
||||||
"quickUpload": "快捷上傳",
|
"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": "上傳失敗",
|
"uploadFailed": "上傳失敗",
|
||||||
"uploadHint": "支持所有文件類型,但推薦僅上傳圖片",
|
"uploadHint": "支持所有文件類型,但推薦僅上傳圖片",
|
||||||
"uploadingMultipleUrls": "正在上傳 {count} 個URL...",
|
"uploadingMultipleUrls": "正在上傳 {count} 個URL...",
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
<span>{{ t('pages.upload.imageProcessNameSingle') }}</span>
|
<span>{{ t('pages.upload.imageProcessNameSingle') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="segmented-button" :title="t('pages.upload.imageProcessName')" @click="handleImageProcess">
|
<button class="segmented-button" :title="t('pages.upload.imageProcessName')" @click="handleImageProcess">
|
||||||
<Settings :size="16" />
|
|
||||||
<span>{{ t('pages.upload.imageProcessName') }}</span>
|
<span>{{ t('pages.upload.imageProcessName') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,6 +97,17 @@
|
|||||||
<LinkIcon :size="20" />
|
<LinkIcon :size="20" />
|
||||||
<span>{{ t('pages.upload.urlUpload') }}</span>
|
<span>{{ t('pages.upload.urlUpload') }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="quick-action-button"
|
||||||
|
:class="{ 'has-badge': taskQueueStatus.tasks.length > 0 }"
|
||||||
|
@click="openTaskDialog"
|
||||||
|
>
|
||||||
|
<ListTodoIcon :size="20" />
|
||||||
|
<span>{{ t('pages.upload.taskUpload') }}</span>
|
||||||
|
<span v-if="taskQueueStatus.tasks.length > 0" class="task-count-badge">
|
||||||
|
{{ taskQueueStatus.tasks.length }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -164,20 +174,379 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
|
<!-- Task Queue Manager Modal -->
|
||||||
|
<transition name="modal">
|
||||||
|
<div v-if="taskDialogVisible" class="modal-overlay" @click="taskDialogVisible = false">
|
||||||
|
<div class="modal-container task-queue-modal" @click.stop>
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-header-text">
|
||||||
|
<h3 class="modal-title">
|
||||||
|
<ListTodoIcon :size="22" />
|
||||||
|
{{ t('pages.upload.taskQueue.title') }}
|
||||||
|
</h3>
|
||||||
|
<span class="modal-subtitle">
|
||||||
|
{{
|
||||||
|
t('pages.upload.taskQueue.stats', {
|
||||||
|
completed: taskQueueStatus.stats.completed,
|
||||||
|
total: taskQueueStatus.stats.total,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button class="modal-close" @click="taskDialogVisible = false">
|
||||||
|
<XIcon :size="20" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-content task-queue-content">
|
||||||
|
<!-- Action Bar -->
|
||||||
|
<div class="task-action-bar">
|
||||||
|
<div class="action-bar-left">
|
||||||
|
<button class="action-btn primary" @click="addFilesToTask">
|
||||||
|
<PlusIcon :size="16" />
|
||||||
|
{{ t('pages.upload.taskQueue.addFiles') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!taskQueueStatus.config.isRunning && taskQueueStatus.stats.pending > 0"
|
||||||
|
class="action-btn success"
|
||||||
|
@click="startTaskQueue"
|
||||||
|
>
|
||||||
|
<PlayIcon :size="16" />
|
||||||
|
{{ t('pages.upload.taskQueue.start') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="taskQueueStatus.config.isRunning && !taskQueueStatus.config.isPaused"
|
||||||
|
class="action-btn warning"
|
||||||
|
@click="pauseTaskQueue"
|
||||||
|
>
|
||||||
|
<PauseIcon :size="16" />
|
||||||
|
{{ t('pages.upload.taskQueue.pause') }}
|
||||||
|
</button>
|
||||||
|
<button v-if="taskQueueStatus.config.isPaused" class="action-btn success" @click="resumeTaskQueue">
|
||||||
|
<PlayIcon :size="16" />
|
||||||
|
{{ t('pages.upload.taskQueue.resume') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="action-bar-right">
|
||||||
|
<button v-if="taskQueueStatus.stats.failed > 0" class="action-btn" @click="retryAllFailedTasks">
|
||||||
|
<RefreshCwIcon :size="16" />
|
||||||
|
{{ t('pages.upload.taskQueue.retryAllFailed') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="taskQueueStatus.config.isRunning || taskQueueStatus.stats.pending > 0"
|
||||||
|
class="action-btn danger"
|
||||||
|
@click="cancelAllTasks"
|
||||||
|
>
|
||||||
|
<XIcon :size="16" />
|
||||||
|
{{ t('pages.upload.taskQueue.cancelAll') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="
|
||||||
|
taskQueueStatus.stats.completed > 0 ||
|
||||||
|
taskQueueStatus.stats.failed > 0 ||
|
||||||
|
taskQueueStatus.stats.cancelled > 0
|
||||||
|
"
|
||||||
|
class="action-btn"
|
||||||
|
@click="clearFinishedTasks"
|
||||||
|
>
|
||||||
|
<Trash2Icon :size="16" />
|
||||||
|
{{ t('pages.upload.taskQueue.clearFinished') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="action-btn"
|
||||||
|
:class="{ active: showTaskSettings }"
|
||||||
|
@click="showTaskSettings = !showTaskSettings"
|
||||||
|
>
|
||||||
|
<SettingsIcon :size="16" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Overall Progress -->
|
||||||
|
<div v-if="taskQueueStatus.stats.total > 0" class="overall-progress">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span class="progress-label">{{ t('pages.upload.taskQueue.overallProgress') }}</span>
|
||||||
|
<span class="progress-percentage">{{ overallProgressPercent }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar-fill" :style="{ width: `${overallProgressPercent}%` }" />
|
||||||
|
</div>
|
||||||
|
<div class="progress-details">
|
||||||
|
<span v-if="taskQueueStatus.stats.avgSpeed > 0" class="progress-detail-item">
|
||||||
|
<ZapIcon :size="14" />
|
||||||
|
{{ formatSpeed(taskQueueStatus.stats.avgSpeed) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="taskQueueStatus.stats.estimatedTimeMs > 0 && taskQueueStatus.config.isRunning"
|
||||||
|
class="progress-detail-item"
|
||||||
|
>
|
||||||
|
<ClockIcon :size="14" />
|
||||||
|
{{ formatTime(taskQueueStatus.stats.estimatedTimeMs) }}
|
||||||
|
</span>
|
||||||
|
<span class="progress-detail-item">
|
||||||
|
<HardDriveIcon :size="14" />
|
||||||
|
{{ formatSize(taskQueueStatus.stats.completedSize) }} /
|
||||||
|
{{ formatSize(taskQueueStatus.stats.totalSize) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Panel -->
|
||||||
|
<transition name="settings-slide">
|
||||||
|
<div v-if="showTaskSettings" class="settings-panel">
|
||||||
|
<div class="settings-grid">
|
||||||
|
<div class="setting-item">
|
||||||
|
<label class="setting-label">
|
||||||
|
<TimerIcon :size="14" />
|
||||||
|
{{ t('pages.upload.taskQueue.interval') }}
|
||||||
|
</label>
|
||||||
|
<div class="input-with-unit">
|
||||||
|
<input
|
||||||
|
v-model.number="uploadInterval"
|
||||||
|
type="number"
|
||||||
|
min="0.1"
|
||||||
|
max="99999"
|
||||||
|
step="0.1"
|
||||||
|
class="setting-input"
|
||||||
|
:disabled="taskQueueStatus.config.isRunning"
|
||||||
|
@change="updateInterval"
|
||||||
|
/>
|
||||||
|
<span class="input-unit">s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label class="setting-label">{{ t('pages.upload.taskQueue.maxRetry') }}</label>
|
||||||
|
<input
|
||||||
|
v-model.number="maxRetryCount"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="10"
|
||||||
|
step="1"
|
||||||
|
class="setting-input"
|
||||||
|
@change="updateSettings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item toggle-item">
|
||||||
|
<label class="setting-label" for="task-auto-start">
|
||||||
|
{{ t('pages.upload.taskQueue.autoStart') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="task-auto-start"
|
||||||
|
v-model="autoStart"
|
||||||
|
type="checkbox"
|
||||||
|
class="setting-checkbox"
|
||||||
|
@change="updateSettings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item toggle-item">
|
||||||
|
<label class="setting-label" for="task-pause-on-error">
|
||||||
|
{{ t('pages.upload.taskQueue.pauseOnError') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="task-pause-on-error"
|
||||||
|
v-model="pauseOnError"
|
||||||
|
type="checkbox"
|
||||||
|
class="setting-checkbox"
|
||||||
|
@change="updateSettings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- Filter & Search Bar -->
|
||||||
|
<div v-if="taskQueueStatus.tasks.length > 0" class="filter-search-bar">
|
||||||
|
<div class="search-box">
|
||||||
|
<SearchIcon :size="16" />
|
||||||
|
<input
|
||||||
|
v-model="taskSearchQuery"
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
:placeholder="t('pages.upload.taskQueue.searchPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="filter-tabs">
|
||||||
|
<button class="filter-tab" :class="{ active: taskFilter === 'all' }" @click="taskFilter = 'all'">
|
||||||
|
{{ t('pages.upload.taskQueue.filterAll') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="filter-tab"
|
||||||
|
:class="{ active: taskFilter === 'pending' }"
|
||||||
|
@click="taskFilter = 'pending'"
|
||||||
|
>
|
||||||
|
{{ t('pages.upload.taskQueue.filterPending') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="filter-tab"
|
||||||
|
:class="{ active: taskFilter === 'completed' }"
|
||||||
|
@click="taskFilter = 'completed'"
|
||||||
|
>
|
||||||
|
{{ t('pages.upload.taskQueue.filterCompleted') }}
|
||||||
|
</button>
|
||||||
|
<button class="filter-tab" :class="{ active: taskFilter === 'failed' }" @click="taskFilter = 'failed'">
|
||||||
|
{{ t('pages.upload.taskQueue.filterFailed') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Task List -->
|
||||||
|
<div v-if="taskQueueStatus.tasks.length > 0" class="task-list-container">
|
||||||
|
<TransitionGroup name="task" tag="div" class="task-list">
|
||||||
|
<div
|
||||||
|
v-for="task in filteredTasks"
|
||||||
|
:key="task.id"
|
||||||
|
class="task-item"
|
||||||
|
:class="getTaskStatusClass(task.status)"
|
||||||
|
>
|
||||||
|
<div class="task-content">
|
||||||
|
<div class="task-header-row">
|
||||||
|
<div class="task-name">
|
||||||
|
<span class="task-filename" :title="task.filePath">{{ task.fileName }}</span>
|
||||||
|
<span v-if="task.priority === 2" class="priority-badge">
|
||||||
|
<StarIcon :size="12" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="task-status-badge" :class="getTaskStatusClass(task.status)">
|
||||||
|
{{ getTaskStatusText(task.status) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="task-meta-row">
|
||||||
|
<span v-if="task.fileSize > 0" class="task-meta-item">
|
||||||
|
<HardDriveIcon :size="12" />
|
||||||
|
{{ formatSize(task.fileSize) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="task.uploadSpeed && task.status === 'uploading'" class="task-meta-item">
|
||||||
|
<ZapIcon :size="12" />
|
||||||
|
{{ formatSpeed(task.uploadSpeed) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="task.retryCount > 0" class="task-meta-item retry">
|
||||||
|
{{ t('pages.upload.taskQueue.retryCount', { count: task.retryCount }) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="task.error" class="task-meta-item error" :title="task.error">
|
||||||
|
{{ task.error }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="task-actions">
|
||||||
|
<!-- Pending task actions -->
|
||||||
|
<template v-if="task.status === 'pending'">
|
||||||
|
<button
|
||||||
|
class="task-icon-btn"
|
||||||
|
:title="t('pages.upload.taskQueue.moveUp')"
|
||||||
|
@click="moveTaskUp(task.id)"
|
||||||
|
>
|
||||||
|
<ChevronUpIcon :size="16" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="task-icon-btn"
|
||||||
|
:title="t('pages.upload.taskQueue.moveDown')"
|
||||||
|
@click="moveTaskDown(task.id)"
|
||||||
|
>
|
||||||
|
<ChevronDownIcon :size="16" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="task-icon-btn priority"
|
||||||
|
:class="{ 'is-high': task.priority === 2 }"
|
||||||
|
:title="t('pages.upload.taskQueue.togglePriority')"
|
||||||
|
@click="toggleTaskPriority(task.id, task.priority)"
|
||||||
|
>
|
||||||
|
<StarIcon :size="16" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="task-icon-btn danger"
|
||||||
|
:title="t('pages.upload.taskQueue.cancelTask')"
|
||||||
|
@click="cancelTask(task.id)"
|
||||||
|
>
|
||||||
|
<XIcon :size="16" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<!-- Failed task actions -->
|
||||||
|
<template v-if="task.status === 'failed'">
|
||||||
|
<button
|
||||||
|
class="task-icon-btn"
|
||||||
|
:title="t('pages.upload.taskQueue.retryTask')"
|
||||||
|
@click="retryTask(task.id)"
|
||||||
|
>
|
||||||
|
<RefreshCwIcon :size="16" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="task-icon-btn danger"
|
||||||
|
:title="t('pages.upload.taskQueue.removeTask')"
|
||||||
|
@click="removeTask(task.id)"
|
||||||
|
>
|
||||||
|
<Trash2Icon :size="16" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<!-- Completed/Cancelled task actions -->
|
||||||
|
<template v-if="task.status === 'completed' || task.status === 'cancelled'">
|
||||||
|
<button
|
||||||
|
class="task-icon-btn"
|
||||||
|
:title="t('pages.upload.taskQueue.removeTask')"
|
||||||
|
@click="removeTask(task.id)"
|
||||||
|
>
|
||||||
|
<Trash2Icon :size="16" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<!-- Status icon -->
|
||||||
|
<div class="task-status-icon">
|
||||||
|
<CheckCircleIcon v-if="task.status === 'completed'" :size="18" class="icon-success" />
|
||||||
|
<XCircleIcon v-if="task.status === 'failed'" :size="18" class="icon-error" />
|
||||||
|
<LoaderIcon v-if="task.status === 'uploading'" :size="18" class="icon-loading spinning" />
|
||||||
|
<ClockIcon v-if="task.status === 'pending'" :size="18" class="icon-pending" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
<ListTodoIcon :size="48" />
|
||||||
|
<h4>{{ t('pages.upload.taskQueue.empty') }}</h4>
|
||||||
|
<p>{{ t('pages.upload.taskQueue.emptyHint') }}</p>
|
||||||
|
<button class="action-btn primary" @click="addFilesToTask">
|
||||||
|
<PlusIcon :size="16" />
|
||||||
|
{{ t('pages.upload.taskQueue.selectFiles') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
import {
|
import {
|
||||||
ArrowLeftRightIcon,
|
ArrowLeftRightIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
ClipboardIcon,
|
ClipboardIcon,
|
||||||
|
ClockIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
|
HardDriveIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
|
ListTodoIcon,
|
||||||
|
LoaderIcon,
|
||||||
|
PauseIcon,
|
||||||
|
PlayIcon,
|
||||||
|
PlusIcon,
|
||||||
|
RefreshCwIcon,
|
||||||
|
SearchIcon,
|
||||||
Settings,
|
Settings,
|
||||||
|
SettingsIcon,
|
||||||
|
StarIcon,
|
||||||
|
TimerIcon,
|
||||||
|
Trash2Icon,
|
||||||
UploadCloudIcon,
|
UploadCloudIcon,
|
||||||
|
XCircleIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
|
ZapIcon,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { computed, onBeforeMount, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'
|
import { computed, onBeforeMount, onBeforeUnmount, reactive, ref, useTemplateRef, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
@@ -193,6 +562,49 @@ import { getConfig, saveConfig } from '@/utils/dataSender'
|
|||||||
import { useDragEventListeners } from '@/utils/drag'
|
import { useDragEventListeners } from '@/utils/drag'
|
||||||
import { IPasteStyle, IRPCActionType } from '@/utils/enum'
|
import { IPasteStyle, IRPCActionType } from '@/utils/enum'
|
||||||
|
|
||||||
|
// Task queue types
|
||||||
|
interface IUploadTaskItem {
|
||||||
|
id: string
|
||||||
|
fileName: string
|
||||||
|
filePath: string
|
||||||
|
fileSize: number
|
||||||
|
status: string
|
||||||
|
progress: number
|
||||||
|
error?: string
|
||||||
|
result?: any
|
||||||
|
createdAt: number
|
||||||
|
startedAt?: number
|
||||||
|
completedAt?: number
|
||||||
|
retryCount: number
|
||||||
|
priority: number
|
||||||
|
uploadSpeed?: number
|
||||||
|
uploadDuration?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUploadTaskQueueStatus {
|
||||||
|
tasks: IUploadTaskItem[]
|
||||||
|
config: {
|
||||||
|
intervalS: number
|
||||||
|
isRunning: boolean
|
||||||
|
isPaused: boolean
|
||||||
|
autoStart: boolean
|
||||||
|
pauseOnError: boolean
|
||||||
|
maxRetryCount: number
|
||||||
|
}
|
||||||
|
stats: {
|
||||||
|
total: number
|
||||||
|
pending: number
|
||||||
|
completed: number
|
||||||
|
failed: number
|
||||||
|
cancelled: number
|
||||||
|
uploading: number
|
||||||
|
totalSize: number
|
||||||
|
completedSize: number
|
||||||
|
avgSpeed: number
|
||||||
|
estimatedTimeMs: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useDragEventListeners()
|
useDragEventListeners()
|
||||||
const $router = useRouter()
|
const $router = useRouter()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -200,6 +612,7 @@ const message = useMessage()
|
|||||||
const { picBedG, defaultPicBedG, defaultConfigNameG, defaultIdG, updatePicBeds } = usePicBed()
|
const { picBedG, defaultPicBedG, defaultConfigNameG, defaultIdG, updatePicBeds } = usePicBed()
|
||||||
|
|
||||||
const imageProcessDialogVisible = ref(false)
|
const imageProcessDialogVisible = ref(false)
|
||||||
|
const taskDialogVisible = ref(false)
|
||||||
const useShortUrl = ref(false)
|
const useShortUrl = ref(false)
|
||||||
const dragover = ref(false)
|
const dragover = ref(false)
|
||||||
const progress = ref(0)
|
const progress = ref(0)
|
||||||
@@ -208,6 +621,65 @@ const showError = ref(false)
|
|||||||
const pasteStyle = ref(IPasteStyle.MARKDOWN)
|
const pasteStyle = ref(IPasteStyle.MARKDOWN)
|
||||||
const PicBedId = ref('')
|
const PicBedId = ref('')
|
||||||
const fileInput = useTemplateRef('fileInput')
|
const fileInput = useTemplateRef('fileInput')
|
||||||
|
const uploadInterval = ref(1000)
|
||||||
|
|
||||||
|
// New task queue settings
|
||||||
|
const showTaskSettings = useStorage('upload-task-queue-show-settings', true)
|
||||||
|
const taskSearchQuery = ref('')
|
||||||
|
const taskFilter = ref<'all' | 'pending' | 'completed' | 'failed'>('all')
|
||||||
|
const autoStart = ref(false)
|
||||||
|
const pauseOnError = ref(false)
|
||||||
|
const maxRetryCount = ref(3)
|
||||||
|
|
||||||
|
// Task queue status
|
||||||
|
const taskQueueStatus = reactive<IUploadTaskQueueStatus>({
|
||||||
|
tasks: [],
|
||||||
|
config: {
|
||||||
|
intervalS: 1,
|
||||||
|
isRunning: false,
|
||||||
|
isPaused: false,
|
||||||
|
autoStart: false,
|
||||||
|
pauseOnError: false,
|
||||||
|
maxRetryCount: 3,
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
total: 0,
|
||||||
|
pending: 0,
|
||||||
|
completed: 0,
|
||||||
|
failed: 0,
|
||||||
|
cancelled: 0,
|
||||||
|
uploading: 0,
|
||||||
|
totalSize: 0,
|
||||||
|
completedSize: 0,
|
||||||
|
avgSpeed: 0,
|
||||||
|
estimatedTimeMs: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
const filteredTasks = computed(() => {
|
||||||
|
let tasks = taskQueueStatus.tasks
|
||||||
|
|
||||||
|
// Filter by status
|
||||||
|
if (taskFilter.value !== 'all') {
|
||||||
|
tasks = tasks.filter(t => t.status === taskFilter.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by search query
|
||||||
|
if (taskSearchQuery.value) {
|
||||||
|
const query = taskSearchQuery.value.toLowerCase()
|
||||||
|
tasks = tasks.filter(t => t.fileName.toLowerCase().includes(query))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks
|
||||||
|
})
|
||||||
|
|
||||||
|
const overallProgressPercent = computed(() => {
|
||||||
|
if (taskQueueStatus.stats.total === 0) return 0
|
||||||
|
const completed = taskQueueStatus.stats.completed
|
||||||
|
const total = taskQueueStatus.stats.total - taskQueueStatus.stats.cancelled
|
||||||
|
return total > 0 ? Math.round((completed / total) * 100) : 0
|
||||||
|
})
|
||||||
|
|
||||||
const picBedName = computed(() => {
|
const picBedName = computed(() => {
|
||||||
if (!picBedG.value || picBedG.value.length === 0) {
|
if (!picBedG.value || picBedG.value.length === 0) {
|
||||||
@@ -421,18 +893,193 @@ async function handleChangePicBed() {
|
|||||||
window.electron.sendRPC(IRPCActionType.SHOW_UPLOAD_PAGE_MENU)
|
window.electron.sendRPC(IRPCActionType.SHOW_UPLOAD_PAGE_MENU)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openTaskDialog() {
|
||||||
|
taskDialogVisible.value = true
|
||||||
|
refreshTaskStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshTaskStatus() {
|
||||||
|
const status = await window.electron.triggerRPC<IUploadTaskQueueStatus>(IRPCActionType.UPLOAD_TASK_GET_STATUS)
|
||||||
|
if (status) {
|
||||||
|
Object.assign(taskQueueStatus, status)
|
||||||
|
uploadInterval.value = status.config.intervalS
|
||||||
|
autoStart.value = status.config.autoStart
|
||||||
|
pauseOnError.value = status.config.pauseOnError
|
||||||
|
maxRetryCount.value = status.config.maxRetryCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addFilesToTask() {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.multiple = true
|
||||||
|
input.onchange = async (e: Event) => {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
if (target.files && target.files.length > 0) {
|
||||||
|
const files: IFileWithPath[] = Array.from(target.files).map(file => ({
|
||||||
|
name: file.name,
|
||||||
|
path: window.electron.showFilePath(file),
|
||||||
|
}))
|
||||||
|
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_ADD, files)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
message.success(t('pages.upload.taskQueue.filesAdded', { count: files.length }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startTaskQueue() {
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_START, uploadInterval.value)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
message.success(t('pages.upload.taskQueue.started'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pauseTaskQueue() {
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_PAUSE)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
message.info(t('pages.upload.taskQueue.paused'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resumeTaskQueue() {
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_RESUME)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
message.success(t('pages.upload.taskQueue.resumed'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelAllTasks() {
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_CANCEL_ALL)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
message.info(t('pages.upload.taskQueue.allCancelled'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelTask(taskId: string) {
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_CANCEL_ONE, taskId)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTask(taskId: string) {
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_REMOVE_ONE, taskId)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearFinishedTasks() {
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_CLEAR_FINISHED)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
message.success(t('pages.upload.taskQueue.cleared'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateInterval() {
|
||||||
|
uploadInterval.value = Math.max(100, Math.min(60000, uploadInterval.value))
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_SET_INTERVAL, uploadInterval.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function retryTask(taskId: string) {
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_RETRY_ONE, taskId)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
message.success(t('pages.upload.taskQueue.taskRetried'))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function retryAllFailedTasks() {
|
||||||
|
const count = await window.electron.triggerRPC<number>(IRPCActionType.UPLOAD_TASK_RETRY_ALL_FAILED)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
message.success(t('pages.upload.taskQueue.retriedAllFailed', { count }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveTaskUp(taskId: string) {
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_MOVE_UP, taskId)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveTaskDown(taskId: string) {
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_MOVE_DOWN, taskId)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleTaskPriority(taskId: string, currentPriority: number) {
|
||||||
|
const newPriority = currentPriority === 2 ? 1 : 2
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_SET_PRIORITY, taskId, newPriority)
|
||||||
|
await refreshTaskStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSettings() {
|
||||||
|
await window.electron.triggerRPC(IRPCActionType.UPLOAD_TASK_UPDATE_SETTINGS, {
|
||||||
|
intervalS: uploadInterval.value,
|
||||||
|
autoStart: autoStart.value,
|
||||||
|
pauseOnError: pauseOnError.value,
|
||||||
|
maxRetryCount: maxRetryCount.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function formatSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSpeed(bytesPerSecond: number): string {
|
||||||
|
return formatSize(bytesPerSecond) + '/s'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(ms: number): string {
|
||||||
|
if (ms < 1000) return '< 1s'
|
||||||
|
const seconds = Math.floor(ms / 1000)
|
||||||
|
if (seconds < 60) return `${seconds}s`
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const remainingSeconds = seconds % 60
|
||||||
|
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`
|
||||||
|
const hours = Math.floor(minutes / 60)
|
||||||
|
const remainingMinutes = minutes % 60
|
||||||
|
return `${hours}h ${remainingMinutes}m`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTaskStatusClass(status: string): string {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
pending: 'status-pending',
|
||||||
|
uploading: 'status-uploading',
|
||||||
|
completed: 'status-completed',
|
||||||
|
failed: 'status-failed',
|
||||||
|
cancelled: 'status-cancelled',
|
||||||
|
}
|
||||||
|
return statusMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTaskStatusText(status: string): string {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
pending: t('pages.upload.taskQueue.statusPending'),
|
||||||
|
uploading: t('pages.upload.taskQueue.statusUploading'),
|
||||||
|
completed: t('pages.upload.taskQueue.statusCompleted'),
|
||||||
|
failed: t('pages.upload.taskQueue.statusFailed'),
|
||||||
|
cancelled: t('pages.upload.taskQueue.statusCancelled'),
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
function taskQueueUpdateHandler(status: IUploadTaskQueueStatus) {
|
||||||
|
Object.assign(taskQueueStatus, status)
|
||||||
|
uploadInterval.value = status.config.intervalS
|
||||||
|
}
|
||||||
|
|
||||||
|
let removeTaskQueueUpdateListenerCallback: () => void = () => {}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
$bus.off(SHOW_INPUT_BOX_RESPONSE)
|
$bus.off(SHOW_INPUT_BOX_RESPONSE)
|
||||||
removeUploadProgressListenerCallback()
|
removeUploadProgressListenerCallback()
|
||||||
removeSyncPicBedListenerCallback()
|
removeSyncPicBedListenerCallback()
|
||||||
|
removeTaskQueueUpdateListenerCallback()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
removeUploadProgressListenerCallback = window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler)
|
removeUploadProgressListenerCallback = window.electron.ipcRendererOn('uploadProgress', uploadProgressHandler)
|
||||||
removeSyncPicBedListenerCallback = window.electron.ipcRendererOn('syncPicBed', syncPicBedHandler)
|
removeSyncPicBedListenerCallback = window.electron.ipcRendererOn('syncPicBed', syncPicBedHandler)
|
||||||
|
removeTaskQueueUpdateListenerCallback = window.electron.ipcRendererOn('uploadTaskQueueUpdate', taskQueueUpdateHandler)
|
||||||
$bus.on(SHOW_INPUT_BOX_RESPONSE, handleInputBoxValue)
|
$bus.on(SHOW_INPUT_BOX_RESPONSE, handleInputBoxValue)
|
||||||
getUseShortUrl()
|
getUseShortUrl()
|
||||||
getPasteStyle()
|
getPasteStyle()
|
||||||
|
refreshTaskStatus()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,27 @@ export const IRPCActionType = {
|
|||||||
UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE: 'UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE',
|
UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE: 'UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE',
|
||||||
UPLOAD_CHOOSED_FILES: 'UPLOAD_CHOOSED_FILES',
|
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 rpc
|
||||||
GALLERY_PASTE_TEXT: 'GALLERY_PASTE_TEXT',
|
GALLERY_PASTE_TEXT: 'GALLERY_PASTE_TEXT',
|
||||||
GALLERY_REMOVE_FILES: 'GALLERY_REMOVE_FILES',
|
GALLERY_REMOVE_FILES: 'GALLERY_REMOVE_FILES',
|
||||||
|
|||||||
2
src/universal/types/i18nt.d.ts
vendored
2
src/universal/types/i18nt.d.ts
vendored
@@ -69,5 +69,7 @@ interface ILocales {
|
|||||||
QUIT: string
|
QUIT: string
|
||||||
OPERATION_SUCCEED: string
|
OPERATION_SUCCEED: string
|
||||||
OPERATION_FAILED: string
|
OPERATION_FAILED: string
|
||||||
|
UPLOAD_TASK_COMPLETED: string
|
||||||
|
UPLOAD_TASK_COMPLETED_BODY: string
|
||||||
}
|
}
|
||||||
type ILocalesKey = keyof ILocales
|
type ILocalesKey = keyof ILocales
|
||||||
|
|||||||
Reference in New Issue
Block a user