From 94c405f08e0cb3456a5862bea17af6985482f587 Mon Sep 17 00:00:00 2001 From: Ionut Andrei Oanca Date: Wed, 14 Oct 2020 20:40:46 +0100 Subject: [PATCH 01/13] Implement basic SyncPlay settings --- src/assets/css/dashboard.scss | 6 + src/components/syncPlay/core/Manager.js | 1 + src/components/syncPlay/core/PlaybackCore.js | 41 ++- src/components/syncPlay/core/Settings.js | 84 ++++++ src/components/syncPlay/core/index.js | 2 + .../syncPlay/core/timeSync/TimeSyncCore.js | 63 ++++- .../syncPlay/ui/groupSelectionMenu.js | 13 + .../syncPlay/ui/settings/SettingsEditor.js | 245 ++++++++++++++++++ .../syncPlay/ui/settings/advancedTab.html | 29 +++ .../syncPlay/ui/settings/editor.html | 21 ++ .../syncPlay/ui/settings/localTab.html | 29 +++ src/elements/emby-checkbox/emby-checkbox.js | 4 +- src/elements/emby-checkbox/emby-checkbox.scss | 11 + src/strings/en-us.json | 28 ++ src/themes/appletv/theme.css | 9 + src/themes/blueradiance/theme.css | 9 + src/themes/dark/theme.css | 9 + src/themes/light/theme.css | 9 + src/themes/purplehaze/theme.css | 13 + src/themes/wmc/theme.css | 9 + 20 files changed, 618 insertions(+), 17 deletions(-) create mode 100644 src/components/syncPlay/core/Settings.js create mode 100644 src/components/syncPlay/ui/settings/SettingsEditor.js create mode 100644 src/components/syncPlay/ui/settings/advancedTab.html create mode 100644 src/components/syncPlay/ui/settings/editor.html create mode 100644 src/components/syncPlay/ui/settings/localTab.html diff --git a/src/assets/css/dashboard.scss b/src/assets/css/dashboard.scss index a5209d2604..113774908a 100644 --- a/src/assets/css/dashboard.scss +++ b/src/assets/css/dashboard.scss @@ -77,6 +77,7 @@ progress[aria-valuenow]::before { height: 4em; } +.controlGroupButton, a[data-role=button] { background: #292929 !important; background-clip: padding-box; @@ -93,6 +94,7 @@ a[data-role=button] { text-decoration: none !important; } +div[data-role=controlgroup] .controlGroupButton, div[data-role=controlgroup] a[data-role=button] { display: inline-block !important; margin: 0 !important; @@ -102,6 +104,7 @@ div[data-role=controlgroup] a[data-role=button] { border-radius: 0; } +div[data-role=controlgroup] .controlGroupButton:first-child, div[data-role=controlgroup] a[data-role=button]:first-child { -webkit-border-bottom-left-radius: 0.3125em; border-bottom-left-radius: 0.3125em; @@ -109,6 +112,7 @@ div[data-role=controlgroup] a[data-role=button]:first-child { border-top-left-radius: 0.3125em; } +div[data-role=controlgroup] .controlGroupButton:last-child, div[data-role=controlgroup] a[data-role=button]:last-child { -webkit-border-bottom-right-radius: 0.3125em; border-bottom-right-radius: 0.3125em; @@ -116,11 +120,13 @@ div[data-role=controlgroup] a[data-role=button]:last-child { border-top-right-radius: 0.3125em; } +div[data-role=controlgroup] .controlGroupButton + .controlGroupButton, div[data-role=controlgroup] a[data-role=button] + a[data-role=button] { border-left-width: 0 !important; margin: 0 0 0 -0.4em !important; } +div[data-role=controlgroup] .controlGroupButton.ui-btn-active, div[data-role=controlgroup] a.ui-btn-active { background: #00a4dc !important; color: #292929 !important; diff --git a/src/components/syncPlay/core/Manager.js b/src/components/syncPlay/core/Manager.js index 70513b3b35..f74813fe7d 100644 --- a/src/components/syncPlay/core/Manager.js +++ b/src/components/syncPlay/core/Manager.js @@ -5,6 +5,7 @@ import { Events } from 'jellyfin-apiclient'; import * as Helper from './Helper'; +import Settings from './Settings'; import TimeSyncCore from './timeSync/TimeSyncCore'; import PlaybackCore from './PlaybackCore'; import QueueCore from './QueueCore'; diff --git a/src/components/syncPlay/core/PlaybackCore.js b/src/components/syncPlay/core/PlaybackCore.js index 2cab13e784..6396253e58 100644 --- a/src/components/syncPlay/core/PlaybackCore.js +++ b/src/components/syncPlay/core/PlaybackCore.js @@ -5,6 +5,7 @@ import { Events } from 'jellyfin-apiclient'; import * as Helper from './Helper'; +import Settings from './Settings'; /** * Class that manages the playback of SyncPlay. @@ -25,6 +26,8 @@ class PlaybackCore { this.lastCommand = null; // Last scheduled playback command, might not be the latest one. this.scheduledCommandTimeout = null; this.syncTimeout = null; + + this.loadPreferences(); } /** @@ -35,26 +38,35 @@ class PlaybackCore { this.manager = syncPlayManager; this.timeSyncCore = syncPlayManager.getTimeSyncCore(); + Events.on(Settings, 'update', (event) => { + this.loadPreferences(); + }); + } + + /** + * Loads preferences from saved settings. + */ + loadPreferences() { // Minimum required delay for SpeedToSync to kick in, in milliseconds. - this.minDelaySpeedToSync = 60.0; + this.minDelaySpeedToSync = Settings.getFloat('minDelaySpeedToSync', 60.0); // Maximum delay after which SkipToSync is used instead of SpeedToSync, in milliseconds. - this.maxDelaySpeedToSync = 3000.0; + this.maxDelaySpeedToSync = Settings.getFloat('maxDelaySpeedToSync', 3000.0); // Time during which the playback is sped up, in milliseconds. - this.speedToSyncDuration = 1000.0; + this.speedToSyncDuration = Settings.getFloat('speedToSyncDuration', 1000.0); // Minimum required delay for SkipToSync to kick in, in milliseconds. - this.minDelaySkipToSync = 400.0; + this.minDelaySkipToSync = Settings.getFloat('minDelaySkipToSync', 400.0); // Whether SpeedToSync should be used. - this.useSpeedToSync = true; + this.useSpeedToSync = Settings.getBool('useSpeedToSync', true); // Whether SkipToSync should be used. - this.useSkipToSync = true; + this.useSkipToSync = Settings.getBool('useSkipToSync', true); // Whether sync correction during playback is active. - this.enableSyncCorrection = true; + this.enableSyncCorrection = Settings.getBool('enableSyncCorrection', true); } /** @@ -526,7 +538,12 @@ class PlaybackCore { // Diff might be caused by the player internally starting the playback. const diffMillis = (serverPositionTicks - currentPositionTicks) / Helper.TicksPerMillisecond; + // Adapt playback diff to selected device for time syncing. + const targetPlaybackDiff = diffMillis - this.timeSyncCore.getPlaybackDiff(); + + // Notify update for playback sync. this.playbackDiffMillis = diffMillis; + Events.trigger(this.manager, 'playback-diff', [this.playbackDiffMillis]); // Avoid overloading the browser. const elapsed = currentTime - this.lastSyncTime; @@ -536,22 +553,22 @@ class PlaybackCore { const playerWrapper = this.manager.getPlayerWrapper(); if (this.syncEnabled && this.enableSyncCorrection) { - const absDiffMillis = Math.abs(diffMillis); + const absDiffMillis = Math.abs(targetPlaybackDiff); // TODO: SpeedToSync sounds bad on songs. // TODO: SpeedToSync is failing on Safari (Mojave); even if playbackRate is supported, some delay seems to exist. // TODO: both SpeedToSync and SpeedToSync seem to have a hard time keeping up on Android Chrome as well. if (playerWrapper.hasPlaybackRate() && this.useSpeedToSync && absDiffMillis >= this.minDelaySpeedToSync && absDiffMillis < this.maxDelaySpeedToSync) { // Fix negative speed when client is ahead of time more than speedToSyncTime. const MinSpeed = 0.2; - if (diffMillis <= -speedToSyncTime * MinSpeed) { - speedToSyncTime = Math.abs(diffMillis) / (1.0 - MinSpeed); + if (targetPlaybackDiff <= -speedToSyncTime * MinSpeed) { + speedToSyncTime = Math.abs(targetPlaybackDiff) / (1.0 - MinSpeed); } // SpeedToSync strategy. - const speed = 1 + diffMillis / speedToSyncTime; + const speed = 1 + targetPlaybackDiff / speedToSyncTime; if (speed <= 0) { - console.error('SyncPlay error: speed should not be negative!', speed, diffMillis, speedToSyncTime); + console.error('SyncPlay error: speed should not be negative!', speed, targetPlaybackDiff, speedToSyncTime); } playerWrapper.setPlaybackRate(speed); diff --git a/src/components/syncPlay/core/Settings.js b/src/components/syncPlay/core/Settings.js new file mode 100644 index 0000000000..6e8609b2fd --- /dev/null +++ b/src/components/syncPlay/core/Settings.js @@ -0,0 +1,84 @@ +/** + * Module that manages SyncPlay settings. + * @module components/syncPlay/core/Settings + */ + +import { Events, AppStorage } from 'jellyfin-apiclient'; + +/** + * Class that manages SyncPlay settings. + */ +class SyncPlaySettings { + constructor() { + // Do nothing + } + + /** + * Gets the key used to store a setting in the App Storage. + * @param {string} name The name of the setting. + * @returns {string} The key. + */ + getKey(name) { + return 'syncPlay-' + name; + } + + /** + * Gets the value of a setting. + * @param {string} name The name of the setting. + * @returns {string} The value. + */ + get(name) { + return AppStorage.getItem(this.getKey(name)); + } + + /** + * Sets the value of a setting. Triggers an update if the new value differs from the old one. + * @param {string} name The name of the setting. + * @param {Object} value The value of the setting. + */ + set(name, value) { + const oldValue = this.get(name); + AppStorage.setItem(this.getKey(name), value); + const newValue = this.get(name); + + if (oldValue !== newValue) { + Events.trigger(this, name, [newValue, oldValue]); + } + + console.debug(`SyncPlay Settings set: '${name}' from '${oldValue}' to '${newValue}'.`); + } + + /** + * Gets the value of a setting as boolean. + * @param {string} name The name of the setting. + * @param {boolean} defaultValue The default value if the setting does not exist. + * @returns {boolean} The value. + */ + getBool(name, defaultValue = false) { + const value = this.get(name); + if (value !== 'true' && value !== 'false') { + return defaultValue; + } else { + return this.get(name) !== 'false'; + } + } + + /** + * Gets the value of a setting as float number. + * @param {string} name The name of the setting. + * @param {number} defaultValue The default value if the setting does not exist. + * @returns {number} The value. + */ + getFloat(name, defaultValue = 0) { + const value = this.get(name); + if (value === null || value === '' || isNaN(value)) { + return defaultValue; + } else { + return Number.parseFloat(value); + } + } +} + +/** SyncPlaySettings singleton. */ +const Settings = new SyncPlaySettings(); +export default Settings; diff --git a/src/components/syncPlay/core/index.js b/src/components/syncPlay/core/index.js index ca07e9b361..964a68965f 100644 --- a/src/components/syncPlay/core/index.js +++ b/src/components/syncPlay/core/index.js @@ -1,4 +1,5 @@ import * as Helper from './Helper'; +import Settings from './Settings'; import ManagerClass from './Manager'; import PlayerFactoryClass from './players/PlayerFactory'; import GenericPlayer from './players/GenericPlayer'; @@ -8,6 +9,7 @@ const Manager = new ManagerClass(PlayerFactory); export default { Helper, + Settings, Manager, PlayerFactory, Players: { diff --git a/src/components/syncPlay/core/timeSync/TimeSyncCore.js b/src/components/syncPlay/core/timeSync/TimeSyncCore.js index a67752648d..4308d450de 100644 --- a/src/components/syncPlay/core/timeSync/TimeSyncCore.js +++ b/src/components/syncPlay/core/timeSync/TimeSyncCore.js @@ -4,6 +4,7 @@ */ import { Events } from 'jellyfin-apiclient'; +import Settings from '../Settings'; import TimeSyncServer from './TimeSyncServer'; /** @@ -13,6 +14,9 @@ class TimeSyncCore { constructor() { this.manager = null; this.timeSyncServer = null; + + this.timeSyncDeviceId = Settings.get('timeSyncDevice') || 'server'; + this.extraTimeOffset = Settings.getFloat('extraTimeOffset', 0.0); } /** @@ -31,6 +35,10 @@ class TimeSyncCore { Events.trigger(this, 'time-sync-server-update', [timeOffset, ping]); }); + + Events.on(Settings, 'extraTimeOffset', (event, value, oldValue) => { + this.extraTimeOffset = Settings.getFloat('extraTimeOffset', 0.0); + }); } /** @@ -40,6 +48,32 @@ class TimeSyncCore { this.timeSyncServer.forceUpdate(); } + /** + * Gets the list of available devices for time sync. + * @returns {Array} The list of devices. + */ + getDevices() { + const devices = [{ + type: 'server', + id: 'server', + name: 'Server', + timeOffset: this.timeSyncServer.getTimeOffset(), + ping: this.timeSyncServer.getPing(), + peerTimeOffset: 0, + peerPing: 0 + }]; + + return devices; + } + + /** + * Gets the identifier of the selected device for time sync. Default value is 'server'. + * @returns {string} The identifier. + */ + getActiveDevice() { + return this.timeSyncDeviceId; + } + /** * Gets the display name of the selected device for time sync. * @returns {string} The display name. @@ -54,7 +88,8 @@ class TimeSyncCore { * @returns {Date} Local time. */ remoteDateToLocal(remote) { - return this.timeSyncServer.remoteDateToLocal(remote); + const date = this.timeSyncServer.remoteDateToLocal(remote); + return this.offsetDate(date, -this.extraTimeOffset); } /** @@ -63,15 +98,35 @@ class TimeSyncCore { * @returns {Date} Server time. */ localDateToRemote(local) { - return this.timeSyncServer.localDateToRemote(local); + const date = this.timeSyncServer.localDateToRemote(local); + return this.offsetDate(date, this.extraTimeOffset); } /** - * Gets time offset that should be used for time syncing, in milliseconds. + * Gets time offset that should be used for time syncing, in milliseconds. Takes into account server and active device selected for syncing. * @returns {number} The time offset. */ getTimeOffset() { - return this.timeSyncServer.getTimeOffset(); + return this.timeSyncServer.getTimeOffset() + this.extraTimeOffset; + } + + /** + * Gets the playback diff that should be used to offset local playback, in milliseconds. + * @returns {number} The time offset. + */ + getPlaybackDiff() { + // TODO: this will use playback data from WebRTC peers. + return 0; + } + + /** + * Offsets a given date by a given ammount of milliseconds. + * @param {Date} date The date. + * @param {number} offset The offset, in milliseconds. + * @returns {Date} The offset date. + */ + offsetDate(date, offset) { + return new Date(date.getTime() + offset); } } diff --git a/src/components/syncPlay/ui/groupSelectionMenu.js b/src/components/syncPlay/ui/groupSelectionMenu.js index 48c1327ea7..0e0956a53a 100644 --- a/src/components/syncPlay/ui/groupSelectionMenu.js +++ b/src/components/syncPlay/ui/groupSelectionMenu.js @@ -1,5 +1,6 @@ import { Events } from 'jellyfin-apiclient'; import SyncPlay from '../core'; +import SyncPlaySettingsEditor from './settings/SettingsEditor'; import loading from '../../loading/loading'; import toast from '../../toast/toast'; import actionsheet from '../../actionSheet/actionSheet'; @@ -120,6 +121,14 @@ class GroupSelectionMenu { }); } + menuItems.push({ + name: globalize.translate('Settings'), + icon: 'video_settings', + id: 'settings', + selected: false, + secondaryText: globalize.translate('LabelSyncPlaySettingsDescription') + }); + menuItems.push({ name: globalize.translate('LabelSyncPlayLeaveGroup'), icon: 'meeting_room', @@ -144,6 +153,10 @@ class GroupSelectionMenu { SyncPlay.Manager.haltGroupPlayback(apiClient); } else if (id == 'leave-group') { apiClient.leaveSyncPlayGroup(); + } else if (id == 'settings') { + new SyncPlaySettingsEditor(apiClient, SyncPlay.Manager.getTimeSyncCore(), { + groupInfo: groupInfo + }); } }).catch((error) => { console.error('SyncPlay: unexpected error showing group menu:', error); diff --git a/src/components/syncPlay/ui/settings/SettingsEditor.js b/src/components/syncPlay/ui/settings/SettingsEditor.js new file mode 100644 index 0000000000..1906b8ed2f --- /dev/null +++ b/src/components/syncPlay/ui/settings/SettingsEditor.js @@ -0,0 +1,245 @@ +/** + * Module that displays an editor for changing SyncPlay settings. + * @module components/syncPlay/settings/SettingsEditor + */ + +import { Events } from 'jellyfin-apiclient'; +import SyncPlay from '../../core'; +import dialogHelper from '../../../dialogHelper/dialogHelper'; +import layoutManager from '../../../layoutManager'; +import loading from '../../../loading/loading'; +import toast from '../../../toast/toast'; +import globalize from '../../../../scripts/globalize'; +import 'material-design-icons-iconfont'; +import '../../../../elements/emby-input/emby-input'; +import '../../../../elements/emby-select/emby-select'; +import '../../../../elements/emby-button/emby-button'; +import '../../../../elements/emby-button/paper-icon-button-light'; +import '../../../../elements/emby-checkbox/emby-checkbox'; +import '../../../listview/listview.scss'; +import '../../../formdialog.scss'; + +function centerFocus(elem, horiz, on) { + import('../../../../scripts/scrollHelper').then((scrollHelper) => { + const fn = on ? 'on' : 'off'; + scrollHelper.centerFocus[fn](elem, horiz); + }); +} + +/** + * Class that displays an editor for changing SyncPlay settings. + */ +class SettingsEditor { + constructor(apiClient, timeSyncCore, options = {}) { + this.apiClient = apiClient; + this.timeSyncCore = timeSyncCore; + this.options = options; + + this.tabNames = []; + this.tabs = {}; + + this.embed(); + + Events.on(this.timeSyncCore, 'refresh-devices', (event) => { + this.refreshTimeSyncDevices(); + }); + + Events.on(this.timeSyncCore, 'time-sync-server-update', (event) => { + this.refreshTimeSyncDevices(); + }); + } + + insertBefore(newNode, existingNode) { + existingNode.parentNode.insertBefore(newNode, existingNode); + } + + addTab(name, tab) { + this.tabNames.push(name); + this.tabs[name] = tab; + } + + showTab(tabName) { + this.tabNames.forEach(id => { + this.tabs[id].style.display = 'none'; + this.context.querySelector('#show-' + id).classList.remove('ui-btn-active'); + }); + + const tab = this.tabs[tabName]; + if (tab) { + tab.style.display = 'block'; + this.context.querySelector('#show-' + tabName).classList.add('ui-btn-active'); + } + } + + async embed() { + const dialogOptions = { + removeOnClose: true, + scrollY: true + }; + + if (layoutManager.tv) { + dialogOptions.size = 'fullscreen'; + } else { + dialogOptions.size = 'small'; + } + + this.context = dialogHelper.createDialog(dialogOptions); + this.context.classList.add('formDialog'); + + const { default: editorTemplate } = await import('./editor.html'); + this.context.innerHTML = globalize.translateHtml(editorTemplate, 'core'); + const footer = this.context.querySelector('#footer'); + + // Create tabs + const { default: localTabTemplate } = await import('./localTab.html'); + const localTab = this.translateTemplate(localTabTemplate); + + const { default: advancedTabTemplate } = await import('./advancedTab.html'); + const advancedTab = this.translateTemplate(advancedTabTemplate); + + this.insertBefore(localTab, footer); + this.insertBefore(advancedTab, footer); + + // Switch tabs using nav + this.addTab('localTab', localTab); + this.addTab('advancedTab', advancedTab); + + this.showTab('localTab'); + + const tabButtons = this.context.querySelectorAll('.controlGroupButton'); + tabButtons.forEach(button => { + button.addEventListener('click', (event) => { + const tabName = event.target.getAttribute('data-showTab'); + if (tabName) { + this.showTab(tabName); + } + }); + }); + + // Set callbacks for form submission + this.context.querySelector('form').addEventListener('submit', (event) => { + // Disable default form submission + if (event) { + event.preventDefault(); + } + return false; + }); + + this.context.querySelector('.btnSave').addEventListener('click', () => { + this.onSubmit(); + }); + + this.context.querySelector('.btnCancel').addEventListener('click', () => { + dialogHelper.close(this.context); + }); + + await this.initEditor(); + + if (layoutManager.tv) { + centerFocus(this.context.querySelector('.formDialogContent'), false, true); + } + + return dialogHelper.open(this.context).then(() => { + if (layoutManager.tv) { + centerFocus(this.context.querySelector('.formDialogContent'), false, false); + } + + if (this.context.submitted) { + return Promise.resolve(); + } + + return Promise.reject(); + }); + } + + async initEditor() { + const { context } = this; + + context.querySelector('#txtExtraTimeOffset').value = SyncPlay.Settings.getFloat('extraTimeOffset', 0.0); + context.querySelector('#chkSyncCorrection').checked = SyncPlay.Settings.getBool('enableSyncCorrection', true); + context.querySelector('#txtMinDelaySpeedToSync').value = SyncPlay.Settings.getFloat('minDelaySpeedToSync', 60.0); + context.querySelector('#txtMaxDelaySpeedToSync').value = SyncPlay.Settings.getFloat('maxDelaySpeedToSync', 3000.0); + context.querySelector('#txtSpeedToSyncDuration').value = SyncPlay.Settings.getFloat('speedToSyncDuration', 1000.0); + context.querySelector('#txtMinDelaySkipToSync').value = SyncPlay.Settings.getFloat('minDelaySkipToSync', 400.0); + context.querySelector('#chkSpeedToSync').checked = SyncPlay.Settings.getBool('useSpeedToSync', true); + context.querySelector('#chkSkipToSync').checked = SyncPlay.Settings.getBool('useSkipToSync', true); + + this.refreshTimeSyncDevices(); + const timeSyncSelect = context.querySelector('#selectTimeSync'); + timeSyncSelect.value = this.timeSyncCore.getActiveDevice(); + this.timeSyncSelectedValue = timeSyncSelect.value; + + timeSyncSelect.addEventListener('change', () => { + this.timeSyncSelectedValue = timeSyncSelect.value; + }); + } + + refreshTimeSyncDevices() { + const { context } = this; + const timeSyncSelect = context.querySelector('#selectTimeSync'); + const devices = this.timeSyncCore.getDevices(); + + timeSyncSelect.innerHTML = devices.map(device => { + return ``; + }).join(''); + + timeSyncSelect.value = this.timeSyncSelectedValue; + } + + /** + * @param {string} html HTML string representing a single element. + * @return {Element} The element. + */ + htmlToElement(html) { + const template = document.createElement('template'); + html = html.trim(); // Avoid returning a text node of whitespace. + template.innerHTML = html; + return template.content.firstChild; + } + + translateTemplate(template) { + const translatedTemplate = globalize.translateHtml(template, 'core'); + return this.htmlToElement(translatedTemplate); + } + + onSubmit() { + this.save(); + dialogHelper.close(this.context); + } + + async save() { + loading.show(); + await this.saveToAppSettings(); + loading.hide(); + toast(globalize.translate('SettingsSaved')); + Events.trigger(this, 'saved'); + } + + async saveToAppSettings() { + const { context } = this; + + const timeSyncDevice = context.querySelector('#selectTimeSync').value; + const extraTimeOffset = context.querySelector('#txtExtraTimeOffset').value; + const syncCorrection = context.querySelector('#chkSyncCorrection').checked; + const minDelaySpeedToSync = context.querySelector('#txtMinDelaySpeedToSync').value; + const maxDelaySpeedToSync = context.querySelector('#txtMaxDelaySpeedToSync').value; + const speedToSyncDuration = context.querySelector('#txtSpeedToSyncDuration').value; + const minDelaySkipToSync = context.querySelector('#txtMinDelaySkipToSync').value; + const useSpeedToSync = context.querySelector('#chkSpeedToSync').checked; + const useSkipToSync = context.querySelector('#chkSkipToSync').checked; + + SyncPlay.Settings.set('timeSyncDevice', timeSyncDevice); + SyncPlay.Settings.set('extraTimeOffset', extraTimeOffset); + SyncPlay.Settings.set('enableSyncCorrection', syncCorrection); + SyncPlay.Settings.set('minDelaySpeedToSync', minDelaySpeedToSync); + SyncPlay.Settings.set('maxDelaySpeedToSync', maxDelaySpeedToSync); + SyncPlay.Settings.set('speedToSyncDuration', speedToSyncDuration); + SyncPlay.Settings.set('minDelaySkipToSync', minDelaySkipToSync); + SyncPlay.Settings.set('useSpeedToSync', useSpeedToSync); + SyncPlay.Settings.set('useSkipToSync', useSkipToSync); + + Events.trigger(SyncPlay.Settings, 'update'); + } +} + +export default SettingsEditor; diff --git a/src/components/syncPlay/ui/settings/advancedTab.html b/src/components/syncPlay/ui/settings/advancedTab.html new file mode 100644 index 0000000000..e853215d24 --- /dev/null +++ b/src/components/syncPlay/ui/settings/advancedTab.html @@ -0,0 +1,29 @@ +
+

${HeaderSyncPlayTimeSyncSettings}

+
+ +
${LabelSyncPlaySettingsExtraTimeOffsetHelp}
+
+

${HeaderSyncPlayPlaybackSettings}

+
+ +
${LabelSyncPlaySettingsMinDelaySpeedToSyncHelp}
+
+
+ +
${LabelSyncPlaySettingsMaxDelaySpeedToSyncHelp}
+
+
+ +
${LabelSyncPlaySettingsSpeedToSyncDurationHelp}
+
+
+ +
${LabelSyncPlaySettingsMinDelaySkipToSyncHelp}
+
+
diff --git a/src/components/syncPlay/ui/settings/editor.html b/src/components/syncPlay/ui/settings/editor.html new file mode 100644 index 0000000000..5df312c67e --- /dev/null +++ b/src/components/syncPlay/ui/settings/editor.html @@ -0,0 +1,21 @@ +
+ +

${HeaderSyncPlaySettings}

+
+
+
+
+ + +
+ +
+
diff --git a/src/components/syncPlay/ui/settings/localTab.html b/src/components/syncPlay/ui/settings/localTab.html new file mode 100644 index 0000000000..4cd3a5ea0d --- /dev/null +++ b/src/components/syncPlay/ui/settings/localTab.html @@ -0,0 +1,29 @@ +
+

${HeaderSyncPlayTimeSyncSettings}

+
+ +
${LabelSyncPlaySettingsTimeSyncHelp}
+
+

${HeaderSyncPlayPlaybackSettings}

+
+ +
${LabelSyncPlaySettingsSyncCorrectionHelp}
+
+
+ +
${LabelSyncPlaySettingsSpeedToSyncHelp}
+
+
+ +
${LabelSyncPlaySettingsSkipToSyncHelp}
+
+
diff --git a/src/elements/emby-checkbox/emby-checkbox.js b/src/elements/emby-checkbox/emby-checkbox.js index ef21ff8136..4472ce1ac3 100644 --- a/src/elements/emby-checkbox/emby-checkbox.js +++ b/src/elements/emby-checkbox/emby-checkbox.js @@ -65,7 +65,9 @@ import 'webcomponents.js/webcomponents-lite'; const uncheckedHtml = ''; labelElement.insertAdjacentHTML('beforeend', '' + checkHtml + uncheckedHtml + ''); - labelTextElement.classList.add('checkboxLabel'); + if (labelTextElement) { + labelTextElement.classList.add('checkboxLabel'); + } this.addEventListener('keydown', onKeyDown); diff --git a/src/elements/emby-checkbox/emby-checkbox.scss b/src/elements/emby-checkbox/emby-checkbox.scss index b33a216140..e839e44f61 100644 --- a/src/elements/emby-checkbox/emby-checkbox.scss +++ b/src/elements/emby-checkbox/emby-checkbox.scss @@ -22,6 +22,10 @@ display: flex; } +.checkboxContainer-noText { + margin-bottom: 0; +} + .checkboxListContainer { margin-bottom: 1.8em; } @@ -63,6 +67,10 @@ justify-content: center; } +.checkboxContainer-noText .checkboxOutline { + top: auto; +} + .checkboxIcon { font-size: 1.6em; color: #fff; @@ -73,16 +81,19 @@ display: none; } +.emby-checkbox:checked + .checkboxOutline > .checkboxIcon-checked, .emby-checkbox:checked + span + .checkboxOutline > .checkboxIcon-checked { /* background color set by theme */ display: flex !important; } +.emby-checkbox:checked + .checkboxOutline > .checkboxIcon-unchecked, .emby-checkbox:checked + span + .checkboxOutline > .checkboxIcon-unchecked { /* background color set by theme */ display: none !important; } +.emby-checkbox:checked[disabled] + .checkboxOutline > .checkboxIcon, .emby-checkbox:checked[disabled] + span + .checkboxOutline > .checkboxIcon { background-color: rgba(0, 0, 0, 0.26); } diff --git a/src/strings/en-us.json b/src/strings/en-us.json index d27f6f5500..94cebd565a 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -452,6 +452,12 @@ "HeaderSubtitleProfilesHelp": "Subtitle profiles describe the subtitle formats supported by the device.", "HeaderSyncPlayEnabled": "SyncPlay enabled", "HeaderSyncPlaySelectGroup": "Join a group", + "HeaderSyncPlaySettings": "SyncPlay Settings", + "HeaderSyncPlayGroupSettings": "Group", + "HeaderSyncPlayAdvancedSettings": "Advanced", + "HeaderSyncPlayPlaybackSettings": "Playback", + "HeaderSyncPlayLocalSettings": "Local", + "HeaderSyncPlayTimeSyncSettings": "Time sync", "HeaderSystemDlnaProfiles": "System Profiles", "HeaderTaskTriggers": "Task Triggers", "HeaderThisUserIsCurrentlyDisabled": "This user is currently disabled", @@ -866,6 +872,28 @@ "LabelSyncPlaySyncMethod": "Sync method:", "LabelSyncPlayTimeSyncDevice": "Time syncing with:", "LabelSyncPlayTimeSyncOffset": "Time offset:", + "LabelSyncPlaySettingsDescription": "Change SyncPlay preferences", + "LabelSyncPlaySettingsGroupName": "Group name:", + "LabelSyncPlaySettingsGroupNameHelp": "Change the group's name.", + "LabelSyncPlaySettingsTimeSync": "Device:", + "LabelSyncPlaySettingsTimeSyncHelp": "Pick the device to time sync with. Picking a local peer ensures less noticeable delay between devices on the same network.", + "LabelSyncPlaySettingsExtraTimeOffset": "Extra time offset:", + "LabelSyncPlaySettingsExtraTimeOffsetHelp": "Manually adjust time offset with selected device for time sync. Tweak with care.", + "LabelSyncPlaySettingsSyncCorrection": "Sync Correction", + "LabelSyncPlaySettingsSyncCorrectionHelp": "Enable active syncing of playback by either speeding up the media or by seeking to the estimated position. Disable this in case of heavy stuttering.", + "LabelSyncPlaySettingsMinDelaySpeedToSync": "SpeedToSync minimum delay:", + "LabelSyncPlaySettingsMinDelaySpeedToSyncHelp": "Minimum playback delay after which SpeedToSync attempts to correct playback position.", + "LabelSyncPlaySettingsMaxDelaySpeedToSync": "SpeedToSync maximum delay:", + "LabelSyncPlaySettingsMaxDelaySpeedToSyncHelp": "Maximum playback delay after which SkipToSync is used instead of SpeedToSync.", + "LabelSyncPlaySettingsSpeedToSyncDuration": "SpeedToSync duration:", + "LabelSyncPlaySettingsSpeedToSyncDurationHelp": "Amount of time used by SpeedToSync to correct playback position.", + "LabelSyncPlaySettingsMinDelaySkipToSync": "SkipToSync minimum delay:", + "LabelSyncPlaySettingsMinDelaySkipToSyncHelp": "Minimum playback delay after which SkipToSync attempts to correct playback position.", + "LabelSyncPlaySettingsSpeedToSync": "Enable SpeedToSync", + "LabelSyncPlaySettingsSpeedToSyncHelp": "Sync correction method that consist in speeding up the playback. Sync Correction must be enabled.", + "LabelSyncPlaySettingsSkipToSync": "Enable SkipToSync", + "LabelSyncPlaySettingsSkipToSyncHelp": "Sync correction method that consist in seeking to the estimated position. Sync Correction must be enabled.", + "LabelSyncPlaySettingsCreateGroup": "Create group", "LabelTag": "Tag:", "LabelTagline": "Tagline:", "LabelTextBackgroundColor": "Text background color:", diff --git a/src/themes/appletv/theme.css b/src/themes/appletv/theme.css index 535e18ff99..2ce01fe025 100644 --- a/src/themes/appletv/theme.css +++ b/src/themes/appletv/theme.css @@ -294,6 +294,14 @@ html { border: 0.07em solid rgba(0, 0, 0, 0.158); } +.emby-checkbox:focus + .checkboxOutline { + border-color: #fff; +} + +.emby-checkbox:checked + .checkboxOutline { + background-color: #00a4dc; +} + .emby-checkbox:checked + span + .checkboxOutline, .emby-select-withcolor:focus { border-color: #00a4dc; @@ -321,6 +329,7 @@ html { margin: 0.4rem 0.5rem 0.4rem 0.5rem; } +.emby-checkbox:focus:not(:checked) + .checkboxOutline, .emby-checkbox:focus:not(:checked) + span + .checkboxOutline { border-color: #00a4dc; } diff --git a/src/themes/blueradiance/theme.css b/src/themes/blueradiance/theme.css index 76275f5c16..4e338fb1c1 100644 --- a/src/themes/blueradiance/theme.css +++ b/src/themes/blueradiance/theme.css @@ -308,6 +308,14 @@ html { color: #fff !important; } +.emby-checkbox:focus + .checkboxOutline { + border-color: #fff; +} + +.emby-checkbox:checked + .checkboxOutline { + background-color: #00a4dc; +} + .emby-checkbox:checked + span + .checkboxOutline { border-color: #00a4dc; } @@ -321,6 +329,7 @@ html { background-color: #00a4dc; } +.emby-checkbox:focus:not(:checked) + .checkboxOutline, .emby-checkbox:focus:not(:checked) + span + .checkboxOutline { border-color: #00a4dc; } diff --git a/src/themes/dark/theme.css b/src/themes/dark/theme.css index f1c17fe9fd..76ab9299fd 100644 --- a/src/themes/dark/theme.css +++ b/src/themes/dark/theme.css @@ -289,6 +289,14 @@ html { color: #fff !important; } +.emby-checkbox:focus + .checkboxOutline { + border-color: #fff; +} + +.emby-checkbox:checked + .checkboxOutline { + background-color: #00a4dc; +} + .emby-checkbox:checked + span + .checkboxOutline { border-color: #00a4dc; } @@ -302,6 +310,7 @@ html { background-color: #00a4dc; } +.emby-checkbox:focus:not(:checked) + .checkboxOutline, .emby-checkbox:focus:not(:checked) + span + .checkboxOutline { border-color: #00a4dc; } diff --git a/src/themes/light/theme.css b/src/themes/light/theme.css index a241c80567..0784726e5e 100644 --- a/src/themes/light/theme.css +++ b/src/themes/light/theme.css @@ -293,6 +293,14 @@ html { border: 0.07em solid rgba(0, 0, 0, 0.158); } +.emby-checkbox:focus + .checkboxOutline { + border-color: #000; +} + +.emby-checkbox:checked + .checkboxOutline { + background-color: #00a4dc; +} + .emby-checkbox:checked + span + .checkboxOutline, .emby-select-withcolor:focus { border-color: #00a4dc; @@ -307,6 +315,7 @@ html { background-color: #00a4dc; } +.emby-checkbox:focus:not(:checked) + .checkboxOutline, .emby-checkbox:focus:not(:checked) + span + .checkboxOutline { border-color: #00a4dc; } diff --git a/src/themes/purplehaze/theme.css b/src/themes/purplehaze/theme.css index f3de5f8036..92805ebcaa 100644 --- a/src/themes/purplehaze/theme.css +++ b/src/themes/purplehaze/theme.css @@ -105,11 +105,13 @@ progress::-webkit-progress-value { background: #ff77f1; } +div[data-role=controlgroup] .controlGroupButton.ui-btn-active, div[data-role=controlgroup] a.ui-btn-active { background: #55828b !important; color: #e1e5f2 !important; } +.controlGroupButton, a[data-role=button] { background: rgba(2, 43, 58, 0.521) !important; } @@ -391,11 +393,21 @@ a[data-role=button] { color: #fff !important; } +.emby-checkbox:focus + .checkboxOutline { + border-color: #ff77f1; +} + +.emby-checkbox:checked + .checkboxOutline { + background-color: #030322; + border: 0.14em solid rgb(72, 195, 200); +} + .emby-checkbox:checked + span + .checkboxOutline { background-color: #030322; border: 0.14em solid rgb(72, 195, 200); } +.emby-checkbox:checked + .checkboxOutline > .minimalCheckboxIcon-checked, .emby-checkbox:checked + span + .checkboxOutline > .checkboxIcon-checked { color: rgb(12, 232, 214); } @@ -404,6 +416,7 @@ a[data-role=button] { border-color: #ff77f1; } +.emby-checkbox:focus:not(:checked) + .checkboxOutline, .emby-checkbox:focus:not(:checked) + span + .checkboxOutline { border: 0.14em solid #ff77f1; } diff --git a/src/themes/wmc/theme.css b/src/themes/wmc/theme.css index 3004a2db5b..5848456689 100644 --- a/src/themes/wmc/theme.css +++ b/src/themes/wmc/theme.css @@ -276,6 +276,14 @@ html { border: 0.07em solid rgba(255, 255, 255, 0.135); } +.emby-checkbox:focus + .checkboxOutline { + border-color: #fff; +} + +.emby-checkbox:checked + .checkboxOutline { + background-color: #00a4dc; +} + .emby-checkbox:checked + span + .checkboxOutline, .emby-select-withcolor:focus { border-color: #00a4dc; @@ -290,6 +298,7 @@ html { background-color: #00a4dc; } +.emby-checkbox:focus:not(:checked) + .checkboxOutline, .emby-checkbox:focus:not(:checked) + span + .checkboxOutline { border-color: #00a4dc; } From 02a8160e4552f67461dfccc5054713d8dbc13365 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 31 Aug 2021 23:44:22 -0400 Subject: [PATCH 02/13] Revert label-less checkbox support --- src/elements/emby-checkbox/emby-checkbox.js | 4 +--- src/elements/emby-checkbox/emby-checkbox.scss | 11 ----------- src/themes/appletv/theme.css | 9 --------- src/themes/blueradiance/theme.css | 9 --------- src/themes/dark/theme.css | 9 --------- src/themes/light/theme.css | 9 --------- src/themes/purplehaze/theme.css | 11 ----------- src/themes/wmc/theme.css | 9 --------- 8 files changed, 1 insertion(+), 70 deletions(-) diff --git a/src/elements/emby-checkbox/emby-checkbox.js b/src/elements/emby-checkbox/emby-checkbox.js index 4472ce1ac3..ef21ff8136 100644 --- a/src/elements/emby-checkbox/emby-checkbox.js +++ b/src/elements/emby-checkbox/emby-checkbox.js @@ -65,9 +65,7 @@ import 'webcomponents.js/webcomponents-lite'; const uncheckedHtml = ''; labelElement.insertAdjacentHTML('beforeend', '' + checkHtml + uncheckedHtml + ''); - if (labelTextElement) { - labelTextElement.classList.add('checkboxLabel'); - } + labelTextElement.classList.add('checkboxLabel'); this.addEventListener('keydown', onKeyDown); diff --git a/src/elements/emby-checkbox/emby-checkbox.scss b/src/elements/emby-checkbox/emby-checkbox.scss index e839e44f61..b33a216140 100644 --- a/src/elements/emby-checkbox/emby-checkbox.scss +++ b/src/elements/emby-checkbox/emby-checkbox.scss @@ -22,10 +22,6 @@ display: flex; } -.checkboxContainer-noText { - margin-bottom: 0; -} - .checkboxListContainer { margin-bottom: 1.8em; } @@ -67,10 +63,6 @@ justify-content: center; } -.checkboxContainer-noText .checkboxOutline { - top: auto; -} - .checkboxIcon { font-size: 1.6em; color: #fff; @@ -81,19 +73,16 @@ display: none; } -.emby-checkbox:checked + .checkboxOutline > .checkboxIcon-checked, .emby-checkbox:checked + span + .checkboxOutline > .checkboxIcon-checked { /* background color set by theme */ display: flex !important; } -.emby-checkbox:checked + .checkboxOutline > .checkboxIcon-unchecked, .emby-checkbox:checked + span + .checkboxOutline > .checkboxIcon-unchecked { /* background color set by theme */ display: none !important; } -.emby-checkbox:checked[disabled] + .checkboxOutline > .checkboxIcon, .emby-checkbox:checked[disabled] + span + .checkboxOutline > .checkboxIcon { background-color: rgba(0, 0, 0, 0.26); } diff --git a/src/themes/appletv/theme.css b/src/themes/appletv/theme.css index 2ce01fe025..535e18ff99 100644 --- a/src/themes/appletv/theme.css +++ b/src/themes/appletv/theme.css @@ -294,14 +294,6 @@ html { border: 0.07em solid rgba(0, 0, 0, 0.158); } -.emby-checkbox:focus + .checkboxOutline { - border-color: #fff; -} - -.emby-checkbox:checked + .checkboxOutline { - background-color: #00a4dc; -} - .emby-checkbox:checked + span + .checkboxOutline, .emby-select-withcolor:focus { border-color: #00a4dc; @@ -329,7 +321,6 @@ html { margin: 0.4rem 0.5rem 0.4rem 0.5rem; } -.emby-checkbox:focus:not(:checked) + .checkboxOutline, .emby-checkbox:focus:not(:checked) + span + .checkboxOutline { border-color: #00a4dc; } diff --git a/src/themes/blueradiance/theme.css b/src/themes/blueradiance/theme.css index 4e338fb1c1..76275f5c16 100644 --- a/src/themes/blueradiance/theme.css +++ b/src/themes/blueradiance/theme.css @@ -308,14 +308,6 @@ html { color: #fff !important; } -.emby-checkbox:focus + .checkboxOutline { - border-color: #fff; -} - -.emby-checkbox:checked + .checkboxOutline { - background-color: #00a4dc; -} - .emby-checkbox:checked + span + .checkboxOutline { border-color: #00a4dc; } @@ -329,7 +321,6 @@ html { background-color: #00a4dc; } -.emby-checkbox:focus:not(:checked) + .checkboxOutline, .emby-checkbox:focus:not(:checked) + span + .checkboxOutline { border-color: #00a4dc; } diff --git a/src/themes/dark/theme.css b/src/themes/dark/theme.css index 76ab9299fd..f1c17fe9fd 100644 --- a/src/themes/dark/theme.css +++ b/src/themes/dark/theme.css @@ -289,14 +289,6 @@ html { color: #fff !important; } -.emby-checkbox:focus + .checkboxOutline { - border-color: #fff; -} - -.emby-checkbox:checked + .checkboxOutline { - background-color: #00a4dc; -} - .emby-checkbox:checked + span + .checkboxOutline { border-color: #00a4dc; } @@ -310,7 +302,6 @@ html { background-color: #00a4dc; } -.emby-checkbox:focus:not(:checked) + .checkboxOutline, .emby-checkbox:focus:not(:checked) + span + .checkboxOutline { border-color: #00a4dc; } diff --git a/src/themes/light/theme.css b/src/themes/light/theme.css index 0784726e5e..a241c80567 100644 --- a/src/themes/light/theme.css +++ b/src/themes/light/theme.css @@ -293,14 +293,6 @@ html { border: 0.07em solid rgba(0, 0, 0, 0.158); } -.emby-checkbox:focus + .checkboxOutline { - border-color: #000; -} - -.emby-checkbox:checked + .checkboxOutline { - background-color: #00a4dc; -} - .emby-checkbox:checked + span + .checkboxOutline, .emby-select-withcolor:focus { border-color: #00a4dc; @@ -315,7 +307,6 @@ html { background-color: #00a4dc; } -.emby-checkbox:focus:not(:checked) + .checkboxOutline, .emby-checkbox:focus:not(:checked) + span + .checkboxOutline { border-color: #00a4dc; } diff --git a/src/themes/purplehaze/theme.css b/src/themes/purplehaze/theme.css index 92805ebcaa..474d6b08f4 100644 --- a/src/themes/purplehaze/theme.css +++ b/src/themes/purplehaze/theme.css @@ -393,21 +393,11 @@ a[data-role=button] { color: #fff !important; } -.emby-checkbox:focus + .checkboxOutline { - border-color: #ff77f1; -} - -.emby-checkbox:checked + .checkboxOutline { - background-color: #030322; - border: 0.14em solid rgb(72, 195, 200); -} - .emby-checkbox:checked + span + .checkboxOutline { background-color: #030322; border: 0.14em solid rgb(72, 195, 200); } -.emby-checkbox:checked + .checkboxOutline > .minimalCheckboxIcon-checked, .emby-checkbox:checked + span + .checkboxOutline > .checkboxIcon-checked { color: rgb(12, 232, 214); } @@ -416,7 +406,6 @@ a[data-role=button] { border-color: #ff77f1; } -.emby-checkbox:focus:not(:checked) + .checkboxOutline, .emby-checkbox:focus:not(:checked) + span + .checkboxOutline { border: 0.14em solid #ff77f1; } diff --git a/src/themes/wmc/theme.css b/src/themes/wmc/theme.css index 5848456689..3004a2db5b 100644 --- a/src/themes/wmc/theme.css +++ b/src/themes/wmc/theme.css @@ -276,14 +276,6 @@ html { border: 0.07em solid rgba(255, 255, 255, 0.135); } -.emby-checkbox:focus + .checkboxOutline { - border-color: #fff; -} - -.emby-checkbox:checked + .checkboxOutline { - background-color: #00a4dc; -} - .emby-checkbox:checked + span + .checkboxOutline, .emby-select-withcolor:focus { border-color: #00a4dc; @@ -298,7 +290,6 @@ html { background-color: #00a4dc; } -.emby-checkbox:focus:not(:checked) + .checkboxOutline, .emby-checkbox:focus:not(:checked) + span + .checkboxOutline { border-color: #00a4dc; } From 3adc118864fa46516154de5ed40ed7256528dc0a Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 31 Aug 2021 23:50:55 -0400 Subject: [PATCH 03/13] Remove unused import --- src/components/syncPlay/core/Manager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/syncPlay/core/Manager.js b/src/components/syncPlay/core/Manager.js index f74813fe7d..70513b3b35 100644 --- a/src/components/syncPlay/core/Manager.js +++ b/src/components/syncPlay/core/Manager.js @@ -5,7 +5,6 @@ import { Events } from 'jellyfin-apiclient'; import * as Helper from './Helper'; -import Settings from './Settings'; import TimeSyncCore from './timeSync/TimeSyncCore'; import PlaybackCore from './PlaybackCore'; import QueueCore from './QueueCore'; From 4b581f4160b700d9ad962408c58052e3b12749da Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 1 Sep 2021 00:43:44 -0400 Subject: [PATCH 04/13] Fix dialog footer styles on tv --- src/components/formdialog.scss | 1 + src/themes/appletv/theme.css | 16 +++++++++++----- src/themes/blueradiance/theme.css | 4 ++++ src/themes/dark/theme.css | 4 ++++ src/themes/light/theme.css | 5 +++++ src/themes/purplehaze/theme.css | 5 +++++ src/themes/wmc/theme.css | 5 +++++ 7 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/components/formdialog.scss b/src/components/formdialog.scss index d7cb162e8c..d7a6d4e6a8 100644 --- a/src/components/formdialog.scss +++ b/src/components/formdialog.scss @@ -60,6 +60,7 @@ } .layout-tv .formDialogFooter { + position: relative; align-items: center; justify-content: center; flex-wrap: wrap; diff --git a/src/themes/appletv/theme.css b/src/themes/appletv/theme.css index 535e18ff99..b322f697cf 100644 --- a/src/themes/appletv/theme.css +++ b/src/themes/appletv/theme.css @@ -159,11 +159,6 @@ html { background-color: #f57f17; } -.formDialogFooter:not(.formDialogFooter-clear) { - border-top: 1px solid #ddd; - border-top: 1px solid rgba(0, 0, 0, 0.08); -} - .cardText-secondary, .fieldDescription, .guide-programNameCaret, @@ -203,6 +198,11 @@ html { color: rgba(255, 255, 255, 0.87); } +.formDialogFooter:not(.formDialogFooter-clear) { + border-top: 1px solid #ddd; + border-top: 1px solid rgba(0, 0, 0, 0.08); +} + .appfooter, .formDialogFooter:not(.formDialogFooter-clear), .playlistSectionButton { @@ -214,6 +214,12 @@ html { background: linear-gradient(to right, #bcbcbc, #a7b4b7, #beb5a5, #adbec2, #b9c7cb); } +.layout-tv .formDialogFooter:not(.formDialogFooter-clear) { + border: none; + color: initial; + background: none; +} + .nowPlayingBarSecondaryText { color: #999; } diff --git a/src/themes/blueradiance/theme.css b/src/themes/blueradiance/theme.css index 76275f5c16..9cd209b45d 100644 --- a/src/themes/blueradiance/theme.css +++ b/src/themes/blueradiance/theme.css @@ -139,6 +139,10 @@ html { background-color: rgba(0, 0, 0, 0.5); } +.layout-tv .formDialogFooter:not(.formDialogFooter-clear) { + background-color: transparent; +} + .defaultCardBackground1 { background-color: #213440; } diff --git a/src/themes/dark/theme.css b/src/themes/dark/theme.css index f1c17fe9fd..06982c398a 100644 --- a/src/themes/dark/theme.css +++ b/src/themes/dark/theme.css @@ -127,6 +127,10 @@ html { background-color: #202020; } +.layout-tv .formDialogFooter:not(.formDialogFooter-clear) { + background-color: transparent; +} + .defaultCardBackground1 { background-color: #00455c; } diff --git a/src/themes/light/theme.css b/src/themes/light/theme.css index a241c80567..ebb65dfd00 100644 --- a/src/themes/light/theme.css +++ b/src/themes/light/theme.css @@ -170,6 +170,11 @@ html { color: inherit; } +.layout-tv .formDialogFooter:not(.formDialogFooter-clear) { + background-color: transparent; + border: none; +} + .cardText-secondary, .fieldDescription, .guide-programNameCaret, diff --git a/src/themes/purplehaze/theme.css b/src/themes/purplehaze/theme.css index 474d6b08f4..8ca8b16777 100644 --- a/src/themes/purplehaze/theme.css +++ b/src/themes/purplehaze/theme.css @@ -223,6 +223,11 @@ a[data-role=button] { border-radius: 0.8em; } +.layout-tv .formDialogFooter:not(.formDialogFooter-clear) { + background-color: transparent; + border-radius: 0; +} + .cardOverlayContainer { border-radius: 0.8em; } diff --git a/src/themes/wmc/theme.css b/src/themes/wmc/theme.css index 3004a2db5b..067d2887ad 100644 --- a/src/themes/wmc/theme.css +++ b/src/themes/wmc/theme.css @@ -193,6 +193,11 @@ html { color: rgba(255, 255, 255, 0.78); } +.layout-tv .formDialogFooter:not(.formDialogFooter-clear) { + background: transparent; + color: initial; +} + .itemSelectionPanel { border: 1px solid #00a4dc; } From 730b9d36a89eb6ac8e6d9f3e903885918e98aab7 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 1 Sep 2021 01:06:37 -0400 Subject: [PATCH 05/13] Fix lint issues --- src/components/syncPlay/core/PlaybackCore.js | 2 +- src/components/syncPlay/core/timeSync/TimeSyncCore.js | 2 +- src/components/syncPlay/ui/settings/SettingsEditor.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/syncPlay/core/PlaybackCore.js b/src/components/syncPlay/core/PlaybackCore.js index 6396253e58..4793773278 100644 --- a/src/components/syncPlay/core/PlaybackCore.js +++ b/src/components/syncPlay/core/PlaybackCore.js @@ -38,7 +38,7 @@ class PlaybackCore { this.manager = syncPlayManager; this.timeSyncCore = syncPlayManager.getTimeSyncCore(); - Events.on(Settings, 'update', (event) => { + Events.on(Settings, 'update', () => { this.loadPreferences(); }); } diff --git a/src/components/syncPlay/core/timeSync/TimeSyncCore.js b/src/components/syncPlay/core/timeSync/TimeSyncCore.js index 4308d450de..1b5bf31658 100644 --- a/src/components/syncPlay/core/timeSync/TimeSyncCore.js +++ b/src/components/syncPlay/core/timeSync/TimeSyncCore.js @@ -36,7 +36,7 @@ class TimeSyncCore { Events.trigger(this, 'time-sync-server-update', [timeOffset, ping]); }); - Events.on(Settings, 'extraTimeOffset', (event, value, oldValue) => { + Events.on(Settings, 'extraTimeOffset', () => { this.extraTimeOffset = Settings.getFloat('extraTimeOffset', 0.0); }); } diff --git a/src/components/syncPlay/ui/settings/SettingsEditor.js b/src/components/syncPlay/ui/settings/SettingsEditor.js index 1906b8ed2f..7072da06d3 100644 --- a/src/components/syncPlay/ui/settings/SettingsEditor.js +++ b/src/components/syncPlay/ui/settings/SettingsEditor.js @@ -40,11 +40,11 @@ class SettingsEditor { this.embed(); - Events.on(this.timeSyncCore, 'refresh-devices', (event) => { + Events.on(this.timeSyncCore, 'refresh-devices', () => { this.refreshTimeSyncDevices(); }); - Events.on(this.timeSyncCore, 'time-sync-server-update', (event) => { + Events.on(this.timeSyncCore, 'time-sync-server-update', () => { this.refreshTimeSyncDevices(); }); } From 6172c00c3cbb3c601684e31e9e03c9d27708f2f4 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 1 Sep 2021 10:28:49 -0400 Subject: [PATCH 06/13] Remove unused playback diff setting --- src/components/syncPlay/core/PlaybackCore.js | 13 +++----- .../syncPlay/core/timeSync/TimeSyncCore.js | 33 +++++++------------ 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/components/syncPlay/core/PlaybackCore.js b/src/components/syncPlay/core/PlaybackCore.js index 4793773278..18a9c43e61 100644 --- a/src/components/syncPlay/core/PlaybackCore.js +++ b/src/components/syncPlay/core/PlaybackCore.js @@ -538,9 +538,6 @@ class PlaybackCore { // Diff might be caused by the player internally starting the playback. const diffMillis = (serverPositionTicks - currentPositionTicks) / Helper.TicksPerMillisecond; - // Adapt playback diff to selected device for time syncing. - const targetPlaybackDiff = diffMillis - this.timeSyncCore.getPlaybackDiff(); - // Notify update for playback sync. this.playbackDiffMillis = diffMillis; Events.trigger(this.manager, 'playback-diff', [this.playbackDiffMillis]); @@ -553,22 +550,22 @@ class PlaybackCore { const playerWrapper = this.manager.getPlayerWrapper(); if (this.syncEnabled && this.enableSyncCorrection) { - const absDiffMillis = Math.abs(targetPlaybackDiff); + const absDiffMillis = Math.abs(diffMillis); // TODO: SpeedToSync sounds bad on songs. // TODO: SpeedToSync is failing on Safari (Mojave); even if playbackRate is supported, some delay seems to exist. // TODO: both SpeedToSync and SpeedToSync seem to have a hard time keeping up on Android Chrome as well. if (playerWrapper.hasPlaybackRate() && this.useSpeedToSync && absDiffMillis >= this.minDelaySpeedToSync && absDiffMillis < this.maxDelaySpeedToSync) { // Fix negative speed when client is ahead of time more than speedToSyncTime. const MinSpeed = 0.2; - if (targetPlaybackDiff <= -speedToSyncTime * MinSpeed) { - speedToSyncTime = Math.abs(targetPlaybackDiff) / (1.0 - MinSpeed); + if (diffMillis <= -speedToSyncTime * MinSpeed) { + speedToSyncTime = Math.abs(diffMillis) / (1.0 - MinSpeed); } // SpeedToSync strategy. - const speed = 1 + targetPlaybackDiff / speedToSyncTime; + const speed = 1 + diffMillis / speedToSyncTime; if (speed <= 0) { - console.error('SyncPlay error: speed should not be negative!', speed, targetPlaybackDiff, speedToSyncTime); + console.error('SyncPlay error: speed should not be negative!', speed, diffMillis, speedToSyncTime); } playerWrapper.setPlaybackRate(speed); diff --git a/src/components/syncPlay/core/timeSync/TimeSyncCore.js b/src/components/syncPlay/core/timeSync/TimeSyncCore.js index 1b5bf31658..2413bf4d7b 100644 --- a/src/components/syncPlay/core/timeSync/TimeSyncCore.js +++ b/src/components/syncPlay/core/timeSync/TimeSyncCore.js @@ -7,6 +7,16 @@ import { Events } from 'jellyfin-apiclient'; import Settings from '../Settings'; import TimeSyncServer from './TimeSyncServer'; +/** + * Utility function to offset a given date by a given amount of milliseconds. + * @param {Date} date The date. + * @param {number} offset The offset, in milliseconds. + * @returns {Date} The offset date. + */ +function offsetDate(date, offset) { + return new Date(date.getTime() + offset); +} + /** * Class that manages time syncing with several devices. */ @@ -89,7 +99,7 @@ class TimeSyncCore { */ remoteDateToLocal(remote) { const date = this.timeSyncServer.remoteDateToLocal(remote); - return this.offsetDate(date, -this.extraTimeOffset); + return offsetDate(date, -this.extraTimeOffset); } /** @@ -99,7 +109,7 @@ class TimeSyncCore { */ localDateToRemote(local) { const date = this.timeSyncServer.localDateToRemote(local); - return this.offsetDate(date, this.extraTimeOffset); + return offsetDate(date, this.extraTimeOffset); } /** @@ -109,25 +119,6 @@ class TimeSyncCore { getTimeOffset() { return this.timeSyncServer.getTimeOffset() + this.extraTimeOffset; } - - /** - * Gets the playback diff that should be used to offset local playback, in milliseconds. - * @returns {number} The time offset. - */ - getPlaybackDiff() { - // TODO: this will use playback data from WebRTC peers. - return 0; - } - - /** - * Offsets a given date by a given ammount of milliseconds. - * @param {Date} date The date. - * @param {number} offset The offset, in milliseconds. - * @returns {Date} The offset date. - */ - offsetDate(date, offset) { - return new Date(date.getTime() + offset); - } } export default TimeSyncCore; From 99a3ca1cbda4e3e1c9bd2bd903caa8e66416b4fb Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 1 Sep 2021 12:23:27 -0400 Subject: [PATCH 07/13] Remove unused device setting --- .../syncPlay/core/timeSync/TimeSyncCore.js | 26 ---------------- .../syncPlay/ui/settings/SettingsEditor.js | 31 ------------------- .../syncPlay/ui/settings/localTab.html | 5 --- src/strings/en-us.json | 2 -- 4 files changed, 64 deletions(-) diff --git a/src/components/syncPlay/core/timeSync/TimeSyncCore.js b/src/components/syncPlay/core/timeSync/TimeSyncCore.js index 2413bf4d7b..0f3511fc55 100644 --- a/src/components/syncPlay/core/timeSync/TimeSyncCore.js +++ b/src/components/syncPlay/core/timeSync/TimeSyncCore.js @@ -58,32 +58,6 @@ class TimeSyncCore { this.timeSyncServer.forceUpdate(); } - /** - * Gets the list of available devices for time sync. - * @returns {Array} The list of devices. - */ - getDevices() { - const devices = [{ - type: 'server', - id: 'server', - name: 'Server', - timeOffset: this.timeSyncServer.getTimeOffset(), - ping: this.timeSyncServer.getPing(), - peerTimeOffset: 0, - peerPing: 0 - }]; - - return devices; - } - - /** - * Gets the identifier of the selected device for time sync. Default value is 'server'. - * @returns {string} The identifier. - */ - getActiveDevice() { - return this.timeSyncDeviceId; - } - /** * Gets the display name of the selected device for time sync. * @returns {string} The display name. diff --git a/src/components/syncPlay/ui/settings/SettingsEditor.js b/src/components/syncPlay/ui/settings/SettingsEditor.js index 7072da06d3..d476e0e50e 100644 --- a/src/components/syncPlay/ui/settings/SettingsEditor.js +++ b/src/components/syncPlay/ui/settings/SettingsEditor.js @@ -39,14 +39,6 @@ class SettingsEditor { this.tabs = {}; this.embed(); - - Events.on(this.timeSyncCore, 'refresh-devices', () => { - this.refreshTimeSyncDevices(); - }); - - Events.on(this.timeSyncCore, 'time-sync-server-update', () => { - this.refreshTimeSyncDevices(); - }); } insertBefore(newNode, existingNode) { @@ -163,27 +155,6 @@ class SettingsEditor { context.querySelector('#txtMinDelaySkipToSync').value = SyncPlay.Settings.getFloat('minDelaySkipToSync', 400.0); context.querySelector('#chkSpeedToSync').checked = SyncPlay.Settings.getBool('useSpeedToSync', true); context.querySelector('#chkSkipToSync').checked = SyncPlay.Settings.getBool('useSkipToSync', true); - - this.refreshTimeSyncDevices(); - const timeSyncSelect = context.querySelector('#selectTimeSync'); - timeSyncSelect.value = this.timeSyncCore.getActiveDevice(); - this.timeSyncSelectedValue = timeSyncSelect.value; - - timeSyncSelect.addEventListener('change', () => { - this.timeSyncSelectedValue = timeSyncSelect.value; - }); - } - - refreshTimeSyncDevices() { - const { context } = this; - const timeSyncSelect = context.querySelector('#selectTimeSync'); - const devices = this.timeSyncCore.getDevices(); - - timeSyncSelect.innerHTML = devices.map(device => { - return ``; - }).join(''); - - timeSyncSelect.value = this.timeSyncSelectedValue; } /** @@ -218,7 +189,6 @@ class SettingsEditor { async saveToAppSettings() { const { context } = this; - const timeSyncDevice = context.querySelector('#selectTimeSync').value; const extraTimeOffset = context.querySelector('#txtExtraTimeOffset').value; const syncCorrection = context.querySelector('#chkSyncCorrection').checked; const minDelaySpeedToSync = context.querySelector('#txtMinDelaySpeedToSync').value; @@ -228,7 +198,6 @@ class SettingsEditor { const useSpeedToSync = context.querySelector('#chkSpeedToSync').checked; const useSkipToSync = context.querySelector('#chkSkipToSync').checked; - SyncPlay.Settings.set('timeSyncDevice', timeSyncDevice); SyncPlay.Settings.set('extraTimeOffset', extraTimeOffset); SyncPlay.Settings.set('enableSyncCorrection', syncCorrection); SyncPlay.Settings.set('minDelaySpeedToSync', minDelaySpeedToSync); diff --git a/src/components/syncPlay/ui/settings/localTab.html b/src/components/syncPlay/ui/settings/localTab.html index 4cd3a5ea0d..0710ee44d7 100644 --- a/src/components/syncPlay/ui/settings/localTab.html +++ b/src/components/syncPlay/ui/settings/localTab.html @@ -1,9 +1,4 @@
-

${HeaderSyncPlayTimeSyncSettings}

-
- -
${LabelSyncPlaySettingsTimeSyncHelp}
-

${HeaderSyncPlayPlaybackSettings}

-
- - -
+ +
+

${HeaderSyncPlayPlaybackSettings}

+ + +
+ +
${LabelSyncPlaySettingsSyncCorrectionHelp}
+
+ + +
+ +
${LabelSyncPlaySettingsSpeedToSyncHelp}
+
+
+ +
${LabelSyncPlaySettingsMinDelaySpeedToSyncHelp}
+
+
+ +
${LabelSyncPlaySettingsMaxDelaySpeedToSyncHelp}
+
+
+ +
${LabelSyncPlaySettingsSpeedToSyncDurationHelp}
+
+ + +
+ +
${LabelSyncPlaySettingsSkipToSyncHelp}
+
+
+ +
${LabelSyncPlaySettingsMinDelaySkipToSyncHelp}
+
+ + +

${HeaderSyncPlayTimeSyncSettings}

+
+ +
${LabelSyncPlaySettingsExtraTimeOffsetHelp}
+
+
+