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

Merge branch 'jellyfin:master' into master

This commit is contained in:
hazil 2021-09-04 17:35:05 +05:30 committed by Hazil Mohamed
commit 13f55130a7
98 changed files with 5871 additions and 3582 deletions

View file

@ -31,8 +31,16 @@ class AppRouter {
startPages = ['home', 'login', 'selectserver'];
constructor() {
window.addEventListener('popstate', () => {
this.popstateOccurred = true;
// WebKit fires a popstate event on document load
// Skip it using timeout
// For Tizen 2.x
// https://stackoverflow.com/a/12214354
window.addEventListener('load', () => {
setTimeout(() => {
window.addEventListener('popstate', () => {
this.popstateOccurred = true;
});
}, 0);
});
document.addEventListener('viewshow', () => {

View file

@ -1,4 +1,4 @@
import { version as appVersion } from '../../package.json';
import Package from '../../package.json';
import appSettings from '../scripts/settings/appSettings';
import browser from '../scripts/browser';
import { Events } from 'jellyfin-apiclient';
@ -33,7 +33,7 @@ function getDeviceProfile(item) {
let profile;
if (window.NativeShell) {
profile = window.NativeShell.AppHost.getDeviceProfile(profileBuilder, appVersion);
profile = window.NativeShell.AppHost.getDeviceProfile(profileBuilder, Package.version);
} else {
const builderOpts = getBaseProfileOptions(item);
profile = profileBuilder(builderOpts);
@ -275,7 +275,7 @@ const supportedFeatures = function () {
*/
function doExit() {
try {
if (window.NativeShell) {
if (window.NativeShell?.AppHost?.exit) {
window.NativeShell.AppHost.exit();
} else if (browser.tizen) {
tizen.application.getCurrentApplication().exit();
@ -360,16 +360,20 @@ export const appHost = {
};
},
deviceName: function () {
return window.NativeShell ? window.NativeShell.AppHost.deviceName() : getDeviceName();
return window.NativeShell?.AppHost?.deviceName
? window.NativeShell.AppHost.deviceName() : getDeviceName();
},
deviceId: function () {
return window.NativeShell ? window.NativeShell.AppHost.deviceId() : getDeviceId();
return window.NativeShell?.AppHost?.deviceId
? window.NativeShell.AppHost.deviceId() : getDeviceId();
},
appName: function () {
return window.NativeShell ? window.NativeShell.AppHost.appName() : appName;
return window.NativeShell?.AppHost?.appName
? window.NativeShell.AppHost.appName() : appName;
},
appVersion: function () {
return window.NativeShell ? window.NativeShell.AppHost.appVersion() : appVersion;
return window.NativeShell?.AppHost?.appVersion
? window.NativeShell.AppHost.appVersion() : Package.version;
},
getPushTokenInfo: function () {
return {};

View file

@ -379,7 +379,7 @@ import '../../assets/css/scrollstyles.scss';
dlg.setAttribute('data-lockscroll', 'true');
}
if (options.enableHistory === true) {
if (options.enableHistory !== false) {
dlg.setAttribute('data-history', 'true');
}

View file

@ -11,6 +11,7 @@ import { Events } from 'jellyfin-apiclient';
import '../../elements/emby-select/emby-select';
import '../../elements/emby-checkbox/emby-checkbox';
import '../../elements/emby-button/emby-button';
import '../../elements/emby-textarea/emby-textarea';
import ServerConnections from '../ServerConnections';
import toast from '../toast/toast';
import template from './displaySettings.template.html';
@ -122,6 +123,10 @@ import template from './displaySettings.template.html';
context.querySelector('#chkBlurhash').checked = userSettings.enableBlurhash();
context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops();
context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner();
context.querySelector('#chkUseEpisodeImagesInNextUp').checked = userSettings.useEpisodeImagesInNextUpAndResume();
context.querySelector('#chkDisableCustomCss').checked = userSettings.disableCustomCss();
context.querySelector('#txtLocalCustomCss').value = userSettings.customCss();
context.querySelector('#selectLanguage').value = userSettings.language() || '';
context.querySelector('.selectDateTimeLocale').value = userSettings.dateTimeLocale() || '';
@ -156,6 +161,10 @@ import template from './displaySettings.template.html';
userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked);
userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked);
userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked);
userSettingsInstance.useEpisodeImagesInNextUpAndResume(context.querySelector('#chkUseEpisodeImagesInNextUp').checked);
userSettingsInstance.disableCustomCss(context.querySelector('#chkDisableCustomCss').checked);
userSettingsInstance.customCss(context.querySelector('#txtLocalCustomCss').value);
if (user.Id === apiClient.getCurrentUserId()) {
skinManager.setTheme(userSettingsInstance.theme());

View file

@ -156,6 +156,19 @@
<select id="selectTheme" is="emby-select" label="${LabelTheme}"></select>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkDisableCustomCss" />
<span>${DisableCustomCss}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${LabelDisableCustomCss}</div>
</div>
<div class="inputContainer customCssContainer">
<textarea is="emby-textarea" id="txtLocalCustomCss" label="${LabelCustomCss}" class="textarea-mono"></textarea>
<div class="fieldDescription">${LabelLocalCustomCss}</div>
</div>
<div class="selectContainer selectDashboardThemeContainer hide">
<select id="selectDashboardTheme" is="emby-select" label="${LabelDashboardTheme}"></select>
</div>
@ -225,6 +238,14 @@
<div class="fieldDescription checkboxFieldDescription">${DisplayMissingEpisodesWithinSeasonsHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription fldUseEpisodeImagesInNextUp">
<label>
<input type="checkbox" is="emby-checkbox" id="chkUseEpisodeImagesInNextUp" />
<span>${UseEpisodeImagesInNextUp}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${UseEpisodeImagesInNextUpHelp}</div>
</div>
<button is="emby-button" type="submit" class="raised button-submit block btnSave hide">
<span>${Save}</span>
</button>

View file

@ -32,9 +32,6 @@ import template from './filterdialog.template.html';
}
function renderFilters(context, result, query) {
if (result.Tags) {
result.Tags.length = Math.min(result.Tags.length, 50);
}
renderOptions(context, '.genreFilters', 'chkGenreFilter', result.Genres, function (i) {
const delimeter = '|';
return (delimeter + (query.Genres || '') + delimeter).includes(delimeter + i + delimeter);

View file

@ -144,17 +144,17 @@ import ServerConnections from '../ServerConnections';
} else if (section === 'librarybuttons') {
loadlibraryButtons(elem, apiClient, user, userSettings, userViews);
} else if (section === 'resume') {
return loadResume(elem, apiClient, 'HeaderContinueWatching', 'Video');
return loadResume(elem, apiClient, 'HeaderContinueWatching', 'Video', userSettings);
} else if (section === 'resumeaudio') {
return loadResume(elem, apiClient, 'HeaderContinueListening', 'Audio');
return loadResume(elem, apiClient, 'HeaderContinueListening', 'Audio', userSettings);
} else if (section === 'activerecordings') {
loadLatestLiveTvRecordings(elem, true, apiClient);
} else if (section === 'nextup') {
loadNextUp(elem, apiClient);
loadNextUp(elem, apiClient, userSettings);
} else if (section === 'onnow' || section === 'livetv') {
return loadOnNow(elem, apiClient, user);
} else if (section === 'resumebook') {
return loadResume(elem, apiClient, 'HeaderContinueReading', 'Book');
return loadResume(elem, apiClient, 'HeaderContinueReading', 'Book', userSettings);
} else {
elem.innerHTML = '';
return Promise.resolve();
@ -374,7 +374,7 @@ import ServerConnections from '../ServerConnections';
'Video': 'videoplayback,markplayed'
};
function loadResume(elem, apiClient, headerText, mediaType) {
function loadResume(elem, apiClient, headerText, mediaType, userSettings) {
let html = '';
const dataMonitor = dataMonitorHints[mediaType] || 'markplayed';
@ -397,7 +397,7 @@ import ServerConnections from '../ServerConnections';
const itemsContainer = elem.querySelector('.itemsContainer');
itemsContainer.fetchData = getItemsToResumeFn(mediaType, apiClient.serverId());
itemsContainer.getItemsHtml = getItemsToResumeHtml;
itemsContainer.getItemsHtml = getItemsToResumeHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume(), mediaType);
itemsContainer.parentContainer = elem;
}
@ -428,25 +428,28 @@ import ServerConnections from '../ServerConnections';
};
}
function getItemsToResumeHtml(items) {
const cardLayout = false;
return cardBuilder.getCardsHtml({
items: items,
preferThumb: true,
defaultShape: getThumbShape(),
overlayText: false,
showTitle: true,
showParentTitle: true,
lazy: true,
showDetailsMenu: true,
overlayPlayButton: true,
context: 'home',
centerText: !cardLayout,
allowBottomPadding: false,
cardLayout: cardLayout,
showYear: true,
lines: 2
});
function getItemsToResumeHtmlFn(useEpisodeImages, mediaType) {
return function (items) {
const cardLayout = false;
return cardBuilder.getCardsHtml({
items: items,
preferThumb: true,
inheritThumb: !useEpisodeImages,
shape: (mediaType === 'Book') ? getPortraitShape() : getThumbShape(),
overlayText: false,
showTitle: true,
showParentTitle: true,
lazy: true,
showDetailsMenu: true,
overlayPlayButton: true,
context: 'home',
centerText: !cardLayout,
allowBottomPadding: false,
cardLayout: cardLayout,
showYear: true,
lines: 2
});
};
}
function getOnNowFetchFn(serverId) {
@ -607,25 +610,28 @@ import ServerConnections from '../ServerConnections';
};
}
function getNextUpItemsHtml(items) {
const cardLayout = false;
return cardBuilder.getCardsHtml({
items: items,
preferThumb: true,
shape: getThumbShape(),
overlayText: false,
showTitle: true,
showParentTitle: true,
lazy: true,
overlayPlayButton: true,
context: 'home',
centerText: !cardLayout,
allowBottomPadding: !enableScrollX(),
cardLayout: cardLayout
});
function getNextUpItemsHtmlFn(useEpisodeImages) {
return function (items) {
const cardLayout = false;
return cardBuilder.getCardsHtml({
items: items,
preferThumb: true,
inheritThumb: !useEpisodeImages,
shape: getThumbShape(),
overlayText: false,
showTitle: true,
showParentTitle: true,
lazy: true,
overlayPlayButton: true,
context: 'home',
centerText: !cardLayout,
allowBottomPadding: !enableScrollX(),
cardLayout: cardLayout
});
};
}
function loadNextUp(elem, apiClient) {
function loadNextUp(elem, apiClient, userSettings) {
let html = '';
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
@ -660,7 +666,7 @@ import ServerConnections from '../ServerConnections';
const itemsContainer = elem.querySelector('.itemsContainer');
itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId());
itemsContainer.getItemsHtml = getNextUpItemsHtml;
itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume());
itemsContainer.parentContainer = elem;
}

View file

@ -429,7 +429,7 @@ function getPlaybackInfo(player,
enableDirectStream,
allowVideoStreamCopy,
allowAudioStreamCopy) {
if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio') {
if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio' && !player.useServerPlaybackInfoForAudio) {
return Promise.resolve({
MediaSources: [
{
@ -1692,7 +1692,7 @@ class PlaybackManager {
if (validatePlaybackInfoResult(self, result)) {
currentMediaSource = result.MediaSources[0];
const streamInfo = createStreamInfo(apiClient, currentItem.MediaType, currentItem, currentMediaSource, ticks);
const streamInfo = createStreamInfo(apiClient, currentItem.MediaType, currentItem, currentMediaSource, ticks, player);
streamInfo.fullscreen = currentPlayOptions.fullscreen;
streamInfo.lastMediaInfoQuery = lastMediaInfoQuery;
@ -2273,7 +2273,7 @@ class PlaybackManager {
playOptions.items = null;
return getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, mediaSourceId, audioStreamIndex, subtitleStreamIndex).then(function (mediaSource) {
const streamInfo = createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition);
const streamInfo = createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition, player);
streamInfo.fullscreen = playOptions.fullscreen;
@ -2312,7 +2312,7 @@ class PlaybackManager {
return player.getDeviceProfile(item).then(function (deviceProfile) {
return getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, options.mediaSourceId, options.audioStreamIndex, options.subtitleStreamIndex).then(function (mediaSource) {
return createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition);
return createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition, player);
});
});
});
@ -2338,7 +2338,7 @@ class PlaybackManager {
});
};
function createStreamInfo(apiClient, type, item, mediaSource, startPosition) {
function createStreamInfo(apiClient, type, item, mediaSource, startPosition, player) {
let mediaUrl;
let contentType;
let transcodingOffsetTicks = 0;
@ -2350,6 +2350,14 @@ class PlaybackManager {
const mediaSourceContainer = (mediaSource.Container || '').toLowerCase();
let directOptions;
if (mediaSource.MediaStreams && player.useFullSubtitleUrls) {
mediaSource.MediaStreams.forEach(stream => {
if (stream.DeliveryUrl && stream.DeliveryUrl.startsWith('/')) {
stream.DeliveryUrl = apiClient.getUrl(stream.DeliveryUrl);
}
});
}
if (type === 'Video' || type === 'Audio') {
contentType = getMimeType(type.toLowerCase(), mediaSourceContainer);
@ -3009,6 +3017,9 @@ class PlaybackManager {
}
return promise.then(function () {
// Clear the data since we were not listening 'stopped'
getPlayerData(activePlayer).streamInfo = null;
bindStopped(activePlayer);
if (enableLocalPlaylistManagement(activePlayer)) {

View file

@ -41,7 +41,8 @@ function showQualityMenu(player, btn) {
return actionsheet.show({
items: menuItems,
positionTo: btn
positionTo: btn,
enableHistory: false
}).then(function (id) {
const bitrate = parseInt(id);
if (bitrate !== selectedBitrate) {
@ -77,7 +78,8 @@ function showRepeatModeMenu(player, btn) {
return actionsheet.show({
items: menuItems,
positionTo: btn
positionTo: btn,
enableHistory: false
}).then(function (mode) {
if (mode) {
playbackManager.setRepeatMode(mode, player);
@ -138,7 +140,8 @@ function showAspectRatioMenu(player, btn) {
return actionsheet.show({
items: menuItems,
positionTo: btn
positionTo: btn,
enableHistory: false
}).then(function (id) {
if (id) {
playbackManager.setAspectRatio(id, player);
@ -160,7 +163,8 @@ function showPlaybackRateMenu(player, btn) {
return actionsheet.show({
items: menuItems,
positionTo: btn
positionTo: btn,
enableHistory: false
}).then(function (id) {
if (id) {
playbackManager.setPlaybackRate(id, player);
@ -237,7 +241,8 @@ function showWithUser(options, player, user) {
return actionsheet.show({
items: menuItems,
positionTo: options.positionTo
positionTo: options.positionTo,
enableHistory: false
}).then(function (id) {
return handleSelectedOption(id, options, player);
});

View file

@ -173,6 +173,12 @@ import ServerConnections from '../ServerConnections';
value: session.TranscodingInfo.TranscodeReasons.map(translateReason).join('<br/>')
});
}
if (session.TranscodingInfo.HardwareAccelerationType) {
sessionStats.push({
label: globalize.translate('LabelHardwareEncoding'),
value: session.TranscodingInfo.HardwareAccelerationType
});
}
}
return sessionStats;

View file

@ -3,6 +3,11 @@ import globalize from '../scripts/globalize';
import loading from './loading/loading';
import appSettings from '../scripts/settings/appSettings';
import { playbackManager } from './playback/playbackmanager';
import { appHost } from '../components/apphost';
import { appRouter } from '../components/appRouter';
import * as inputManager from '../scripts/inputManager';
import toast from '../components/toast/toast';
import confirm from '../components/confirm/confirm';
/* eslint-disable indent */
@ -90,7 +95,13 @@ import { playbackManager } from './playback/playbackmanager';
events: Events,
loading,
appSettings,
playbackManager
playbackManager,
globalize,
appHost,
appRouter,
inputManager,
toast,
confirm
});
} else {
console.debug(`Loading plugin (via dynamic import): ${pluginSpec}`);

View file

@ -173,6 +173,15 @@ import layoutManager from './layoutManager';
return Math.min(document.documentElement.clientHeight, document.body.clientHeight);
}
/**
* Returns attribute value.
* @param {string} attributeName - Attibute name.
* @return {string} Attibute value.
*/
getAttribute(attributeName) {
return document.body.getAttribute(attributeName);
}
/**
* Returns bounding client rect.
* @return {Rect} Bounding client rect.
@ -201,6 +210,21 @@ import layoutManager from './layoutManager';
*/
const documentScroller = new DocumentScroller();
const scrollerHints = {
x: {
nameScroll: 'scrollWidth',
nameClient: 'clientWidth',
nameStyle: 'overflowX',
nameScrollMode: 'data-scroll-mode-x'
},
y: {
nameScroll: 'scrollHeight',
nameClient: 'clientHeight',
nameStyle: 'overflowY',
nameScrollMode: 'data-scroll-mode-y'
}
};
/**
* Returns parent element that can be scrolled. If no such, returns document scroller.
*
@ -210,23 +234,28 @@ import layoutManager from './layoutManager';
*/
function getScrollableParent(element, vertical) {
if (element) {
let nameScroll = 'scrollWidth';
let nameClient = 'clientWidth';
let nameClass = 'scrollX';
if (vertical) {
nameScroll = 'scrollHeight';
nameClient = 'clientHeight';
nameClass = 'scrollY';
}
const scrollerHint = vertical ? scrollerHints.y : scrollerHints.x;
let parent = element.parentElement;
while (parent) {
// Skip 'emby-scroller' and 'emby-tabs' because they scroll by themselves
if (!parent.classList.contains('emby-scroller') &&
!parent.classList.contains('emby-tabs') &&
parent[nameScroll] > parent[nameClient] && parent.classList.contains(nameClass)) {
while (parent && parent !== document.body) {
const scrollMode = parent.getAttribute(scrollerHint.nameScrollMode);
// Stop on self-scrolled containers
if (scrollMode === 'custom') {
return parent;
}
const styles = window.getComputedStyle(parent);
// Stop on fixed parent
if (styles.position === 'fixed') {
return parent;
}
const overflow = styles[scrollerHint.nameStyle];
if (overflow === 'scroll' || overflow === 'auto' && parent[scrollerHint.nameScroll] > parent[scrollerHint.nameClient]) {
return parent;
}
@ -242,6 +271,8 @@ import layoutManager from './layoutManager';
* @property {number} scrollPos - Current scroll position.
* @property {number} scrollSize - Scroll size.
* @property {number} clientSize - Client size.
* @property {string} mode - Scrolling mode.
* @property {boolean} custom - Custom scrolling mode.
*/
/**
@ -258,12 +289,16 @@ import layoutManager from './layoutManager';
data.scrollPos = scroller.scrollLeft;
data.scrollSize = scroller.scrollWidth;
data.clientSize = scroller.clientWidth;
data.mode = scroller.getAttribute(scrollerHints.x.nameScrollMode);
} else {
data.scrollPos = scroller.scrollTop;
data.scrollSize = scroller.scrollHeight;
data.clientSize = scroller.clientHeight;
data.mode = scroller.getAttribute(scrollerHints.y.nameScrollMode);
}
data.custom = data.mode === 'custom';
return data;
}
@ -348,9 +383,13 @@ import layoutManager from './layoutManager';
const scrollBehavior = smooth ? 'smooth' : 'instant';
if (xScroller !== yScroller) {
scrollToHelper(xScroller, {left: scrollX, behavior: scrollBehavior});
scrollToHelper(yScroller, {top: scrollY, behavior: scrollBehavior});
} else {
if (xScroller) {
scrollToHelper(xScroller, {left: scrollX, behavior: scrollBehavior});
}
if (yScroller) {
scrollToHelper(yScroller, {top: scrollY, behavior: scrollBehavior});
}
} else if (xScroller) {
scrollToHelper(xScroller, {left: scrollX, top: scrollY, behavior: scrollBehavior});
}
}
@ -377,8 +416,8 @@ import layoutManager from './layoutManager';
* @param {number} scrollY - Vertical coordinate.
*/
function animateScroll(xScroller, scrollX, yScroller, scrollY) {
const ox = xScroller.scrollLeft;
const oy = yScroller.scrollTop;
const ox = xScroller ? xScroller.scrollLeft : scrollX;
const oy = yScroller ? yScroller.scrollTop : scrollY;
const dx = scrollX - ox;
const dy = scrollY - oy;
@ -502,30 +541,51 @@ import layoutManager from './layoutManager';
scrollCenterX = scrollCenterY = false;
}
const xScroller = getScrollableParent(element, false);
const yScroller = getScrollableParent(element, true);
const elementRect = element.getBoundingClientRect();
let xScroller = getScrollableParent(element, false);
let yScroller = getScrollableParent(element, true);
const xScrollerData = getScrollerData(xScroller, false);
const yScrollerData = getScrollerData(yScroller, true);
const xPos = getScrollerChildPos(xScroller, element, false);
const yPos = getScrollerChildPos(yScroller, element, true);
const scrollX = calcScroll(xScrollerData, xPos, elementRect.width, scrollCenterX);
let scrollY = calcScroll(yScrollerData, yPos, elementRect.height, scrollCenterY);
// HACK: Scroll to top for top menu because it is hidden
// FIXME: Need a marker to scroll top/bottom
if (isFixed && elementRect.bottom < 0) {
scrollY = 0;
// Exit, since we have no control over scrolling in this container
if (xScroller === yScroller && (xScrollerData.custom || yScrollerData.custom)) {
return;
}
// HACK: Ensure we are at the top
// FIXME: Need a marker to scroll top/bottom
if (scrollY < minimumScrollY() && yScroller === documentScroller) {
scrollY = 0;
// Exit, since we have no control over scrolling in these containers
if (xScrollerData.custom && yScrollerData.custom) {
return;
}
const elementRect = element.getBoundingClientRect();
let scrollX = 0;
let scrollY = 0;
if (!xScrollerData.custom) {
const xPos = getScrollerChildPos(xScroller, element, false);
scrollX = calcScroll(xScrollerData, xPos, elementRect.width, scrollCenterX);
} else {
xScroller = null;
}
if (!yScrollerData.custom) {
const yPos = getScrollerChildPos(yScroller, element, true);
scrollY = calcScroll(yScrollerData, yPos, elementRect.height, scrollCenterY);
// HACK: Scroll to top for top menu because it is hidden
// FIXME: Need a marker to scroll top/bottom
if (isFixed && elementRect.bottom < 0) {
scrollY = 0;
}
// HACK: Ensure we are at the top
// FIXME: Need a marker to scroll top/bottom
if (scrollY < minimumScrollY() && yScroller === documentScroller) {
scrollY = 0;
}
} else {
yScroller = null;
}
doScroll(xScroller, scrollX, yScroller, scrollY, smooth);

View file

@ -118,9 +118,11 @@ class PlaybackCore {
* Sends a buffering request to the server.
* @param {boolean} isBuffering Whether this client is buffering or not.
*/
sendBufferingRequest(isBuffering = true) {
async sendBufferingRequest(isBuffering = true) {
const playerWrapper = this.manager.getPlayerWrapper();
const currentPosition = playerWrapper.currentTime();
const currentPosition = (playerWrapper.currentTimeAsync
? await playerWrapper.currentTimeAsync()
: playerWrapper.currentTime());
const currentPositionTicks = Math.round(currentPosition * Helper.TicksPerMillisecond);
const isPlaying = playerWrapper.isPlaying();
@ -155,7 +157,7 @@ class PlaybackCore {
* Applies a command and checks the playback state if a duplicate command is received.
* @param {Object} command The playback command.
*/
applyCommand(command) {
async applyCommand(command) {
// Check if duplicate.
if (this.lastCommand &&
this.lastCommand.When.getTime() === command.When.getTime() &&
@ -177,7 +179,9 @@ class PlaybackCore {
} else {
// Check if playback state matches requested command.
const playerWrapper = this.manager.getPlayerWrapper();
const currentPositionTicks = Math.round(playerWrapper.currentTime() * Helper.TicksPerMillisecond);
const currentPositionTicks = Math.round((playerWrapper.currentTimeAsync
? await playerWrapper.currentTimeAsync()
: playerWrapper.currentTime()) * Helper.TicksPerMillisecond);
const isPlaying = playerWrapper.isPlaying();
switch (command.Command) {
@ -255,14 +259,16 @@ class PlaybackCore {
* @param {Date} playAtTime The server's UTC time at which to resume playback.
* @param {number} positionTicks The PositionTicks from where to resume.
*/
scheduleUnpause(playAtTime, positionTicks) {
async scheduleUnpause(playAtTime, positionTicks) {
this.clearScheduledCommand();
const enableSyncTimeout = this.maxDelaySpeedToSync / 2.0;
const currentTime = new Date();
const playAtTimeLocal = this.timeSyncCore.remoteDateToLocal(playAtTime);
const playerWrapper = this.manager.getPlayerWrapper();
const currentPositionTicks = playerWrapper.currentTime() * Helper.TicksPerMillisecond;
const currentPositionTicks = (playerWrapper.currentTimeAsync
? await playerWrapper.currentTimeAsync()
: playerWrapper.currentTime()) * Helper.TicksPerMillisecond;
if (playAtTimeLocal > currentTime) {
const playTimeout = playAtTimeLocal - currentTime;

View file

@ -167,14 +167,16 @@ class QueueCore {
* @param {string} origin The origin of the wait call, used for debug.
*/
scheduleReadyRequestOnPlaybackStart(apiClient, origin) {
Helper.waitForEventOnce(this.manager, 'playbackstart', Helper.WaitForEventDefaultTimeout, ['playbackerror']).then(() => {
Helper.waitForEventOnce(this.manager, 'playbackstart', Helper.WaitForEventDefaultTimeout, ['playbackerror']).then(async () => {
console.debug('SyncPlay scheduleReadyRequestOnPlaybackStart: local pause and notify server.');
const playerWrapper = this.manager.getPlayerWrapper();
playerWrapper.localPause();
const currentTime = new Date();
const now = this.manager.timeSyncCore.localDateToRemote(currentTime);
const currentPosition = playerWrapper.currentTime();
const currentPosition = (playerWrapper.currentTimeAsync
? await playerWrapper.currentTimeAsync()
: playerWrapper.currentTime());
const currentPositionTicks = Math.round(currentPosition * Helper.TicksPerMillisecond);
const isPlaying = playerWrapper.isPlaying();

View file

@ -44,13 +44,15 @@ class PlayerFactory {
return this.getDefaultWrapper(syncPlayManager);
}
console.debug('SyncPlay WrapperFactory getWrapper:', player.id);
const Wrapper = this.wrappers[player.id];
const playerId = player.syncPlayWrapAs || player.id;
console.debug('SyncPlay WrapperFactory getWrapper:', playerId);
const Wrapper = this.wrappers[playerId];
if (Wrapper) {
return new Wrapper(player, syncPlayManager);
}
console.debug(`SyncPlay WrapperFactory getWrapper: unknown player ${player.id}, using default wrapper.`);
console.debug(`SyncPlay WrapperFactory getWrapper: unknown player ${playerId}, using default wrapper.`);
return this.getDefaultWrapper(syncPlayManager);
}

View file

@ -63,7 +63,8 @@ class GroupSelectionMenu {
items: menuItems,
positionTo: button,
resolveOnClick: true,
border: true
border: true,
enableHistory: false
};
actionsheet.show(menuOptions).then(function (id) {
@ -132,7 +133,8 @@ class GroupSelectionMenu {
items: menuItems,
positionTo: button,
resolveOnClick: true,
border: true
border: true,
enableHistory: false
};
actionsheet.show(menuOptions).then(function (id) {

View file

@ -1,3 +1,5 @@
import { appHost } from '../../apphost';
/**
* Creates an audio element that plays a silent sound.
* @returns {HTMLMediaElement} The audio element.
@ -33,6 +35,10 @@ class PlaybackPermissionManager {
* @returns {Promise} Promise that resolves succesfully if playback permission is allowed.
*/
check () {
if (appHost.supports('htmlaudioautoplay')) {
return Promise.resolve(true);
}
return new Promise((resolve, reject) => {
const media = createTestMediaElement();
media.play().then(() => {

View file

@ -17,6 +17,16 @@ class HtmlVideoPlayer extends NoActivePlayer {
this.isPlayerActive = false;
this.savedPlaybackRate = 1.0;
this.minBufferingThresholdMillis = 3000;
if (player.currentTimeAsync) {
/**
* Gets current playback position.
* @returns {Promise<number>} The player position, in milliseconds.
*/
this.currentTimeAsync = () => {
return this.player.currentTimeAsync();
};
}
}
/**

View file

@ -94,13 +94,13 @@ import '../../assets/css/flexstyles.scss';
}
}
function onStartNowClick() {
async function onStartNowClick() {
const options = this.options;
if (options) {
const player = options.player;
this.hide();
await this.hide();
playbackManager.nextTrack(player);
}
@ -139,7 +139,7 @@ import '../../assets/css/flexstyles.scss';
Events.trigger(instance, 'hide');
}
function hideComingUpNext() {
async function hideComingUpNext() {
const instance = this;
clearCountdownTextTimeout(this);
@ -159,17 +159,21 @@ import '../../assets/css/flexstyles.scss';
return;
}
// trigger a reflow to force it to animate again
void elem.offsetWidth;
elem.classList.add('upNextDialog-hidden');
const fn = onHideAnimationComplete.bind(instance);
instance._onHideAnimationComplete = fn;
dom.addEventListener(elem, transitionEndEventName, fn, {
once: true
const transitionEvent = await new Promise((resolve) => {
dom.addEventListener(elem, transitionEndEventName, resolve, {
once: true
});
// trigger a reflow to force it to animate again
void elem.offsetWidth;
elem.classList.add('upNextDialog-hidden');
});
instance._onHideAnimationComplete(transitionEvent);
}
function getTimeRemainingMs(instance) {
@ -226,8 +230,8 @@ class UpNextDialog {
startComingUpNextHideTimer(this);
}
hide() {
hideComingUpNext.call(this);
async hide() {
await hideComingUpNext.bind(this)();
}
destroy() {
hideComingUpNext.call(this);