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}
+
+ +
+ +