1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge pull request #2204 from OancaAndrei/syncplay-settings

This commit is contained in:
Bill Thornton 2021-09-06 12:29:26 -04:00 committed by GitHub
commit 6dffc58e29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 407 additions and 18 deletions

View file

@ -60,6 +60,7 @@
} }
.layout-tv .formDialogFooter { .layout-tv .formDialogFooter {
position: relative;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;

View file

@ -4,7 +4,9 @@
*/ */
import { Events } from 'jellyfin-apiclient'; import { Events } from 'jellyfin-apiclient';
import { toBoolean, toFloat } from '../../../scripts/stringUtils';
import * as Helper from './Helper'; import * as Helper from './Helper';
import { getSetting } from './Settings';
/** /**
* Class that manages the playback of SyncPlay. * 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.lastCommand = null; // Last scheduled playback command, might not be the latest one.
this.scheduledCommandTimeout = null; this.scheduledCommandTimeout = null;
this.syncTimeout = null; this.syncTimeout = null;
this.loadPreferences();
} }
/** /**
@ -35,26 +39,35 @@ class PlaybackCore {
this.manager = syncPlayManager; this.manager = syncPlayManager;
this.timeSyncCore = syncPlayManager.getTimeSyncCore(); 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. // 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. // 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. // 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. // 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. // Whether SpeedToSync should be used.
this.useSpeedToSync = true; this.useSpeedToSync = toBoolean(getSetting('useSpeedToSync'), true);
// Whether SkipToSync should be used. // Whether SkipToSync should be used.
this.useSkipToSync = true; this.useSkipToSync = toBoolean(getSetting('useSkipToSync'), true);
// Whether sync correction during playback is active. // 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. // Diff might be caused by the player internally starting the playback.
const diffMillis = (serverPositionTicks - currentPositionTicks) / Helper.TicksPerMillisecond; const diffMillis = (serverPositionTicks - currentPositionTicks) / Helper.TicksPerMillisecond;
// Notify update for playback sync.
this.playbackDiffMillis = diffMillis; this.playbackDiffMillis = diffMillis;
Events.trigger(this.manager, 'playback-diff', [this.playbackDiffMillis]);
// Avoid overloading the browser. // Avoid overloading the browser.
const elapsed = currentTime - this.lastSyncTime; const elapsed = currentTime - this.lastSyncTime;

View 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);
}

View file

@ -4,8 +4,21 @@
*/ */
import { Events } from 'jellyfin-apiclient'; import { Events } from 'jellyfin-apiclient';
import appSettings from '../../../../scripts/settings/appSettings';
import { toFloat } from '../../../../scripts/stringUtils';
import { getSetting } from '../Settings';
import TimeSyncServer from './TimeSyncServer'; 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. * Class that manages time syncing with several devices.
*/ */
@ -13,6 +26,9 @@ class TimeSyncCore {
constructor() { constructor() {
this.manager = null; this.manager = null;
this.timeSyncServer = 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.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. * @returns {Date} Local time.
*/ */
remoteDateToLocal(remote) { 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. * @returns {Date} Server time.
*/ */
localDateToRemote(local) { 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. * @returns {number} The time offset.
*/ */
getTimeOffset() { getTimeOffset() {
return this.timeSyncServer.getTimeOffset(); return this.timeSyncServer.getTimeOffset() + this.extraTimeOffset;
} }
} }

View file

@ -1,5 +1,6 @@
import { Events } from 'jellyfin-apiclient'; import { Events } from 'jellyfin-apiclient';
import SyncPlay from '../core'; import SyncPlay from '../core';
import SyncPlaySettingsEditor from './settings/SettingsEditor';
import loading from '../../loading/loading'; import loading from '../../loading/loading';
import toast from '../../toast/toast'; import toast from '../../toast/toast';
import actionsheet from '../../actionSheet/actionSheet'; import actionsheet from '../../actionSheet/actionSheet';
@ -77,7 +78,9 @@ class GroupSelectionMenu {
}); });
} }
}).catch((error) => { }).catch((error) => {
console.error('SyncPlay: unexpected error listing groups:', error); if (error) {
console.error('SyncPlay: unexpected error listing groups:', error);
}
}); });
loading.hide(); 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({ menuItems.push({
name: globalize.translate('LabelSyncPlayLeaveGroup'), name: globalize.translate('LabelSyncPlayLeaveGroup'),
icon: 'meeting_room', icon: 'meeting_room',
@ -142,9 +153,19 @@ class GroupSelectionMenu {
SyncPlay.Manager.haltGroupPlayback(apiClient); SyncPlay.Manager.haltGroupPlayback(apiClient);
} else if (id == 'leave-group') { } else if (id == 'leave-group') {
apiClient.leaveSyncPlayGroup(); 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) => { }).catch((error) => {
console.error('SyncPlay: unexpected error showing group menu:', error); if (error) {
console.error('SyncPlay: unexpected error showing group menu:', error);
}
}); });
loading.hide(); loading.hide();

View 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;

View 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>

View 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);
}
}

View file

