forked from openlp/openlp
Merge trunk updates
This commit is contained in:
commit
53bf0fe420
@ -8,7 +8,7 @@
|
||||
*.nja
|
||||
*.orig
|
||||
*.pyc
|
||||
*.qm
|
||||
resources/i18n/*.ts
|
||||
*.rej
|
||||
*.ropeproject
|
||||
*.~\?~
|
||||
|
@ -16,3 +16,5 @@ include copyright.txt
|
||||
include LICENSE
|
||||
include README.txt
|
||||
include openlp/.version
|
||||
include package.json
|
||||
include karma.conf.js
|
||||
|
@ -26,18 +26,21 @@ module.exports = function(config) {
|
||||
// source files, that you wanna generate coverage for
|
||||
// do not include tests or libraries
|
||||
// (these files will be instrumented by Istanbul)
|
||||
"display.js": ["coverage"]
|
||||
// "display.js": ["coverage"]
|
||||
},
|
||||
|
||||
// test results reporter to use
|
||||
// possible values: "dots", "progress"
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ["progress", "coverage"],
|
||||
reporters: ["dots", "junit"],
|
||||
|
||||
// configure the coverateReporter
|
||||
coverageReporter: {
|
||||
/* coverageReporter: {
|
||||
type : "html",
|
||||
dir : "htmlcov/"
|
||||
}, */
|
||||
junitReporter: {
|
||||
outputFile: "test-results.xml"
|
||||
},
|
||||
|
||||
// web server port
|
||||
@ -60,11 +63,11 @@ module.exports = function(config) {
|
||||
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ["PhantomJS"],
|
||||
browsers: ["Chromium"],
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: false,
|
||||
singleRun: true,
|
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultaneous
|
||||
|
@ -374,7 +374,7 @@ var Display = {
|
||||
* @param {string} text - The HTML for the verse, e.g. "line1<br>line2"
|
||||
* @param {string} footer_text - The HTML for the footer"
|
||||
*/
|
||||
addTextSlide: function (verse, text, footer_text) {
|
||||
addTextSlide: function (verse, text, footerText) {
|
||||
var html = _prepareText(text);
|
||||
if (this._slides.hasOwnProperty(verse)) {
|
||||
var slide = $("#" + verse)[0];
|
||||
@ -390,11 +390,9 @@ var Display = {
|
||||
slidesDiv.appendChild(slide);
|
||||
var slides = $(".slides > section");
|
||||
this._slides[verse] = slides.length - 1;
|
||||
|
||||
console.debug(" footer_text: " + footer_text);
|
||||
|
||||
var footerDiv = $(".footer")[0];
|
||||
footerDiv.innerHTML = footer_text;
|
||||
if (footerText) {
|
||||
$(".footer")[0].innerHTML = footerText;
|
||||
}
|
||||
}
|
||||
if ((arguments.length > 3) && (arguments[3] === true)) {
|
||||
this.reinit();
|
||||
@ -426,9 +424,10 @@ var Display = {
|
||||
var section = document.createElement("section");
|
||||
section.setAttribute("id", index);
|
||||
section.setAttribute("data-background", "#000");
|
||||
section.setAttribute("style", "height: 100%; width: 100%;");
|
||||
var img = document.createElement('img');
|
||||
img.src = slide["path"];
|
||||
img.setAttribute("style", "height: 100%; width: 100%;");
|
||||
img.setAttribute("style", "max-width: 100%; max-height: 100%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);");
|
||||
section.appendChild(img);
|
||||
slidesDiv.appendChild(section);
|
||||
Display._slides[index.toString()] = index;
|
||||
|
@ -67,6 +67,15 @@ FOOTER_COPYRIGHT = 'Public Domain'
|
||||
CCLI_NO = '123456'
|
||||
|
||||
|
||||
def remove_chords(text):
|
||||
"""
|
||||
Remove chords from the text
|
||||
|
||||
:param text: Text to be cleaned
|
||||
"""
|
||||
return re.sub(r'\[.+?\]', r'', text)
|
||||
|
||||
|
||||
def remove_tags(text, can_remove_chords=False):
|
||||
"""
|
||||
Remove Tags from text for display
|
||||
@ -82,7 +91,7 @@ def remove_tags(text, can_remove_chords=False):
|
||||
text = text.replace(tag['end tag'], '')
|
||||
# Remove ChordPro tags
|
||||
if can_remove_chords:
|
||||
text = re.sub(r'\[.+?\]', r'', text)
|
||||
text = remove_chords(text)
|
||||
return text
|
||||
|
||||
|
||||
@ -377,6 +386,8 @@ def render_tags(text, can_render_chords=False, is_printing=False):
|
||||
text = render_chords_for_printing(text, '{br}')
|
||||
else:
|
||||
text = render_chords(text)
|
||||
else:
|
||||
text = remove_chords(text)
|
||||
for tag in FormattingTags.get_html_tags():
|
||||
text = text.replace(tag['start tag'], tag['start html'])
|
||||
text = text.replace(tag['end tag'], tag['end html'])
|
||||
@ -503,6 +514,26 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow):
|
||||
self.force_page = False
|
||||
return None
|
||||
|
||||
def get_theme(self, item):
|
||||
"""
|
||||
:param item: The :class:`~openlp.core.lib.serviceitem.ServiceItem` item object
|
||||
:return string: The name of the theme to be used
|
||||
|
||||
"""
|
||||
# Just assume we use the global theme.
|
||||
theme_name = Registry().get('theme_manager').global_theme
|
||||
# The theme level is either set to Service or Item. Use the service theme if one is set. We also have to use the
|
||||
# service theme, even when the theme level is set to Item, because the item does not necessarily have to have a
|
||||
# theme.
|
||||
if self.theme_level != ThemeLevel.Global:
|
||||
# When the theme level is at Service and we actually have a service theme then use it.
|
||||
if self.theme_level == ThemeLevel.Service:
|
||||
theme_name = Registry().get('service_manager').service_theme
|
||||
# If we have Item level and have an item theme then use it.
|
||||
if self.theme_level == ThemeLevel.Song and item.theme:
|
||||
theme_name = item.theme
|
||||
return theme_name
|
||||
|
||||
def format_slide(self, text, item):
|
||||
"""
|
||||
Calculate how much text can fit on a slide.
|
||||
@ -515,7 +546,7 @@ class ThemePreviewRenderer(LogMixin, DisplayWindow):
|
||||
QtWidgets.QApplication.instance().processEvents()
|
||||
self.log_debug('format slide')
|
||||
if item:
|
||||
theme_name = item.theme if item.theme else Registry().get('theme_manager').global_theme
|
||||
theme_name = self.get_theme(item)
|
||||
theme_data = Registry().get('theme_manager').get_theme_data(theme_name)
|
||||
self.theme_height = theme_data.font_main_height
|
||||
# Set theme for preview
|
||||
|
@ -67,7 +67,7 @@ def database_exists(url):
|
||||
create_database(engine.url)
|
||||
database_exists(engine.url) #=> True
|
||||
|
||||
Borrowed from SQLAlchemy_Utils (v0.32.14 )since we only need this one function.
|
||||
Borrowed from SQLAlchemy_Utils (v0.32.14) since we only need this one function.
|
||||
"""
|
||||
|
||||
url = copy(make_url(url))
|
||||
|
@ -39,7 +39,7 @@ from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.i18n import translate
|
||||
from openlp.core.common.mixins import RegistryProperties
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.display.render import remove_tags, render_tags
|
||||
from openlp.core.display.render import remove_tags, render_tags, render_chords_for_printing
|
||||
from openlp.core.lib import ItemCapabilities
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
|
||||
@ -74,6 +74,7 @@ class ServiceItem(RegistryProperties):
|
||||
self.name = plugin.name
|
||||
self._rendered_slides = None
|
||||
self._display_slides = None
|
||||
self._print_slides = None
|
||||
self.title = ''
|
||||
self.slides = []
|
||||
self.processor = None
|
||||
@ -185,7 +186,7 @@ class ServiceItem(RegistryProperties):
|
||||
self._rendered_slides.append(rendered_slide)
|
||||
display_slide = {
|
||||
'title': raw_slide['title'],
|
||||
'text': remove_tags(page),
|
||||
'text': remove_tags(page, can_remove_chords=True),
|
||||
'verse': verse_tag,
|
||||
}
|
||||
self._display_slides.append(display_slide)
|
||||
@ -209,6 +210,34 @@ class ServiceItem(RegistryProperties):
|
||||
self._create_slides()
|
||||
return self._display_slides
|
||||
|
||||
@property
|
||||
def print_slides(self):
|
||||
"""
|
||||
Render the frames for printing and return them
|
||||
|
||||
:param can_render_chords: bool Whether or not to render the chords
|
||||
"""
|
||||
if not self._print_slides:
|
||||
self._print_slides = []
|
||||
previous_pages = {}
|
||||
index = 0
|
||||
for raw_slide in self.slides:
|
||||
verse_tag = raw_slide['verse']
|
||||
if verse_tag in previous_pages and previous_pages[verse_tag][0] == raw_slide:
|
||||
pages = previous_pages[verse_tag][1]
|
||||
else:
|
||||
pages = self.renderer.format_slide(raw_slide['text'], self)
|
||||
previous_pages[verse_tag] = (raw_slide, pages)
|
||||
for page in pages:
|
||||
slide = {
|
||||
'title': raw_slide['title'],
|
||||
'text': render_chords_for_printing(remove_tags(page), '\n'),
|
||||
'verse': index,
|
||||
'footer': self.raw_footer,
|
||||
}
|
||||
self._print_slides.append(slide)
|
||||
return self._print_slides
|
||||
|
||||
def add_from_image(self, path, title, background=None, thumbnail=None):
|
||||
"""
|
||||
Add an image slide to the service item.
|
||||
@ -321,6 +350,13 @@ class ServiceItem(RegistryProperties):
|
||||
'display_title': slide['display_title'], 'notes': slide['notes']})
|
||||
return {'header': service_header, 'data': service_data}
|
||||
|
||||
def render_text_items(self):
|
||||
"""
|
||||
This method forces the display to be regenerated
|
||||
"""
|
||||
self._display_slides = []
|
||||
self._rendered_slides = []
|
||||
|
||||
def set_from_service(self, service_item, path=None):
|
||||
"""
|
||||
This method takes a service item from a saved service file (passed from the ServiceManager) and extracts the
|
||||
@ -503,7 +539,6 @@ class ServiceItem(RegistryProperties):
|
||||
:param row: The service item slide to be returned
|
||||
"""
|
||||
if self.service_item_type == ServiceItemType.Text:
|
||||
# return self.display_frames[row]['html'].split('\n')[0]
|
||||
return self.rendered_slides[row]['text']
|
||||
elif self.service_item_type == ServiceItemType.Image:
|
||||
return self.slides[row]['path']
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -50,19 +50,20 @@ class AboutForm(QtWidgets.QDialog, UiAboutDialog):
|
||||
Set up the dialog. This method is mocked out in tests.
|
||||
"""
|
||||
self.setup_ui(self)
|
||||
self.button_box.buttons()[0].setFocus()
|
||||
application_version = get_version()
|
||||
about_text = self.about_text_edit.toPlainText()
|
||||
about_text = about_text.replace('<version>', application_version['version'])
|
||||
about_text = self.about_text_edit.toHtml()
|
||||
about_text = about_text.replace('{version}', application_version['version'])
|
||||
if application_version['build']:
|
||||
build_text = translate('OpenLP.AboutForm', ' build {version}').format(version=application_version['build'])
|
||||
else:
|
||||
build_text = ''
|
||||
about_text = about_text.replace('<revision>', build_text)
|
||||
self.about_text_edit.setPlainText(about_text)
|
||||
self.volunteer_button.clicked.connect(self.on_volunteer_button_clicked)
|
||||
about_text = about_text.replace('{revision}', build_text)
|
||||
self.about_text_edit.setHtml(about_text)
|
||||
self.contribute_button.clicked.connect(self.on_contribute_button_clicked)
|
||||
|
||||
def on_volunteer_button_clicked(self):
|
||||
def on_contribute_button_clicked(self):
|
||||
"""
|
||||
Launch a web browser and go to the contribute page on the site.
|
||||
"""
|
||||
webbrowser.open_new('http://openlp.org/en/contribute')
|
||||
webbrowser.open_new('http://openlp.org/contribute')
|
||||
|
@ -546,7 +546,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
|
||||
Wait for the threads
|
||||
"""
|
||||
# Sometimes the threads haven't finished, let's wait for them
|
||||
wait_dialog = QtWidgets.QProgressDialog('Waiting for some things to finish...', '', 0, 0, self)
|
||||
wait_dialog = QtWidgets.QProgressDialog(translate('OpenLP.MainWindow', 'Waiting for some things to finish...'),
|
||||
'', 0, 0, self)
|
||||
wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
wait_dialog.setAutoClose(False)
|
||||
wait_dialog.setCancelButton(None)
|
||||
|
@ -218,7 +218,7 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
|
||||
if item.is_text():
|
||||
verse_def = None
|
||||
verse_html = None
|
||||
for slide in item.get_frames():
|
||||
for slide in item.print_slides:
|
||||
if not verse_def or verse_def != slide['verse'] or verse_html == slide['text']:
|
||||
text_div = self._add_element('div', parent=div, class_id='itemText')
|
||||
elif 'chordspacing' not in slide['text']:
|
||||
|
@ -53,7 +53,6 @@ class ScreensTab(SettingsTab):
|
||||
self.setObjectName('self')
|
||||
self.tab_layout = QtWidgets.QVBoxLayout(self)
|
||||
self.tab_layout.setObjectName('tab_layout')
|
||||
|
||||
self.screen_selection_widget = ScreenSelectionWidget(self, ScreenList())
|
||||
self.tab_layout.addWidget(self.screen_selection_widget)
|
||||
self.generic_group_box = QtWidgets.QGroupBox(self)
|
||||
@ -63,13 +62,11 @@ class ScreensTab(SettingsTab):
|
||||
self.display_on_monitor_check.setObjectName('monitor_combo_box')
|
||||
self.generic_group_layout.addWidget(self.display_on_monitor_check)
|
||||
self.tab_layout.addWidget(self.generic_group_box)
|
||||
|
||||
Registry().register_function('config_screen_changed', self.screen_selection_widget.load)
|
||||
|
||||
self.retranslate_ui()
|
||||
|
||||
def retranslate_ui(self):
|
||||
self.setWindowTitle(translate('self', 'self')) # TODO: ???
|
||||
self.generic_group_box.setTitle(translate('OpenLP.ScreensTab', 'Generic screen settings'))
|
||||
self.display_on_monitor_check.setText(translate('OpenLP.ScreensTab', 'Display if a single screen'))
|
||||
|
||||
|
@ -46,7 +46,7 @@ from openlp.core.common.settings import Settings
|
||||
from openlp.core.lib import build_icon
|
||||
from openlp.core.lib.exceptions import ValidationError
|
||||
from openlp.core.lib.plugin import PluginStatus
|
||||
from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem
|
||||
from openlp.core.lib.serviceitem import ItemCapabilities, ServiceItem, ServiceItemType
|
||||
from openlp.core.lib.ui import create_widget_action, critical_error_message_box, find_and_set_in_combo_box
|
||||
from openlp.core.ui.icons import UiIcons
|
||||
from openlp.core.ui.media import AUDIO_EXT, VIDEO_EXT
|
||||
@ -749,9 +749,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
if theme:
|
||||
find_and_set_in_combo_box(self.theme_combo_box, theme, set_missing=False)
|
||||
if theme == self.theme_combo_box.currentText():
|
||||
# TODO: Use a local display widget
|
||||
# self.preview_display.set_theme(get_theme_from_name(theme))
|
||||
pass
|
||||
self.service_theme = theme
|
||||
else:
|
||||
if self._save_lite:
|
||||
service_item.set_from_service(item)
|
||||
@ -1197,9 +1195,9 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
service_item_from_item = item['service_item']
|
||||
tree_widget_item = QtWidgets.QTreeWidgetItem(self.service_manager_list)
|
||||
if service_item_from_item.is_valid:
|
||||
if service_item_from_item.notes:
|
||||
icon = service_item_from_item.icon.pixmap(80, 80).toImage()
|
||||
icon = icon.scaled(80, 80, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
if service_item_from_item.notes:
|
||||
overlay = UiIcons().notes.pixmap(40, 40).toImage()
|
||||
overlay = overlay.scaled(40, 40, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
painter = QtGui.QPainter(icon)
|
||||
@ -1207,8 +1205,6 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
painter.end()
|
||||
tree_widget_item.setIcon(0, build_icon(icon))
|
||||
elif service_item_from_item.temporary_edit:
|
||||
icon = service_item_from_item.icon.pixmap(80, 80).toImage()
|
||||
icon = icon.scaled(80, 80, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
overlay = QtGui.QImage(UiIcons().upload)
|
||||
overlay = overlay.scaled(40, 40, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
painter = QtGui.QPainter(icon)
|
||||
@ -1242,13 +1238,14 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
tree_widget_item.setData(0, QtCore.Qt.UserRole, item['order'])
|
||||
tree_widget_item.setSelected(item['selected'])
|
||||
# Add the children to their parent tree_widget_item.
|
||||
for slide_index, slide in enumerate(service_item_from_item.slides):
|
||||
for slide_index, slide in enumerate(service_item_from_item.get_frames()):
|
||||
child = QtWidgets.QTreeWidgetItem(tree_widget_item)
|
||||
# prefer to use a display_title
|
||||
if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle):
|
||||
text = slide['display_title'].replace('\n', ' ')
|
||||
else:
|
||||
if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle) or \
|
||||
service_item_from_item.service_item_type == ServiceItemType.Image:
|
||||
text = slide['title'].replace('\n', ' ')
|
||||
else:
|
||||
text = service_item_from_item.get_rendered_frame(slide_index)
|
||||
child.setText(0, text[:40])
|
||||
child.setData(0, QtCore.Qt.UserRole, slide_index)
|
||||
if service_item == item_index:
|
||||
@ -1275,8 +1272,6 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
:param current_index: The combo box index for the selected item
|
||||
"""
|
||||
self.service_theme = self.theme_combo_box.currentText()
|
||||
# TODO: Use a local display widget
|
||||
# self.preview_display.set_theme(get_theme_from_name(theme))
|
||||
Settings().setValue(self.main_window.service_manager_settings_section + '/service theme', self.service_theme)
|
||||
self.regenerate_service_items(True)
|
||||
|
||||
@ -1335,7 +1330,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
"""
|
||||
for item_count, item in enumerate(self.service_items):
|
||||
if item['service_item'].edit_id == new_item.edit_id and item['service_item'].name == new_item.name:
|
||||
new_item.render()
|
||||
new_item.create_slides()
|
||||
new_item.merge(item['service_item'])
|
||||
item['service_item'] = new_item
|
||||
self.repaint_service_list(item_count + 1, 0)
|
||||
@ -1368,7 +1363,7 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
self.repaint_service_list(s_item, child)
|
||||
self.live_controller.replace_service_manager_item(item)
|
||||
else:
|
||||
# item.render()
|
||||
item.render_text_items()
|
||||
# nothing selected for dnd
|
||||
if self.drop_position == -1:
|
||||
if isinstance(item, list):
|
||||
@ -1617,8 +1612,6 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
||||
theme_group.addAction(create_widget_action(self.theme_menu, theme, text=theme, checked=False,
|
||||
triggers=self.on_theme_change_action))
|
||||
find_and_set_in_combo_box(self.theme_combo_box, self.service_theme)
|
||||
# TODO: Sort this out
|
||||
# self.renderer.set_service_theme(self.service_theme)
|
||||
self.regenerate_service_items()
|
||||
|
||||
def on_theme_change_action(self):
|
||||
|
@ -613,7 +613,7 @@ class ThemeManager(QtWidgets.QWidget, RegistryBase, Ui_ThemeManager, LogMixin, R
|
||||
self.log_exception('Importing theme from zip failed {name}'.format(name=file_path))
|
||||
critical_error_message_box(
|
||||
translate('OpenLP.ThemeManager', 'Import Error'),
|
||||
translate('OpenLP.ThemeManager', 'There was a problem imoorting {file_name}.\n\nIt is corrupt,'
|
||||
translate('OpenLP.ThemeManager', 'There was a problem importing {file_name}.\n\nIt is corrupt, '
|
||||
'inaccessible or not a valid theme.').format(file_name=file_path))
|
||||
finally:
|
||||
if not abort_import:
|
||||
|
@ -210,9 +210,13 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties):
|
||||
pixmap = QtGui.QPixmap(str(slide['image']))
|
||||
else:
|
||||
pixmap = QtGui.QPixmap(str(slide['path']))
|
||||
if pixmap.height() > 0:
|
||||
pixmap_ratio = pixmap.width() / pixmap.height()
|
||||
else:
|
||||
pixmap_ratio = 1
|
||||
label.setPixmap(pixmap)
|
||||
container = QtWidgets.QWidget()
|
||||
layout = AspectRatioLayout(container, self.screen_ratio)
|
||||
layout = AspectRatioLayout(container, pixmap_ratio)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(label)
|
||||
container.setLayout(layout)
|
||||
|
@ -27,6 +27,7 @@ from Pyro4 import Proxy
|
||||
|
||||
from openlp.core.common import delete_file, is_macosx
|
||||
from openlp.core.common.applocation import AppLocation
|
||||
from openlp.core.common.mixins import LogMixin
|
||||
from openlp.core.common.path import Path
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.display.screens import ScreenList
|
||||
@ -47,7 +48,7 @@ log = logging.getLogger(__name__)
|
||||
register_classes()
|
||||
|
||||
|
||||
class MacLOController(PresentationController):
|
||||
class MacLOController(PresentationController, LogMixin):
|
||||
"""
|
||||
Class to control interactions with MacLO presentations on Mac OS X via Pyro4. It starts the Pyro4 nameserver,
|
||||
starts the LibreOfficeServer, and then controls MacLO via Pyro4.
|
||||
|
@ -291,7 +291,6 @@ class Ui_EditSongDialog(object):
|
||||
self.warning_label.setObjectName('warning_label')
|
||||
self.bottom_layout.addWidget(self.warning_label)
|
||||
self.button_box = create_button_box(edit_song_dialog, 'button_box', ['cancel', 'save'])
|
||||
self.save_button = self.button_box.button(QtWidgets.QDialogButtonBox.Save)
|
||||
self.bottom_layout.addWidget(self.button_box)
|
||||
self.dialog_layout.addLayout(self.bottom_layout)
|
||||
self.retranslate_ui(edit_song_dialog)
|
||||
@ -342,7 +341,6 @@ class Ui_EditSongDialog(object):
|
||||
translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> Not all of the verses are in use.')
|
||||
self.no_verse_order_entered_warning = \
|
||||
translate('SongsPlugin.EditSongForm', '<strong>Warning:</strong> You have not entered a verse order.')
|
||||
self.save_button.setText(UiStrings().SaveAndPreview)
|
||||
|
||||
|
||||
def create_combo_box(parent, name, editable=True):
|
||||
|
@ -28,6 +28,7 @@ import re
|
||||
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.plugins.songs.lib.importers.songimport import SongImport
|
||||
from openlp.plugins.songs.lib.db import AuthorType
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -39,6 +40,7 @@ class ChordProImport(SongImport):
|
||||
|
||||
This importer is based on the information available on these webpages:
|
||||
|
||||
- https://www.chordpro.org
|
||||
- http://webchord.sourceforge.net/tech.html
|
||||
- http://www.vromans.org/johan/projects/Chordii/chordpro/
|
||||
- http://www.tenbyten.com/software/songsgen/help/HtmlHelp/files_reference.htm
|
||||
@ -73,6 +75,29 @@ class ChordProImport(SongImport):
|
||||
self.title = tag_value
|
||||
elif tag_name in ['subtitle', 'su', 'st']:
|
||||
self.alternate_title = tag_value
|
||||
elif tag_name == 'composer':
|
||||
self.parse_author(tag_value, AuthorType.Music)
|
||||
elif tag_name in ['lyricist', 'artist', 'author']: # author is not an official directive
|
||||
self.parse_author(tag_value, AuthorType.Words)
|
||||
elif tag_name == 'meta':
|
||||
meta_tag_name, meta_tag_value = tag_value.split(' ', 1)
|
||||
# Skip, if no value
|
||||
if not meta_tag_value:
|
||||
continue
|
||||
# The meta-tag can contain anything. We check for the ones above and a few more
|
||||
if meta_tag_name in ['title', 't']:
|
||||
self.title = meta_tag_value
|
||||
elif meta_tag_name in ['subtitle', 'su', 'st']:
|
||||
self.alternate_title = meta_tag_value
|
||||
elif meta_tag_name == 'composer':
|
||||
self.parse_author(meta_tag_value, AuthorType.Music)
|
||||
elif meta_tag_name in ['lyricist', 'artist', 'author']:
|
||||
self.parse_author(meta_tag_value, AuthorType.Words)
|
||||
elif meta_tag_name in ['topic', 'topics']:
|
||||
for topic in meta_tag_value.split(','):
|
||||
self.topics.append(topic.strip())
|
||||
elif 'ccli' in meta_tag_name:
|
||||
self.ccli_number = meta_tag_value
|
||||
elif tag_name in ['comment', 'c', 'comment_italic', 'ci', 'comment_box', 'cb']:
|
||||
# Detect if the comment is used as a chorus repeat marker
|
||||
if tag_value.lower().startswith('chorus'):
|
||||
@ -156,6 +181,13 @@ class ChordProImport(SongImport):
|
||||
'songs/disable chords import'):
|
||||
current_verse = re.sub(r'\[.*?\]', '', current_verse)
|
||||
self.add_verse(current_verse.rstrip(), current_verse_type)
|
||||
# if no title was in directives, get it from the first line
|
||||
if not self.title:
|
||||
(verse_def, verse_text, lang) = self.verses[0]
|
||||
# strip any chords from the title
|
||||
self.title = re.sub(r'\[.*?\]', '', verse_text.split('\n')[0])
|
||||
# strip the last char if it a punctuation
|
||||
self.title = re.sub(r'[^\w\s]$', '', self.title)
|
||||
if not self.finish():
|
||||
self.log_error(song_file.name)
|
||||
|
||||
|
@ -34,7 +34,7 @@ class SongStrings(object):
|
||||
Author = translate('OpenLP.Ui', 'Author', 'Singular')
|
||||
Authors = translate('OpenLP.Ui', 'Authors', 'Plural')
|
||||
AuthorUnknown = translate('OpenLP.Ui', 'Author Unknown') # Used to populate the database.
|
||||
CopyrightSymbol = translate('OpenLP.Ui', '\xa9', 'Copyright symbol.')
|
||||
CopyrightSymbol = '\xa9'
|
||||
SongBook = translate('OpenLP.Ui', 'Songbook', 'Singular')
|
||||
SongBooks = translate('OpenLP.Ui', 'Songbooks', 'Plural')
|
||||
SongIncomplete = translate('OpenLP.Ui', 'Title and/or verses not found')
|
||||
|
13
package.json
13
package.json
@ -9,17 +9,16 @@
|
||||
"dependencies": {
|
||||
"jasmine-core": "^2.6.4",
|
||||
"karma": "^3.1.4",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-coverage": "^1.1.2",
|
||||
"karma-firefox-launcher": "^1.2.0",
|
||||
"karma-jasmine": "^1.1.0",
|
||||
"karma-phantomjs-launcher": "^1.0.4",
|
||||
"phantomjs-prebuilt": "^2.1.16"
|
||||
"karma-junit-reporter": "^1.2.0",
|
||||
"karma-log-reporter": "0.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "karma start"
|
||||
"test": "karma start --single-run"
|
||||
},
|
||||
"author": "OpenLP Developers",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"karma-log-reporter": "0.0.4"
|
||||
}
|
||||
"license": "GPL-3.0-or-later"
|
||||
}
|
||||
|
10977
resources/i18n/af.ts
10977
resources/i18n/af.ts
File diff suppressed because it is too large
Load Diff
10975
resources/i18n/bg.ts
10975
resources/i18n/bg.ts
File diff suppressed because it is too large
Load Diff
11003
resources/i18n/cs.ts
11003
resources/i18n/cs.ts
File diff suppressed because it is too large
Load Diff
10996
resources/i18n/da.ts
10996
resources/i18n/da.ts
File diff suppressed because it is too large
Load Diff
10995
resources/i18n/de.ts
10995
resources/i18n/de.ts
File diff suppressed because it is too large
Load Diff
10973
resources/i18n/el.ts
10973
resources/i18n/el.ts
File diff suppressed because it is too large
Load Diff
11075
resources/i18n/en.ts
11075
resources/i18n/en.ts
File diff suppressed because it is too large
Load Diff
11073
resources/i18n/en_GB.ts
11073
resources/i18n/en_GB.ts
File diff suppressed because it is too large
Load Diff
10995
resources/i18n/en_ZA.ts
10995
resources/i18n/en_ZA.ts
File diff suppressed because it is too large
Load Diff
11055
resources/i18n/es.ts
11055
resources/i18n/es.ts
File diff suppressed because it is too large
Load Diff
10995
resources/i18n/et.ts
10995
resources/i18n/et.ts
File diff suppressed because it is too large
Load Diff
11251
resources/i18n/fi.ts
11251
resources/i18n/fi.ts
File diff suppressed because it is too large
Load Diff
10990
resources/i18n/fr.ts
10990
resources/i18n/fr.ts
File diff suppressed because it is too large
Load Diff
11050
resources/i18n/hu.ts
11050
resources/i18n/hu.ts
File diff suppressed because it is too large
Load Diff
11015
resources/i18n/id.ts
11015
resources/i18n/id.ts
File diff suppressed because it is too large
Load Diff
10975
resources/i18n/ja.ts
10975
resources/i18n/ja.ts
File diff suppressed because it is too large
Load Diff
10975
resources/i18n/ko.ts
10975
resources/i18n/ko.ts
File diff suppressed because it is too large
Load Diff
11065
resources/i18n/lt.ts
11065
resources/i18n/lt.ts
File diff suppressed because it is too large
Load Diff
11053
resources/i18n/nb.ts
11053
resources/i18n/nb.ts
File diff suppressed because it is too large
Load Diff
10993
resources/i18n/nl.ts
10993
resources/i18n/nl.ts
File diff suppressed because it is too large
Load Diff
10995
resources/i18n/pl.ts
10995
resources/i18n/pl.ts
File diff suppressed because it is too large
Load Diff
11008
resources/i18n/pt_BR.ts
11008
resources/i18n/pt_BR.ts
File diff suppressed because it is too large
Load Diff
10995
resources/i18n/ru.ts
10995
resources/i18n/ru.ts
File diff suppressed because it is too large
Load Diff
10994
resources/i18n/sk.ts
10994
resources/i18n/sk.ts
File diff suppressed because it is too large
Load Diff
10985
resources/i18n/sv.ts
10985
resources/i18n/sv.ts
File diff suppressed because it is too large
Load Diff
10974
resources/i18n/ta_LK.ts
10974
resources/i18n/ta_LK.ts
File diff suppressed because it is too large
Load Diff
10974
resources/i18n/th_TH.ts
10974
resources/i18n/th_TH.ts
File diff suppressed because it is too large
Load Diff
10971
resources/i18n/zh_CN.ts
10971
resources/i18n/zh_CN.ts
File diff suppressed because it is too large
Load Diff
10976
resources/i18n/zh_TW.ts
10976
resources/i18n/zh_TW.ts
File diff suppressed because it is too large
Load Diff
9
scripts/.tx/config
Normal file
9
scripts/.tx/config
Normal file
@ -0,0 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[openlp.openlp-30x]
|
||||
file_filter = ../resources/i18n/<lang>.ts
|
||||
minimum_perc = 0
|
||||
source_file = ../resources/i18n/en.ts
|
||||
source_lang = en
|
||||
type = qt
|
@ -97,7 +97,7 @@ MODULES = [
|
||||
|
||||
OPTIONAL_MODULES = [
|
||||
('qdarkstyle', '(dark style support)'),
|
||||
('mysql.connector', '(MySQL support)'),
|
||||
('pymysql', '(MySQL support)'),
|
||||
('pyodbc', '(ODBC support)'),
|
||||
('psycopg2', '(PostgreSQL support)'),
|
||||
('enchant', '(spell checker)'),
|
||||
|
46
scripts/pull_translations
Executable file
46
scripts/pull_translations
Executable file
@ -0,0 +1,46 @@
|
||||
#!/bin/sh
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2019 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
#
|
||||
# This script automates the update of the translations on OpenLP.
|
||||
#
|
||||
# It uses the tx client from Transifex for all the heavy lifting
|
||||
# All download *.ts files are converted to *.qm files which are used by
|
||||
# OpenLP.
|
||||
#
|
||||
###############################################################################
|
||||
pwd=`pwd`
|
||||
result=${PWD##*/}; echo $result
|
||||
|
||||
if [ $result != 'scripts' ] ; then
|
||||
echo 'This script must be run from the scripts directory'
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
rm ../resources/i18n/*.ts
|
||||
|
||||
echo
|
||||
echo Downloading the translated files
|
||||
echo
|
||||
tx pull -a --minimum-perc=45
|
||||
|
||||
echo Translation update complete
|
52
scripts/push_translations
Executable file
52
scripts/push_translations
Executable file
@ -0,0 +1,52 @@
|
||||
##########################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2019 OpenLP Developers #
|
||||
# ---------------------------------------------------------------------- #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
#
|
||||
# This script automates the update of the translations on OpenLP.
|
||||
#
|
||||
# It uses the tx client from Transifex for all the heavy lifting
|
||||
# All download *.ts files are converted to *.qm files which are used by
|
||||
# OpenLP.
|
||||
#
|
||||
###############################################################################
|
||||
pwd=`pwd`
|
||||
result=${PWD##*/}; echo $result
|
||||
|
||||
if [ $result != 'scripts' ] ; then
|
||||
echo 'This script must be run from the scripts directory'
|
||||
exit
|
||||
fi
|
||||
|
||||
echo
|
||||
echo Generation translation control file
|
||||
echo
|
||||
rm ../resources/i18n/*.ts
|
||||
python3 $pwd/translation_utils.py -p
|
||||
|
||||
echo Creating base translation file
|
||||
cd ..
|
||||
pylupdate5 -verbose -noobsolete openlp.pro
|
||||
cd scripts
|
||||
|
||||
echo Check of invalid characters in push file
|
||||
grep -axv '.*' ../resources/i18n/en.ts
|
||||
|
||||
tx push -s
|
||||
|
||||
echo New translation file pushed.
|
||||
|
@ -22,210 +22,29 @@
|
||||
##########################################################################
|
||||
|
||||
"""
|
||||
This script is used to maintain the translation files in OpenLP. It downloads
|
||||
the latest translation files from the Transifex translation server, updates the
|
||||
local translation files from both the source code and the files from Transifex,
|
||||
and can also generate the compiled translation files.
|
||||
|
||||
Create New Language
|
||||
-------------------
|
||||
|
||||
To create a new language, simply run this script with the ``-c`` command line
|
||||
option::
|
||||
|
||||
@:~$ ./translation_utils.py -c
|
||||
|
||||
Update Translation Files
|
||||
------------------------
|
||||
|
||||
The best way to update the translations is to download the files from Transifex,
|
||||
and then update the local files using both the downloaded files and the source.
|
||||
This is done easily via the ``-d``, ``-p`` and ``-u`` options::
|
||||
|
||||
@:~$ ./translation_utils.py -dpu
|
||||
|
||||
This script is used to maintain the translation files in OpenLP.
|
||||
It generates the base en.ts file used to drive all translations
|
||||
on Transifex.
|
||||
For more details on the translation process see the Translation pages on the
|
||||
Wiki
|
||||
"""
|
||||
import base64
|
||||
import glob
|
||||
import json
|
||||
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import webbrowser
|
||||
from argparse import ArgumentParser
|
||||
from getpass import getpass
|
||||
|
||||
from lxml import objectify
|
||||
from PyQt5 import QtCore
|
||||
|
||||
SERVER_URL = 'http://www.transifex.com/api/2/project/openlp/resource/openlp-26x/'
|
||||
IGNORED_PATHS = ['scripts']
|
||||
IGNORED_PATHS = ['scripts', 'tests']
|
||||
IGNORED_FILES = ['setup.py']
|
||||
|
||||
verbose_mode = False
|
||||
quiet_mode = False
|
||||
username = ''
|
||||
password = ''
|
||||
|
||||
|
||||
class Command(object):
|
||||
"""
|
||||
Provide an enumeration of commands.
|
||||
"""
|
||||
Download = 1
|
||||
Create = 2
|
||||
Prepare = 3
|
||||
Update = 4
|
||||
Generate = 5
|
||||
Check = 6
|
||||
|
||||
|
||||
class CommandStack(object):
|
||||
"""
|
||||
This class provides an iterable stack.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.current_index = 0
|
||||
self.data = []
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def __getitem__(self, index):
|
||||
if index not in self.data:
|
||||
return None
|
||||
elif self.data[index].get('arguments'):
|
||||
return self.data[index]['command'], self.data[index]['arguments']
|
||||
else:
|
||||
return self.data[index]['command']
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if self.current_index == len(self.data):
|
||||
raise StopIteration
|
||||
else:
|
||||
current_item = self.data[self.current_index]['command']
|
||||
self.current_index += 1
|
||||
return current_item
|
||||
|
||||
def append(self, command, **kwargs):
|
||||
data = {'command': command}
|
||||
if 'arguments' in kwargs:
|
||||
data['arguments'] = kwargs['arguments']
|
||||
self.data.append(data)
|
||||
|
||||
def reset(self):
|
||||
self.current_index = 0
|
||||
|
||||
def arguments(self):
|
||||
if self.data[self.current_index - 1].get('arguments'):
|
||||
return self.data[self.current_index - 1]['arguments']
|
||||
else:
|
||||
return []
|
||||
|
||||
def __repr__(self):
|
||||
results = []
|
||||
for item in self.data:
|
||||
if item.get('arguments'):
|
||||
results.append(str((item['command'], item['arguments'])))
|
||||
else:
|
||||
results.append(str((item['command'], )))
|
||||
return '[%s]' % ', '.join(results)
|
||||
|
||||
|
||||
def print_quiet(text, linefeed=True):
|
||||
"""
|
||||
This method checks to see if we are in quiet mode, and if not prints ``text`` out.
|
||||
|
||||
:param text: The text to print.
|
||||
:param linefeed: Linefeed required
|
||||
"""
|
||||
global quiet_mode
|
||||
if not quiet_mode:
|
||||
if linefeed:
|
||||
print(text)
|
||||
else:
|
||||
print(text, end=' ')
|
||||
|
||||
|
||||
def print_verbose(text):
|
||||
"""
|
||||
This method checks to see if we are in verbose mode, and if so prints ``text`` out.
|
||||
|
||||
:param text: The text to print.
|
||||
"""
|
||||
global verbose_mode, quiet_mode
|
||||
if not quiet_mode and verbose_mode:
|
||||
print(' %s' % text)
|
||||
|
||||
|
||||
def run(command):
|
||||
"""
|
||||
This method runs an external application.
|
||||
|
||||
:param command: The command to run.
|
||||
"""
|
||||
print_verbose(command)
|
||||
process = QtCore.QProcess()
|
||||
process.start(command)
|
||||
while process.waitForReadyRead():
|
||||
print_verbose('ReadyRead: %s' % process.readAll())
|
||||
print_verbose('Error(s):\n%s' % process.readAllStandardError())
|
||||
print_verbose('Output:\n%s' % process.readAllStandardOutput())
|
||||
|
||||
|
||||
def download_translations():
|
||||
"""
|
||||
This method downloads the translation files from the Pootle server.
|
||||
|
||||
**Note:** URLs and headers need to remain strings, not unicode.
|
||||
"""
|
||||
global username, password
|
||||
print_quiet('Download translation files from Transifex')
|
||||
if not username:
|
||||
username = input(' Transifex username: ')
|
||||
if not password:
|
||||
password = getpass(' Transifex password: ')
|
||||
# First get the list of languages
|
||||
base64string = base64.encodebytes(('%s:%s' % (username, password)).encode())[:-1]
|
||||
auth_header = 'Basic %s' % base64string.decode()
|
||||
request = urllib.request.Request(SERVER_URL + '?details')
|
||||
request.add_header('Authorization', auth_header)
|
||||
print_verbose('Downloading list of languages from: %s' % SERVER_URL)
|
||||
try:
|
||||
json_response = urllib.request.urlopen(request)
|
||||
except urllib.error.HTTPError:
|
||||
print_quiet('Username or password incorrect.')
|
||||
return False
|
||||
json_dict = json.loads(json_response.read().decode())
|
||||
languages = [lang['code'] for lang in json_dict['available_languages']]
|
||||
for language in languages:
|
||||
lang_url = SERVER_URL + 'translation/%s/?file' % language
|
||||
request = urllib.request.Request(lang_url)
|
||||
request.add_header('Authorization', auth_header)
|
||||
filename = os.path.join(os.path.abspath('..'), 'resources', 'i18n', language + '.ts')
|
||||
print_verbose('Get Translation File: %s' % filename)
|
||||
response = urllib.request.urlopen(request)
|
||||
fd = open(filename, 'wb')
|
||||
fd.write(response.read())
|
||||
fd.close()
|
||||
print_quiet(' Done.')
|
||||
return True
|
||||
|
||||
|
||||
def prepare_project():
|
||||
"""
|
||||
This method creates the project file needed to update the translation files and compile them into .qm files.
|
||||
"""
|
||||
print_quiet('Generating the openlp.pro file')
|
||||
print('Generating the openlp.pro file')
|
||||
lines = []
|
||||
start_dir = os.path.abspath('..')
|
||||
start_dir = start_dir + os.sep
|
||||
print_verbose('Starting directory: %s' % start_dir)
|
||||
print('Starting directory: %s' % start_dir)
|
||||
for root, dirs, files in os.walk(start_dir):
|
||||
for file in files:
|
||||
path = root.replace(start_dir, '').replace('\\', '/')
|
||||
@ -250,180 +69,14 @@ def prepare_project():
|
||||
line = '%s/%s' % (path, file)
|
||||
else:
|
||||
line = file
|
||||
print_verbose('Parsing "%s"' % line)
|
||||
print('Parsing "%s"' % line)
|
||||
lines.append('SOURCES += %s' % line)
|
||||
elif file.endswith('.ts'):
|
||||
line = '%s/%s' % (path, file)
|
||||
print_verbose('Parsing "%s"' % line)
|
||||
lines.append('TRANSLATIONS += %s' % line)
|
||||
lines.append('TRANSLATIONS += resources/i18n/en.ts')
|
||||
lines.sort()
|
||||
file = open(os.path.join(start_dir, 'openlp.pro'), 'w')
|
||||
file.write('\n'.join(lines))
|
||||
file.close()
|
||||
print_quiet(' Done.')
|
||||
|
||||
|
||||
def update_translations():
|
||||
print_quiet('Update the translation files')
|
||||
if not os.path.exists(os.path.join(os.path.abspath('..'), 'openlp.pro')):
|
||||
print('You have not generated a project file yet, please run this script with the -p option.')
|
||||
return
|
||||
else:
|
||||
os.chdir(os.path.abspath('..'))
|
||||
run('pylupdate5 -verbose -noobsolete openlp.pro')
|
||||
os.chdir(os.path.abspath('scripts'))
|
||||
|
||||
|
||||
def generate_binaries():
|
||||
print_quiet('Generate the related *.qm files')
|
||||
if not os.path.exists(os.path.join(os.path.abspath('..'), 'openlp.pro')):
|
||||
print('You have not generated a project file yet, please run this script with the -p option. It is also ' +
|
||||
'recommended that you this script with the -u option to update the translation files as well.')
|
||||
return
|
||||
else:
|
||||
os.chdir(os.path.abspath('..'))
|
||||
run('lrelease openlp.pro')
|
||||
print_quiet(' Done.')
|
||||
|
||||
|
||||
def create_translation():
|
||||
"""
|
||||
This method opens a browser to the OpenLP project page at Transifex so
|
||||
that the user can request a new language.
|
||||
"""
|
||||
print_quiet('Please request a new language at the OpenLP project on Transifex.')
|
||||
webbrowser.open('https://www.transifex.net/projects/p/openlp/resource/ents/')
|
||||
print_quiet('Opening browser to OpenLP project...')
|
||||
|
||||
|
||||
def check_format_strings():
|
||||
"""
|
||||
This method runs through the ts-files and looks for mismatches between format strings in the original text
|
||||
and in the translations.
|
||||
"""
|
||||
is_ok = True
|
||||
path = os.path.join(os.path.abspath('..'), 'resources', 'i18n', '*.ts')
|
||||
file_list = glob.glob(path)
|
||||
for filename in file_list:
|
||||
print_quiet('Checking %s' % filename)
|
||||
file = open(filename, 'rb')
|
||||
tree = objectify.parse(file)
|
||||
root = tree.getroot()
|
||||
for tag in root.iter('message'):
|
||||
location = tag.location.get('filename')
|
||||
line = tag.location.get('line')
|
||||
org_text = tag.source.text
|
||||
translation = tag.translation.text
|
||||
if not translation:
|
||||
for num in tag.iter('numerusform'):
|
||||
print_verbose('parsed numerusform: location: %s, source: %s, translation: %s' % (
|
||||
location, org_text, num.text))
|
||||
if num and org_text.count('%') != num.text.count('%'):
|
||||
is_ok = False
|
||||
print_quiet(
|
||||
'ERROR: Translation from %s at line %s has a mismatch of format input:\n%s\n%s\n' % (
|
||||
location, line, org_text, num.text))
|
||||
else:
|
||||
print_verbose('parsed: location: %s, source: %s, translation: %s' % (location, org_text, translation))
|
||||
if org_text.count('%') != translation.count('%'):
|
||||
is_ok = False
|
||||
print_quiet('ERROR: Translation from %s at line %s has a mismatch of format input:\n%s\n%s\n' % (
|
||||
location, line, org_text, translation))
|
||||
return is_ok
|
||||
|
||||
|
||||
def process_stack(command_stack):
|
||||
"""
|
||||
This method looks at the commands in the command stack, and processes them
|
||||
in the order they are in the stack.
|
||||
|
||||
``command_stack``
|
||||
The command stack to process.
|
||||
"""
|
||||
is_success = True
|
||||
if command_stack:
|
||||
print_quiet('Processing %d commands...' % len(command_stack))
|
||||
for command in command_stack:
|
||||
print_quiet('%d.' % (command_stack.current_index), False)
|
||||
if command == Command.Download:
|
||||
if not download_translations():
|
||||
return
|
||||
elif command == Command.Prepare:
|
||||
prepare_project()
|
||||
elif command == Command.Update:
|
||||
update_translations()
|
||||
elif command == Command.Generate:
|
||||
generate_binaries()
|
||||
elif command == Command.Create:
|
||||
create_translation()
|
||||
elif command == Command.Check:
|
||||
is_success = check_format_strings()
|
||||
print_quiet('Finished processing commands.')
|
||||
else:
|
||||
print_quiet('No commands to process.')
|
||||
return is_success
|
||||
|
||||
|
||||
def main():
|
||||
global verbose_mode, quiet_mode, username, password
|
||||
# Set up command line options.
|
||||
usage = '%(prog)s [options]\nOptions are parsed in the order they are ' + \
|
||||
'listed below. If no options are given, "-dpug" will be used.\n\n' + \
|
||||
'This script is used to manage OpenLP\'s translation files.'
|
||||
parser = ArgumentParser(usage=usage)
|
||||
parser.add_argument('-U', '--username', dest='username', metavar='USERNAME',
|
||||
help='Transifex username, used for authentication')
|
||||
parser.add_argument('-P', '--password', dest='password', metavar='PASSWORD',
|
||||
help='Transifex password, used for authentication')
|
||||
parser.add_argument('-d', '--download-ts', dest='download',
|
||||
action='store_true', help='download language files from Transifex')
|
||||
parser.add_argument('-c', '--create', dest='create', action='store_true',
|
||||
help='go to Transifex to request a new translation file')
|
||||
parser.add_argument('-p', '--prepare', dest='prepare', action='store_true',
|
||||
help='generate a project file, used to update the translations')
|
||||
parser.add_argument('-u', '--update', action='store_true', dest='update',
|
||||
help='update translation files (needs a project file)')
|
||||
parser.add_argument('-g', '--generate', dest='generate', action='store_true',
|
||||
help='compile .ts files into .qm files')
|
||||
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
|
||||
help='show extra information while processing translations')
|
||||
parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
|
||||
help='suppress all output other than errors')
|
||||
parser.add_argument('-f', '--check-format-strings', dest='check', action='store_true',
|
||||
help='check that format strings are matching in translations')
|
||||
args = parser.parse_args()
|
||||
# Create and populate the command stack
|
||||
command_stack = CommandStack()
|
||||
if args.download:
|
||||
command_stack.append(Command.Download)
|
||||
if args.create:
|
||||
command_stack.append(Command.Create, arguments=[args.create])
|
||||
if args.prepare:
|
||||
command_stack.append(Command.Prepare)
|
||||
if args.update:
|
||||
command_stack.append(Command.Update)
|
||||
if args.generate:
|
||||
command_stack.append(Command.Generate)
|
||||
if args.check:
|
||||
command_stack.append(Command.Check)
|
||||
verbose_mode = args.verbose
|
||||
quiet_mode = args.quiet
|
||||
if args.username:
|
||||
username = args.username
|
||||
if args.password:
|
||||
password = args.password
|
||||
if not command_stack:
|
||||
command_stack.append(Command.Download)
|
||||
command_stack.append(Command.Prepare)
|
||||
command_stack.append(Command.Update)
|
||||
command_stack.append(Command.Generate)
|
||||
# Process the commands
|
||||
return process_stack(command_stack)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if os.path.split(os.path.abspath('.'))[1] != 'scripts':
|
||||
print('You need to run this script from the scripts directory.')
|
||||
else:
|
||||
if not main():
|
||||
sys.exit(1)
|
||||
prepare_project()
|
||||
|
2
setup.py
2
setup.py
@ -191,7 +191,7 @@ using a computer and a data projector.""",
|
||||
extras_require={
|
||||
'agpl-pdf': ['PyMuPDF'],
|
||||
'darkstyle': ['QDarkStyle'],
|
||||
'mysql': ['mysql-connector-python'],
|
||||
'mysql': ['pymysql'],
|
||||
'odbc': ['pyodbc'],
|
||||
'postgresql': ['psycopg2'],
|
||||
'spellcheck': ['pyenchant >= 1.6'],
|
||||
|
@ -22,10 +22,20 @@
|
||||
"""
|
||||
Test the :mod:`~openlp.core.display.render` package.
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
import sys
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.common import ThemeLevel
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
sys.modules['PyQt5.QtWebEngineWidgets'] = MagicMock()
|
||||
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.display.render import compare_chord_lyric_width, find_formatting_tags, remove_tags, render_chords, \
|
||||
render_chords_for_printing, render_tags
|
||||
render_chords_for_printing, render_tags, ThemePreviewRenderer
|
||||
from openlp.core.lib.formattingtags import FormattingTags
|
||||
|
||||
|
||||
@ -208,3 +218,101 @@ def test_find_formatting_tags():
|
||||
|
||||
# THEN: The list of active tags should contain only 'st'
|
||||
assert active_tags == ['st'], 'The list of active tags should contain only "st"'
|
||||
|
||||
|
||||
class TestThemePreviewRenderer(TestMixin, TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up the components need for all tests.
|
||||
"""
|
||||
# Create the Registry
|
||||
self.application = QtWidgets.QApplication.instance()
|
||||
Registry.create()
|
||||
self.application.setOrganizationName('OpenLP-tests')
|
||||
self.application.setOrganizationDomain('openlp.org')
|
||||
|
||||
def test_get_theme_global(self):
|
||||
"""
|
||||
Test the return of the global theme if set to Global level
|
||||
"""
|
||||
# GIVEN: A set up with a Global Theme and settings at Global
|
||||
mocked_theme_manager = MagicMock()
|
||||
mocked_theme_manager.global_theme = 'my_global_theme'
|
||||
Registry().register('theme_manager', mocked_theme_manager)
|
||||
with patch('openlp.core.display.webengine.WebEngineView'), \
|
||||
patch('PyQt5.QtWidgets.QVBoxLayout'):
|
||||
tpr = ThemePreviewRenderer()
|
||||
tpr.theme_level = ThemeLevel.Global
|
||||
# WHEN: I Request the theme to Use
|
||||
theme = tpr.get_theme(MagicMock())
|
||||
|
||||
# THEN: The list of active tags should contain only 'st'
|
||||
assert theme == mocked_theme_manager.global_theme, 'The Theme returned is not that of the global theme'
|
||||
|
||||
def test_get_theme_service(self):
|
||||
"""
|
||||
Test the return of the global theme if set to Global level
|
||||
"""
|
||||
# GIVEN: A set up with a Global Theme and settings at Global
|
||||
mocked_theme_manager = MagicMock()
|
||||
mocked_theme_manager.global_theme = 'my_global_theme'
|
||||
mocked_service_manager = MagicMock()
|
||||
mocked_service_manager.service_theme = 'my_service_theme'
|
||||
Registry().register('theme_manager', mocked_theme_manager)
|
||||
Registry().register('service_manager', mocked_service_manager)
|
||||
with patch('openlp.core.display.webengine.WebEngineView'), \
|
||||
patch('PyQt5.QtWidgets.QVBoxLayout'):
|
||||
tpr = ThemePreviewRenderer()
|
||||
tpr.theme_level = ThemeLevel.Service
|
||||
# WHEN: I Request the theme to Use
|
||||
theme = tpr.get_theme(MagicMock())
|
||||
|
||||
# THEN: The list of active tags should contain only 'st'
|
||||
assert theme == mocked_service_manager.service_theme, 'The Theme returned is not that of the Service theme'
|
||||
|
||||
def test_get_theme_item_level_none(self):
|
||||
"""
|
||||
Test the return of the global theme if set to Global level
|
||||
"""
|
||||
# GIVEN: A set up with a Global Theme and settings at Global
|
||||
mocked_theme_manager = MagicMock()
|
||||
mocked_theme_manager.global_theme = 'my_global_theme'
|
||||
mocked_service_manager = MagicMock()
|
||||
mocked_service_manager.service_theme = 'my_service_theme'
|
||||
mocked_item = MagicMock()
|
||||
mocked_item.theme = None
|
||||
Registry().register('theme_manager', mocked_theme_manager)
|
||||
Registry().register('service_manager', mocked_service_manager)
|
||||
with patch('openlp.core.display.webengine.WebEngineView'), \
|
||||
patch('PyQt5.QtWidgets.QVBoxLayout'):
|
||||
tpr = ThemePreviewRenderer()
|
||||
tpr.theme_level = ThemeLevel.Song
|
||||
# WHEN: I Request the theme to Use
|
||||
theme = tpr.get_theme(mocked_item)
|
||||
|
||||
# THEN: The list of active tags should contain only 'st'
|
||||
assert theme == mocked_theme_manager.global_theme, 'The Theme returned is not that of the global theme'
|
||||
|
||||
def test_get_theme_item_level_set(self):
|
||||
"""
|
||||
Test the return of the global theme if set to Global level
|
||||
"""
|
||||
# GIVEN: A set up with a Global Theme and settings at Global
|
||||
mocked_theme_manager = MagicMock()
|
||||
mocked_theme_manager.global_theme = 'my_global_theme'
|
||||
mocked_service_manager = MagicMock()
|
||||
mocked_service_manager.service_theme = 'my_service_theme'
|
||||
mocked_item = MagicMock()
|
||||
mocked_item.theme = "my_item_theme"
|
||||
Registry().register('theme_manager', mocked_theme_manager)
|
||||
Registry().register('service_manager', mocked_service_manager)
|
||||
with patch('openlp.core.display.webengine.WebEngineView'), \
|
||||
patch('PyQt5.QtWidgets.QVBoxLayout'):
|
||||
tpr = ThemePreviewRenderer()
|
||||
tpr.theme_level = ThemeLevel.Song
|
||||
# WHEN: I Request the theme to Use
|
||||
theme = tpr.get_theme(mocked_item)
|
||||
|
||||
# THEN: The list of active tags should contain only 'st'
|
||||
assert theme == mocked_item.theme, 'The Theme returned is not that of the item theme'
|
||||
|
@ -30,10 +30,10 @@ from openlp.core.ui.aboutform import AboutForm
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
class TestFirstTimeForm(TestCase, TestMixin):
|
||||
class TestAboutForm(TestCase, TestMixin):
|
||||
|
||||
@patch('openlp.core.ui.aboutform.webbrowser')
|
||||
def test_on_volunteer_button_clicked(self, mocked_webbrowser):
|
||||
def test_on_contribute_button_clicked(self, mocked_webbrowser):
|
||||
"""
|
||||
Test that clicking on the "Volunteer" button opens a web page.
|
||||
"""
|
||||
@ -41,10 +41,10 @@ class TestFirstTimeForm(TestCase, TestMixin):
|
||||
about_form = AboutForm(None)
|
||||
|
||||
# WHEN: The "Volunteer" button is "clicked"
|
||||
about_form.on_volunteer_button_clicked()
|
||||
about_form.on_contribute_button_clicked()
|
||||
|
||||
# THEN: A web browser is opened
|
||||
mocked_webbrowser.open_new.assert_called_with('http://openlp.org/en/contribute')
|
||||
mocked_webbrowser.open_new.assert_called_with('http://openlp.org/contribute')
|
||||
|
||||
@patch('openlp.core.ui.aboutform.get_version')
|
||||
def test_about_form_build_number(self, mocked_get_version):
|
||||
@ -66,11 +66,11 @@ class TestFirstTimeForm(TestCase, TestMixin):
|
||||
Test that the copyright date is included correctly
|
||||
"""
|
||||
# GIVEN: A correct application date
|
||||
date_string = "2004-%s" % datetime.date.today().year
|
||||
date_string = '2004-{year}'.format(year=datetime.date.today().year)
|
||||
|
||||
# WHEN: The about form is created
|
||||
about_form = AboutForm(None)
|
||||
license_text = about_form.license_text_edit.toPlainText()
|
||||
about_text = about_form.about_text_edit.toPlainText()
|
||||
|
||||
# THEN: The date should be in the text twice.
|
||||
assert license_text.count(date_string, 0) == 2, "The text string should be added twice to the license string"
|
||||
assert about_text.count(date_string, 0) == 1, "The text string should be added twice to the license string"
|
||||
|
@ -1,3 +1,12 @@
|
||||
function _createDiv(attrs) {
|
||||
var div = document.createElement("div");
|
||||
for (key in attrs) {
|
||||
div.setAttribute(key, attrs[key]);
|
||||
}
|
||||
document.body.appendChild(div);
|
||||
return div;
|
||||
}
|
||||
|
||||
describe("The enumeration object", function () {
|
||||
it("BackgroundType should exist", function () {
|
||||
expect(BackgroundType).toBeDefined();
|
||||
@ -22,9 +31,7 @@ describe("The enumeration object", function () {
|
||||
|
||||
describe("The function", function () {
|
||||
it("$() should return the right element", function () {
|
||||
var div = document.createElement("div");
|
||||
div.setAttribute("id", "dollar-test");
|
||||
document.body.appendChild(div);
|
||||
var div = _createDiv({"id": "dollar-test"});
|
||||
expect($("#dollar-test")[0]).toBe(div);
|
||||
});
|
||||
|
||||
@ -39,10 +46,8 @@ describe("The function", function () {
|
||||
});
|
||||
|
||||
it("_getStyle should return the correct style on an element", function () {
|
||||
var div = document.createElement("div");
|
||||
var div = _createDiv({"id": "style-test"});
|
||||
div.style.setProperty("width", "100px");
|
||||
div.setAttribute("id", "style-test");
|
||||
document.body.appendChild(div);
|
||||
expect(_getStyle($("#style-test")[0], "width")).toBe("100px");
|
||||
});
|
||||
|
||||
@ -120,10 +125,8 @@ describe("The Display object", function () {
|
||||
expect(Display.clearSlides).toBeDefined();
|
||||
|
||||
document.body.innerHTML = "";
|
||||
var slidesDiv = document.createElement("div");
|
||||
slidesDiv.setAttribute("class", "slides");
|
||||
var slidesDiv = _createDiv({"class": "slides"});
|
||||
slidesDiv.innerHTML = "<section><p></p></section>";
|
||||
document.body.appendChild(slidesDiv);
|
||||
|
||||
Display.clearSlides();
|
||||
expect($(".slides")[0].innerHTML).toEqual("");
|
||||
@ -143,17 +146,18 @@ describe("The Display object", function () {
|
||||
describe("Display.addTextSlide", function () {
|
||||
beforeEach(function() {
|
||||
document.body.innerHTML = "";
|
||||
var slidesDiv = document.createElement("div");
|
||||
slidesDiv.setAttribute("class", "slides");
|
||||
document.body.appendChild(slidesDiv);
|
||||
_createDiv({"class": "slides"});
|
||||
_createDiv({"class": "footer"});
|
||||
Display._slides = {};
|
||||
});
|
||||
|
||||
it("should add a new slide", function () {
|
||||
var verse = "v1", text = "Amazing grace,\nhow sweet the sound";
|
||||
var verse = "v1",
|
||||
text = "Amazing grace,\nhow sweet the sound",
|
||||
footer = "Public Domain";
|
||||
spyOn(Display, "reinit");
|
||||
|
||||
Display.addTextSlide(verse, text);
|
||||
Display.addTextSlide(verse, text, footer);
|
||||
|
||||
expect(Display._slides[verse]).toEqual(0);
|
||||
expect($(".slides > section").length).toEqual(1);
|
||||
@ -162,10 +166,12 @@ describe("Display.addTextSlide", function () {
|
||||
});
|
||||
|
||||
it("should add a new slide without calling reinit()", function () {
|
||||
var verse = "v1", text = "Amazing grace,\nhow sweet the sound";
|
||||
var verse = "v1",
|
||||
text = "Amazing grace,\nhow sweet the sound",
|
||||
footer = "Public Domain";
|
||||
spyOn(Display, "reinit");
|
||||
|
||||
Display.addTextSlide(verse, text, false);
|
||||
Display.addTextSlide(verse, text, footer, false);
|
||||
|
||||
expect(Display._slides[verse]).toEqual(0);
|
||||
expect($(".slides > section").length).toEqual(1);
|
||||
@ -174,8 +180,10 @@ describe("Display.addTextSlide", function () {
|
||||
});
|
||||
|
||||
it("should update an existing slide", function () {
|
||||
var verse = "v1", text = "Amazing grace, how sweet the sound\nThat saved a wretch like me";
|
||||
Display.addTextSlide(verse, "Amazing grace,\nhow sweet the sound", false);
|
||||
var verse = "v1",
|
||||
text = "Amazing grace, how sweet the sound\nThat saved a wretch like me",
|
||||
footer = "Public Domain";
|
||||
Display.addTextSlide(verse, "Amazing grace,\nhow sweet the sound", footer, false);
|
||||
spyOn(Display, "reinit");
|
||||
|
||||
Display.addTextSlide(verse, text, true);
|
||||
@ -190,18 +198,9 @@ describe("Display.addTextSlide", function () {
|
||||
describe("Display.setTextSlides", function () {
|
||||
beforeEach(function() {
|
||||
document.body.innerHTML = "";
|
||||
var slidesDiv = document.createElement("div");
|
||||
slidesDiv.setAttribute("class", "slides");
|
||||
document.body.appendChild(slidesDiv);
|
||||
|
||||
var background = document.createElement("div");
|
||||
background.setAttribute("id", "global-background");
|
||||
document.body.appendChild(background);
|
||||
|
||||
var footer = document.createElement("div");
|
||||
footer.setAttribute("class", "footer");
|
||||
document.body.appendChild(footer);
|
||||
|
||||
_createDiv({"class": "slides"});
|
||||
_createDiv({"class": "footer"});
|
||||
_createDiv({"id": "global-background"});
|
||||
Display._slides = {};
|
||||
});
|
||||
|
||||
@ -210,12 +209,14 @@ describe("Display.setTextSlides", function () {
|
||||
{
|
||||
"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"
|
||||
"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."
|
||||
"How precious did that grace appear,\nthe hour I first believed.",
|
||||
"footer": "Public Domain"
|
||||
}
|
||||
];
|
||||
spyOn(Display, "clearSlides");
|
||||
@ -232,29 +233,27 @@ describe("Display.setTextSlides", function () {
|
||||
expect(Reveal.slide).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it("should correctly set outline width", function () {
|
||||
xit("should correctly set outline width (skipped for now)", function () {
|
||||
const 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"
|
||||
"I once was lost, but now I'm found\nWas blind but now I see",
|
||||
"footer": "Public Domain"
|
||||
}
|
||||
];
|
||||
|
||||
const theme = {
|
||||
'font_main_color': 'yellow',
|
||||
'font_main_outline': true,
|
||||
'font_main_outline_size': 42,
|
||||
'font_main_outline_color': 'red'
|
||||
};
|
||||
|
||||
spyOn(Display, "reinit");
|
||||
|
||||
Display.setTextSlides(slides);
|
||||
Display.setTheme(theme);
|
||||
|
||||
const slidesDiv = $(".slides")[0];
|
||||
|
||||
expect(slidesDiv.style['-webkit-text-stroke']).toEqual('42pt red');
|
||||
expect(slidesDiv.style['padding-left']).toEqual('84pt');
|
||||
expect(slidesDiv.style['-webkit-text-fill-color']).toEqual('yellow');
|
||||
@ -264,12 +263,9 @@ describe("Display.setTextSlides", function () {
|
||||
describe("Display.setImageSlides", function () {
|
||||
beforeEach(function() {
|
||||
document.body.innerHTML = "";
|
||||
var slidesDiv = document.createElement("div");
|
||||
slidesDiv.setAttribute("class", "slides");
|
||||
document.body.appendChild(slidesDiv);
|
||||
var backgroundDiv = document.createElement("div");
|
||||
backgroundDiv.setAttribute("id", "global-background");
|
||||
document.body.appendChild(backgroundDiv);
|
||||
_createDiv({"class": "slides"});
|
||||
_createDiv({"class": "footer"});
|
||||
_createDiv({"id": "global-background"});
|
||||
Display._slides = {};
|
||||
});
|
||||
|
||||
@ -286,7 +282,9 @@ describe("Display.setImageSlides", function () {
|
||||
expect($(".slides > section").length).toEqual(2);
|
||||
expect($(".slides > section > img").length).toEqual(2);
|
||||
expect($(".slides > section > img")[0].getAttribute("src")).toEqual("file:///openlp1.jpg")
|
||||
expect($(".slides > section > img")[0].getAttribute("style")).toEqual("max-width: 100%; max-height: 100%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);")
|
||||
expect($(".slides > section > img")[1].getAttribute("src")).toEqual("file:///openlp2.jpg")
|
||||
expect($(".slides > section > img")[1].getAttribute("style")).toEqual("max-width: 100%; max-height: 100%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);")
|
||||
expect(Display.reinit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -294,12 +292,8 @@ describe("Display.setImageSlides", function () {
|
||||
describe("Display.setVideo", function () {
|
||||
beforeEach(function() {
|
||||
document.body.innerHTML = "";
|
||||
var slidesDiv = document.createElement("div");
|
||||
slidesDiv.setAttribute("class", "slides");
|
||||
document.body.appendChild(slidesDiv);
|
||||
var backgroundDiv = document.createElement("div");
|
||||
backgroundDiv.setAttribute("id", "global-background");
|
||||
document.body.appendChild(backgroundDiv);
|
||||
_createDiv({"class": "slides"});
|
||||
_createDiv({"id": "global-background"});
|
||||
Display._slides = {};
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
{title:Swing Low Sweet Chariot}
|
||||
{st:Traditional}
|
||||
{lyricist:Wallis Willis}
|
||||
{meta:composer Wallis Willis}
|
||||
|
||||
{start_of_chorus}
|
||||
Swing [D]low, sweet [G]chari[D]ot,
|
||||
|
@ -1,6 +1,10 @@
|
||||
{
|
||||
"title": "Swing Low Sweet Chariot",
|
||||
"alternative_title": "Traditional",
|
||||
"authors": [
|
||||
["Wallis Willis", "words"],
|
||||
["Wallis Willis", "music"]
|
||||
],
|
||||
"verses": [
|
||||
[
|
||||
"Swing [D]low, sweet [G]chari[D]ot,\nComin' for to carry me [A7]home.\nSwing [D7]low, sweet [G]chari[D]ot,\nComin' for to [A7]carry me [D]home.",
|
||||
|
Loading…
Reference in New Issue
Block a user