新增拖入功能
This commit is contained in:
parent
7e9abf5f07
commit
75dfb67754
57
src/App.vue
57
src/App.vue
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user