@ -455,6 +455,9 @@
"HeaderSubtitleProfilesHelp": "Subtitle profiles describe the subtitle formats supported by the device.", "HeaderSubtitleProfilesHelp": "Subtitle profiles describe the subtitle formats supported by the device.",
"HeaderSyncPlayEnabled": "SyncPlay enabled", "HeaderSyncPlayEnabled": "SyncPlay enabled",
"HeaderSyncPlaySelectGroup": "Join a group", "HeaderSyncPlaySelectGroup": "Join a group",
"HeaderSyncPlaySettings": "SyncPlay Settings",
"HeaderSyncPlayPlaybackSettings": "Playback",
"HeaderSyncPlayTimeSyncSettings": "Time sync",
"HeaderSystemDlnaProfiles": "System Profiles", "HeaderSystemDlnaProfiles": "System Profiles",
"HeaderTaskTriggers": "Task Triggers", "HeaderTaskTriggers": "Task Triggers",
"HeaderThisUserIsCurrentlyDisabled": "This user is currently disabled", "HeaderThisUserIsCurrentlyDisabled": "This user is currently disabled",
@ -869,6 +872,23 @@
"LabelSyncPlaySyncMethod": "Sync method:", "LabelSyncPlaySyncMethod": "Sync method:",
"LabelSyncPlayTimeSyncDevice": "Time syncing with:", "LabelSyncPlayTimeSyncDevice": "Time syncing with:",
"LabelSyncPlayTimeSyncOffset": "Time offset:", "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:", "LabelTag": "Tag:",
"LabelTagline": "Tagline:", "LabelTagline": "Tagline:",
"LabelTextBackgroundColor": "Text background color:", "LabelTextBackgroundColor": "Text background color:",

View file

@ -159,11 +159,6 @@ html {
background-color: #f57f17; background-color: #f57f17;
} }
.formDialogFooter:not(.formDialogFooter-clear) {
border-top: 1px solid #ddd;
border-top: 1px solid rgba(0, 0, 0, 0.08);
}
.cardText-secondary, .cardText-secondary,
.fieldDescription, .fieldDescription,
.guide-programNameCaret, .guide-programNameCaret,
@ -203,6 +198,11 @@ html {
color: rgba(255, 255, 255, 0.87); 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, .appfooter,
.formDialogFooter:not(.formDialogFooter-clear), .formDialogFooter:not(.formDialogFooter-clear),
.playlistSectionButton { .playlistSectionButton {
@ -214,6 +214,12 @@ html {
background: linear-gradient(to right, #bcbcbc, #a7b4b7, #beb5a5, #adbec2, #b9c7cb); background: linear-gradient(to right, #bcbcbc, #a7b4b7, #beb5a5, #adbec2, #b9c7cb);
} }
.layout-tv .formDialogFooter:not(.formDialogFooter-clear) {
border: none;
color: initial;
background: none;
}
.nowPlayingBarSecondaryText { .nowPlayingBarSecondaryText {
color: #999; color: #999;
} }

View file

@ -139,6 +139,10 @@ html {
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
} }
.layout-tv .formDialogFooter:not(.formDialogFooter-clear) {
background-color: transparent;
}
.defaultCardBackground1 { .defaultCardBackground1 {
background-color: #213440; background-color: #213440;
} }

View file

@ -127,6 +127,10 @@ html {
background-color: #202020; background-color: #202020;
} }
.layout-tv .formDialogFooter:not(.formDialogFooter-clear) {
background-color: transparent;
}
.defaultCardBackground1 { .defaultCardBackground1 {
background-color: #00455c; background-color: #00455c;
} }

View file

@ -170,6 +170,11 @@ html {
color: inherit; color: inherit;
} }
.layout-tv .formDialogFooter:not(.formDialogFooter-clear) {
background-color: transparent;
border: none;
}
.cardText-secondary, .cardText-secondary,
.fieldDescription, .fieldDescription,
.guide-programNameCaret, .guide-programNameCaret,

View file

@ -105,11 +105,13 @@ progress::-webkit-progress-value {
background: #ff77f1; background: #ff77f1;
} }
div[data-role=controlgroup] .controlGroupButton.ui-btn-active,
div[data-role=controlgroup] a.ui-btn-active { div[data-role=controlgroup] a.ui-btn-active {
background: #55828b !important; background: #55828b !important;
color: #e1e5f2 !important; color: #e1e5f2 !important;
} }
.controlGroupButton,
a[data-role=button] { a[data-role=button] {
background: rgba(2, 43, 58, 0.521) !important; background: rgba(2, 43, 58, 0.521) !important;
} }
@ -221,6 +223,11 @@ a[data-role=button] {
border-radius: 0.8em; border-radius: 0.8em;
} }
.layout-tv .formDialogFooter:not(.formDialogFooter-clear) {
background-color: transparent;
border-radius: 0;
}
.cardOverlayContainer { .cardOverlayContainer {
border-radius: 0.8em; border-radius: 0.8em;
} }

View file

@ -193,6 +193,11 @@ html {
color: rgba(255, 255, 255, 0.78); color: rgba(255, 255, 255, 0.78);
} }
.layout-tv .formDialogFooter:not(.formDialogFooter-clear) {
background: transparent;
color: initial;
}
.itemSelectionPanel { .itemSelectionPanel {
border: 1px solid #00a4dc; border: 1px solid #00a4dc;
} }