head r2249

This commit is contained in:
Andreas Preikschat 2013-06-13 12:52:47 +02:00
commit 3c4e67545b
7 changed files with 257 additions and 65 deletions

View File

@ -779,8 +779,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
""" """
We need to make sure, that the SlidePreview's size is correct. We need to make sure, that the SlidePreview's size is correct.
""" """
self.preview_controller.previewSizeChanged() self.preview_controller.preview_size_changed()
self.live_controller.previewSizeChanged() self.live_controller.preview_size_changed()
def on_settings_shortcuts_item_clicked(self): def on_settings_shortcuts_item_clicked(self):
""" """
@ -989,8 +989,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.application.set_busy_cursor() self.application.set_busy_cursor()
self.image_manager.update_display() self.image_manager.update_display()
self.renderer.update_display() self.renderer.update_display()
self.preview_controller.screenSizeChanged() self.preview_controller.screen_size_changed()
self.live_controller.screenSizeChanged() self.live_controller.screen_size_changed()
self.setFocus() self.setFocus()
self.activateWindow() self.activateWindow()
self.application.set_normal_cursor() self.application.set_normal_cursor()

View File

@ -89,7 +89,7 @@ class SlideController(DisplayController):
Set up the Slide Controller. Set up the Slide Controller.
""" """
DisplayController.__init__(self, parent, is_live) DisplayController.__init__(self, parent, is_live)
Registry().register_function(u'bootstrap_post_set_up', self.screenSizeChanged) Registry().register_function(u'bootstrap_post_set_up', self.screen_size_changed)
self.screens = ScreenList() self.screens = ScreenList()
try: try:
self.ratio = float(self.screens.current[u'size'].width()) / float(self.screens.current[u'size'].height()) self.ratio = float(self.screens.current[u'size'].width()) / float(self.screens.current[u'size'].height())
@ -121,6 +121,8 @@ class SlideController(DisplayController):
self.update_slide_limits() self.update_slide_limits()
self.panel = QtGui.QWidget(parent.controlSplitter) self.panel = QtGui.QWidget(parent.controlSplitter)
self.slideList = {} self.slideList = {}
self.slide_count = 0
self.slide_image = None
# Layout for holding panel # Layout for holding panel
self.panel_layout = QtGui.QVBoxLayout(self.panel) self.panel_layout = QtGui.QVBoxLayout(self.panel)
self.panel_layout.setSpacing(0) self.panel_layout.setSpacing(0)
@ -321,18 +323,18 @@ class SlideController(DisplayController):
self.slide_layout.insertWidget(0, self.preview_display) self.slide_layout.insertWidget(0, self.preview_display)
self.preview_display.hide() self.preview_display.hide()
# Actual preview screen # Actual preview screen
self.slidePreview = QtGui.QLabel(self) self.slide_preview = QtGui.QLabel(self)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0) size_policy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) size_policy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.slidePreview.sizePolicy().hasHeightForWidth()) size_policy.setHeightForWidth(self.slide_preview.sizePolicy().hasHeightForWidth())
self.slidePreview.setSizePolicy(sizePolicy) self.slide_preview.setSizePolicy(size_policy)
self.slidePreview.setFrameShape(QtGui.QFrame.Box) self.slide_preview.setFrameShape(QtGui.QFrame.Box)
self.slidePreview.setFrameShadow(QtGui.QFrame.Plain) self.slide_preview.setFrameShadow(QtGui.QFrame.Plain)
self.slidePreview.setLineWidth(1) self.slide_preview.setLineWidth(1)
self.slidePreview.setScaledContents(True) self.slide_preview.setScaledContents(True)
self.slidePreview.setObjectName(u'slidePreview') self.slide_preview.setObjectName(u'slide_preview')
self.slide_layout.insertWidget(0, self.slidePreview) self.slide_layout.insertWidget(0, self.slide_preview)
self.grid.addLayout(self.slide_layout, 0, 0, 1, 1) self.grid.addLayout(self.slide_layout, 0, 0, 1, 1)
if self.is_live: if self.is_live:
self.current_shortcut = u'' self.current_shortcut = u''
@ -517,10 +519,9 @@ class SlideController(DisplayController):
self.service_manager.next_item() self.service_manager.next_item()
self.keypress_loop = False self.keypress_loop = False
def screenSizeChanged(self): def screen_size_changed(self):
""" """
Settings dialog has changed the screen size of adjust output and Settings dialog has changed the screen size of adjust output and screen previews.
screen previews.
""" """
# rebuild display as screen size changed # rebuild display as screen size changed
if self.display: if self.display:
@ -536,14 +537,14 @@ class SlideController(DisplayController):
except ZeroDivisionError: except ZeroDivisionError:
self.ratio = 1 self.ratio = 1
self.media_controller.setup_display(self.display, False) self.media_controller.setup_display(self.display, False)
self.previewSizeChanged() self.preview_size_changed()
self.preview_display.setup() self.preview_display.setup()
service_item = ServiceItem() service_item = ServiceItem()
self.preview_display.web_view.setHtml(build_html(service_item, self.preview_display.screen, None, self.is_live, self.preview_display.web_view.setHtml(build_html(service_item, self.preview_display.screen, None, self.is_live,
plugins=self.plugin_manager.plugins)) plugins=self.plugin_manager.plugins))
self.media_controller.setup_display(self.preview_display, True) self.media_controller.setup_display(self.preview_display, True)
if self.service_item: if self.service_item:
self.refreshServiceItem() self.refresh_service_item()
def __addActionsToWidget(self, widget): def __addActionsToWidget(self, widget):
""" """
@ -554,7 +555,7 @@ class SlideController(DisplayController):
self.previousService, self.nextService, self.previousService, self.nextService,
self.escapeItem]) self.escapeItem])
def previewSizeChanged(self): def preview_size_changed(self):
""" """
Takes care of the SlidePreview's size. Is called when one of the the Takes care of the SlidePreview's size. Is called when one of the the
splitters is moved or when the screen size is changed. Note, that this splitters is moved or when the screen size is changed. Note, that this
@ -563,14 +564,14 @@ class SlideController(DisplayController):
if self.ratio < float(self.preview_frame.width()) / float(self.preview_frame.height()): if self.ratio < float(self.preview_frame.width()) / float(self.preview_frame.height()):
# We have to take the height as limit. # We have to take the height as limit.
max_height = self.preview_frame.height() - self.grid.margin() * 2 max_height = self.preview_frame.height() - self.grid.margin() * 2
self.slidePreview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height)) self.slide_preview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height))
self.preview_display.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height)) self.preview_display.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height))
self.preview_display.screen = { self.preview_display.screen = {
u'size': self.preview_display.geometry()} u'size': self.preview_display.geometry()}
else: else:
# We have to take the width as limit. # We have to take the width as limit.
max_width = self.preview_frame.width() - self.grid.margin() * 2 max_width = self.preview_frame.width() - self.grid.margin() * 2
self.slidePreview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio)) self.slide_preview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
self.preview_display.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio)) self.preview_display.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
self.preview_display.screen = { self.preview_display.screen = {
u'size': self.preview_display.geometry()} u'size': self.preview_display.geometry()}
@ -624,17 +625,16 @@ class SlideController(DisplayController):
""" """
self.slide_limits = Settings().value(self.main_window.advanced_settings_section + u'/slide limits') self.slide_limits = Settings().value(self.main_window.advanced_settings_section + u'/slide limits')
def enableToolBar(self, item): def enable_tool_bar(self, item):
""" """
Allows the toolbars to be reconfigured based on Controller Type Allows the toolbars to be reconfigured based on Controller Type and ServiceItem Type
and ServiceItem Type
""" """
if self.is_live: if self.is_live:
self.enableLiveToolBar(item) self.enable_live_tool_bar(item)
else: else:
self.enablePreviewToolBar(item) self.enable_preview_tool_bar(item)
def enableLiveToolBar(self, item): def enable_live_tool_bar(self, item):
""" """
Allows the live toolbar to be customised Allows the live toolbar to be customised
""" """
@ -663,7 +663,7 @@ class SlideController(DisplayController):
# See bug #791050 # See bug #791050
self.toolbar.show() self.toolbar.show()
def enablePreviewToolBar(self, item): def enable_preview_tool_bar(self, item):
""" """
Allows the Preview toolbar to be customised Allows the Preview toolbar to be customised
""" """
@ -682,15 +682,15 @@ class SlideController(DisplayController):
# See bug #791050 # See bug #791050
self.toolbar.show() self.toolbar.show()
def refreshServiceItem(self): def refresh_service_item(self):
""" """
Method to update the service item if the screen has changed Method to update the service item if the screen has changed
""" """
log.debug(u'refreshServiceItem live = %s' % self.is_live) log.debug(u'refresh_service_item live = %s' % self.is_live)
if self.service_item.is_text() or self.service_item.is_image(): if self.service_item.is_text() or self.service_item.is_image():
item = self.service_item item = self.service_item
item.render() item.render()
self._processItem(item, self.selected_row) self._process_item(item, self.selected_row)
def add_service_item(self, item): def add_service_item(self, item):
""" """
@ -703,14 +703,14 @@ class SlideController(DisplayController):
if self.song_edit: if self.song_edit:
slideno = self.selected_row slideno = self.selected_row
self.song_edit = False self.song_edit = False
self._processItem(item, slideno) self._process_item(item, slideno)
def replaceServiceManagerItem(self, item): def replaceServiceManagerItem(self, item):
""" """
Replacement item following a remote edit Replacement item following a remote edit
""" """
if item == self.service_item: if item == self.service_item:
self._processItem(item, self.preview_list_widget.currentRow()) self._process_item(item, self.preview_list_widget.currentRow())
def addServiceManagerItem(self, item, slideno): def addServiceManagerItem(self, item, slideno):
""" """
@ -729,7 +729,7 @@ class SlideController(DisplayController):
self.__checkUpdateSelectedSlide(slidenum) self.__checkUpdateSelectedSlide(slidenum)
self.slideSelected() self.slideSelected()
else: else:
self._processItem(item, slidenum) self._process_item(item, slidenum)
if self.is_live and item.auto_play_slides_loop and item.timed_slide_interval > 0: if self.is_live and item.auto_play_slides_loop and item.timed_slide_interval > 0:
self.play_slides_loop.setChecked(item.auto_play_slides_loop) self.play_slides_loop.setChecked(item.auto_play_slides_loop)
self.delay_spin_box.setValue(int(item.timed_slide_interval)) self.delay_spin_box.setValue(int(item.timed_slide_interval))
@ -739,7 +739,7 @@ class SlideController(DisplayController):
self.delay_spin_box.setValue(int(item.timed_slide_interval)) self.delay_spin_box.setValue(int(item.timed_slide_interval))
self.onPlaySlidesOnce() self.onPlaySlidesOnce()
def _processItem(self, service_item, slideno): def _process_item(self, service_item, slideno):
""" """
Loads a ServiceItem into the system from ServiceManager Loads a ServiceItem into the system from ServiceManager
Display the slide number passed Display the slide number passed
@ -827,10 +827,9 @@ class SlideController(DisplayController):
self.preview_list_widget.setVerticalHeaderLabels(text) self.preview_list_widget.setVerticalHeaderLabels(text)
if self.service_item.is_text(): if self.service_item.is_text():
self.preview_list_widget.resizeRowsToContents() self.preview_list_widget.resizeRowsToContents()
self.preview_list_widget.setColumnWidth(0, self.preview_list_widget.setColumnWidth(0, self.preview_list_widget.viewport().size().width())
self.preview_list_widget.viewport().size().width())
self.__updatePreviewSelection(slideno) self.__updatePreviewSelection(slideno)
self.enableToolBar(service_item) self.enable_tool_bar(service_item)
# Pass to display for viewing. # Pass to display for viewing.
# Postpone image build, we need to do this later to avoid the theme # Postpone image build, we need to do this later to avoid the theme
# flashing on the screen # flashing on the screen
@ -1050,27 +1049,28 @@ class SlideController(DisplayController):
def updatePreview(self): def updatePreview(self):
""" """
This updates the preview frame, for example after changing a slide or This updates the preview frame, for example after changing a slide or using *Blank to Theme*.
using *Blank to Theme*.
""" """
log.debug(u'updatePreview %s ' % self.screens.current[u'primary']) log.debug(u'updatePreview %s ' % self.screens.current[u'primary'])
if not self.screens.current[u'primary'] and self.service_item and \ if not self.screens.current[u'primary'] and self.service_item and \
self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay): self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay):
# Grab now, but try again in a couple of seconds if slide change # Grab now, but try again in a couple of seconds if slide change is slow
# is slow QtCore.QTimer.singleShot(0.5, self.grab_maindisplay)
QtCore.QTimer.singleShot(0.5, self.grabMainDisplay) QtCore.QTimer.singleShot(2.5, self.grab_maindisplay)
QtCore.QTimer.singleShot(2.5, self.grabMainDisplay)
else: else:
self.slidePreview.setPixmap(self.display.preview()) self.slide_image = self.display.preview()
self.slide_preview.setPixmap(self.slide_image)
self.slide_count += 1
def grabMainDisplay(self): def grab_maindisplay(self):
""" """
Creates an image of the current screen and updates the preview frame. Creates an image of the current screen and updates the preview frame.
""" """
winid = QtGui.QApplication.desktop().winId() win_id = QtGui.QApplication.desktop().winId()
rect = self.screens.current[u'size'] rect = self.screens.current[u'size']
winimg = QtGui.QPixmap.grabWindow(winid, rect.x(), rect.y(), rect.width(), rect.height()) win_image = QtGui.QPixmap.grabWindow(win_id, rect.x(), rect.y(), rect.width(), rect.height())
self.slidePreview.setPixmap(winimg) self.slide_preview.setPixmap(win_image)
self.slide_image = win_image
def on_slide_selected_next_action(self, checked): def on_slide_selected_next_action(self, checked):
""" """
@ -1276,7 +1276,7 @@ class SlideController(DisplayController):
self.media_controller.video(self.controller_type, item, self.hide_mode()) self.media_controller.video(self.controller_type, item, self.hide_mode())
if not self.is_live: if not self.is_live:
self.preview_display.show() self.preview_display.show()
self.slidePreview.hide() self.slide_preview.hide()
def onMediaClose(self): def onMediaClose(self):
""" """
@ -1285,7 +1285,7 @@ class SlideController(DisplayController):
log.debug(u'SlideController onMediaClose') log.debug(u'SlideController onMediaClose')
self.media_controller.media_reset(self) self.media_controller.media_reset(self)
self.preview_display.hide() self.preview_display.hide()
self.slidePreview.show() self.slide_preview.show()
def _resetBlank(self): def _resetBlank(self):
""" """

