Display Custom Scheme

This commit is contained in:
Mateus Meyer Jiacomelli 2023-05-18 05:08:43 +00:00 committed by Raoul Snyman
parent 9e34f9eca1
commit 69d20d8fa2
9 changed files with 436 additions and 188 deletions

View File

@ -23,166 +23,11 @@ import os
from flask import Blueprint, send_from_directory
from openlp.core.common.applocation import AppLocation
from openlp.core.common.mime import get_mime_type
main_views = Blueprint('main', __name__)
def get_mime_type(file):
if file.lower().endswith('.aac'):
mime_type = 'audio/aac'
elif file.lower().endswith('.abw'):
mime_type = 'application/x-abiword'
elif file.lower().endswith('.arc'):
mime_type = 'application/x-freearc'
elif file.lower().endswith('.avi'):
mime_type = 'video/x-msvideo'
elif file.lower().endswith('.azw'):
mime_type = 'application/vnd.amazon.ebook'
elif file.lower().endswith('.bin'):
mime_type = 'application/octet-stream'
elif file.lower().endswith('.bmp'):
mime_type = 'image/bmp'
elif file.lower().endswith('.bz'):
mime_type = 'application/x-bzip'
elif file.lower().endswith('.bz2'):
mime_type = 'application/x-bzip2'
elif file.lower().endswith('.cda'):
mime_type = 'application/x-cdf'
elif file.lower().endswith('.csh'):
mime_type = 'application/x-csh'
elif file.lower().endswith('.css'):
mime_type = 'text/css'
elif file.lower().endswith('.csv'):
mime_type = 'text/csv'
elif file.lower().endswith('.doc'):
mime_type = 'application/msword'
elif file.lower().endswith('.docx'):
mime_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
elif file.lower().endswith('.eot'):
mime_type = 'application/vnd.ms-fontobject'
elif file.lower().endswith('.epub'):
mime_type = 'application/epub+zip'
elif file.lower().endswith('.gz'):
mime_type = 'application/gzip'
elif file.lower().endswith('.gif'):
mime_type = 'image/gif'
elif file.lower().endswith('.htm'):
mime_type = 'text/html'
elif file.lower().endswith('.html'):
mime_type = 'text/html'
elif file.lower().endswith('.ico'):
mime_type = 'image/vnd.microsoft.icon'
elif file.lower().endswith('.ics'):
mime_type = 'text/calendar'
elif file.lower().endswith('.jar'):
mime_type = 'application/java-archive'
elif file.lower().endswith('.jpeg'):
mime_type = 'image/jpeg'
elif file.lower().endswith('.jpg'):
mime_type = 'image/jpeg'
elif file.lower().endswith('.js'):
mime_type = 'application/javascript'
elif file.lower().endswith('.json'):
mime_type = 'application/json'
elif file.lower().endswith('.jsonld'):
mime_type = 'application/ld+json'
elif file.lower().endswith('.mid'):
mime_type = 'audio/midi'
elif file.lower().endswith('.midi'):
mime_type = 'audio/x-midi'
elif file.lower().endswith('.mjs'):
mime_type = 'text/javascript'
elif file.lower().endswith('.mp3'):
mime_type = 'audio/mpeg'
elif file.lower().endswith('.mp4'):
mime_type = 'video/mp4'
elif file.lower().endswith('.mpeg'):
mime_type = 'video/mpeg'
elif file.lower().endswith('.mpkg'):
mime_type = 'application/vnd.apple.installer+xml'
elif file.lower().endswith('.odp'):
mime_type = 'application/vnd.oasis.opendocument.presentation'
elif file.lower().endswith('.ods'):
mime_type = 'application/vnd.oasis.opendocument.spreadsheet'
elif file.lower().endswith('.odt'):
mime_type = 'application/vnd.oasis.opendocument.text'
elif file.lower().endswith('.oga'):
mime_type = 'audio/ogg'
elif file.lower().endswith('.ogv'):
mime_type = 'video/ogg'
elif file.lower().endswith('.ogx'):
mime_type = 'application/ogg'
elif file.lower().endswith('.opus'):
mime_type = 'audio/opus'
elif file.lower().endswith('.otf'):
mime_type = 'application/x-font-opentype'
elif file.lower().endswith('.png'):
mime_type = 'image/png'
elif file.lower().endswith('.pdf'):
mime_type = 'application/pdf'
elif file.lower().endswith('.php'):
mime_type = 'application/x-httpd-php'
elif file.lower().endswith('.ppt'):
mime_type = 'application/vnd.ms-powerpoint'
elif file.lower().endswith('.pptx'):
mime_type = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
elif file.lower().endswith('.rar'):
mime_type = 'application/vnd.rar'
elif file.lower().endswith('.rtf'):
mime_type = 'application/rtf'
elif file.lower().endswith('.sfnt'):
mime_type = 'application/font-sfnt'
elif file.lower().endswith('.sh'):
mime_type = 'application/x-sh'
elif file.lower().endswith('.svg'):
mime_type = 'image/svg+xml'
elif file.lower().endswith('.swf'):
mime_type = 'application/x-shockwave-flash'
elif file.lower().endswith('.tar'):
mime_type = 'application/x-tar'
elif file.lower().endswith('.tif'):
mime_type = 'image/tiff'
elif file.lower().endswith('.tiff'):
mime_type = 'image/tiff'
elif file.lower().endswith('.ts'):
mime_type = 'video/mp2t'
elif file.lower().endswith('.ttf'):
mime_type = 'application/x-font-ttf'
elif file.lower().endswith('.txt'):
mime_type = 'text/plain'
elif file.lower().endswith('.vsd'):
mime_type = 'application/vnd.visio'
elif file.lower().endswith('.wav'):
mime_type = 'audio/wav'
elif file.lower().endswith('.weba'):
mime_type = 'audio/webm'
elif file.lower().endswith('.webm'):
mime_type = 'video/webm'
elif file.lower().endswith('.webp'):
mime_type = 'image/webp'
elif file.lower().endswith('.woff'):
mime_type = 'application/font-woff'
elif file.lower().endswith('.woff2'):
mime_type = 'application/font-woff2'
elif file.lower().endswith('.xhtml'):
mime_type = 'application/xhtml+xml'
elif file.lower().endswith('.xls'):
mime_type = 'application/vnd.ms-excel'
elif file.lower().endswith('.xlsx'):
mime_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
elif file.lower().endswith('.xml'):
mime_type = 'application/xml'
elif file.lower().endswith('.xul'):
mime_type = 'application/vnd.mozilla.xul+xml'
elif file.lower().endswith('.zip'):
mime_type = 'application/zip'
elif file.lower().endswith('.7z'):
mime_type = 'application/x-7z-compressed'
else:
mime_type = 'application/octet-stream'
return mime_type
@main_views.route('/', defaults={'path': ''})
@main_views.route('/<path>')
def index(path):

