Merge branch 'footer-per-slide' into 'master'

Adding foundational support to Footer per slide

See merge request openlp/openlp!576
This commit is contained in:
Tomas Groth 2023-04-20 18:52:39 +00:00
commit 9e34f9eca1
5 changed files with 184 additions and 6 deletions

View File

@ -427,3 +427,11 @@ section.text-slides.stack.present {
.reveal[class*=fade].overview .slides section {
transition: none; }
.footer .footer-item {
display: none;
}
.footer .footer-item.active {
display: block;
}

View File

@ -290,8 +290,11 @@ function _fixFontName(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: {},
@ -352,6 +355,7 @@ var Display = {
Display._backgroundsContainer = $(".backgrounds")[0];
Display._doTransitions = isDisplay;
Reveal.initialize(Display._revealConfig);
Reveal.addEventListener('slidechanged', Display._onSlideChanged);
Display.setItemTransition(doItemTransitions && isDisplay);
displayWatcher.setInitialised(true);
},
@ -649,6 +653,18 @@ var Display = {
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"}
@ -658,12 +674,20 @@ var Display = {
var slide_html;
var parentSection = document.createElement("section");
parentSection.classList = "text-slides";
slides.forEach(function (slide) {
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) {
Display._footerContainer.innerHTML = slide.footer;
var footerSlide = document.createElement('div');
footerSlide.classList.add('footer-item');
footerSlide.setAttribute('data-slide', index);
if (index == 0) {
footerSlide.classList.add('active');
}
footerSlide.innerHTML = slide.footer;
Display._footerContainer.append(footerSlide);
}
});
Display.replaceSlides(parentSection, true);
@ -689,6 +713,7 @@ var Display = {
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);
@ -711,6 +736,7 @@ var Display = {
var img = document.createElement('img');
img.src = 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;
@ -730,6 +756,7 @@ var Display = {
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) {

View File

@ -221,12 +221,18 @@ class ServiceItem(RegistryProperties):
pages = self.renderer.format_slide(raw_slide['text'], self)
previous_pages[verse_tag] = (raw_slide, pages)
for page in pages:
footer_html = None
has_footer_html = 'footer_html' in raw_slide
if has_footer_html:
footer_html = raw_slide['footer_html']
else:
footer_html = self.footer_html
rendered_slide = {
'title': raw_slide['title'],
'text': render_tags(page),
'chords': remove_tags(page),
'verse': index,
'footer': self.footer_html
'footer': footer_html
}
self._rendered_slides.append(rendered_slide)
display_slide = {
@ -303,12 +309,13 @@ class ServiceItem(RegistryProperties):
self.slides.append(slide)
self._new_item()
def add_from_text(self, text, verse_tag=None):
def add_from_text(self, text, verse_tag=None, footer_html=None):
"""
Add a text slide to the service item.
:param text: The raw text of the slide.
:param verse_tag:
:param footer_html: Custom HTML footer for current slide
"""
if verse_tag:
verse_tag = verse_tag.upper()
@ -317,7 +324,10 @@ class ServiceItem(RegistryProperties):
verse_tag = str(len(self.slides) + 1)
self.service_item_type = ServiceItemType.Text
title = text[:30].split('\n')[0]
self.slides.append({'title': title, 'text': text, 'verse': verse_tag})
slide = {'title': title, 'text': text, 'verse': verse_tag}
if footer_html is not None:
slide['footer_html'] = footer_html
self.slides.append(slide)
self._new_item()
def add_from_command(self, path, file_name, image, display_title=None, notes=None, file_hash=None):
@ -506,7 +516,8 @@ class ServiceItem(RegistryProperties):
self.theme_overwritten = header.get('theme_overwritten', False)
if self.service_item_type == ServiceItemType.Text:
for slide in service_item['serviceitem']['data']:
self.add_from_text(slide['raw_slide'], slide['verseTag'])
footer_html = slide['footer_html'] if 'footer_html' in slide else None
self.add_from_text(slide['raw_slide'], slide['verseTag'], footer_html)
self._create_slides()
elif self.service_item_type == ServiceItemType.Image:
if path:

View File

@ -170,6 +170,7 @@ describe("The Display object", function () {
it("should initialise Reveal when init is called", function () {
spyOn(Reveal, "initialize");
spyOn(Reveal, "addEventListener");
document.body.innerHTML = "";
Display.init();
expect(Reveal.initialize).toHaveBeenCalled();
@ -177,6 +178,7 @@ describe("The Display object", function () {
it("should have checkerboard class when init is called when not display", function () {
spyOn(Reveal, "initialize");
spyOn(Reveal, "addEventListener");
document.body.innerHTML = "";
document.body.classList = "";
Display.init({isDisplay: false});
@ -185,6 +187,7 @@ describe("The Display object", function () {
it("should not have checkerboard class when init is called when is a display", function () {
spyOn(Reveal, "initialize");
spyOn(Reveal, "addEventListener");
document.body.innerHTML = "";
document.body.classList = "";
Display.init({isDisplay: true});
@ -207,6 +210,14 @@ describe("The Display object", function () {
expect(Display.setItemTransition).toBeDefined();
});
it("should register _onSlideChanged event for slide change", function () {
spyOn(Reveal, "initialize");
spyOn(Reveal, "addEventListener");
document.body.innerHTML = "";
Display.init();
expect(Reveal.addEventListener).toHaveBeenCalledWith('slidechanged', Display._onSlideChanged);
});
it("should have a correctly functioning clearSlides() method", function () {
expect(Display.clearSlides).toBeDefined();
@ -936,6 +947,33 @@ describe("Display.setTextSlides", function () {
expect(slidesDiv.style['width']).toEqual('1230px');
expect(slidesDiv.style['height']).toEqual('4560px');
})
it("should work correctly with different footer contents per slide", function () {
var slides = [
{
"verse": "v1",
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
"I once was lost, but now I'm found\nWas blind but now I see",
"footer": "Public Domain"
},
{
"verse": "v2",
"text": "'twas Grace that taught, my heart to fear\nAnd grace, my fears relieved.\n" +
"How precious did that grace appear,\nthe hour I first believed.",
"footer": "Public Domain, Second Test"
}
];
spyOn(Display, "clearSlides");
spyOn(Reveal, "sync");
spyOn(Reveal, "slide");
Display.setTextSlides(slides);
expect(Display.clearSlides).toHaveBeenCalledTimes(0);
expect($(".footer > .footer-item").length).toEqual(2);
expect(document.querySelectorAll(".footer > .footer-item")[0].innerHTML).toEqual(slides[0].footer);
expect(document.querySelectorAll(".footer > .footer-item")[1].innerHTML).toEqual(slides[1].footer);
});
});
describe("Display.setImageSlides", function () {
@ -1171,3 +1209,45 @@ describe("Display.toggleVideoMute", function () {
expect(mockVideo.muted).toEqual(false);
});
});
describe("Reveal slidechanged event", function () {
it("should swap footer content", function (done) {
var slides = [
{
"verse": "v1",
"text": "Amazing grace, how sweet the sound\nThat saved a wretch like me\n" +
"I once was lost, but now I'm found\nWas blind but now I see",
"footer": "Public Domain"
},
{
"verse": "v2",
"text": "'twas Grace that taught, my heart to fear\nAnd grace, my fears relieved.\n" +
"How precious did that grace appear,\nthe hour I first believed.",
"footer": "Public Domain, Second Test"
}
];
var slidesDiv = _createDiv({"class": "slides"});
slidesDiv.innerHTML = "<section><p></p></section>";
Display._slidesContainer = slidesDiv;
var footerDiv = _createDiv({"class": "footer"});
Display._footerContainer = footerDiv;
var revealDiv = _createDiv({"class": "reveal"});
revealDiv.append(slidesDiv);
revealDiv.append(footerDiv);
document.body.appendChild(revealDiv);
Display.init({isDisplay: false, doItemTransitions: false});
var oldDisplaySlideChanged = Display._onSlideChanged;
Display._onSlideChanged = function(event) {
oldDisplaySlideChanged(event);
expect(document.querySelector(".footer > .footer-item.active").getAttribute('data-slide')).toEqual('1');
done();
}
Display.setTextSlides(slides);
var currentSlide = Display._slidesContainer.querySelector('*:nth-child(2)');
currentSlide.id = '1';
Display._onSlideChanged({currentSlide: currentSlide});
});
});

View File

@ -968,6 +968,58 @@ def test_to_dict_presentation_item(mocked_image_uri, mocked_get_data_path, state
assert result == expected_dict
def test_add_from_text_adds_per_slide_footer_html():
"""
Test the Service Item - adding text slides with per slide footer_html
"""
# GIVEN: A service item and two slides
service_item = ServiceItem(None)
slide1 = "This is the first slide"
slide1FooterHtml = '<small>First Footer</small>'
slide2 = "This is the second slide"
slide2FooterHtml = '<small>Second Footer</small>'
# WHEN: adding text slides to service_item
service_item.add_from_text(slide1, footer_html=slide1FooterHtml)
service_item.add_from_text(slide2, footer_html=slide2FooterHtml)
# THEN: Slides should be added with correctly numbered verse tags (Should start at 1)
assert service_item.slides == [
{'text': 'This is the first slide', 'title': 'This is the first slide', 'verse': '1',
'footer_html': slide1FooterHtml},
{'text': 'This is the second slide', 'title': 'This is the second slide', 'verse': '2',
'footer_html': slide2FooterHtml}
]
@patch('openlp.core.lib.serviceitem.UiIcons')
def test_add_from_text_per_slide_footer_html_is_honoured(mock_uiicons, settings, registry):
"""
Test the Service Item - adding text slides with per slide footer_html is honoured
"""
# GIVEN: A service item, mocked live_controller and renderer, and two slides
renderer_mock = MagicMock()
Registry().register('live_controller', MagicMock())
Registry().register('renderer', renderer_mock)
Registry().register('service_list', MagicMock())
renderer_mock.format_slide.side_effect = lambda text, item: [text]
service_item = ServiceItem(None)
slide1 = "This is the first slide"
slide1FooterHtml = '<small>First Footer</small>'
slide2 = "This is the second slide"
slide2FooterHtml = '<small>Second Footer</small>'
# WHEN: adding text slides to service_item
service_item.add_from_text(slide1, footer_html=slide1FooterHtml)
service_item.add_from_text(slide2, footer_html=slide2FooterHtml)
service_item._create_slides()
# THEN: Slides should be added with correctly numbered verse tags (Should start at 1)
assert service_item._rendered_slides[0]['footer'] == slide1FooterHtml
assert service_item._rendered_slides[1]['footer'] == slide2FooterHtml
@pytest.mark.parametrize('plugin_name,icon', [('songs', 'music'), ('bibles', 'bible'),
('presentations', 'presentation'), ('images', 'picture'),
('media', 'video')])