Skip to content

Commit bfdc10a

Browse files
committed
Subtitle editing
1 parent f31b1f1 commit bfdc10a

3 files changed

Lines changed: 135 additions & 8 deletions

File tree

chrome/player/SubtitleTrack.mjs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,30 @@ export class SubtitleTrack {
2424
});
2525
}
2626

27+
shiftAfter(cue, time) {
28+
// only shift cues that are the given cue or come after it. This allows users to shift a single cue without affecting cues that come before it.
29+
let shift = false;
30+
this.cues.forEach((c) => {
31+
if (c === cue) {
32+
shift = true;
33+
}
34+
if (shift) {
35+
c.startTime += time;
36+
c.endTime += time;
37+
}
38+
});
39+
}
40+
2741
loadText(text) {
2842
if (text.substring(0, 5) === '<?xml') {
2943
text = SubtitleUtils.xml2vtt(text);
3044
} else if (text.trim().split('\n')[0].trim().substr(0, 6) !== 'WEBVTT') {
3145
text = SubtitleUtils.srt2webvtt(text);
3246
}
3347

34-
// sometimes formatting in subtitles are not properly
48+
// sometimes formatting in subtitles are not properly
3549
// converted into webvtt, so we need to convert them manually
36-
text = SubtitleUtils.convertSubtitleFormatting(text)
50+
text = SubtitleUtils.convertSubtitleFormatting(text);
3751

3852
// eslint-disable-next-line new-cap
3953
const parser = new WebVTT.Parser(window, WebVTT.StringDecoder());

chrome/player/ui/FineTimeControls.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,13 @@ export class FineTimeControls extends EventEmitter {
525525
});
526526
}
527527