View File

@ -46,6 +46,7 @@ from openlp.core.common.platform import is_macosx, is_win
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.display.screens import ScreenList
from openlp.core.display.webengine import init_webview_custom_schemes
from openlp.core.loader import loader
from openlp.core.resources import qInitResources
from openlp.core.server import Server
@ -414,6 +415,7 @@ def main():
qInitResources()
# Now create and actually run the application.
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
init_webview_custom_schemes()
application = QtWidgets.QApplication(qt_args)
application.setOrganizationName('OpenLP')
application.setOrganizationDomain('openlp.org')

175
openlp/core/common/mime.py Normal file
View File

@ -0,0 +1,175 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2023 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/>. #
##########################################################################
def get_mime_type(file):
if file.lower().endswith('.aac'):
mime_type = 'audio/aac'
elif file.lower().endswith('.abw'):
mime_type = 'application/x-abiword'
elif file.lower().endswith('.arc'):
mime_type = 'application/x-freearc'
elif file.lower().endswith('.avi'):
mime_type = 'video/x-msvideo'
elif file.lower().endswith('.azw'):
mime_type = 'application/vnd.amazon.ebook'
elif file.lower().endswith('.bin'):
mime_type = 'application/octet-stream'
elif file.lower().endswith('.bmp'):
mime_type = 'image/bmp'
elif file.lower().endswith('.bz'):
mime_type = 'application/x-bzip'
elif file.lower().endswith('.bz2'):
mime_type = 'application/x-bzip2'
elif file.lower().endswith('.cda'):
mime_type = 'application/x-cdf'
elif file.lower().endswith('.csh'):
mime_type = 'application/x-csh'
elif file.lower().endswith('.css'):
mime_type = 'text/css'
elif file.lower().endswith('.csv'):
mime_type = 'text/csv'
elif file.lower().endswith('.doc'):
mime_type = 'application/msword'
elif file.lower().endswith('.docx'):
mime_type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
elif file.lower().endswith('.eot'):
mime_type = 'application/vnd.ms-fontobject'
elif file.lower().endswith('.epub'):
mime_type = 'application/epub+zip'
elif file.lower().endswith('.gz'):
mime_type = 'application/gzip'
elif file.lower().endswith('.gif'):
mime_type = 'image/gif'
elif file.lower().endswith('.htm'):
mime_type = 'text/html'
elif file.lower().endswith('.html'):
mime_type = 'text/html'
elif file.lower().endswith('.ico'):
mime_type = 'image/vnd.microsoft.icon'
elif file.lower().endswith('.ics'):
mime_type = 'text/calendar'
elif file.lower().endswith('.jar'):
mime_type = 'application/java-archive'
elif file.lower().endswith('.jpeg'):
mime_type = 'image/jpeg'
elif file.lower().endswith('.jpg'):
mime_type = 'image/jpeg'
elif file.lower().endswith('.js'):
mime_type = 'application/javascript'
elif file.lower().endswith('.json'):
mime_type = 'application/json'
elif file.lower().endswith('.jsonld'):
mime_type = 'application/ld+json'
elif file.lower().endswith('.mid'):
mime_type = 'audio/midi'
elif file.lower().endswith('.midi'):
mime_type = 'audio/x-midi'
elif file.lower().endswith('.mjs'):
mime_type = 'text/javascript'
elif file.lower().endswith('.mp3'):
mime_type = 'audio/mpeg'
elif file.lower().endswith('.mp4'):
mime_type = 'video/mp4'
elif file.lower().endswith('.mpeg'):
mime_type = 'video/mpeg'
elif file.lower().endswith('.mpkg'):
mime_type = 'application/vnd.apple.installer+xml'
elif file.lower().endswith('.odp'):
mime_type = 'application/vnd.oasis.opendocument.presentation'
elif file.lower().endswith('.ods'):
mime_type = 'application/vnd.oasis.opendocument.spreadsheet'
elif file.lower().endswith('.odt'):
mime_type = 'application/vnd.oasis.opendocument.text'
elif file.lower().endswith('.oga'):
mime_type = 'audio/ogg'
elif file.lower().endswith('.ogv'):
mime_type = 'video/ogg'
elif file.lower().endswith('.ogx'):
mime_type = 'application/ogg'
elif file.lower().endswith('.opus'):
mime_type = 'audio/opus'
elif file.lower().endswith('.otf'):
mime_type = 'application/x-font-opentype'
elif file.lower().endswith('.png'):
mime_type = 'image/png'
elif file.lower().endswith('.pdf'):
mime_type = 'application/pdf'
elif file.lower().endswith('.php'):
mime_type = 'application/x-httpd-php'
elif file.lower().endswith('.ppt'):
mime_type = 'application/vnd.ms-powerpoint'
elif file.lower().endswith('.pptx'):
mime_type = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
elif file.lower().endswith('.rar'):
mime_type = 'application/vnd.rar'
elif file.lower().endswith('.rtf'):
mime_type = 'application/rtf'
elif file.lower().endswith('.sfnt'):
mime_type = 'application/font-sfnt'
elif file.lower().endswith('.sh'):
mime_type = 'application/x-sh'
elif file.lower().endswith('.svg'):
mime_type = 'image/svg+xml'
elif file.lower().endswith('.swf'):
mime_type = 'application/x-shockwave-flash'
elif file.lower().endswith('.tar'):
mime_type = 'application/x-tar'
elif file.lower().endswith('.tif'):
mime_type = 'image/tiff'
elif file.lower().endswith('.tiff'):
mime_type = 'image/tiff'
elif file.lower().endswith('.ts'):
mime_type = 'video/mp2t'
elif file.lower().endswith('.ttf'):
mime_type = 'application/x-font-ttf'
elif file.lower().endswith('.txt'):
mime_type = 'text/plain'
elif file.lower().endswith('.vsd'):
mime_type = 'application/vnd.visio'
elif file.lower().endswith('.wav'):
mime_type = 'audio/wav'
elif file.lower().endswith('.weba'):
mime_type = 'audio/webm'
elif file.lower().endswith('.webm'):
mime_type = 'video/webm'
elif file.lower().endswith('.webp'):
mime_type = 'image/webp'
elif file.lower().endswith('.woff'):
mime_type = 'application/font-woff'
elif file.lower().endswith('.woff2'):
mime_type = 'application/font-woff2'
elif file.lower().endswith('.xhtml'):
mime_type = 'application/xhtml+xml'
elif file.lower().endswith('.xls'):
mime_type = 'application/vnd.ms-excel'
elif file.lower().endswith('.xlsx'):
mime_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
elif file.lower().endswith('.xml'):
mime_type = 'application/xml'
elif file.lower().endswith('.xul'):
mime_type = 'application/vnd.mozilla.xul+xml'
elif file.lower().endswith('.zip'):
mime_type = 'application/zip'
elif file.lower().endswith('.7z'):
mime_type = 'application/x-7z-compressed'
else:
mime_type = 'application/octet-stream'
return mime_type

