Merge pull request #1408 from dmitrylyzo/fix-subtitle-line-spacing

Fix subtitle line spacing and add position
This commit is contained in:
Joshua M. Boniface 2020-08-06 16:22:47 -04:00 committed by GitHub
commit e2aff66203
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 247 additions and 69 deletions

View File

@ -3,52 +3,29 @@
* @module components/subtitleSettings/subtitleAppearanceHelper * @module components/subtitleSettings/subtitleAppearanceHelper
*/ */
function getTextStyles(settings, isCue) { function getTextStyles(settings, preview) {
let list = []; let list = [];
if (isCue) { switch (settings.textSize || '') {
switch (settings.textSize || '') { case 'smaller':
case 'smaller': list.push({ name: 'font-size', value: '.8em' });
list.push({ name: 'font-size', value: '.5em' }); break;
break; case 'small':
case 'small': list.push({ name: 'font-size', value: 'inherit' });
list.push({ name: 'font-size', value: '.7em' }); break;
break; case 'larger':
case 'large': list.push({ name: 'font-size', value: '2em' });
list.push({ name: 'font-size', value: '1.3em' }); break;
break; case 'extralarge':
case 'larger': list.push({ name: 'font-size', value: '2.2em' });
list.push({ name: 'font-size', value: '1.72em' }); break;
break; case 'large':
case 'extralarge': list.push({ name: 'font-size', value: '1.72em' });
list.push({ name: 'font-size', value: '2em' }); break;
break; default:
default: case 'medium':
case 'medium': list.push({ name: 'font-size', value: '1.36em' });
break; break;
}
} else {
switch (settings.textSize || '') {
case 'smaller':
list.push({ name: 'font-size', value: '.8em' });
break;
case 'small':
list.push({ name: 'font-size', value: 'inherit' });
break;
case 'larger':
list.push({ name: 'font-size', value: '2em' });
break;
case 'extralarge':
list.push({ name: 'font-size', value: '2.2em' });
break;
case 'large':
list.push({ name: 'font-size', value: '1.72em' });
break;
default:
case 'medium':
list.push({ name: 'font-size', value: '1.36em' });
break;
}
} }
switch (settings.dropShadow || '') { switch (settings.dropShadow || '') {
@ -111,13 +88,43 @@ function getTextStyles(settings, isCue) {
break; break;
} }
if (!preview) {
const pos = parseInt(settings.verticalPosition, 10);
const lineHeight = 1.35; // FIXME: It is better to read this value from element
const line = Math.abs(pos * lineHeight);
if (pos < 0) {
list.push({ name: 'min-height', value: `${line}em` });
list.push({ name: 'margin-top', value: '' });
} else {
list.push({ name: 'min-height', value: '' });
list.push({ name: 'margin-top', value: `${line}em` });
}
}
return list; return list;
} }
export function getStyles(settings, isCue) { function getWindowStyles(settings, preview) {
const list = [];
if (!preview) {
const pos = parseInt(settings.verticalPosition, 10);
if (pos < 0) {
list.push({ name: 'top', value: '' });
list.push({ name: 'bottom', value: '0' });
} else {
list.push({ name: 'top', value: '0' });
list.push({ name: 'bottom', value: '' });
}
}
return list;
}
export function getStyles(settings, preview) {
return { return {
text: getTextStyles(settings, isCue), text: getTextStyles(settings, preview),
window: [] window: getWindowStyles(settings, preview)
}; };
} }
@ -130,7 +137,7 @@ function applyStyleList(styles, elem) {
} }
export function applyStyles(elements, appearanceSettings) { export function applyStyles(elements, appearanceSettings) {
let styles = getStyles(appearanceSettings); let styles = getStyles(appearanceSettings, !!elements.preview);
if (elements.text) { if (elements.text) {
applyStyleList(styles.text, elements.text); applyStyleList(styles.text, elements.text);

View File

@ -0,0 +1,26 @@
.subtitleappearance-fullpreview {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1000;
pointer-events: none;
transition: 0.2s;
}
.subtitleappearance-fullpreview-hide {
opacity: 0;
}
.subtitleappearance-fullpreview-window {
position: absolute;
width: 100%;
font-size: 170%;
text-align: center;
}
.subtitleappearance-fullpreview-text {
display: inline-block;
max-width: 70%;
}

View File

@ -2,6 +2,7 @@ import globalize from 'globalize';
import appHost from 'apphost'; import appHost from 'apphost';
import appSettings from 'appSettings'; import appSettings from 'appSettings';
import focusManager from 'focusManager'; import focusManager from 'focusManager';
import layoutManager from 'layoutManager';
import loading from 'loading'; import loading from 'loading';
import connectionManager from 'connectionManager'; import connectionManager from 'connectionManager';
import subtitleAppearanceHelper from 'subtitleAppearanceHelper'; import subtitleAppearanceHelper from 'subtitleAppearanceHelper';
@ -10,9 +11,11 @@ import dom from 'dom';
import events from 'events'; import events from 'events';
import 'listViewStyle'; import 'listViewStyle';
import 'emby-select'; import 'emby-select';
import 'emby-slider';
import 'emby-input'; import 'emby-input';
import 'emby-checkbox'; import 'emby-checkbox';
import 'flexStyles'; import 'flexStyles';
import 'css!./subtitlesettings';
/** /**
* Subtitle settings. * Subtitle settings.
@ -27,6 +30,7 @@ function getSubtitleAppearanceObject(context) {
appearanceSettings.font = context.querySelector('#selectFont').value; appearanceSettings.font = context.querySelector('#selectFont').value;
appearanceSettings.textBackground = context.querySelector('#inputTextBackground').value; appearanceSettings.textBackground = context.querySelector('#inputTextBackground').value;
appearanceSettings.textColor = context.querySelector('#inputTextColor').value; appearanceSettings.textColor = context.querySelector('#inputTextColor').value;
appearanceSettings.verticalPosition = context.querySelector('#sliderVerticalPosition').value;
return appearanceSettings; return appearanceSettings;
} }
@ -51,6 +55,7 @@ function loadForm(context, user, userSettings, appearanceSettings, apiClient) {
context.querySelector('#inputTextBackground').value = appearanceSettings.textBackground || 'transparent'; context.querySelector('#inputTextBackground').value = appearanceSettings.textBackground || 'transparent';
context.querySelector('#inputTextColor').value = appearanceSettings.textColor || '#ffffff'; context.querySelector('#inputTextColor').value = appearanceSettings.textColor || '#ffffff';
context.querySelector('#selectFont').value = appearanceSettings.font || ''; context.querySelector('#selectFont').value = appearanceSettings.font || '';
context.querySelector('#sliderVerticalPosition').value = appearanceSettings.verticalPosition;
context.querySelector('#selectSubtitleBurnIn').value = appSettings.get('subtitleburnin') || ''; context.querySelector('#selectSubtitleBurnIn').value = appSettings.get('subtitleburnin') || '';
@ -112,10 +117,45 @@ function onAppearanceFieldChange(e) {
let elements = { let elements = {
window: view.querySelector('.subtitleappearance-preview-window'), window: view.querySelector('.subtitleappearance-preview-window'),
text: view.querySelector('.subtitleappearance-preview-text') text: view.querySelector('.subtitleappearance-preview-text'),
preview: true
}; };
subtitleAppearanceHelper.applyStyles(elements, appearanceSettings); subtitleAppearanceHelper.applyStyles(elements, appearanceSettings);
subtitleAppearanceHelper.applyStyles({
window: view.querySelector('.subtitleappearance-fullpreview-window'),
text: view.querySelector('.subtitleappearance-fullpreview-text')
}, appearanceSettings);
}
const subtitlePreviewDelay = 1000;
let subtitlePreviewTimer;
function showSubtitlePreview(persistent) {
clearTimeout(subtitlePreviewTimer);
this._fullPreview.classList.remove('subtitleappearance-fullpreview-hide');
if (persistent) {
this._refFullPreview++;
}
if (this._refFullPreview === 0) {
subtitlePreviewTimer = setTimeout(hideSubtitlePreview.bind(this), subtitlePreviewDelay);
}
}
function hideSubtitlePreview(persistent) {
clearTimeout(subtitlePreviewTimer);
if (persistent) {
this._refFullPreview--;
}
if (this._refFullPreview === 0) {
this._fullPreview.classList.add('subtitleappearance-fullpreview-hide');
}
} }
function embed(options, self) { function embed(options, self) {
@ -138,6 +178,36 @@ function embed(options, self) {
if (appHost.supports('subtitleappearancesettings')) { if (appHost.supports('subtitleappearancesettings')) {
options.element.querySelector('.subtitleAppearanceSection').classList.remove('hide'); options.element.querySelector('.subtitleAppearanceSection').classList.remove('hide');
self._fullPreview = options.element.querySelector('.subtitleappearance-fullpreview');
self._refFullPreview = 0;
const sliderVerticalPosition = options.element.querySelector('#sliderVerticalPosition');
sliderVerticalPosition.addEventListener('input', onAppearanceFieldChange);
sliderVerticalPosition.addEventListener('input', () => showSubtitlePreview.call(self));
const eventPrefix = window.PointerEvent ? 'pointer' : 'mouse';
sliderVerticalPosition.addEventListener(`${eventPrefix}enter`, () => showSubtitlePreview.call(self, true));
sliderVerticalPosition.addEventListener(`${eventPrefix}leave`, () => hideSubtitlePreview.call(self, true));
if (layoutManager.tv) {
sliderVerticalPosition.addEventListener('focus', () => showSubtitlePreview.call(self, true));
sliderVerticalPosition.addEventListener('blur', () => hideSubtitlePreview.call(self, true));
// Give CustomElements time to attach
setTimeout(() => {
sliderVerticalPosition.classList.add('focusable');
sliderVerticalPosition.enableKeyboardDragging();
}, 0);
}
options.element.querySelector('.chkPreview').addEventListener('change', (e) => {
if (e.target.checked) {
showSubtitlePreview.call(self, true);
} else {
hideSubtitlePreview.call(self, true);
}
});
} }
self.loadData(); self.loadData();

View File

@ -38,6 +38,16 @@
${HeaderSubtitleAppearance} ${HeaderSubtitleAppearance}
</h2> </h2>
<div class="subtitleappearance-fullpreview subtitleappearance-fullpreview-hide">
<div class="subtitleappearance-fullpreview-window">
<div class="subtitleappearance-fullpreview-text">
${HeaderSubtitleAppearance}
<br>
${TheseSettingsAffectSubtitlesOnThisDevice}
</div>
</div>
</div>
<div style="margin: 2em 0 2em;"> <div style="margin: 2em 0 2em;">
<div class="subtitleappearance-preview flex align-items-center justify-content-center" style="margin:2em 0;padding:1.6em;color:black;background:linear-gradient(140deg,#aa5cc3,#00a4dc);"> <div class="subtitleappearance-preview flex align-items-center justify-content-center" style="margin:2em 0;padding:1.6em;color:black;background:linear-gradient(140deg,#aa5cc3,#00a4dc);">
<div class="subtitleappearance-preview-window flex align-items-center justify-content-center" style="width: 90%; padding: .25em;"> <div class="subtitleappearance-preview-window flex align-items-center justify-content-center" style="width: 90%; padding: .25em;">
@ -89,6 +99,20 @@
<option value="">${DropShadow}</option> <option value="">${DropShadow}</option>
</select> </select>
</div> </div>
<div class="sliderContainer-settings">
<div class="sliderContainer">
<input is="emby-slider" id="sliderVerticalPosition" label="${LabelSubtitleVerticalPosition}" type="range" min="-16" max="16" />
</div>
<div class="fieldDescription">${SubtitleVerticalPositionHelp}</div>
</div>
<div class="checkboxContainer">
<label>
<input is="emby-checkbox" type="checkbox" class="chkPreview" />
<span>${Preview}</span>
</label>
</div>
</div> </div>
<button is="emby-button" type="submit" class="raised button-submit block btnSave hide"> <button is="emby-button" type="submit" class="raised button-submit block btnSave hide">

View File

@ -230,3 +230,18 @@
margin: 0; margin: 0;
padding: 0.5em 0.75em; padding: 0.5em 0.75em;
} }
/* FIXME: 'sliderContainer' is used to wrap slider's pieces */
.sliderContainer-settings {
margin-bottom: 1.8em;
position: relative;
}
.sliderContainer-settings .mdl-slider-container {
height: 2.83em; /* similar to emby-input with its 110% font-size */
}
.sliderLabel {
display: block;
margin-bottom: 0.25em;
}

View File

@ -150,6 +150,16 @@ import 'emby-input';
this.classList.add('show-focus'); this.classList.add('show-focus');
} }
const topContainer = dom.parentWithClass(this, 'sliderContainer-settings');
if (topContainer && this.getAttribute('label')) {
const label = this.ownerDocument.createElement('label');
label.innerHTML = this.getAttribute('label');
label.classList.add('sliderLabel');
label.htmlFor = this.id;
topContainer.insertBefore(label, topContainer.firstChild);
}
const containerElement = this.parentNode; const containerElement = this.parentNode;
containerElement.classList.add('mdl-slider-container'); containerElement.classList.add('mdl-slider-container');

View File

@ -1132,7 +1132,7 @@ function tryRemoveElement(elem) {
*/ */
getCueCss(appearance, selector) { getCueCss(appearance, selector) {
return `${selector}::cue { return `${selector}::cue {
${appearance.text.map((s) => `${s.name}:${s.value}!important;`).join('')} ${appearance.text.map((s) => s.value !== undefined && s.value !== '' ? `${s.name}:${s.value}!important;` : '').join('')}
}`; }`;
} }
@ -1150,7 +1150,7 @@ function tryRemoveElement(elem) {
document.getElementsByTagName('head')[0].appendChild(styleElem); document.getElementsByTagName('head')[0].appendChild(styleElem);
} }
styleElem.innerHTML = this.getCueCss(subtitleAppearanceHelper.getStyles(userSettings.getSubtitleAppearanceSettings(), true), '.htmlvideoplayer'); styleElem.innerHTML = this.getCueCss(subtitleAppearanceHelper.getStyles(userSettings.getSubtitleAppearanceSettings()), '.htmlvideoplayer');
}); });
} }
@ -1195,17 +1195,28 @@ function tryRemoveElement(elem) {
// download the track json // download the track json
this.fetchSubtitles(track, item).then(function (data) { this.fetchSubtitles(track, item).then(function (data) {
// show in ui import('userSettings').then((userSettings) => {
console.debug(`downloaded ${data.TrackEvents.length} track events`); // show in ui
// add some cues to show the text console.debug(`downloaded ${data.TrackEvents.length} track events`);
// in safari, the cues need to be added before setting the track mode to showing
for (const trackEvent of data.TrackEvents) {
const trackCueObject = window.VTTCue || window.TextTrackCue;
const cue = new trackCueObject(trackEvent.StartPositionTicks / 10000000, trackEvent.EndPositionTicks / 10000000, normalizeTrackEventText(trackEvent.Text, false));
trackElement.addCue(cue); const subtitleAppearance = userSettings.getSubtitleAppearanceSettings();
} const cueLine = parseInt(subtitleAppearance.verticalPosition, 10);
trackElement.mode = 'showing';
// add some cues to show the text
// in safari, the cues need to be added before setting the track mode to showing
for (const trackEvent of data.TrackEvents) {
const trackCueObject = window.VTTCue || window.TextTrackCue;
const cue = new trackCueObject(trackEvent.StartPositionTicks / 10000000, trackEvent.EndPositionTicks / 10000000, normalizeTrackEventText(trackEvent.Text, false));
if (cue.line === 'auto') {
cue.line = cueLine;
}
trackElement.addCue(cue);
}
trackElement.mode = 'showing';
});
}); });
} }

