diff --git a/editor-settings.toml b/editor-settings.toml index 5cf12af19..441a2aa84 100644 --- a/editor-settings.toml +++ b/editor-settings.toml @@ -99,21 +99,18 @@ #mainFlavor = "captions" [subtitles.languages] -## 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" } +## A list of languages for which subtitles can be created +"captions/source+de" = "Deutsch" +"captions/source+en" = "English" +"captions/source+es" = "Spanish" [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. -"de-DE" = "DE" -"en-US" = "EN" -"es" = "ES" +"captions/source+de" = "πŸ‡©πŸ‡ͺ" +"captions/source+en" = "πŸ‡ΊπŸ‡Έ" +"captions/source+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 33025b748..4916c3de0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,6 @@ "redux": "^4.2.1", "standardized-audio-context": "^25.3.57", "typescript": "^5.1.6", - "uuid": "^9.0.1", "webvtt-parser": "^2.2.0" }, "devDependencies": { @@ -66,7 +65,6 @@ "@types/react-resizable": "^3.0.5", "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", - "@types/uuid": "^9.0.4", "use-resize-observer": "^9.1.0" } }, @@ -8717,19 +8715,6 @@ "version": "1.0.0", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "license": "MIT" diff --git a/package.json b/package.json index 08549b459..e40b867b0 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "redux": "^4.2.1", "standardized-audio-context": "^25.3.57", "typescript": "^5.1.6", - "uuid": "^9.0.1", "webvtt-parser": "^2.2.0" }, "overrides": { @@ -86,7 +85,6 @@ "@types/react-resizable": "^3.0.5", "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", - "@types/uuid": "^9.0.4", "use-resize-observer": "^9.1.0" } } diff --git a/public/editor-settings.toml b/public/editor-settings.toml index 94ad8b6b9..a5e419c38 100644 --- a/public/editor-settings.toml +++ b/public/editor-settings.toml @@ -34,15 +34,15 @@ show = true mainFlavor = "captions" [subtitles.languages] -german = { lang = "de-DE" } -english = { lang = "en-US", type = "closed-caption" } -spanish = { lang = "es" } - +"captions/source+de" = "Deutsch" +"captions/source+en" = "English" +"captions/source+es" = "Spanish" +"captions/source" = "Generic" [subtitles.icons] -"de-DE" = "DE" -"en-US" = "EN" -"es" = "ES" +"captions/source+de" = "πŸ‡©πŸ‡ͺ" +"captions/source+en" = "πŸ‡ΊπŸ‡Έ" +"captions/source+es" = "πŸ‡ͺπŸ‡Έ" [thumbnail] show = true diff --git a/src/config.ts b/src/config.ts index cb977caab..993795227 100644 --- a/src/config.ts +++ b/src/config.ts @@ -28,13 +28,6 @@ export interface configureFieldsAttributes { readonly: boolean, } -export interface subtitleTags { - lang: string, - 'auto-generated': string, - 'auto-generator': string, - type: string, -} - /** * Settings interface */ @@ -63,7 +56,7 @@ interface iSettings { subtitles: { show: boolean, mainFlavor: string, - languages: { [key: string]: subtitleTags } | undefined, + languages: { [key: string]: string } | undefined, icons: { [key: string]: string } | undefined, defaultVideoFlavor: Flavor | undefined, } @@ -387,7 +380,7 @@ const SCHEMA = { subtitles: { show: types.boolean, mainFlavor: types.string, - languages: types.objectsWithinObjects, + languages: types.map, icons: types.map, defaultVideoFlavor: types.map, }, diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 3e97194fa..2f0b8e050 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -251,8 +251,7 @@ "backButton-tooltip": "Return to subtitle selection", "editTitle": "Subtitle Editor - {{title}}", "editTitle-loading": "Loading", - "generic": "Generic", - "autoGenerated": "Auto-generated" + "generic": "Generic" }, "subtitleList": { diff --git a/src/main/Save.tsx b/src/main/Save.tsx index 87346b29a..a4fee032b 100644 --- a/src/main/Save.tsx +++ b/src/main/Save.tsx @@ -22,6 +22,7 @@ import { selectSubtitles, selectHasChanges as selectSubtitleHasChanges, import { serializeSubtitle } from "../util/utilityFunctions"; import { useTheme } from "../themes"; import { ThemedTooltip } from "./Tooltip"; +import { Flavor } from "../types"; /** * Shown if the user wishes to save. @@ -139,11 +140,9 @@ export const SaveButton: React.FC = () => { const subtitlesForPosting = [] for (const identifier in subtitles) { - subtitlesForPosting.push({ - id: identifier, - subtitle: serializeSubtitle(subtitles[identifier].cues), - tags: subtitles[identifier].tags - }) + const flavor: Flavor = {type: identifier.split("/")[0], subtype: identifier.split("/")[1]} + subtitlesForPosting.push({flavor: flavor, subtitle: serializeSubtitle(subtitles[identifier])}) + } return subtitlesForPosting } diff --git a/src/main/SubtitleEditor.tsx b/src/main/SubtitleEditor.tsx index c064e313b..70d7d2354 100644 --- a/src/main/SubtitleEditor.tsx +++ b/src/main/SubtitleEditor.tsx @@ -3,16 +3,18 @@ import { css } from "@emotion/react"; import { basicButtonStyle, flexGapReplacementStyle } from "../cssStyles"; import { LuChevronLeft} from "react-icons/lu"; import { - selectSubtitlesFromOpencastById, + selectCaptionTrackByFlavor, } from '../redux/videoSlice' import { useDispatch, useSelector } from "react-redux"; +import { SubtitleCue } from "../types"; import SubtitleListEditor from "./SubtitleListEditor"; import { setIsDisplayEditView, - selectSelectedSubtitleById, - selectSelectedSubtitleId, + selectSelectedSubtitleByFlavor, + selectSelectedSubtitleFlavor, setSubtitle } from '../redux/subtitleSlice' +import { settings } from "../config"; import SubtitleVideoArea from "./SubtitleVideoArea"; import SubtitleTimeline from "./SubtitleTimeline"; import { useTranslation } from "react-i18next"; @@ -20,7 +22,6 @@ import { useTheme } from "../themes"; import { parseSubtitle } from "../util/utilityFunctions"; import { ThemedTooltip } from "./Tooltip"; import { titleStyle, titleStyleBold } from "../cssStyles"; -import { generateButtonTitle } from "./SubtitleSelect"; /** * Displays an editor view for a selected subtitle file @@ -31,17 +32,17 @@ const SubtitleEditor : React.FC = () => { const dispatch = useDispatch() const [getError, setGetError] = useState(undefined) - const subtitle = useSelector(selectSelectedSubtitleById) - const selectedId = useSelector(selectSelectedSubtitleId) - const captionTrack = useSelector(selectSubtitlesFromOpencastById(selectedId)) + const subtitle : SubtitleCue[] = useSelector(selectSelectedSubtitleByFlavor) + const selectedFlavor = useSelector(selectSelectedSubtitleFlavor) + const captionTrack = useSelector(selectCaptionTrackByFlavor(selectedFlavor)) const theme = useTheme() // Prepare subtitle in redux useEffect(() => { // Parse subtitle data from Opencast - if (subtitle?.cues === undefined && captionTrack !== undefined && captionTrack.subtitle !== undefined && selectedId) { + if (subtitle === undefined && captionTrack !== undefined && captionTrack.subtitle !== undefined && selectedFlavor) { try { - dispatch(setSubtitle({identifier: selectedId, subtitles: { cues: parseSubtitle(captionTrack.subtitle), tags: captionTrack.tags } })) + dispatch(setSubtitle({identifier: selectedFlavor, subtitles: parseSubtitle(captionTrack.subtitle)})) } catch (error) { if (error instanceof Error) { setGetError(error.message) @@ -51,18 +52,15 @@ const SubtitleEditor : React.FC = () => { } // Or create a new subtitle instead - } else if (subtitle?.cues === undefined && captionTrack === undefined && selectedId) { + } else if (subtitle === undefined && captionTrack === undefined && selectedFlavor) { // Create an empty subtitle - dispatch(setSubtitle({identifier: selectedId, subtitles: { cues: [], tags: [] }})) + dispatch(setSubtitle({identifier: selectedFlavor, subtitles: []})) } - }, [dispatch, captionTrack, subtitle, selectedId]) + }, [dispatch, captionTrack, subtitle, selectedFlavor]) const getTitle = () => { - if (subtitle) { - return generateButtonTitle(subtitle.tags, t) - } else { - return t("subtitles.editTitle-loading") - } + return (settings.subtitles.languages !== undefined && subtitle && selectedFlavor) ? + settings.subtitles.languages[selectedFlavor] : t("subtitles.editTitle-loading") } const subtitleEditorStyle = css({ diff --git a/src/main/SubtitleListEditor.tsx b/src/main/SubtitleListEditor.tsx index 3f91ffabc..49fd60f7d 100644 --- a/src/main/SubtitleListEditor.tsx +++ b/src/main/SubtitleListEditor.tsx @@ -13,8 +13,8 @@ import { addCueAtIndex, selectFocusSegmentId, selectFocusSegmentTriggered, selectFocusSegmentTriggered2, - selectSelectedSubtitleById, - selectSelectedSubtitleId, + selectSelectedSubtitleByFlavor, + selectSelectedSubtitleFlavor, setCueAtIndex, setCurrentlyAt, setFocusSegmentTriggered, @@ -39,8 +39,8 @@ const SubtitleListEditor : React.FC = () => { const dispatch = useDispatch() - const subtitle = useSelector(selectSelectedSubtitleById) - const subtitleId = useSelector(selectSelectedSubtitleId, shallowEqual) + const subtitle = useSelector(selectSelectedSubtitleByFlavor) + const subtitleFlavor = useSelector(selectSelectedSubtitleFlavor, 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?.cues) { - itemsRef.current = itemsRef.current.slice(0, subtitle.cues.length); + if (subtitle) { + itemsRef.current = itemsRef.current.slice(0, subtitle.length); } - }, [subtitle?.cues]); + }, [subtitle]); // Scroll to segment when triggered by reduxState useEffect(() => { if (focusTriggered) { - if (itemsRef && itemsRef.current && subtitle?.cues) { - const itemIndex = subtitle?.cues.findIndex(item => item.idInternal === focusId) + if (itemsRef && itemsRef.current && subtitle) { + const itemIndex = subtitle.findIndex(item => item.idInternal === 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?.cues]) + }, [dispatch, focusId, focusTriggered, itemsRef, subtitle]) // Automatically create a segment if there are no segments useEffect(() => { - if (subtitle?.cues && subtitle?.cues.length === 0) { + if (subtitle && subtitle.length === 0) { dispatch(addCueAtIndex({ - identifier: subtitleId, + identifier: subtitleFlavor, cueIndex: 0, text: "", startTime: 0, endTime: defaultSegmentLength })) } - }, [dispatch, subtitle?.cues, subtitleId]) + }, [dispatch, subtitle, subtitleFlavor]) const listStyle = css({ display: 'flex', @@ -110,7 +110,7 @@ const SubtitleListEditor : React.FC = () => { return segmentHeight }, []) - const itemData = createItemData(subtitle?.cues, subtitleId, defaultSegmentLength) + const itemData = createItemData(subtitle, subtitleFlavor, defaultSegmentLength) return (
@@ -118,7 +118,7 @@ const SubtitleListEditor : React.FC = () => { {({ height, width }: {height: string | number, width: string | number}) => ( segmentHeight} itemKey={(index, data) => data.items[index].idInternal} diff --git a/src/main/SubtitleSelect.tsx b/src/main/SubtitleSelect.tsx index f11fafecf..661a8b934 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, subtitleTags } from '../config' -import { selectSubtitles, setSelectedSubtitleId, setSubtitle } from "../redux/subtitleSlice"; +import { settings } from '../config' +import { selectSubtitles, setSelectedSubtitleFlavor, setSubtitle } from "../redux/subtitleSlice"; import { useDispatch, useSelector } from "react-redux"; import { setIsDisplayEditView } from "../redux/subtitleSlice"; import { LuPlus} from "react-icons/lu"; @@ -10,86 +10,50 @@ import { Form } from "react-final-form"; import { Select } from "mui-rff"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { selectSubtitlesFromOpencast } from "../redux/videoSlice"; +import { selectCaptions } from "../redux/videoSlice"; import { useTheme } from "../themes"; 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 subtitle they want to edit + * Displays buttons that allow the user to select the flavor/language they want to edit */ const SubtitleSelect : React.FC = () => { const { t } = useTranslation(); - const subtitlesFromOpencast = useSelector(selectSubtitlesFromOpencast) // track objects received from Opencast - const subtitles = useSelector(selectSubtitles) // parsed subtitles stored in redux + const captionTracks = useSelector(selectCaptions) // track objects received from Opencast + const subtitles = useSelector(selectSubtitles) // parsed subtitles stored in redux - const [displaySubtitles, setDisplaySubtitles] = useState<{id: string, tags: string[]}[]>([]) - const [canBeAddedSubtitles, setCanBeAddedSubtitles] = useState<{id: string, tags: string[]}[]>([]) + const [displayFlavors, setDisplayFlavors] = useState<{subFlavor: string, title: string}[]>([]) + const [canBeAddedFlavors, setCanBeAddedFlavors] = useState<{subFlavor: string, title: string}[]>([]) - // Update the collections for the select and add buttons + // Update the displayFlavors and canBeAddedFlavors useEffect(() => { const languages = { ...settings.subtitles.languages }; - // 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 - const subtitlesFromOpencastLangs = subtitlesFromOpencast - .reduce((result: {id: string, lang: string}[], track) => { - const lang = track.tags.find(e => e.startsWith('lang:')) - if (lang) { - result.push({id: track.id, lang: lang.split(':')[1].trim()}) - } - return result; - }, []); - - const subtitlesLangs = Object.entries(subtitles) - .reduce((result: {id: string, lang: string}[], track) => { - const lang = track[1].tags.find(e => e.startsWith('lang:')) - if (lang) { - result.push({id: track[0], lang: lang.split(':')[1].trim()}) - } - return result; - }, []); - - const existingLangs = subtitlesFromOpencastLangs.concat(subtitlesLangs); - - // Create list of subtitles that can be added - const 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() - } + // Get flavors of already created tracks or existing subtitle tracks + const subtitleFlavors = captionTracks + .map(track => track.flavor.type + '/' + track.flavor.subtype) + .filter(flavor => !subtitles[flavor]) + .concat(Object.keys(subtitles)); + const 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)); + + // List of unused languages + const tempCanBeAddedFlavors = Object.keys(languages) + .map(flavor => ({subFlavor: flavor, title: languages[flavor]})) + .sort((lang1, lang2) => lang1.title.localeCompare(lang2.title)); + + setDisplayFlavors(tempDisplayFlavors) + setCanBeAddedFlavors(tempCanBeAddedFlavors) + }, [captionTracks, subtitles, t]) const subtitleSelectStyle = css({ display: 'flex', @@ -105,43 +69,40 @@ const SubtitleSelect : React.FC = () => { return buttons } - for (const 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 - + for (const subFlavor of displayFlavors) { + const icon = ((settings.subtitles || {}).icons || {})[subFlavor.subFlavor]; buttons.push( ) } - return buttons.sort((dat1, dat2) => dat1.props["title"].localeCompare(dat2.props["title"])) + return buttons } return (
{renderButtons()} {/* TODO: Only show the add button when there are still languages to add*/} - +
); } /** - * A button that sets the subtitle that should be edited + * A button that sets the flavor 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 = useTheme() @@ -164,6 +125,7 @@ const SubtitleSelectButton: React.FC<{ const titleStyle = css({ overflow: 'hidden', textOverflow: 'ellipsis', + whiteSpace: 'nowrap', minWidth: 0, }) @@ -174,27 +136,23 @@ const SubtitleSelectButton: React.FC<{ aria-label={t("subtitles.selectSubtitleButton-tooltip-aria", {title: title})} onClick={() => { dispatch(setIsDisplayEditView(true)) - dispatch(setSelectedSubtitleId(id)) + dispatch(setSelectedSubtitleFlavor(flavor)) }} onKeyDown={(event: React.KeyboardEvent) => { if (event.key === " " || event.key === "Enter") { dispatch(setIsDisplayEditView(true)) - dispatch(setSelectedSubtitleId(id)) + dispatch(setSelectedSubtitleFlavor(flavor)) } }}> {icon &&
{icon}
} -
{title ?? t('subtitles.generic') + " " + id}
+
{title}
); }; /** - * Actually not a button, but a container for a form that allows creating new subtitles for editing + * Actually not a button, but a container for a form that allows creating new flavors for editing */ -const SubtitleAddButton: React.FC<{ - subtitlesForDropdown: {id: string, tags: string[]}[] -}> = ({ - subtitlesForDropdown -}) => { +const SubtitleAddButton: React.FC<{languages: {subFlavor: string, title: string}[]}> = ({languages}) => { const { t } = useTranslation(); const theme = useTheme() @@ -206,27 +164,22 @@ const SubtitleAddButton: React.FC<{ // Parse language data into a format the dropdown understands const selectData = () => { const data = [] - for (const subtitle of subtitlesForDropdown) { - const lang = generateButtonTitle(subtitle.tags, t) - data.push({label: lang ?? t('subtitles.generic') + " " + subtitle.id, value: subtitle.id}) + for (const lan of languages) { + data.push({label: lan.title, value: lan.subFlavor}) } - data.sort((dat1, dat2) => dat1.label.localeCompare(dat2.label)) return data } - 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 }})) + const onSubmit = (values: { languages: any; }) => { + // Create new subtitle for the given flavor + dispatch(setSubtitle({identifier: values.languages, subtitles: []})) // Reset setIsPlusDisplay(true) // Move to editor view dispatch(setIsDisplayEditView(true)) - dispatch(setSelectedSubtitleId(id)) + dispatch(setSelectedSubtitleFlavor(values.languages)) } const plusIconStyle = css({ @@ -282,7 +235,7 @@ const SubtitleAddButton: React.FC<{ @@ -293,12 +246,14 @@ const SubtitleAddButton: React.FC<{ * disabled elements, add a simple wrapper element, such as a span." * see: https://mui.com/material-ui/react-tooltip/#disabled-elements */} - + + + @@ -309,29 +264,4 @@ const SubtitleAddButton: React.FC<{ ); } -/** - * Generates a title for the buttons from the tags - */ -export function generateButtonTitle(tags: string[], t: any) { - let lang = tags.find(e => e.startsWith('lang:')) - lang = lang ? lang.split(':')[1].trim() : undefined - lang = languageCodeToName(lang?.trim()) ?? lang - - let cc = '' - const type = tags.find(e => e.startsWith('type:')) - const isCC = type ? type.split(':')[1].trim() === 'closed-caption' : undefined - if (isCC) { - cc = '[CC]' - } - - let autoGen = '' - const genType = tags.find(e => e.startsWith('generator-type:')) - const isAutoGen = genType ? genType.split(':')[1].trim() === 'auto' : undefined - if (isAutoGen) { - autoGen = "(" + t('subtitles.autoGenerated') + ")" - } - - return cc + " " + lang + " " + autoGen -} - export default SubtitleSelect; diff --git a/src/main/SubtitleTimeline.tsx b/src/main/SubtitleTimeline.tsx index 8dd6a64f9..ec8734ccf 100644 --- a/src/main/SubtitleTimeline.tsx +++ b/src/main/SubtitleTimeline.tsx @@ -3,8 +3,8 @@ import { css } from "@emotion/react"; import { SegmentsList as CuttingSegmentsList, Waveforms } from "./Timeline"; import { selectCurrentlyAt, - selectSelectedSubtitleById, - selectSelectedSubtitleId, + selectSelectedSubtitleByFlavor, + selectSelectedSubtitleFlavor, setClickTriggered, setCueAtIndex, setCurrentlyAt, @@ -177,7 +177,7 @@ const SubtitleTimeline: React.FC = () => { const TimelineSubtitleSegmentsList: React.FC<{timelineWidth: number}> = ({timelineWidth}) => { const arbitraryHeight = 80 - const subtitle = useSelector(selectSelectedSubtitleById) + const subtitle = useSelector(selectSelectedSubtitleByFlavor) const segmentsListStyle = css({ position: 'relative', @@ -188,7 +188,7 @@ const TimelineSubtitleSegmentsList: React.FC<{timelineWidth: number}> = ({timeli return (
- {subtitle?.cues?.map((item, i) => { + {subtitle?.map((item, i) => { return ( ) @@ -209,7 +209,7 @@ const TimelineSubtitleSegment: React.FC<{ // Redux const dispatch = useDispatch() - const selectedId = useSelector(selectSelectedSubtitleId) + const selectedFlavor = useSelector(selectSelectedSubtitleFlavor) const duration = useSelector(selectDuration) // Dimensions and position offsets in px. Required for resizing @@ -246,7 +246,7 @@ const TimelineSubtitleSegment: React.FC<{ } dispatch(setCueAtIndex({ - identifier: selectedId, + identifier: selectedFlavor, cueIndex: props.index, newCue: { id: props.cue.id, diff --git a/src/main/SubtitleVideoArea.tsx b/src/main/SubtitleVideoArea.tsx index 9a73555c9..55329d6ba 100644 --- a/src/main/SubtitleVideoArea.tsx +++ b/src/main/SubtitleVideoArea.tsx @@ -11,7 +11,7 @@ import { selectCurrentlyAt, selectAspectRatio, setAspectRatio, selectCurrentlyAtInSeconds, - selectSelectedSubtitleById, + selectSelectedSubtitleByFlavor, selectIsPlayPreview, setIsPlayPreview, setCurrentlyAtAndTriggerPreview} from "../redux/subtitleSlice"; @@ -39,7 +39,7 @@ import { selectFieldStyle } from "../cssStyles"; const SubtitleVideoArea : React.FC = () => { const tracks = useSelector(selectVideos) - const subtitle = useSelector(selectSelectedSubtitleById) + const subtitle = useSelector(selectSelectedSubtitleByFlavor) const [selectedFlavor, setSelectedFlavor] = useState() const [subtitleUrl, setSubtitleUrl] = useState("") @@ -80,12 +80,12 @@ const SubtitleVideoArea : React.FC = () => { // Parse subtitles to something the video player understands useEffect(() => { - if (subtitle?.cues) { - const serializedSubtitle = serializeSubtitle(subtitle?.cues) + if (subtitle) { + const serializedSubtitle = serializeSubtitle(subtitle) setSubtitleUrl(window.URL.createObjectURL(new Blob([serializedSubtitle], {type: 'text/vtt'}))) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [subtitle?.cues]) + }, [subtitle]) const areaWrapper = css({ display: 'block', diff --git a/src/main/WorkflowConfiguration.tsx b/src/main/WorkflowConfiguration.tsx index 46e48138b..acf9425cc 100644 --- a/src/main/WorkflowConfiguration.tsx +++ b/src/main/WorkflowConfiguration.tsx @@ -18,6 +18,7 @@ import { AppDispatch } from "../redux/store"; import { selectSubtitles } from "../redux/subtitleSlice"; import { serializeSubtitle } from "../util/utilityFunctions"; import { useTheme } from "../themes"; +import { Flavor } from "../types"; /** * Will eventually display settings based on the selected workflow index @@ -93,11 +94,9 @@ export const SaveAndProcessButton: React.FC<{text: string}> = ({text}) => { const subtitlesForPosting = [] for (const identifier in subtitles) { - subtitlesForPosting.push({ - id: identifier, - subtitle: serializeSubtitle(subtitles[identifier].cues), - tags: subtitles[identifier].tags - }) + const flavor: Flavor = {type: identifier.split("/")[0], subtype: identifier.split("/")[1]} + subtitlesForPosting.push({flavor: flavor, subtitle: serializeSubtitle(subtitles[identifier])}) + } return subtitlesForPosting } diff --git a/src/redux/subtitleSlice.ts b/src/redux/subtitleSlice.ts index 6c4ce43eb..a73ab4bf7 100644 --- a/src/redux/subtitleSlice.ts +++ b/src/redux/subtitleSlice.ts @@ -1,4 +1,4 @@ -import { Segment, SubtitleCue, SubtitlesInEditor } from './../types'; +import { Segment, SubtitleCue } from './../types'; import { createSlice, Dispatch, nanoid, PayloadAction } from '@reduxjs/toolkit' import { roundToDecimalPlace } from '../util/utilityFunctions'; import type { RootState } from '../redux/store' @@ -11,8 +11,8 @@ export interface subtitle { previewTriggered: boolean, // Basically acts as a callback for the video players. currentlyAt: number, // Position in the video in milliseconds clickTriggered: boolean, // Another video player callback - subtitles: { [identifier: string]: SubtitlesInEditor }, - selectedSubtitleId: string, + subtitles: { [identifier: string]: SubtitleCue[] }, + selectedSubtitleFlavor: string, aspectRatios: {width: number, height: number}[], // Aspect ratios of every video focusSegmentTriggered: boolean, // a segment in the timeline was clicked focusSegmentId: string, // which segment in the timeline was clicked @@ -29,7 +29,7 @@ const initialState: subtitle = { currentlyAt: 0, clickTriggered: false, subtitles: {}, - selectedSubtitleId: "", + selectedSubtitleFlavor: "", focusSegmentTriggered: false, focusSegmentId: "", focusSegmentTriggered2: false, @@ -74,16 +74,16 @@ export const subtitleSlice = createSlice({ setClickTriggered: (state, action) => { state.clickTriggered = action.payload }, - setSubtitle: (state, action: PayloadAction<{identifier: string, subtitles: SubtitlesInEditor}>) => { + setSubtitle: (state, action: PayloadAction<{identifier: string, subtitles: SubtitleCue[]}>) => { state.subtitles[action.payload.identifier] = action.payload.subtitles }, setCueAtIndex: (state, action: PayloadAction<{identifier: string, cueIndex: number, newCue: SubtitleCue}>) => { - if (action.payload.cueIndex < 0 || action.payload.cueIndex >= state.subtitles[action.payload.identifier].cues.length) { + if (action.payload.cueIndex < 0 || action.payload.cueIndex >= state.subtitles[action.payload.identifier].length) { console.log("WARNING: Tried to set segment for subtitle " + action.payload.identifier + " but was out of range") return } - const cue = state.subtitles[action.payload.identifier].cues[action.payload.cueIndex] + const cue = state.subtitles[action.payload.identifier][action.payload.cueIndex] cue.id = action.payload.newCue.id cue.idInternal = action.payload.newCue.idInternal cue.text = action.payload.newCue.text @@ -92,7 +92,7 @@ export const subtitleSlice = createSlice({ cue.tree.children[0].value = action.payload.newCue.text - state.subtitles[action.payload.identifier].cues[action.payload.cueIndex] = cue + state.subtitles[action.payload.identifier][action.payload.cueIndex] = cue sortSubtitle(state, action.payload.identifier) state.hasChanges = true @@ -114,31 +114,31 @@ export const subtitleSlice = createSlice({ state.focusSegmentId = cue.idInternal if (action.payload.cueIndex < 0) { - state.subtitles[action.payload.identifier].cues.splice(0, 0, cue); + state.subtitles[action.payload.identifier].splice(0, 0, cue); } - if (action.payload.cueIndex >= 0 || action.payload.cueIndex < state.subtitles[action.payload.identifier].cues.length) { - state.subtitles[action.payload.identifier].cues.splice(action.payload.cueIndex, 0, cue); + if (action.payload.cueIndex >= 0 || action.payload.cueIndex < state.subtitles[action.payload.identifier].length) { + state.subtitles[action.payload.identifier].splice(action.payload.cueIndex, 0, cue); } - if (action.payload.cueIndex >= state.subtitles[action.payload.identifier].cues.length) { - state.subtitles[action.payload.identifier].cues.push(cue) + if (action.payload.cueIndex >= state.subtitles[action.payload.identifier].length) { + state.subtitles[action.payload.identifier].push(cue) } sortSubtitle(state, action.payload.identifier) state.hasChanges = true }, removeCue: (state, action: PayloadAction<{identifier: string, cue: SubtitleCue}>) => { - const cueIndex = state.subtitles[action.payload.identifier].cues.findIndex(i => i.idInternal === action.payload.cue.idInternal); + const cueIndex = state.subtitles[action.payload.identifier].findIndex(i => i.id === action.payload.cue.id); if (cueIndex > -1) { - state.subtitles[action.payload.identifier].cues.splice(cueIndex, 1); + state.subtitles[action.payload.identifier].splice(cueIndex, 1); } sortSubtitle(state, action.payload.identifier) state.hasChanges = true }, - setSelectedSubtitleId: (state, action: PayloadAction) => { - state.selectedSubtitleId = action.payload + setSelectedSubtitleFlavor: (state, action: PayloadAction) => { + state.selectedSubtitleFlavor = action.payload }, setFocusSegmentTriggered: (state, action: PayloadAction) => { state.focusSegmentTriggered = action.payload @@ -151,20 +151,20 @@ export const subtitleSlice = createSlice({ state.focusSegmentTriggered2 = action.payload }, setFocusToSegmentAboveId: (state, action: PayloadAction<{identifier: string, segmentId: subtitle["focusSegmentId"]}>) => { - let cueIndex = state.subtitles[action.payload.identifier].cues.findIndex(i => i.idInternal === action.payload.segmentId); + let cueIndex = state.subtitles[action.payload.identifier].findIndex(i => i.id === action.payload.segmentId); cueIndex = cueIndex - 1 if (cueIndex < 0) { cueIndex = 0 } - state.focusSegmentId = state.subtitles[action.payload.identifier].cues[cueIndex].idInternal + state.focusSegmentId = state.subtitles[action.payload.identifier][cueIndex].idInternal }, setFocusToSegmentBelowId: (state, action: PayloadAction<{identifier: string, segmentId: subtitle["focusSegmentId"]}>) => { - let cueIndex = state.subtitles[action.payload.identifier].cues.findIndex(i => i.idInternal === action.payload.segmentId); + let cueIndex = state.subtitles[action.payload.identifier].findIndex(i => i.idInternal === action.payload.segmentId); cueIndex = cueIndex + 1 - if (cueIndex >= state.subtitles[action.payload.identifier].cues.length) { - cueIndex = state.subtitles[action.payload.identifier].cues.length - 1 + if (cueIndex >= state.subtitles[action.payload.identifier].length) { + cueIndex = state.subtitles[action.payload.identifier].length - 1 } - state.focusSegmentId = state.subtitles[action.payload.identifier].cues[cueIndex].idInternal + state.focusSegmentId = state.subtitles[action.payload.identifier][cueIndex].idInternal }, setAspectRatio: (state, action: PayloadAction<{dataKey: number} & {width: number, height: number}>) => { state.aspectRatios[action.payload.dataKey] = {width: action.payload.width, height: action.payload.height} @@ -177,13 +177,13 @@ export const subtitleSlice = createSlice({ // Sort a subtitle array by startTime const sortSubtitle = (state: subtitle, identifier: string) => { - state.subtitles[identifier].cues.sort((a, b) => a.startTime - b.startTime) + state.subtitles[identifier].sort((a, b) => a.startTime - b.startTime) } // Export Actions export const { setIsDisplayEditView, setIsPlaying, setIsPlayPreview, setPreviewTriggered, setCurrentlyAt, setCurrentlyAtInSeconds, setClickTriggered, setSubtitle, setCueAtIndex, addCueAtIndex, removeCue, - setSelectedSubtitleId, setFocusSegmentTriggered, setFocusSegmentId, setFocusSegmentTriggered2, + setSelectedSubtitleFlavor, setFocusSegmentTriggered, setFocusSegmentId, setFocusSegmentTriggered2, setFocusToSegmentAboveId, setFocusToSegmentBelowId, setAspectRatio, setHasChanges } = subtitleSlice.actions // Export Selectors @@ -214,11 +214,11 @@ export const selectAspectRatio = (_state: { subtitleState: { aspectRatios: subti export const selectSubtitles = (state: { subtitleState: { subtitles: subtitle["subtitles"] } }) => state.subtitleState.subtitles -export const selectSelectedSubtitleId = (state: { subtitleState: { selectedSubtitleId: subtitle["selectedSubtitleId"] } }) => - state.subtitleState.selectedSubtitleId -export const selectSelectedSubtitleById = (state: { subtitleState: - { subtitles: subtitle["subtitles"]; selectedSubtitleId: subtitle["selectedSubtitleId"]; }; }) => - state.subtitleState.subtitles[state.subtitleState.selectedSubtitleId] +export const selectSelectedSubtitleFlavor = (state: { subtitleState: { selectedSubtitleFlavor: subtitle["selectedSubtitleFlavor"] } }) => + state.subtitleState.selectedSubtitleFlavor +export const selectSelectedSubtitleByFlavor = (state: { subtitleState: + { subtitles: subtitle["subtitles"]; selectedSubtitleFlavor: subtitle["selectedSubtitleFlavor"]; }; }) => + state.subtitleState.subtitles[state.subtitleState.selectedSubtitleFlavor] export const selectHasChanges = (state: { subtitleState: { hasChanges: subtitle["hasChanges"] } }) => state.subtitleState.hasChanges diff --git a/src/redux/videoSlice.ts b/src/redux/videoSlice.ts index c9c230fb0..8f90717b9 100644 --- a/src/redux/videoSlice.ts +++ b/src/redux/videoSlice.ts @@ -15,7 +15,7 @@ export interface video { currentlyAt: number, // Position in the video in milliseconds segments: Segment[], tracks: Track[], - subtitlesFromOpencast: SubtitlesFromOpencast[], + captions: SubtitlesFromOpencast[], activeSegmentIndex: number, // Index of the segment that is currenlty hovered selectedWorkflowId: string, // Id of the currently selected workflow aspectRatios: {width: number, height: number}[], // Aspect ratios of every video @@ -49,7 +49,7 @@ export const initialState: video & httpRequestState = { currentlyAt: 0, // Position in the video in milliseconds segments: [{id: nanoid(), start: 0, end: 1, deleted: false}], tracks: [], - subtitlesFromOpencast: [], + captions: [], activeSegmentIndex: 0, selectedWorkflowId: "", previewTriggered: false, @@ -241,7 +241,7 @@ const videoSlice = createSlice({ // eslint-disable-next-line no-sequences state.videoURLs = videos.reduce((a: string[], o: { uri: string }) => (a.push(o.uri), a), []) state.videoCount = state.videoURLs.length - state.subtitlesFromOpencast = action.payload.subtitles ? state.subtitlesFromOpencast = action.payload.subtitles : [] + state.captions = action.payload.subtitles ? state.captions = action.payload.subtitles : [] state.duration = action.payload.duration state.title = action.payload.title state.segments = parseSegments(action.payload.segments, action.payload.duration) @@ -430,11 +430,11 @@ export const selectTracks = (state: { videoState: { tracks: video["tracks"] } }) export const selectWorkflows = (state: { videoState: { workflows: video["workflows"] } }) => state.videoState.workflows export const selectAspectRatio = (state: { videoState: { aspectRatios: video["aspectRatios"] } }) => calculateTotalAspectRatio(state.videoState.aspectRatios) -export const selectSubtitlesFromOpencast = (state: { videoState: { subtitlesFromOpencast: video["subtitlesFromOpencast"]; }; }) => - state.videoState.subtitlesFromOpencast -export const selectSubtitlesFromOpencastById = (id: string) => (state: { videoState: { subtitlesFromOpencast: video["subtitlesFromOpencast"]; }; }) => { - for (const cap of state.videoState.subtitlesFromOpencast) { - if (cap.id === id) { +export const selectCaptions = (state: { videoState: { captions: video["captions"]; }; }) => + state.videoState.captions +export const selectCaptionTrackByFlavor = (flavor: string) => (state: { videoState: { captions: video["captions"]; }; }) => { + for (const cap of state.videoState.captions) { + if (cap.flavor.type + "/" + cap.flavor.subtype === flavor) { return cap } } diff --git a/src/types.ts b/src/types.ts index 0a9723f85..c0dca9610 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,14 +33,8 @@ export interface TimelineState { } export interface SubtitlesFromOpencast { - id: string, + flavor: Flavor, subtitle: string, - tags: string[], -} - -export interface SubtitlesInEditor { - cues: SubtitleCue[], - tags: string[], } export interface SubtitleCue { @@ -66,7 +60,7 @@ export interface ExtendedSubtitleCue extends SubtitleCue { export interface PostEditArgument { segments: Segment[] tracks: Track[] - subtitles: SubtitlesFromOpencast[] + subtitles: {flavor: Flavor, subtitle: string}[] } export interface PostAndProcessEditArgument extends PostEditArgument{