View File

@ -0,0 +1,39 @@
/******************************************************************************
* OpenLP - Open Source Lyrics Projection *
* --------------------------------------------------------------------------- *
* Copyright (c) 2008-2013 Raoul Snyman *
* Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan *
* Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, *
* Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. *
* Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, *
* Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, *
* Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, *
* Frode Woldsund, Martin Zibricky *
* --------------------------------------------------------------------------- *
* 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; version 2 of the License. *
* *
* 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, write to the Free Software Foundation, Inc., 59 *
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
******************************************************************************/
body {
background-color: black;
font-family: sans-serif;
overflow: hidden;
}
.size {
position: absolute;
top: 0px;
vertical-align: middle;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
}

View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<!--
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2013 Raoul Snyman #
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
-->
<head>
<meta charset="utf-8" />
<title>${live_title}</title>
<link rel="stylesheet" href="/files/live.css" />
<link rel="shortcut icon" type="image/x-icon" href="/files/images/favicon.ico">
<script type="text/javascript" src="/files/jquery.js"></script>
<script type="text/javascript" src="/files/live.js"></script>
</head>
<body>
<img id="image" class="size"/>
</body>
</html>

View File

@ -0,0 +1,52 @@
/******************************************************************************
* OpenLP - Open Source Lyrics Projection *
* --------------------------------------------------------------------------- *
* Copyright (c) 2008-2013 Raoul Snyman *
* Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan *
* Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, *
* Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. *
* Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, *
* Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, *
* Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, *
* Frode Woldsund, Martin Zibricky *
* --------------------------------------------------------------------------- *
* 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; version 2 of the License. *
* *
* 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, write to the Free Software Foundation, Inc., 59 *
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
******************************************************************************/
window.OpenLP = {
loadSlide: function (event) {
$.getJSON(
"/live/image",
function (data, status) {
var img = document.getElementById('image');
img.src = data.results.slide_image;
img.style.display = 'block';
}
);
},
pollServer: function () {
$.getJSON(
"/live/poll",
function (data, status) {
if (OpenLP.slideCount != data.results.slide_count) {
OpenLP.slideCount = data.results.slide_count;
OpenLP.loadSlide();
}
}
);
}
}
$.ajaxSetup({ cache: false });
setInterval("OpenLP.pollServer();", 500);
OpenLP.pollServer();