View File

@ -33,16 +33,22 @@ video::-webkit-media-controls {
text-shadow: 0.14em 0.14em 0.14em rgba(0, 0, 0, 1); text-shadow: 0.14em 0.14em 0.14em rgba(0, 0, 0, 1);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
font-family: inherit; font-family: inherit;
line-height: normal; /* Restore value. See -webkit-media-text-track-container 'line-height' */
} }
.htmlvideoplayer-moveupsubtitles::-webkit-media-text-track-display { .htmlvideoplayer::-webkit-media-text-track-container {
/* style the text itself */ font-size: 170% !important; /* Override element inline style */
margin-top: -2em; line-height: 50%; /* Child element cannot set line height smaller than its parent has. This allow smaller values for children */
}
.htmlvideoplayer::-webkit-media-text-track-display {
max-width: 70%;
margin-left: 15%;
} }
.videoSubtitles { .videoSubtitles {
position: fixed; position: fixed;
bottom: 10%; bottom: 0;
text-align: center; text-align: center;
left: 0; left: 0;
right: 0; right: 0;
@ -53,7 +59,6 @@ video::-webkit-media-controls {
.videoSubtitlesInner { .videoSubtitlesInner {
max-width: 70%; max-width: 70%;
background-color: rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.8);
padding: 0.25em;
margin: auto; margin: auto;
display: inline-block; display: inline-block;
} }

View File

@ -15,6 +15,10 @@ function saveServerPreferences(instance) {
instance.saveTimeout = setTimeout(onSaveTimeout.bind(instance), 50); instance.saveTimeout = setTimeout(onSaveTimeout.bind(instance), 50);
} }
const defaultSubtitleAppearanceSettings = {
verticalPosition: -3
};
export class UserSettings { export class UserSettings {
constructor() { constructor() {
} }
@ -412,7 +416,7 @@ export class UserSettings {
*/ */
getSubtitleAppearanceSettings(key) { getSubtitleAppearanceSettings(key) {
key = key || 'localplayersubtitleappearance3'; key = key || 'localplayersubtitleappearance3';
return JSON.parse(this.get(key, false) || '{}'); return Object.assign(defaultSubtitleAppearanceSettings, JSON.parse(this.get(key, false) || '{}'));
} }
/** /**

View File

@ -1542,5 +1542,8 @@
"ButtonCast": "Cast", "ButtonCast": "Cast",
"ButtonPlayer": "Player", "ButtonPlayer": "Player",
"StopPlayback": "Stop playback", "StopPlayback": "Stop playback",
"ClearQueue": "Clear queue" "ClearQueue": "Clear queue",
"LabelSubtitleVerticalPosition": "Vertical position:",
"SubtitleVerticalPositionHelp": "Line number where text appears. Positive numbers indicate top down. Negative numbers indicate bottom up.",
"Preview": "Preview"
} }

View File

@ -1542,5 +1542,8 @@
"ButtonPlayer": "Проигрыватель", "ButtonPlayer": "Проигрыватель",
"PreviousTrack": "Перейти к предыдущему", "PreviousTrack": "Перейти к предыдущему",
"NextTrack": "Перейти к следующему", "NextTrack": "Перейти к следующему",
"LabelUnstable": "Нестабильная" "LabelUnstable": "Нестабильная",
"LabelSubtitleVerticalPosition": "Вертикальная позиция:",
"SubtitleVerticalPositionHelp": "Номер строки, где появляется текст. Положительные числа означают сверху вниз. Отрицательные числа означают снизу вверх.",
"Preview": "Предварительный просмотр"
} }