178 lines
6.3 KiB
Vue
178 lines
6.3 KiB
Vue
<script setup lang="ts">
|
||
import { computed, ref, nextTick, watch } from 'vue'
|
||
import type { SubtitleSegment, SubtitleTask } from '../lib/types'
|
||
|
||
const PAGE_SIZE = 100
|
||
|
||
const props = defineProps<{
|
||
task: SubtitleTask | null
|
||
logs: string[]
|
||
}>()
|
||
|
||
const isProcessing = computed(() => {
|
||
if (!props.task) return false
|
||
return !['completed', 'failed'].includes(props.task.status)
|
||
})
|
||
|
||
const canExport = computed(() => {
|
||
return props.task?.status === 'completed' && (props.task.segments?.length ?? 0) > 0
|
||
})
|
||
|
||
const canTranslate = computed(() => {
|
||
if (!props.task) return false
|
||
return (
|
||
props.task.segments.length > 0 &&
|
||
props.task.segments.some((s) => s.sourceText)
|
||
)
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
save: [segment: SubtitleSegment]
|
||
export: [format: 'srt' | 'vtt' | 'ass']
|
||
translate: []
|
||
}>()
|
||
|
||
const sortedSegments = computed(() =>
|
||
[...(props.task?.segments ?? [])].sort((left, right) => {
|
||
if (left.start !== right.start) return left.start - right.start
|
||
if (left.end !== right.end) return left.end - right.end
|
||
return left.id.localeCompare(right.id)
|
||
}),
|
||
)
|
||
|
||
const currentPage = ref(1)
|
||
const totalPages = computed(() => Math.max(1, Math.ceil(sortedSegments.value.length / PAGE_SIZE)))
|
||
const hasPagination = computed(() => sortedSegments.value.length > PAGE_SIZE)
|
||
|
||
const visibleSegments = computed(() => {
|
||
const start = (currentPage.value - 1) * PAGE_SIZE
|
||
return sortedSegments.value.slice(start, start + PAGE_SIZE)
|
||
})
|
||
|
||
watch(totalPages, (newTotal) => {
|
||
if (currentPage.value > newTotal) {
|
||
currentPage.value = newTotal
|
||
}
|
||
})
|
||
|
||
function goToPage(page: number) {
|
||
currentPage.value = Math.max(1, Math.min(page, totalPages.value))
|
||
}
|
||
|
||
function prevPage() {
|
||
goToPage(currentPage.value - 1)
|
||
}
|
||
|
||
function nextPage() {
|
||
goToPage(currentPage.value + 1)
|
||
}
|
||
|
||
const logsExpanded = ref(false)
|
||
|
||
const logContainerRef = ref<HTMLElement | null>(null)
|
||
|
||
watch(() => props.logs.length, () => {
|
||
nextTick(() => {
|
||
if (logContainerRef.value) {
|
||
logContainerRef.value.scrollTop = logContainerRef.value.scrollHeight
|
||
}
|
||
})
|
||
})
|
||
|
||
function formatTime(seconds: number) {
|
||
const ms = Math.round(seconds * 1000)
|
||
const h = Math.floor(ms / 3600000)
|
||
const m = Math.floor((ms % 3600000) / 60000)
|
||
const s = Math.floor((ms % 60000) / 1000)
|
||
const milli = ms % 1000
|
||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${String(milli).padStart(3, '0')}`
|
||
}
|
||
|
||
function updateTranslatedText(segment: SubtitleSegment, value: string) {
|
||
emit('save', { ...segment, translatedText: value })
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="panel workspace-panel">
|
||
<div class="workspace-header">
|
||
<div>
|
||
<strong>{{ task?.fileName ?? $t('editor.title') }}</strong>
|
||
<p class="panel-subtitle">
|
||
{{ task ? $t('editor.segments', { count: sortedSegments.length }) : $t('editor.selectTask') }}
|
||
</p>
|
||
</div>
|
||
<div v-if="task" class="export-actions">
|
||
<button
|
||
v-if="canTranslate"
|
||
class="button primary small"
|
||
:disabled="isProcessing"
|
||
@click="emit('translate')"
|
||
>{{ $t('editor.translate') }}</button>
|
||
<button class="button secondary small" :disabled="!canExport" @click="emit('export', 'srt')">SRT</button>
|
||
<button class="button secondary small" :disabled="!canExport" @click="emit('export', 'vtt')">VTT</button>
|
||
<button class="button secondary small" :disabled="!canExport" @click="emit('export', 'ass')">ASS</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="!task" class="empty-state">
|
||
<p>{{ $t('editor.empty') }}</p>
|
||
<p style="margin-top: 6px; font-size: 11px;">{{ $t('editor.emptyHint') }}</p>
|
||
</div>
|
||
|
||
<div v-else-if="sortedSegments.length === 0" class="empty-state">
|
||
<template v-if="isProcessing">{{ $t('editor.processing') }}</template>
|
||
<template v-else-if="task?.status === 'failed'">{{ $t('editor.failed') }}</template>
|
||
<template v-else>{{ $t('editor.noSegments') }}</template>
|
||
</div>
|
||
|
||
<div v-else class="segment-list">
|
||
<div v-if="hasPagination" class="pagination-bar">
|
||
<button class="button small" :disabled="currentPage <= 1" @click="prevPage">‹</button>
|
||
<span class="pagination-info">{{ $t('editor.pageInfo', { current: currentPage, total: totalPages }) }}</span>
|
||
<button class="button small" :disabled="currentPage >= totalPages" @click="nextPage">›</button>
|
||
<span class="pagination-count">{{ $t('editor.segments', { count: sortedSegments.length }) }}</span>
|
||
</div>
|
||
<article
|
||
v-for="segment in visibleSegments"
|
||
:key="segment.id"
|
||
class="segment-item"
|
||
>
|
||
<div class="task-row subtle">
|
||
<span>{{ formatTime(segment.start) }} - {{ formatTime(segment.end) }}</span>
|
||
<span class="segment-id">{{ segment.id }}</span>
|
||
</div>
|
||
<p class="source-text">{{ segment.sourceText || $t('editor.waiting') }}</p>
|
||
<textarea
|
||
class="editor-input"
|
||
:value="segment.translatedText ?? ''"
|
||
:placeholder="task.outputMode === 'translate' ? $t('editor.translation') : $t('editor.source')"
|
||
:disabled="task.outputMode === 'source'"
|
||
@change="updateTranslatedText(segment, ($event.target as HTMLTextAreaElement).value)"
|
||
/>
|
||
</article>
|
||
<div v-if="hasPagination" class="pagination-bar bottom">
|
||
<button class="button small" :disabled="currentPage <= 1" @click="prevPage">‹</button>
|
||
<span class="pagination-info">{{ $t('editor.pageInfo', { current: currentPage, total: totalPages }) }}</span>
|
||
<button class="button small" :disabled="currentPage >= totalPages" @click="nextPage">›</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="log-drawer" :class="{ expanded: logsExpanded }">
|
||
<button class="log-toggle" type="button" @click="logsExpanded = !logsExpanded">
|
||
<span>{{ $t('editor.logs') }}</span>
|
||
<span class="subtle">{{ logs.length }}</span>
|
||
<span class="log-chevron">{{ logsExpanded ? '−' : '+' }}</span>
|
||
</button>
|
||
<div v-if="logsExpanded" class="log-panel">
|
||
<div v-if="logs.length === 0" class="empty-state">
|
||
{{ $t('editor.noLogs') }}
|
||
</div>
|
||
<div v-else ref="logContainerRef" class="log-list">
|
||
<pre v-for="(line, i) in logs" :key="i" class="log-line">{{ line }}</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</template>
|