View File

@ -456,7 +456,7 @@ var Display = {
section.setAttribute("data-background", bg_color);
section.setAttribute("style", "height: 100%; width: 100%;");
var img = document.createElement('img');
img.src = image;
img.src = Display._getFileUrl(image);
img.setAttribute("style", "position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; max-height: 100%; max-width: 100%");
section.appendChild(img);
Display._slides['0'] = 0;
@ -734,7 +734,7 @@ var Display = {
section.setAttribute("id", index);
section.setAttribute("style", "height: 100%; width: 100%;");
var img = document.createElement('img');
img.src = slide.path;
img.src = Display._getFileUrl(slide.path);
img.setAttribute("style", "width: 100%; height: 100%; margin: 0; object-fit: contain;");
img.setAttribute('data-slide', index);
section.appendChild(img);
@ -1211,12 +1211,12 @@ var Display = {
}
break;
case BackgroundType.Image:
backgroundContent = "url('" + Display._theme.background_filename + "')";
backgroundContent = "url('" + Display._getFileUrl(Display._theme.background_filename) + "')";
break;
case BackgroundType.Video:
// never actually used since background type is overridden from video to transparent in window.py
backgroundContent = Display._theme.background_border_color;
backgroundHtml = "<video loop autoplay muted><source src='" + Display._theme.background_filename + "'></video>";
backgroundHtml = "<video loop autoplay muted><source src='" + Display._getFileUrl(Display._theme.background_filename) + "'></video>";
break;
default:
backgroundContent = "#000";
@ -1393,7 +1393,16 @@ var Display = {
value[1] = '/';
value[2] = Object.keys(Display._slides).length;
return value;
}
},
/**
* Translates file:// protocol URLs to openlp-library://local-file/ scheme
*/
_getFileUrl: function(url) {
if (url && (url.indexOf('file://') === 0)) {
return url.replace('file://', 'openlp-library://local-file/');
}
return url;
},
};
new QWebChannel(qt.webChannelTransport, function (channel) {
window.displayWatcher = channel.objects.displayWatcher;

View File

@ -23,10 +23,15 @@ Subclass of QWebEngineView. Adds some special eventhandling needed for screensho
Heavily inspired by https://stackoverflow.com/questions/33467776/qt-qwebengine-render-after-scrolling/33576100#33576100
"""
import logging
import os.path
from PyQt5 import QtCore, QtWebEngineWidgets, QtWidgets
from PyQt5 import QtCore, QtWebEngineWidgets, QtWidgets, QtWebEngineCore
from typing import Tuple
from openlp.core.common import Singleton
from openlp.core.common.applocation import AppLocation
from openlp.core.common.mime import get_mime_type
from openlp.core.common.platform import is_win
LOG_LEVELS = {
@ -107,3 +112,180 @@ class WebEngineView(QtWebEngineWidgets.QWebEngineView):
self._child = w
w.installEventFilter(self)
return super(WebEngineView, self).event(ev)
class LocalSchemeHelper():
def deny_access(self, request: QtWebEngineCore.QWebEngineUrlRequestJob, url: str, real_path: str):
log.exception('{request} denied. Real Path: {path}'.format(request=url,
path=real_path))
request.fail(QtWebEngineCore.QWebEngineUrlRequestJob.Error.RequestDenied)
def not_found(self, request: QtWebEngineCore.QWebEngineUrlRequestJob, url: str, real_path: str):
log.exception('{request} not found. Real Path: {path}'.format(request=url,
path=real_path))
request.fail(QtWebEngineCore.QWebEngineUrlRequestJob.Error.UrlNotFound)
def exception(self, request: QtWebEngineCore.QWebEngineUrlRequestJob, url: str, exception: Exception):
log.exception('{request} failed: {error:d}; {message}'.format(request=url, error=str(type(exception)),
message=str(exception)))
request.fail(QtWebEngineCore.QWebEngineUrlRequestJob.Error.RequestFailed)
def invalid(self, request: QtWebEngineCore.QWebEngineUrlRequestJob, url: str):
log.exception('{request} is invalid.'.format(request=url))
request.fail(QtWebEngineCore.QWebEngineUrlRequestJob.Error.UrlInvalid)
def reply_file(self, request: QtWebEngineCore.QWebEngineUrlRequestJob, path: str) -> Tuple[QtCore.QBuffer, str]:
raw_html = open(path, 'rb').read()
buffer = QtCore.QBuffer(self)
buffer.open(QtCore.QIODevice.OpenModeFlag.WriteOnly)
buffer.write(raw_html)
buffer.seek(0)
buffer.close()
mime_type = get_mime_type(path)
request.reply(bytes(mime_type, 'utf-8'), buffer)
def request_path_to_file(self, base_path: str, url_path: str):
if is_win() and not base_path:
while len(url_path) > 0 and url_path[0] == '/':
url_path = url_path[1:]
return os.path.realpath(base_path + os.path.normpath(url_path))
class OpenLPSchemeHandler(QtWebEngineCore.QWebEngineUrlSchemeHandler, LocalSchemeHelper):
def __init__(self, root_paths, parent):
super().__init__(parent)
self.root_paths = {base_path: os.path.realpath(value) for base_path, value in root_paths.items()}
def requestStarted(self, request: QtWebEngineCore.QWebEngineUrlRequestJob) -> None:
url = request.requestUrl()
try:
request_base_key = url.host()
# Checking root to forbid relative path attacks
base_real_path = self.root_paths[request_base_key]
request_real_path = self.request_path_to_file(base_real_path, url.path())
if not request_real_path.startswith(base_real_path):
return self.deny_access(request, url, request_real_path)
if not os.path.exists(request_real_path):
return self.not_found(request, url, request_real_path)
self.reply_file(request, request_real_path)
return
except Exception as exception:
self.exception(request, url, exception)
class OpenLPLibrarySchemeHandler(QtWebEngineCore.QWebEngineUrlSchemeHandler, LocalSchemeHelper):
def __init__(self, parent):
super().__init__(parent)
def requestStarted(self, request: QtWebEngineCore.QWebEngineUrlRequestJob) -> None:
url = request.requestUrl()
try:
request_base_key = url.host()
if request_base_key == 'local-file':
# Checking root to forbid relative path attacks
request_real_path = self.request_path_to_file('', url.path())
if not os.path.exists(request_real_path):
return self.not_found(request, url, request_real_path)
return self.reply_file(request, request_real_path)
return self.invalid(request, url)
except Exception as exception:
self.exception(request, url, exception)
class WebViewCustomScheme(QtCore.QObject):
"""
Allows the registering of custom protocols inside WebView. Can be used mainly for
circumventing default security measures placed on http(s):// or file:// protocols.
"""
def __init__(self, parent):
super().__init__(parent)
scheme = QtWebEngineCore.QWebEngineUrlScheme(self.scheme_name)
scheme.setSyntax(QtWebEngineCore.QWebEngineUrlScheme.Syntax.Host)
self.set_scheme_flags(scheme)
QtWebEngineCore.QWebEngineUrlScheme.registerScheme(scheme)
def set_scheme_flags(self, scheme: QtWebEngineCore.QWebEngineUrlScheme):
scheme.setFlags(QtWebEngineCore.QWebEngineUrlScheme.Flag.CorsEnabled
| QtWebEngineCore.QWebEngineUrlScheme.Flag.ContentSecurityPolicyIgnored
| QtWebEngineCore.QWebEngineUrlScheme.Flag.SecureScheme
| QtWebEngineCore.QWebEngineUrlScheme.Flag.LocalScheme
| QtWebEngineCore.QWebEngineUrlScheme.Flag.LocalAccessAllowed)
def create_scheme_handler(self):
raise Exception('Needs to be implemented.')
def init_handler(self, profile=None):
if profile is None:
profile = QtWebEngineWidgets.QWebEngineProfile.defaultProfile()
handler = profile.urlSchemeHandler(self.scheme_name)
if handler is not None:
profile.removeUrlSchemeHandler(handler)
self.handler = self.create_scheme_handler()
profile.installUrlSchemeHandler(self.scheme_name, self.handler)
class OpenLPScheme(WebViewCustomScheme):
scheme_name = b'openlp'
def __init__(self, root_paths, parent=None):
super().__init__(parent)
self.root_paths = root_paths
def create_scheme_handler(self):
return OpenLPSchemeHandler(self.root_paths, self)
def set_root_path(self, base: str, path: str):
if hasattr(self, 'handler'):
self.handler.root_paths[base] = path
self.root_paths[base] = path
# Allows to circumvent default file-on-HTTP restrictions and/or provide remote/non-local file locations.
class OpenLPLibraryScheme(WebViewCustomScheme):
scheme_name = b'openlp-library'
def __init__(self, parent=None):
super().__init__(parent)
def set_scheme_flags(self, scheme: QtWebEngineCore.QWebEngineUrlScheme):
scheme.setFlags(QtWebEngineCore.QWebEngineUrlScheme.Flag.ContentSecurityPolicyIgnored)
def create_scheme_handler(self):
return OpenLPLibrarySchemeHandler(self)
def init_webview_custom_schemes():
"""
This inits the custom scheme protocols used in OpenLP WebEngines. It must happen before
QApplication instantiation.
"""
openlp_root_paths = {
"display": AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'display' / 'html'
}
# openlp:// protocol
WebViewSchemes().register_scheme(OpenLPScheme(openlp_root_paths))
# openlp-library:// protocol
WebViewSchemes().register_scheme(OpenLPLibraryScheme())
def set_webview_display_path(path):
scheme = WebViewSchemes().get_scheme(OpenLPScheme.scheme_name)
if (scheme):
scheme.set_root_path('display', path)
class WebViewSchemes(metaclass=Singleton):
def __init__(self) -> None:
self._registered_schemes = {}
def get_scheme(self, name) -> WebViewCustomScheme:
if isinstance(name, bytes):
name = name.decode('utf-8')
return self._registered_schemes[name] if name in self._registered_schemes else None
def register_scheme(self, scheme: WebViewCustomScheme):
name = scheme.scheme_name
if isinstance(name, bytes):
name = name.decode('utf-8')
self._registered_schemes[name] = scheme

View File

@ -29,7 +29,6 @@ import re
from PyQt5 import QtCore, QtWebChannel, QtWidgets
from openlp.core.common.applocation import AppLocation
from openlp.core.common.enum import ServiceItemType
from openlp.core.common.i18n import translate
from openlp.core.common.mixins import LogMixin, RegistryProperties
@ -146,8 +145,6 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_X11NetWmWindowTypeDialog)
if is_macosx():
self.setAttribute(QtCore.Qt.WA_MacAlwaysShowToolWindow, True)
# Need to import this inline to get around a QtWebEngine issue
from openlp.core.display.webengine import WebEngineView
self._is_initialised = False
self._is_manual_close = False
self._can_show_startup_screen = can_show_startup_screen
@ -159,22 +156,22 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.webview = WebEngineView(self)
self.webview = self.init_webengine()
self.webview.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.webview.page().setBackgroundColor(QtCore.Qt.transparent)
self.webview.display_clicked = self.disable_display
self.layout.addWidget(self.webview)
self.webview.loadFinished.connect(self.after_loaded)
display_base_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'display' / 'html'
self.display_path = display_base_path / 'display.html'
self.checkerboard_path = display_base_path / 'checkerboard.png'
self.openlp_splash_screen_path = display_base_path / 'openlp-splash-screen.png'
self.display_path = 'openlp://display/display.html'
self.checkerboard_path = 'openlp://display/checkerboard.png'
self.openlp_splash_screen_path = 'openlp://display/openlp-splash-screen.png'
self.channel = QtWebChannel.QWebChannel(self)
self.display_watcher = DisplayWatcher(self)
self.channel.registerObject('displayWatcher', self.display_watcher)
self.webview.page().setWebChannel(self.channel)
self.display_watcher.initialised.connect(self.on_initialised)
self.set_url(QtCore.QUrl.fromLocalFile(path_to_str(self.display_path)))
qUrl = QtCore.QUrl(self.display_path)
self.set_url(qUrl)
self.is_display = False
self.scale = 1
self.hide_mode = None
@ -198,6 +195,16 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
if not self._is_manual_close:
event.ignore()
def init_webengine(self):
# Need to import this inline to get around a QtWebEngine issue
from openlp.core.display.webengine import WebEngineView, WebViewSchemes, OpenLPScheme, OpenLPLibraryScheme
webview = WebEngineView(self)
profile = webview.page().profile()
WebViewSchemes().get_scheme(OpenLPScheme.scheme_name).init_handler(profile)
WebViewSchemes().get_scheme(OpenLPLibraryScheme.scheme_name).init_handler(profile)
return webview
def _fix_font_name(self, font_name):
"""
Do some font machinations to see if we can fix the font name
@ -260,11 +267,12 @@ class DisplayWindow(QtWidgets.QWidget, RegistryProperties, LogMixin):
bg_color = self.settings.value('core/logo background color')
image = self.settings.value('core/logo file')
if path_to_str(image).startswith(':'):
image = self.openlp_splash_screen_path
try:
image_uri = image.as_uri()
except Exception:
image_uri = ''
image_uri = self.openlp_splash_screen_path
else:
try:
image_uri = image.as_uri().replace('file://', 'openlp-library://local-file/')
except Exception:
image_uri = ''
# if set to hide logo on startup, do not send the logo
if self.settings.value('core/logo hide on startup'):
image_uri = ''

View File

@ -987,7 +987,7 @@ describe("Display.setImageSlides", function () {
});
it("should add a list of images", function () {
var slides = [{"path": "file:///openlp1.jpg"}, {"path": "file:///openlp2.jpg"}];
var slides = [{"path": "openlp-library://local-file//openlp1.jpg"}, {"path": "openlp-library://local-file//openlp2.jpg"}];
spyOn(Reveal, "sync");
spyOn(Reveal, "slide");
@ -997,9 +997,9 @@ describe("Display.setImageSlides", function () {
expect(Display._slides["1"]).toEqual(1);
expect($(".slides > section > section").length).toEqual(2);
expect($(".slides > section > section > img").length).toEqual(2);
expect($(".slides > section > section > img")[0].getAttribute("src")).toEqual("file:///openlp1.jpg");
expect($(".slides > section > section > img")[0].getAttribute("src")).toEqual("openlp-library://local-file//openlp1.jpg");
expect($(".slides > section > section > img")[0].getAttribute("style")).toEqual("width: 100%; height: 100%; margin: 0; object-fit: contain;");
expect($(".slides > section > section > img")[1].getAttribute("src")).toEqual("file:///openlp2.jpg");
expect($(".slides > section > section > img")[1].getAttribute("src")).toEqual("openlp-library://local-file//openlp2.jpg");
expect($(".slides > section > section > img")[1].getAttribute("style")).toEqual("width: 100%; height: 100%; margin: 0; object-fit: contain;");
expect(Reveal.sync).toHaveBeenCalledTimes(1);
});
@ -1210,6 +1210,14 @@ describe("Display.toggleVideoMute", function () {
});
});
describe("localFile", function() {
it('should translate file:// protocol to openlp-library://local-file/ scheme', function() {
const fileUrl = 'file:///home/path/to/image.png';
const resultFile = Display._getFileUrl(fileUrl);
expect(resultFile).toEqual('openlp-library://local-file//home/path/to/image.png');
})
})
describe("Reveal slidechanged event", function () {
it("should swap footer content", function (done) {
var slides = [

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2023 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/>. #
##########################################################################
"""
Package to test the openlp.core.display.webengine package.
"""

View File

@ -226,7 +226,7 @@ def test_set_startup_screen(display_window_env, mock_settings):
# THEN: javascript should be run
display_window.run_javascript.assert_called_once_with(
'Display.setStartupSplashScreen("red", "file://{path}");'.format(path=expect_image_path))
'Display.setStartupSplashScreen("red", "openlp-library://local-file/{path}");'.format(path=expect_image_path))
def test_set_startup_screen_default_image(display_window_env, mock_settings):
@ -237,13 +237,9 @@ def test_set_startup_screen_default_image(display_window_env, mock_settings):
display_window = DisplayWindow()
display_window._is_initialised = True
display_window.run_javascript = MagicMock()
if is_win():
splash_screen_path = 'c:/default/splash_screen.png'
expect_splash_screen_path = '/' + splash_screen_path
else:
splash_screen_path = '/default/splash_screen.png'
expect_splash_screen_path = splash_screen_path
display_window.openlp_splash_screen_path = Path(splash_screen_path)
splash_screen_path = 'openlp://display/openlp-splash-screen.png'
expect_splash_screen_path = splash_screen_path
display_window.openlp_splash_screen_path = splash_screen_path
settings = {
'core/logo background color': 'blue',
'core/logo file': Path(':/graphics/openlp-splash-screen.png'),
@ -256,7 +252,7 @@ def test_set_startup_screen_default_image(display_window_env, mock_settings):
# THEN: javascript should be run
display_window.run_javascript.assert_called_with(
'Display.setStartupSplashScreen("blue", "file://{path}");'.format(path=expect_splash_screen_path))
'Display.setStartupSplashScreen("blue", "{path}");'.format(path=expect_splash_screen_path))
def test_set_startup_screen_missing(display_window_env, mock_settings):