528+
mousePositionToTime(clientX) {
529+
const video = this.client.player.getVideo();
530+
const rect = this.ui.timelineTicks.getBoundingClientRect();
531+
const time = ((clientX - rect.left) / rect.width) * video.duration;
532+
return time;
533+
}
534+
528535
renderTimeline() {
529536
if (!this.started || !this.client.interfaceController.controlsVisible || !this.client.player) return;
530537

@@ -533,7 +540,7 @@ export class FineTimeControls extends EventEmitter {
533540
const video = this.client.player.getVideo();
534541
const duration = video.duration;
535542

536-
const timePerWidth = 60;
543+
const timePerWidth = window.subEditMode ? 30 : 60;
537544
const minTime = Math.floor(Math.max(0, time - timePerWidth / 2 - 5));
538545
const maxTime = Math.ceil(Math.min(video.duration, time + timePerWidth / 2 + 5));
539546
this.ui.timelineContainer.style.width = (video.duration / timePerWidth) * 100 + '%';

chrome/player/ui/subtitles/SubtitleSyncer.mjs

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Localize} from '../../modules/Localize.mjs';
22
import {EventEmitter} from '../../modules/eventemitter.mjs';
33
import {WebVTT} from '../../modules/vtt.mjs';
4+
import {AlertPolyfill} from '../../utils/AlertPolyfill.mjs';
45
import {WebUtils} from '../../utils/WebUtils.mjs';
56
import {DOMElements} from '../DOMElements.mjs';
67

@@ -31,27 +32,132 @@ export class SubtitleSyncer extends EventEmitter {
3132
// track line is grabbable
3233
let isGrabbingTrack = false;
3334
let grabStartTrack = 0;
35+
let grabbedCue = null;
36+
let grabbedEdge = null;
37+
3438

3539
this.ui.timelineTrack.addEventListener('mousedown', (e) => {
3640
isGrabbingTrack = true;
3741
grabStartTrack = e.clientX;
42+
if (window.subEditMode && !e.shiftKey) {
43+
const grabbed = this.trackElements.find((el) => {
44+
const rect = el.element.getBoundingClientRect();
45+
return e.clientX >= rect.left && e.clientX <= rect.right;
46+
}) || null;
47+
// check if grabbing right edge to resize cue
48+
if (grabbed) {
49+
const rect = grabbed.element.getBoundingClientRect();
50+
if (e.clientX >= rect.right - 5 && e.clientX <= rect.right + 5) {
51+
grabbedEdge = 'right';
52+
} else {
53+
grabbedEdge = null;
54+
}
55+
56+
grabbedCue = grabbed.cue;
57+
} else {
58+
grabbedCue = null;
59+
}
60+
} else {
61+
grabbedCue = null;
62+
grabbedEdge = null;
63+
}
3864
});
3965

40-
DOMElements.playerContainer.addEventListener('mouseup', () => {
41-
isGrabbingTrack = false;
66+
// double click to edit
67+
this.ui.timelineTrack.addEventListener('dblclick', (e) => {
68+
if (!window.subEditMode) return;
69+
const time = this.client.interfaceController.fineTimeControls.mousePositionToTime(e.clientX);
70+
const cue = this.trackToSync.cues.find((c) => {
71+
return time >= c.startTime && time <= c.endTime;
72+
});
73+
if (cue) {
74+
AlertPolyfill.prompt('Edit subtitle text', cue.text).then((newText) => {
75+
if (newText) {
76+
cue.text = newText;
77+
cue.dom2 = null; // reset cached DOM tree so it will be regenerated with new text
78+
cue.dom = null; // reset cached DOM tree so it will be regenerated with new text
79+
// update element
80+
const el = this.trackElements.find((el) => el.cue === cue);
81+
if (el) {
82+
el.element.replaceChildren();
83+
if (!cue.dom2) {
84+
cue.dom2 = WebVTT.convertCueToDOMTree(window, cue.text);
85+
}
86+
el.element.appendChild(cue.dom2);
87+
el.element.title = cue.text;
88+
}
89+
} else {
90+
// if text is empty, remove cue
91+
const index = this.trackToSync.cues.indexOf(cue);
92+
if (index !== -1) {
93+
this.trackToSync.cues.splice(index, 1);
94+
}
95+
}
96+
});
97+
} else {
98+
// create new cue at this time with default duration of 2 seconds
99+
const newCue = new VTTCue(time, time + 2, 'New subtitle');
100+
this.trackToSync.cues.push(newCue);
101+
// sort cues by start time
102+
this.trackToSync.cues.sort((a, b) => a.startTime - b.startTime);
103+
this.client.interfaceController.subtitlesManager.renderSubtitles();
104+
AlertPolyfill.prompt('Edit subtitle text', newCue.text).then((newText) => {
105+
if (newText) {
106+
newCue.text = newText;
107+
newCue.dom2 = null; // reset cached DOM tree so it will be regenerated with new text
108+
newCue.dom = null; // reset cached DOM tree so it will be regenerated with new text
109+
const el = this.trackElements.find((el) => el.cue === newCue);
110+
if (el) {
111+
el.element.replaceChildren();
112+
if (!newCue.dom2) {
113+
newCue.dom2 = WebVTT.convertCueToDOMTree(window, newCue.text);
114+
}
115+
el.element.appendChild(newCue.dom2);
116+
el.element.title = newCue.text;
117+
}
118+
this.client.interfaceController.subtitlesManager.renderSubtitles();
119+
}
120+
});
121+
}
42122
});
43123

44-
DOMElements.playerContainer.addEventListener('mouseleave', () => {
124+
const clearGrabbing = () => {
125+
// if cue was grabbed then resort
126+
if (grabbedCue) {
127+
this.trackToSync.cues.sort((a, b) => a.startTime - b.startTime);
128+
}
129+
45130
isGrabbingTrack = false;
46-
});
131+
grabbedCue = null;
132+
grabbedEdge = null;
133+
};
134+
135+
DOMElements.playerContainer.addEventListener('mouseup', clearGrabbing);
136+
137+
DOMElements.playerContainer.addEventListener('mouseleave', clearGrabbing);
47138

48139
DOMElements.playerContainer.addEventListener('mousemove', (e) => {
49140
if (!this.client.player) return;
50141
const video = this.client.player.getVideo();
51142
if (isGrabbingTrack) {
52143
const delta = e.clientX - grabStartTrack;
53144
grabStartTrack = e.clientX;
54-
this.trackToSync.shift(delta / this.ui.timelineTrack.clientWidth * video.duration);
145+
const amount = delta / this.ui.timelineTrack.clientWidth * video.duration;
146+
if (grabbedCue) {
147+
if (grabbedEdge === 'right') {
148+
grabbedCue.endTime += amount;
149+
// update element
150+
const el = this.trackElements.find((el) => el.cue === grabbedCue);
151+
if (el) {
152+
el.element.style.width = (grabbedCue.endTime - grabbedCue.startTime) / video.duration * 100 + '%';
153+
}
154+
} else {
155+
this.trackToSync.shiftAfter(grabbedCue, amount);
156+
}
157+
} else {
158+
if (window.subEditMode) return;
159+
this.trackToSync.shift(amount);
160+
}
55161
this.client.interfaceController.subtitlesManager.renderSubtitles();
56162
}
57163
});

0 commit comments

Comments
 (0)