feat: save modified subtitle
This commit is contained in:
@@ -37,9 +37,9 @@ defineShortcuts({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
'meta_s': {
|
'meta_s': {
|
||||||
handler: () => {
|
handler: async () => {
|
||||||
if (isDropdownOpen.value && isDownloadable.value) {
|
if (isDropdownOpen.value && isDownloadable.value) {
|
||||||
startDownload(props.course.subtitle_url, `眩生花微课_${ props.course.title }_${ props.course.task_id }.srt`)
|
await startDownload(await fetchCourseSubtitleUrl(props.course), `眩生花微课_${ props.course.title }_${ props.course.task_id }.srt`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -220,7 +220,7 @@ const copyTaskId = (extraMessage?: string) => {
|
|||||||
disabled: !isDownloadable,
|
disabled: !isDownloadable,
|
||||||
click: () => isPreviewModalOpen = true,
|
click: () => isPreviewModalOpen = true,
|
||||||
}, {
|
}, {
|
||||||
label: '查看字幕',
|
label: '编辑字幕',
|
||||||
icon: 'i-solar-subtitles-linear',
|
icon: 'i-solar-subtitles-linear',
|
||||||
shortcuts: [metaSymbol, 'D'],
|
shortcuts: [metaSymbol, 'D'],
|
||||||
disabled: !isDownloadable,
|
disabled: !isDownloadable,
|
||||||
@@ -233,8 +233,8 @@ const copyTaskId = (extraMessage?: string) => {
|
|||||||
icon: 'i-tabler-file-download',
|
icon: 'i-tabler-file-download',
|
||||||
shortcuts: [metaSymbol, 'S'],
|
shortcuts: [metaSymbol, 'S'],
|
||||||
disabled: !isDownloadable,
|
disabled: !isDownloadable,
|
||||||
click: () => {
|
click: async () => {
|
||||||
startDownload(course.subtitle_url, `眩生花微课_${ props.course.title }_${ props.course.task_id }.srt`)
|
await startDownload(await fetchCourseSubtitleUrl(course), `眩生花微课_${ props.course.title }_${ props.course.task_id }.srt`)
|
||||||
}
|
}
|
||||||
}], [{
|
}], [{
|
||||||
label: '删除记录',
|
label: '删除记录',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
|
import { encode } from '@monosky/base64'
|
||||||
|
|
||||||
interface Subtitle {
|
interface Subtitle {
|
||||||
start: string;
|
start: string;
|
||||||
@@ -17,18 +18,22 @@ const props = defineProps({
|
|||||||
|
|
||||||
const dayjs = useDayjs()
|
const dayjs = useDayjs()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const loginState = useLoginState()
|
||||||
|
|
||||||
const isDrawerActive = ref(false)
|
const isDrawerActive = ref(false)
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
|
const isSaving = ref(false)
|
||||||
const rawSrt = ref<string | null>(null)
|
const rawSrt = ref<string | null>(null)
|
||||||
const subtitles = ref<Subtitle[]>([])
|
const subtitles = ref<Subtitle[]>([])
|
||||||
|
const modified = ref(false)
|
||||||
|
|
||||||
const videoElement = ref<HTMLVideoElement | null>(null)
|
const videoElement = ref<HTMLVideoElement | null>(null)
|
||||||
|
|
||||||
const loadSrt = async () => {
|
const loadSrt = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await fetch(props.course.subtitle_url)
|
// const response = await fetch(props.course.subtitle_url)
|
||||||
|
const response = await fetch(await fetchCourseSubtitleUrl(props.course))
|
||||||
const text = await response.text()
|
const text = await response.text()
|
||||||
rawSrt.value = text
|
rawSrt.value = text
|
||||||
parseSrt(text)
|
parseSrt(text)
|
||||||
@@ -129,6 +134,30 @@ const onSubtitleInputClick = (subtitle: Subtitle) => {
|
|||||||
videoElement.value.pause()
|
videoElement.value.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveNewSubtitle = () => {
|
||||||
|
isSaving.value = true
|
||||||
|
const encodedSubtitle = encode(generateSrt())
|
||||||
|
useFetchWrapped<
|
||||||
|
req.gen.CourseSubtitleCreate & AuthedRequest,
|
||||||
|
BaseResponse<resp.gen.CourseSubtitleCreate>
|
||||||
|
>('App.Digital_VideoSubtitle.CreateFile', {
|
||||||
|
token: loginState.token!,
|
||||||
|
user_id: loginState.user.id,
|
||||||
|
sub_type: 1,
|
||||||
|
sub_content: encodedSubtitle,
|
||||||
|
task_id: props.course?.task_id,
|
||||||
|
}).then(_ => {
|
||||||
|
modified.value = false
|
||||||
|
toast.add({
|
||||||
|
color: 'green',
|
||||||
|
title: '字幕已保存',
|
||||||
|
description: '修改后的字幕文件已保存',
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
isSaving.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (rawSrt.value) {
|
if (rawSrt.value) {
|
||||||
parseSrt(rawSrt.value)
|
parseSrt(rawSrt.value)
|
||||||
@@ -240,6 +269,7 @@ defineExpose({
|
|||||||
:autofocus="false"
|
:autofocus="false"
|
||||||
:color="subtitle.active ? 'primary' : undefined"
|
:color="subtitle.active ? 'primary' : undefined"
|
||||||
@click="onSubtitleInputClick(subtitle)"
|
@click="onSubtitleInputClick(subtitle)"
|
||||||
|
@input="() => { if(!modified) modified = true }"
|
||||||
>
|
>
|
||||||
<template #trailing>
|
<template #trailing>
|
||||||
<Icon v-if="subtitle.active" name="tabler:keyframe-align-vertical-filled"/>
|
<Icon v-if="subtitle.active" name="tabler:keyframe-align-vertical-filled"/>
|
||||||
@@ -252,11 +282,12 @@ defineExpose({
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<!-- TODO: 24/07/02 Modified subtitles upload -->
|
<!-- TODO: 24/07/02 Modified subtitles upload -->
|
||||||
<UButton @click="() => {
|
<div class="flex justify-end items-center gap-2">
|
||||||
console.log(generateSrt())
|
<span v-if="modified" class="text-sm text-yellow-500 font-medium">已更改但未保存</span>
|
||||||
}">
|
<UButton :disabled="!modified" :loading="isSaving" @click="saveNewSubtitle">
|
||||||
Generate
|
保存{{ isSaving ? '中' : '' }}
|
||||||
</UButton>
|
</UButton>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UCard>
|
</UCard>
|
||||||
</USlideover>
|
</USlideover>
|
||||||
|
|||||||
24
composables/fetchCourseSubtitleUrl.ts
Normal file
24
composables/fetchCourseSubtitleUrl.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export const fetchCourseSubtitleUrl = async (course: resp.gen.CourseGenItem) => {
|
||||||
|
const loginState = useLoginState()
|
||||||
|
|
||||||
|
const subtitleRecord = await useFetchWrapped<
|
||||||
|
{
|
||||||
|
page?: number
|
||||||
|
perpage?: number
|
||||||
|
task_id: string
|
||||||
|
} & AuthedRequest,
|
||||||
|
BaseResponse<PagedData<resp.gen.CourseSubtitleCreate>>
|
||||||
|
>('App.Digital_VideoSubtitle.GetList', {
|
||||||
|
token: loginState.token!,
|
||||||
|
user_id: loginState.user.id,
|
||||||
|
task_id: course.task_id,
|
||||||
|
page: 1,
|
||||||
|
perpage: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (subtitleRecord.data.items.length !== 1) {
|
||||||
|
return course.subtitle_url
|
||||||
|
}
|
||||||
|
|
||||||
|
return subtitleRecord.data.items[0].url
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ export default defineNuxtConfig({
|
|||||||
icons: ['tabler', 'solar', 'line-md', 'svg-spinners'],
|
icons: ['tabler', 'solar', 'line-md', 'svg-spinners'],
|
||||||
},
|
},
|
||||||
colorMode: {
|
colorMode: {
|
||||||
preference: 'dark',
|
preference: 'light',
|
||||||
},
|
},
|
||||||
dayjs: {
|
dayjs: {
|
||||||
locales: ['zh', 'en'],
|
locales: ['zh', 'en'],
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@iconify-json/solar": "^1.1.9",
|
"@iconify-json/solar": "^1.1.9",
|
||||||
"@iconify-json/svg-spinners": "^1.1.2",
|
"@iconify-json/svg-spinners": "^1.1.2",
|
||||||
"@iconify-json/tabler": "^1.1.105",
|
"@iconify-json/tabler": "^1.1.105",
|
||||||
|
"@monosky/base64": "^0.0.3",
|
||||||
"@nuxt/image": "^1.7.0",
|
"@nuxt/image": "^1.7.0",
|
||||||
"@nuxt/ui": "^2.14.1",
|
"@nuxt/ui": "^2.14.1",
|
||||||
"@uniiem/object-trim": "^0.2.0",
|
"@uniiem/object-trim": "^0.2.0",
|
||||||
|
|||||||
21
typings/types.d.ts
vendored
21
typings/types.d.ts
vendored
@@ -85,6 +85,18 @@ namespace req {
|
|||||||
to_user_id: number
|
to_user_id: number
|
||||||
task_id: string
|
task_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sub_type 0为绿幕生成,1为PPT生成
|
||||||
|
* @param sub_content BASE64后的ass字幕字符串
|
||||||
|
* @param sub_ver optional 字幕版本
|
||||||
|
*/
|
||||||
|
interface CourseSubtitleCreate {
|
||||||
|
sub_type: 0 | 1
|
||||||
|
task_id: string
|
||||||
|
sub_content: string
|
||||||
|
sub_ver?: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AssistantTemplateList {
|
interface AssistantTemplateList {
|
||||||
@@ -152,6 +164,15 @@ namespace resp {
|
|||||||
interface CourseGenDelete {
|
interface CourseGenDelete {
|
||||||
code: 0 | 1
|
code: 0 | 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param video_sub_id 字幕记录 ID
|
||||||
|
* @param url 已上传的ass文件URL(文件存放于OSS)
|
||||||
|
*/
|
||||||
|
interface CourseSubtitleCreate {
|
||||||
|
video_sub_id: number
|
||||||
|
url: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
yarn.lock
36
yarn.lock
@@ -1541,6 +1541,11 @@
|
|||||||
semver "^7.3.5"
|
semver "^7.3.5"
|
||||||
tar "^6.1.11"
|
tar "^6.1.11"
|
||||||
|
|
||||||
|
"@monosky/base64@^0.0.3":
|
||||||
|
version "0.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@monosky/base64/-/base64-0.0.3.tgz#32f512911df482df9f5bce208c9eff3a13a94589"
|
||||||
|
integrity sha512-PNNOPniUyjY725FvPjfRZ++Bbufjy3sy2YH7HdvpmwexuZSSdJ1UDkZx03gnWAbhM3hIhBTWbF49/9xMSbNRgg==
|
||||||
|
|
||||||
"@netlify/functions@^2.8.0":
|
"@netlify/functions@^2.8.0":
|
||||||
version "2.8.0"
|
version "2.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@netlify/functions/-/functions-2.8.0.tgz#245f4be789891159d95391c171263ded8ee1ee3a"
|
resolved "https://registry.yarnpkg.com/@netlify/functions/-/functions-2.8.0.tgz#245f4be789891159d95391c171263ded8ee1ee3a"
|
||||||
@@ -7647,16 +7652,7 @@ streamx@^2.15.0, streamx@^2.18.0:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
bare-events "^2.2.0"
|
bare-events "^2.2.0"
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||||
version "4.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
|
||||||
dependencies:
|
|
||||||
emoji-regex "^8.0.0"
|
|
||||||
is-fullwidth-code-point "^3.0.0"
|
|
||||||
strip-ansi "^6.0.1"
|
|
||||||
|
|
||||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@@ -7743,14 +7739,7 @@ stringify-object@^3.3.0:
|
|||||||
is-obj "^1.0.1"
|
is-obj "^1.0.1"
|
||||||
is-regexp "^1.0.0"
|
is-regexp "^1.0.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
version "6.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
|
||||||
dependencies:
|
|
||||||
ansi-regex "^5.0.1"
|
|
||||||
|
|
||||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
@@ -8838,16 +8827,7 @@ workbox-window@7.1.0, workbox-window@^7.1.0:
|
|||||||
"@types/trusted-types" "^2.0.2"
|
"@types/trusted-types" "^2.0.2"
|
||||||
workbox-core "7.1.0"
|
workbox-core "7.1.0"
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
|
||||||
dependencies:
|
|
||||||
ansi-styles "^4.0.0"
|
|
||||||
string-width "^4.1.0"
|
|
||||||
strip-ansi "^6.0.0"
|
|
||||||
|
|
||||||
wrap-ansi@^7.0.0:
|
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
|||||||
Reference in New Issue
Block a user