新增拖入功能

This commit is contained in:
kura 2026-05-01 23:38:09 +08:00
parent 7e9abf5f07
commit 75dfb67754
5 changed files with 113 additions and 1 deletions

View File

@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog' import { open } from '@tauri-apps/plugin-dialog'
import { listen, type UnlistenFn } from '@tauri-apps/api/event' import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import TaskQueue from './components/TaskQueue.vue' import TaskQueue from './components/TaskQueue.vue'
import SubtitleEditor from './components/SubtitleEditor.vue' import SubtitleEditor from './components/SubtitleEditor.vue'
import { useTaskStore } from './stores/tasks' import { useTaskStore } from './stores/tasks'
@ -71,7 +72,9 @@ const translationConfig = ref<TranslationConfig>({
const pending = ref(false) const pending = ref(false)
const feedback = ref('') const feedback = ref('')
const showAdvanced = ref(false) const showAdvanced = ref(false)
const isDragging = ref(false)
let unlistenMenuAction: UnlistenFn | null = null let unlistenMenuAction: UnlistenFn | null = null
let dragDropUnlistenFn: UnlistenFn | null = null
const selectedTask = computed(() => taskStore.selectedTask) const selectedTask = computed(() => taskStore.selectedTask)
const hasTranslationKey = computed(() => translationConfig.value.apiKey.trim().length > 0) const hasTranslationKey = computed(() => translationConfig.value.apiKey.trim().length > 0)
@ -80,6 +83,7 @@ onMounted(() => {
taskStore.initialize() taskStore.initialize()
void loadDefaultModelPaths() void loadDefaultModelPaths()
void bindMenuActions() void bindMenuActions()
void initDragDrop()
}) })
onUnmounted(() => { onUnmounted(() => {
@ -87,6 +91,10 @@ onUnmounted(() => {
unlistenMenuAction() unlistenMenuAction()
unlistenMenuAction = null unlistenMenuAction = null
} }
if (dragDropUnlistenFn) {
dragDropUnlistenFn()
dragDropUnlistenFn = null
}
}) })
watch(locale, (newLocale) => { watch(locale, (newLocale) => {
@ -219,6 +227,43 @@ async function handlePickFiles() {
} }
} }
function getFileExtension(filePath: string): string {
const lastDot = filePath.lastIndexOf('.')
if (lastDot === -1) return ''
return filePath.slice(lastDot + 1).toLowerCase()
}
function isMediaFile(filePath: string): boolean {
const ext = getFileExtension(filePath)
return MEDIA_EXTENSIONS.includes(ext as typeof MEDIA_EXTENSIONS[number])
}
async function initDragDrop() {
try {
dragDropUnlistenFn = await getCurrentWebviewWindow().onDragDropEvent((event) => {
if (event.payload.type === 'enter' || event.payload.type === 'over') {
isDragging.value = true
} else if (event.payload.type === 'leave') {
isDragging.value = false
} else if (event.payload.type === 'drop') {
isDragging.value = false
const mediaFiles = event.payload.paths.filter(isMediaFile)
if (mediaFiles.length === 0) {
feedback.value = t('app.feedback.noMediaFiles')
return
}
if (mediaFiles.length !== event.payload.paths.length) {
const skipped = event.payload.paths.length - mediaFiles.length
feedback.value = t('app.feedback.someFilesSkipped', { count: skipped })
}
void submitFiles(mediaFiles)
}
})
} catch (error) {
console.error('Failed to initialize drag-drop:', error)
}
}
async function handleFiles(event: Event) { async function handleFiles(event: Event) {
const input = event.target as HTMLInputElement const input = event.target as HTMLInputElement
const files = Array.from(input.files ?? []) const files = Array.from(input.files ?? [])
@ -264,6 +309,18 @@ async function handleTranslateFromEditor() {
<template> <template>
<main class="app-shell"> <main class="app-shell">
<Transition name="drag">
<div v-if="isDragging" class="drag-overlay">
<div class="drag-overlay-content">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<span>{{ $t('app.feedback.dropHint') }}</span>
</div>
</div>
</Transition>
<section class="topbar panel"> <section class="topbar panel">
<div class="toolbar-main"> <div class="toolbar-main">
<div class="toolbar-title"> <div class="toolbar-title">

View File

@ -2,7 +2,7 @@
import { computed, ref, nextTick, watch } from 'vue' import { computed, ref, nextTick, watch } from 'vue'
import type { SubtitleSegment, SubtitleTask } from '../lib/types' import type { SubtitleSegment, SubtitleTask } from '../lib/types'
const PAGE_SIZE = 200 const PAGE_SIZE = 100
const props = defineProps<{ const props = defineProps<{
task: SubtitleTask | null task: SubtitleTask | null

View File

@ -30,6 +30,9 @@ export default {
noApiKey: 'Please configure LLM API Key first', noApiKey: 'Please configure LLM API Key first',
translationStarted: 'Translation task started', translationStarted: 'Translation task started',
translationFailed: 'Translation failed', translationFailed: 'Translation failed',
noMediaFiles: 'No supported media files found',
someFilesSkipped: '{count} file(s) skipped (unsupported format)',
dropHint: 'Drop to add files',
}, },
llm: { llm: {
apiBase: 'LLM API Base', apiBase: 'LLM API Base',

View File

@ -30,6 +30,9 @@ export default {
noApiKey: '请先配置 LLM API Key', noApiKey: '请先配置 LLM API Key',
translationStarted: '翻译任务已开始', translationStarted: '翻译任务已开始',
translationFailed: '翻译失败', translationFailed: '翻译失败',
noMediaFiles: '拖入的文件中没有支持的音视频文件',
someFilesSkipped: '{count} 个文件被跳过(不支持的格式)',
dropHint: '松开以添加文件',
}, },
llm: { llm: {
apiBase: 'LLM API Base', apiBase: 'LLM API Base',

View File

@ -885,3 +885,52 @@ textarea {
grid-column: span 1; grid-column: span 1;
} }
} }
.drag-overlay {
position: fixed;
inset: 0;
z-index: 9999;
background: rgba(245, 245, 247, 0.85);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
border: 3px dashed var(--c-accent);
margin: 12px;
border-radius: calc(var(--radius-lg) + 4px);
}
.drag-overlay-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
color: var(--c-accent);
font-size: 18px;
font-weight: 500;
opacity: 0.8;
}
.drag-overlay-content svg {
animation: drag-bounce 1.2s ease-in-out infinite;
}
@keyframes drag-bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
.drag-enter-active,
.drag-leave-active {
transition: opacity 0.2s ease;
}
.drag-enter-from,
.drag-leave-to {
opacity: 0;
}