Skip to content

Commit 08fa48b

Browse files
author
Armanc
committed
Separate and refactor SubtitleSync and OffsetController
Extract offset state and interaction handling from SubtitleSync into a dedicated OffsetController class. Use real private methods, a currentOffset getter/setter, and a shared slider bubble helper. No user-facing behavior changes. Paves the way for the subtitle sync timeline feature. Add Armanc to CONTRIBUTORS.
1 parent 958fdca commit 08fa48b

2 files changed

Lines changed: 172 additions & 122 deletions

File tree

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
- [lmaotrigine](https://github.com/lmaotrigine)
109109
- [bjorntp](https://github.com/bjorntp)
110110
- [austinhardaway](https://github.com/austinhardaway)
111+
- [Armanc](https://github.com/armanckeser)
111112

112113
## Emby Contributors
113114

src/components/subtitlesync/subtitlesync.js

Lines changed: 171 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,142 @@
1-
21
import { playbackManager } from '../playback/playbackmanager';
32
import layoutManager from '../layoutManager';
43
import template from './subtitlesync.template.html';
54
import './subtitlesync.scss';
65

7-
let player;
8-
let subtitleSyncSlider;
9-
let subtitleSyncTextField;
10-
let subtitleSyncCloseButton;
11-
let subtitleSyncContainer;
12-
13-
function init(instance) {
14-
const parent = document.createElement('div');
15-
document.body.appendChild(parent);
16-
parent.innerHTML = template;
17-
18-
subtitleSyncSlider = parent.querySelector('.subtitleSyncSlider');
19-
subtitleSyncTextField = parent.querySelector('.subtitleSyncTextField');
20-
subtitleSyncCloseButton = parent.querySelector('.subtitleSync-closeButton');
21-
subtitleSyncContainer = parent.querySelector('.subtitleSyncContainer');
22-
23-
if (layoutManager.tv) {
24-
subtitleSyncSlider.classList.add('focusable');
25-
// HACK: Delay to give time for registered element attach (Firefox)
26-
setTimeout(function () {
27-
subtitleSyncSlider.enableKeyboardDragging();
28-
}, 0);
29-
}
30-
31-
subtitleSyncContainer.classList.add('hide');
32-
33-
subtitleSyncTextField.updateOffset = function (offset) {
34-
this.textContent = offset + 's';
35-
};
36-
37-
subtitleSyncTextField.addEventListener('click', function () {
38-
// keep focus to prevent fade with osd
39-
this.hasFocus = true;
40-
});
41-
42-
subtitleSyncTextField.addEventListener('keydown', function (event) {
43-
if (event.key === 'Enter') {
44-
// if input key is enter search for float pattern
45-
let inputOffset = /[-+]?\d+\.?\d*/g.exec(this.textContent);
46-
if (inputOffset) {
47-
inputOffset = inputOffset[0];
48-
inputOffset = parseFloat(inputOffset);
49-
50-
subtitleSyncSlider.updateOffset(inputOffset);
51-
} else {
52-
this.textContent = (playbackManager.getPlayerSubtitleOffset(player) || 0) + 's';
53-
}
54-
this.hasFocus = false;
55-
event.preventDefault();
56-
} else {
57-
// keep focus to prevent fade with osd
58-
this.hasFocus = true;
59-
if (event.key.match(/[+-\d.s]/) === null) {
60-
event.preventDefault();
61-
}
62-
}
6+
// Constants
7+
const DEFAULT_OFFSET = 0;
8+
9+
function createSliderBubbleHtml(_, value) {
10+
return '<h1 class="sliderBubbleText">'
11+
+ (value > 0 ? '+' : '') + Number.parseFloat(value) + 's'
12+
+ '</h1>';
13+
}
14+
15+
class OffsetController {
16+
constructor(player, slider, textField) {
17+
this.player = player;
18+
this.slider = slider;
19+
this.textField = textField;
20+
21+
this.#initSlider();
22+
this.#initTextField();
23+
24+
// Set initial offset
25+
this.reset();
26+
}
6327

64-
// FIXME: TV layout will require special handling for navigation keys. But now field is not focusable
65-
event.stopPropagation();
66-
});
28+
get currentOffset() {
29+
return Number.parseFloat(this.slider.value);
30+
}
31+
32+
set currentOffset(value) {
33+
this.slider.value = value.toString();
6734

68-
subtitleSyncTextField.blur = function () {
69-
// prevent textfield to blur while element has focus
70-
if (!this.hasFocus && this.prototype) {
71-
this.prototype.blur();
35+
// rely on slider value trimming
36+
value = this.currentOffset;
37+
38+
playbackManager.setSubtitleOffset(value, this.player);
39+
this.textField.updateOffset(value);
40+
}
41+
42+
#initSlider() {
43+
const slider = this.slider;
44+
45+
if (layoutManager.tv) {
46+
slider.classList.add('focusable');
47+
// Delay required for Firefox – wait for custom element to attach
48+
setTimeout(() => slider.enableKeyboardDragging(), 0);
7249
}
73-
};
7450

75-
function updateSubtitleOffset() {
76-
const value = parseFloat(subtitleSyncSlider.value);
77-
// set new offset
78-
playbackManager.setSubtitleOffset(value, player);
79-
// synchronize with textField value
80-
subtitleSyncTextField.updateOffset(value);
51+
// When slider changes we assign the value to the currentOffset to trigger the setter
52+
slider.addEventListener('change', () => {
53+
// eslint-disable-next-line no-self-assign
54+
this.currentOffset = this.currentOffset;
55+
});
56+
57+
slider.getBubbleHtml = createSliderBubbleHtml;
8158
}
8259

83-
subtitleSyncSlider.updateOffset = function (sliderValue) {
84-
// default value is 0s = 0ms
85-
this.value = sliderValue === undefined ? 0 : sliderValue;
60+
#initTextField() {
61+
const textField = this.textField;
8662

87-
updateSubtitleOffset();
88-
};
63+
textField.updateOffset = (offset) => {
64+
textField.textContent = offset + 's';
65+
};
66+
67+
textField.addEventListener('click', () => {
68+
// keep focus to prevent fade with osd
69+
textField.hasFocus = true;
70+
});
71+
72+
textField.addEventListener('keydown', (event) => {
73+
if (event.key === 'Enter') {
74+
// if input key is enter search for float pattern
75+
let inputOffset = /[-+]?\d+\.?\d*/g.exec(textField.textContent);
76+
if (inputOffset) {
77+
inputOffset = Number.parseFloat(inputOffset[0]);
78+
this.currentOffset = inputOffset;
79+
} else {
80+
textField.updateOffset(playbackManager.getPlayerSubtitleOffset(this.player) || 0);
81+
}
82+
textField.hasFocus = false;
83+
event.preventDefault();
84+
} else {
85+
// keep focus to prevent fade with osd
86+
textField.hasFocus = true;
87+
if (event.key.match(/[+-\d.s]/) === null) {
88+
event.preventDefault();
89+
}
90+
}
8991

90-
subtitleSyncSlider.addEventListener('change', () => updateSubtitleOffset());
92+
// TV layout will require special handling for navigation keys. But now field is not focusable
93+
event.stopPropagation();
94+
});
9195

92-
subtitleSyncSlider.getBubbleHtml = function (_, value) {
93-
return '<h1 class="sliderBubbleText">'
94-
+ (value > 0 ? '+' : '') + parseFloat(value) + 's'
95-
+ '</h1>';
96-
};
96+
// Preserve native blur while respecting the custom focus flag
97+
const originalBlur = textField.blur.bind(textField);
98+
textField.blur = function () {
99+
if (!this.hasFocus) {
100+
originalBlur();
101+
}
102+
};
103+
}
97104

98-
subtitleSyncCloseButton.addEventListener('click', function () {
99-
playbackManager.disableShowingSubtitleOffset(player);
100-
SubtitleSync.prototype.toggle('forceToHide');
101-
});
105+
adjustOffset(delta) {
106+
this.currentOffset = this.currentOffset + delta;
107+
}
102108

103-
instance.element = parent;
109+
reset() {
110+
this.currentOffset = DEFAULT_OFFSET;
111+
}
104112
}
105113

106114
class SubtitleSync {
107115
constructor(currentPlayer) {
108-
player = currentPlayer;
109-
init(this);
116+
this.player = currentPlayer;
117+
this.#initUI();
118+
119+
// Create the offset controller
120+
this.offsetController = new OffsetController(
121+
this.player,
122+
this.subtitleSyncSlider,
123+
this.subtitleSyncTextField
124+
);
110125
}
111126

112127
destroy() {
113-
SubtitleSync.prototype.toggle('forceToHide');
114-
if (player) {
115-
playbackManager.disableShowingSubtitleOffset(player);
116-
playbackManager.setSubtitleOffset(0, player);
128+
this.toggle('forceToHide');
129+
if (this.player) {
130+
playbackManager.disableShowingSubtitleOffset(this.player);
131+
this.offsetController.reset();
117132
}
118-
const elem = this.element;
119-
if (elem) {
120-
elem.parentNode.removeChild(elem);
133+
134+
if (this.element) {
135+
this.element.remove();
121136
this.element = null;
122137
}
138+
139+
this.player = null;
123140
}
124141

125142
toggle(action) {
@@ -128,43 +145,75 @@ class SubtitleSync {
128145
return;
129146
}
130147

131-
if (player && playbackManager.supportSubtitleOffset(player)) {
132-
if (!action) {
133-
// if showing subtitle sync is enabled and if there is an external subtitle stream enabled
134-
if (playbackManager.isShowingSubtitleOffsetEnabled(player) && playbackManager.canHandleOffsetOnCurrentSubtitle(player)) {
135-
// if no subtitle offset is defined or element has focus (offset being defined)
136-
if (!(playbackManager.getPlayerSubtitleOffset(player) || subtitleSyncTextField.hasFocus)) {
137-
// set default offset to '0' = 0ms
138-
subtitleSyncSlider.value = '0';
139-
subtitleSyncTextField.textContent = '0s';
140-
playbackManager.setSubtitleOffset(0, player);
141-
}
142-
// show subtitle sync
143-
subtitleSyncContainer.classList.remove('hide');
144-
return;
145-
}
146-
} else if (action === 'hide' && subtitleSyncTextField.hasFocus) {
147-
// do not hide if element has focus
148-
return;
149-
}
148+
if (!this.player || !playbackManager.supportSubtitleOffset(this.player)) {
149+
return;
150+
}
150151

151-
subtitleSyncContainer.classList.add('hide');
152+
if (!action) {
153+
this.#tryShowSubtitleSync();
154+
} else if (action === 'hide' && this.subtitleSyncTextField.hasFocus) {
155+
// do not hide if element has focus
156+
return;
157+
} else {
158+
this.subtitleSyncContainer.classList.add('hide');
152159
}
153160
}
154161

155-
update(offset) {
156-
this.toggle();
162+
#initUI() {
163+
const parent = document.createElement('div');
164+
document.body.appendChild(parent);
165+
parent.innerHTML = template;
166+
167+
// Store DOM elements
168+
this.element = parent;
169+
this.subtitleSyncSlider = parent.querySelector('.subtitleSyncSlider');
170+
this.subtitleSyncTextField = parent.querySelector('.subtitleSyncTextField');
171+
this.subtitleSyncCloseButton = parent.querySelector('.subtitleSync-closeButton');
172+
this.subtitleSyncContainer = parent.querySelector('.subtitleSyncContainer');
157173

158-
const value = parseFloat(subtitleSyncSlider.value) + offset;
159-
subtitleSyncSlider.updateOffset(value);
174+
this.#setupCloseButton();
175+
176+
// Initially hide the container
177+
this.subtitleSyncContainer.classList.add('hide');
178+
}
179+
180+
#setupCloseButton() {
181+
this.subtitleSyncCloseButton.addEventListener('click', () => {
182+
playbackManager.disableShowingSubtitleOffset(this.player);
183+
this.toggle('forceToHide');
184+
});
185+
}
186+
187+
#tryShowSubtitleSync() {
188+
// if showing subtitle sync is enabled and if there is an external subtitle stream enabled
189+
if (!this.#canShowSubtitleSync()) {
190+
this.subtitleSyncContainer.classList.add('hide');
191+
return;
192+
}
193+
194+
// If no subtitle offset is defined or element has focus (offset being defined)
195+
if (!(playbackManager.getPlayerSubtitleOffset(this.player) || this.subtitleSyncTextField.hasFocus)) {
196+
// reset to default offset
197+
this.offsetController.reset();
198+
}
199+
200+
// show subtitle sync
201+
this.subtitleSyncContainer.classList.remove('hide');
202+
}
203+
204+
#canShowSubtitleSync() {
205+
return playbackManager.isShowingSubtitleOffsetEnabled(this.player)
206+
&& playbackManager.canHandleOffsetOnCurrentSubtitle(this.player);
160207
}
161208

162209
incrementOffset() {
163-
this.update(+subtitleSyncSlider.step);
210+
this.toggle();
211+
this.offsetController.adjustOffset(+this.subtitleSyncSlider.step);
164212
}
165213

166214
decrementOffset() {
167-
this.update(-subtitleSyncSlider.step);
215+
this.toggle();
216+
this.offsetController.adjustOffset(-this.subtitleSyncSlider.step);
168217
}
169218
}
170219

0 commit comments

Comments
 (0)