openlp/openlp/core/display/html/display.js

1426 lines
46 KiB
JavaScript

/**
* display.js is the main Javascript file that is used to drive the display.
*/
/**
* Background type enumeration
*/
var BackgroundType = {
Transparent: "transparent",
Solid: "solid",
Gradient: "gradient",
Video: "video",
Image: "image"
};
/**
* Gradient type enumeration
*/
var GradientType = {
Horizontal: "horizontal",
LeftTop: "leftTop",
LeftBottom: "leftBottom",
Vertical: "vertical",
Circular: "circular"
};
/**
* Horizontal alignment enumeration
*/
var HorizontalAlign = {
Left: 0,
Right: 1,
Center: 2,
Justify: 3
};
/**
* Vertical alignment enumeration
*/
var VerticalAlign = {
Top: 0,
Middle: 1,
Bottom: 2
};
/**
* Transition type enumeration
*/
var TransitionType = {
Fade: 0,
Slide: 1,
Convex: 2,
Concave: 3,
Zoom: 4
};
/**
* Transition speed enumeration
*/
var TransitionSpeed = {
Normal: 0,
Fast: 1,
Slow: 2
};
/**
* Transition direction enumeration
*/
var TransitionDirection = {
Horizontal: 0,
Vertical: 1
};
/**
* Audio state enumeration
*/
var AudioState = {
Playing: "playing",
Paused: "paused",
Stopped: "stopped"
};
/**
* Transition state enumeration
*/
var TransitionState = {
EntranceTransition: "entranceTransition",
NoTransition: "noTransition",
ExitTransition: "exitTransition"
};
/**
* Animation state enumeration
*/
var AnimationState = {
NoAnimation: "noAnimation",
ScrollingText: "scrollingText",
NonScrollingText: "noScrollingText"
};
/**
* Alert location enumeration
*/
var AlertLocation = {
Top: 0,
Middle: 1,
Bottom: 2
};
/**
* Alert state enumeration
*/
var AlertState = {
Displaying: "displaying",
NotDisplaying: "notDisplaying"
};
/**
* Alert delay enumeration
*/
var AlertDelay = {
FiftyMilliseconds: 50,
OneSecond: 1000,
OnePointFiveSeconds: 1500
};
/**
* Return an array of elements based on the selector query
* @param {string} selector - The selector to find elements
* @returns {array} An array of matching elements
*/
function $(selector) {
return Array.from(document.querySelectorAll(selector));
}
/**
* Build linear gradient CSS
* @private
* @param {string} direction - The direction or angle of the gradient e.g. to bottom or 90deg
* @param {string} startColor - The starting color
* @param {string} endColor - The ending color
* @returns {string} A string of the gradient CSS
*/
function _buildLinearGradient(direction, startColor, endColor) {
return "linear-gradient(" + direction + ", " + startColor + ", " + endColor + ") fixed";
}
/**
* Build radial gradient CSS
* @private
* @param {string} width - Width of the gradient
* @param {string} startColor - The starting color
* @param {string} endColor - The ending color
* @returns {string} A string of the gradient CSS
*/
function _buildRadialGradient(width, startColor, endColor) {
return "radial-gradient(" + startColor + ", " + endColor + ") fixed";
}
/**
* Build a set of text shadows to form an outline
* @private
* @param {Number} size - The desired width of the outline
* @param {string} color - The color of the outline
* @returns {array} A list of shadows to be given to "text-shadow"
*/
function _buildTextOutline(size, color) {
let shadows = [];
// Outlines work from -(size) to +(size)
let from = size * -1;
// Loop through all the possible size iterations and add them to the array
for (let i = from; i <= size; i++) {
for (let j = from; j <= size; j++) {
shadows.push(color + " " + i + "pt " + j + "pt 0pt");
}
}
return shadows;
}
/**
* Build a text shadow
* @private
* @param {Number} offset - The offset of the shadow
* @param {string} color - The color that the shadow should be
* @returns {string} The text-shadow rule
*/
function _buildTextShadow(offset, size, color) {
let shadows = [];
let from = (size * -1) + offset;
let to = size + offset;
for (let i = from; i <= to; i++) {
for (let j = from; j <= to; j++) {
shadows.push(color + " " + i + "pt " + j + "pt 0pt");
}
}
return shadows.join(", ");
}
/**
* Get a style value from an element (computed or manual)
* @private
* @param {Object} element - The element whose style we want
* @param {string} style - The name of the style we want
* @returns {(Number|string)} The style value (type depends on the style)
*/
function _getStyle(element, style) {
return document.defaultView.getComputedStyle(element).getPropertyValue(style);
}
/**
* Convert newlines to <br> tags
* @private
* @param {string} text - The text to parse
* @returns {string} The text now with <br> tags
*/
function _nl2br(text) {
return text.replace("\r\n", "\n").replace("\n", "<br>");
}
/**
* Prepare text by creating paragraphs and calling _nl2br to convert newlines to <br> tags
* @private
* @param {string} text - The text to parse
* @returns {string} The text now with <p> and <br> tags
*/
function _prepareText(text) {
return "<p>" + _nl2br(text) + "</p>";
}
/**
* Change a camelCaseString to a camel-case-string
* @private
* @param {string} text
* @returns {string} the Un-camel-case-ified string
*/
function _fromCamelCase(text) {
return text.replace(/([A-Z])/g, function (match, submatch) {
return '-' + submatch.toLowerCase();
});
}
/**
* Create a CSS style
* @private
* @param {string} selector - The selector for this style
* @param {Object} rules - The rules to apply to the style
*/
function _createStyle(selector, rules) {
var style;
var id = selector.replace("#", "").replace(" .", "-").replace(".", "-").replace(" ", "_");
if ($("style#" + id).length != 0) {
style = $("style#" + id)[0];
}
else {
style = document.createElement("style");
document.getElementsByTagName("head")[0].appendChild(style);
style.type = "text/css";
style.id = id;
}
var rulesString = selector + " { ";
for (var key in rules) {
var ruleValue = rules[key];
var ruleKey = _fromCamelCase(key);
rulesString += "" + ruleKey + ": " + ruleValue + ";";
}
rulesString += " } ";
if (style.styleSheet) {
style.styleSheet.cssText = rulesString;
}
else {
style.appendChild(document.createTextNode(rulesString));
}
}
/**
* Fixes font name to match CSS names.
* @param {string} fontName Font Name
* @returns Fixed Font Name
*/
function _fixFontName(fontName) {
if (!fontName || (fontName == 'Sans Serif')) {
return 'sans-serif';
}
return "'" + fontName + "'";
}
/**
* The Display object is what we use from OpenLP
*/
var Display = {
/** @type {HTMLElement} */
_slidesContainer: null,
/** @type {HTMLElement} */
_footerContainer: null,
/** @type {HTMLElement} */
_backgroundsContainer: null,
_alerts: [],
_slides: {},
_alertSettings: {},
_alertState: AlertState.NotDisplaying,
_transitionState: TransitionState.NoTransition,
_animationState: AnimationState.NoAnimation,
_doTransitions: false,
_doItemTransitions: false,
_skipNextTransition: false,
_themeApplied: true,
_revealConfig: {
margin: 0.0,
minScale: 1.0,
maxScale: 1.0,
controls: false,
progress: false,
history: false,
keyboard: false,
overview: false,
center: false,
touch: false,
help: false,
transition: "none",
backgroundTransition: "none",
viewDistance: 9999,
width: "100%",
height: "100%"
},
_lastRequestAnimationFrameHandle: null,
/**
* Start up reveal and do any other initialisation
* @param {object} options - The initialisation options:
* * {bool} isDisplay - Is this a real display output
* * {bool} doItemTransitions - Transition between service items
* * {bool} hideMouse - Hide the cursor when hovering on this display
*/
init: function (options) {
// Set defaults for undefined values
options = options || {};
let isDisplay = options.isDisplay || false;
let doItemTransitions = options.doItemTransitions || false;
let hideMouse = options.hideMouse || false;
if (options.slideNumbersInFooter) {
Display._revealConfig.slideNumber = Display.setFooterSlideNumbers;
}
// Now continue to initialisation
if (!isDisplay) {
document.body.classList.add('checkerboard');
}
if (hideMouse) {
document.body.classList.add('hide-mouse');
}
Display._slidesContainer = $(".slides")[0];
Display._footerContainer = $(".footer")[0];
Display._backgroundsContainer = $(".backgrounds")[0];
Display._doTransitions = isDisplay;
Reveal.initialize(Display._revealConfig);
Reveal.addEventListener('slidechanged', Display._onSlideChanged);
Display.setItemTransition(doItemTransitions && isDisplay);
displayWatcher.setInitialised(true);
},
/**
* Reinitialise Reveal
*/
reinit: function () {
Reveal.sync();
// Python expects to be on first page after reinit
Reveal.slide(0);
},
/**
* Enable/Disable item transitions
*/
setItemTransition: function (enable) {
Display._doItemTransitions = enable;
var body = $("body")[0];
if (enable) {
body.classList.add("transition");
Reveal.configure({"backgroundTransition": "fade", "transitionSpeed": "default"});
} else {
body.classList.remove("transition");
Reveal.configure({"backgroundTransition": "none"});
}
},
/**
* Clear the current list of slides
*/
clearSlides: function () {
Display._slidesContainer.innerHTML = "";
Display._clearSlidesList();
},
/**
* Clear the current list of slides
*/
_clearSlidesList: function () {
Display._footerContainer.innerHTML = "";
Display._slides = {};
},
/**
* Add new item/slides, replacing the old one
* Clears current list of slides but allows time for a transition animation
* Items are ordered newest to oldest in the slides container
* @param {element} new_slides - New slides to display
* @param {element} is_text - Used to decide if the theme main area constraints should apply
*/
replaceSlides: function (new_slides, is_text=false) {
if (Display._doItemTransitions) {
new_slides.setAttribute('data-transition', "fade");
new_slides.setAttribute('data-transition-speed', "default");
}
new_slides.classList.add("future");
Display.applyTheme(new_slides, is_text);
Display._slidesContainer.prepend(new_slides);
var currentSlide = Reveal.getIndices();
if (Display._doItemTransitions && Display._slidesContainer.children.length >= 2 && !Display._skipNextTransition) {
// Set the slide one section ahead so we'll stay on the old slide after reinit
Reveal.slide(1, currentSlide.v);
Display.reinit();
// Timeout to allow time to transition before deleting the old slides
setTimeout (Display._removeLastSection, 5000);
} else {
Reveal.slide(0, currentSlide.v);
Reveal.sync();
Display._removeLastSection();
Display._skipNextTransition = false;
}
},
/**
* Removes the last slides item if there are more than one
*/
_removeLastSection: function () {
if (Display._slidesContainer.children.length > 1) {
Display._slidesContainer.lastChild.remove();
}
},
/**
* Checks if the present slide content fits within the slide
*/
doesContentFit: function () {
var currSlide = $("section.text-slides");
if (currSlide.length === 0) {
currSlide = Display._footerContainer;
} else {
currSlide = currSlide[0];
}
return currSlide.clientHeight >= currSlide.scrollHeight;
},
/**
* Generate the OpenLP startup splashscreen
* @param {string} bg_color - The background color
* @param {string} image - Path to the splash image
*/
setStartupSplashScreen: function(bg_color, image) {
Display._clearSlidesList();
var section = document.createElement("section");
section.setAttribute("id", 0);
section.setAttribute("data-background", bg_color);
section.setAttribute("style", "height: 100%; width: 100%;");
var img = document.createElement('img');
img.src = Display._getFileUrl(image);
img.setAttribute("style", "position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; max-height: 100%; max-width: 100%");
section.appendChild(img);
Display._slides['0'] = 0;
Display.replaceSlides(section);
},
/**
* Set fullscreen image from path
* @param {string} bg_color - The background color
* @param {string} image - Path to the image
*/
setFullscreenImage: function(bg_color, image) {
Display.clearSlides();
var section = document.createElement("section");
section.setAttribute("id", 0);
section.setAttribute("data-background", bg_color);
section.setAttribute("style", "height: 100%; width: 100%;");
var img = document.createElement('img');
img.src = image;
img.setAttribute("style", "height: 100%; width: 100%");
section.appendChild(img);
Display._slides['0'] = 0;
Display.replaceSlides(section);
},
/**
* Set fullscreen image from base64 data
* @param {string} bg_color - The background color
* @param {string} image_data - base64 encoded image data
*/
setFullscreenImageFromData: function(bg_color, image_data) {
Display.clearSlides();
var section = document.createElement("section");
section.setAttribute("id", 0);
section.setAttribute("data-background", bg_color);
section.setAttribute("style", "height: 100%; width: 100%;");
var img = document.createElement('img');
img.src = 'data:image/jpeg;base64,' + image_data;
img.setAttribute("style", "height: 100%; width: 100%");
section.appendChild(img);
Display._slidesContainer.appendChild(section);
Display._slides['0'] = 0;
Display.reinit();
},
/**
* Display an alert. If there's an alert already showing, add this one to the queue
* @param {string} text - The alert text
* @param {Object} JSON object - The settings for the alert object
*/
alert: function (text, settings) {
if (text == "") {
return null;
}
if (Display._alertState === AlertState.Displaying) {
Display.addAlertToQueue(text, settings);
}
else {
Display.showAlert(text, settings);
}
},
/**
* Show the alert on the screen
* @param {string} text - The alert text
* @param {Object} JSON object - The settings for the alert
*/
showAlert: function (text, settings) {
var alertBackground = $('#alert-background')[0];
var alertText = $('#alert-text')[0];
// create styles for the alerts from the settings
_createStyle("#alert-background.settings", {
backgroundColor: settings.backgroundColor,
fontFamily: _fixFontName(settings.fontFace),
fontSize: settings.fontSize.toString() + "pt",
color: settings.fontColor
});
alertBackground.classList.add("settings");
alertBackground.classList.replace("hide", "show");
alertText.innerHTML = text;
Display.setAlertLocation(settings.location);
Display._transitionState = TransitionState.EntranceTransition;
/* Check if the alert is a queued alert */
if (Display._alertState !== AlertState.Displaying) {
Display._alertState = AlertState.Displaying;
}
alertBackground.addEventListener('transitionend', Display.alertTransitionEndEvent, false);
alertText.addEventListener('animationend', Display.alertAnimationEndEvent, false);
/* Either scroll the alert, or make it disappear at the end of its time */
if (settings.scroll) {
Display._animationState = AnimationState.ScrollingText;
alertText.classList.add('scrolling');
alertText.classList.replace("hide", "show");
var animationSettings = "alert-scrolling-text " + settings.timeout +
"s linear 0.6s " + settings.repeat + " normal";
alertText.style.animation = animationSettings;
}
else {
Display._animationState = AnimationState.NonScrollingText;
alertText.classList.replace("hide", "show");
setTimeout (function () {
Display._animationState = AnimationState.NoAnimation;
Display.hideAlert();
}, settings.timeout * AlertDelay.OneSecond);
}
},
/**
* Hide the alert at the end
*/
hideAlert: function () {
var alertBackground = $('#alert-background')[0];
var alertText = $('#alert-text')[0];
Display._transitionState = TransitionState.ExitTransition;
alertText.classList.replace("show", "hide");
alertBackground.classList.replace("show", "hide");
alertText.style.animation = "";
Display._alertState = AlertState.NotDisplaying;
},
/**
* Add an alert to the alert queue
* @param {string} text - The alert text to be displayed
* @param {Object} setttings - JSON object containing the settings for the alert
*/
addAlertToQueue: function (text, settings) {
Display._alerts.push({text: text, settings: settings});
},
/**
* The alertTransitionEndEvent called after a transition has ended
*/
alertTransitionEndEvent: function (e) {
e.stopPropagation();
if (Display._transitionState === TransitionState.EntranceTransition) {
Display._transitionState = TransitionState.NoTransition;
}
else if (Display._transitionState === TransitionState.ExitTransition) {
Display._transitionState = TransitionState.NoTransition;
Display.hideAlert();
Display.showNextAlert();
}
},
/**
* The alertAnimationEndEvent called after an animation has ended
*/
alertAnimationEndEvent: function (e) {
e.stopPropagation();
Display.hideAlert();
},
/**
* Set the location of the alert
* @param {int} location - Integer number with the location of the alert on screen
*/
setAlertLocation: function (location) {
var alertContainer = $(".alert-container")[0];
// Remove an existing location classes
alertContainer.classList.remove("top");
alertContainer.classList.remove("middle");
alertContainer.classList.remove("bottom");
// Apply the location class we want
switch (location) {
case AlertLocation.Top:
alertContainer.classList.add("top");
break;
case AlertLocation.Middle:
alertContainer.classList.add("middle");
break;
case AlertLocation.Bottom:
default:
alertContainer.classList.add("bottom");
break;
}
},
/**
* Display the next alert in the queue
*/
showNextAlert: function () {
if (Display._alerts.length > 0) {
var alertObject = Display._alerts.shift();
Display._alertState = AlertState.DisplayingFromQueue;
Display.showAlert(alertObject.text, alertObject.settings);
}
else {
// For the tests
return null;
}
},
/**
* Create a text slide.
* @param {string} verse - The verse number, e.g. "v1"
* @param {string} text - The HTML for the verse, e.g. "line1<br>line2"
*/
_createTextSlide: function (verse, text) {
var slide;
var html = _prepareText(text);
slide = document.createElement("section");
slide.setAttribute("id", verse);
// The "future" class is used internally by reveal, it's used here to hide newly added slides
slide.classList.add("future");
slide.innerHTML = html;
return slide;
},
_onSlideChanged: function(event) {
Display._footerContainer.querySelectorAll('.footer-item')
.forEach(footerItem => footerItem.classList.remove('active'));
var currentSlideNth = parseInt(event.currentSlide.getAttribute('data-slide'));
var newActiveFooter = Display._footerContainer.querySelector('.footer-item[data-slide="' + currentSlideNth + '"]');
if (newActiveFooter) {
newActiveFooter.classList.add('active');
}
},
/**
* Set text slides.
* @param {Object[]} slides - A list of slides to add as JS objects: {"verse": "v1", "text": "line 1\nline2"}
*/
setTextSlides: function (slides) {
Display._clearSlidesList();
var slide_html;
var parentSection = document.createElement("section");
parentSection.classList = "text-slides";
slides.forEach(function (slide, index) {
slide_html = Display._createTextSlide(slide.verse, slide.text);
slide_html.setAttribute('data-slide', index);
parentSection.appendChild(slide_html);
Display._slides[slide.verse] = parentSection.children.length - 1;
if (slide.footer) {
var footerSlide = document.createElement('div');
footerSlide.classList.add('footer-item');
footerSlide.setAttribute('data-slide', index);
var currentSlide = Reveal.getIndices();
if (currentSlide) {
currentSlide = currentSlide.v;
} else {
currentSlide = 0;
}
if (index == currentSlide) {
footerSlide.classList.add('active');
}
footerSlide.innerHTML = slide.footer;
Display._footerContainer.append(footerSlide);
}
});
Display.replaceSlides(parentSection, true);
},
/**
* Set a single text slide. This changes the slide with no transition.
* Prevents the need to reapply the theme if only changing content.
* @param String slide - Text to put on the slide
*/
setTextSlide: function (text) {
if (Display._slides.hasOwnProperty("test-slide") && Object.keys(Display._slides).length === 1) {
var slide = $("#" + "test-slide")[0];
var html = _prepareText(text);
if (slide.innerHTML != html) {
slide.innerHTML = html;
}
if (!Display._themeApplied) {
Display.applyTheme(slide.parent);
}
} else {
Display._clearSlidesList();
var slide_html;
var parentSection = document.createElement("section");
parentSection.classList = "text-slides";
slide_html = Display._createTextSlide("test-slide", text);
slide_html.setAttribute('data-slide', 0);
parentSection.appendChild(slide_html);
Display._slides["test-slide"] = 0;
Display.applyTheme(parentSection);
Display._slidesContainer.innerHTML = "";
Display._slidesContainer.prepend(parentSection);
Display.reinit();
}
},
/**
* Set image slides
* @param {Object[]} slides - A list of images to add as JS objects [{"path": "url/to/file"}]
*/
setImageSlides: function (slides) {
Display._clearSlidesList();
var parentSection = document.createElement("section");
slides.forEach(function (slide, index) {
var section = document.createElement("section");
section.setAttribute("id", index);
section.setAttribute("style", "height: 100%; width: 100%;");
var img = document.createElement('img');
img.src = Display._getFileUrl(slide.path);
img.setAttribute("style", "width: 100%; height: 100%; margin: 0; object-fit: contain;");
img.setAttribute('data-slide', index);
section.appendChild(img);
parentSection.appendChild(section);
Display._slides[index.toString()] = index;
});
Display.replaceSlides(parentSection);
},
/**
* Set a video
* @param {Object} video - The video to show as a JS object: {"path": "url/to/file"}
*/
setVideo: function (video) {
Display._clearSlidesList();
var section = document.createElement("section");
section.setAttribute("data-background", "#000");
var videoElement = document.createElement("video");
videoElement.src = video.path;
videoElement.preload = "auto";
videoElement.setAttribute("id", "video");
videoElement.setAttribute("style", "height: 100%; width: 100%;");
videoElement.setAttribute('data-slide', 0);
videoElement.autoplay = false;
// All the update methods below are Python functions, hence not camelCase
videoElement.addEventListener("durationchange", function (event) {
mediaWatcher.update_duration(event.target.duration);
});
videoElement.addEventListener("timeupdate", function (event) {
mediaWatcher.update_progress(event.target.currentTime);
});
videoElement.addEventListener("volumeupdate", function (event) {
mediaWatcher.update_volume(event.target.volume);
});
videoElement.addEventListener("ratechange", function (event) {
mediaWatcher.update_playback_rate(event.target.playbackRate);
});
videoElement.addEventListener("ended", function (event) {
mediaWatcher.has_ended(event.target.ended);
});
videoElement.addEventListener("muted", function (event) {
mediaWatcher.has_muted(event.target.muted);
});
section.appendChild(videoElement);
Display.replaceSlides(section);
},
/**
* Play a video
*/
playVideo: function () {
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].play();
}
},
/**
* Pause a video
*/
pauseVideo: function () {
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].pause();
}
},
/**
* Stop a video
*/
stopVideo: function () {
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].pause();
videoElem[0].currentTime = 0.0;
}
},
/**
* Go to a particular time in a video
* @param seconds The position in seconds to seek to
*/
seekVideo: function (seconds) {
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].currentTime = seconds;
}
},
/**
* Set the playback rate of a video
* @param rate A Double of the rate. 1.0 => 100% speed, 0.75 => 75% speed, 1.25 => 125% speed, etc.
*/
setPlaybackRate: function (rate) {
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].playbackRate = rate;
}
},
/**
* Set the volume
* @param level The volume level from 0 to 100.
*/
setVideoVolume: function (level) {
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].volume = level / 100.0;
}
},
/**
* Mute the volume
*/
toggleVideoMute: function () {
var videoElem = $("#video");
if (videoElem.length == 1) {
videoElem[0].muted = !videoElem[0].muted;
}
},
/**
* Clear the background audio playlist
*/
clearPlaylist: function () {
var backgroundAudoElem = $("#background-audio");
if (backgroundAudoElem.length == 1) {
var audio = backgroundAudoElem[0];
/* audio.playList */
}
},
/**
* Add background audio
* @param files The list of files as objects in an array
*/
addBackgroundAudio: function (files) {
},
/**
* Go to a slide.
* @param slide The slide number or name, e.g. "v1", 0
*/
goToSlide: function (slide) {
if (Display._slides.hasOwnProperty(slide)) {
Reveal.slide(0, Display._slides[slide]);
}
else {
Reveal.slide(0, slide);
}
},
/**
* Go to the next slide in the list
*/
next: Reveal.nextFragment,
/**
* Go to the previous slide in the list
*/
prev: Reveal.prevFragment,
/**
* Blank the screen
*/
toBlack: function () {
return new Promise((resolve, reject) => {
/* Avoid race conditions where display goes to transparent and quickly goes to black */
Display._abortLastTransitionOperation();
/*
Reveal's black overlay should be shown before the transitions are
restored, to avoid screen flashes
*/
Display._restorePauseBehavior();
Display._requestAnimationFrameExclusive(function() {
if (!Reveal.isPaused()) {
Reveal.togglePause();
}
Display._reenableGlobalTransitions(function() {
var documentBody = $("body")[0];
documentBody.style.opacity = 1;
resolve();
});
});
});
},
/**
* Hide all but theme background
*/
toTheme: function () {
return new Promise((resolve, reject) => {
Display._abortLastTransitionOperation();
/*
Reveal's black overlay should be shown before the transitions are
restored, to avoid screen flashes
*/
Display._restorePauseBehavior();
var documentBody = $("body")[0];
documentBody.style.opacity = 1;
Display._slidesContainer.style.opacity = 0;
Display._footerContainer.style.opacity = 0;
if (Reveal.isPaused()) {
Reveal.togglePause();
}
Display._reenableGlobalTransitions(function() {
resolve();
});
});
},
/**
* Hide everything (CAUTION: Causes a invisible mouse barrier)
*/
toTransparent: function () {
return new Promise((resolve, reject) => {
Display._abortLastTransitionOperation();
var documentBody = $("body")[0];
documentBody.style.opacity = 0;
if (!Reveal.isPaused()) {
/*
Removing previously the overlay if it's not paused, to avoid a
content flash while going from black screen to transparent
*/
document.body.classList.add('is-desktop');
Reveal.togglePause();
}
/*
Waiting for body transition to happen, now it would be safe to
hide the Webview (as other transitions were suppressed)
*/
Display._abortLastTransitionOperation();
Display._addTransitionEndEventToBody(transitionEndEvent);
function transitionEndEvent(e) {
// Targeting only body
if (e.target != documentBody) {
return;
}
/*
Disabling all transitions (except body) to allow the Webview to attain the
transparent state before it gets hidden by Qt.
*/
document.body.classList.add('disable-transitions');
document.body.classList.add('is-desktop');
Display._slidesContainer.style.opacity = 0;
Display._footerContainer.style.opacity = 0;
/*
Repainting before hiding the Webview to avoid flashes when
showing it again.
*/
displayWatcher.pleaseRepaint();
/* Waiting for repaint to happen before saying that it's done. */
Display._requestAnimationFrameExclusive(function() {
/* We're transparent now, aborting any transition event between */
Display._abortLastTransitionOperation();
resolve();
});
}
});
},
/**
* Show the screen
*/
show: function () {
return new Promise((resolve, reject) => {
var documentBody = $("body")[0];
/*
Removing transitionend event, avoids the content being hidden if the user
tries to show content again before toTransparent() transitionend event
happens
*/
Display._abortLastTransitionOperation();
Display._slidesContainer.style.opacity = 1;
Display._footerContainer.style.opacity = 1;
if (Reveal.isPaused()) {
Reveal.togglePause();
}
Display._restorePauseBehavior();
Display._reenableGlobalTransitions(function() {
documentBody.style.opacity = 1;
resolve();
});
});
},
_reenableGlobalTransitions: function(afterCallback) {
Display._requestAnimationFrameExclusive(function() {
/*
Waiting for the previous opacity + unpause operations to complete
to restore the transitions behavior
*/
document.body.classList.remove('disable-transitions');
if (typeof afterCallback === 'function') {
afterCallback();
}
});
},
/**
* Shows again the Reveal's black pause overlay that was
* hidden before Webview was hidden
*/
_restorePauseBehavior: function() {
document.body.classList.remove('is-desktop');
},
/**
* Cancels previous requested animationFrame.
* Last animationFrame should be aborted to avoid race condition bugs when
* the user changes the view modes too quickly, for example.
*/
_requestAnimationFrameExclusive: function(callback) {
cancelAnimationFrame(Display._lastRequestAnimationFrameHandle);
Display._lastRequestAnimationFrameHandle = requestAnimationFrame(callback);
},
/**
* Aborts last body's transitionend and requestAnimationFrame's events, to avoid
* race condition bugs.
*/
_abortLastTransitionOperation: function() {
Display._removeTransitionEndEventToBody();
cancelAnimationFrame(Display._lastRequestAnimationFrameHandle);
},
/**
* Intercepts the addEventListener call and stores it, so that it acts
* like the ontransitionend GlobalEventHandler.
*/
_addTransitionEndEventToBody: function(listener) {
Display._lastTransitionEndBodyEvent = listener;
document.body.addEventListener('transitionend', listener);
},
_removeTransitionEndEventToBody: function() {
document.body.removeEventListener('transitionend', Display._lastTransitionEndBodyEvent);
},
/**
* Figure out how many lines can fit on a slide given the font size
* @param fontSize The font size in pts
*/
calculateLineCount: function (fontSize) {
var p = $(".slides > section > section > p");
if (p.length == 0) {
Display.addSlide("v1", "Arky arky");
p = $(".slides > section > section > p");
}
p = p[0];
p.style.fontSize = "" + fontSize + "pt";
var d = $(".slides > section")[0];
var lh = parseFloat(_getStyle(p, "line-height"));
var dh = parseFloat(_getStyle(d, "height"));
return Math.floor(dh / lh);
},
/**
* Prepare the theme for the next item to be added
* @param theme The theme to be used
*/
setTheme: function (theme) {
if (Display._theme != theme) {
Display._themeApplied = false;
Display._theme = theme;
}
},
/**
* Set background image, replaced when theme is updated/applied
* @param image_path Image path
*/
setBackgroundImage: function (image_path) {
var targetElement = $(".slides > section")[0];
targetElement.setAttribute("data-background", "url('" + image_path + "')");
targetElement.setAttribute("data-background-size", "cover");
Reveal.sync();
},
/**
* Reset/reapply the theme
*/
resetTheme: function () {
var targetElement = $(".slides > section")[0];
if (!targetElement) {
console.warn("Couldn't reset theme: No slides exist");
return;
}
Display.applyTheme(targetElement, targetElement.classList.contains("text-slides"));
Reveal.sync();
},
/**
* Apply the theme to the provided element
* @param targetElement The target element to apply the theme (expected to be a <section> in the slides container)
* @param is_text Used to decide if the main area constraints should be applied
*/
applyTheme: function (targetElement, is_text=true) {
Display._themeApplied = true;
if (!Display._theme) {
return;
}
// Set slide transitions
var new_transition_type = "none",
new_transition_speed = "default";
if (!!Display._theme.display_slide_transition && Display._doTransitions) {
switch (Display._theme.display_slide_transition_type) {
case TransitionType.Fade:
new_transition_type = "fade";
break;
case TransitionType.Slide:
new_transition_type = "slide";
break;
case TransitionType.Convex:
new_transition_type = "convex";
break;
case TransitionType.Concave:
new_transition_type = "concave";
break;
case TransitionType.Zoom:
new_transition_type = "zoom";
break;
default:
new_transition_type = "fade";
}
switch (Display._theme.display_slide_transition_speed) {
case TransitionSpeed.Normal:
new_transition_speed = "default";
break;
case TransitionSpeed.Fast:
new_transition_speed = "fast";
break;
case TransitionSpeed.Slow:
new_transition_speed = "slow";
break;
default:
new_transition_speed = "default";
}
switch (Display._theme.display_slide_transition_direction) {
case TransitionDirection.Vertical:
new_transition_type += "-vertical";
break;
case TransitionDirection.Horizontal:
default:
new_transition_type += "-horizontal";
}
if (Display._theme.display_slide_transition_reverse) {
new_transition_type += "-reverse";
}
}
var slides = targetElement.children;
for (var i = 0; i < slides.length; i++) {
slides[i].setAttribute("data-transition", new_transition_type);
slides[i].setAttribute("data-transition-speed", new_transition_speed);
}
// Set the background
var backgroundContent = "";
var backgroundHtml = "";
switch (Display._theme.background_type) {
case BackgroundType.Transparent:
backgroundContent = "transparent";
break;
case BackgroundType.Solid:
backgroundContent = Display._theme.background_color;
break;
case BackgroundType.Gradient:
switch (Display._theme.background_direction) {
case GradientType.Horizontal:
backgroundContent = _buildLinearGradient("to right",
Display._theme.background_start_color,
Display._theme.background_end_color);
break;
case GradientType.Vertical:
backgroundContent = _buildLinearGradient("to bottom",
Display._theme.background_start_color,
Display._theme.background_end_color);
break;
case GradientType.LeftTop:
backgroundContent = _buildLinearGradient("to right bottom",
Display._theme.background_start_color,
Display._theme.background_end_color);
break;
case GradientType.LeftBottom:
backgroundContent = _buildLinearGradient("to top right",
Display._theme.background_start_color,
Display._theme.background_end_color);
break;
case GradientType.Circular:
backgroundContent = _buildRadialGradient(window.innerWidth / 2, Display._theme.background_start_color,
Display._theme.background_end_color);
break;
default:
backgroundContent = "#000";
}
break;
case BackgroundType.Image:
backgroundContent = "url('" + Display._getFileUrl(Display._theme.background_filename) + "')";
break;
case BackgroundType.Video:
// never actually used since background type is overridden from video to transparent in window.py
backgroundContent = Display._theme.background_border_color;
backgroundHtml = "<video loop autoplay muted><source src='" + Display._getFileUrl(Display._theme.background_filename) + "'></video>";
break;
default:
backgroundContent = "#000";
}
targetElement.style.cssText = "";
targetElement.setAttribute("data-background", backgroundContent);
targetElement.setAttribute("data-background-size", "cover");
// set up the main area
if (!is_text) {
// only transition and background for non text slides
return;
}
mainStyle = {};
// These need to be fixed, in the Python they use a width passed in as a parameter
mainStyle.width = Display._theme.font_main_width + "px";
mainStyle.height = Display._theme.font_main_height + "px";
mainStyle["margin-top"] = "" + Display._theme.font_main_y + "px";
mainStyle.left = "" + Display._theme.font_main_x + "px";
mainStyle.color = Display._theme.font_main_color;
mainStyle["font-family"] = Display._theme.font_main_name;
mainStyle["font-size"] = "" + Display._theme.font_main_size + "pt";
mainStyle["font-style"] = !!Display._theme.font_main_italics ? "italic" : "";
mainStyle["font-weight"] = !!Display._theme.font_main_bold ? "bold" : "";
mainStyle["line-height"] = "" + (100 + Display._theme.font_main_line_adjustment) + "%";
mainStyle["letter-spacing"] = "" + (Display._theme.font_main_letter_adjustment) + 'px';
// Using text-align-last because there is a <br> seperating each line
switch (Display._theme.display_horizontal_align) {
case HorizontalAlign.Justify:
mainStyle["text-align"] = "justify";
mainStyle["text-align-last"] = "justify";
break;
case HorizontalAlign.Center:
mainStyle["text-align"] = "center";
mainStyle["text-align-last"] = "center";
break;
case HorizontalAlign.Left:
mainStyle["text-align"] = "left";
mainStyle["text-align-last"] = "left";
break;
case HorizontalAlign.Right:
mainStyle["text-align"] = "right";
mainStyle["text-align-last"] = "right";
break;
default:
mainStyle["text-align"] = "center";
mainStyle["text-align-last"] = "center";
}
switch (Display._theme.display_vertical_align) {
case VerticalAlign.Middle:
mainStyle["justify-content"] = "center";
break;
case VerticalAlign.Top:
mainStyle["justify-content"] = "flex-start";
break;
case VerticalAlign.Bottom:
mainStyle["justify-content"] = "flex-end";
// This gets around the webkit scroll height bug
mainStyle["padding-bottom"] = "" + (Display._theme.font_main_size / 8) + "px";
break;
default:
mainStyle["justify-content"] = "center";
}
/**
* This section draws the font outline. Previously we used the proprietary -webkit-text-stroke property
* but it draws the outline INSIDE the text, instead of OUTSIDE, so we had to go back to the old way
* of using multiple text-shadow rules to fake an outline.
*/
if (!!Display._theme.font_main_outline && Display._theme.hasOwnProperty('font_main_shadow_size') && !!Display._theme.font_main_shadow) {
let outlineShadows = _buildTextOutline(Display._theme.font_main_outline_size, Display._theme.font_main_outline_color);
let textShadow = _buildTextShadow(Display._theme.font_main_shadow_size, Display._theme.font_main_outline_size, Display._theme.font_main_shadow_color);
mainStyle["text-shadow"] = outlineShadows.join(", ") + ", " + textShadow;
}
else if (!!Display._theme.font_main_outline) {
let outlineShadows = _buildTextOutline(Display._theme.font_main_outline_size, Display._theme.font_main_outline_color);
mainStyle["text-shadow"] = outlineShadows.join(", ");
}
else if (Display._theme.hasOwnProperty('font_main_shadow_size') && !!Display._theme.font_main_shadow) {
mainStyle["text-shadow"] = _buildTextShadow(Display._theme.font_main_shadow_size, 0, Display._theme.font_main_shadow_color);
}
targetElement.style.cssText = "";
for (var mainKey in mainStyle) {
if (mainStyle.hasOwnProperty(mainKey)) {
targetElement.style.setProperty(mainKey, mainStyle[mainKey]);
}
}
// Set up the footer
footerStyle = {
"text-align": "left"
};
footerStyle.position = "absolute";
footerStyle.left = "" + Display._theme.font_footer_x + "px";
footerStyle.top = "" + Display._theme.font_footer_y + "px";
footerStyle.width = "" + Display._theme.font_footer_width + "px";
footerStyle.height = "" + Display._theme.font_footer_height + "px";
footerStyle.color = Display._theme.font_footer_color;
footerStyle["font-family"] = Display._theme.font_footer_name;
footerStyle["font-size"] = "" + Display._theme.font_footer_size + "pt";
footerStyle["font-style"] = !!Display._theme.font_footer_italics ? "italic" : "";
footerStyle["font-weight"] = !!Display._theme.font_footer_bold ? "bold" : "";
footerStyle["line-height"] = "" + (100 + Display._theme.font_footer_line_adjustment) + "%";
footerStyle["letter-spacing"] = "" + (Display._theme.font_footer_letter_adjustment) + 'px';
footerStyle["white-space"] = Display._theme.font_footer_wrap ? "normal" : "nowrap";
for (var footerKey in footerStyle) {
if (footerStyle.hasOwnProperty(footerKey)) {
Display._footerContainer.style.setProperty(footerKey, footerStyle[footerKey]);
}
}
},
/**
* Called whenever openlp wants to finish completely with the current text/image slides
* because a different window (eg presentation or vlc) is going to be displaying the next item
* and we don't want any flashbacks to the current slide contents
*/
finishWithCurrentItem: function () {
Display.setTextSlide('');
var documentBody = $("body")[0];
documentBody.style.opacity = 1;
Display._skipNextTransition = true;
displayWatcher.pleaseRepaint();
},
/**
* Return the video types supported by the video tag
*/
getVideoTypes: function () {
var videoElement = document.createElement('video');
var videoTypes = [];
if (videoElement.canPlayType('video/mp4; codecs="mp4v.20.8"') == "probably" ||
videoElement.canPlayType('video/mp4; codecs="avc1.42E01E"') == "pobably" ||
videoElement.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"') == "probably") {
videoTypes.push(['video/mp4', '*.mp4']);
}
if (videoElement.canPlayType('video/ogg; codecs="theora"') == "probably") {
videoTypes.push(['video/ogg', '*.ogv']);
}
if (videoElement.canPlayType('video/webm; codecs="vp8, vorbis"') == "probably") {
videoTypes.push(['video/webm', '*.webm']);
}
return videoTypes;
},
/**
* Sets the scale of the page - used to make preview widgets scale
*/
setScale: function(scale) {
document.body.style.zoom = scale+"%";
},
/**
* In order to check if a font exists, we need a container to do
* calculations on. This method creates that container and caches
* some width values so that we don't have to do this step every
* time we check if a font exists.
*/
_prepareFontContainer: function() {
Display._fontContainer = document.createElement("span");
Display._fontContainer.id = "does-font-exist";
Display._fontContainer.innerHTML = Array(100).join("wi");
Display._fontContainer.style.cssText = [
"position: absolute",
"width: auto",
"font-size: 128px",
"left: -999999px"
].join(" !important;");
document.body.appendChild(Display._fontContainer);
},
/**
* Prepare the slide number (slide x/y) for insertion into the Reveal footer
* This is a callback function which Reveal calls to get the values
* Fixes https://gitlab.com/openlp/openlp/-/issues/942
*/
setFooterSlideNumbers: function (slide) {
let value = ['', '', ''];
// Reveal does call this function passing undefined
if (typeof slide === 'undefined') {
return value;
}
value[0] = Reveal.getSlidePastCount(slide) + 1;
value[1] = '/';
value[2] = Object.keys(Display._slides).length;
return value;
},
/**
* Translates file:// protocol URLs to openlp-library://local-file/ scheme
*/
_getFileUrl: function(url) {
if (url && (url.indexOf('file://') === 0)) {
return url.replace('file://', 'openlp-library://local-file/');
}
return url;
},
};
Display._handleNativeCall = (action, ...values) => {
if (Display[action]) {
return Display[action](...values);
}
};
initCommunicationBridge();
communicationBridge.setDisplayTarget(Display);