diff --git a/.bzrignore b/.bzrignore
index adae3204e..f60d6cfff 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -45,4 +45,11 @@ resources/innosetup/Output
resources/windows/warnOpenLP.txt
*.ropeproject
tags
+output
+htmlcov
+node_modules
+openlp-test-projectordb.sqlite
+package-lock.json
+.cache
+test
tests.kdev4
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 000000000..2a1038a0e
--- /dev/null
+++ b/karma.conf.js
@@ -0,0 +1,77 @@
+module.exports = function(config) {
+ config.set({
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: "",
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ["jasmine"],
+
+ // list of files / patterns to load in the browser
+ files: [
+ "tests/js/polyfill.js",
+ "tests/js/fake_webchannel.js",
+ "openlp/core/display/html/reveal.js",
+ "openlp/core/display/html/display.js",
+ "tests/js/test_*.js"
+ ],
+
+ // list of files to exclude
+ exclude: [
+ ],
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ // source files, that you wanna generate coverage for
+ // do not include tests or libraries
+ // (these files will be instrumented by Istanbul)
+ "display.js": ["coverage"]
+ },
+
+ // test results reporter to use
+ // possible values: "dots", "progress"
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ["progress", "coverage"],
+
+ // configure the coverateReporter
+ coverageReporter: {
+ type : "html",
+ dir : "htmlcov/"
+ },
+
+ // web server port
+ port: 9876,
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_DEBUG,
+
+ // loggers
+ /* loggers: [
+ {"type": "file", "filename": "karma.log"}
+ ],*/
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: true,
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ["PhantomJS"],
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: false,
+
+ // Concurrency level
+ // how many browser should be started simultaneous
+ concurrency: Infinity,
+
+ client: {
+ captureConsole: true
+ }
+ })
+}
diff --git a/nose2.cfg b/nose2.cfg
index ae73407d7..451737d6c 100644
--- a/nose2.cfg
+++ b/nose2.cfg
@@ -1,5 +1,5 @@
[unittest]
-verbose = True
+verbose = true
plugins = nose2.plugins.mp
[log-capture]
@@ -9,19 +9,19 @@ filter = -nose
log-level = ERROR
[test-result]
-always-on = True
-descriptions = True
+always-on = true
+descriptions = true
[coverage]
-always-on = True
+always-on = true
coverage = openlp
coverage-report = html
[multiprocess]
-always-on = False
+always-on = false
processes = 4
[output-buffer]
-always-on = True
-stderr = True
-stdout = False
+always-on = true
+stderr = true
+stdout = true
diff --git a/openlp/.version b/openlp/.version
index c8e38b614..a4b9b4eef 100644
--- a/openlp/.version
+++ b/openlp/.version
@@ -1 +1 @@
-2.9.0
+2.5.dev2856
\ No newline at end of file
diff --git a/openlp/core/api/deploy.py b/openlp/core/api/deploy.py
index e336019d3..6bc2517a4 100644
--- a/openlp/core/api/deploy.py
+++ b/openlp/core/api/deploy.py
@@ -25,7 +25,7 @@ Download and "install" the remote web client
from zipfile import ZipFile
from openlp.core.common.applocation import AppLocation
-from openlp.core.common.httputils import download_file, get_web_page, get_url_file_size
+from openlp.core.common.httputils import download_file, get_url_file_size, get_web_page
from openlp.core.common.registry import Registry
diff --git a/openlp/core/api/endpoint/controller.py b/openlp/core/api/endpoint/controller.py
index 7aa75b182..7af852d91 100644
--- a/openlp/core/api/endpoint/controller.py
+++ b/openlp/core/api/endpoint/controller.py
@@ -34,6 +34,7 @@ from openlp.core.common.settings import Settings
from openlp.core.lib import create_thumb
from openlp.core.lib.serviceitem import ItemCapabilities
+
log = logging.getLogger(__name__)
controller_endpoint = Endpoint('controller')
@@ -48,7 +49,7 @@ def controller_text(request):
:param request: the http request - not used
"""
- log.debug("controller_text ")
+ log.debug('controller_text')
live_controller = Registry().get('live_controller')
current_item = live_controller.service_item
data = []
@@ -57,13 +58,14 @@ def controller_text(request):
item = {}
# Handle text (songs, custom, bibles)
if current_item.is_text():
- if frame['verseTag']:
- item['tag'] = str(frame['verseTag'])
+ if frame['verse']:
+ item['tag'] = str(frame['verse'])
else:
item['tag'] = str(index + 1)
- item['chords_text'] = str(frame['chords_text'])
- item['text'] = str(frame['text'])
- item['html'] = str(frame['html'])
+ # TODO: Figure out rendering chords
+ item['chords_text'] = str(frame.get('chords_text', ''))
+ item['text'] = frame['text']
+ item['html'] = current_item.get_rendered_frame(index)
# Handle images, unless a custom thumbnail is given or if thumbnails is disabled
elif current_item.is_image() and not frame.get('image', '') and Settings().value('api/thumbnails'):
item['tag'] = str(index + 1)
diff --git a/openlp/core/api/endpoint/core.py b/openlp/core/api/endpoint/core.py
index 43a2b8459..d7fee2817 100644
--- a/openlp/core/api/endpoint/core.py
+++ b/openlp/core/api/endpoint/core.py
@@ -30,8 +30,8 @@ from openlp.core.api.http.endpoint import Endpoint
from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.registry import Registry
from openlp.core.lib import image_to_byte
-from openlp.core.lib.plugin import StringContent
-from openlp.core.lib.plugin import PluginStatus
+from openlp.core.lib.plugin import PluginStatus, StringContent
+
template_dir = 'templates'
static_dir = 'static'
diff --git a/openlp/core/api/endpoint/remote.py b/openlp/core/api/endpoint/remote.py
index 0e637339f..62b44f73b 100644
--- a/openlp/core/api/endpoint/remote.py
+++ b/openlp/core/api/endpoint/remote.py
@@ -24,6 +24,7 @@ import logging
from openlp.core.api.endpoint.core import TRANSLATED_STRINGS
from openlp.core.api.http.endpoint import Endpoint
+
log = logging.getLogger(__name__)
remote_endpoint = Endpoint('remote', template_dir='remotes')
diff --git a/openlp/core/api/endpoint/service.py b/openlp/core/api/endpoint/service.py
index b642c3151..737fd7a7a 100644
--- a/openlp/core/api/endpoint/service.py
+++ b/openlp/core/api/endpoint/service.py
@@ -26,6 +26,7 @@ from openlp.core.api.http import requires_auth
from openlp.core.api.http.endpoint import Endpoint
from openlp.core.common.registry import Registry
+
log = logging.getLogger(__name__)
service_endpoint = Endpoint('service')
diff --git a/openlp/core/api/http/server.py b/openlp/core/api/http/server.py
index 914ce2278..68a8e6781 100644
--- a/openlp/core/api/http/server.py
+++ b/openlp/core/api/http/server.py
@@ -30,22 +30,21 @@ from PyQt5 import QtCore, QtWidgets
from waitress.server import create_server
from openlp.core.api.deploy import download_and_check, download_sha256
-from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint
-from openlp.core.api.endpoint.core import chords_endpoint, stage_endpoint, blank_endpoint, main_endpoint
+from openlp.core.api.endpoint.controller import api_controller_endpoint, controller_endpoint
+from openlp.core.api.endpoint.core import blank_endpoint, chords_endpoint, main_endpoint, stage_endpoint
from openlp.core.api.endpoint.remote import remote_endpoint
-from openlp.core.api.endpoint.service import service_endpoint, api_service_endpoint
-from openlp.core.api.http import application
-from openlp.core.api.http import register_endpoint
+from openlp.core.api.endpoint.service import api_service_endpoint, service_endpoint
+from openlp.core.api.http import application, register_endpoint
from openlp.core.api.poll import Poller
from openlp.core.common.applocation import AppLocation
-from openlp.core.common.i18n import UiStrings
-from openlp.core.common.i18n import translate
+from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.path import create_paths
from openlp.core.common.registry import Registry, RegistryBase
from openlp.core.common.settings import Settings
from openlp.core.threading import ThreadWorker, run_thread
+
log = logging.getLogger(__name__)
diff --git a/openlp/core/api/http/wsgiapp.py b/openlp/core/api/http/wsgiapp.py
index aa90a28aa..f8f0cdd20 100644
--- a/openlp/core/api/http/wsgiapp.py
+++ b/openlp/core/api/http/wsgiapp.py
@@ -33,6 +33,7 @@ from webob.static import DirectoryApp
from openlp.core.api.http.errors import HttpError, NotFound, ServerError
from openlp.core.common.applocation import AppLocation
+
ARGS_REGEX = re.compile(r'''\{(\w+)(?::([^}]+))?\}''', re.VERBOSE)
log = logging.getLogger(__name__)
diff --git a/openlp/core/api/tab.py b/openlp/core/api/tab.py
index 61e922c32..ac8af9007 100644
--- a/openlp/core/api/tab.py
+++ b/openlp/core/api/tab.py
@@ -31,6 +31,7 @@ from openlp.core.common.settings import Settings
from openlp.core.lib.settingstab import SettingsTab
from openlp.core.ui.icons import UiIcons
+
ZERO_URL = '0.0.0.0'
@@ -43,9 +44,9 @@ class ApiTab(SettingsTab):
advanced_translated = translate('OpenLP.AdvancedTab', 'Advanced')
super(ApiTab, self).__init__(parent, 'api', advanced_translated)
- def setupUi(self):
+ def setup_ui(self):
self.setObjectName('ApiTab')
- super(ApiTab, self).setupUi()
+ super(ApiTab, self).setup_ui()
self.server_settings_group_box = QtWidgets.QGroupBox(self.left_column)
self.server_settings_group_box.setObjectName('server_settings_group_box')
self.server_settings_layout = QtWidgets.QFormLayout(self.server_settings_group_box)
@@ -154,7 +155,7 @@ class ApiTab(SettingsTab):
self.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed)
self.address_edit.textChanged.connect(self.set_urls)
- def retranslateUi(self):
+ def retranslate_ui(self):
self.tab_title_visible = translate('RemotePlugin.RemoteTab', 'Remote Interface')
self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
self.address_label.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:'))
diff --git a/openlp/core/api/websockets.py b/openlp/core/api/websockets.py
index 5528b4250..3dcaafea6 100644
--- a/openlp/core/api/websockets.py
+++ b/openlp/core/api/websockets.py
@@ -35,6 +35,7 @@ from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.threading import ThreadWorker, run_thread
+
log = logging.getLogger(__name__)
diff --git a/openlp/core/app.py b/openlp/core/app.py
index bc030f61c..18718d6c7 100644
--- a/openlp/core/app.py
+++ b/openlp/core/app.py
@@ -33,27 +33,28 @@ import time
from datetime import datetime
from traceback import format_exception
-from PyQt5 import QtCore, QtWidgets
+from PyQt5 import QtCore, QtWebEngineWidgets, QtWidgets # noqa
from openlp.core.state import State
from openlp.core.common import is_macosx, is_win
from openlp.core.common.applocation import AppLocation
from openlp.core.loader import loader
from openlp.core.common.i18n import LanguageManager, UiStrings, translate
-from openlp.core.common.path import create_paths, copytree
+from openlp.core.common.path import copytree, create_paths
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.resources import qInitResources
-from openlp.core.ui.splashscreen import SplashScreen
+from openlp.core.server import Server
from openlp.core.ui.exceptionform import ExceptionForm
from openlp.core.ui.firsttimeform import FirstTimeForm
from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm
from openlp.core.ui.mainwindow import MainWindow
+from openlp.core.ui.splashscreen import SplashScreen
from openlp.core.ui.style import get_application_stylesheet
-from openlp.core.server import Server
from openlp.core.version import check_for_update, get_version
+
__all__ = ['OpenLP', 'main']
@@ -74,7 +75,8 @@ class OpenLP(QtWidgets.QApplication):
"""
self.is_event_loop_active = True
result = QtWidgets.QApplication.exec()
- self.server.close_server()
+ if hasattr(self, 'server'):
+ self.server.close_server()
return result
def run(self, args):
@@ -317,7 +319,7 @@ def set_up_logging(log_path):
file_path = log_path / 'openlp.log'
# TODO: FileHandler accepts a Path object in Py3.6
logfile = logging.FileHandler(str(file_path), 'w', encoding='UTF-8')
- logfile.setFormatter(logging.Formatter('%(asctime)s %(name)-55s %(levelname)-8s %(message)s'))
+ logfile.setFormatter(logging.Formatter('%(asctime)s %(threadName)s %(name)-55s %(levelname)-8s %(message)s'))
log.addHandler(logfile)
if log.isEnabledFor(logging.DEBUG):
print('Logging to: {name}'.format(name=file_path))
@@ -330,7 +332,8 @@ def main(args=None):
:param args: Some args
"""
args = parse_options(args)
- qt_args = []
+ qt_args = ['--disable-web-security']
+ # qt_args = []
if args and args.loglevel.lower() in ['d', 'debug']:
log.setLevel(logging.DEBUG)
elif args and args.loglevel.lower() in ['w', 'warning']:
diff --git a/openlp/core/common/actions.py b/openlp/core/common/actions.py
index 94822bbac..59b2f3540 100644
--- a/openlp/core/common/actions.py
+++ b/openlp/core/common/actions.py
@@ -29,6 +29,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
from openlp.core.common.settings import Settings
+
log = logging.getLogger(__name__)
@@ -113,7 +114,6 @@ class CategoryActionList(object):
if item[1] == action:
self.actions.remove(item)
return
- log.warning('Action "{action}" does not exist.'.format(action=action))
class CategoryList(object):
diff --git a/openlp/core/common/applocation.py b/openlp/core/common/applocation.py
index 7b7c5781d..8d2f04105 100644
--- a/openlp/core/common/applocation.py
+++ b/openlp/core/common/applocation.py
@@ -29,10 +29,11 @@ import sys
import appdirs
import openlp
-from openlp.core.common import get_frozen_path, is_win, is_macosx
+from openlp.core.common import get_frozen_path, is_macosx, is_win
from openlp.core.common.path import Path, create_paths
from openlp.core.common.settings import Settings
+
log = logging.getLogger(__name__)
FROZEN_APP_PATH = Path(sys.argv[0]).parent
diff --git a/openlp/core/common/db.py b/openlp/core/common/db.py
index ef5b7b2b7..b3e66aef7 100644
--- a/openlp/core/common/db.py
+++ b/openlp/core/common/db.py
@@ -27,6 +27,7 @@ from copy import deepcopy
import sqlalchemy
+
log = logging.getLogger(__name__)
diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py
index 54173a8d2..5eab07047 100644
--- a/openlp/core/common/httputils.py
+++ b/openlp/core/common/httputils.py
@@ -34,6 +34,7 @@ from openlp.core.common import trace_error_handler
from openlp.core.common.registry import Registry
from openlp.core.common.settings import ProxyMode, Settings
+
log = logging.getLogger(__name__ + '.__init__')
USER_AGENTS = {
diff --git a/openlp/core/common/i18n.py b/openlp/core/common/i18n.py
index b2579d132..985d2f486 100644
--- a/openlp/core/common/i18n.py
+++ b/openlp/core/common/i18n.py
@@ -29,10 +29,11 @@ from collections import namedtuple
from PyQt5 import QtCore, QtWidgets
-from openlp.core.common import is_win, is_macosx
+from openlp.core.common import is_macosx, is_win
from openlp.core.common.applocation import AppLocation
from openlp.core.common.settings import Settings
+
log = logging.getLogger(__name__)
diff --git a/openlp/core/common/mixins.py b/openlp/core/common/mixins.py
index c78611077..9b58ffa63 100644
--- a/openlp/core/common/mixins.py
+++ b/openlp/core/common/mixins.py
@@ -28,6 +28,7 @@ import logging
from openlp.core.common import is_win, trace_error_handler
from openlp.core.common.registry import Registry
+
DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed',
'preview_size_changed', 'resizeEvent']
diff --git a/openlp/core/common/path.py b/openlp/core/common/path.py
index 64ec64a94..b3d96da71 100644
--- a/openlp/core/common/path.py
+++ b/openlp/core/common/path.py
@@ -25,6 +25,7 @@ from contextlib import suppress
from openlp.core.common import is_win
+
if is_win():
from pathlib import WindowsPath as PathVariant # pragma: nocover
else:
diff --git a/openlp/core/common/registry.py b/openlp/core/common/registry.py
index 39ebe8d83..628bf4750 100644
--- a/openlp/core/common/registry.py
+++ b/openlp/core/common/registry.py
@@ -27,6 +27,7 @@ import sys
from openlp.core.common import de_hump, trace_error_handler
+
log = logging.getLogger(__name__)
@@ -143,6 +144,7 @@ class Registry(object):
if event in self.functions_list:
for function in self.functions_list[event]:
try:
+ log.debug('Running function {} for {}'.format(function, event))
result = function(*args, **kwargs)
if result:
results.append(result)
diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py
index 403b82d97..d31506420 100644
--- a/openlp/core/common/settings.py
+++ b/openlp/core/common/settings.py
@@ -33,7 +33,8 @@ from PyQt5 import QtCore, QtGui
from openlp.core.common import SlideLimits, ThemeLevel, is_linux, is_win
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder
-from openlp.core.common.path import Path, str_to_path, files_to_paths
+from openlp.core.common.path import Path, files_to_paths, str_to_path
+
log = logging.getLogger(__name__)
@@ -70,6 +71,34 @@ def media_players_conv(string):
return string
+def upgrade_screens(number, x_position, y_position, height, width, can_override, is_display_screen):
+ """
+ Upgrade them monitor setting from a few single entries to a composite JSON entry
+
+ :param int number: The old monitor number
+ :param int x_position: The X position
+ :param int y_position: The Y position
+ :param bool can_override: Are the screen positions overridden
+ :param bool is_display_screen: Is this a display screen
+ :returns dict: Dictionary with the new value
+ """
+ geometry_key = 'geometry'
+ if can_override:
+ geometry_key = 'custom_geometry'
+ return {
+ number: {
+ 'number': number,
+ geometry_key: {
+ 'x': x_position,
+ 'y': y_position,
+ 'height': height,
+ 'width': width
+ },
+ 'is_display': is_display_screen
+ }
+ }
+
+
class Settings(QtCore.QSettings):
"""
Class to wrap QSettings.
@@ -175,6 +204,7 @@ class Settings(QtCore.QSettings):
# circular dependency.
'core/display on monitor': True,
'core/override position': False,
+ 'core/monitor': {},
'core/application version': '0.0',
'images/background color': '#000000',
'media/players': 'system,webkit',
@@ -276,6 +306,8 @@ class Settings(QtCore.QSettings):
('songuasge/db hostname', 'songusage/db hostname', []),
('songuasge/db database', 'songusage/db database', []),
('presentations / Powerpoint Viewer', '', []),
+ (['core/monitor', 'core/x position', 'core/y position', 'core/height', 'core/width', 'core/override',
+ 'core/display on monitor'], 'core/screens', [(upgrade_screens, [1, 0, 0, None, None, False, False])]),
('bibles/proxy name', '', []), # Just remove these bible proxy settings. They weren't used in 2.4!
('bibles/proxy address', '', []),
('bibles/proxy username', '', []),
@@ -545,7 +577,7 @@ class Settings(QtCore.QSettings):
:param value: The value to save
:rtype: None
"""
- if isinstance(value, Path) or (isinstance(value, list) and value and isinstance(value[0], Path)):
+ if isinstance(value, (Path, dict)) or (isinstance(value, list) and value and isinstance(value[0], Path)):
value = json.dumps(value, cls=OpenLPJsonEncoder)
super().setValue(key, value)
@@ -568,8 +600,11 @@ class Settings(QtCore.QSettings):
# An empty list saved to the settings results in a None type being returned.
elif isinstance(default_value, list):
return []
+ # An empty dictionary saved to the settings results in a None type being returned.
+ elif isinstance(default_value, dict):
+ return {}
elif isinstance(setting, str):
- if '__Path__' in setting:
+ if '__Path__' in setting or setting.startswith('{'):
return json.loads(setting, cls=OpenLPJsonDecoder)
# Convert the setting to the correct type.
if isinstance(default_value, bool):
@@ -578,6 +613,8 @@ class Settings(QtCore.QSettings):
# Sometimes setting is string instead of a boolean.
return setting == 'true'
if isinstance(default_value, int):
+ if setting is None:
+ return 0
return int(setting)
return setting
diff --git a/openlp/core/display/html/black.css b/openlp/core/display/html/black.css
new file mode 100644
index 000000000..96e4fd480
--- /dev/null
+++ b/openlp/core/display/html/black.css
@@ -0,0 +1,292 @@
+/**
+ * Black theme for reveal.js. This is the opposite of the 'white' theme.
+ *
+ * By Hakim El Hattab, http://hakim.se
+ */
+@import url(../../lib/font/source-sans-pro/source-sans-pro.css);
+section.has-light-background, section.has-light-background h1, section.has-light-background h2, section.has-light-background h3, section.has-light-background h4, section.has-light-background h5, section.has-light-background h6 {
+ color: #222; }
+
+/*********************************************
+ * GLOBAL STYLES
+ *********************************************/
+body {
+ background: #222;
+ background-color: #222; }
+
+.reveal {
+ font-family: "Source Sans Pro", Helvetica, sans-serif;
+ font-size: 42px;
+ font-weight: normal;
+ color: #fff; }
+
+::selection {
+ color: #fff;
+ background: #bee4fd;
+ text-shadow: none; }
+
+::-moz-selection {
+ color: #fff;
+ background: #bee4fd;
+ text-shadow: none; }
+
+.reveal .slides > section,
+.reveal .slides > section > section {
+ line-height: 1.3;
+ font-weight: inherit; }
+
+/*********************************************
+ * HEADERS
+ *********************************************/
+.reveal h1,
+.reveal h2,
+.reveal h3,
+.reveal h4,
+.reveal h5,
+.reveal h6 {
+ margin: 0 0 20px 0;
+ color: #fff;
+ font-family: "Source Sans Pro", Helvetica, sans-serif;
+ font-weight: 600;
+ line-height: 1.2;
+ letter-spacing: normal;
+ text-transform: uppercase;
+ text-shadow: none;
+ word-wrap: break-word; }
+
+.reveal h1 {
+ font-size: 2.5em; }
+
+.reveal h2 {
+ font-size: 1.6em; }
+
+.reveal h3 {
+ font-size: 1.3em; }
+
+.reveal h4 {
+ font-size: 1em; }
+
+.reveal h1 {
+ text-shadow: none; }
+
+/*********************************************
+ * OTHER
+ *********************************************/
+.reveal p {
+ margin: 20px 0;
+ line-height: 1.3; }
+
+/* Ensure certain elements are never larger than the slide itself */
+.reveal img,
+.reveal video,
+.reveal iframe {
+ max-width: 95%;
+ max-height: 95%; }
+
+.reveal strong,
+.reveal b {
+ font-weight: bold; }
+
+.reveal em {
+ font-style: italic; }
+
+.reveal ol,
+.reveal dl,
+.reveal ul {
+ display: inline-block;
+ text-align: left;
+ margin: 0 0 0 1em; }
+
+.reveal ol {
+ list-style-type: decimal; }
+
+.reveal ul {
+ list-style-type: disc; }
+
+.reveal ul ul {
+ list-style-type: square; }
+
+.reveal ul ul ul {
+ list-style-type: circle; }
+
+.reveal ul ul,
+.reveal ul ol,
+.reveal ol ol,
+.reveal ol ul {
+ display: block;
+ margin-left: 40px; }
+
+.reveal dt {
+ font-weight: bold; }
+
+.reveal dd {
+ margin-left: 40px; }
+
+.reveal q,
+.reveal blockquote {
+ quotes: none; }
+
+.reveal blockquote {
+ display: block;
+ position: relative;
+ width: 70%;
+ margin: 20px auto;
+ padding: 5px;
+ font-style: italic;
+ background: rgba(255, 255, 255, 0.05);
+ box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); }
+
+.reveal blockquote p:first-child,
+.reveal blockquote p:last-child {
+ display: inline-block; }
+
+.reveal q {
+ font-style: italic; }
+
+.reveal pre {
+ display: block;
+ position: relative;
+ width: 90%;
+ margin: 20px auto;
+ text-align: left;
+ font-size: 0.55em;
+ font-family: monospace;
+ line-height: 1.2em;
+ word-wrap: break-word;
+ box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); }
+
+.reveal code {
+ font-family: monospace; }
+
+.reveal pre code {
+ display: block;
+ padding: 5px;
+ overflow: auto;
+ max-height: 400px;
+ word-wrap: normal; }
+
+.reveal table {
+ margin: auto;
+ border-collapse: collapse;
+ border-spacing: 0; }
+
+.reveal table th {
+ font-weight: bold; }
+
+.reveal table th,
+.reveal table td {
+ text-align: left;
+ padding: 0.2em 0.5em 0.2em 0.5em;
+ border-bottom: 1px solid; }
+
+.reveal table th[align="center"],
+.reveal table td[align="center"] {
+ text-align: center; }
+
+.reveal table th[align="right"],
+.reveal table td[align="right"] {
+ text-align: right; }
+
+.reveal table tbody tr:last-child th,
+.reveal table tbody tr:last-child td {
+ border-bottom: none; }
+
+.reveal sup {
+ vertical-align: super; }
+
+.reveal sub {
+ vertical-align: sub; }
+
+.reveal small {
+ display: inline-block;
+ font-size: 0.6em;
+ line-height: 1.2em;
+ vertical-align: top; }
+
+.reveal small * {
+ vertical-align: top; }
+
+/*********************************************
+ * LINKS
+ *********************************************/
+.reveal a {
+ color: #42affa;
+ text-decoration: none;
+ -webkit-transition: color .15s ease;
+ -moz-transition: color .15s ease;
+ transition: color .15s ease; }
+
+.reveal a:hover {
+ color: #8dcffc;
+ text-shadow: none;
+ border: none; }
+
+.reveal .roll span:after {
+ color: #fff;
+ background: #068de9; }
+
+/*********************************************
+ * IMAGES
+ *********************************************/
+.reveal section img {
+ margin: 15px 0px;
+ background: rgba(255, 255, 255, 0.12);
+ border: 4px solid #fff;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); }
+
+.reveal section img.plain {
+ border: 0;
+ box-shadow: none; }
+
+.reveal a img {
+ -webkit-transition: all .15s linear;
+ -moz-transition: all .15s linear;
+ transition: all .15s linear; }
+
+.reveal a:hover img {
+ background: rgba(255, 255, 255, 0.2);
+ border-color: #42affa;
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); }
+
+/*********************************************
+ * NAVIGATION CONTROLS
+ *********************************************/
+.reveal .controls .navigate-left,
+.reveal .controls .navigate-left.enabled {
+ border-right-color: #42affa; }
+
+.reveal .controls .navigate-right,
+.reveal .controls .navigate-right.enabled {
+ border-left-color: #42affa; }
+
+.reveal .controls .navigate-up,
+.reveal .controls .navigate-up.enabled {
+ border-bottom-color: #42affa; }
+
+.reveal .controls .navigate-down,
+.reveal .controls .navigate-down.enabled {
+ border-top-color: #42affa; }
+
+.reveal .controls .navigate-left.enabled:hover {
+ border-right-color: #8dcffc; }
+
+.reveal .controls .navigate-right.enabled:hover {
+ border-left-color: #8dcffc; }
+
+.reveal .controls .navigate-up.enabled:hover {
+ border-bottom-color: #8dcffc; }
+
+.reveal .controls .navigate-down.enabled:hover {
+ border-top-color: #8dcffc; }
+
+/*********************************************
+ * PROGRESS BAR
+ *********************************************/
+.reveal .progress {
+ background: rgba(0, 0, 0, 0.2); }
+
+.reveal .progress span {
+ background: #42affa;
+ -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985);
+ -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985);
+ transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); }
diff --git a/openlp/core/display/html/checkerboard.png b/openlp/core/display/html/checkerboard.png
new file mode 100644
index 000000000..52b51242f
Binary files /dev/null and b/openlp/core/display/html/checkerboard.png differ
diff --git a/openlp/core/display/html/display.html b/openlp/core/display/html/display.html
new file mode 100644
index 000000000..83e0bac90
--- /dev/null
+++ b/openlp/core/display/html/display.html
@@ -0,0 +1,39 @@
+
+
+
+ Display Window
+
+
+
+
+
+
+
+
+
+
diff --git a/openlp/core/display/html/display.js b/openlp/core/display/html/display.js
new file mode 100644
index 000000000..9c6eda634
--- /dev/null
+++ b/openlp/core/display/html/display.js
@@ -0,0 +1,789 @@
+/**
+ * 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: "left",
+ Right: "right",
+ Center: "center",
+ Justify: "justify"
+};
+
+/**
+ * Vertical alignment enumeration
+ */
+var VerticalAlign = {
+ Top: "top",
+ Middle: "middle",
+ Bottom: "bottom"
+};
+
+/**
+ * Audio state enumeration
+ */
+var AudioState = {
+ Playing: "playing",
+ Paused: "paused",
+ Stopped: "stopped"
+};
+
+/**
+ * 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} startDir - Starting direction
+ * @param {string} endDir - Ending direction
+ * @param {string} startColor - The starting color
+ * @param {string} endColor - The ending color
+ * @returns {string} A string of the gradient CSS
+ */
+function _buildLinearGradient(startDir, endDir, startColor, endColor) {
+ return "-webkit-gradient(linear, " + startDir + ", " + endDir + ", from(" + startColor + "), to(" + 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 "-webkit-gradient(radial, " + width + " 50%, 100, " + width + " 50%, " + width + ", from(" + startColor + "), to(" + endColor + ")) fixed";
+}
+
+/**
+ * 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
tags
+ * @private
+ * @param {string} text - The text to parse
+ * @returns {string} The text now with
tags
+ */
+function _nl2br(text) {
+ return text.replace("\r\n", "\n").replace("\n", "
");
+}
+
+/**
+ * Prepare text by creating paragraphs and calling _nl2br to convert newlines to
tags
+ * @private
+ * @param {string} text - The text to parse
+ * @returns {string} The text now with and
tags
+ */
+function _prepareText(text) {
+ return "
" + _nl2br(text) + "
";
+}
+
+/**
+ * The paths we get are JSON versions of Python Path objects, so let's just fix that.
+ * @private
+ * @param {object} path - The Path object
+ * @returns {string} The actual file path
+ */
+function _pathToString(path) {
+ var filename = path.__Path__.join("/").replace("//", "/");
+ if (!filename.startsWith("/")) {
+ filename = "/" + filename;
+ }
+ return filename;
+}
+
+/**
+ * An audio player with a play list
+ */
+var AudioPlayer = function (audioElement) {
+ this._audioElement = null;
+ this._eventListeners = {};
+ this._playlist = [];
+ this._currentTrack = null;
+ this._canRepeat = false;
+ this._state = AudioState.Stopped;
+ this.createAudioElement();
+};
+
+/**
+ * Call all listeners associated with this event
+ * @private
+ * @param {object} event - The event that was emitted
+ */
+AudioPlayer.prototype._callListener = function (event) {
+ if (this._eventListeners.hasOwnProperty(event.type)) {
+ this._eventListeners[event.type].forEach(function (listener) {
+ listener(event);
+ });
+ }
+ else {
+ console.warn("Received unknown event \"" + event.type + "\", doing nothing.");
+ }
+};
+
+/**
+ * Create the