Merge pull request #2204 from OancaAndrei/syncplay-settings
This commit is contained in:
commit
6dffc58e29
15 changed files with 407 additions and 18 deletions
|
@ -60,6 +60,7 @@
|
|||
}
|
||||
|
||||
.layout-tv .formDialogFooter {
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
*/
|
||||
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import { toBoolean, toFloat } from '../../../scripts/stringUtils';
|
||||
import * as Helper from './Helper';
|
||||
import { getSetting } from './Settings';
|
||||
|
||||
/**
|
||||
* Class that manages the playback of SyncPlay.
|
||||
|
@ -25,6 +27,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 +39,35 @@ class PlaybackCore {
|
|||
this.manager = syncPlayManager;
|
||||
this.timeSyncCore = syncPlayManager.getTimeSyncCore();
|
||||
|
||||
Events.on(this.manager, 'settings-update', () => {
|
||||
this.loadPreferences();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads preferences from saved settings.
|
||||
*/
|
||||
loadPreferences() {
|
||||
// Minimum required delay for SpeedToSync to kick in, in milliseconds.
|
||||
this.minDelaySpeedToSync = 60.0;
|
||||
this.minDelaySpeedToSync = toFloat(getSetting('minDelaySpeedToSync'), 60.0);
|
||||
|
||||
// Maximum delay after which SkipToSync is used instead of SpeedToSync, in milliseconds.
|
||||
this.maxDelaySpeedToSync = 3000.0;
|
||||
this.maxDelaySpeedToSync = toFloat(getSetting('maxDelaySpeedToSync'), 3000.0);
|
||||
|
||||
// Time during which the playback is sped up, in milliseconds.
|
||||
this.speedToSyncDuration = 1000.0;
|
||||
this.speedToSyncDuration = toFloat(getSetting('speedToSyncDuration'), 1000.0);
|
||||
|
||||
// Minimum required delay for SkipToSync to kick in, in milliseconds.
|
||||
this.minDelaySkipToSync = 400.0;
|
||||
this.minDelaySkipToSync = toFloat(getSetting('minDelaySkipToSync'), 400.0);
|
||||
|
||||
// Whether SpeedToSync should be used.
|
||||
this.useSpeedToSync = true;
|
||||
this.useSpeedToSync = toBoolean(getSetting('useSpeedToSync'), true);
|
||||
|
||||
// Whether SkipToSync should be used.
|
||||
this.useSkipToSync = true;
|
||||
this.useSkipToSync = toBoolean(getSetting('useSkipToSync'), true);
|
||||
|
||||
// Whether sync correction during playback is active.
|
||||
this.enableSyncCorrection = true;
|
||||
this.enableSyncCorrection = toBoolean(getSetting('enableSyncCorrection'), true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -526,7 +539,9 @@ class PlaybackCore {
|
|||
// Diff might be caused by the player internally starting the playback.
|
||||
const diffMillis = (serverPositionTicks - currentPositionTicks) / Helper.TicksPerMillisecond;
|
||||
|
||||
// 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;
|
||||
|
|
28
src/components/syncPlay/core/Settings.js
Normal file
28
src/components/syncPlay/core/Settings.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Module that manages SyncPlay settings.
|
||||
* @module components/syncPlay/core/Settings
|
||||
*/
|
||||
import appSettings from '../../../scripts/settings/appSettings';
|
||||
|
||||
/**
|
||||
* Prefix used when saving SyncPlay settings.
|
||||
*/
|
||||
const PREFIX = 'syncPlay';
|
||||
|
||||
/**
|
||||
* Gets the value of a setting.
|
||||
* @param {string} name The name of the setting.
|
||||
* @returns {string} The value.
|
||||
*/
|
||||
export function getSetting(name) {
|
||||
return appSettings.get(name, PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function setSetting(name, value) {
|
||||
return appSettings.set(name, value, PREFIX);
|
||||
}
|
|
@ -4,8 +4,21 @@
|
|||
*/
|
||||
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import appSettings from '../../../../scripts/settings/appSettings';
|
||||
import { toFloat } from '../../../../scripts/stringUtils';
|
||||
import { getSetting } 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.
|
||||
*/
|
||||
|
@ -13,6 +26,9 @@ class TimeSyncCore {
|
|||
constructor() {
|
||||
this.manager = null;
|
||||
this.timeSyncServer = null;
|
||||
|
||||
this.timeSyncDeviceId = getSetting('timeSyncDevice') || 'server';
|
||||
this.extraTimeOffset = toFloat(getSetting('extraTimeOffset'), 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,6 +47,12 @@ class TimeSyncCore {
|
|||
|
||||
Events.trigger(this, 'time-sync-server-update', [timeOffset, ping]);
|
||||
});
|
||||
|
||||
Events.on(appSettings, 'change', function (e, name) {
|
||||
if (name === 'extraTimeOffset') {
|
||||
this.extraTimeOffset = toFloat(getSetting('extraTimeOffset'), 0.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,7 +76,8 @@ class TimeSyncCore {
|
|||
* @returns {Date} Local time.
|
||||
*/
|
||||
remoteDateToLocal(remote) {
|
||||
return this.timeSyncServer.remoteDateToLocal(remote);
|
||||
const date = this.timeSyncServer.remoteDateToLocal(remote);
|
||||
return offsetDate(date, -this.extraTimeOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,15 +86,16 @@ class TimeSyncCore {
|
|||
* @returns {Date} Server time.
|
||||
*/
|
||||
localDateToRemote(local) {
|
||||
return this.timeSyncServer.localDateToRemote(local);
|
||||
const date = this.timeSyncServer.localDateToRemote(local);
|
||||
return 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
@ -77,7 +78,9 @@ class GroupSelectionMenu {
|
|||
});
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('SyncPlay: unexpected error listing groups:', error);
|
||||
if (error) {
|
||||
console.error('SyncPlay: unexpected error listing groups:', error);
|
||||
}
|
||||
});
|
||||
|
||||
loading.hide();
|
||||
|
@ -119,6 +122,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',
|
||||
|
@ -142,9 +153,19 @@ 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 })
|
||||
.embed()
|
||||
.catch(error => {
|
||||
if (error) {
|
||||
console.error('Error creating SyncPlay settings editor', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('SyncPlay: unexpected error showing group menu:', error);
|
||||
if (error) {
|
||||
console.error('SyncPlay: unexpected error showing group menu:', error);
|
||||
}
|
||||
});
|
||||
|
||||
loading.hide();
|
||||
|
|
147
src/components/syncPlay/ui/settings/SettingsEditor.js
Normal file
147
src/components/syncPlay/ui/settings/SettingsEditor.js
Normal file
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* Module that displays an editor for changing SyncPlay settings.
|
||||
* @module components/syncPlay/settings/SettingsEditor
|
||||
*/
|
||||
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import SyncPlay from '../../core';
|
||||
import { getSetting, setSetting } from '../../core/Settings';
|
||||
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 { toBoolean, toFloat } from '../../../../scripts/stringUtils';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// 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 = toFloat(getSetting('extraTimeOffset'), 0.0);
|
||||
context.querySelector('#chkSyncCorrection').checked = toBoolean(getSetting('enableSyncCorrection'), true);
|
||||
context.querySelector('#txtMinDelaySpeedToSync').value = toFloat(getSetting('minDelaySpeedToSync'), 60.0);
|
||||
context.querySelector('#txtMaxDelaySpeedToSync').value = toFloat(getSetting('maxDelaySpeedToSync'), 3000.0);
|
||||
context.querySelector('#txtSpeedToSyncDuration').value = toFloat(getSetting('speedToSyncDuration'), 1000.0);
|
||||
context.querySelector('#txtMinDelaySkipToSync').value = toFloat(getSetting('minDelaySkipToSync'), 400.0);
|
||||
context.querySelector('#chkSpeedToSync').checked = toBoolean(getSetting('useSpeedToSync'), true);
|
||||
context.querySelector('#chkSkipToSync').checked = toBoolean(getSetting('useSkipToSync'), true);
|
||||
}
|
||||
|
||||
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 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;
|
||||
|
||||
setSetting('extraTimeOffset', extraTimeOffset);
|
||||
setSetting('enableSyncCorrection', syncCorrection);
|
||||
setSetting('minDelaySpeedToSync', minDelaySpeedToSync);
|
||||
setSetting('maxDelaySpeedToSync', maxDelaySpeedToSync);
|
||||
setSetting('speedToSyncDuration', speedToSyncDuration);
|
||||
setSetting('minDelaySkipToSync', minDelaySkipToSync);
|
||||
setSetting('useSpeedToSync', useSpeedToSync);
|
||||
setSetting('useSkipToSync', useSkipToSync);
|
||||
|
||||
Events.trigger(SyncPlay.Manager, 'settings-update');
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsEditor;
|
75
src/components/syncPlay/ui/settings/editor.html
Normal file
75
src/components/syncPlay/ui/settings/editor.html
Normal file
|
@ -0,0 +1,75 @@
|
|||
<div class="formDialogHeader">
|
||||
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1">
|
||||
<span class="material-icons arrow_back"></span>
|
||||
</button>
|
||||
<h3 class="formDialogHeaderTitle">${HeaderSyncPlaySettings}</h3>
|
||||
</div>
|
||||
<div class="formDialogContent smoothScrollY">
|
||||
<div class="dialogContentInner dialog-content-centered">
|
||||
|
||||
<form style="margin: auto;">
|
||||
<h2 class="sectionTitle">${HeaderSyncPlayPlaybackSettings}</h2>
|
||||
|
||||
<!-- Sync Correction Setting -->
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkSyncCorrection" />
|
||||
<span>${LabelSyncPlaySettingsSyncCorrection}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelSyncPlaySettingsSyncCorrectionHelp}</div>
|
||||
</div>
|
||||
|
||||
<!-- SpeedToSync Settings -->
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkSpeedToSync" />
|
||||
<span>${LabelSyncPlaySettingsSpeedToSync}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelSyncPlaySettingsSpeedToSyncHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer inputContainer-withDescription">
|
||||
<input type="number" is="emby-input" id="txtMinDelaySpeedToSync" pattern="[0-9]*"
|
||||
label="${LabelSyncPlaySettingsMinDelaySpeedToSync}" />
|
||||
<div class="fieldDescription">${LabelSyncPlaySettingsMinDelaySpeedToSyncHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer inputContainer-withDescription">
|
||||
<input type="number" is="emby-input" id="txtMaxDelaySpeedToSync" pattern="[0-9]*"
|
||||
label="${LabelSyncPlaySettingsMaxDelaySpeedToSync}" />
|
||||
<div class="fieldDescription">${LabelSyncPlaySettingsMaxDelaySpeedToSyncHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer inputContainer-withDescription">
|
||||
<input type="number" is="emby-input" id="txtSpeedToSyncDuration" pattern="[0-9]*"
|
||||
label="${LabelSyncPlaySettingsSpeedToSyncDuration}" />
|
||||
<div class="fieldDescription">${LabelSyncPlaySettingsSpeedToSyncDurationHelp}</div>
|
||||
</div>
|
||||
|
||||
<!-- SkipToSync Settings -->
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkSkipToSync" />
|
||||
<span>${LabelSyncPlaySettingsSkipToSync}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelSyncPlaySettingsSkipToSyncHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer inputContainer-withDescription">
|
||||
<input type="number" is="emby-input" id="txtMinDelaySkipToSync" pattern="[0-9]*"
|
||||
label="${LabelSyncPlaySettingsMinDelaySkipToSync}" />
|
||||
<div class="fieldDescription">${LabelSyncPlaySettingsMinDelaySkipToSyncHelp}</div>
|
||||
</div>
|
||||
|
||||
<!-- Time Settings -->
|
||||
<h2 class="sectionTitle">${HeaderSyncPlayTimeSyncSettings}</h2>
|
||||
<div class="inputContainer inputContainer-withDescription">
|
||||
<input type="number" is="emby-input" id="txtExtraTimeOffset" pattern="[0-9]*"
|
||||
label="${LabelSyncPlaySettingsExtraTimeOffset}" />
|
||||
<div class="fieldDescription">${LabelSyncPlaySettingsExtraTimeOffsetHelp}</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="formDialogFooter" id="footer">
|
||||
<button is="emby-button" type="submit" class="raised button-submit block btnSave formDialogFooterItem">
|
||||
<span id="saveButtonText">${Save}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
27
src/scripts/stringUtils.js
Normal file
27
src/scripts/stringUtils.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Gets the value of a string as boolean.
|
||||
* @param {string} name The value as a string.
|
||||
* @param {boolean} defaultValue The default value if the string is invalid.
|
||||
* @returns {boolean} The value.
|
||||
*/
|
||||
export function toBoolean(value, defaultValue = false) {
|
||||
if (value !== 'true' && value !== 'false') {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return value !== 'false';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of a string as float number.
|
||||
* @param {string} value The value as a string.
|
||||
* @param {number} defaultValue The default value if the string is invalid.
|
||||
* @returns {number} The value.
|
||||
*/
|
||||
export function toFloat(value, defaultValue = 0) {
|
||||
if (value === null || value === '' || isNaN(value)) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return parseFloat(value);
|
||||
}
|
||||
}
|
|
@ -455,6 +455,9 @@
|
|||
"HeaderSubtitleProfilesHelp": "Subtitle profiles describe the subtitle formats supported by the device.",
|
||||
"HeaderSyncPlayEnabled": "SyncPlay enabled",
|
||||
"HeaderSyncPlaySelectGroup": "Join a group",
|
||||
"HeaderSyncPlaySettings": "SyncPlay Settings",
|
||||
"HeaderSyncPlayPlaybackSettings": "Playback",
|
||||
"HeaderSyncPlayTimeSyncSettings": "Time sync",
|
||||
"HeaderSystemDlnaProfiles": "System Profiles",
|
||||
"HeaderTaskTriggers": "Task Triggers",
|
||||
"HeaderThisUserIsCurrentlyDisabled": "This user is currently disabled",
|
||||
|
@ -869,6 +872,23 @@
|
|||
"LabelSyncPlaySyncMethod": "Sync method:",
|
||||
"LabelSyncPlayTimeSyncDevice": "Time syncing with:",
|
||||
"LabelSyncPlayTimeSyncOffset": "Time offset:",
|
||||
"LabelSyncPlaySettingsDescription": "Change SyncPlay preferences",
|
||||
"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.",
|
||||
"LabelTag": "Tag:",
|
||||
"LabelTagline": "Tagline:",
|
||||
"LabelTextBackgroundColor": "Text background color:",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -127,6 +127,10 @@ html {
|
|||
background-color: #202020;
|
||||
}
|
||||
|
||||
.layout-tv .formDialogFooter:not(.formDialogFooter-clear) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.defaultCardBackground1 {
|
||||
background-color: #00455c;
|
||||
}
|
||||
|
|
|
@ -170,6 +170,11 @@ html {
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
.layout-tv .formDialogFooter:not(.formDialogFooter-clear) {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cardText-secondary,
|
||||
.fieldDescription,
|
||||
.guide-programNameCaret,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
@ -221,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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue