From 5a904063a3ba2a5ac057177358b770f29a069f63 Mon Sep 17 00:00:00 2001 From: Arnei Date: Mon, 16 Jan 2023 11:49:41 +0100 Subject: [PATCH] Change subtitle identification from flavor to id With subtitles becoming first class citizens in Opencast, their flavor cannot be used as an identifier anymore. This is problematic for the editor, which relies heavily on identifying subtitles by subflavor. This PR switches from subflavor based identification to a track identifier based identification. The frontend will still try to match subtitle tracks from the backend to languages defined in the frontend settings. However, it now has to do so by "lang" tag, which is not guaranteed to exist. In order to avoid creating new subtitle tracks in the backend on every save, the frontend settings will therefore require that a defined language MUST have a "lang" tag. Furthermore, this PR also includes a behavioural change in which subtitles are displayed in the subtitle select view. Before, only subtitles that were specified in the frontend settings were displayed. With this PR, all subtitles will be displayed, regardless of whether they are specified in the frontens settings or not. This change was made with the rationale that subtitle tags are optional, and subtitle tracks should therefore be editable in the editor even if no "lang" tag is specified. --- editor-settings.toml | 17 +-- package-lock.json | 41 ++++++- package.json | 2 + public/editor-settings.toml | 14 +-- src/config.ts | 11 +- src/i18n/locales/en-US.json | 3 +- src/main/Save.tsx | 9 +- src/main/SubtitleEditor.tsx | 32 ++--- src/main/SubtitleListEditor.tsx | 30 ++--- src/main/SubtitleSelect.tsx | 188 ++++++++++++++++++++--------- src/main/SubtitleTimeline.tsx | 12 +- src/main/SubtitleVideoArea.tsx | 10 +- src/main/WorkflowConfiguration.tsx | 9 +- src/redux/subtitleSlice.ts | 60 ++++----- src/redux/videoSlice.ts | 16 +-- src/types.ts | 10 +- src/util/utilityFunctions.ts | 21 +++- 17 files changed, 314 insertions(+), 171 deletions(-) diff --git a/editor-settings.toml b/editor-settings.toml index 7f9bc0c8e..66d7b49d6 100644 --- a/editor-settings.toml +++ b/editor-settings.toml @@ -76,18 +76,21 @@ #mainFlavor = "captions" [subtitles.languages] -## A list of languages for which subtitles can be created -"captions/source+de" = "Deutsch" -"captions/source+en" = "English" -"captions/source+es" = "Spanish" +## A list of languages for which new subtitles can be created +# For each language, various tags can be specified +# A list of officially recommended tags can be found at: TODO: link to opencast documentation for subtitle tags +# At least the "lang" tag MUST be specified +german = { lang = "de-DE" } +english = { lang = "en-US", type = "closed-caption" } +spanish = { lang = "es" } [subtitles.icons] # A list of icons to be displayed for languages defined above. # Values are strings but should preferably be Unicode icons. # These are optional and you can also choose to have no icons. -"captions/source+de" = "πŸ‡©πŸ‡ͺ" -"captions/source+en" = "πŸ‡ΊπŸ‡Έ" -"captions/source+es" = "πŸ‡ͺπŸ‡Έ" +"de-DE" = "πŸ‡©πŸ‡ͺ" +"en-US" = "πŸ‡ΊπŸ‡Έ" +"es" = "πŸ‡ͺπŸ‡Έ" [subtitles.defaultVideoFlavor] # Specify the default video in the subtitle video player by flavor diff --git a/package-lock.json b/package-lock.json index f7971f2ee..3c49b56a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,6 +67,7 @@ "stream": "0.0.2", "subtitle": "^4.1.2", "typescript": "^4.9.4", + "uuid": "^9.0.0", "webvtt-parser": "^2.2.0" }, "devDependencies": { @@ -79,6 +80,7 @@ "@types/react-resizable": "^3.0.3", "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", + "@types/uuid": "^9.0.0", "use-resize-observer": "^9.0.2" } }, @@ -5657,6 +5659,12 @@ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, + "node_modules/@types/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -18603,6 +18611,14 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -19826,9 +19842,9 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", "bin": { "uuid": "dist/bin/uuid" } @@ -24687,6 +24703,12 @@ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, + "@types/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==", + "dev": true + }, "@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -33966,6 +33988,13 @@ "faye-websocket": "^0.11.3", "uuid": "^8.3.2", "websocket-driver": "^0.7.4" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } } }, "source-list-map": { @@ -34893,9 +34922,9 @@ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" }, "v8-to-istanbul": { "version": "8.1.1", diff --git a/package.json b/package.json index 4019c0977..351eaff4c 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "stream": "0.0.2", "subtitle": "^4.1.2", "typescript": "^4.9.4", + "uuid": "^9.0.0", "webvtt-parser": "^2.2.0" }, "scripts": { @@ -100,6 +101,7 @@ "@types/react-resizable": "^3.0.3", "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", + "@types/uuid": "^9.0.0", "use-resize-observer": "^9.0.2" } } diff --git a/public/editor-settings.toml b/public/editor-settings.toml index 1aa2c0ae0..f46db7697 100644 --- a/public/editor-settings.toml +++ b/public/editor-settings.toml @@ -29,15 +29,15 @@ show = true mainFlavor = "captions" [subtitles.languages] -"captions/source+de" = "Deutsch" -"captions/source+en" = "English" -"captions/source+es" = "Spanish" -"captions/source" = "Generic" +german = { lang = "de-DE" } +english = { lang = "en-US", type = "closed-caption" } +spanish = { lang = "es" } + [subtitles.icons] -"captions/source+de" = "πŸ‡©πŸ‡ͺ" -"captions/source+en" = "πŸ‡ΊπŸ‡Έ" -"captions/source+es" = "πŸ‡ͺπŸ‡Έ" +"de-DE" = "πŸ‡©πŸ‡ͺ" +"en-US" = "πŸ‡ΊπŸ‡Έ" +"es" = "πŸ‡ͺπŸ‡Έ" [thumbnail] show = true diff --git a/src/config.ts b/src/config.ts index 0b83bef2e..aff061fc4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -29,6 +29,13 @@ export interface configureFieldsAttributes { readonly: boolean, } +export interface subtitleTags { + lang: string, + 'auto-generated': string, + 'auto-generator': string, + type: string, +} + /** * Settings interface */ @@ -53,7 +60,7 @@ interface iSettings { subtitles: { show: boolean, mainFlavor: string, - languages: { [key: string]: string } | undefined, + languages: { [key: string]: subtitleTags } | undefined, icons: { [key: string]: string } | undefined, defaultVideoFlavor: Flavor | undefined, } @@ -361,7 +368,7 @@ const SCHEMA = { subtitles: { show: types.boolean, mainFlavor: types.string, - languages: types.map, + languages: types.objectsWithinObjects, icons: types.map, defaultVideoFlavor: types.map, }, diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index d52486c80..7d84ecbcb 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -239,7 +239,8 @@ "backButton-tooltip": "Return to subtitle selection", "editTitle": "Subtitle Editor - {{title}}", "editTitle-loading": "Loading", - "generic": "Generic" + "generic": "Generic", + "autoGenerated": "Auto-generated" }, "subtitleList": { diff --git a/src/main/Save.tsx b/src/main/Save.tsx index 779575b51..06b92cdec 100644 --- a/src/main/Save.tsx +++ b/src/main/Save.tsx @@ -23,7 +23,6 @@ import { postMetadata, selectPostError, selectPostStatus, setHasChanges as metad selectHasChanges as metadataSelectHasChanges } from "../redux/metadataSlice"; import { selectSubtitles } from "../redux/subtitleSlice"; import { serializeSubtitle } from "../util/utilityFunctions"; -import { Flavor } from "../types"; import { selectTheme } from "../redux/themeSlice"; import { ThemedTooltip } from "./Tooltip"; @@ -141,9 +140,11 @@ export const SaveButton: React.FC<{}> = () => { const subtitlesForPosting = [] for (const identifier in subtitles) { - let flavor: Flavor = {type: identifier.split("/")[0], subtype: identifier.split("/")[1]} - subtitlesForPosting.push({flavor: flavor, subtitle: serializeSubtitle(subtitles[identifier])}) - + subtitlesForPosting.push({ + id: identifier, + subtitle: serializeSubtitle(subtitles[identifier].cues), + tags: subtitles[identifier].tags + }) } return subtitlesForPosting } diff --git a/src/main/SubtitleEditor.tsx b/src/main/SubtitleEditor.tsx index 436042709..a6a606ca5 100644 --- a/src/main/SubtitleEditor.tsx +++ b/src/main/SubtitleEditor.tsx @@ -4,24 +4,23 @@ import { basicButtonStyle, flexGapReplacementStyle } from "../cssStyles"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { - selectCaptionTrackByFlavor, + selectSubtitlesFromOpencastById, } from '../redux/videoSlice' import { useDispatch, useSelector } from "react-redux"; -import { SubtitleCue } from "../types"; import SubtitleListEditor from "./SubtitleListEditor"; import { setIsDisplayEditView, - selectSelectedSubtitleByFlavor, - selectSelectedSubtitleFlavor, + selectSelectedSubtitleById, + selectSelectedSubtitleId, setSubtitle } from '../redux/subtitleSlice' -import { settings } from "../config"; import SubtitleVideoArea from "./SubtitleVideoArea"; import SubtitleTimeline from "./SubtitleTimeline"; import { useTranslation } from "react-i18next"; import { selectTheme } from "../redux/themeSlice"; import { parseSubtitle } from "../util/utilityFunctions"; import { ThemedTooltip } from "./Tooltip"; +import { generateButtonTitle } from "./SubtitleSelect"; /** * Displays an editor view for a selected subtitle file @@ -32,17 +31,17 @@ import { ThemedTooltip } from "./Tooltip"; const dispatch = useDispatch() const [getError, setGetError] = useState(undefined) - const subtitle : SubtitleCue[] = useSelector(selectSelectedSubtitleByFlavor) - const selectedFlavor = useSelector(selectSelectedSubtitleFlavor) - const captionTrack = useSelector(selectCaptionTrackByFlavor(selectedFlavor)) + const subtitle = useSelector(selectSelectedSubtitleById) + const selectedId = useSelector(selectSelectedSubtitleId) + const captionTrack = useSelector(selectSubtitlesFromOpencastById(selectedId)) const theme = useSelector(selectTheme) // Prepare subtitle in redux useEffect(() => { // Parse subtitle data from Opencast - if (subtitle === undefined && captionTrack !== undefined && captionTrack.subtitle !== undefined && selectedFlavor) { + if (subtitle?.cues === undefined && captionTrack !== undefined && captionTrack.subtitle !== undefined && selectedId) { try { - dispatch(setSubtitle({identifier: selectedFlavor, subtitles: parseSubtitle(captionTrack.subtitle)})) + dispatch(setSubtitle({identifier: selectedId, subtitles: { cues: parseSubtitle(captionTrack.subtitle), tags: captionTrack.tags } })) } catch (error) { if (error instanceof Error) { setGetError(error.message) @@ -52,15 +51,18 @@ import { ThemedTooltip } from "./Tooltip"; } // Or create a new subtitle instead - } else if (subtitle === undefined && captionTrack === undefined && selectedFlavor) { + } else if (subtitle?.cues === undefined && captionTrack === undefined && selectedId) { // Create an empty subtitle - dispatch(setSubtitle({identifier: selectedFlavor, subtitles: []})) + dispatch(setSubtitle({identifier: selectedId, subtitles: { cues: [], tags: [] }})) } - }, [dispatch, captionTrack, subtitle, selectedFlavor]) + }, [dispatch, captionTrack, subtitle, selectedId]) const getTitle = () => { - return (settings.subtitles.languages !== undefined && subtitle && selectedFlavor) ? - settings.subtitles.languages[selectedFlavor] : t("subtitles.editTitle-loading") + if (subtitle) { + return generateButtonTitle(subtitle.tags, t) + } else { + return t("subtitles.editTitle-loading") + } } const subtitleEditorStyle = css({ diff --git a/src/main/SubtitleListEditor.tsx b/src/main/SubtitleListEditor.tsx index 1e4c8442f..c08b0583b 100644 --- a/src/main/SubtitleListEditor.tsx +++ b/src/main/SubtitleListEditor.tsx @@ -14,8 +14,8 @@ import { addCueAtIndex, selectFocusSegmentId, selectFocusSegmentTriggered, selectFocusSegmentTriggered2, - selectSelectedSubtitleByFlavor, - selectSelectedSubtitleFlavor, + selectSelectedSubtitleById, + selectSelectedSubtitleId, setCueAtIndex, setCurrentlyAt, setFocusSegmentTriggered, @@ -39,8 +39,8 @@ const SubtitleListEditor : React.FC<{}> = () => { const dispatch = useDispatch() - const subtitle = useSelector(selectSelectedSubtitleByFlavor) - const subtitleFlavor = useSelector(selectSelectedSubtitleFlavor, shallowEqual) + const subtitle = useSelector(selectSelectedSubtitleById) + const subtitleId = useSelector(selectSelectedSubtitleId, shallowEqual) const focusTriggered = useSelector(selectFocusSegmentTriggered, shallowEqual) const focusId = useSelector(selectFocusSegmentId, shallowEqual) const defaultSegmentLength = 5000 @@ -51,16 +51,16 @@ const SubtitleListEditor : React.FC<{}> = () => { // Update ref array size useEffect(() => { - if (subtitle) { - itemsRef.current = itemsRef.current.slice(0, subtitle.length); + if (subtitle?.cues) { + itemsRef.current = itemsRef.current.slice(0, subtitle.cues.length); } - }, [subtitle]); + }, [subtitle?.cues]); // Scroll to segment when triggered by reduxState useEffect(() => { if (focusTriggered) { - if (itemsRef && itemsRef.current && subtitle) { - const itemIndex = subtitle.findIndex(item => item.id === focusId) + if (itemsRef && itemsRef.current && subtitle?.cues) { + const itemIndex = subtitle?.cues.findIndex(item => item.id === focusId) if (listRef && listRef.current) { listRef.current.scrollToItem(itemIndex, "center"); @@ -68,20 +68,20 @@ const SubtitleListEditor : React.FC<{}> = () => { } dispatch(setFocusSegmentTriggered(false)) } - }, [dispatch, focusId, focusTriggered, itemsRef, subtitle]) + }, [dispatch, focusId, focusTriggered, itemsRef, subtitle?.cues]) // Automatically create a segment if there are no segments useEffect(() => { - if (subtitle && subtitle.length === 0) { + if (subtitle?.cues && subtitle?.cues.length === 0) { dispatch(addCueAtIndex({ - identifier: subtitleFlavor, + identifier: subtitleId, cueIndex: 0, text: "", startTime: 0, endTime: defaultSegmentLength })) } - }, [dispatch, subtitle, subtitleFlavor]) + }, [dispatch, subtitle?.cues, subtitleId]) const listStyle = css({ display: 'flex', @@ -110,7 +110,7 @@ const SubtitleListEditor : React.FC<{}> = () => { return segmentHeight }, []) - const itemData = createItemData(subtitle, subtitleFlavor, defaultSegmentLength) + const itemData = createItemData(subtitle?.cues, subtitleId, defaultSegmentLength) return (
@@ -118,7 +118,7 @@ const SubtitleListEditor : React.FC<{}> = () => { {({ height, width }) => ( segmentHeight} itemKey={(index, data) => data.items[index].id} diff --git a/src/main/SubtitleSelect.tsx b/src/main/SubtitleSelect.tsx index c11158c78..e19af64cd 100644 --- a/src/main/SubtitleSelect.tsx +++ b/src/main/SubtitleSelect.tsx @@ -1,8 +1,8 @@ import React, { useEffect } from "react"; import { css } from "@emotion/react"; import { basicButtonStyle, flexGapReplacementStyle, tileButtonStyle, disableButtonAnimation, subtitleSelectStyle } from "../cssStyles"; -import { settings } from '../config' -import { selectSubtitles, setSelectedSubtitleFlavor, setSubtitle } from "../redux/subtitleSlice"; +import { settings, subtitleTags } from '../config' +import { selectSubtitles, setSelectedSubtitleId, setSubtitle } from "../redux/subtitleSlice"; import { useDispatch, useSelector } from "react-redux"; import { setIsDisplayEditView } from "../redux/subtitleSlice"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -11,50 +11,86 @@ import { Form } from "react-final-form"; import { Select } from "mui-rff"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { selectCaptions } from "../redux/videoSlice"; +import { selectSubtitlesFromOpencast } from "../redux/videoSlice"; import { selectTheme } from "../redux/themeSlice"; import { ThemeProvider } from '@mui/material/styles'; import { ThemedTooltip } from "./Tooltip"; +import { languageCodeToName } from "../util/utilityFunctions"; +import { v4 as uuidv4 } from 'uuid'; /** - * Displays buttons that allow the user to select the flavor/language they want to edit + * Displays buttons that allow the user to select the subtitle they want to edit */ const SubtitleSelect : React.FC<{}> = () => { const { t } = useTranslation(); - const captionTracks = useSelector(selectCaptions) // track objects received from Opencast - const subtitles = useSelector(selectSubtitles) // parsed subtitles stored in redux + const subtitlesFromOpencast = useSelector(selectSubtitlesFromOpencast) // track objects received from Opencast + const subtitles = useSelector(selectSubtitles) // parsed subtitles stored in redux - const [displayFlavors, setDisplayFlavors] = useState<{subFlavor: string, title: string}[]>([]) - const [canBeAddedFlavors, setCanBeAddedFlavors] = useState<{subFlavor: string, title: string}[]>([]) + const [displaySubtitles, setDisplaySubtitles] = useState<{id: string, tags: string[]}[]>([]) + const [canBeAddedSubtitles, setCanBeAddedSubtitles] = useState<{id: string, tags: string[]}[]>([]) - // Update the displayFlavors and canBeAddedFlavors + // Update the collections for the select and add buttons useEffect(() => { let languages = { ...settings.subtitles.languages }; - // Get flavors of already created tracks or existing subtitle tracks - let subtitleFlavors = captionTracks - .map(track => track.flavor.type + '/' + track.flavor.subtype) - .filter(flavor => !subtitles[flavor]) - .concat(Object.keys(subtitles)); - let tempDisplayFlavors = [] - for (const flavor of subtitleFlavors) { - const lang = flavor.replace(/^[^+]*/, '') || t('subtitles.generic'); - tempDisplayFlavors.push({ - subFlavor: flavor, - title: languages[flavor] || lang}); - delete languages[flavor]; - } - tempDisplayFlavors.sort((f1, f2) => f1.title.localeCompare(f2.title)); + // Get ids of already created tracks or exisiting subtitle tracks + let existingSubtitles = subtitlesFromOpencast + .filter(track => !subtitles[track.id]) + .map(track => { + return { id: track.id, tags: track.tags } + }); + + existingSubtitles = Object.entries(subtitles) + .map(track => { + return { id: track[0], tags: track[1].tags } + }) + .concat(existingSubtitles); + + // Looks for languages in existing subtitles + // so that those languages don't show in the addSubtitles dropdown + let subtitlesFromOpencastLangs = subtitlesFromOpencast + .reduce((result: {id: string, lang: string}[], track) => { + let lang = track.tags.find(e => e.startsWith('lang:')) + if (lang) { + result.push({id: track.id, lang: lang.split(':')[1].trim()}) + } + return result; + }, []); - // List of unused languages - let tempCanBeAddedFlavors = Object.keys(languages) - .map(flavor => ({subFlavor: flavor, title: languages[flavor]})) - .sort((lang1, lang2) => lang1.title.localeCompare(lang2.title)); + let subtitlesLangs = Object.entries(subtitles) + .reduce((result: {id: string, lang: string}[], track) => { + let lang = track[1].tags.find(e => e.startsWith('lang:')) + if (lang) { + result.push({id: track[0], lang: lang.split(':')[1].trim()}) + } + return result; + }, []); - setDisplayFlavors(tempDisplayFlavors) - setCanBeAddedFlavors(tempCanBeAddedFlavors) - }, [captionTracks, subtitles, t]) + let existingLangs = subtitlesFromOpencastLangs.concat(subtitlesLangs); + + // Create list of subtitles that can be added + let canBeAddedSubtitles = Object.entries(languages) + .reduce((result: string[][], language) => { + if (!existingLangs.find(e => e.lang === language[1]["lang"])) { + result.push(convertTags(language[1])) + } else { + delete languages[language[0]] + } + return result; + }, []) + .map(tags => { return {id: uuidv4(), tags: tags} }) + + setDisplaySubtitles(existingSubtitles) + setCanBeAddedSubtitles(canBeAddedSubtitles) + }, [subtitlesFromOpencast, subtitles, t]) + + // Converts tags from the config file format to opencast format + const convertTags = (tags: subtitleTags) => { + return Object.entries(tags) + .map(tag => `${tag[0]}: ${tag[1]}`) + .concat() + } const subtitleSelectStyle = css({ display: 'grid', @@ -68,40 +104,43 @@ const SubtitleSelect : React.FC<{}> = () => { return buttons } - for (let subFlavor of displayFlavors) { - const icon = ((settings.subtitles || {}).icons || {})[subFlavor.subFlavor]; + for (let subtitle of displaySubtitles) { + let lang = subtitle.tags.find(e => e.startsWith('lang:')) + lang = lang ? lang.split(':')[1].trim(): undefined + const icon = lang ? ((settings.subtitles || {}).icons || {})[lang] : undefined + buttons.push( ) } - return buttons + return buttons.sort((dat1, dat2) => dat1.props["title"].localeCompare(dat2.props["title"])) } return (
{renderButtons()} {/* TODO: Only show the add button when there are still languages to add*/} - +
); } /** - * A button that sets the flavor that should be edited + * A button that sets the subtitle that should be edited */ const SubtitleSelectButton: React.FC<{ + id: string, title: string, icon: string | undefined, - flavor: string, }> = ({ + id, title, icon, - flavor }) => { const { t } = useTranslation(); const theme = useSelector(selectTheme) @@ -115,7 +154,6 @@ const SubtitleSelectButton: React.FC<{ const titleStyle = css({ overflow: 'hidden', textOverflow: 'ellipsis', - whiteSpace: 'nowrap', minWidth: 0, }) @@ -126,23 +164,27 @@ const SubtitleSelectButton: React.FC<{ aria-label={t("subtitles.selectSubtitleButton-tooltip-aria", {title: title})} onClick={ () => { dispatch(setIsDisplayEditView(true)) - dispatch(setSelectedSubtitleFlavor(flavor)) + dispatch(setSelectedSubtitleId(id)) }} onKeyDown={(event: React.KeyboardEvent) => { if (event.key === " " || event.key === "Enter") { dispatch(setIsDisplayEditView(true)) - dispatch(setSelectedSubtitleFlavor(flavor)) + dispatch(setSelectedSubtitleId(id)) }}}> {icon &&
{icon}
} -
{title}
+
{title ?? t('subtitles.generic') + " " + id}
); }; /** - * Actually not a button, but a container for a form that allows creating new flavors for editing + * Actually not a button, but a container for a form that allows creating new subtitles for editing */ -const SubtitleAddButton: React.FC<{languages: {subFlavor: string, title: string}[]}> = ({languages}) => { +const SubtitleAddButton: React.FC<{ + subtitlesForDropdown: {id: string, tags: string[]}[] +}> = ({ + subtitlesForDropdown +}) => { const { t } = useTranslation(); const theme = useSelector(selectTheme) @@ -154,22 +196,27 @@ const SubtitleAddButton: React.FC<{languages: {subFlavor: string, title: string} // Parse language data into a format the dropdown understands const selectData = () => { const data = [] - for (const lan of languages) { - data.push({label: lan.title, value: lan.subFlavor}) + for (const subtitle of subtitlesForDropdown) { + let lang = generateButtonTitle(subtitle.tags, t) + data.push({label: lang ?? t('subtitles.generic') + " " + subtitle.id, value: subtitle.id}) } + data.sort((dat1, dat2) => dat1.label.localeCompare(dat2.label)) return data } - const onSubmit = (values: { languages: any; }) => { - // Create new subtitle for the given flavor - dispatch(setSubtitle({identifier: values.languages, subtitles: []})) + const onSubmit = (values: { selectedSubtitle: any; }) => { + // Create new subtitle for the given language + const id = values.selectedSubtitle + const relatedSubtitle = subtitlesForDropdown.find(tag => tag.id === id) + const tags = relatedSubtitle ? relatedSubtitle.tags : [] + dispatch(setSubtitle({identifier: id, subtitles: { cues: [], tags: tags }})) // Reset setIsPlusDisplay(true) // Move to editor view dispatch(setIsDisplayEditView(true)) - dispatch(setSelectedSubtitleFlavor(values.languages)) + dispatch(setSelectedSubtitleId(id)) } const plusIconStyle = css({ @@ -221,17 +268,17 @@ const SubtitleAddButton: React.FC<{languages: {subFlavor: string, title: string} - - {/* "By default disabled elements like