View File

@ -124,7 +124,7 @@ import cherrypy
from mako.template import Template from mako.template import Template
from PyQt4 import QtCore from PyQt4 import QtCore
from openlp.core.lib import Registry, Settings, PluginStatus, StringContent from openlp.core.lib import Registry, Settings, PluginStatus, StringContent, image_to_byte
from openlp.core.utils import AppLocation, translate from openlp.core.utils import AppLocation, translate
from cherrypy._cpcompat import sha, ntob from cherrypy._cpcompat import sha, ntob
@ -136,6 +136,7 @@ def make_sha_hash(password):
""" """
Create an encrypted password for the given password. Create an encrypted password for the given password.
""" """
log.debug("make_sha_hash")
return sha(ntob(password)).hexdigest() return sha(ntob(password)).hexdigest()
@ -143,6 +144,7 @@ def fetch_password(username):
""" """
Fetch the password for a provided user. Fetch the password for a provided user.
""" """
log.debug("Fetch Password")
if username != Settings().value(u'remotes/user id'): if username != Settings().value(u'remotes/user id'):
return None return None
return make_sha_hash(Settings().value(u'remotes/password')) return make_sha_hash(Settings().value(u'remotes/password'))
@ -175,9 +177,11 @@ class HttpServer(object):
self.root = self.Public() self.root = self.Public()
self.root.files = self.Files() self.root.files = self.Files()
self.root.stage = self.Stage() self.root.stage = self.Stage()
self.root.live = self.Live()
self.root.router = self.router self.root.router = self.router
self.root.files.router = self.router self.root.files.router = self.router
self.root.stage.router = self.router self.root.stage.router = self.router
self.root.live.router = self.router
cherrypy.tree.mount(self.root, '/', config=self.define_config()) cherrypy.tree.mount(self.root, '/', config=self.define_config())
# Turn off the flood of access messages cause by poll # Turn off the flood of access messages cause by poll
cherrypy.log.access_log.propagate = False cherrypy.log.access_log.propagate = False
@ -212,6 +216,9 @@ class HttpServer(object):
u'tools.staticdir.dir': self.router.html_dir, u'tools.staticdir.dir': self.router.html_dir,
u'tools.basic_auth.on': False}, u'tools.basic_auth.on': False},
u'/stage': {u'tools.staticdir.on': True, u'/stage': {u'tools.staticdir.on': True,
u'tools.staticdir.dir': self.router.html_dir,
u'tools.basic_auth.on': False},
u'/live': {u'tools.staticdir.on': True,
u'tools.staticdir.dir': self.router.html_dir, u'tools.staticdir.dir': self.router.html_dir,
u'tools.basic_auth.on': False}} u'tools.basic_auth.on': False}}
return directory_config return directory_config
@ -239,7 +246,16 @@ class HttpServer(object):
class Stage(object): class Stage(object):
""" """
Stageview is read only so security is not relevant and would reduce it's usability Stage view is read only so security is not relevant and would reduce it's usability
"""
@cherrypy.expose
def default(self, *args, **kwargs):
url = urlparse.urlparse(cherrypy.url())
return self.router.process_http_request(url.path, *args)
class Live(object):
"""
Live view is read only so security is not relevant and would reduce it's usability
""" """
@cherrypy.expose @cherrypy.expose
def default(self, *args, **kwargs): def default(self, *args, **kwargs):
@ -265,9 +281,12 @@ class HttpRouter(object):
self.routes = [ self.routes = [
(u'^/$', self.serve_file), (u'^/$', self.serve_file),
(u'^/(stage)$', self.serve_file), (u'^/(stage)$', self.serve_file),
(u'^/(live)$', self.serve_file),
(r'^/files/(.*)$', self.serve_file), (r'^/files/(.*)$', self.serve_file),
(r'^/api/poll$', self.poll), (r'^/api/poll$', self.poll),
(r'^/stage/poll$', self.poll), (r'^/stage/poll$', self.poll),
(r'^/live/poll$', self.live_poll),
(r'^/live/image$', self.live_image),
(r'^/api/controller/(live|preview)/(.*)$', self.controller), (r'^/api/controller/(live|preview)/(.*)$', self.controller),
(r'^/stage/controller/(live|preview)/(.*)$', self.controller), (r'^/stage/controller/(live|preview)/(.*)$', self.controller),
(r'^/api/service/(.*)$', self.service), (r'^/api/service/(.*)$', self.service),
@ -305,6 +324,7 @@ class HttpRouter(object):
if response: if response:
return response return response
else: else:
log.debug('Path not found %s', url_path)
return self._http_not_found() return self._http_not_found()
def _get_service_items(self): def _get_service_items(self):
@ -334,6 +354,7 @@ class HttpRouter(object):
self.template_vars = { self.template_vars = {
'app_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Remote'), 'app_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Remote'),
'stage_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Stage View'), 'stage_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Stage View'),
'live_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Live View'),
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'), 'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'), 'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
'alerts': translate('RemotePlugin.Mobile', 'Alerts'), 'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
@ -359,18 +380,19 @@ class HttpRouter(object):
def serve_file(self, filename=None): def serve_file(self, filename=None):
""" """
Send a file to the socket. For now, just a subset of file types Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder.
and must be top level inside the html folder.
If subfolders requested return 404, easier for security for the present. If subfolders requested return 404, easier for security for the present.
Ultimately for i18n, this could first look for xx/file.html before Ultimately for i18n, this could first look for xx/file.html before falling back to file.html.
falling back to file.html... where xx is the language, e.g. 'en' where xx is the language, e.g. 'en'
""" """
log.debug(u'serve file request %s' % filename) log.debug(u'serve file request %s' % filename)
if not filename: if not filename:
filename = u'index.html' filename = u'index.html'
elif filename == u'stage': elif filename == u'stage':
filename = u'stage.html' filename = u'stage.html'
elif filename == u'live':
filename = u'live.html'
path = os.path.normpath(os.path.join(self.html_dir, filename)) path = os.path.normpath(os.path.join(self.html_dir, filename))
if not path.startswith(self.html_dir): if not path.startswith(self.html_dir):
return self._http_not_found() return self._http_not_found()
@ -425,6 +447,26 @@ class HttpRouter(object):
cherrypy.response.headers['Content-Type'] = u'application/json' cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': result}) return json.dumps({u'results': result})
def live_poll(self):
"""
Poll OpenLP to determine the current slide count.
"""
result = {
u'slide_count': self.live_controller.slide_count
}
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': result})
def live_image(self):
"""
Return the latest display image as a byte stream.
"""
result = {
u'slide_image': u'data:image/png;base64,' + str(image_to_byte(self.live_controller.slide_image))
}
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': result})
def display(self, action): def display(self, action):
""" """
Hide or show the display screen. Hide or show the display screen.

