Skip to content

Commit

Permalink
Add internal identifier for subtitle cues
Browse files Browse the repository at this point in the history
The subtitle editor requires unique identifiers to work properly.
However, the webvtt parser does not provide such ids. WebVTT cue ids are
not a good fit either, as they hardly qualifiy as identifiers.

This adds a new field to the subtitle cues to act as a unique
identifier. This should also stop the editor from adding
randomly generated WebVTT cue ids to subtitle files.
  • Loading branch information
Arnei authored and lkiesow committed Apr 5, 2023
1 parent 598f86f commit cc7d597
Show file tree
Hide file tree
Showing 5 changed files with 27 additions and 19 deletions.
17 changes: 10 additions & 7 deletions src/main/SubtitleListEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const SubtitleListEditor : React.FC<{}> = () => {
useEffect(() => {
if (focusTriggered) {
if (itemsRef && itemsRef.current && subtitle) {
const itemIndex = subtitle.findIndex(item => item.id === focusId)
const itemIndex = subtitle.findIndex(item => item.idInternal === focusId)
if (listRef && listRef.current) {
listRef.current.scrollToItem(itemIndex, "center");

Expand Down Expand Up @@ -121,7 +121,7 @@ const SubtitleListEditor : React.FC<{}> = () => {
itemCount={subtitle !== undefined ? subtitle.length : 0}
itemData={itemData}
itemSize={(index) => segmentHeight}
itemKey={(index, data) => data.items[index].id}
itemKey={(index, data) => data.items[index].idInternal}
width={width}
overscanCount={4}
estimatedItemSize={calcEstimatedSize()}
Expand Down Expand Up @@ -196,20 +196,21 @@ const SubtitleListSegment = React.memo((props: subtitleListSegmentProps) => {

// Set focus to textarea
useEffect(() => {
if (focusTriggered2 && focusId2 === cue.id) {
if (focusTriggered2 && focusId2 === cue.idInternal) {
if (textAreaRef && textAreaRef.current) {
textAreaRef.current.focus()
}
dispatch(setFocusSegmentTriggered2(false))
}
}, [cue.id, dispatch, focusId2, focusTriggered2])
}, [cue.idInternal, dispatch, focusId2, focusTriggered2])

const updateCueText = (event: { target: { value: any } }) => {
dispatch(setCueAtIndex({
identifier: identifier,
cueIndex: props.index,
newCue: {
id: cue.id,
idInternal: cue.idInternal,
text: event.target.value,
startTime: cue.startTime,
endTime: cue.endTime,
Expand All @@ -224,6 +225,7 @@ const SubtitleListSegment = React.memo((props: subtitleListSegmentProps) => {
cueIndex: props.index,
newCue: {
id: cue.id,
idInternal: cue.idInternal,
text: cue.text,
startTime: event.target.value,
endTime: cue.endTime,
Expand All @@ -238,6 +240,7 @@ const SubtitleListSegment = React.memo((props: subtitleListSegmentProps) => {
cueIndex: props.index,
newCue: {
id: cue.id,
idInternal: cue.idInternal,
text: cue.text,
startTime: cue.startTime,
endTime: event.target.value,
Expand Down Expand Up @@ -278,15 +281,15 @@ const SubtitleListSegment = React.memo((props: subtitleListSegmentProps) => {
addBelow: () => addCueBelow(),
jumpAbove: () => {
dispatch(setFocusSegmentTriggered(true))
dispatch(setFocusToSegmentAboveId({identifier: identifier, segmentId: cue.id}))
dispatch(setFocusToSegmentAboveId({identifier: identifier, segmentId: cue.idInternal}))
},
jumpBelow: () => {
dispatch(setFocusSegmentTriggered(true))
dispatch(setFocusToSegmentBelowId({identifier: identifier, segmentId: cue.id}))
dispatch(setFocusToSegmentBelowId({identifier: identifier, segmentId: cue.idInternal}))
},
delete: () => {
dispatch(setFocusSegmentTriggered(true))
dispatch(setFocusToSegmentAboveId({identifier: identifier, segmentId: cue.id}))
dispatch(setFocusToSegmentAboveId({identifier: identifier, segmentId: cue.idInternal}))
deleteCue()
},
}
Expand Down
5 changes: 3 additions & 2 deletions src/main/SubtitleTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ const TimelineSubtitleSegmentsList: React.FC<{timelineWidth: number}> = ({timeli
<div css={segmentsListStyle}>
{subtitle?.map((item, i) => {
return (
<TimelineSubtitleSegment timelineWidth={timelineWidth} cue={item} height={arbitraryHeight} key={item.id} index={i}/>
<TimelineSubtitleSegment timelineWidth={timelineWidth} cue={item} height={arbitraryHeight} key={item.idInternal} index={i}/>
)
})}
</div>
Expand Down Expand Up @@ -237,6 +237,7 @@ const TimelineSubtitleSegment: React.FC<{
cueIndex: props.index,
newCue: {
id: props.cue.id,
idInternal: props.cue.idInternal,
text: props.cue.text,
startTime: newStartTime,
endTime: newEndTime,
Expand Down Expand Up @@ -322,7 +323,7 @@ const TimelineSubtitleSegment: React.FC<{

// Inform list view which segment was clicked
dispatch(setFocusSegmentTriggered(true))
dispatch(setFocusSegmentId(props.cue.id))
dispatch(setFocusSegmentId(props.cue.idInternal))
dispatch(setFocusSegmentTriggered2(true))
}

Expand Down
16 changes: 9 additions & 7 deletions src/redux/subtitleSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const subtitleSlice = createSlice({

let 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
cue.startTime = Math.round(action.payload.newCue.startTime)
cue.endTime = Math.round(action.payload.newCue.endTime)
Expand All @@ -96,7 +97,8 @@ export const subtitleSlice = createSlice({
addCueAtIndex: (state, action: PayloadAction<{identifier: string, cueIndex: number, text: string, startTime: number, endTime: number}>) => {
const startTime = action.payload.startTime >= 0 ? action.payload.startTime : 0
const cue: SubtitleCue = {
id: nanoid(),
id: undefined,
idInternal: nanoid(),
text: action.payload.text,
startTime: Math.round(startTime),
endTime: Math.round(action.payload.endTime),
Expand All @@ -106,7 +108,7 @@ export const subtitleSlice = createSlice({
// Trigger a callback in the list component that focuses the newly added element
state.focusSegmentTriggered = true
state.focusSegmentTriggered2 = true
state.focusSegmentId = cue.id
state.focusSegmentId = cue.idInternal

if (action.payload.cueIndex < 0 ) {
state.subtitles[action.payload.identifier].splice(0, 0, cue);
Expand All @@ -123,7 +125,7 @@ export const subtitleSlice = createSlice({
sortSubtitle(state, action.payload.identifier)
},
removeCue: (state, action: PayloadAction<{identifier: string, cue: SubtitleCue}>) => {
const cueIndex = state.subtitles[action.payload.identifier].findIndex(i => i.id === action.payload.cue.id);
const cueIndex = state.subtitles[action.payload.identifier].findIndex(i => i.idInternal === action.payload.cue.idInternal);
if (cueIndex > -1) {
state.subtitles[action.payload.identifier].splice(cueIndex, 1);
}
Expand All @@ -144,20 +146,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].findIndex(i => i.id === action.payload.segmentId);
let cueIndex = state.subtitles[action.payload.identifier].findIndex(i => i.idInternal === action.payload.segmentId);
cueIndex = cueIndex - 1
if (cueIndex < 0 ) {
cueIndex = 0
}
state.focusSegmentId = state.subtitles[action.payload.identifier][cueIndex].id
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].findIndex(i => i.id === 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].length) {
cueIndex = state.subtitles[action.payload.identifier].length - 1
}
state.focusSegmentId = state.subtitles[action.payload.identifier][cueIndex].id
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}
Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export interface SubtitlesFromOpencast {
}

export interface SubtitleCue {
id: string,
id?: string, // Actually not useful as an identifier, as it is not guaranteed to exist
idInternal: string, // Identifier for internal use. Has nothing to do with the webvtt parser.
text: string,
startTime: number,
endTime: number,
Expand Down
5 changes: 3 additions & 2 deletions src/util/utilityFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ export function serializeSubtitle(subtitle: SubtitleCue[]) {
cue.endTime = cue.endTime / 1000

const extendedCue : ExtendedSubtitleCue = {
id: cue.id,
id: cue.id ? cue.id : undefined,
idInternal: cue.idInternal,
text: cue.text,
startTime: cue.startTime,
endTime: cue.endTime,
Expand Down Expand Up @@ -145,7 +146,7 @@ export function parseSubtitle(subtitle: String) {
let index = 0
for (let cue of tree.cues) {
if (!cue.id) {
cue.id = nanoid()
cue.idInternal = nanoid()
tree.cues[index] = cue
}

Expand Down

0 comments on commit cc7d597

Please sign in to comment.