diff --git a/.eslintignore b/.eslintignore
index 8e3aee83fb..74b18ddcf6 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -2,4 +2,3 @@ node_modules
dist
.idea
.vscode
-src/libraries
diff --git a/package.json b/package.json
index d1c45ef2b5..79bb6170f2 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"babel-loader": "^8.0.6",
"browser-sync": "^2.26.12",
"copy-webpack-plugin": "^5.1.1",
- "css-loader": "^4.2.0",
+ "css-loader": "^4.2.1",
"cssnano": "^4.1.10",
"del": "^5.1.0",
"eslint": "^7.6.0",
@@ -170,6 +170,7 @@
"src/components/syncPlay/syncPlayManager.js",
"src/components/syncPlay/timeSyncManager.js",
"src/components/tabbedview/tabbedview.js",
+ "src/components/viewManager/viewManager.js",
"src/components/tvproviders/schedulesdirect.js",
"src/components/tvproviders/xmltv.js",
"src/components/toast/toast.js",
@@ -277,6 +278,8 @@
"src/elements/emby-tabs/emby-tabs.js",
"src/elements/emby-textarea/emby-textarea.js",
"src/elements/emby-toggle/emby-toggle.js",
+ "src/libraries/navdrawer/navdrawer.js",
+ "src/libraries/scroller.js",
"src/plugins/backdropScreensaver/plugin.js",
"src/plugins/bookPlayer/plugin.js",
"src/plugins/bookPlayer/tableOfContents.js",
@@ -303,10 +306,13 @@
"src/scripts/mouseManager.js",
"src/scripts/multiDownload.js",
"src/scripts/playlists.js",
+ "src/scripts/scrollHelper.js",
+ "src/scripts/serverNotifications.js",
"src/scripts/routes.js",
"src/scripts/settings/appSettings.js",
"src/scripts/settings/userSettings.js",
"src/scripts/settings/webSettings.js",
+ "src/scripts/shell.js",
"src/scripts/taskbutton.js",
"src/scripts/themeLoader.js",
"src/scripts/touchHelper.js"
diff --git a/src/assets/css/librarybrowser.css b/src/assets/css/librarybrowser.css
index 047ae0a1c6..643fb9ca97 100644
--- a/src/assets/css/librarybrowser.css
+++ b/src/assets/css/librarybrowser.css
@@ -236,12 +236,6 @@
text-align: center;
}
-.layout-desktop .searchTabButton,
-.layout-mobile .searchTabButton,
-.layout-tv .headerSearchButton {
- display: none !important;
-}
-
.mainDrawer-scrollContainer {
padding-bottom: 10vh;
}
diff --git a/src/components/appRouter.js b/src/components/appRouter.js
index da3b08317c..e7b697daf4 100644
--- a/src/components/appRouter.js
+++ b/src/components/appRouter.js
@@ -1,6 +1,7 @@
define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdrop', 'browser', 'page', 'appSettings', 'apphost', 'connectionManager'], function (loading, globalize, events, viewManager, skinManager, backdrop, browser, page, appSettings, appHost, connectionManager) {
'use strict';
+ viewManager = viewManager.default || viewManager;
browser = browser.default || browser;
loading = loading.default || loading;
diff --git a/src/components/collectionEditor/collectionEditor.js b/src/components/collectionEditor/collectionEditor.js
index a115b86a8f..dd8b3d6837 100644
--- a/src/components/collectionEditor/collectionEditor.js
+++ b/src/components/collectionEditor/collectionEditor.js
@@ -200,7 +200,7 @@ import 'flexStyles';
}
function centerFocus(elem, horiz, on) {
- import('scrollHelper').then(scrollHelper => {
+ import('scrollHelper').then((scrollHelper) => {
const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
diff --git a/src/components/dialogHelper/dialogHelper.js b/src/components/dialogHelper/dialogHelper.js
index 1f11d8a195..77643791ad 100644
--- a/src/components/dialogHelper/dialogHelper.js
+++ b/src/components/dialogHelper/dialogHelper.js
@@ -354,7 +354,7 @@ import 'scrollStyles';
}
function centerFocus(elem, horiz, on) {
- import('scrollHelper').then(scrollHelper => {
+ import('scrollHelper').then((scrollHelper) => {
const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
diff --git a/src/components/filtermenu/filtermenu.js b/src/components/filtermenu/filtermenu.js
index b02b5fb9f8..37fb66e0d9 100644
--- a/src/components/filtermenu/filtermenu.js
+++ b/src/components/filtermenu/filtermenu.js
@@ -151,6 +151,7 @@ define(['require', 'dom', 'focusManager', 'dialogHelper', 'loading', 'apphost',
function centerFocus(elem, horiz, on) {
require(['scrollHelper'], function (scrollHelper) {
+ scrollHelper = scrollHelper.default || scrollHelper;
var fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
diff --git a/src/components/guide/guide-settings.js b/src/components/guide/guide-settings.js
index c3ba49f283..a644c9c9b0 100644
--- a/src/components/guide/guide-settings.js
+++ b/src/components/guide/guide-settings.js
@@ -1,6 +1,8 @@
define(['dialogHelper', 'globalize', 'userSettings', 'layoutManager', 'connectionManager', 'require', 'loading', 'scrollHelper', 'emby-checkbox', 'emby-radio', 'css!./../formdialog', 'material-icons'], function (dialogHelper, globalize, userSettings, layoutManager, connectionManager, require, loading, scrollHelper) {
'use strict';
+ scrollHelper = scrollHelper.default || scrollHelper;
+
function saveCategories(context, options) {
var categories = [];
diff --git a/src/components/guide/guide.js b/src/components/guide/guide.js
index 71d63f82c0..05fa2b608d 100644
--- a/src/components/guide/guide.js
+++ b/src/components/guide/guide.js
@@ -5,6 +5,8 @@ define(['require', 'inputManager', 'browser', 'globalize', 'connectionManager',
browser = browser.default || browser;
loading = loading.default || loading;
focusManager = focusManager.default || focusManager;
+ scrollHelper = scrollHelper.default || scrollHelper;
+ serverNotifications = serverNotifications.default || serverNotifications;
function showViewSettings(instance) {
require(['guide-settings-dialog'], function (guideSettingsDialog) {
diff --git a/src/components/itemsrefresher.js b/src/components/itemsrefresher.js
index 74b08db07f..5ce9a3b6e4 100644
--- a/src/components/itemsrefresher.js
+++ b/src/components/itemsrefresher.js
@@ -1,6 +1,7 @@
define(['playbackManager', 'serverNotifications', 'events'], function (playbackManager, serverNotifications, events) {
'use strict';
+ serverNotifications = serverNotifications.default || serverNotifications;
playbackManager = playbackManager.default || playbackManager;
function onUserDataChanged(e, apiClient, userData) {
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index 0bf270f2a1..289c784bd9 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -2,6 +2,7 @@ define(['serverNotifications', 'playbackManager', 'events', 'globalize', 'requir
'use strict';
playbackManager = playbackManager.default || playbackManager;
+ serverNotifications = serverNotifications.default || serverNotifications;
function onOneDocumentClick() {
document.removeEventListener('click', onOneDocumentClick);
diff --git a/src/components/playlisteditor/playlisteditor.js b/src/components/playlisteditor/playlisteditor.js
index 7b1e915e1f..78f0cf3dec 100644
--- a/src/components/playlisteditor/playlisteditor.js
+++ b/src/components/playlisteditor/playlisteditor.js
@@ -210,7 +210,7 @@ import 'emby-button';
}
function centerFocus(elem, horiz, on) {
- import('scrollHelper').then(scrollHelper => {
+ import('scrollHelper').then((scrollHelper) => {
const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
diff --git a/src/components/recordingcreator/recordingcreator.js b/src/components/recordingcreator/recordingcreator.js
index ca5c475829..9c2d8fcb68 100644
--- a/src/components/recordingcreator/recordingcreator.js
+++ b/src/components/recordingcreator/recordingcreator.js
@@ -1,6 +1,8 @@
define(['dialogHelper', 'globalize', 'layoutManager', 'mediaInfo', 'apphost', 'connectionManager', 'require', 'loading', 'scrollHelper', 'datetime', 'imageLoader', 'recordingFields', 'events', 'emby-checkbox', 'emby-button', 'emby-collapse', 'emby-input', 'paper-icon-button-light', 'css!./../formdialog', 'css!./recordingcreator', 'material-icons'], function (dialogHelper, globalize, layoutManager, mediaInfo, appHost, connectionManager, require, loading, scrollHelper, datetime, imageLoader, recordingFields, events) {
'use strict';
+ scrollHelper = scrollHelper.default || scrollHelper;
+
var currentDialog;
var closeAction;
var currentRecordingFields;
diff --git a/src/components/recordingcreator/recordingeditor.js b/src/components/recordingcreator/recordingeditor.js
index 2e54b3601c..7117778eb0 100644
--- a/src/components/recordingcreator/recordingeditor.js
+++ b/src/components/recordingcreator/recordingeditor.js
@@ -1,6 +1,7 @@
define(['dialogHelper', 'globalize', 'layoutManager', 'mediaInfo', 'apphost', 'connectionManager', 'require', 'loading', 'scrollHelper', 'imageLoader', 'scrollStyles', 'emby-button', 'emby-collapse', 'emby-input', 'paper-icon-button-light', 'css!./../formdialog', 'css!./recordingcreator', 'material-icons', 'flexStyles'], function (dialogHelper, globalize, layoutManager, mediaInfo, appHost, connectionManager, require, loading, scrollHelper, imageLoader) {
'use strict';
+ scrollHelper = scrollHelper.default || scrollHelper;
loading = loading.default || loading;
var currentDialog;
diff --git a/src/components/recordingcreator/recordingfields.js b/src/components/recordingcreator/recordingfields.js
index 741570581e..c93200d053 100644
--- a/src/components/recordingcreator/recordingfields.js
+++ b/src/components/recordingcreator/recordingfields.js
@@ -1,6 +1,7 @@
define(['globalize', 'connectionManager', 'serverNotifications', 'require', 'loading', 'apphost', 'dom', 'recordingHelper', 'events', 'paper-icon-button-light', 'emby-button', 'css!./recordingfields', 'flexStyles'], function (globalize, connectionManager, serverNotifications, require, loading, appHost, dom, recordingHelper, events) {
'use strict';
+ serverNotifications = serverNotifications.default || serverNotifications;
recordingHelper = recordingHelper.default || recordingHelper;
loading = loading.default || loading;
diff --git a/src/components/sortmenu/sortmenu.js b/src/components/sortmenu/sortmenu.js
index 52a7b95c40..d1cea0c490 100644
--- a/src/components/sortmenu/sortmenu.js
+++ b/src/components/sortmenu/sortmenu.js
@@ -17,6 +17,7 @@ define(['require', 'dom', 'focusManager', 'dialogHelper', 'loading', 'layoutMana
function centerFocus(elem, horiz, on) {
require(['scrollHelper'], function (scrollHelper) {
+ scrollHelper = scrollHelper.default || scrollHelper;
var fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
diff --git a/src/components/subtitleeditor/subtitleeditor.js b/src/components/subtitleeditor/subtitleeditor.js
index c42658b2d5..b52b911cb9 100644
--- a/src/components/subtitleeditor/subtitleeditor.js
+++ b/src/components/subtitleeditor/subtitleeditor.js
@@ -337,6 +337,7 @@ define(['dialogHelper', 'require', 'layoutManager', 'globalize', 'userSettings',
function centerFocus(elem, horiz, on) {
require(['scrollHelper'], function (scrollHelper) {
+ scrollHelper = scrollHelper.default || scrollHelper;
var fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
diff --git a/src/components/subtitlesettings/subtitleappearancehelper.js b/src/components/subtitlesettings/subtitleappearancehelper.js
index 7e3e2de7ac..904c018bfc 100644
--- a/src/components/subtitlesettings/subtitleappearancehelper.js
+++ b/src/components/subtitlesettings/subtitleappearancehelper.js
@@ -3,52 +3,29 @@
* @module components/subtitleSettings/subtitleAppearanceHelper
*/
-function getTextStyles(settings, isCue) {
+function getTextStyles(settings, preview) {
let list = [];
- if (isCue) {
- switch (settings.textSize || '') {
- case 'smaller':
- list.push({ name: 'font-size', value: '.5em' });
- break;
- case 'small':
- list.push({ name: 'font-size', value: '.7em' });
- break;
- case 'large':
- list.push({ name: 'font-size', value: '1.3em' });
- break;
- case 'larger':
- list.push({ name: 'font-size', value: '1.72em' });
- break;
- case 'extralarge':
- list.push({ name: 'font-size', value: '2em' });
- break;
- default:
- case 'medium':
- 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.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 || '') {
@@ -111,13 +88,43 @@ function getTextStyles(settings, isCue) {
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;
}
-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 {
- text: getTextStyles(settings, isCue),
- window: []
+ text: getTextStyles(settings, preview),
+ window: getWindowStyles(settings, preview)
};
}
@@ -130,7 +137,7 @@ function applyStyleList(styles, elem) {
}
export function applyStyles(elements, appearanceSettings) {
- let styles = getStyles(appearanceSettings);
+ let styles = getStyles(appearanceSettings, !!elements.preview);
if (elements.text) {
applyStyleList(styles.text, elements.text);
diff --git a/src/components/subtitlesettings/subtitlesettings.css b/src/components/subtitlesettings/subtitlesettings.css
new file mode 100644
index 0000000000..204757f10f
--- /dev/null
+++ b/src/components/subtitlesettings/subtitlesettings.css
@@ -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%;
+}
diff --git a/src/components/subtitlesettings/subtitlesettings.js b/src/components/subtitlesettings/subtitlesettings.js
index 79fa289ab0..12e230b1e7 100644
--- a/src/components/subtitlesettings/subtitlesettings.js
+++ b/src/components/subtitlesettings/subtitlesettings.js
@@ -2,6 +2,7 @@ import globalize from 'globalize';
import appHost from 'apphost';
import appSettings from 'appSettings';
import focusManager from 'focusManager';
+import layoutManager from 'layoutManager';
import loading from 'loading';
import connectionManager from 'connectionManager';
import subtitleAppearanceHelper from 'subtitleAppearanceHelper';
@@ -10,9 +11,11 @@ import dom from 'dom';
import events from 'events';
import 'listViewStyle';
import 'emby-select';
+import 'emby-slider';
import 'emby-input';
import 'emby-checkbox';
import 'flexStyles';
+import 'css!./subtitlesettings';
/**
* Subtitle settings.
@@ -27,6 +30,7 @@ function getSubtitleAppearanceObject(context) {
appearanceSettings.font = context.querySelector('#selectFont').value;
appearanceSettings.textBackground = context.querySelector('#inputTextBackground').value;
appearanceSettings.textColor = context.querySelector('#inputTextColor').value;
+ appearanceSettings.verticalPosition = context.querySelector('#sliderVerticalPosition').value;
return appearanceSettings;
}
@@ -51,6 +55,7 @@ function loadForm(context, user, userSettings, appearanceSettings, apiClient) {
context.querySelector('#inputTextBackground').value = appearanceSettings.textBackground || 'transparent';
context.querySelector('#inputTextColor').value = appearanceSettings.textColor || '#ffffff';
context.querySelector('#selectFont').value = appearanceSettings.font || '';
+ context.querySelector('#sliderVerticalPosition').value = appearanceSettings.verticalPosition;
context.querySelector('#selectSubtitleBurnIn').value = appSettings.get('subtitleburnin') || '';
@@ -112,10 +117,45 @@ function onAppearanceFieldChange(e) {
let elements = {
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({
+ 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) {
@@ -138,6 +178,36 @@ function embed(options, self) {
if (appHost.supports('subtitleappearancesettings')) {
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();
diff --git a/src/components/subtitlesettings/subtitlesettings.template.html b/src/components/subtitlesettings/subtitlesettings.template.html
index 716296a257..af9139188c 100644
--- a/src/components/subtitlesettings/subtitlesettings.template.html
+++ b/src/components/subtitlesettings/subtitlesettings.template.html
@@ -38,6 +38,16 @@
${HeaderSubtitleAppearance}
+
+
+
+ ${HeaderSubtitleAppearance}
+
+ ${TheseSettingsAffectSubtitlesOnThisDevice}
+
+
+
+
@@ -89,6 +99,20 @@
+
+
+
+
+
+
${SubtitleVerticalPositionHelp}
+
+
+
+
+