托盘
This commit is contained in:
@@ -11,9 +11,10 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog'
|
||||
import { useDownloaderStore, type DownloadTask, type TaskStatus } from '@/stores/downloaderStore'
|
||||
import { useDownloaderStore, type DownloadTask, type TaskStatus, type CheckUrlResult } from '@/stores/downloaderStore'
|
||||
import { useModuleTabs } from '@/lib/useModuleTabs'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
import { pendingNewDownload } from '@/lib/trayEvents'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -257,6 +258,15 @@ const handleOpenDir = async (task: DownloadTask) => {
|
||||
}
|
||||
|
||||
// ===== 添加下载 =====
|
||||
/** 重复确认对话框状态 */
|
||||
const duplicateDialogState = ref<{
|
||||
open: boolean
|
||||
url: string
|
||||
result: CheckUrlResult | null
|
||||
pendingUrls: string[]
|
||||
currentIndex: number
|
||||
}>({ open: false, url: '', result: null, pendingUrls: [], currentIndex: 0 })
|
||||
|
||||
const handleAddDownload = async () => {
|
||||
const text = addUriText.value.trim()
|
||||
if (!text) {
|
||||
@@ -272,23 +282,23 @@ const handleAddDownload = async () => {
|
||||
addingTask.value = true
|
||||
try {
|
||||
const dir = addDir.value.trim() || undefined
|
||||
let successCount = 0
|
||||
for (const uri of uris) {
|
||||
try {
|
||||
await store.addTask(uri, undefined, dir)
|
||||
successCount++
|
||||
} catch (e) {
|
||||
logger.error(`添加 ${uri} 失败: ` + e)
|
||||
}
|
||||
}
|
||||
if (successCount > 0) {
|
||||
toast.success(`已添加 ${successCount} 个下载任务`)
|
||||
addUriText.value = ''
|
||||
addDir.value = ''
|
||||
addDialogOpen.value = false
|
||||
activeTab.value = 'tasks'
|
||||
// 检查是否启用重复检查
|
||||
const checkEnabled = store.settings?.checkDuplicate ?? true
|
||||
if (checkEnabled) {
|
||||
// 逐个检查重复
|
||||
await processUrlsWithCheck(uris, dir)
|
||||
} else {
|
||||
toast.error('添加任务失败')
|
||||
// 直接添加
|
||||
let successCount = 0
|
||||
for (const uri of uris) {
|
||||
try {
|
||||
await store.addTask(uri, undefined, dir, undefined, true)
|
||||
successCount++
|
||||
} catch (e) {
|
||||
logger.error(`添加 ${uri} 失败: ` + e)
|
||||
}
|
||||
}
|
||||
finishAdd(successCount)
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('添加失败: ' + e)
|
||||
@@ -297,6 +307,146 @@ const handleAddDownload = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 逐个检查 URL 重复性,发现重复时弹出确认对话框 */
|
||||
async function processUrlsWithCheck(urls: string[], dir: string | undefined) {
|
||||
let successCount = 0
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const uri = urls[i]
|
||||
try {
|
||||
const result = await store.checkUrl(uri, dir)
|
||||
if (!result.ok) {
|
||||
// 探测失败:直接添加(后端会标记为 error)
|
||||
try {
|
||||
await store.addTask(uri, undefined, dir, undefined, true)
|
||||
successCount++
|
||||
} catch (e) {
|
||||
logger.error(`添加 ${uri} 失败: ` + e)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (result.duplicate === 'none') {
|
||||
// 无重复:直接添加(自动重命名以防磁盘文件冲突)
|
||||
try {
|
||||
await store.addTask(uri, undefined, dir, undefined, true)
|
||||
successCount++
|
||||
} catch (e) {
|
||||
logger.error(`添加 ${uri} 失败: ` + e)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 发现重复:弹出确认对话框,暂停处理
|
||||
duplicateDialogState.value = {
|
||||
open: true,
|
||||
url: uri,
|
||||
result,
|
||||
pendingUrls: urls,
|
||||
currentIndex: i,
|
||||
}
|
||||
return // 等待用户确认后继续
|
||||
} catch (e) {
|
||||
logger.error(`检查 ${uri} 失败: ` + e)
|
||||
// 检查失败:直接添加
|
||||
try {
|
||||
await store.addTask(uri, undefined, dir, undefined, true)
|
||||
successCount++
|
||||
} catch (e2) {
|
||||
logger.error(`添加 ${uri} 失败: ` + e2)
|
||||
}
|
||||
}
|
||||
}
|
||||
finishAdd(successCount)
|
||||
}
|
||||
|
||||
/** 完成添加:显示提示并关闭对话框 */
|
||||
function finishAdd(successCount: number) {
|
||||
if (successCount > 0) {
|
||||
toast.success(`已添加 ${successCount} 个下载任务`)
|
||||
addUriText.value = ''
|
||||
addDir.value = ''
|
||||
addDialogOpen.value = false
|
||||
activeTab.value = 'tasks'
|
||||
} else {
|
||||
toast.error('添加任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 重复确认对话框:用户选择"仍然下载"(自动重命名) */
|
||||
const onDuplicateConfirm = async () => {
|
||||
const { url, pendingUrls, currentIndex } = duplicateDialogState.value
|
||||
const dir = addDir.value.trim() || undefined
|
||||
duplicateDialogState.value.open = false
|
||||
|
||||
let successCount = 0
|
||||
try {
|
||||
await store.addTask(url, undefined, dir, undefined, true) // autoRename=true
|
||||
successCount++
|
||||
} catch (e) {
|
||||
logger.error(`添加 ${url} 失败: ` + e)
|
||||
}
|
||||
|
||||
// 继续处理剩余 URL
|
||||
const remaining = pendingUrls.slice(currentIndex + 1)
|
||||
for (const uri of remaining) {
|
||||
try {
|
||||
const result = await store.checkUrl(uri, dir)
|
||||
if (result.ok && result.duplicate !== 'none') {
|
||||
// 又发现重复,再次弹出确认
|
||||
duplicateDialogState.value = {
|
||||
open: true,
|
||||
url: uri,
|
||||
result,
|
||||
pendingUrls,
|
||||
currentIndex: pendingUrls.indexOf(uri),
|
||||
}
|
||||
// 已添加的成功数通过闭包传递不太方便,直接在这里 finish 后再继续
|
||||
// 简化处理:保存 successCount 到 state,下一次确认时累加
|
||||
duplicateSuccessCount.value = successCount
|
||||
return
|
||||
}
|
||||
await store.addTask(uri, undefined, dir, undefined, true)
|
||||
successCount++
|
||||
} catch (e) {
|
||||
logger.error(`添加 ${uri} 失败: ` + e)
|
||||
}
|
||||
}
|
||||
finishAdd(successCount)
|
||||
}
|
||||
|
||||
/** 累积的成功计数(跨多次重复确认) */
|
||||
const duplicateSuccessCount = ref(0)
|
||||
|
||||
/** 重复确认对话框:用户选择"跳过" */
|
||||
const onDuplicateSkip = () => {
|
||||
const { pendingUrls, currentIndex } = duplicateDialogState.value
|
||||
duplicateDialogState.value.open = false
|
||||
const remaining = pendingUrls.slice(currentIndex + 1)
|
||||
if (remaining.length > 0) {
|
||||
// 继续处理剩余 URL(不添加当前这个)
|
||||
const dir = addDir.value.trim() || undefined
|
||||
processUrlsWithCheck(remaining, dir).then(() => {})
|
||||
} else {
|
||||
finishAdd(duplicateSuccessCount.value)
|
||||
duplicateSuccessCount.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
/** 重复确认对话框:用户取消整个添加流程 */
|
||||
const onDuplicateCancel = () => {
|
||||
duplicateDialogState.value.open = false
|
||||
finishAdd(duplicateSuccessCount.value)
|
||||
duplicateSuccessCount.value = 0
|
||||
}
|
||||
|
||||
/** 重复类型文本 */
|
||||
const duplicateKindText = (kind: string): string => {
|
||||
switch (kind) {
|
||||
case 'url': return 'URL 已存在下载任务'
|
||||
case 'filename': return '同名文件已在下载列表中'
|
||||
case 'fileExists': return '目标目录已存在同名文件'
|
||||
default: return '重复'
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectDir = async () => {
|
||||
try {
|
||||
const selected = await openDialog({ directory: true, multiple: false })
|
||||
@@ -367,6 +517,11 @@ const handleCopy = async (text: string, label: string) => {
|
||||
|
||||
onMounted(async () => {
|
||||
await store.init()
|
||||
// 消费托盘菜单"新建下载"标志位
|
||||
if (pendingNewDownload.value) {
|
||||
pendingNewDownload.value = false
|
||||
addDialogOpen.value = true
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -458,8 +613,8 @@ const toggleSortOrder = () => {
|
||||
<TabsContent value="tasks" class="flex-1 mt-4 min-h-0 tab-animate">
|
||||
<div class="h-full flex flex-col gap-4">
|
||||
<!-- 状态栏 -->
|
||||
<Card class="shrink-0">
|
||||
<CardContent class="pl-4 pr-4">
|
||||
<Card class="shrink-0 !py-0 !gap-0">
|
||||
<CardContent class="pl-4 pr-4 py-3">
|
||||
<div class="flex items-center justify-between flex-wrap gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -611,8 +766,8 @@ const toggleSortOrder = () => {
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else key="task-list" class="flex flex-col gap-3 pb-4">
|
||||
<Card v-for="task in pagedTasks" :key="task.id" class="overflow-hidden">
|
||||
<CardContent class="pl-4 pr-4">
|
||||
<Card v-for="task in pagedTasks" :key="task.id" class="overflow-hidden !py-0 !gap-0">
|
||||
<CardContent class="pl-4 pr-4 py-3">
|
||||
<div class="flex items-start justify-between gap-3 mb-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
@@ -893,6 +1048,17 @@ const toggleSortOrder = () => {
|
||||
@update:model-value="(v: boolean) => store.settings && (store.settings.deleteFilesOnRemove = v)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<Label for="check-duplicate" class="cursor-pointer">重复下载检查</Label>
|
||||
<span class="text-xs text-muted-foreground">添加下载前检查 URL 和文件名重复,发现重复时询问</span>
|
||||
</div>
|
||||
<Switch
|
||||
id="check-duplicate"
|
||||
:model-value="store.settings?.checkDuplicate ?? true"
|
||||
@update:model-value="(v: boolean) => store.settings && (store.settings.checkDuplicate = v)"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1218,6 +1384,39 @@ const toggleSortOrder = () => {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- ===== 重复下载确认弹窗 ===== -->
|
||||
<AlertDialog :open="duplicateDialogState.open">
|
||||
<AlertDialogContent class="max-w-md">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle class="flex items-center gap-2">
|
||||
<AlertCircle class="h-4 w-4 text-amber-500" />
|
||||
下载重复提醒
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription class="text-xs">
|
||||
<span class="block mb-2">
|
||||
{{ duplicateKindText(duplicateDialogState.result?.duplicate ?? '') }}
|
||||
</span>
|
||||
<span class="block font-mono text-xs break-all">{{ duplicateDialogState.url }}</span>
|
||||
<span v-if="duplicateDialogState.result?.existing" class="block mt-2 text-xs">
|
||||
已存在:
|
||||
<span class="font-medium">{{ duplicateDialogState.result.existing.filename }}</span>
|
||||
</span>
|
||||
<span class="block mt-2 text-muted-foreground">
|
||||
选择"仍然下载"将自动重命名(追加序号),选择"跳过"将不下载此链接。
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel @click="onDuplicateCancel">取消全部</AlertDialogCancel>
|
||||
<Button variant="outline" @click="onDuplicateSkip">跳过此链接</Button>
|
||||
<AlertDialogAction @click="onDuplicateConfirm">
|
||||
<Download class="h-4 w-4" />
|
||||
仍然下载
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<!-- ===== 删除任务专用对话框(带"同时删除文件"开关) ===== -->
|
||||
<AlertDialog :open="removeDialogState.open" @update:open="onRemoveOpenChange">
|
||||
<AlertDialogContent>
|
||||
|
||||
Reference in New Issue
Block a user