mirror of https://gitlab.com/openlp/openlp.git
1426 lines
46 KiB
JavaScript
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);
|