View File

@ -86,6 +86,12 @@ class RemoteTab(SettingsTab):
self.stage_url.setObjectName(u'stage_url') self.stage_url.setObjectName(u'stage_url')
self.stage_url.setOpenExternalLinks(True) self.stage_url.setOpenExternalLinks(True)
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url) self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
self.live_url_label = QtGui.QLabel(self.http_settings_group_box)
self.live_url_label.setObjectName(u'live_url_label')
self.live_url = QtGui.QLabel(self.http_settings_group_box)
self.live_url.setObjectName(u'live_url')
self.live_url.setOpenExternalLinks(True)
self.http_setting_layout.addRow(self.live_url_label, self.live_url)
self.left_layout.addWidget(self.http_settings_group_box) self.left_layout.addWidget(self.http_settings_group_box)
self.https_settings_group_box = QtGui.QGroupBox(self.left_column) self.https_settings_group_box = QtGui.QGroupBox(self.left_column)
self.https_settings_group_box.setCheckable(True) self.https_settings_group_box.setCheckable(True)
@ -116,6 +122,12 @@ class RemoteTab(SettingsTab):
self.stage_https_url.setObjectName(u'stage_https_url') self.stage_https_url.setObjectName(u'stage_https_url')
self.stage_https_url.setOpenExternalLinks(True) self.stage_https_url.setOpenExternalLinks(True)
self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url) self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url)
self.live_https_url_label = QtGui.QLabel(self.https_settings_group_box)
self.live_https_url_label.setObjectName(u'live_url_label')
self.live_https_url = QtGui.QLabel(self.https_settings_group_box)
self.live_https_url.setObjectName(u'live_https_url')
self.live_https_url.setOpenExternalLinks(True)
self.https_settings_layout.addRow(self.live_https_url_label, self.live_https_url)
self.left_layout.addWidget(self.https_settings_group_box) self.left_layout.addWidget(self.https_settings_group_box)
self.user_login_group_box = QtGui.QGroupBox(self.left_column) self.user_login_group_box = QtGui.QGroupBox(self.left_column)
self.user_login_group_box.setCheckable(True) self.user_login_group_box.setCheckable(True)
@ -163,6 +175,7 @@ class RemoteTab(SettingsTab):
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:')) self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:')) self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:')) self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format')) self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App')) self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
self.qr_description_label.setText(translate('RemotePlugin.RemoteTab', self.qr_description_label.setText(translate('RemotePlugin.RemoteTab',
@ -176,6 +189,7 @@ class RemoteTab(SettingsTab):
self.https_port_label.setText(self.port_label.text()) self.https_port_label.setText(self.port_label.text())
self.remote_https_url_label.setText(self.remote_url_label.text()) self.remote_https_url_label.setText(self.remote_url_label.text())
self.stage_https_url_label.setText(self.stage_url_label.text()) self.stage_https_url_label.setText(self.stage_url_label.text())
self.live_https_url_label.setText(self.live_url_label.text())
self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication')) self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:')) self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:')) self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
@ -203,10 +217,14 @@ class RemoteTab(SettingsTab):
https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value()) https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value())
self.remote_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url)) self.remote_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
self.remote_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url)) self.remote_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
http_url += u'stage' http_url_temp = http_url + u'stage'
https_url += u'stage' https_url_temp = https_url + u'stage'
self.stage_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url)) self.stage_url.setText(u'<a href="%s">%s</a>' % (http_url_temp, http_url_temp))
self.stage_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url)) self.stage_https_url.setText(u'<a href="%s">%s</a>' % (https_url_temp, https_url_temp))
http_url_temp = http_url + u'live'
https_url_temp = https_url + u'live'
self.live_url.setText(u'<a href="%s">%s</a>' % (http_url_temp, http_url_temp))
self.live_https_url.setText(u'<a href="%s">%s</a>' % (https_url_temp, https_url_temp))
def load(self): def load(self):
""" """