Merge lp:~trb143/openlp/webfull into lp:openlp

Proposed by Tim Bentley
Status: Merged
Merged at revision: 2759
Proposed branch: lp:~trb143/openlp/webfull
Merge into: lp:openlp
Diff against target: 26912 lines (+3670/-22117)
92 files modified
openlp/core/__init__.py (+5/-4)
openlp/core/api/__init__.py (+28/-0)
openlp/core/api/endpoint/__init__.py (+25/-0)
openlp/core/api/endpoint/controller.py (+144/-0)
openlp/core/api/endpoint/core.py (+182/-0)
openlp/core/api/endpoint/pluginhelpers.py (+138/-0)
openlp/core/api/endpoint/service.py (+100/-0)
openlp/core/api/http/__init__.py (+110/-0)
openlp/core/api/http/endpoint.py (+80/-0)
openlp/core/api/http/errors.py (+65/-0)
openlp/core/api/http/server.py (+97/-0)
openlp/core/api/http/wsgiapp.py (+181/-0)
openlp/core/api/poll.py (+130/-0)
openlp/core/api/tab.py (+316/-0)
openlp/core/api/websockets.py (+142/-0)
openlp/core/common/httputils.py (+16/-0)
openlp/core/common/settings.py (+19/-0)
openlp/core/common/uistrings.py (+2/-0)
openlp/core/common/versionchecker.py (+38/-7)
openlp/core/lib/imagemanager.py (+28/-7)
openlp/core/ui/firsttimeform.py (+1/-3)
openlp/core/ui/firsttimewizard.py (+2/-2)
openlp/core/ui/mainwindow.py (+8/-2)
openlp/core/ui/media/__init__.py (+1/-0)
openlp/core/ui/media/endpoint.py (+72/-0)
openlp/core/ui/media/mediacontroller.py (+28/-1)
openlp/core/ui/settingsform.py (+8/-1)
openlp/core/ui/slidecontroller.py (+7/-0)
openlp/plugins/alerts/alertsplugin.py (+4/-0)
openlp/plugins/alerts/endpoint.py (+60/-0)
openlp/plugins/bibles/bibleplugin.py (+4/-0)
openlp/plugins/bibles/endpoint.py (+100/-0)
openlp/plugins/custom/customplugin.py (+4/-0)
openlp/plugins/custom/endpoint.py (+100/-0)
openlp/plugins/images/endpoint.py (+113/-0)
openlp/plugins/images/imageplugin.py (+4/-0)
openlp/plugins/media/endpoint.py (+100/-0)
openlp/plugins/media/mediaplugin.py (+5/-2)
openlp/plugins/presentations/endpoint.py (+114/-0)
openlp/plugins/presentations/presentationplugin.py (+5/-1)
openlp/plugins/remotes/__init__.py (+0/-68)
openlp/plugins/remotes/deploy.py (+69/-0)
openlp/plugins/remotes/endpoint.py (+46/-0)
openlp/plugins/remotes/html/assets/jquery.js (+0/-9404)
openlp/plugins/remotes/html/assets/jquery.min.js (+0/-4)
openlp/plugins/remotes/html/assets/jquery.mobile.js (+0/-9357)
openlp/plugins/remotes/html/assets/jquery.mobile.min.css (+0/-2)
openlp/plugins/remotes/html/assets/jquery.mobile.min.js (+0/-2)
openlp/plugins/remotes/html/chords.html (+0/-46)
openlp/plugins/remotes/html/css/chords.css (+0/-96)
openlp/plugins/remotes/html/css/main.css (+0/-32)
openlp/plugins/remotes/html/css/openlp.css (+0/-31)
openlp/plugins/remotes/html/css/stage.css (+0/-68)
openlp/plugins/remotes/html/index.html (+0/-177)
openlp/plugins/remotes/html/js/chords.js (+0/-331)
openlp/plugins/remotes/html/js/main.js (+0/-45)
openlp/plugins/remotes/html/js/openlp.js (+0/-384)
openlp/plugins/remotes/html/js/stage.js (+0/-170)
openlp/plugins/remotes/html/main.html (+0/-34)
openlp/plugins/remotes/html/stage.html (+0/-41)
openlp/plugins/remotes/lib/__init__.py (+0/-27)
openlp/plugins/remotes/lib/httprouter.py (+0/-709)
openlp/plugins/remotes/lib/httpserver.py (+0/-155)
openlp/plugins/remotes/lib/remotetab.py (+0/-271)
openlp/plugins/remotes/remoteplugin.py (+94/-65)
openlp/plugins/songs/endpoint.py (+100/-0)
openlp/plugins/songs/lib/mediaitem.py (+1/-1)
openlp/plugins/songs/songsplugin.py (+8/-2)
scripts/check_dependencies.py (+5/-1)
scripts/jenkins_script.py (+1/-1)
scripts/websocket_client.py (+37/-0)
tests/functional/openlp_core_api/__init__.py (+21/-0)
tests/functional/openlp_core_api/test_tab.py (+139/-0)
tests/functional/openlp_core_api/test_websockets.py (+119/-0)
tests/functional/openlp_core_api_http/__init__.py (+21/-0)
tests/functional/openlp_core_api_http/test_error.py (+59/-0)
tests/functional/openlp_core_api_http/test_http.py (+57/-0)
tests/functional/openlp_core_api_http/test_wsgiapp.py (+86/-0)
tests/functional/openlp_core_common/test_httputils.py (+27/-1)
tests/functional/openlp_core_lib/test_image_manager.py (+2/-0)
tests/functional/openlp_core_ui/test_mainwindow.py (+5/-2)
tests/functional/openlp_core_ui/test_settingsform.py (+8/-0)
tests/functional/openlp_plugins/media/test_mediaplugin.py (+1/-3)
tests/functional/openlp_plugins/presentations/test_impresscontroller.py (+1/-1)
tests/functional/openlp_plugins/remotes/test_deploy.py (+64/-0)
tests/functional/openlp_plugins/remotes/test_remotetab.py (+0/-134)
tests/functional/openlp_plugins/remotes/test_router.py (+0/-399)
tests/interfaces/openlp_core_api/__init__.py (+21/-0)
tests/interfaces/openlp_core_ui/test_mainwindow.py (+4/-1)
tests/interfaces/openlp_core_ui/test_servicemanager.py (+4/-1)
tests/interfaces/openlp_plugins/remotes/__init__.py (+0/-21)
tests/interfaces/openlp_plugins/remotes/test_deploy.py (+84/-0)
To merge this branch: bzr merge lp:~trb143/openlp/webfull
Reviewer Review Type Date Requested Status
Raoul Snyman Approve
Review via email: mp+328953@code.launchpad.net

This proposal supersedes a proposal from 2017-03-05.

Description of the change

This is getting to big to stay external any longer.
The web interface works with the existing HTML which has been externalised and can be pulled from OpenLP.

- Add new web and socket servers to API and replace all existing API's
- remove most of the Remote plugin but leave the base there to allow for the html and js code to land there.
- amend the FTW to download a package of html, JS and CSS and install in the remote directory
- add switch to turn off the servers to allow PyCharm to debug. It gets lost in threads if you do not!

Issues
The new UI String has gone wrong and not sure why
The FTW works fine for download but how should the Web Settings have a pop up as well?

It does work fine so have a play, you will need to run the FTW to install a set of web pages as they are missing.

Fixed up the issues Raoul brought up in the last review.

lp:~trb143/openlp/webfull (revision 2833)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2169/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/2074/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1962/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1332/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1169/
[SUCCESS] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/299/
[FAILURE] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/142/

To post a comment you must log in.
Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

So, some thoughts around removing the web remote from OpenLP:

 - The remote plugin should handle the download and install of the web remote
 - The downloadable web remote should be versioned
 - OpenLP needs to be aware of the versions
 - Users should be able to check if there's a new version of the web remote
 - Users should be able to download an updated version of the web remote
 - Technically, the remote plugin should implement the old API
 - We should really make the new API RESTful, which means returning 400,
   500 and 200 status codes and not {"result": {"success": true}}

I'm going to write up more on the wiki about how I think the new API methods should look.

Also, see inline comments.

Revision history for this message
Raoul Snyman (raoul-snyman) wrote : Posted in a previous version of this proposal

Oh, also, with the new web remote we won't need Mako or the translations because it'll be completely client-side.

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

So, I realised that the register_endpoint() method isn't helpful because if the modules are never imported the endpoint is never registered. What if we "moved" register_endpoint into the Plugin class?

Revision history for this message
Tim Bentley (trb143) wrote :

That could work.
A challenge with this is its size now and the time taken to stop getting it broken.
Due to time scales (renderer) it would be nice to get this in and work from within core code.

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Sure. Let's try to get it working, especially with the current remotes, and then we're definitely closer to having a better solution.

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

I can always come in and refine it later.

Revision history for this message
Tim Bentley (trb143) wrote :

The current remote is packaged and will download and install with this code.
That needs a new truck to be created and defined but it allows the existing functionality to work.

Revision history for this message
Raoul Snyman (raoul-snyman) wrote :

Note: I haven't done a thorough code review. Let's get this in and start iterating.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openlp/core/__init__.py'
2--- openlp/core/__init__.py 2017-08-01 20:59:41 +0000
3+++ openlp/core/__init__.py 2017-08-13 07:11:15 +0000
4@@ -153,10 +153,8 @@
5 self.processEvents()
6 if not has_run_wizard:
7 self.main_window.first_time()
8- # update_check = Settings().value('core/update check')
9- # if update_check:
10- # version = VersionThread(self.main_window)
11- # version.start()
12+ version = VersionThread(self.main_window)
13+ version.start()
14 self.main_window.is_display_blank()
15 self.main_window.app_startup()
16 return self.exec()
17@@ -337,6 +335,8 @@
18 parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true',
19 help='Ignore the version file and pull the version directly from Bazaar')
20 parser.add_argument('-s', '--style', dest='style', help='Set the Qt5 style (passed directly to Qt5).')
21+ parser.add_argument('-w', '--no-web-server', dest='no_web_server', action='store_false',
22+ help='Turn off the Web and Socket Server ')
23 parser.add_argument('rargs', nargs='?', default=[])
24 # Parse command line options and deal with them. Use args supplied pragmatically if possible.
25 return parser.parse_args(args) if args else parser.parse_args()
26@@ -410,6 +410,7 @@
27 set_up_logging(str(AppLocation.get_directory(AppLocation.CacheDir)))
28 Registry.create()
29 Registry().register('application', application)
30+ Registry().set_flag('no_web_server', args.no_web_server)
31 application.setApplicationVersion(get_application_version()['version'])
32 # Check if an instance of OpenLP is already running. Quit if there is a running instance and the user only wants one
33 if application.is_already_running():
34
35=== added directory 'openlp/core/api'
36=== added file 'openlp/core/api/__init__.py'
37--- openlp/core/api/__init__.py 1970-01-01 00:00:00 +0000
38+++ openlp/core/api/__init__.py 2017-08-13 07:11:15 +0000
39@@ -0,0 +1,28 @@
40+# -*- coding: utf-8 -*-
41+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
42+
43+###############################################################################
44+# OpenLP - Open Source Lyrics Projection #
45+# --------------------------------------------------------------------------- #
46+# Copyright (c) 2008-2017 OpenLP Developers #
47+# --------------------------------------------------------------------------- #
48+# This program is free software; you can redistribute it and/or modify it #
49+# under the terms of the GNU General Public License as published by the Free #
50+# Software Foundation; version 2 of the License. #
51+# #
52+# This program is distributed in the hope that it will be useful, but WITHOUT #
53+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
54+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
55+# more details. #
56+# #
57+# You should have received a copy of the GNU General Public License along #
58+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
59+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
60+###############################################################################
61+
62+from openlp.core.api.http.endpoint import Endpoint
63+from openlp.core.api.http import register_endpoint, requires_auth
64+from openlp.core.api.tab import ApiTab
65+from openlp.core.api.poll import Poller
66+
67+__all__ = ['Endpoint', 'ApiTab', 'register_endpoint', 'requires_auth']
68
69=== added directory 'openlp/core/api/endpoint'
70=== added file 'openlp/core/api/endpoint/__init__.py'
71--- openlp/core/api/endpoint/__init__.py 1970-01-01 00:00:00 +0000
72+++ openlp/core/api/endpoint/__init__.py 2017-08-13 07:11:15 +0000
73@@ -0,0 +1,25 @@
74+# -*- coding: utf-8 -*-
75+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
76+
77+###############################################################################
78+# OpenLP - Open Source Lyrics Projection #
79+# --------------------------------------------------------------------------- #
80+# Copyright (c) 2008-2017 OpenLP Developers #
81+# --------------------------------------------------------------------------- #
82+# This program is free software; you can redistribute it and/or modify it #
83+# under the terms of the GNU General Public License as published by the Free #
84+# Software Foundation; version 2 of the License. #
85+# #
86+# This program is distributed in the hope that it will be useful, but WITHOUT #
87+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
88+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
89+# more details. #
90+# #
91+# You should have received a copy of the GNU General Public License along #
92+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
93+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
94+###############################################################################
95+"""
96+The Endpoint class, which provides plugins with a way to serve their own portion of the API
97+"""
98+from .pluginhelpers import search, live, service
99
100=== added file 'openlp/core/api/endpoint/controller.py'
101--- openlp/core/api/endpoint/controller.py 1970-01-01 00:00:00 +0000
102+++ openlp/core/api/endpoint/controller.py 2017-08-13 07:11:15 +0000
103@@ -0,0 +1,144 @@
104+# -*- coding: utf-8 -*-
105+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
106+
107+###############################################################################
108+# OpenLP - Open Source Lyrics Projection #
109+# --------------------------------------------------------------------------- #
110+# Copyright (c) 2008-2017 OpenLP Developers #
111+# --------------------------------------------------------------------------- #
112+# This program is free software; you can redistribute it and/or modify it #
113+# under the terms of the GNU General Public License as published by the Free #
114+# Software Foundation; version 2 of the License. #
115+# #
116+# This program is distributed in the hope that it will be useful, but WITHOUT #
117+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
118+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
119+# more details. #
120+# #
121+# You should have received a copy of the GNU General Public License along #
122+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
123+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
124+###############################################################################
125+import logging
126+import os
127+import urllib.request
128+import urllib.error
129+import json
130+
131+from openlp.core.api.http.endpoint import Endpoint
132+from openlp.core.api.http import requires_auth
133+from openlp.core.common import Registry, AppLocation, Settings
134+from openlp.core.lib import ItemCapabilities, create_thumb
135+
136+log = logging.getLogger(__name__)
137+
138+controller_endpoint = Endpoint('controller')
139+api_controller_endpoint = Endpoint('api')
140+
141+
142+@api_controller_endpoint.route('controller/live/text')
143+@controller_endpoint.route('live/text')
144+def controller_text(request):
145+ """
146+ Perform an action on the slide controller.
147+
148+ :param request: the http request - not used
149+ """
150+ log.debug("controller_text ")
151+ live_controller = Registry().get('live_controller')
152+ current_item = live_controller.service_item
153+ data = []
154+ if current_item:
155+ for index, frame in enumerate(current_item.get_frames()):
156+ item = {}
157+ # Handle text (songs, custom, bibles)
158+ if current_item.is_text():
159+ if frame['verseTag']:
160+ item['tag'] = str(frame['verseTag'])
161+ else:
162+ item['tag'] = str(index + 1)
163+ item['chords_text'] = str(frame['chords_text'])
164+ item['text'] = str(frame['text'])
165+ item['html'] = str(frame['html'])
166+ # Handle images, unless a custom thumbnail is given or if thumbnails is disabled
167+ elif current_item.is_image() and not frame.get('image', '') and Settings().value('api/thumbnails'):
168+ item['tag'] = str(index + 1)
169+ thumbnail_path = os.path.join('images', 'thumbnails', frame['title'])
170+ full_thumbnail_path = os.path.join(AppLocation.get_data_path(), thumbnail_path)
171+ # Create thumbnail if it doesn't exists
172+ if not os.path.exists(full_thumbnail_path):
173+ create_thumb(current_item.get_frame_path(index), full_thumbnail_path, False)
174+ Registry().get('image_manager').add_image(full_thumbnail_path, frame['title'], None, 88, 88)
175+ item['img'] = urllib.request.pathname2url(os.path.sep + thumbnail_path)
176+ item['text'] = str(frame['title'])
177+ item['html'] = str(frame['title'])
178+ else:
179+ # Handle presentation etc.
180+ item['tag'] = str(index + 1)
181+ if current_item.is_capable(ItemCapabilities.HasDisplayTitle):
182+ item['title'] = str(frame['display_title'])
183+ if current_item.is_capable(ItemCapabilities.HasNotes):
184+ item['slide_notes'] = str(frame['notes'])
185+ if current_item.is_capable(ItemCapabilities.HasThumbnails) and \
186+ Settings().value('api/thumbnails'):
187+ # If the file is under our app directory tree send the portion after the match
188+ data_path = AppLocation.get_data_path()
189+ if frame['image'][0:len(data_path)] == data_path:
190+ item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
191+ Registry().get('image_manager').add_image(frame['image'], frame['title'], None, 88, 88)
192+ item['text'] = str(frame['title'])
193+ item['html'] = str(frame['title'])
194+ item['selected'] = (live_controller.selected_row == index)
195+ data.append(item)
196+ json_data = {'results': {'slides': data}}
197+ if current_item:
198+ json_data['results']['item'] = live_controller.service_item.unique_identifier
199+ return json_data
200+
201+
202+@api_controller_endpoint.route('controller/live/set')
203+@controller_endpoint.route('live/set')
204+@requires_auth
205+def controller_set(request):
206+ """
207+ Perform an action on the slide controller.
208+
209+ :param request: The action to perform.
210+ """
211+ event = getattr(Registry().get('live_controller'), 'slidecontroller_live_set')
212+ try:
213+ json_data = request.GET.get('data')
214+ data = int(json.loads(json_data)['request']['id'])
215+ event.emit([data])
216+ except KeyError:
217+ log.error("Endpoint controller/live/set request id not found")
218+ return {'results': {'success': True}}
219+
220+
221+@controller_endpoint.route('{action:next|previous}')
222+@requires_auth
223+def controller_direction(request, controller, action):
224+ """
225+ Handles requests for setting service items in the slide controller
226+11
227+ :param request: The http request object.
228+ :param controller: the controller slides forward or backward.
229+ :param action: the controller slides forward or backward.
230+ """
231+ event = getattr(Registry().get('live_controller'), 'slidecontroller_{controller}_{action}'.
232+ format(controller=controller, action=action))
233+ event.emit()
234+
235+
236+@api_controller_endpoint.route('controller/{controller}/{action:next|previous}')
237+@requires_auth
238+def controller_direction_api(request, controller, action):
239+ """
240+ Handles requests for setting service items in the slide controller
241+11
242+ :param request: The http request object.
243+ :param controller: the controller slides forward or backward.
244+ :param action: the controller slides forward or backward.
245+ """
246+ controller_direction(request, controller, action)
247+ return {'results': {'success': True}}
248
249=== added file 'openlp/core/api/endpoint/core.py'
250--- openlp/core/api/endpoint/core.py 1970-01-01 00:00:00 +0000
251+++ openlp/core/api/endpoint/core.py 2017-08-13 07:11:15 +0000
252@@ -0,0 +1,182 @@
253+# -*- coding: utf-8 -*-
254+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
255+
256+###############################################################################
257+# OpenLP - Open Source Lyrics Projection #
258+# --------------------------------------------------------------------------- #
259+# Copyright (c) 2008-2017 OpenLP Developers #
260+# --------------------------------------------------------------------------- #
261+# This program is free software; you can redistribute it and/or modify it #
262+# under the terms of the GNU General Public License as published by the Free #
263+# Software Foundation; version 2 of the License. #
264+# #
265+# This program is distributed in the hope that it will be useful, but WITHOUT #
266+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
267+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
268+# more details. #
269+# #
270+# You should have received a copy of the GNU General Public License along #
271+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
272+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
273+###############################################################################
274+import logging
275+import os
276+
277+from openlp.core.api.http.endpoint import Endpoint
278+from openlp.core.api.http import requires_auth
279+from openlp.core.common import Registry, UiStrings, translate
280+from openlp.core.lib import image_to_byte, PluginStatus, StringContent
281+
282+
283+template_dir = 'templates'
284+static_dir = 'static'
285+blank_dir = os.path.join(static_dir, 'index')
286+
287+
288+log = logging.getLogger(__name__)
289+
290+chords_endpoint = Endpoint('chords', template_dir=template_dir, static_dir=static_dir)
291+stage_endpoint = Endpoint('stage', template_dir=template_dir, static_dir=static_dir)
292+main_endpoint = Endpoint('main', template_dir=template_dir, static_dir=static_dir)
293+blank_endpoint = Endpoint('', template_dir=template_dir, static_dir=blank_dir)
294+
295+FILE_TYPES = {
296+ '.html': 'text/html',
297+ '.css': 'text/css',
298+ '.js': 'application/javascript',
299+ '.jpg': 'image/jpeg',
300+ '.gif': 'image/gif',
301+ '.ico': 'image/x-icon',
302+ '.png': 'image/png'
303+}
304+
305+remote = translate('RemotePlugin.Mobile', 'Remote')
306+stage = translate('RemotePlugin.Mobile', 'Stage View')
307+live = translate('RemotePlugin.Mobile', 'Live View')
308+chords = translate('RemotePlugin.Mobile', 'Chords View')
309+
310+TRANSLATED_STRINGS = {
311+ 'app_title': "{main} {remote}".format(main=UiStrings().OpenLP, remote=remote),
312+ 'stage_title': "{main} {stage}".format(main=UiStrings().OpenLP, stage=stage),
313+ 'live_title': "{main} {live}".format(main=UiStrings().OpenLP, live=live),
314+ 'chords_title': "{main} {chords}".format(main=UiStrings().OpenLP, chords=chords),
315+ 'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
316+ 'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
317+ 'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
318+ 'search': translate('RemotePlugin.Mobile', 'Search'),
319+ 'home': translate('RemotePlugin.Mobile', 'Home'),
320+ 'refresh': translate('RemotePlugin.Mobile', 'Refresh'),
321+ 'blank': translate('RemotePlugin.Mobile', 'Blank'),
322+ 'theme': translate('RemotePlugin.Mobile', 'Theme'),
323+ 'desktop': translate('RemotePlugin.Mobile', 'Desktop'),
324+ 'show': translate('RemotePlugin.Mobile', 'Show'),
325+ 'prev': translate('RemotePlugin.Mobile', 'Prev'),
326+ 'next': translate('RemotePlugin.Mobile', 'Next'),
327+ 'text': translate('RemotePlugin.Mobile', 'Text'),
328+ 'show_alert': translate('RemotePlugin.Mobile', 'Show Alert'),
329+ 'go_live': translate('RemotePlugin.Mobile', 'Go Live'),
330+ 'add_to_service': translate('RemotePlugin.Mobile', 'Add to Service'),
331+ 'add_and_go_to_service': translate('RemotePlugin.Mobile', 'Add & Go to Service'),
332+ 'no_results': translate('RemotePlugin.Mobile', 'No Results'),
333+ 'options': translate('RemotePlugin.Mobile', 'Options'),
334+ 'service': translate('RemotePlugin.Mobile', 'Service'),
335+ 'slides': translate('RemotePlugin.Mobile', 'Slides'),
336+ 'settings': translate('RemotePlugin.Mobile', 'Settings'),
337+}
338+
339+
340+@stage_endpoint.route('')
341+def stage_index(request):
342+ """
343+ Deliver the page for the /stage url
344+ """
345+ return stage_endpoint.render_template('stage.mako', **TRANSLATED_STRINGS)
346+
347+
348+@chords_endpoint.route('')
349+def chords_index(request):
350+ """
351+ Deliver the page for the /chords url
352+ """
353+ return chords_endpoint.render_template('chords.mako', **TRANSLATED_STRINGS)
354+
355+
356+@main_endpoint.route('')
357+def main_index(request):
358+ """
359+ Deliver the page for the /main url
360+ """
361+ return main_endpoint.render_template('main.mako', **TRANSLATED_STRINGS)
362+
363+
364+@blank_endpoint.route('')
365+def index(request):
366+ """
367+ Deliver the page for the / url
368+ :param request:
369+ """
370+ return blank_endpoint.render_template('index.mako', **TRANSLATED_STRINGS)
371+
372+
373+@blank_endpoint.route('api/poll')
374+@blank_endpoint.route('poll')
375+def poll(request):
376+ """
377+ Deliver the page for the /poll url
378+
379+ :param request:
380+ """
381+ return Registry().get('poller').poll()
382+
383+
384+@blank_endpoint.route('api/display/{display:hide|show|blank|theme|desktop}')
385+@blank_endpoint.route('display/{display:hide|show|blank|theme|desktop}')
386+@requires_auth
387+def toggle_display(request, display):
388+ """
389+ Deliver the functions for the /display url
390+ :param request: the http request - not used
391+ :param display: the display function to be triggered
392+ """
393+ Registry().get('live_controller').slidecontroller_toggle_display.emit(display)
394+ return {'results': {'success': True}}
395+
396+
397+@blank_endpoint.route('api/plugin/search')
398+@blank_endpoint.route('plugin/search')
399+def plugin_search_list(request):
400+ """
401+ Deliver a list of active plugins that support search
402+ :param request: the http request - not used
403+ """
404+ searches = []
405+ for plugin in Registry().get('plugin_manager').plugins:
406+ if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
407+ searches.append([plugin.name, str(plugin.text_strings[StringContent.Name]['plural'])])
408+ return {'results': {'items': searches}}
409+
410+
411+@main_endpoint.route('image')
412+def main_image(request):
413+ """
414+ Return the latest display image as a byte stream.
415+ :param request: base path of the URL. Not used but passed by caller
416+ :return:
417+ """
418+ live_controller = Registry().get('live_controller')
419+ result = {
420+ 'slide_image': 'data:image/png;base64,' + str(image_to_byte(live_controller.slide_image))
421+ }
422+ return {'results': result}
423+
424+
425+def get_content_type(file_name):
426+ """
427+ Examines the extension of the file and determines what the content_type should be, defaults to text/plain
428+ Returns the extension and the content_type
429+
430+ :param file_name: name of file
431+ """
432+ ext = os.path.splitext(file_name)[1]
433+ content_type = FILE_TYPES.get(ext, 'text/plain')
434+ return ext, content_type
435
436=== added file 'openlp/core/api/endpoint/pluginhelpers.py'
437--- openlp/core/api/endpoint/pluginhelpers.py 1970-01-01 00:00:00 +0000
438+++ openlp/core/api/endpoint/pluginhelpers.py 2017-08-13 07:11:15 +0000
439@@ -0,0 +1,138 @@
440+# -*- coding: utf-8 -*-
441+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
442+
443+###############################################################################
444+# OpenLP - Open Source Lyrics Projection #
445+# --------------------------------------------------------------------------- #
446+# Copyright (c) 2008-2017 OpenLP Developers #
447+# --------------------------------------------------------------------------- #
448+# This program is free software; you can redistribute it and/or modify it #
449+# under the terms of the GNU General Public License as published by the Free #
450+# Software Foundation; version 2 of the License. #
451+# #
452+# This program is distributed in the hope that it will be useful, but WITHOUT #
453+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
454+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
455+# more details. #
456+# #
457+# You should have received a copy of the GNU General Public License along #
458+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
459+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
460+###############################################################################
461+import os
462+import json
463+import re
464+import urllib
465+
466+from urllib.parse import urlparse
467+from webob import Response
468+
469+from openlp.core.api.http.errors import NotFound
470+from openlp.core.common import Registry, AppLocation
471+from openlp.core.lib import PluginStatus, image_to_byte
472+
473+
474+def search(request, plugin_name, log):
475+ """
476+ Handles requests for searching the plugins
477+
478+ :param request: The http request object.
479+ :param plugin_name: The plugin name.
480+ :param log: The class log object.
481+ """
482+ try:
483+ json_data = request.GET.get('data')
484+ text = json.loads(json_data)['request']['text']
485+ except KeyError:
486+ log.error("Endpoint {text} search request text not found".format(text=plugin_name))
487+ text = ""
488+ text = urllib.parse.unquote(text)
489+ plugin = Registry().get('plugin_manager').get_plugin_by_name(plugin_name)
490+ if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
491+ results = plugin.media_item.search(text, False)
492+ return {'results': {'items': results}}
493+ else:
494+ raise NotFound()
495+
496+
497+def live(request, plugin_name, log):
498+ """
499+ Handles requests for making live of the plugins
500+
501+ :param request: The http request object.
502+ :param plugin_name: The plugin name.
503+ :param log: The class log object.
504+ """
505+ try:
506+ json_data = request.GET.get('data')
507+ request_id = json.loads(json_data)['request']['id']
508+ except KeyError:
509+ log.error("Endpoint {text} search request text not found".format(text=plugin_name))
510+ return []
511+ plugin = Registry().get('plugin_manager').get_plugin_by_name(plugin_name)
512+ if plugin.status == PluginStatus.Active and plugin.media_item:
513+ getattr(plugin.media_item, '{name}_go_live'.format(name=plugin_name)).emit([request_id, True])
514+
515+
516+def service(request, plugin_name, log):
517+ """
518+ Handles requests for adding to a service of the plugins
519+
520+ :param request: The http request object.
521+ :param plugin_name: The plugin name.
522+ :param log: The class log object.
523+ """
524+ try:
525+ json_data = request.GET.get('data')
526+ request_id = json.loads(json_data)['request']['id']
527+ except KeyError:
528+ log.error("Endpoint {plugin} search request text not found".format(plugin=plugin_name))
529+ return []
530+ plugin = Registry().get('plugin_manager').get_plugin_by_name(plugin_name)
531+ if plugin.status == PluginStatus.Active and plugin.media_item:
532+ item_id = plugin.media_item.create_item_from_id(request_id)
533+ getattr(plugin.media_item, '{name}_add_to_service'.format(name=plugin_name)).emit([item_id, True])
534+
535+
536+def display_thumbnails(request, controller_name, log, dimensions, file_name, slide=None):
537+ """
538+ Handles requests for adding a song to the service
539+
540+ Return an image to a web page based on a URL
541+ :param request: Request object
542+ :param controller_name: which controller is requesting the image
543+ :param log: the logger object
544+ :param dimensions: the image size eg 88x88
545+ :param file_name: the file name of the image
546+ :param slide: the individual image name
547+ :return:
548+ """
549+ log.debug('serve thumbnail {cname}/thumbnails{dim}/{fname}/{slide}'.format(cname=controller_name,
550+ dim=dimensions,
551+ fname=file_name,
552+ slide=slide))
553+ # -1 means use the default dimension in ImageManager
554+ width = -1
555+ height = -1
556+ image = None
557+ if dimensions:
558+ match = re.search('(\d+)x(\d+)', dimensions)
559+ if match:
560+ # let's make sure that the dimensions are within reason
561+ width = sorted([10, int(match.group(1)), 1000])[1]
562+ height = sorted([10, int(match.group(2)), 1000])[1]
563+ if controller_name and file_name:
564+ file_name = urllib.parse.unquote(file_name)
565+ if '..' not in file_name: # no hacking please
566+ if slide:
567+ full_path = os.path.normpath(os.path.join(AppLocation.get_section_data_path(controller_name),
568+ 'thumbnails', file_name, slide))
569+ else:
570+ full_path = os.path.normpath(os.path.join(AppLocation.get_section_data_path(controller_name),
571+
572+ 'thumbnails', file_name))
573+ if os.path.exists(full_path):
574+ path, just_file_name = os.path.split(full_path)
575+ Registry().get('image_manager').add_image(full_path, just_file_name, None, width, height)
576+ image = Registry().get('image_manager').get_image(full_path, just_file_name, width, height)
577+ return Response(body=image_to_byte(image, False), status=200, content_type='image/png', charset='utf8')
578
579=== added file 'openlp/core/api/endpoint/service.py'
580--- openlp/core/api/endpoint/service.py 1970-01-01 00:00:00 +0000
581+++ openlp/core/api/endpoint/service.py 2017-08-13 07:11:15 +0000
582@@ -0,0 +1,100 @@
583+# -*- coding: utf-8 -*-
584+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
585+
586+###############################################################################
587+# OpenLP - Open Source Lyrics Projection #
588+# --------------------------------------------------------------------------- #
589+# Copyright (c) 2008-2017 OpenLP Developers #
590+# --------------------------------------------------------------------------- #
591+# This program is free software; you can redistribute it and/or modify it #
592+# under the terms of the GNU General Public License as published by the Free #
593+# Software Foundation; version 2 of the License. #
594+# #
595+# This program is distributed in the hope that it will be useful, but WITHOUT #
596+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
597+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
598+# more details. #
599+# #
600+# You should have received a copy of the GNU General Public License along #
601+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
602+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
603+###############################################################################
604+import logging
605+import json
606+
607+from openlp.core.api.http.endpoint import Endpoint
608+from openlp.core.api.http import register_endpoint, requires_auth
609+from openlp.core.common import Registry
610+
611+
612+log = logging.getLogger(__name__)
613+
614+service_endpoint = Endpoint('service')
615+api_service_endpoint = Endpoint('api/service')
616+
617+
618+@api_service_endpoint.route('list')
619+@service_endpoint.route('list')
620+def list_service(request):
621+ """
622+ Handles requests for service items in the service manager
623+
624+ :param request: The http request object.
625+ """
626+ return {'results': {'items': get_service_items()}}
627+
628+
629+@api_service_endpoint.route('set')
630+@service_endpoint.route('set')
631+@requires_auth
632+def service_set(request):
633+ """
634+ Handles requests for setting service items in the service manager
635+
636+ :param request: The http request object.
637+ """
638+ event = getattr(Registry().get('service_manager'), 'servicemanager_set_item')
639+ try:
640+ json_data = request.GET.get('data')
641+ data = int(json.loads(json_data)['request']['id'])
642+ event.emit(data)
643+ except KeyError:
644+ log.error("Endpoint service/set request id not found")
645+ return {'results': {'success': True}}
646+
647+
648+@api_service_endpoint.route('{action:next|previous}')
649+@service_endpoint.route('{action:next|previous}')
650+@requires_auth
651+def service_direction(request, action):
652+ """
653+ Handles requests for setting service items in the service manager
654+
655+ :param request: The http request object.
656+ :param action: the the service slides forward or backward.
657+ """
658+ event = getattr(Registry().get('service_manager'), 'servicemanager_{action}_item'.format(action=action))
659+ event.emit()
660+ return {'results': {'success': True}}
661+
662+
663+def get_service_items():
664+ """
665+ Read the service item in use and return the data as a json object
666+ """
667+ live_controller = Registry().get('live_controller')
668+ service_items = []
669+ if live_controller.service_item:
670+ current_unique_identifier = live_controller.service_item.unique_identifier
671+ else:
672+ current_unique_identifier = None
673+ for item in Registry().get('service_manager').service_items:
674+ service_item = item['service_item']
675+ service_items.append({
676+ 'id': str(service_item.unique_identifier),
677+ 'title': str(service_item.get_display_title()),
678+ 'plugin': str(service_item.name),
679+ 'notes': str(service_item.notes),
680+ 'selected': (service_item.unique_identifier == current_unique_identifier)
681+ })
682+ return service_items
683
684=== added directory 'openlp/core/api/http'
685=== added file 'openlp/core/api/http/__init__.py'
686--- openlp/core/api/http/__init__.py 1970-01-01 00:00:00 +0000
687+++ openlp/core/api/http/__init__.py 2017-08-13 07:11:15 +0000
688@@ -0,0 +1,110 @@
689+# -*- coding: utf-8 -*-
690+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
691+
692+###############################################################################
693+# OpenLP - Open Source Lyrics Projection #
694+# --------------------------------------------------------------------------- #
695+# Copyright (c) 2008-2017 OpenLP Developers #
696+# --------------------------------------------------------------------------- #
697+# This program is free software; you can redistribute it and/or modify it #
698+# under the terms of the GNU General Public License as published by the Free #
699+# Software Foundation; version 2 of the License. #
700+# #
701+# This program is distributed in the hope that it will be useful, but WITHOUT #
702+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
703+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
704+# more details. #
705+# #
706+# You should have received a copy of the GNU General Public License along #
707+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
708+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
709+###############################################################################
710+
711+import base64
712+from functools import wraps
713+from webob import Response
714+
715+from openlp.core.common.settings import Settings
716+from openlp.core.api.http.wsgiapp import WSGIApplication
717+from .errors import NotFound, ServerError, HttpError
718+
719+application = WSGIApplication('api')
720+
721+
722+def _route_from_url(url_prefix, url):
723+ """
724+ Create a route from the URL
725+ """
726+ url_prefix = '/{prefix}/'.format(prefix=url_prefix.strip('/'))
727+ if not url:
728+ url = url_prefix[:-1]
729+ else:
730+ url = url_prefix + url
731+ url = url.replace('//', '/')
732+ return url
733+
734+
735+def register_endpoint(end_point):
736+ """
737+ Register an endpoint with the app
738+ """
739+ for url, view_func, method in end_point.routes:
740+ # Set the view functions
741+ route = _route_from_url(end_point.url_prefix, url)
742+ application.add_route(route, view_func, method)
743+ # Add a static route if necessary
744+ if end_point.static_dir:
745+ static_route = _route_from_url(end_point.url_prefix, 'static')
746+ static_route += '(.*)'
747+ application.add_static_route(static_route, end_point.static_dir)
748+
749+
750+def check_auth(auth):
751+ """
752+ This function is called to check if a username password combination is valid.
753+
754+ :param auth: the authorisation object which needs to be tested
755+ :return Whether authentication have been successful
756+ """
757+ auth_code = "{user}:{password}".format(user=Settings().value('api/user id'),
758+ password=Settings().value('api/password'))
759+ try:
760+ auth_base = base64.b64encode(auth_code)
761+ except TypeError:
762+ auth_base = base64.b64encode(auth_code.encode()).decode()
763+ if auth[1] == auth_base:
764+ return True
765+ else:
766+ return False
767+
768+
769+def authenticate():
770+ """
771+ Sends a 401 response that enables basic auth to be triggered
772+ """
773+ resp = Response(status=401)
774+ resp.www_authenticate = 'Basic realm="OpenLP Login Required"'
775+ return resp
776+
777+
778+def requires_auth(f):
779+ """
780+ Decorates a function which needs to be authenticated before it can be used from the remote.
781+
782+ :param f: The function which has been wrapped
783+ :return: the called function or a request to authenticate
784+ """
785+ @wraps(f)
786+ def decorated(*args, **kwargs):
787+ if not Settings().value('api/authentication enabled'):
788+ return f(*args, **kwargs)
789+ req = args[0]
790+ if not hasattr(req, 'authorization'):
791+ return authenticate()
792+ else:
793+ auth = req.authorization
794+ if auth and check_auth(auth):
795+ return f(*args, **kwargs)
796+ else:
797+ return authenticate()
798+ return decorated
799
800=== added file 'openlp/core/api/http/endpoint.py'
801--- openlp/core/api/http/endpoint.py 1970-01-01 00:00:00 +0000
802+++ openlp/core/api/http/endpoint.py 2017-08-13 07:11:15 +0000
803@@ -0,0 +1,80 @@
804+# -*- coding: utf-8 -*-
805+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
806+
807+###############################################################################
808+# OpenLP - Open Source Lyrics Projection #
809+# --------------------------------------------------------------------------- #
810+# Copyright (c) 2008-2017 OpenLP Developers #
811+# --------------------------------------------------------------------------- #
812+# This program is free software; you can redistribute it and/or modify it #
813+# under the terms of the GNU General Public License as published by the Free #
814+# Software Foundation; version 2 of the License. #
815+# #
816+# This program is distributed in the hope that it will be useful, but WITHOUT #
817+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
818+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
819+# more details. #
820+# #
821+# You should have received a copy of the GNU General Public License along #
822+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
823+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
824+###############################################################################
825+"""
826+The Endpoint class, which provides plugins with a way to serve their own portion of the API
827+"""
828+
829+import os
830+
831+from openlp.core.common import AppLocation
832+from mako.template import Template
833+
834+
835+class Endpoint(object):
836+ """
837+ This is an endpoint for the HTTP API
838+ """
839+ def __init__(self, url_prefix, template_dir=None, static_dir=None, assets_dir=None):
840+ """
841+ Create an endpoint with a URL prefix
842+ """
843+ self.url_prefix = url_prefix
844+ self.static_dir = static_dir
845+ self.template_dir = template_dir
846+ if assets_dir:
847+ self.assets_dir = assets_dir
848+ else:
849+ self.assets_dir = None
850+ self.routes = []
851+
852+ def add_url_route(self, url, view_func, method):
853+ """
854+ Add a url route to the list of routes
855+ """
856+ self.routes.append((url, view_func, method))
857+
858+ def route(self, rule, method='GET'):
859+ """
860+ Set up a URL route
861+ """
862+ def decorator(func):
863+ """
864+ Make this a decorator
865+ """
866+ self.add_url_route(rule, func, method)
867+ return func
868+ return decorator
869+
870+ def render_template(self, filename, **kwargs):
871+ """
872+ Render a mako template
873+ """
874+ root = os.path.join(str(AppLocation.get_section_data_path('remotes')))
875+ if not self.template_dir:
876+ raise Exception('No template directory specified')
877+ path = os.path.join(root, self.template_dir, filename)
878+ # path = os.path.abspath(os.path.join(self.template_dir, filename))
879+ if self.static_dir:
880+ kwargs['static_url'] = '/{prefix}/static'.format(prefix=self.url_prefix)
881+ kwargs['static_url'] = kwargs['static_url'].replace('//', '/')
882+ kwargs['assets_url'] = '/assets'
883+ return Template(filename=path, input_encoding='utf-8').render(**kwargs)
884
885=== added file 'openlp/core/api/http/errors.py'
886--- openlp/core/api/http/errors.py 1970-01-01 00:00:00 +0000
887+++ openlp/core/api/http/errors.py 2017-08-13 07:11:15 +0000
888@@ -0,0 +1,65 @@
889+# -*- coding: utf-8 -*-
890+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
891+
892+###############################################################################
893+# OpenLP - Open Source Lyrics Projection #
894+# --------------------------------------------------------------------------- #
895+# Copyright (c) 2008-2017 OpenLP Developers #
896+# --------------------------------------------------------------------------- #
897+# This program is free software; you can redistribute it and/or modify it #
898+# under the terms of the GNU General Public License as published by the Free #
899+# Software Foundation; version 2 of the License. #
900+# #
901+# This program is distributed in the hope that it will be useful, but WITHOUT #
902+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
903+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
904+# more details. #
905+# #
906+# You should have received a copy of the GNU General Public License along #
907+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
908+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
909+###############################################################################
910+"""
911+HTTP Error classes
912+"""
913+
914+
915+class HttpError(Exception):
916+ """
917+ A base HTTP error (aka status code)
918+ """
919+ def __init__(self, status, message):
920+ """
921+ Initialise the exception
922+ """
923+ super(HttpError, self).__init__(message)
924+ self.status = status
925+ self.message = message
926+
927+ def to_response(self):
928+ """
929+ Convert this exception to a Response object
930+ """
931+ return self.message, self.status
932+
933+
934+class NotFound(HttpError):
935+ """
936+ A 404
937+ """
938+ def __init__(self):
939+ """
940+ Make this a 404
941+ """
942+ super(NotFound, self).__init__(404, 'Not Found')
943+
944+
945+class ServerError(HttpError):
946+ """
947+ A 500
948+ """
949+ def __init__(self):
950+ """
951+ Make this a 500
952+ """
953+ super(ServerError, self).__init__(500, 'Server Error')
954
955=== added file 'openlp/core/api/http/server.py'
956--- openlp/core/api/http/server.py 1970-01-01 00:00:00 +0000
957+++ openlp/core/api/http/server.py 2017-08-13 07:11:15 +0000
958@@ -0,0 +1,97 @@
959+# -*- coding: utf-8 -*-
960+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
961+
962+###############################################################################
963+# OpenLP - Open Source Lyrics Projection #
964+# --------------------------------------------------------------------------- #
965+# Copyright (c) 2008-2017 OpenLP Developers #
966+# --------------------------------------------------------------------------- #
967+# This program is free software; you can redistribute it and/or modify it #
968+# under the terms of the GNU General Public License as published by the Free #
969+# Software Foundation; version 2 of the License. #
970+# #
971+# This program is distributed in the hope that it will be useful, but WITHOUT #
972+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
973+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
974+# more details. #
975+# #
976+# You should have received a copy of the GNU General Public License along #
977+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
978+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
979+###############################################################################
980+
981+"""
982+The :mod:`http` module contains the API web server. This is a lightweight web server used by remotes to interact
983+with OpenLP. It uses JSON to communicate with the remotes.
984+"""
985+
986+import logging
987+
988+from PyQt5 import QtCore
989+from waitress import serve
990+
991+from openlp.core.api.http import register_endpoint
992+from openlp.core.api.http import application
993+from openlp.core.common import RegistryMixin, RegistryProperties, OpenLPMixin, Settings, Registry
994+from openlp.core.api.poll import Poller
995+from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint
996+from openlp.core.api.endpoint.core import chords_endpoint, stage_endpoint, blank_endpoint, main_endpoint
997+from openlp.core.api.endpoint.service import service_endpoint, api_service_endpoint
998+
999+log = logging.getLogger(__name__)
1000+
1001+
1002+class HttpWorker(QtCore.QObject):
1003+ """
1004+ A special Qt thread class to allow the HTTP server to run at the same time as the UI.
1005+ """
1006+ def __init__(self):
1007+ """
1008+ Constructor for the thread class.
1009+
1010+ :param server: The http server class.
1011+ """
1012+ super(HttpWorker, self).__init__()
1013+
1014+ def run(self):
1015+ """
1016+ Run the thread.
1017+ """
1018+ address = Settings().value('api/ip address')
1019+ port = Settings().value('api/port')
1020+ serve(application, host=address, port=port)
1021+
1022+ def stop(self):
1023+ pass
1024+
1025+
1026+class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin):
1027+ """
1028+ Wrapper round a server instance
1029+ """
1030+ def __init__(self, parent=None):
1031+ """
1032+ Initialise the http server, and start the http server
1033+ """
1034+ super(HttpServer, self).__init__(parent)
1035+ self.worker = HttpWorker()
1036+ self.thread = QtCore.QThread()
1037+ self.worker.moveToThread(self.thread)
1038+ self.thread.started.connect(self.worker.run)
1039+ self.thread.start()
1040+
1041+ def bootstrap_post_set_up(self):
1042+ """
1043+ Register the poll return service and start the servers.
1044+ """
1045+ self.poller = Poller()
1046+ Registry().register('poller', self.poller)
1047+ application.initialise()
1048+ register_endpoint(controller_endpoint)
1049+ register_endpoint(api_controller_endpoint)
1050+ register_endpoint(chords_endpoint)
1051+ register_endpoint(stage_endpoint)
1052+ register_endpoint(blank_endpoint)
1053+ register_endpoint(main_endpoint)
1054+ register_endpoint(service_endpoint)
1055+ register_endpoint(api_service_endpoint)
1056
1057=== added file 'openlp/core/api/http/wsgiapp.py'
1058--- openlp/core/api/http/wsgiapp.py 1970-01-01 00:00:00 +0000
1059+++ openlp/core/api/http/wsgiapp.py 2017-08-13 07:11:15 +0000
1060@@ -0,0 +1,181 @@
1061+# -*- coding: utf-8 -*-
1062+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
1063+# pylint: disable=logging-format-interpolation
1064+
1065+###############################################################################
1066+# OpenLP - Open Source Lyrics Projection #
1067+# --------------------------------------------------------------------------- #
1068+# Copyright (c) 2008-2017 OpenLP Developers #
1069+# --------------------------------------------------------------------------- #
1070+# This program is free software; you can redistribute it and/or modify it #
1071+# under the terms of the GNU General Public License as published by the Free #
1072+# Software Foundation; version 2 of the License. #
1073+# #
1074+# This program is distributed in the hope that it will be useful, but WITHOUT #
1075+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
1076+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
1077+# more details. #
1078+# #
1079+# You should have received a copy of the GNU General Public License along #
1080+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
1081+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
1082+###############################################################################
1083+"""
1084+App stuff
1085+"""
1086+import json
1087+import logging
1088+import os
1089+import re
1090+
1091+from webob import Request, Response
1092+from webob.static import DirectoryApp
1093+
1094+from openlp.core.common import AppLocation
1095+from openlp.core.api.http.errors import HttpError, NotFound, ServerError
1096+
1097+
1098+ARGS_REGEX = re.compile(r'''\{(\w+)(?::([^}]+))?\}''', re.VERBOSE)
1099+
1100+log = logging.getLogger(__name__)
1101+
1102+
1103+def _route_to_regex(route):
1104+ """
1105+ Convert a route to a regular expression
1106+
1107+ For example:
1108+
1109+ 'songs/{song_id}' becomes 'songs/(?P<song_id>[^/]+)'
1110+
1111+ and
1112+
1113+ 'songs/{song_id:\d+}' becomes 'songs/(?P<song_id>\d+)'
1114+
1115+ """
1116+ route_regex = ''
1117+ last_pos = 0
1118+ for match in ARGS_REGEX.finditer(route):
1119+ route_regex += re.escape(route[last_pos:match.start()])
1120+ arg_name = match.group(1)
1121+ expr = match.group(2) or '[^/]+'
1122+ expr = '(?P<%s>%s)' % (arg_name, expr)
1123+ route_regex += expr
1124+ last_pos = match.end()
1125+ route_regex += re.escape(route[last_pos:])
1126+ route_regex = '^%s$' % route_regex
1127+ return route_regex
1128+
1129+
1130+def _make_response(view_result):
1131+ """
1132+ Create a Response object from response
1133+ """
1134+ if isinstance(view_result, Response):
1135+ return view_result
1136+ elif isinstance(view_result, tuple):
1137+ content_type = 'text/html'
1138+ body = view_result[0]
1139+ if isinstance(body, dict):
1140+ content_type = 'application/json'
1141+ body = json.dumps(body)
1142+ response = Response(body=body, status=view_result[1],
1143+ content_type=content_type, charset='utf8')
1144+ if len(view_result) >= 3:
1145+ response.headers.update(view_result[2])
1146+ return response
1147+ elif isinstance(view_result, dict):
1148+ return Response(body=json.dumps(view_result), status=200,
1149+ content_type='application/json', charset='utf8')
1150+ elif isinstance(view_result, str):
1151+ return Response(body=view_result, status=200,
1152+ content_type='text/html', charset='utf8')
1153+
1154+
1155+def _handle_exception(error):
1156+ """
1157+ Handle exceptions
1158+ """
1159+ log.exception(error)
1160+ if isinstance(error, HttpError):
1161+ return error.to_response()
1162+ else:
1163+ return ServerError().to_response()
1164+
1165+
1166+class WSGIApplication(object):
1167+ """
1168+ This is the core of the API, the WSGI app
1169+ """
1170+ def __init__(self, name):
1171+ """
1172+ Create the app object
1173+ """
1174+ self.name = name
1175+ self.static_routes = {}
1176+ self.route_map = {}
1177+
1178+ def initialise(self):
1179+ """
1180+ Set up generic roots for the whole application
1181+ :return: None
1182+ """
1183+ self.add_static_route('/assets(.*)', '')
1184+ self.add_static_route('/images(.*)', '')
1185+ pass
1186+
1187+ def add_route(self, route, view_func, method):
1188+ """
1189+ Add a route
1190+ """
1191+ route_regex = _route_to_regex(route)
1192+ if route_regex not in self.route_map:
1193+ self.route_map[route_regex] = {}
1194+ self.route_map[route_regex][method.upper()] = view_func
1195+
1196+ def add_static_route(self, route, static_dir):
1197+ """
1198+ Add a static directory as a route
1199+ """
1200+ if route not in self.static_routes:
1201+ root = os.path.join(str(AppLocation.get_section_data_path('remotes')))
1202+ self.static_routes[route] = DirectoryApp(os.path.abspath(os.path.join(root, static_dir)))
1203+
1204+ def dispatch(self, request):
1205+ """
1206+ Find the appropriate URL and run the view function
1207+ """
1208+ # If not a static route, try the views
1209+ for route, views in self.route_map.items():
1210+ match = re.match(route, request.path)
1211+ if match and request.method.upper() in views:
1212+ kwargs = match.groupdict()
1213+ log.debug('Found {method} {url}'.format(method=request.method, url=request.path))
1214+ view_func = views[request.method.upper()]
1215+ return _make_response(view_func(request, **kwargs))
1216+ # Look to see if this is a static file request
1217+ for route, static_app in self.static_routes.items():
1218+ if re.match(route, request.path):
1219+ return request.get_response(static_app)
1220+ log.error('URL {url} - Not found'.format(url=request.path))
1221+ raise NotFound()
1222+
1223+ def wsgi_app(self, environ, start_response):
1224+ """
1225+ The actual WSGI application.
1226+ """
1227+ request = Request(environ)
1228+ try:
1229+ response = self.dispatch(request)
1230+ except Exception as e:
1231+ response = _make_response(_handle_exception(e))
1232+ response.headers.add("cache-control", "no-cache, no-store, must-revalidate")
1233+ response.headers.add("pragma", "no-cache")
1234+ response.headers.add("expires", "0")
1235+ return response(environ, start_response)
1236+
1237+ def __call__(self, environ, start_response):
1238+ """
1239+ Shortcut for wsgi_app.
1240+ """
1241+ return self.wsgi_app(environ, start_response)
1242
1243=== added file 'openlp/core/api/poll.py'
1244--- openlp/core/api/poll.py 1970-01-01 00:00:00 +0000
1245+++ openlp/core/api/poll.py 2017-08-13 07:11:15 +0000
1246@@ -0,0 +1,130 @@
1247+# -*- coding: utf-8 -*-
1248+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
1249+
1250+###############################################################################
1251+# OpenLP - Open Source Lyrics Projection #
1252+# --------------------------------------------------------------------------- #
1253+# Copyright (c) 2008-2017 OpenLP Developers #
1254+# --------------------------------------------------------------------------- #
1255+# This program is free software; you can redistribute it and/or modify it #
1256+# under the terms of the GNU General Public License as published by the Free #
1257+# Software Foundation; version 2 of the License. #
1258+# #
1259+# This program is distributed in the hope that it will be useful, but WITHOUT #
1260+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
1261+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
1262+# more details. #
1263+# #
1264+# You should have received a copy of the GNU General Public License along #
1265+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
1266+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
1267+###############################################################################
1268+
1269+import json
1270+
1271+from openlp.core.common import RegistryProperties, Settings
1272+from openlp.core.common.httputils import get_web_page
1273+
1274+
1275+class Poller(RegistryProperties):
1276+ """
1277+ Accessed by the web layer to get status type information from the application
1278+ """
1279+ def __init__(self):
1280+ """
1281+ Constructor for the poll builder class.
1282+ """
1283+ super(Poller, self).__init__()
1284+ self.live_cache = None
1285+ self.stage_cache = None
1286+ self.chords_cache = None
1287+
1288+ def raw_poll(self):
1289+ return {
1290+ 'service': self.service_manager.service_id,
1291+ 'slide': self.live_controller.selected_row or 0,
1292+ 'item': self.live_controller.service_item.unique_identifier if self.live_controller.service_item else '',
1293+ 'twelve': Settings().value('api/twelve hour'),
1294+ 'blank': self.live_controller.blank_screen.isChecked(),
1295+ 'theme': self.live_controller.theme_screen.isChecked(),
1296+ 'display': self.live_controller.desktop_screen.isChecked(),
1297+ 'version': 3,
1298+ 'isSecure': Settings().value('api/authentication enabled'),
1299+ 'isAuthorised': False,
1300+ 'chordNotation': Settings().value('songs/chord notation'),
1301+ 'isStagedActive': self.is_stage_active(),
1302+ 'isLiveActive': self.is_live_active(),
1303+ 'isChordsActive': self.is_chords_active()
1304+ }
1305+
1306+ def poll(self):
1307+ """
1308+ Poll OpenLP to determine the current slide number and item name.
1309+ """
1310+ return {'results': self.raw_poll()}
1311+
1312+ def main_poll(self):
1313+ """
1314+ Poll OpenLP to determine the current slide count.
1315+ """
1316+ result = {
1317+ 'slide_count': self.live_controller.slide_count
1318+ }
1319+ return json.dumps({'results': result}).encode()
1320+
1321+ def reset_cache(self):
1322+ """
1323+ Reset the caches as the web has changed
1324+ :return:
1325+ """
1326+ self.stage_cache = None
1327+ self.live_cache = None
1328+ self.chords.cache = None
1329+
1330+ def is_stage_active(self):
1331+ """
1332+ Is stage active - call it and see but only once
1333+ :return: if stage is active or not
1334+ """
1335+ if self.stage_cache is None:
1336+ try:
1337+ page = get_web_page("http://localhost:4316/stage")
1338+ except:
1339+ page = None
1340+ if page:
1341+ self.stage_cache = True
1342+ else:
1343+ self.stage_cache = False
1344+ return self.stage_cache
1345+
1346+ def is_live_active(self):
1347+ """
1348+ Is main active - call it and see but only once
1349+ :return: if live is active or not
1350+ """
1351+ if self.live_cache is None:
1352+ try:
1353+ page = get_web_page("http://localhost:4316/main")
1354+ except:
1355+ page = None
1356+ if page:
1357+ self.live_cache = True
1358+ else:
1359+ self.live_cache = False
1360+ return self.live_cache
1361+
1362+ def is_chords_active(self):
1363+ """
1364+ Is chords active - call it and see but only once
1365+ :return: if live is active or not
1366+ """
1367+ if self.chords_cache is None:
1368+ try:
1369+ page = get_web_page("http://localhost:4316/chords")
1370+ except:
1371+ page = None
1372+ if page:
1373+ self.chords_cache = True
1374+ else:
1375+ self.chords_cache = False
1376+ return self.chords_cache
1377
1378=== added file 'openlp/core/api/tab.py'
1379--- openlp/core/api/tab.py 1970-01-01 00:00:00 +0000
1380+++ openlp/core/api/tab.py 2017-08-13 07:11:15 +0000
1381@@ -0,0 +1,316 @@
1382+# -*- coding: utf-8 -*-
1383+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
1384+
1385+###############################################################################
1386+# OpenLP - Open Source Lyrics Projection #
1387+# --------------------------------------------------------------------------- #
1388+# Copyright (c) 2008-2017 OpenLP Developers #
1389+# --------------------------------------------------------------------------- #
1390+# This program is free software; you can redistribute it and/or modify it #
1391+# under the terms of the GNU General Public License as published by the Free #
1392+# Software Foundation; version 2 of the License. #
1393+# #
1394+# This program is distributed in the hope that it will be useful, but WITHOUT #
1395+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
1396+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
1397+# more details. #
1398+# #
1399+# You should have received a copy of the GNU General Public License along #
1400+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
1401+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
1402+###############################################################################
1403+
1404+from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets
1405+
1406+from openlp.core.common import UiStrings, Registry, Settings, translate
1407+from openlp.core.lib import SettingsTab
1408+
1409+ZERO_URL = '0.0.0.0'
1410+
1411+
1412+class ApiTab(SettingsTab):
1413+ """
1414+ RemoteTab is the Remotes settings tab in the settings dialog.
1415+ """
1416+ def __init__(self, parent):
1417+ self.icon_path = ':/plugins/plugin_remote.png'
1418+ advanced_translated = translate('OpenLP.AdvancedTab', 'Advanced')
1419+ super(ApiTab, self).__init__(parent, 'api', advanced_translated)
1420+ self.define_main_window_icon()
1421+ self.generate_icon()
1422+
1423+ def setupUi(self):
1424+ self.setObjectName('ApiTab')
1425+ super(ApiTab, self).setupUi()
1426+ self.server_settings_group_box = QtWidgets.QGroupBox(self.left_column)
1427+ self.server_settings_group_box.setObjectName('server_settings_group_box')
1428+ self.server_settings_layout = QtWidgets.QFormLayout(self.server_settings_group_box)
1429+ self.server_settings_layout.setObjectName('server_settings_layout')
1430+ self.address_label = QtWidgets.QLabel(self.server_settings_group_box)
1431+ self.address_label.setObjectName('address_label')
1432+ self.address_edit = QtWidgets.QLineEdit(self.server_settings_group_box)
1433+ self.address_edit.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
1434+ self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'),
1435+ self))
1436+ self.address_edit.setObjectName('address_edit')
1437+ self.server_settings_layout.addRow(self.address_label, self.address_edit)
1438+ self.twelve_hour_check_box = QtWidgets.QCheckBox(self.server_settings_group_box)
1439+ self.twelve_hour_check_box.setObjectName('twelve_hour_check_box')
1440+ self.server_settings_layout.addRow(self.twelve_hour_check_box)
1441+ self.thumbnails_check_box = QtWidgets.QCheckBox(self.server_settings_group_box)
1442+ self.thumbnails_check_box.setObjectName('thumbnails_check_box')
1443+ self.server_settings_layout.addRow(self.thumbnails_check_box)
1444+ self.left_layout.addWidget(self.server_settings_group_box)
1445+ self.http_settings_group_box = QtWidgets.QGroupBox(self.left_column)
1446+ self.http_settings_group_box.setObjectName('http_settings_group_box')
1447+ self.http_setting_layout = QtWidgets.QFormLayout(self.http_settings_group_box)
1448+ self.http_setting_layout.setObjectName('http_setting_layout')
1449+ self.port_label = QtWidgets.QLabel(self.http_settings_group_box)
1450+ self.port_label.setObjectName('port_label')
1451+ self.port_spin_box = QtWidgets.QLabel(self.http_settings_group_box)
1452+ self.port_spin_box.setObjectName('port_spin_box')
1453+ self.http_setting_layout.addRow(self.port_label, self.port_spin_box)
1454+ self.remote_url_label = QtWidgets.QLabel(self.http_settings_group_box)
1455+ self.remote_url_label.setObjectName('remote_url_label')
1456+ self.remote_url = QtWidgets.QLabel(self.http_settings_group_box)
1457+ self.remote_url.setObjectName('remote_url')
1458+ self.remote_url.setOpenExternalLinks(True)
1459+ self.http_setting_layout.addRow(self.remote_url_label, self.remote_url)
1460+ self.stage_url_label = QtWidgets.QLabel(self.http_settings_group_box)
1461+ self.stage_url_label.setObjectName('stage_url_label')
1462+ self.stage_url = QtWidgets.QLabel(self.http_settings_group_box)
1463+ self.stage_url.setObjectName('stage_url')
1464+ self.stage_url.setOpenExternalLinks(True)
1465+ self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
1466+ self.chords_url_label = QtWidgets.QLabel(self.http_settings_group_box)
1467+ self.chords_url_label.setObjectName('chords_url_label')
1468+ self.chords_url = QtWidgets.QLabel(self.http_settings_group_box)
1469+ self.chords_url.setObjectName('chords_url')
1470+ self.chords_url.setOpenExternalLinks(True)
1471+ self.http_setting_layout.addRow(self.chords_url_label, self.chords_url)
1472+ self.live_url_label = QtWidgets.QLabel(self.http_settings_group_box)
1473+ self.live_url_label.setObjectName('live_url_label')
1474+ self.live_url = QtWidgets.QLabel(self.http_settings_group_box)
1475+ self.live_url.setObjectName('live_url')
1476+ self.live_url.setOpenExternalLinks(True)
1477+ self.http_setting_layout.addRow(self.live_url_label, self.live_url)
1478+ self.left_layout.addWidget(self.http_settings_group_box)
1479+ self.user_login_group_box = QtWidgets.QGroupBox(self.left_column)
1480+ self.user_login_group_box.setCheckable(True)
1481+ self.user_login_group_box.setChecked(False)
1482+ self.user_login_group_box.setObjectName('user_login_group_box')
1483+ self.user_login_layout = QtWidgets.QFormLayout(self.user_login_group_box)
1484+ self.user_login_layout.setObjectName('user_login_layout')
1485+ self.user_id_label = QtWidgets.QLabel(self.user_login_group_box)
1486+ self.user_id_label.setObjectName('user_id_label')
1487+ self.user_id = QtWidgets.QLineEdit(self.user_login_group_box)
1488+ self.user_id.setObjectName('user_id')
1489+ self.user_login_layout.addRow(self.user_id_label, self.user_id)
1490+ self.password_label = QtWidgets.QLabel(self.user_login_group_box)
1491+ self.password_label.setObjectName('password_label')
1492+ self.password = QtWidgets.QLineEdit(self.user_login_group_box)
1493+ self.password.setObjectName('password')
1494+ self.user_login_layout.addRow(self.password_label, self.password)
1495+ self.left_layout.addWidget(self.user_login_group_box)
1496+ self.update_site_group_box = QtWidgets.QGroupBox(self.left_column)
1497+ self.update_site_group_box.setCheckable(True)
1498+ self.update_site_group_box.setChecked(False)
1499+ self.update_site_group_box.setObjectName('update_site_group_box')
1500+ self.update_site_layout = QtWidgets.QFormLayout(self.update_site_group_box)
1501+ self.update_site_layout.setObjectName('update_site_layout')
1502+ self.current_version_label = QtWidgets.QLabel(self.update_site_group_box)
1503+ self.current_version_label.setObjectName('current_version_label')
1504+ self.current_version_value = QtWidgets.QLabel(self.update_site_group_box)
1505+ self.current_version_value.setObjectName('current_version_value')
1506+ self.update_site_layout.addRow(self.current_version_label, self.current_version_value)
1507+ self.master_version_label = QtWidgets.QLabel(self.update_site_group_box)
1508+ self.master_version_label.setObjectName('master_version_label')
1509+ self.master_version_value = QtWidgets.QLabel(self.update_site_group_box)
1510+ self.master_version_value.setObjectName('master_version_value')
1511+ self.update_site_layout.addRow(self.master_version_label, self.master_version_value)
1512+ self.left_layout.addWidget(self.update_site_group_box)
1513+ self.android_app_group_box = QtWidgets.QGroupBox(self.right_column)
1514+ self.android_app_group_box.setObjectName('android_app_group_box')
1515+ self.right_layout.addWidget(self.android_app_group_box)
1516+ self.android_qr_layout = QtWidgets.QVBoxLayout(self.android_app_group_box)
1517+ self.android_qr_layout.setObjectName('android_qr_layout')
1518+ self.android_qr_code_label = QtWidgets.QLabel(self.android_app_group_box)
1519+ self.android_qr_code_label.setPixmap(QtGui.QPixmap(':/remotes/android_app_qr.png'))
1520+ self.android_qr_code_label.setAlignment(QtCore.Qt.AlignCenter)
1521+ self.android_qr_code_label.setObjectName('android_qr_code_label')
1522+ self.android_qr_layout.addWidget(self.android_qr_code_label)
1523+ self.android_qr_description_label = QtWidgets.QLabel(self.android_app_group_box)
1524+ self.android_qr_description_label.setObjectName('android_qr_description_label')
1525+ self.android_qr_description_label.setOpenExternalLinks(True)
1526+ self.android_qr_description_label.setWordWrap(True)
1527+ self.android_qr_layout.addWidget(self.android_qr_description_label)
1528+ self.ios_app_group_box = QtWidgets.QGroupBox(self.right_column)
1529+ self.ios_app_group_box.setObjectName('ios_app_group_box')
1530+ self.right_layout.addWidget(self.ios_app_group_box)
1531+ self.ios_qr_layout = QtWidgets.QVBoxLayout(self.ios_app_group_box)
1532+ self.ios_qr_layout.setObjectName('ios_qr_layout')
1533+ self.ios_qr_code_label = QtWidgets.QLabel(self.ios_app_group_box)
1534+ self.ios_qr_code_label.setPixmap(QtGui.QPixmap(':/remotes/ios_app_qr.png'))
1535+ self.ios_qr_code_label.setAlignment(QtCore.Qt.AlignCenter)
1536+ self.ios_qr_code_label.setObjectName('ios_qr_code_label')
1537+ self.ios_qr_layout.addWidget(self.ios_qr_code_label)
1538+ self.ios_qr_description_label = QtWidgets.QLabel(self.ios_app_group_box)
1539+ self.ios_qr_description_label.setObjectName('ios_qr_description_label')
1540+ self.ios_qr_description_label.setOpenExternalLinks(True)
1541+ self.ios_qr_description_label.setWordWrap(True)
1542+ self.ios_qr_layout.addWidget(self.ios_qr_description_label)
1543+ self.left_layout.addStretch()
1544+ self.right_layout.addStretch()
1545+ self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed)
1546+ self.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed)
1547+ self.address_edit.textChanged.connect(self.set_urls)
1548+
1549+ def define_main_window_icon(self):
1550+ """
1551+ Define an icon on the main window to show the state of the server
1552+ :return:
1553+ """
1554+ self.remote_server_icon = QtWidgets.QLabel(self.main_window.status_bar)
1555+ size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
1556+ size_policy.setHorizontalStretch(0)
1557+ size_policy.setVerticalStretch(0)
1558+ size_policy.setHeightForWidth(self.remote_server_icon.sizePolicy().hasHeightForWidth())
1559+ self.remote_server_icon.setSizePolicy(size_policy)
1560+ self.remote_server_icon.setFrameShadow(QtWidgets.QFrame.Plain)
1561+ self.remote_server_icon.setLineWidth(1)
1562+ self.remote_server_icon.setScaledContents(True)
1563+ self.remote_server_icon.setFixedSize(20, 20)
1564+ self.remote_server_icon.setObjectName('remote_server_icon')
1565+ self.main_window.status_bar.insertPermanentWidget(2, self.remote_server_icon)
1566+
1567+ def retranslateUi(self):
1568+ self.tab_title_visible = translate('RemotePlugin.RemoteTab', 'Remote Interface')
1569+ self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
1570+ self.address_label.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:'))
1571+ self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
1572+ self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
1573+ self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
1574+ self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
1575+ self.chords_url_label.setText(translate('RemotePlugin.RemoteTab', 'Chords view URL:'))
1576+ self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
1577+ self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
1578+ 'Show thumbnails of non-text slides in remote and stage view.'))
1579+ self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
1580+ self.android_qr_description_label.setText(
1581+ translate('RemotePlugin.RemoteTab',
1582+ 'Scan the QR code or click <a href="{qr}">download</a> to install the Android app from Google '
1583+ 'Play.').format(qr='https://play.google.com/store/apps/details?id=org.openlp.android2'))
1584+ self.ios_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'iOS App'))
1585+ self.ios_qr_description_label.setText(
1586+ translate('RemotePlugin.RemoteTab',
1587+ 'Scan the QR code or click <a href="{qr}">download</a> to install the iOS app from the App '
1588+ 'Store.').format(qr='https://itunes.apple.com/app/id1096218725'))
1589+ self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
1590+ self.aa = UiStrings()
1591+ self.update_site_group_box.setTitle(UiStrings().WebDownloadText)
1592+ self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
1593+ self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
1594+ self.current_version_label.setText(translate('RemotePlugin.RemoteTab', 'Current Version number:'))
1595+ self.master_version_label.setText(translate('RemotePlugin.RemoteTab', 'Latest Version number:'))
1596+
1597+ def set_urls(self):
1598+ """
1599+ Update the display based on the data input on the screen
1600+ """
1601+ ip_address = self.get_ip_address(self.address_edit.text())
1602+ http_url = 'http://{url}:{text}/'.format(url=ip_address, text=self.port_spin_box.text())
1603+ self.remote_url.setText('<a href="{url}">{url}</a>'.format(url=http_url))
1604+ http_url_temp = http_url + 'stage'
1605+ self.stage_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
1606+ http_url_temp = http_url + 'main'
1607+ self.live_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
1608+
1609+ @staticmethod
1610+ def get_ip_address(ip_address):
1611+ """
1612+ returns the IP address in dependency of the passed address
1613+ ip_address == 0.0.0.0: return the IP address of the first valid interface
1614+ else: return ip_address
1615+ """
1616+ if ip_address == ZERO_URL:
1617+ interfaces = QtNetwork.QNetworkInterface.allInterfaces()
1618+ for interface in interfaces:
1619+ if not interface.isValid():
1620+ continue
1621+ if not (interface.flags() & (QtNetwork.QNetworkInterface.IsUp | QtNetwork.QNetworkInterface.IsRunning)):
1622+ continue
1623+ for address in interface.addressEntries():
1624+ ip = address.ip()
1625+ if ip.protocol() == QtNetwork.QAbstractSocket.IPv4Protocol and \
1626+ ip != QtNetwork.QHostAddress.LocalHost:
1627+ return ip.toString()
1628+ return ip_address
1629+
1630+ def load(self):
1631+ """
1632+ Load the configuration and update the server configuration if necessary
1633+ """
1634+ self.port_spin_box.setText(str(Settings().value(self.settings_section + '/port')))
1635+ self.address_edit.setText(Settings().value(self.settings_section + '/ip address'))
1636+ self.twelve_hour = Settings().value(self.settings_section + '/twelve hour')
1637+ self.twelve_hour_check_box.setChecked(self.twelve_hour)
1638+ self.thumbnails = Settings().value(self.settings_section + '/thumbnails')
1639+ self.thumbnails_check_box.setChecked(self.thumbnails)
1640+ self.user_login_group_box.setChecked(Settings().value(self.settings_section + '/authentication enabled'))
1641+ self.user_id.setText(Settings().value(self.settings_section + '/user id'))
1642+ self.password.setText(Settings().value(self.settings_section + '/password'))
1643+ self.current_version_value.setText(Settings().value('remotes/download version'))
1644+ self.master_version_value.setText(Registry().get_flag('website_version'))
1645+ if self.master_version_value.text() == self.current_version_value.text():
1646+ self.update_site_group_box.setEnabled(False)
1647+ self.set_urls()
1648+
1649+ def save(self):
1650+ """
1651+ Save the configuration and update the server configuration if necessary
1652+ """
1653+ if Settings().value(self.settings_section + '/ip address') != self.address_edit.text():
1654+ self.settings_form.register_post_process('remotes_config_updated')
1655+ Settings().setValue(self.settings_section + '/ip address', self.address_edit.text())
1656+ Settings().setValue(self.settings_section + '/twelve hour', self.twelve_hour)
1657+ Settings().setValue(self.settings_section + '/thumbnails', self.thumbnails)
1658+ Settings().setValue(self.settings_section + '/authentication enabled', self.user_login_group_box.isChecked())
1659+ Settings().setValue(self.settings_section + '/user id', self.user_id.text())
1660+ Settings().setValue(self.settings_section + '/password', self.password.text())
1661+ self.generate_icon()
1662+ if self.update_site_group_box.isChecked():
1663+ self.settings_form.register_post_process('download_website')
1664+
1665+ def on_twelve_hour_check_box_changed(self, check_state):
1666+ """
1667+ Toggle the 12 hour check box.
1668+ """
1669+ self.twelve_hour = False
1670+ # we have a set value convert to True/False
1671+ if check_state == QtCore.Qt.Checked:
1672+ self.twelve_hour = True
1673+
1674+ def on_thumbnails_check_box_changed(self, check_state):
1675+ """
1676+ Toggle the thumbnail check box.
1677+ """
1678+ self.thumbnails = False
1679+ # we have a set value convert to True/False
1680+ if check_state == QtCore.Qt.Checked:
1681+ self.thumbnails = True
1682+
1683+ def generate_icon(self):
1684+ """
1685+ Generate icon for main window
1686+ """
1687+ self.remote_server_icon.hide()
1688+ icon = QtGui.QImage(':/remote/network_server.png')
1689+ icon = icon.scaled(80, 80, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
1690+ if Settings().value(self.settings_section + '/authentication enabled'):
1691+ overlay = QtGui.QImage(':/remote/network_auth.png')
1692+ overlay = overlay.scaled(60, 60, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
1693+ painter = QtGui.QPainter(icon)
1694+ painter.drawImage(20, 0, overlay)
1695+ painter.end()
1696+ self.remote_server_icon.setPixmap(QtGui.QPixmap.fromImage(icon))
1697+ self.remote_server_icon.show()
1698
1699=== added file 'openlp/core/api/websockets.py'
1700--- openlp/core/api/websockets.py 1970-01-01 00:00:00 +0000
1701+++ openlp/core/api/websockets.py 2017-08-13 07:11:15 +0000
1702@@ -0,0 +1,142 @@
1703+# -*- coding: utf-8 -*-
1704+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
1705+
1706+###############################################################################
1707+# OpenLP - Open Source Lyrics Projection #
1708+# --------------------------------------------------------------------------- #
1709+# Copyright (c) 2008-2017 OpenLP Developers #
1710+# --------------------------------------------------------------------------- #
1711+# This program is free software; you can redistribute it and/or modify it #
1712+# under the terms of the GNU General Public License as published by the Free #
1713+# Software Foundation; version 2 of the License. #
1714+# #
1715+# This program is distributed in the hope that it will be useful, but WITHOUT #
1716+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
1717+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
1718+# more details. #
1719+# #
1720+# You should have received a copy of the GNU General Public License along #
1721+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
1722+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
1723+###############################################################################
1724+
1725+"""
1726+The :mod:`http` module contains the API web server. This is a lightweight web server used by remotes to interact
1727+with OpenLP. It uses JSON to communicate with the remotes.
1728+"""
1729+
1730+import asyncio
1731+import websockets
1732+import json
1733+import logging
1734+import time
1735+
1736+from PyQt5 import QtCore
1737+
1738+from openlp.core.common import Settings, RegistryProperties, OpenLPMixin, Registry
1739+
1740+log = logging.getLogger(__name__)
1741+
1742+
1743+class WebSocketWorker(QtCore.QObject):
1744+ """
1745+ A special Qt thread class to allow the WebSockets server to run at the same time as the UI.
1746+ """
1747+ def __init__(self, server):
1748+ """
1749+ Constructor for the thread class.
1750+
1751+ :param server: The http server class.
1752+ """
1753+ self.ws_server = server
1754+ super(WebSocketWorker, self).__init__()
1755+
1756+ def run(self):
1757+ """
1758+ Run the thread.
1759+ """
1760+ self.ws_server.start_server()
1761+
1762+ def stop(self):
1763+ self.ws_server.stop = True
1764+
1765+
1766+class WebSocketServer(RegistryProperties, OpenLPMixin):
1767+ """
1768+ Wrapper round a server instance
1769+ """
1770+ def __init__(self):
1771+ """
1772+ Initialise and start the WebSockets server
1773+ """
1774+ super(WebSocketServer, self).__init__()
1775+ self.settings_section = 'api'
1776+ self.worker = WebSocketWorker(self)
1777+ self.thread = QtCore.QThread()
1778+ self.worker.moveToThread(self.thread)
1779+ self.thread.started.connect(self.worker.run)
1780+ self.thread.start()
1781+
1782+ def start_server(self):
1783+ """
1784+ Start the correct server and save the handler
1785+ """
1786+ address = Settings().value(self.settings_section + '/ip address')
1787+ port = Settings().value(self.settings_section + '/websocket port')
1788+ self.start_websocket_instance(address, port)
1789+ # If web socket server start listening
1790+ if hasattr(self, 'ws_server') and self.ws_server:
1791+ event_loop = asyncio.new_event_loop()
1792+ asyncio.set_event_loop(event_loop)
1793+ event_loop.run_until_complete(self.ws_server)
1794+ event_loop.run_forever()
1795+ else:
1796+ log.debug('Failed to start ws server on port {port}'.format(port=port))
1797+
1798+ def start_websocket_instance(self, address, port):
1799+ """
1800+ Start the server
1801+
1802+ :param address: The server address
1803+ :param port: The run port
1804+ """
1805+ loop = 1
1806+ while loop < 4:
1807+ try:
1808+ self.ws_server = websockets.serve(self.handle_websocket, address, port)
1809+ log.debug("Web Socket Server started for class {address} {port}".format(address=address, port=port))
1810+ break
1811+ except Exception as e:
1812+ log.error('Failed to start ws server {why}'.format(why=e))
1813+ loop += 1
1814+ time.sleep(0.1)
1815+
1816+ @staticmethod
1817+ async def handle_websocket(request, path):
1818+ """
1819+ Handle web socket requests and return the poll information.
1820+ Check ever 0.2 seconds to get the latest position and send if changed.
1821+ Only gets triggered when 1st client attaches
1822+
1823+ :param request: request from client
1824+ :param path: determines the endpoints supported
1825+ :return:
1826+ """
1827+ log.debug("web socket handler registered with client")
1828+ previous_poll = None
1829+ previous_main_poll = None
1830+ poller = Registry().get('poller')
1831+ if path == '/state':
1832+ while True:
1833+ current_poll = poller.poll()
1834+ if current_poll != previous_poll:
1835+ await request.send(json.dumps(current_poll).encode())
1836+ previous_poll = current_poll
1837+ await asyncio.sleep(0.2)
1838+ elif path == '/live_changed':
1839+ while True:
1840+ main_poll = poller.main_poll()
1841+ if main_poll != previous_main_poll:
1842+ await request.send(main_poll)
1843+ previous_main_poll = main_poll
1844+ await asyncio.sleep(0.2)
1845
1846=== modified file 'openlp/core/common/httputils.py'
1847--- openlp/core/common/httputils.py 2017-02-26 21:14:49 +0000
1848+++ openlp/core/common/httputils.py 2017-08-13 07:11:15 +0000
1849@@ -25,8 +25,10 @@
1850 import hashlib
1851 import logging
1852 import os
1853+import platform
1854 import socket
1855 import sys
1856+import subprocess
1857 import time
1858 import urllib.error
1859 import urllib.parse
1860@@ -215,6 +217,7 @@
1861 block_count = 0
1862 block_size = 4096
1863 retries = 0
1864+ log.debug("url_get_file: " + url)
1865 while True:
1866 try:
1867 filename = open(f_path, "wb")
1868@@ -253,4 +256,17 @@
1869 return True
1870
1871
1872+def ping(host):
1873+ """
1874+ Returns True if host responds to a ping request
1875+ """
1876+ # Ping parameters as function of OS
1877+ ping_str = "-n 1" if platform.system().lower() == "windows" else "-c 1"
1878+ args = "ping " + " " + ping_str + " " + host
1879+ need_sh = False if platform.system().lower() == "windows" else True
1880+
1881+ # Ping
1882+ return subprocess.call(args, shell=need_sh) == 0
1883+
1884+
1885 __all__ = ['get_web_page']
1886
1887=== modified file 'openlp/core/common/settings.py'
1888--- openlp/core/common/settings.py 2017-06-05 06:05:54 +0000
1889+++ openlp/core/common/settings.py 2017-08-13 07:11:15 +0000
1890@@ -134,6 +134,14 @@
1891 'advanced/single click service preview': False,
1892 'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
1893 'advanced/search as type': True,
1894+ 'api/twelve hour': True,
1895+ 'api/port': 4316,
1896+ 'api/websocket port': 4317,
1897+ 'api/user id': 'openlp',
1898+ 'api/password': 'password',
1899+ 'api/authentication enabled': False,
1900+ 'api/ip address': '0.0.0.0',
1901+ 'api/thumbnails': True,
1902 'crashreport/last directory': '',
1903 'formattingTags/html_tags': '',
1904 'core/audio repeat list': False,
1905@@ -214,6 +222,17 @@
1906 ('media/players', 'media/players_temp', [(media_players_conv, None)]), # Convert phonon to system
1907 ('media/players_temp', 'media/players', []), # Move temp setting from above to correct setting
1908 ('advanced/default color', 'core/logo background color', []), # Default image renamed + moved to general > 2.4.
1909+ ('advanced/default image', '/core/logo file', []), # Default image renamed + moved to general after 2.4.
1910+ ('remotes/https enabled', '', []),
1911+ ('remotes/https port', '', []),
1912+ ('remotes/twelve hour', 'api/twelve hour', []),
1913+ ('remotes/port', 'api/port', []),
1914+ ('remotes/websocket port', 'api/websocket port', []),
1915+ ('remotes/user id', 'api/user id', []),
1916+ ('remotes/password', 'api/password', []),
1917+ ('remotes/authentication enabled', 'api/authentication enabled', []),
1918+ ('remotes/ip address', 'api/ip address', []),
1919+ ('remotes/thumbnails', 'api/thumbnails', []),
1920 ('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4.
1921 ('shortcuts/escapeItem', 'shortcuts/desktopScreenEnable', []), # Escape item was removed in 2.6.
1922 ('shortcuts/offlineHelpItem', 'shortcuts/userManualItem', []), # Online and Offline help were combined in 2.6.
1923
1924=== modified file 'openlp/core/common/uistrings.py'
1925--- openlp/core/common/uistrings.py 2017-07-04 22:30:41 +0000
1926+++ openlp/core/common/uistrings.py 2017-08-13 07:11:15 +0000
1927@@ -153,6 +153,7 @@
1928 self.Split = translate('OpenLP.Ui', 'Optional &Split')
1929 self.SplitToolTip = translate('OpenLP.Ui',
1930 'Split a slide into two only if it does not fit on the screen as one slide.')
1931+ self.StartingImport = translate('OpenLP.Ui', 'Starting import...')
1932 self.StopPlaySlidesInLoop = translate('OpenLP.Ui', 'Stop Play Slides in Loop')
1933 self.StopPlaySlidesToEnd = translate('OpenLP.Ui', 'Stop Play Slides to End')
1934 self.Theme = translate('OpenLP.Ui', 'Theme', 'Singular')
1935@@ -166,6 +167,7 @@
1936 self.View = translate('OpenLP.Ui', 'View')
1937 self.ViewMode = translate('OpenLP.Ui', 'View Mode')
1938 self.Video = translate('OpenLP.Ui', 'Video')
1939+ self.WebDownloadText = translate('OpenLP.Ui', 'Web Interface, Download and Install latest Version')
1940 book_chapter = translate('OpenLP.Ui', 'Book Chapter')
1941 chapter = translate('OpenLP.Ui', 'Chapter')
1942 verse = translate('OpenLP.Ui', 'Verse')
1943
1944=== modified file 'openlp/core/common/versionchecker.py'
1945--- openlp/core/common/versionchecker.py 2017-08-01 20:59:41 +0000
1946+++ openlp/core/common/versionchecker.py 2017-08-13 07:11:15 +0000
1947@@ -1,3 +1,27 @@
1948+# -*- coding: utf-8 -*-
1949+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
1950+
1951+###############################################################################
1952+# OpenLP - Open Source Lyrics Projection #
1953+# --------------------------------------------------------------------------- #
1954+# Copyright (c) 2008-2017 OpenLP Developers #
1955+# --------------------------------------------------------------------------- #
1956+# This program is free software; you can redistribute it and/or modify it #
1957+# under the terms of the GNU General Public License as published by the Free #
1958+# Software Foundation; version 2 of the License. #
1959+# #
1960+# This program is distributed in the hope that it will be useful, but WITHOUT #
1961+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
1962+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
1963+# more details. #
1964+# #
1965+# You should have received a copy of the GNU General Public License along #
1966+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
1967+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
1968+###############################################################################
1969+"""
1970+The :mod:`openlp.core.common` module downloads the version details for OpenLP.
1971+"""
1972 import logging
1973 import os
1974 import platform
1975@@ -12,7 +36,8 @@
1976
1977 from PyQt5 import QtCore
1978
1979-from openlp.core.common import AppLocation, Settings
1980+from openlp.core.common import AppLocation, Registry, Settings
1981+from openlp.core.common.httputils import ping
1982
1983 log = logging.getLogger(__name__)
1984
1985@@ -42,12 +67,18 @@
1986 """
1987 self.sleep(1)
1988 log.debug('Version thread - run')
1989- app_version = get_application_version()
1990- version = check_latest_version(app_version)
1991- log.debug("Versions {version1} and {version2} ".format(version1=LooseVersion(str(version)),
1992- version2=LooseVersion(str(app_version['full']))))
1993- if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
1994- self.main_window.openlp_version_check.emit('{version}'.format(version=version))
1995+ found = ping("openlp.io")
1996+ Registry().set_flag('internet_present', found)
1997+ update_check = Settings().value('core/update check')
1998+ if found:
1999+ Registry().execute('get_website_version')
2000+ if update_check:
2001+ app_version = get_application_version()
2002+ version = check_latest_version(app_version)
2003+ log.debug("Versions {version1} and {version2} ".format(version1=LooseVersion(str(version)),
2004+ version2=LooseVersion(str(app_version['full']))))
2005+ if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
2006+ self.main_window.openlp_version_check.emit('{version}'.format(version=version))
2007
2008
2009 def get_application_version():
2010
2011=== modified file 'openlp/core/lib/imagemanager.py'
2012--- openlp/core/lib/imagemanager.py 2017-05-30 18:42:35 +0000
2013+++ openlp/core/lib/imagemanager.py 2017-08-13 07:11:15 +0000
2014@@ -56,7 +56,7 @@
2015 """
2016 Run the thread.
2017 """
2018- self.image_manager._process()
2019+ self.image_manager.process()
2020
2021
2022 class Priority(object):
2023@@ -235,8 +235,15 @@
2024 def get_image(self, path, source, width=-1, height=-1):
2025 """
2026 Return the ``QImage`` from the cache. If not present wait for the background thread to process it.
2027+
2028+ :param: path: The image path
2029+ :param: source: The source of the image
2030+ :param: background: The image background colour
2031+ :param: width: The processed image width
2032+ :param: height: The processed image height
2033 """
2034- log.debug('getImage {path}'.format(path=path))
2035+ log.debug('get_image {path} {source} {width} {height}'.format(path=path, source=source,
2036+ width=width, height=height))
2037 image = self._cache[(path, source, width, height)]
2038 if image.image is None:
2039 self._conversion_queue.modify_priority(image, Priority.High)
2040@@ -255,8 +262,15 @@
2041 def get_image_bytes(self, path, source, width=-1, height=-1):
2042 """
2043 Returns the byte string for an image. If not present wait for the background thread to process it.
2044+
2045+ :param: path: The image path
2046+ :param: source: The source of the image
2047+ :param: background: The image background colour
2048+ :param: width: The processed image width
2049+ :param: height: The processed image height
2050 """
2051- log.debug('get_image_bytes {path}'.format(path=path))
2052+ log.debug('get_image_bytes {path} {source} {width} {height}'.format(path=path, source=source,
2053+ width=width, height=height))
2054 image = self._cache[(path, source, width, height)]
2055 if image.image_bytes is None:
2056 self._conversion_queue.modify_priority(image, Priority.Urgent)
2057@@ -270,9 +284,16 @@
2058 def add_image(self, path, source, background, width=-1, height=-1):
2059 """
2060 Add image to cache if it is not already there.
2061+
2062+ :param: path: The image path
2063+ :param: source: The source of the image
2064+ :param: background: The image background colour
2065+ :param: width: The processed image width
2066+ :param: height: The processed image height
2067 """
2068- log.debug('add_image {path}'.format(path=path))
2069- if (path, source, width, height) not in self._cache:
2070+ log.debug('add_image {path} {source} {width} {height}'.format(path=path, source=source,
2071+ width=width, height=height))
2072+ if not (path, source, width, height) in self._cache:
2073 image = Image(path, source, background, width, height)
2074 self._cache[(path, source, width, height)] = image
2075 self._conversion_queue.put((image.priority, image.secondary_priority, image))
2076@@ -286,11 +307,11 @@
2077 if not self.image_thread.isRunning():
2078 self.image_thread.start()
2079
2080- def _process(self):
2081+ def process(self):
2082 """
2083 Controls the processing called from a ``QtCore.QThread``.
2084 """
2085- log.debug('_process - started')
2086+ log.debug('process - started')
2087 while not self._conversion_queue.empty() and not self.stop_manager:
2088 self._process_cache()
2089 log.debug('_process - ended')
2090
2091=== modified file 'openlp/core/ui/firsttimeform.py'
2092--- openlp/core/ui/firsttimeform.py 2017-08-01 20:59:41 +0000
2093+++ openlp/core/ui/firsttimeform.py 2017-08-13 07:11:15 +0000
2094@@ -202,7 +202,7 @@
2095 self.themes_url = self.web + self.config.get('themes', 'directory') + '/'
2096 self.web_access = True
2097 except (NoSectionError, NoOptionError, MissingSectionHeaderError):
2098- log.debug('A problem occured while parsing the downloaded config file')
2099+ log.debug('A problem occurred while parsing the downloaded config file')
2100 trace_error_handler(log)
2101 self.update_screen_list_combo()
2102 self.application.process_events()
2103@@ -213,7 +213,6 @@
2104 self.presentation_check_box.setChecked(self.plugin_manager.get_plugin_by_name('presentations').is_active())
2105 self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active())
2106 self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active())
2107- self.remote_check_box.setChecked(self.plugin_manager.get_plugin_by_name('remotes').is_active())
2108 self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active())
2109 self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active())
2110 self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active())
2111@@ -530,7 +529,6 @@
2112 self._set_plugin_status(self.presentation_check_box, 'presentations/status')
2113 self._set_plugin_status(self.image_check_box, 'images/status')
2114 self._set_plugin_status(self.media_check_box, 'media/status')
2115- self._set_plugin_status(self.remote_check_box, 'remotes/status')
2116 self._set_plugin_status(self.custom_check_box, 'custom/status')
2117 self._set_plugin_status(self.song_usage_check_box, 'songusage/status')
2118 self._set_plugin_status(self.alert_check_box, 'alerts/status')
2119
2120=== modified file 'openlp/core/ui/firsttimewizard.py'
2121--- openlp/core/ui/firsttimewizard.py 2016-12-31 11:01:36 +0000
2122+++ openlp/core/ui/firsttimewizard.py 2017-08-13 07:11:15 +0000
2123@@ -24,6 +24,7 @@
2124 """
2125 from PyQt5 import QtCore, QtGui, QtWidgets
2126
2127+from openlp.core.common.uistrings import UiStrings
2128 from openlp.core.common import translate, is_macosx, clean_button_text, Settings
2129 from openlp.core.lib import build_icon
2130 from openlp.core.lib.ui import add_welcome_page
2131@@ -254,8 +255,7 @@
2132 self.presentation_check_box.setText(translate('OpenLP.FirstTimeWizard',
2133 'Presentations – Show .ppt, .odp and .pdf files'))
2134 self.media_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Media – Playback of Audio and Video files'))
2135- self.remote_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Remote – Control OpenLP via browser or smart'
2136- 'phone app'))
2137+ self.remote_check_box.setText(str(UiStrings().WebDownloadText))
2138 self.song_usage_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Song Usage Monitor'))
2139 self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard',
2140 'Alerts – Display informative messages while showing other slides'))
2141
2142=== modified file 'openlp/core/ui/mainwindow.py'
2143--- openlp/core/ui/mainwindow.py 2017-08-03 04:21:19 +0000
2144+++ openlp/core/ui/mainwindow.py 2017-08-13 07:11:15 +0000
2145@@ -34,6 +34,8 @@
2146
2147 from PyQt5 import QtCore, QtGui, QtWidgets
2148
2149+from openlp.core.api import websockets
2150+from openlp.core.api.http import server
2151 from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, UiStrings, \
2152 check_directory_exists, translate, is_win, is_macosx, add_actions
2153 from openlp.core.common.actions import ActionList, CategoryOrder
2154@@ -49,6 +51,7 @@
2155 from openlp.core.ui.lib.dockwidget import OpenLPDockWidget
2156 from openlp.core.ui.lib.mediadockmanager import MediaDockManager
2157
2158+
2159 log = logging.getLogger(__name__)
2160
2161 MEDIA_MANAGER_STYLE = """
2162@@ -513,6 +516,9 @@
2163 Settings().set_up_default_values()
2164 self.about_form = AboutForm(self)
2165 MediaController()
2166+ if Registry().get_flag('no_web_server'):
2167+ websockets.WebSocketServer()
2168+ server.HttpServer()
2169 SettingsForm(self)
2170 self.formatting_tag_form = FormattingTagForm(self)
2171 self.shortcut_form = ShortcutListForm(self)
2172@@ -540,7 +546,7 @@
2173 self.tools_first_time_wizard.triggered.connect(self.on_first_time_wizard_clicked)
2174 self.update_theme_images.triggered.connect(self.on_update_theme_images)
2175 self.formatting_tag_item.triggered.connect(self.on_formatting_tag_item_clicked)
2176- self.settings_configure_item.triggered.connect(self.on_settings_configure_iem_clicked)
2177+ self.settings_configure_item.triggered.connect(self.on_settings_configure_item_clicked)
2178 self.settings_shortcuts_item.triggered.connect(self.on_settings_shortcuts_item_clicked)
2179 self.settings_import_item.triggered.connect(self.on_settings_import_item_clicked)
2180 self.settings_export_item.triggered.connect(self.on_settings_export_item_clicked)
2181@@ -803,7 +809,7 @@
2182 """
2183 self.formatting_tag_form.exec()
2184
2185- def on_settings_configure_iem_clicked(self):
2186+ def on_settings_configure_item_clicked(self):
2187 """
2188 Show the Settings dialog
2189 """
2190
2191=== modified file 'openlp/core/ui/media/__init__.py'
2192--- openlp/core/ui/media/__init__.py 2017-01-25 21:17:27 +0000
2193+++ openlp/core/ui/media/__init__.py 2017-08-13 07:11:15 +0000
2194@@ -146,5 +146,6 @@
2195
2196 from .mediacontroller import MediaController
2197 from .playertab import PlayerTab
2198+from .endpoint import media_endpoint
2199
2200 __all__ = ['MediaController', 'PlayerTab']
2201
2202=== added file 'openlp/core/ui/media/endpoint.py'
2203--- openlp/core/ui/media/endpoint.py 1970-01-01 00:00:00 +0000
2204+++ openlp/core/ui/media/endpoint.py 2017-08-13 07:11:15 +0000
2205@@ -0,0 +1,72 @@
2206+# -*- coding: utf-8 -*-
2207+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
2208+
2209+###############################################################################
2210+# OpenLP - Open Source Lyrics Projection #
2211+# --------------------------------------------------------------------------- #
2212+# Copyright (c) 2008-2017 OpenLP Developers #
2213+# --------------------------------------------------------------------------- #
2214+# This program is free software; you can redistribute it and/or modify it #
2215+# under the terms of the GNU General Public License as published by the Free #
2216+# Software Foundation; version 2 of the License. #
2217+# #
2218+# This program is distributed in the hope that it will be useful, but WITHOUT #
2219+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
2220+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
2221+# more details. #
2222+# #
2223+# You should have received a copy of the GNU General Public License along #
2224+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
2225+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
2226+###############################################################################
2227+import logging
2228+
2229+from openlp.core.api.http.endpoint import Endpoint
2230+from openlp.core.api.http import requires_auth
2231+from openlp.core.common import Registry
2232+
2233+
2234+log = logging.getLogger(__name__)
2235+
2236+media_endpoint = Endpoint('media')
2237+
2238+
2239+@media_endpoint.route('play')
2240+@requires_auth
2241+def media_play(request):
2242+ """
2243+ Handles requests for playing media
2244+
2245+ :param request: The http request object.
2246+ """
2247+ media = Registry().get('media_controller')
2248+ live = Registry().get('live_controller')
2249+ status = media.media_play(live, False)
2250+ return {'results': {'success': status}}
2251+
2252+
2253+@media_endpoint.route('pause')
2254+@requires_auth
2255+def media_pause(request):
2256+ """
2257+ Handles requests for pausing media
2258+
2259+ :param request: The http request object.
2260+ """
2261+ media = Registry().get('media_controller')
2262+ live = Registry().get('live_controller')
2263+ status = media.media_pause(live)
2264+ return {'results': {'success': status}}
2265+
2266+
2267+@media_endpoint.route('stop')
2268+@requires_auth
2269+def media_stop(request):
2270+ """
2271+ Handles requests for stopping
2272+
2273+ :param request: The http request object.
2274+ """
2275+ event = getattr(Registry().get('live_controller'), 'mediacontroller_live_stop')
2276+ event.emit()
2277+ return {'results': {'success': True}}
2278
2279=== modified file 'openlp/core/ui/media/mediacontroller.py'
2280--- openlp/core/ui/media/mediacontroller.py 2017-05-30 18:50:39 +0000
2281+++ openlp/core/ui/media/mediacontroller.py 2017-08-13 07:11:15 +0000
2282@@ -28,12 +28,13 @@
2283 import datetime
2284 from PyQt5 import QtCore, QtWidgets
2285
2286+from openlp.core.api.http import register_endpoint
2287 from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, \
2288 extension_loader, translate
2289 from openlp.core.lib import ItemCapabilities
2290 from openlp.core.lib.ui import critical_error_message_box
2291-from openlp.core.common import AppLocation
2292 from openlp.core.ui import DisplayControllerType
2293+from openlp.core.ui.media.endpoint import media_endpoint
2294 from openlp.core.ui.media.vendor.mediainfoWrapper import MediaInfoWrapper
2295 from openlp.core.ui.media.mediaplayer import MediaPlayer
2296 from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\
2297@@ -127,9 +128,11 @@
2298 Registry().register_function('media_unblank', self.media_unblank)
2299 # Signals for background video
2300 Registry().register_function('songs_hide', self.media_hide)
2301+ Registry().register_function('songs_blank', self.media_blank)
2302 Registry().register_function('songs_unblank', self.media_unblank)
2303 Registry().register_function('mediaitem_media_rebuild', self._set_active_players)
2304 Registry().register_function('mediaitem_suffixes', self._generate_extensions_lists)
2305+ register_endpoint(media_endpoint)
2306
2307 def _set_active_players(self):
2308 """
2309@@ -611,6 +614,14 @@
2310 """
2311 self.media_play(msg[0], status)
2312
2313+ def on_media_play(self):
2314+ """
2315+ Responds to the request to play a loaded video from the web.
2316+
2317+ :param msg: First element is the controller which should be used
2318+ """
2319+ self.media_play(Registry().get('live_controller'), False)
2320+
2321 def media_play(self, controller, first_time=True):
2322 """
2323 Responds to the request to play a loaded video
2324@@ -685,6 +696,14 @@
2325 """
2326 self.media_pause(msg[0])
2327
2328+ def on_media_pause(self):
2329+ """
2330+ Responds to the request to pause a loaded video from the web.
2331+
2332+ :param msg: First element is the controller which should be used
2333+ """
2334+ self.media_pause(Registry().get('live_controller'))
2335+
2336 def media_pause(self, controller):
2337 """
2338 Responds to the request to pause a loaded video
2339@@ -725,6 +744,14 @@
2340 """
2341 self.media_stop(msg[0])
2342
2343+ def on_media_stop(self):
2344+ """
2345+ Responds to the request to stop a loaded video from the web.
2346+
2347+ :param msg: First element is the controller which should be used
2348+ """
2349+ self.media_stop(Registry().get('live_controller'))
2350+
2351 def media_stop(self, controller, looping_background=False):
2352 """
2353 Responds to the request to stop a loaded video
2354
2355=== modified file 'openlp/core/ui/settingsform.py'
2356--- openlp/core/ui/settingsform.py 2017-06-04 12:14:23 +0000
2357+++ openlp/core/ui/settingsform.py 2017-08-13 07:11:15 +0000
2358@@ -26,6 +26,7 @@
2359
2360 from PyQt5 import QtCore, QtWidgets
2361
2362+from openlp.core.api import ApiTab
2363 from openlp.core.common import Registry, RegistryProperties
2364 from openlp.core.lib import build_icon
2365 from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab
2366@@ -56,12 +57,13 @@
2367 self.projector_tab = None
2368 self.advanced_tab = None
2369 self.player_tab = None
2370+ self.api_tab = None
2371
2372 def exec(self):
2373 """
2374 Execute the form
2375 """
2376- # load all the
2377+ # load all the widgets
2378 self.setting_list_widget.blockSignals(True)
2379 self.setting_list_widget.clear()
2380 while self.stacked_layout.count():
2381@@ -72,6 +74,7 @@
2382 self.insert_tab(self.advanced_tab)
2383 self.insert_tab(self.player_tab)
2384 self.insert_tab(self.projector_tab)
2385+ self.insert_tab(self.api_tab)
2386 for plugin in self.plugin_manager.plugins:
2387 if plugin.settings_tab:
2388 self.insert_tab(plugin.settings_tab, plugin.is_active())
2389@@ -93,6 +96,7 @@
2390 list_item = QtWidgets.QListWidgetItem(build_icon(tab_widget.icon_path), tab_widget.tab_title_visible)
2391 list_item.setData(QtCore.Qt.UserRole, tab_widget.tab_title)
2392 self.setting_list_widget.addItem(list_item)
2393+ tab_widget.load()
2394
2395 def accept(self):
2396 """
2397@@ -154,10 +158,13 @@
2398 self.advanced_tab = AdvancedTab(self)
2399 # Advanced tab
2400 self.player_tab = PlayerTab(self)
2401+ # Api tab
2402+ self.api_tab = ApiTab(self)
2403 self.general_tab.post_set_up()
2404 self.themes_tab.post_set_up()
2405 self.advanced_tab.post_set_up()
2406 self.player_tab.post_set_up()
2407+ self.api_tab.post_set_up()
2408 for plugin in self.plugin_manager.plugins:
2409 if plugin.settings_tab:
2410 plugin.settings_tab.post_set_up()
2411
2412=== modified file 'openlp/core/ui/slidecontroller.py'
2413--- openlp/core/ui/slidecontroller.py 2017-03-28 05:15:05 +0000
2414+++ openlp/core/ui/slidecontroller.py 2017-08-13 07:11:15 +0000
2415@@ -439,6 +439,10 @@
2416 # NOTE: {t} used to keep line length < maxline
2417 getattr(self,
2418 'slidecontroller_{t}_previous'.format(t=self.type_prefix)).connect(self.on_slide_selected_previous)
2419+ if self.is_live:
2420+ getattr(self, 'mediacontroller_live_play').connect(self.media_controller.on_media_play)
2421+ getattr(self, 'mediacontroller_live_pause').connect(self.media_controller.on_media_pause)
2422+ getattr(self, 'mediacontroller_live_stop').connect(self.media_controller.on_media_stop)
2423
2424 def _slide_shortcut_activated(self):
2425 """
2426@@ -1530,6 +1534,9 @@
2427 slidecontroller_live_next = QtCore.pyqtSignal()
2428 slidecontroller_live_previous = QtCore.pyqtSignal()
2429 slidecontroller_toggle_display = QtCore.pyqtSignal(str)
2430+ mediacontroller_live_play = QtCore.pyqtSignal()
2431+ mediacontroller_live_pause = QtCore.pyqtSignal()
2432+ mediacontroller_live_stop = QtCore.pyqtSignal()
2433
2434 def __init__(self, parent):
2435 """
2436
2437=== modified file 'openlp/plugins/alerts/alertsplugin.py'
2438--- openlp/plugins/alerts/alertsplugin.py 2017-07-04 23:13:51 +0000
2439+++ openlp/plugins/alerts/alertsplugin.py 2017-08-13 07:11:15 +0000
2440@@ -24,6 +24,7 @@
2441
2442 from PyQt5 import QtGui
2443
2444+from openlp.core.api.http import register_endpoint
2445 from openlp.core.common import Settings, UiStrings, translate
2446 from openlp.core.common.actions import ActionList
2447 from openlp.core.lib import Plugin, StringContent, build_icon
2448@@ -31,6 +32,7 @@
2449 from openlp.core.lib.theme import VerticalType
2450 from openlp.core.lib.ui import create_action
2451 from openlp.core.ui import AlertLocation
2452+from openlp.plugins.alerts.endpoint import api_alerts_endpoint, alerts_endpoint
2453 from openlp.plugins.alerts.forms import AlertForm
2454 from openlp.plugins.alerts.lib import AlertsManager, AlertsTab
2455 from openlp.plugins.alerts.lib.db import init_schema
2456@@ -140,6 +142,8 @@
2457 AlertsManager(self)
2458 self.manager = Manager('alerts', init_schema)
2459 self.alert_form = AlertForm(self)
2460+ register_endpoint(alerts_endpoint)
2461+ register_endpoint(api_alerts_endpoint)
2462
2463 def add_tools_menu_item(self, tools_menu):
2464 """
2465
2466=== added file 'openlp/plugins/alerts/endpoint.py'
2467--- openlp/plugins/alerts/endpoint.py 1970-01-01 00:00:00 +0000
2468+++ openlp/plugins/alerts/endpoint.py 2017-08-13 07:11:15 +0000
2469@@ -0,0 +1,60 @@
2470+# -*- coding: utf-8 -*-
2471+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
2472+
2473+###############################################################################
2474+# OpenLP - Open Source Lyrics Projection #
2475+# --------------------------------------------------------------------------- #
2476+# Copyright (c) 2008-2017 OpenLP Developers #
2477+# --------------------------------------------------------------------------- #
2478+# This program is free software; you can redistribute it and/or modify it #
2479+# under the terms of the GNU General Public License as published by the Free #
2480+# Software Foundation; version 2 of the License. #
2481+# #
2482+# This program is distributed in the hope that it will be useful, but WITHOUT #
2483+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
2484+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
2485+# more details. #
2486+# #
2487+# You should have received a copy of the GNU General Public License along #
2488+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
2489+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
2490+###############################################################################
2491+import logging
2492+import json
2493+import urllib
2494+from urllib.parse import urlparse
2495+
2496+from openlp.core.api.http.endpoint import Endpoint
2497+from openlp.core.api.http import requires_auth
2498+from openlp.core.common import Registry
2499+from openlp.core.lib import PluginStatus
2500+
2501+
2502+log = logging.getLogger(__name__)
2503+
2504+alerts_endpoint = Endpoint('alert')
2505+api_alerts_endpoint = Endpoint('api')
2506+
2507+
2508+@alerts_endpoint.route('')
2509+@api_alerts_endpoint.route('alert')
2510+@requires_auth
2511+def alert(request):
2512+ """
2513+ Handles requests for setting service items in the service manager
2514+
2515+ :param request: The http request object.
2516+ """
2517+ plugin = Registry().get('plugin_manager').get_plugin_by_name("alerts")
2518+ if plugin.status == PluginStatus.Active:
2519+ try:
2520+ json_data = request.GET.get('data')
2521+ text = json.loads(json_data)['request']['text']
2522+ except KeyError:
2523+ log.error("Endpoint alerts request text not found")
2524+ text = urllib.parse.unquote(text)
2525+ Registry().get('alerts_manager').alerts_text.emit([text])
2526+ success = True
2527+ else:
2528+ success = False
2529+ return {'results': {'success': success}}
2530
2531=== modified file 'openlp/plugins/bibles/bibleplugin.py'
2532--- openlp/plugins/bibles/bibleplugin.py 2017-07-04 23:13:51 +0000
2533+++ openlp/plugins/bibles/bibleplugin.py 2017-08-13 07:11:15 +0000
2534@@ -22,9 +22,11 @@
2535
2536 import logging
2537
2538+from openlp.core.api.http import register_endpoint
2539 from openlp.core.common import UiStrings
2540 from openlp.core.common.actions import ActionList
2541 from openlp.core.lib import Plugin, StringContent, build_icon, translate
2542+from openlp.plugins.bibles.endpoint import api_bibles_endpoint, bibles_endpoint
2543 from openlp.core.lib.ui import create_action
2544 from openlp.plugins.bibles.lib import BibleManager, BiblesTab, BibleMediaItem, LayoutStyle, DisplayStyle, \
2545 LanguageSelection
2546@@ -75,6 +77,8 @@
2547 self.icon_path = ':/plugins/plugin_bibles.png'
2548 self.icon = build_icon(self.icon_path)
2549 self.manager = BibleManager(self)
2550+ register_endpoint(bibles_endpoint)
2551+ register_endpoint(api_bibles_endpoint)
2552
2553 def initialise(self):
2554 """
2555
2556=== added file 'openlp/plugins/bibles/endpoint.py'
2557--- openlp/plugins/bibles/endpoint.py 1970-01-01 00:00:00 +0000
2558+++ openlp/plugins/bibles/endpoint.py 2017-08-13 07:11:15 +0000
2559@@ -0,0 +1,100 @@
2560+# -*- coding: utf-8 -*-
2561+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
2562+
2563+###############################################################################
2564+# OpenLP - Open Source Lyrics Projection #
2565+# --------------------------------------------------------------------------- #
2566+# Copyright (c) 2008-2017 OpenLP Developers #
2567+# --------------------------------------------------------------------------- #
2568+# This program is free software; you can redistribute it and/or modify it #
2569+# under the terms of the GNU General Public License as published by the Free #
2570+# Software Foundation; version 2 of the License. #
2571+# #
2572+# This program is distributed in the hope that it will be useful, but WITHOUT #
2573+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
2574+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
2575+# more details. #
2576+# #
2577+# You should have received a copy of the GNU General Public License along #
2578+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
2579+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
2580+###############################################################################
2581+import logging
2582+
2583+from openlp.core.api.http.endpoint import Endpoint
2584+from openlp.core.api.http.errors import NotFound
2585+from openlp.core.api.endpoint.pluginhelpers import search, live, service
2586+from openlp.core.api.http import requires_auth
2587+
2588+
2589+log = logging.getLogger(__name__)
2590+
2591+bibles_endpoint = Endpoint('bibles')
2592+api_bibles_endpoint = Endpoint('api')
2593+
2594+
2595+@bibles_endpoint.route('search')
2596+def bibles_search(request):
2597+ """
2598+ Handles requests for searching the bibles plugin
2599+
2600+ :param request: The http request object.
2601+ """
2602+ return search(request, 'bibles', log)
2603+
2604+
2605+@bibles_endpoint.route('live')
2606+@requires_auth
2607+def bibles_live(request):
2608+ """
2609+ Handles requests for making a song live
2610+
2611+ :param request: The http request object.
2612+ """
2613+ return live(request, 'bibles', log)
2614+
2615+
2616+@bibles_endpoint.route('add')
2617+@requires_auth
2618+def bibles_service(request):
2619+ """
2620+ Handles requests for adding a song to the service
2621+
2622+ :param request: The http request object.
2623+ """
2624+ service(request, 'bibles', log)
2625+
2626+
2627+@api_bibles_endpoint.route('bibles/search')
2628+def bibles_search_api(request):
2629+ """
2630+ Handles requests for searching the bibles plugin
2631+
2632+ :param request: The http request object.
2633+ """
2634+ return search(request, 'bibles', log)
2635+
2636+
2637+@api_bibles_endpoint.route('bibles/live')
2638+@requires_auth
2639+def bibles_live_api(request):
2640+ """
2641+ Handles requests for making a song live
2642+
2643+ :param request: The http request object.
2644+ """
2645+ return live(request, 'bibles', log)
2646+
2647+
2648+@api_bibles_endpoint.route('bibles/add')
2649+@requires_auth
2650+def bibles_service_api(request):
2651+ """
2652+ Handles requests for adding a song to the service
2653+
2654+ :param request: The http request object.
2655+ """
2656+ try:
2657+ search(request, 'bibles', log)
2658+ except NotFound:
2659+ return {'results': {'items': []}}
2660
2661=== modified file 'openlp/plugins/custom/customplugin.py'
2662--- openlp/plugins/custom/customplugin.py 2017-06-04 09:52:15 +0000
2663+++ openlp/plugins/custom/customplugin.py 2017-08-13 07:11:15 +0000
2664@@ -26,8 +26,10 @@
2665
2666 import logging
2667
2668+from openlp.core.api.http import register_endpoint
2669 from openlp.core.lib import Plugin, StringContent, build_icon, translate
2670 from openlp.core.lib.db import Manager
2671+from openlp.plugins.custom.endpoint import api_custom_endpoint, custom_endpoint
2672 from openlp.plugins.custom.lib import CustomMediaItem, CustomTab
2673 from openlp.plugins.custom.lib.db import CustomSlide, init_schema
2674 from openlp.plugins.custom.lib.mediaitem import CustomSearch
2675@@ -61,6 +63,8 @@
2676 self.db_manager = Manager('custom', init_schema)
2677 self.icon_path = ':/plugins/plugin_custom.png'
2678 self.icon = build_icon(self.icon_path)
2679+ register_endpoint(custom_endpoint)
2680+ register_endpoint(api_custom_endpoint)
2681
2682 @staticmethod
2683 def about():
2684
2685=== added file 'openlp/plugins/custom/endpoint.py'
2686--- openlp/plugins/custom/endpoint.py 1970-01-01 00:00:00 +0000
2687+++ openlp/plugins/custom/endpoint.py 2017-08-13 07:11:15 +0000
2688@@ -0,0 +1,100 @@
2689+# -*- coding: utf-8 -*-
2690+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
2691+
2692+###############################################################################
2693+# OpenLP - Open Source Lyrics Projection #
2694+# --------------------------------------------------------------------------- #
2695+# Copyright (c) 2008-2017 OpenLP Developers #
2696+# --------------------------------------------------------------------------- #
2697+# This program is free software; you can redistribute it and/or modify it #
2698+# under the terms of the GNU General Public License as published by the Free #
2699+# Software Foundation; version 2 of the License. #
2700+# #
2701+# This program is distributed in the hope that it will be useful, but WITHOUT #
2702+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
2703+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
2704+# more details. #
2705+# #
2706+# You should have received a copy of the GNU General Public License along #
2707+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
2708+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
2709+###############################################################################
2710+import logging
2711+
2712+from openlp.core.api.http.endpoint import Endpoint
2713+from openlp.core.api.http.errors import NotFound
2714+from openlp.core.api.endpoint.pluginhelpers import search, live, service
2715+from openlp.core.api.http import requires_auth
2716+
2717+
2718+log = logging.getLogger(__name__)
2719+
2720+custom_endpoint = Endpoint('custom')
2721+api_custom_endpoint = Endpoint('api')
2722+
2723+
2724+@custom_endpoint.route('search')
2725+def custom_search(request):
2726+ """
2727+ Handles requests for searching the custom plugin
2728+
2729+ :param request: The http request object.
2730+ """
2731+ return search(request, 'custom', log)
2732+
2733+
2734+@custom_endpoint.route('live')
2735+@requires_auth
2736+def custom_live(request):
2737+ """
2738+ Handles requests for making a song live
2739+
2740+ :param request: The http request object.
2741+ """
2742+ return live(request, 'custom', log)
2743+
2744+
2745+@custom_endpoint.route('add')
2746+@requires_auth
2747+def custom_service(request):
2748+ """
2749+ Handles requests for adding a song to the service
2750+
2751+ :param request: The http request object.
2752+ """
2753+ service(request, 'custom', log)
2754+
2755+
2756+@api_custom_endpoint.route('custom/search')
2757+def custom_search_api(request):
2758+ """
2759+ Handles requests for searching the custom plugin
2760+
2761+ :param request: The http request object.
2762+ """
2763+ return search(request, 'custom', log)
2764+
2765+
2766+@api_custom_endpoint.route('custom/live')
2767+@requires_auth
2768+def custom_live_api(request):
2769+ """
2770+ Handles requests for making a song live
2771+
2772+ :param request: The http request object.
2773+ """
2774+ return live(request, 'custom', log)
2775+
2776+
2777+@api_custom_endpoint.route('custom/add')
2778+@requires_auth
2779+def custom_service_api(request):
2780+ """
2781+ Handles requests for adding a song to the service
2782+
2783+ :param request: The http request object.
2784+ """
2785+ try:
2786+ search(request, 'custom', log)
2787+ except NotFound:
2788+ return {'results': {'items': []}}
2789
2790=== added file 'openlp/plugins/images/endpoint.py'
2791--- openlp/plugins/images/endpoint.py 1970-01-01 00:00:00 +0000
2792+++ openlp/plugins/images/endpoint.py 2017-08-13 07:11:15 +0000
2793@@ -0,0 +1,113 @@
2794+# -*- coding: utf-8 -*-
2795+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
2796+
2797+###############################################################################
2798+# OpenLP - Open Source Lyrics Projection #
2799+# --------------------------------------------------------------------------- #
2800+# Copyright (c) 2008-2017 OpenLP Developers #
2801+# --------------------------------------------------------------------------- #
2802+# This program is free software; you can redistribute it and/or modify it #
2803+# under the terms of the GNU General Public License as published by the Free #
2804+# Software Foundation; version 2 of the License. #
2805+# #
2806+# This program is distributed in the hope that it will be useful, but WITHOUT #
2807+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
2808+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
2809+# more details. #
2810+# #
2811+# You should have received a copy of the GNU General Public License along #
2812+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
2813+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
2814+###############################################################################
2815+import logging
2816+
2817+from openlp.core.api.http.endpoint import Endpoint
2818+from openlp.core.api.http.errors import NotFound
2819+from openlp.core.api.endpoint.pluginhelpers import search, live, service, display_thumbnails
2820+from openlp.core.api.http import requires_auth
2821+
2822+
2823+log = logging.getLogger(__name__)
2824+
2825+images_endpoint = Endpoint('images')
2826+api_images_endpoint = Endpoint('api')
2827+
2828+
2829+# images/thumbnails/320x240/1.jpg
2830+@images_endpoint.route('thumbnails/{dimensions}/{file_name}')
2831+def images_thumbnails(request, dimensions, file_name):
2832+ """
2833+ Return an image to a web page based on a URL
2834+ :param request: Request object
2835+ :param dimensions: the image size eg 88x88
2836+ :param file_name: the individual image name
2837+ :return:
2838+ """
2839+ return display_thumbnails(request, 'images', log, dimensions, file_name)
2840+
2841+
2842+@images_endpoint.route('search')
2843+def images_search(request):
2844+ """
2845+ Handles requests for searching the images plugin
2846+
2847+ :param request: The http request object.
2848+ """
2849+ return search(request, 'images', log)
2850+
2851+
2852+@images_endpoint.route('live')
2853+@requires_auth
2854+def images_live(request):
2855+ """
2856+ Handles requests for making a song live
2857+
2858+ :param request: The http request object.
2859+ """
2860+ return live(request, 'images', log)
2861+
2862+
2863+@images_endpoint.route('add')
2864+@requires_auth
2865+def images_service(request):
2866+ """
2867+ Handles requests for adding a song to the service
2868+
2869+ :param request: The http request object.
2870+ """
2871+ service(request, 'images', log)
2872+
2873+
2874+@api_images_endpoint.route('images/search')
2875+def images_search_api(request):
2876+ """
2877+ Handles requests for searching the images plugin
2878+
2879+ :param request: The http request object.
2880+ """
2881+ return search(request, 'images', log)
2882+
2883+
2884+@api_images_endpoint.route('images/live')
2885+@requires_auth
2886+def images_live_api(request):
2887+ """
2888+ Handles requests for making a song live
2889+
2890+ :param request: The http request object.
2891+ """
2892+ return live(request, 'images', log)
2893+
2894+
2895+@api_images_endpoint.route('images/add')
2896+@requires_auth
2897+def images_service_api(request):
2898+ """
2899+ Handles requests for adding a song to the service
2900+
2901+ :param request: The http request object.
2902+ """
2903+ try:
2904+ search(request, 'images', log)
2905+ except NotFound:
2906+ return {'results': {'items': []}}
2907
2908=== modified file 'openlp/plugins/images/imageplugin.py'
2909--- openlp/plugins/images/imageplugin.py 2016-12-31 11:01:36 +0000
2910+++ openlp/plugins/images/imageplugin.py 2017-08-13 07:11:15 +0000
2911@@ -24,9 +24,11 @@
2912
2913 import logging
2914
2915+from openlp.core.api.http import register_endpoint
2916 from openlp.core.common import Settings, translate
2917 from openlp.core.lib import Plugin, StringContent, ImageSource, build_icon
2918 from openlp.core.lib.db import Manager
2919+from openlp.plugins.images.endpoint import api_images_endpoint, images_endpoint
2920 from openlp.plugins.images.lib import ImageMediaItem, ImageTab
2921 from openlp.plugins.images.lib.db import init_schema
2922
2923@@ -51,6 +53,8 @@
2924 self.weight = -7
2925 self.icon_path = ':/plugins/plugin_images.png'
2926 self.icon = build_icon(self.icon_path)
2927+ register_endpoint(images_endpoint)
2928+ register_endpoint(api_images_endpoint)
2929
2930 @staticmethod
2931 def about():
2932
2933=== added file 'openlp/plugins/media/endpoint.py'
2934--- openlp/plugins/media/endpoint.py 1970-01-01 00:00:00 +0000
2935+++ openlp/plugins/media/endpoint.py 2017-08-13 07:11:15 +0000
2936@@ -0,0 +1,100 @@
2937+# -*- coding: utf-8 -*-
2938+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
2939+
2940+###############################################################################
2941+# OpenLP - Open Source Lyrics Projection #
2942+# --------------------------------------------------------------------------- #
2943+# Copyright (c) 2008-2017 OpenLP Developers #
2944+# --------------------------------------------------------------------------- #
2945+# This program is free software; you can redistribute it and/or modify it #
2946+# under the terms of the GNU General Public License as published by the Free #
2947+# Software Foundation; version 2 of the License. #
2948+# #
2949+# This program is distributed in the hope that it will be useful, but WITHOUT #
2950+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
2951+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
2952+# more details. #
2953+# #
2954+# You should have received a copy of the GNU General Public License along #
2955+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
2956+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
2957+###############################################################################
2958+import logging
2959+
2960+from openlp.core.api.http.endpoint import Endpoint
2961+from openlp.core.api.http.errors import NotFound
2962+from openlp.core.api.endpoint.pluginhelpers import search, live, service
2963+from openlp.core.api.http import requires_auth
2964+
2965+
2966+log = logging.getLogger(__name__)
2967+
2968+media_endpoint = Endpoint('media')
2969+api_media_endpoint = Endpoint('api')
2970+
2971+
2972+@media_endpoint.route('search')
2973+def media_search(request):
2974+ """
2975+ Handles requests for searching the media plugin
2976+
2977+ :param request: The http request object.
2978+ """
2979+ return search(request, 'media', log)
2980+
2981+
2982+@media_endpoint.route('live')
2983+@requires_auth
2984+def media_live(request):
2985+ """
2986+ Handles requests for making a song live
2987+
2988+ :param request: The http request object.
2989+ """
2990+ return live(request, 'media', log)
2991+
2992+
2993+@media_endpoint.route('add')
2994+@requires_auth
2995+def media_service(request):
2996+ """
2997+ Handles requests for adding a song to the service
2998+
2999+ :param request: The http request object.
3000+ """
3001+ service(request, 'media', log)
3002+
3003+
3004+@api_media_endpoint.route('media/search')
3005+def media_search_api(request):
3006+ """
3007+ Handles requests for searching the media plugin
3008+
3009+ :param request: The http request object.
3010+ """
3011+ return search(request, 'media', log)
3012+
3013+
3014+@api_media_endpoint.route('media/live')
3015+@requires_auth
3016+def media_live_api(request):
3017+ """
3018+ Handles requests for making a song live
3019+
3020+ :param request: The http request object.
3021+ """
3022+ return live(request, 'media', log)
3023+
3024+
3025+@api_media_endpoint.route('media/add')
3026+@requires_auth
3027+def media_service_api(request):
3028+ """
3029+ Handles requests for adding a song to the service
3030+
3031+ :param request: The http request object.
3032+ """
3033+ try:
3034+ search(request, 'media', log)
3035+ except NotFound:
3036+ return {'results': {'items': []}}
3037
3038=== modified file 'openlp/plugins/media/mediaplugin.py'
3039--- openlp/plugins/media/mediaplugin.py 2017-08-01 20:59:41 +0000
3040+++ openlp/plugins/media/mediaplugin.py 2017-08-13 07:11:15 +0000
3041@@ -26,12 +26,13 @@
3042 import logging
3043 import os
3044 import re
3045-from shutil import which
3046
3047 from PyQt5 import QtCore
3048
3049-from openlp.core.common import AppLocation, Settings, translate, check_binary_exists, is_win
3050+from openlp.core.api.http import register_endpoint
3051+from openlp.core.common import AppLocation, translate, check_binary_exists
3052 from openlp.core.lib import Plugin, StringContent, build_icon
3053+from openlp.plugins.media.endpoint import api_media_endpoint, media_endpoint
3054 from openlp.plugins.media.lib import MediaMediaItem, MediaTab
3055
3056
3057@@ -58,6 +59,8 @@
3058 self.icon = build_icon(self.icon_path)
3059 # passed with drag and drop messages
3060 self.dnd_id = 'Media'
3061+ register_endpoint(media_endpoint)
3062+ register_endpoint(api_media_endpoint)
3063
3064 def initialise(self):
3065 """
3066
3067=== added file 'openlp/plugins/presentations/endpoint.py'
3068--- openlp/plugins/presentations/endpoint.py 1970-01-01 00:00:00 +0000
3069+++ openlp/plugins/presentations/endpoint.py 2017-08-13 07:11:15 +0000
3070@@ -0,0 +1,114 @@
3071+# -*- coding: utf-8 -*-
3072+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3073+
3074+###############################################################################
3075+# OpenLP - Open Source Lyrics Projection #
3076+# --------------------------------------------------------------------------- #
3077+# Copyright (c) 2008-2017 OpenLP Developers #
3078+# --------------------------------------------------------------------------- #
3079+# This program is free software; you can redistribute it and/or modify it #
3080+# under the terms of the GNU General Public License as published by the Free #
3081+# Software Foundation; version 2 of the License. #
3082+# #
3083+# This program is distributed in the hope that it will be useful, but WITHOUT #
3084+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
3085+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
3086+# more details. #
3087+# #
3088+# You should have received a copy of the GNU General Public License along #
3089+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
3090+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
3091+###############################################################################
3092+import logging
3093+
3094+from openlp.core.api.http.endpoint import Endpoint
3095+from openlp.core.api.http.errors import NotFound
3096+from openlp.core.api.endpoint.pluginhelpers import search, live, service, display_thumbnails
3097+from openlp.core.api.http import requires_auth
3098+
3099+
3100+log = logging.getLogger(__name__)
3101+
3102+presentations_endpoint = Endpoint('presentations')
3103+api_presentations_endpoint = Endpoint('api')
3104+
3105+
3106+# /presentations/thumbnails88x88/PA%20Rota.pdf/slide5.png
3107+@presentations_endpoint.route('thumbnails/{dimensions}/{file_name}/{slide}')
3108+def presentations_thumbnails(request, dimensions, file_name, slide):
3109+ """
3110+ Return a presentation to a web page based on a URL
3111+ :param request: Request object
3112+ :param dimensions: the image size eg 88x88
3113+ :param file_name: the file name of the image
3114+ :param slide: the individual image name
3115+ :return:
3116+ """
3117+ return display_thumbnails(request, 'presentations', log, dimensions, file_name, slide)
3118+
3119+
3120+@presentations_endpoint.route('search')
3121+def presentations_search(request):
3122+ """
3123+ Handles requests for searching the presentations plugin
3124+
3125+ :param request: The http request object.
3126+ """
3127+ return search(request, 'presentations', log)
3128+
3129+
3130+@presentations_endpoint.route('live')
3131+@requires_auth
3132+def presentations_live(request):
3133+ """
3134+ Handles requests for making a song live
3135+
3136+ :param request: The http request object.
3137+ """
3138+ return live(request, 'presentations', log)
3139+
3140+
3141+@presentations_endpoint.route('add')
3142+@requires_auth
3143+def presentations_service(request):
3144+ """
3145+ Handles requests for adding a song to the service
3146+
3147+ :param request: The http request object.
3148+ """
3149+ service(request, 'presentations', log)
3150+
3151+
3152+@api_presentations_endpoint.route('presentations/search')
3153+def presentations_search_api(request):
3154+ """
3155+ Handles requests for searching the presentations plugin
3156+
3157+ :param request: The http request object.
3158+ """
3159+ return search(request, 'presentations', log)
3160+
3161+
3162+@api_presentations_endpoint.route('presentations/live')
3163+@requires_auth
3164+def presentations_live_api(request):
3165+ """
3166+ Handles requests for making a song live
3167+
3168+ :param request: The http request object.
3169+ """
3170+ return live(request, 'presentations', log)
3171+
3172+
3173+@api_presentations_endpoint.route('presentations/add')
3174+@requires_auth
3175+def presentations_service_api(request):
3176+ """
3177+ Handles requests for adding a song to the service
3178+
3179+ :param request: The http request object.
3180+ """
3181+ try:
3182+ search(request, 'presentations', log)
3183+ except NotFound:
3184+ return {'results': {'items': []}}
3185
3186=== modified file 'openlp/plugins/presentations/presentationplugin.py'
3187--- openlp/plugins/presentations/presentationplugin.py 2017-06-09 06:06:49 +0000
3188+++ openlp/plugins/presentations/presentationplugin.py 2017-08-13 07:11:15 +0000
3189@@ -28,8 +28,10 @@
3190
3191 from PyQt5 import QtCore
3192
3193-from openlp.core.common import AppLocation, extension_loader, translate
3194+from openlp.core.api.http import register_endpoint
3195+from openlp.core.common import extension_loader, translate
3196 from openlp.core.lib import Plugin, StringContent, build_icon
3197+from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint
3198 from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
3199
3200 log = logging.getLogger(__name__)
3201@@ -66,6 +68,8 @@
3202 self.weight = -8
3203 self.icon_path = ':/plugins/plugin_presentations.png'
3204 self.icon = build_icon(self.icon_path)
3205+ register_endpoint(presentations_endpoint)
3206+ register_endpoint(api_presentations_endpoint)
3207
3208 def create_settings_tab(self, parent):
3209 """
3210
3211=== modified file 'openlp/plugins/remotes/__init__.py'
3212--- openlp/plugins/remotes/__init__.py 2016-12-31 11:01:36 +0000
3213+++ openlp/plugins/remotes/__init__.py 2017-08-13 07:11:15 +0000
3214@@ -19,71 +19,3 @@
3215 # with this program; if not, write to the Free Software Foundation, Inc., 59 #
3216 # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
3217 ###############################################################################
3218-"""
3219-The :mod:`remotes` plugin allows OpenLP to be controlled from another machine
3220-over a network connection.
3221-
3222-Routes:
3223-
3224-``/``
3225- Go to the web interface.
3226-
3227-``/files/{filename}``
3228- Serve a static file.
3229-
3230-``/api/poll``
3231- Poll to see if there are any changes. Returns a JSON-encoded dict of
3232- any changes that occurred::
3233-
3234- {"results": {"type": "controller"}}
3235-
3236- Or, if there were no results, False::
3237-
3238- {"results": False}
3239-
3240-``/api/controller/{live|preview}/{action}``
3241- Perform ``{action}`` on the live or preview controller. Valid actions
3242- are:
3243-
3244- ``next``
3245- Load the next slide.
3246-
3247- ``previous``
3248- Load the previous slide.
3249-
3250- ``jump``
3251- Jump to a specific slide. Requires an id return in a JSON-encoded
3252- dict like so::
3253-
3254- {"request": {"id": 1}}
3255-
3256- ``first``
3257- Load the first slide.
3258-
3259- ``last``
3260- Load the last slide.
3261-
3262- ``text``
3263- Request the text of the current slide.
3264-
3265-``/api/service/{action}``
3266- Perform ``{action}`` on the service manager (e.g. go live). Data is
3267- passed as a json-encoded ``data`` parameter. Valid actions are:
3268-
3269- ``next``
3270- Load the next item in the service.
3271-
3272- ``previous``
3273- Load the previews item in the service.
3274-
3275- ``jump``
3276- Jump to a specific item in the service. Requires an id returned in
3277- a JSON-encoded dict like so::
3278-
3279- {"request": {"id": 1}}
3280-
3281- ``list``
3282- Request a list of items in the service.
3283-
3284-
3285-"""
3286
3287=== added file 'openlp/plugins/remotes/deploy.py'
3288--- openlp/plugins/remotes/deploy.py 1970-01-01 00:00:00 +0000
3289+++ openlp/plugins/remotes/deploy.py 2017-08-13 07:11:15 +0000
3290@@ -0,0 +1,69 @@
3291+# -*- coding: utf-8 -*-
3292+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3293+
3294+###############################################################################
3295+# OpenLP - Open Source Lyrics Projection #
3296+# --------------------------------------------------------------------------- #
3297+# Copyright (c) 2008-2017 OpenLP Developers #
3298+# --------------------------------------------------------------------------- #
3299+# This program is free software; you can redistribute it and/or modify it #
3300+# under the terms of the GNU General Public License as published by the Free #
3301+# Software Foundation; version 2 of the License. #
3302+# #
3303+# This program is distributed in the hope that it will be useful, but WITHOUT #
3304+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
3305+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
3306+# more details. #
3307+# #
3308+# You should have received a copy of the GNU General Public License along #
3309+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
3310+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
3311+###############################################################################
3312+
3313+import os
3314+import zipfile
3315+import urllib.error
3316+
3317+from openlp.core.common import AppLocation, Registry
3318+from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size
3319+
3320+
3321+def deploy_zipfile(app_root, zip_name):
3322+ """
3323+ Process the downloaded zip file and add to the correct directory
3324+
3325+ :param zip_name: the zip file to be processed
3326+ :param app_root: the directory where the zip get expanded to
3327+
3328+ :return: None
3329+ """
3330+ zip_file = os.path.join(app_root, zip_name)
3331+ web_zip = zipfile.ZipFile(zip_file)
3332+ web_zip.extractall(app_root)
3333+
3334+
3335+def download_sha256():
3336+ """
3337+ Download the config file to extract the sha256 and version number
3338+ """
3339+ user_agent = 'OpenLP/' + Registry().get('application').applicationVersion()
3340+ try:
3341+ web_config = get_web_page('{host}{name}'.format(host='https://get.openlp.org/webclient/', name='download.cfg'),
3342+ header=('User-Agent', user_agent))
3343+ except (urllib.error.URLError, ConnectionError) as err:
3344+ return False
3345+ file_bits = web_config.read().decode('utf-8').split()
3346+ return file_bits[0], file_bits[2]
3347+
3348+
3349+def download_and_check(callback=None):
3350+ """
3351+ Download the web site and deploy it.
3352+ """
3353+ sha256, version = download_sha256()
3354+ file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip')
3355+ callback.setRange(0, file_size)
3356+ if url_get_file(callback, '{host}{name}'.format(host='https://get.openlp.org/webclient/', name='site.zip'),
3357+ os.path.join(str(AppLocation.get_section_data_path('remotes')), 'site.zip'),
3358+ sha256=sha256):
3359+ deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip')
3360
3361=== added file 'openlp/plugins/remotes/endpoint.py'
3362--- openlp/plugins/remotes/endpoint.py 1970-01-01 00:00:00 +0000
3363+++ openlp/plugins/remotes/endpoint.py 2017-08-13 07:11:15 +0000
3364@@ -0,0 +1,46 @@
3365+# -*- coding: utf-8 -*-
3366+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
3367+
3368+###############################################################################
3369+# OpenLP - Open Source Lyrics Projection #
3370+# --------------------------------------------------------------------------- #
3371+# Copyright (c) 2008-2017 OpenLP Developers #
3372+# --------------------------------------------------------------------------- #
3373+# This program is free software; you can redistribute it and/or modify it #
3374+# under the terms of the GNU General Public License as published by the Free #
3375+# Software Foundation; version 2 of the License. #
3376+# #
3377+# This program is distributed in the hope that it will be useful, but WITHOUT #
3378+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
3379+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
3380+# more details. #
3381+# #
3382+# You should have received a copy of the GNU General Public License along #
3383+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
3384+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
3385+###############################################################################
3386+import logging
3387+
3388+import os
3389+
3390+from openlp.core.api.http.endpoint import Endpoint
3391+from openlp.core.api.endpoint.core import TRANSLATED_STRINGS
3392+from openlp.core.common import AppLocation
3393+
3394+
3395+static_dir = os.path.join(str(AppLocation.get_section_data_path('remotes')))
3396+
3397+log = logging.getLogger(__name__)
3398+
3399+remote_endpoint = Endpoint('remote', template_dir=static_dir, static_dir=static_dir)
3400+
3401+
3402+@remote_endpoint.route('{view}')
3403+def index(request, view):
3404+ """
3405+ Handles requests for /remotes url
3406+
3407+ :param request: The http request object.
3408+ :param view: The view name to be servered.
3409+ """
3410+ return remote_endpoint.render_template('{view}.mako'.format(view=view), **TRANSLATED_STRINGS)
3411
3412=== removed directory 'openlp/plugins/remotes/html'
3413=== removed directory 'openlp/plugins/remotes/html/assets'
3414=== removed file 'openlp/plugins/remotes/html/assets/jquery.js'
3415--- openlp/plugins/remotes/html/assets/jquery.js 2016-03-19 18:49:55 +0000
3416+++ openlp/plugins/remotes/html/assets/jquery.js 1970-01-01 00:00:00 +0000
3417@@ -1,9404 +0,0 @@
3418-/*!
3419- * jQuery JavaScript Library v1.7.2
3420- * http://jquery.com/
3421- *
3422- * Copyright 2011, John Resig
3423- * Dual licensed under the MIT or GPL Version 2 licenses.
3424- * http://jquery.org/license
3425- *
3426- * Includes Sizzle.js
3427- * http://sizzlejs.com/
3428- * Copyright 2011, The Dojo Foundation
3429- * Released under the MIT, BSD, and GPL Licenses.
3430- *
3431- * Date: Wed Mar 21 12:46:34 2012 -0700
3432- */
3433-(function( window, undefined ) {
3434-
3435-// Use the correct document accordingly with window argument (sandbox)
3436-var document = window.document,
3437- navigator = window.navigator,
3438- location = window.location;
3439-var jQuery = (function() {
3440-
3441-// Define a local copy of jQuery
3442-var jQuery = function( selector, context ) {
3443- // The jQuery object is actually just the init constructor 'enhanced'
3444- return new jQuery.fn.init( selector, context, rootjQuery );
3445- },
3446-
3447- // Map over jQuery in case of overwrite
3448- _jQuery = window.jQuery,
3449-
3450- // Map over the $ in case of overwrite
3451- _$ = window.$,
3452-
3453- // A central reference to the root jQuery(document)
3454- rootjQuery,
3455-
3456- // A simple way to check for HTML strings or ID strings
3457- // Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
3458- quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,
3459-
3460- // Check if a string has a non-whitespace character in it
3461- rnotwhite = /\S/,
3462-
3463- // Used for trimming whitespace
3464- trimLeft = /^\s+/,
3465- trimRight = /\s+$/,
3466-
3467- // Match a standalone tag
3468- rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,
3469-
3470- // JSON RegExp
3471- rvalidchars = /^[\],:{}\s]*$/,
3472- rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,
3473- rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,
3474- rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
3475-
3476- // Useragent RegExp
3477- rwebkit = /(webkit)[ \/]([\w.]+)/,
3478- ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/,
3479- rmsie = /(msie) ([\w.]+)/,
3480- rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/,
3481-
3482- // Matches dashed string for camelizing
3483- rdashAlpha = /-([a-z]|[0-9])/ig,
3484- rmsPrefix = /^-ms-/,
3485-
3486- // Used by jQuery.camelCase as callback to replace()
3487- fcamelCase = function( all, letter ) {
3488- return ( letter + "" ).toUpperCase();
3489- },
3490-
3491- // Keep a UserAgent string for use with jQuery.browser
3492- userAgent = navigator.userAgent,
3493-
3494- // For matching the engine and version of the browser
3495- browserMatch,
3496-
3497- // The deferred used on DOM ready
3498- readyList,
3499-
3500- // The ready event handler
3501- DOMContentLoaded,
3502-
3503- // Save a reference to some core methods
3504- toString = Object.prototype.toString,
3505- hasOwn = Object.prototype.hasOwnProperty,
3506- push = Array.prototype.push,
3507- slice = Array.prototype.slice,
3508- trim = String.prototype.trim,
3509- indexOf = Array.prototype.indexOf,
3510-
3511- // [[Class]] -> type pairs
3512- class2type = {};
3513-
3514-jQuery.fn = jQuery.prototype = {
3515- constructor: jQuery,
3516- init: function( selector, context, rootjQuery ) {
3517- var match, elem, ret, doc;
3518-
3519- // Handle $(""), $(null), or $(undefined)
3520- if ( !selector ) {
3521- return this;
3522- }
3523-
3524- // Handle $(DOMElement)
3525- if ( selector.nodeType ) {
3526- this.context = this[0] = selector;
3527- this.length = 1;
3528- return this;
3529- }
3530-
3531- // The body element only exists once, optimize finding it
3532- if ( selector === "body" && !context && document.body ) {
3533- this.context = document;
3534- this[0] = document.body;
3535- this.selector = selector;
3536- this.length = 1;
3537- return this;
3538- }
3539-
3540- // Handle HTML strings
3541- if ( typeof selector === "string" ) {
3542- // Are we dealing with HTML string or an ID?
3543- if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
3544- // Assume that strings that start and end with <> are HTML and skip the regex check
3545- match = [ null, selector, null ];
3546-
3547- } else {
3548- match = quickExpr.exec( selector );
3549- }
3550-
3551- // Verify a match, and that no context was specified for #id
3552- if ( match && (match[1] || !context) ) {
3553-
3554- // HANDLE: $(html) -> $(array)
3555- if ( match[1] ) {
3556- context = context instanceof jQuery ? context[0] : context;
3557- doc = ( context ? context.ownerDocument || context : document );
3558-
3559- // If a single string is passed in and it's a single tag
3560- // just do a createElement and skip the rest
3561- ret = rsingleTag.exec( selector );
3562-
3563- if ( ret ) {
3564- if ( jQuery.isPlainObject( context ) ) {
3565- selector = [ document.createElement( ret[1] ) ];
3566- jQuery.fn.attr.call( selector, context, true );
3567-
3568- } else {
3569- selector = [ doc.createElement( ret[1] ) ];
3570- }
3571-
3572- } else {
3573- ret = jQuery.buildFragment( [ match[1] ], [ doc ] );
3574- selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes;
3575- }
3576-
3577- return jQuery.merge( this, selector );
3578-
3579- // HANDLE: $("#id")
3580- } else {
3581- elem = document.getElementById( match[2] );
3582-
3583- // Check parentNode to catch when Blackberry 4.6 returns
3584- // nodes that are no longer in the document #6963
3585- if ( elem && elem.parentNode ) {
3586- // Handle the case where IE and Opera return items
3587- // by name instead of ID
3588- if ( elem.id !== match[2] ) {
3589- return rootjQuery.find( selector );
3590- }
3591-
3592- // Otherwise, we inject the element directly into the jQuery object
3593- this.length = 1;
3594- this[0] = elem;
3595- }
3596-
3597- this.context = document;
3598- this.selector = selector;
3599- return this;
3600- }
3601-
3602- // HANDLE: $(expr, $(...))
3603- } else if ( !context || context.jquery ) {
3604- return ( context || rootjQuery ).find( selector );
3605-
3606- // HANDLE: $(expr, context)
3607- // (which is just equivalent to: $(context).find(expr)
3608- } else {
3609- return this.constructor( context ).find( selector );
3610- }
3611-
3612- // HANDLE: $(function)
3613- // Shortcut for document ready
3614- } else if ( jQuery.isFunction( selector ) ) {
3615- return rootjQuery.ready( selector );
3616- }
3617-
3618- if ( selector.selector !== undefined ) {
3619- this.selector = selector.selector;
3620- this.context = selector.context;
3621- }
3622-
3623- return jQuery.makeArray( selector, this );
3624- },
3625-
3626- // Start with an empty selector
3627- selector: "",
3628-
3629- // The current version of jQuery being used
3630- jquery: "1.7.2",
3631-
3632- // The default length of a jQuery object is 0
3633- length: 0,
3634-
3635- // The number of elements contained in the matched element set
3636- size: function() {
3637- return this.length;
3638- },
3639-
3640- toArray: function() {
3641- return slice.call( this, 0 );
3642- },
3643-
3644- // Get the Nth element in the matched element set OR
3645- // Get the whole matched element set as a clean array
3646- get: function( num ) {
3647- return num == null ?
3648-
3649- // Return a 'clean' array
3650- this.toArray() :
3651-
3652- // Return just the object
3653- ( num < 0 ? this[ this.length + num ] : this[ num ] );
3654- },
3655-
3656- // Take an array of elements and push it onto the stack
3657- // (returning the new matched element set)
3658- pushStack: function( elems, name, selector ) {
3659- // Build a new jQuery matched element set
3660- var ret = this.constructor();
3661-
3662- if ( jQuery.isArray( elems ) ) {
3663- push.apply( ret, elems );
3664-
3665- } else {
3666- jQuery.merge( ret, elems );
3667- }
3668-
3669- // Add the old object onto the stack (as a reference)
3670- ret.prevObject = this;
3671-
3672- ret.context = this.context;
3673-
3674- if ( name === "find" ) {
3675- ret.selector = this.selector + ( this.selector ? " " : "" ) + selector;
3676- } else if ( name ) {
3677- ret.selector = this.selector + "." + name + "(" + selector + ")";
3678- }
3679-
3680- // Return the newly-formed element set
3681- return ret;
3682- },
3683-
3684- // Execute a callback for every element in the matched set.
3685- // (You can seed the arguments with an array of args, but this is
3686- // only used internally.)
3687- each: function( callback, args ) {
3688- return jQuery.each( this, callback, args );
3689- },
3690-
3691- ready: function( fn ) {
3692- // Attach the listeners
3693- jQuery.bindReady();
3694-
3695- // Add the callback
3696- readyList.add( fn );
3697-
3698- return this;
3699- },
3700-
3701- eq: function( i ) {
3702- i = +i;
3703- return i === -1 ?
3704- this.slice( i ) :
3705- this.slice( i, i + 1 );
3706- },
3707-
3708- first: function() {
3709- return this.eq( 0 );
3710- },
3711-
3712- last: function() {
3713- return this.eq( -1 );
3714- },
3715-
3716- slice: function() {
3717- return this.pushStack( slice.apply( this, arguments ),
3718- "slice", slice.call(arguments).join(",") );
3719- },
3720-
3721- map: function( callback ) {
3722- return this.pushStack( jQuery.map(this, function( elem, i ) {
3723- return callback.call( elem, i, elem );
3724- }));
3725- },
3726-
3727- end: function() {
3728- return this.prevObject || this.constructor(null);
3729- },
3730-
3731- // For internal use only.
3732- // Behaves like an Array's method, not like a jQuery method.
3733- push: push,
3734- sort: [].sort,
3735- splice: [].splice
3736-};
3737-
3738-// Give the init function the jQuery prototype for later instantiation
3739-jQuery.fn.init.prototype = jQuery.fn;
3740-
3741-jQuery.extend = jQuery.fn.extend = function() {
3742- var options, name, src, copy, copyIsArray, clone,
3743- target = arguments[0] || {},
3744- i = 1,
3745- length = arguments.length,
3746- deep = false;
3747-
3748- // Handle a deep copy situation
3749- if ( typeof target === "boolean" ) {
3750- deep = target;
3751- target = arguments[1] || {};
3752- // skip the boolean and the target
3753- i = 2;
3754- }
3755-
3756- // Handle case when target is a string or something (possible in deep copy)
3757- if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
3758- target = {};
3759- }
3760-
3761- // extend jQuery itself if only one argument is passed
3762- if ( length === i ) {
3763- target = this;
3764- --i;
3765- }
3766-
3767- for ( ; i < length; i++ ) {
3768- // Only deal with non-null/undefined values
3769- if ( (options = arguments[ i ]) != null ) {
3770- // Extend the base object
3771- for ( name in options ) {
3772- src = target[ name ];
3773- copy = options[ name ];
3774-
3775- // Prevent never-ending loop
3776- if ( target === copy ) {
3777- continue;
3778- }
3779-
3780- // Recurse if we're merging plain objects or arrays
3781- if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
3782- if ( copyIsArray ) {
3783- copyIsArray = false;
3784- clone = src && jQuery.isArray(src) ? src : [];
3785-
3786- } else {
3787- clone = src && jQuery.isPlainObject(src) ? src : {};
3788- }
3789-
3790- // Never move original objects, clone them
3791- target[ name ] = jQuery.extend( deep, clone, copy );
3792-
3793- // Don't bring in undefined values
3794- } else if ( copy !== undefined ) {
3795- target[ name ] = copy;
3796- }
3797- }
3798- }
3799- }
3800-
3801- // Return the modified object
3802- return target;
3803-};
3804-
3805-jQuery.extend({
3806- noConflict: function( deep ) {
3807- if ( window.$ === jQuery ) {
3808- window.$ = _$;
3809- }
3810-
3811- if ( deep && window.jQuery === jQuery ) {
3812- window.jQuery = _jQuery;
3813- }
3814-
3815- return jQuery;
3816- },
3817-
3818- // Is the DOM ready to be used? Set to true once it occurs.
3819- isReady: false,
3820-
3821- // A counter to track how many items to wait for before
3822- // the ready event fires. See #6781
3823- readyWait: 1,
3824-
3825- // Hold (or release) the ready event
3826- holdReady: function( hold ) {
3827- if ( hold ) {
3828- jQuery.readyWait++;
3829- } else {
3830- jQuery.ready( true );
3831- }
3832- },
3833-
3834- // Handle when the DOM is ready
3835- ready: function( wait ) {
3836- // Either a released hold or an DOMready/load event and not yet ready
3837- if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) {
3838- // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
3839- if ( !document.body ) {
3840- return setTimeout( jQuery.ready, 1 );
3841- }
3842-
3843- // Remember that the DOM is ready
3844- jQuery.isReady = true;
3845-
3846- // If a normal DOM Ready event fired, decrement, and wait if need be
3847- if ( wait !== true && --jQuery.readyWait > 0 ) {
3848- return;
3849- }
3850-
3851- // If there are functions bound, to execute
3852- readyList.fireWith( document, [ jQuery ] );
3853-
3854- // Trigger any bound ready events
3855- if ( jQuery.fn.trigger ) {
3856- jQuery( document ).trigger( "ready" ).off( "ready" );
3857- }
3858- }
3859- },
3860-
3861- bindReady: function() {
3862- if ( readyList ) {
3863- return;
3864- }
3865-
3866- readyList = jQuery.Callbacks( "once memory" );
3867-
3868- // Catch cases where $(document).ready() is called after the
3869- // browser event has already occurred.
3870- if ( document.readyState === "complete" ) {
3871- // Handle it asynchronously to allow scripts the opportunity to delay ready
3872- return setTimeout( jQuery.ready, 1 );
3873- }
3874-
3875- // Mozilla, Opera and webkit nightlies currently support this event
3876- if ( document.addEventListener ) {
3877- // Use the handy event callback
3878- document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
3879-
3880- // A fallback to window.onload, that will always work
3881- window.addEventListener( "load", jQuery.ready, false );
3882-
3883- // If IE event model is used
3884- } else if ( document.attachEvent ) {
3885- // ensure firing before onload,
3886- // maybe late but safe also for iframes
3887- document.attachEvent( "onreadystatechange", DOMContentLoaded );
3888-
3889- // A fallback to window.onload, that will always work
3890- window.attachEvent( "onload", jQuery.ready );
3891-
3892- // If IE and not a frame
3893- // continually check to see if the document is ready
3894- var toplevel = false;
3895-
3896- try {
3897- toplevel = window.frameElement == null;
3898- } catch(e) {}
3899-
3900- if ( document.documentElement.doScroll && toplevel ) {
3901- doScrollCheck();
3902- }
3903- }
3904- },
3905-
3906- // See test/unit/core.js for details concerning isFunction.
3907- // Since version 1.3, DOM methods and functions like alert
3908- // aren't supported. They return false on IE (#2968).
3909- isFunction: function( obj ) {
3910- return jQuery.type(obj) === "function";
3911- },
3912-
3913- isArray: Array.isArray || function( obj ) {
3914- return jQuery.type(obj) === "array";
3915- },
3916-
3917- isWindow: function( obj ) {
3918- return obj != null && obj == obj.window;
3919- },
3920-
3921- isNumeric: function( obj ) {
3922- return !isNaN( parseFloat(obj) ) && isFinite( obj );
3923- },
3924-
3925- type: function( obj ) {
3926- return obj == null ?
3927- String( obj ) :
3928- class2type[ toString.call(obj) ] || "object";
3929- },
3930-
3931- isPlainObject: function( obj ) {
3932- // Must be an Object.
3933- // Because of IE, we also have to check the presence of the constructor property.
3934- // Make sure that DOM nodes and window objects don't pass through, as well
3935- if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
3936- return false;
3937- }
3938-
3939- try {
3940- // Not own constructor property must be Object
3941- if ( obj.constructor &&
3942- !hasOwn.call(obj, "constructor") &&
3943- !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
3944- return false;
3945- }
3946- } catch ( e ) {
3947- // IE8,9 Will throw exceptions on certain host objects #9897
3948- return false;
3949- }
3950-
3951- // Own properties are enumerated firstly, so to speed up,
3952- // if last one is own, then all properties are own.
3953-
3954- var key;
3955- for ( key in obj ) {}
3956-
3957- return key === undefined || hasOwn.call( obj, key );
3958- },
3959-
3960- isEmptyObject: function( obj ) {
3961- for ( var name in obj ) {
3962- return false;
3963- }
3964- return true;
3965- },
3966-
3967- error: function( msg ) {
3968- throw new Error( msg );
3969- },
3970-
3971- parseJSON: function( data ) {
3972- if ( typeof data !== "string" || !data ) {
3973- return null;
3974- }
3975-
3976- // Make sure leading/trailing whitespace is removed (IE can't handle it)
3977- data = jQuery.trim( data );
3978-
3979- // Attempt to parse using the native JSON parser first
3980- if ( window.JSON && window.JSON.parse ) {
3981- return window.JSON.parse( data );
3982- }
3983-
3984- // Make sure the incoming data is actual JSON
3985- // Logic borrowed from http://json.org/json2.js
3986- if ( rvalidchars.test( data.replace( rvalidescape, "@" )
3987- .replace( rvalidtokens, "]" )
3988- .replace( rvalidbraces, "")) ) {
3989-
3990- return ( new Function( "return " + data ) )();
3991-
3992- }
3993- jQuery.error( "Invalid JSON: " + data );
3994- },
3995-
3996- // Cross-browser xml parsing
3997- parseXML: function( data ) {
3998- if ( typeof data !== "string" || !data ) {
3999- return null;
4000- }
4001- var xml, tmp;
4002- try {
4003- if ( window.DOMParser ) { // Standard
4004- tmp = new DOMParser();
4005- xml = tmp.parseFromString( data , "text/xml" );
4006- } else { // IE
4007- xml = new ActiveXObject( "Microsoft.XMLDOM" );
4008- xml.async = "false";
4009- xml.loadXML( data );
4010- }
4011- } catch( e ) {
4012- xml = undefined;
4013- }
4014- if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) {
4015- jQuery.error( "Invalid XML: " + data );
4016- }
4017- return xml;
4018- },
4019-
4020- noop: function() {},
4021-
4022- // Evaluates a script in a global context
4023- // Workarounds based on findings by Jim Driscoll
4024- // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
4025- globalEval: function( data ) {
4026- if ( data && rnotwhite.test( data ) ) {
4027- // We use execScript on Internet Explorer
4028- // We use an anonymous function so that context is window
4029- // rather than jQuery in Firefox
4030- ( window.execScript || function( data ) {
4031- window[ "eval" ].call( window, data );
4032- } )( data );
4033- }
4034- },
4035-
4036- // Convert dashed to camelCase; used by the css and data modules
4037- // Microsoft forgot to hump their vendor prefix (#9572)
4038- camelCase: function( string ) {
4039- return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
4040- },
4041-
4042- nodeName: function( elem, name ) {
4043- return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
4044- },
4045-
4046- // args is for internal usage only
4047- each: function( object, callback, args ) {
4048- var name, i = 0,
4049- length = object.length,
4050- isObj = length === undefined || jQuery.isFunction( object );
4051-
4052- if ( args ) {
4053- if ( isObj ) {
4054- for ( name in object ) {
4055- if ( callback.apply( object[ name ], args ) === false ) {
4056- break;
4057- }
4058- }
4059- } else {
4060- for ( ; i < length; ) {
4061- if ( callback.apply( object[ i++ ], args ) === false ) {
4062- break;
4063- }
4064- }
4065- }
4066-
4067- // A special, fast, case for the most common use of each
4068- } else {
4069- if ( isObj ) {
4070- for ( name in object ) {
4071- if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
4072- break;
4073- }
4074- }
4075- } else {
4076- for ( ; i < length; ) {
4077- if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) {
4078- break;
4079- }
4080- }
4081- }
4082- }
4083-
4084- return object;
4085- },
4086-
4087- // Use native String.trim function wherever possible
4088- trim: trim ?
4089- function( text ) {
4090- return text == null ?
4091- "" :
4092- trim.call( text );
4093- } :
4094-
4095- // Otherwise use our own trimming functionality
4096- function( text ) {
4097- return text == null ?
4098- "" :
4099- text.toString().replace( trimLeft, "" ).replace( trimRight, "" );
4100- },
4101-
4102- // results is for internal usage only
4103- makeArray: function( array, results ) {
4104- var ret = results || [];
4105-
4106- if ( array != null ) {
4107- // The window, strings (and functions) also have 'length'
4108- // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930
4109- var type = jQuery.type( array );
4110-
4111- if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) {
4112- push.call( ret, array );
4113- } else {
4114- jQuery.merge( ret, array );
4115- }
4116- }
4117-
4118- return ret;
4119- },
4120-
4121- inArray: function( elem, array, i ) {
4122- var len;
4123-
4124- if ( array ) {
4125- if ( indexOf ) {
4126- return indexOf.call( array, elem, i );
4127- }
4128-
4129- len = array.length;
4130- i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
4131-
4132- for ( ; i < len; i++ ) {
4133- // Skip accessing in sparse arrays
4134- if ( i in array && array[ i ] === elem ) {
4135- return i;
4136- }
4137- }
4138- }
4139-
4140- return -1;
4141- },
4142-
4143- merge: function( first, second ) {
4144- var i = first.length,
4145- j = 0;
4146-
4147- if ( typeof second.length === "number" ) {
4148- for ( var l = second.length; j < l; j++ ) {
4149- first[ i++ ] = second[ j ];
4150- }
4151-
4152- } else {
4153- while ( second[j] !== undefined ) {
4154- first[ i++ ] = second[ j++ ];
4155- }
4156- }
4157-
4158- first.length = i;
4159-
4160- return first;
4161- },
4162-
4163- grep: function( elems, callback, inv ) {
4164- var ret = [], retVal;
4165- inv = !!inv;
4166-
4167- // Go through the array, only saving the items
4168- // that pass the validator function
4169- for ( var i = 0, length = elems.length; i < length; i++ ) {
4170- retVal = !!callback( elems[ i ], i );
4171- if ( inv !== retVal ) {
4172- ret.push( elems[ i ] );
4173- }
4174- }
4175-
4176- return ret;
4177- },
4178-
4179- // arg is for internal usage only
4180- map: function( elems, callback, arg ) {
4181- var value, key, ret = [],
4182- i = 0,
4183- length = elems.length,
4184- // jquery objects are treated as arrays
4185- isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ;
4186-
4187- // Go through the array, translating each of the items to their
4188- if ( isArray ) {
4189- for ( ; i < length; i++ ) {
4190- value = callback( elems[ i ], i, arg );
4191-
4192- if ( value != null ) {
4193- ret[ ret.length ] = value;
4194- }
4195- }
4196-
4197- // Go through every key on the object,
4198- } else {
4199- for ( key in elems ) {
4200- value = callback( elems[ key ], key, arg );
4201-
4202- if ( value != null ) {
4203- ret[ ret.length ] = value;
4204- }
4205- }
4206- }
4207-
4208- // Flatten any nested arrays
4209- return ret.concat.apply( [], ret );
4210- },
4211-
4212- // A global GUID counter for objects
4213- guid: 1,
4214-
4215- // Bind a function to a context, optionally partially applying any
4216- // arguments.
4217- proxy: function( fn, context ) {
4218- if ( typeof context === "string" ) {
4219- var tmp = fn[ context ];
4220- context = fn;
4221- fn = tmp;
4222- }
4223-
4224- // Quick check to determine if target is callable, in the spec
4225- // this throws a TypeError, but we will just return undefined.
4226- if ( !jQuery.isFunction( fn ) ) {
4227- return undefined;
4228- }
4229-
4230- // Simulated bind
4231- var args = slice.call( arguments, 2 ),
4232- proxy = function() {
4233- return fn.apply( context, args.concat( slice.call( arguments ) ) );
4234- };
4235-
4236- // Set the guid of unique handler to the same of original handler, so it can be removed
4237- proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;
4238-
4239- return proxy;
4240- },
4241-
4242- // Mutifunctional method to get and set values to a collection
4243- // The value/s can optionally be executed if it's a function
4244- access: function( elems, fn, key, value, chainable, emptyGet, pass ) {
4245- var exec,
4246- bulk = key == null,
4247- i = 0,
4248- length = elems.length;
4249-
4250- // Sets many values
4251- if ( key && typeof key === "object" ) {
4252- for ( i in key ) {
4253- jQuery.access( elems, fn, i, key[i], 1, emptyGet, value );
4254- }
4255- chainable = 1;
4256-
4257- // Sets one value
4258- } else if ( value !== undefined ) {
4259- // Optionally, function values get executed if exec is true
4260- exec = pass === undefined && jQuery.isFunction( value );
4261-
4262- if ( bulk ) {
4263- // Bulk operations only iterate when executing function values
4264- if ( exec ) {
4265- exec = fn;
4266- fn = function( elem, key, value ) {
4267- return exec.call( jQuery( elem ), value );
4268- };
4269-
4270- // Otherwise they run against the entire set
4271- } else {
4272- fn.call( elems, value );
4273- fn = null;
4274- }
4275- }
4276-
4277- if ( fn ) {
4278- for (; i < length; i++ ) {
4279- fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
4280- }
4281- }
4282-
4283- chainable = 1;
4284- }
4285-
4286- return chainable ?
4287- elems :
4288-
4289- // Gets
4290- bulk ?
4291- fn.call( elems ) :
4292- length ? fn( elems[0], key ) : emptyGet;
4293- },
4294-
4295- now: function() {
4296- return ( new Date() ).getTime();
4297- },
4298-
4299- // Use of jQuery.browser is frowned upon.
4300- // More details: http://docs.jquery.com/Utilities/jQuery.browser
4301- uaMatch: function( ua ) {
4302- ua = ua.toLowerCase();
4303-
4304- var match = rwebkit.exec( ua ) ||
4305- ropera.exec( ua ) ||
4306- rmsie.exec( ua ) ||
4307- ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) ||
4308- [];
4309-
4310- return { browser: match[1] || "", version: match[2] || "0" };
4311- },
4312-
4313- sub: function() {
4314- function jQuerySub( selector, context ) {
4315- return new jQuerySub.fn.init( selector, context );
4316- }
4317- jQuery.extend( true, jQuerySub, this );
4318- jQuerySub.superclass = this;
4319- jQuerySub.fn = jQuerySub.prototype = this();
4320- jQuerySub.fn.constructor = jQuerySub;
4321- jQuerySub.sub = this.sub;
4322- jQuerySub.fn.init = function init( selector, context ) {
4323- if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) {
4324- context = jQuerySub( context );
4325- }
4326-
4327- return jQuery.fn.init.call( this, selector, context, rootjQuerySub );
4328- };
4329- jQuerySub.fn.init.prototype = jQuerySub.fn;
4330- var rootjQuerySub = jQuerySub(document);
4331- return jQuerySub;
4332- },
4333-
4334- browser: {}
4335-});
4336-
4337-// Populate the class2type map
4338-jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
4339- class2type[ "[object " + name + "]" ] = name.toLowerCase();
4340-});
4341-
4342-browserMatch = jQuery.uaMatch( userAgent );
4343-if ( browserMatch.browser ) {
4344- jQuery.browser[ browserMatch.browser ] = true;
4345- jQuery.browser.version = browserMatch.version;
4346-}
4347-
4348-// Deprecated, use jQuery.browser.webkit instead
4349-if ( jQuery.browser.webkit ) {
4350- jQuery.browser.safari = true;
4351-}
4352-
4353-// IE doesn't match non-breaking spaces with \s
4354-if ( rnotwhite.test( "\xA0" ) ) {
4355- trimLeft = /^[\s\xA0]+/;
4356- trimRight = /[\s\xA0]+$/;
4357-}
4358-
4359-// All jQuery objects should point back to these
4360-rootjQuery = jQuery(document);
4361-
4362-// Cleanup functions for the document ready method
4363-if ( document.addEventListener ) {
4364- DOMContentLoaded = function() {
4365- document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
4366- jQuery.ready();
4367- };
4368-
4369-} else if ( document.attachEvent ) {
4370- DOMContentLoaded = function() {
4371- // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
4372- if ( document.readyState === "complete" ) {
4373- document.detachEvent( "onreadystatechange", DOMContentLoaded );
4374- jQuery.ready();
4375- }
4376- };
4377-}
4378-
4379-// The DOM ready check for Internet Explorer
4380-function doScrollCheck() {
4381- if ( jQuery.isReady ) {
4382- return;
4383- }
4384-
4385- try {
4386- // If IE is used, use the trick by Diego Perini
4387- // http://javascript.nwbox.com/IEContentLoaded/
4388- document.documentElement.doScroll("left");
4389- } catch(e) {
4390- setTimeout( doScrollCheck, 1 );
4391- return;
4392- }
4393-
4394- // and execute any waiting functions
4395- jQuery.ready();
4396-}
4397-
4398-return jQuery;
4399-
4400-})();
4401-
4402-
4403-// String to Object flags format cache
4404-var flagsCache = {};
4405-
4406-// Convert String-formatted flags into Object-formatted ones and store in cache
4407-function createFlags( flags ) {
4408- var object = flagsCache[ flags ] = {},
4409- i, length;
4410- flags = flags.split( /\s+/ );
4411- for ( i = 0, length = flags.length; i < length; i++ ) {
4412- object[ flags[i] ] = true;
4413- }
4414- return object;
4415-}
4416-
4417-/*
4418- * Create a callback list using the following parameters:
4419- *
4420- * flags: an optional list of space-separated flags that will change how
4421- * the callback list behaves
4422- *
4423- * By default a callback list will act like an event callback list and can be
4424- * "fired" multiple times.
4425- *
4426- * Possible flags:
4427- *
4428- * once: will ensure the callback list can only be fired once (like a Deferred)
4429- *
4430- * memory: will keep track of previous values and will call any callback added
4431- * after the list has been fired right away with the latest "memorized"
4432- * values (like a Deferred)
4433- *
4434- * unique: will ensure a callback can only be added once (no duplicate in the list)
4435- *
4436- * stopOnFalse: interrupt callings when a callback returns false
4437- *
4438- */
4439-jQuery.Callbacks = function( flags ) {
4440-
4441- // Convert flags from String-formatted to Object-formatted
4442- // (we check in cache first)
4443- flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {};
4444-
4445- var // Actual callback list
4446- list = [],
4447- // Stack of fire calls for repeatable lists
4448- stack = [],
4449- // Last fire value (for non-forgettable lists)
4450- memory,
4451- // Flag to know if list was already fired
4452- fired,
4453- // Flag to know if list is currently firing
4454- firing,
4455- // First callback to fire (used internally by add and fireWith)
4456- firingStart,
4457- // End of the loop when firing
4458- firingLength,
4459- // Index of currently firing callback (modified by remove if needed)
4460- firingIndex,
4461- // Add one or several callbacks to the list
4462- add = function( args ) {
4463- var i,
4464- length,
4465- elem,
4466- type,
4467- actual;
4468- for ( i = 0, length = args.length; i < length; i++ ) {
4469- elem = args[ i ];
4470- type = jQuery.type( elem );
4471- if ( type === "array" ) {
4472- // Inspect recursively
4473- add( elem );
4474- } else if ( type === "function" ) {
4475- // Add if not in unique mode and callback is not in
4476- if ( !flags.unique || !self.has( elem ) ) {
4477- list.push( elem );
4478- }
4479- }
4480- }
4481- },
4482- // Fire callbacks
4483- fire = function( context, args ) {
4484- args = args || [];
4485- memory = !flags.memory || [ context, args ];
4486- fired = true;
4487- firing = true;
4488- firingIndex = firingStart || 0;
4489- firingStart = 0;
4490- firingLength = list.length;
4491- for ( ; list && firingIndex < firingLength; firingIndex++ ) {
4492- if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) {
4493- memory = true; // Mark as halted
4494- break;
4495- }
4496- }
4497- firing = false;
4498- if ( list ) {
4499- if ( !flags.once ) {
4500- if ( stack && stack.length ) {
4501- memory = stack.shift();
4502- self.fireWith( memory[ 0 ], memory[ 1 ] );
4503- }
4504- } else if ( memory === true ) {
4505- self.disable();
4506- } else {
4507- list = [];
4508- }
4509- }
4510- },
4511- // Actual Callbacks object
4512- self = {
4513- // Add a callback or a collection of callbacks to the list
4514- add: function() {
4515- if ( list ) {
4516- var length = list.length;
4517- add( arguments );
4518- // Do we need to add the callbacks to the
4519- // current firing batch?
4520- if ( firing ) {
4521- firingLength = list.length;
4522- // With memory, if we're not firing then
4523- // we should call right away, unless previous
4524- // firing was halted (stopOnFalse)
4525- } else if ( memory && memory !== true ) {
4526- firingStart = length;
4527- fire( memory[ 0 ], memory[ 1 ] );
4528- }
4529- }
4530- return this;
4531- },
4532- // Remove a callback from the list
4533- remove: function() {
4534- if ( list ) {
4535- var args = arguments,
4536- argIndex = 0,
4537- argLength = args.length;
4538- for ( ; argIndex < argLength ; argIndex++ ) {
4539- for ( var i = 0; i < list.length; i++ ) {
4540- if ( args[ argIndex ] === list[ i ] ) {
4541- // Handle firingIndex and firingLength
4542- if ( firing ) {
4543- if ( i <= firingLength ) {
4544- firingLength--;
4545- if ( i <= firingIndex ) {
4546- firingIndex--;
4547- }
4548- }
4549- }
4550- // Remove the element
4551- list.splice( i--, 1 );
4552- // If we have some unicity property then
4553- // we only need to do this once
4554- if ( flags.unique ) {
4555- break;
4556- }
4557- }
4558- }
4559- }
4560- }
4561- return this;
4562- },
4563- // Control if a given callback is in the list
4564- has: function( fn ) {
4565- if ( list ) {
4566- var i = 0,
4567- length = list.length;
4568- for ( ; i < length; i++ ) {
4569- if ( fn === list[ i ] ) {
4570- return true;
4571- }
4572- }
4573- }
4574- return false;
4575- },
4576- // Remove all callbacks from the list
4577- empty: function() {
4578- list = [];
4579- return this;
4580- },
4581- // Have the list do nothing anymore
4582- disable: function() {
4583- list = stack = memory = undefined;
4584- return this;
4585- },
4586- // Is it disabled?
4587- disabled: function() {
4588- return !list;
4589- },
4590- // Lock the list in its current state
4591- lock: function() {
4592- stack = undefined;
4593- if ( !memory || memory === true ) {
4594- self.disable();
4595- }
4596- return this;
4597- },
4598- // Is it locked?
4599- locked: function() {
4600- return !stack;
4601- },
4602- // Call all callbacks with the given context and arguments
4603- fireWith: function( context, args ) {
4604- if ( stack ) {
4605- if ( firing ) {
4606- if ( !flags.once ) {
4607- stack.push( [ context, args ] );
4608- }
4609- } else if ( !( flags.once && memory ) ) {
4610- fire( context, args );
4611- }
4612- }
4613- return this;
4614- },
4615- // Call all the callbacks with the given arguments
4616- fire: function() {
4617- self.fireWith( this, arguments );
4618- return this;
4619- },
4620- // To know if the callbacks have already been called at least once
4621- fired: function() {
4622- return !!fired;
4623- }
4624- };
4625-
4626- return self;
4627-};
4628-
4629-
4630-
4631-
4632-var // Static reference to slice
4633- sliceDeferred = [].slice;
4634-
4635-jQuery.extend({
4636-
4637- Deferred: function( func ) {
4638- var doneList = jQuery.Callbacks( "once memory" ),
4639- failList = jQuery.Callbacks( "once memory" ),
4640- progressList = jQuery.Callbacks( "memory" ),
4641- state = "pending",
4642- lists = {
4643- resolve: doneList,
4644- reject: failList,
4645- notify: progressList
4646- },
4647- promise = {
4648- done: doneList.add,
4649- fail: failList.add,
4650- progress: progressList.add,
4651-
4652- state: function() {
4653- return state;
4654- },
4655-
4656- // Deprecated
4657- isResolved: doneList.fired,
4658- isRejected: failList.fired,
4659-
4660- then: function( doneCallbacks, failCallbacks, progressCallbacks ) {
4661- deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks );
4662- return this;
4663- },
4664- always: function() {
4665- deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments );
4666- return this;
4667- },
4668- pipe: function( fnDone, fnFail, fnProgress ) {
4669- return jQuery.Deferred(function( newDefer ) {
4670- jQuery.each( {
4671- done: [ fnDone, "resolve" ],
4672- fail: [ fnFail, "reject" ],
4673- progress: [ fnProgress, "notify" ]
4674- }, function( handler, data ) {
4675- var fn = data[ 0 ],
4676- action = data[ 1 ],
4677- returned;
4678- if ( jQuery.isFunction( fn ) ) {
4679- deferred[ handler ](function() {
4680- returned = fn.apply( this, arguments );
4681- if ( returned && jQuery.isFunction( returned.promise ) ) {
4682- returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify );
4683- } else {
4684- newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] );
4685- }
4686- });
4687- } else {
4688- deferred[ handler ]( newDefer[ action ] );
4689- }
4690- });
4691- }).promise();
4692- },
4693- // Get a promise for this deferred
4694- // If obj is provided, the promise aspect is added to the object
4695- promise: function( obj ) {
4696- if ( obj == null ) {
4697- obj = promise;
4698- } else {
4699- for ( var key in promise ) {
4700- obj[ key ] = promise[ key ];
4701- }
4702- }
4703- return obj;
4704- }
4705- },
4706- deferred = promise.promise({}),
4707- key;
4708-
4709- for ( key in lists ) {
4710- deferred[ key ] = lists[ key ].fire;
4711- deferred[ key + "With" ] = lists[ key ].fireWith;
4712- }
4713-
4714- // Handle state
4715- deferred.done( function() {
4716- state = "resolved";
4717- }, failList.disable, progressList.lock ).fail( function() {
4718- state = "rejected";
4719- }, doneList.disable, progressList.lock );
4720-
4721- // Call given func if any
4722- if ( func ) {
4723- func.call( deferred, deferred );
4724- }
4725-
4726- // All done!
4727- return deferred;
4728- },
4729-
4730- // Deferred helper
4731- when: function( firstParam ) {
4732- var args = sliceDeferred.call( arguments, 0 ),
4733- i = 0,
4734- length = args.length,
4735- pValues = new Array( length ),
4736- count = length,
4737- pCount = length,
4738- deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ?
4739- firstParam :
4740- jQuery.Deferred(),
4741- promise = deferred.promise();
4742- function resolveFunc( i ) {
4743- return function( value ) {
4744- args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
4745- if ( !( --count ) ) {
4746- deferred.resolveWith( deferred, args );
4747- }
4748- };
4749- }
4750- function progressFunc( i ) {
4751- return function( value ) {
4752- pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
4753- deferred.notifyWith( promise, pValues );
4754- };
4755- }
4756- if ( length > 1 ) {
4757- for ( ; i < length; i++ ) {
4758- if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) {
4759- args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) );
4760- } else {
4761- --count;
4762- }
4763- }
4764- if ( !count ) {
4765- deferred.resolveWith( deferred, args );
4766- }
4767- } else if ( deferred !== firstParam ) {
4768- deferred.resolveWith( deferred, length ? [ firstParam ] : [] );
4769- }
4770- return promise;
4771- }
4772-});
4773-
4774-
4775-
4776-
4777-jQuery.support = (function() {
4778-
4779- var support,
4780- all,
4781- a,
4782- select,
4783- opt,
4784- input,
4785- fragment,
4786- tds,
4787- events,
4788- eventName,
4789- i,
4790- isSupported,
4791- div = document.createElement( "div" ),
4792- documentElement = document.documentElement;
4793-
4794- // Preliminary tests
4795- div.setAttribute("className", "t");
4796- div.innerHTML = " <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>";
4797-
4798- all = div.getElementsByTagName( "*" );
4799- a = div.getElementsByTagName( "a" )[ 0 ];
4800-
4801- // Can't get basic test support
4802- if ( !all || !all.length || !a ) {
4803- return {};
4804- }
4805-
4806- // First batch of supports tests
4807- select = document.createElement( "select" );
4808- opt = select.appendChild( document.createElement("option") );
4809- input = div.getElementsByTagName( "input" )[ 0 ];
4810-
4811- support = {
4812- // IE strips leading whitespace when .innerHTML is used
4813- leadingWhitespace: ( div.firstChild.nodeType === 3 ),
4814-
4815- // Make sure that tbody elements aren't automatically inserted
4816- // IE will insert them into empty tables
4817- tbody: !div.getElementsByTagName("tbody").length,
4818-
4819- // Make sure that link elements get serialized correctly by innerHTML
4820- // This requires a wrapper element in IE
4821- htmlSerialize: !!div.getElementsByTagName("link").length,
4822-
4823- // Get the style information from getAttribute
4824- // (IE uses .cssText instead)
4825- style: /top/.test( a.getAttribute("style") ),
4826-
4827- // Make sure that URLs aren't manipulated
4828- // (IE normalizes it by default)
4829- hrefNormalized: ( a.getAttribute("href") === "/a" ),
4830-
4831- // Make sure that element opacity exists
4832- // (IE uses filter instead)
4833- // Use a regex to work around a WebKit issue. See #5145
4834- opacity: /^0.55/.test( a.style.opacity ),
4835-
4836- // Verify style float existence
4837- // (IE uses styleFloat instead of cssFloat)
4838- cssFloat: !!a.style.cssFloat,
4839-
4840- // Make sure that if no value is specified for a checkbox
4841- // that it defaults to "on".
4842- // (WebKit defaults to "" instead)
4843- checkOn: ( input.value === "on" ),
4844-
4845- // Make sure that a selected-by-default option has a working selected property.
4846- // (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
4847- optSelected: opt.selected,
4848-
4849- // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
4850- getSetAttribute: div.className !== "t",
4851-
4852- // Tests for enctype support on a form(#6743)
4853- enctype: !!document.createElement("form").enctype,
4854-
4855- // Makes sure cloning an html5 element does not cause problems
4856- // Where outerHTML is undefined, this still works
4857- html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>",
4858-
4859- // Will be defined later
4860- submitBubbles: true,
4861- changeBubbles: true,
4862- focusinBubbles: false,
4863- deleteExpando: true,
4864- noCloneEvent: true,
4865- inlineBlockNeedsLayout: false,
4866- shrinkWrapBlocks: false,
4867- reliableMarginRight: true,
4868- pixelMargin: true
4869- };
4870-
4871- // jQuery.boxModel DEPRECATED in 1.3, use jQuery.support.boxModel instead
4872- jQuery.boxModel = support.boxModel = (document.compatMode === "CSS1Compat");
4873-
4874- // Make sure checked status is properly cloned
4875- input.checked = true;
4876- support.noCloneChecked = input.cloneNode( true ).checked;
4877-
4878- // Make sure that the options inside disabled selects aren't marked as disabled
4879- // (WebKit marks them as disabled)
4880- select.disabled = true;
4881- support.optDisabled = !opt.disabled;
4882-
4883- // Test to see if it's possible to delete an expando from an element
4884- // Fails in Internet Explorer
4885- try {
4886- delete div.test;
4887- } catch( e ) {
4888- support.deleteExpando = false;
4889- }
4890-
4891- if ( !div.addEventListener && div.attachEvent && div.fireEvent ) {
4892- div.attachEvent( "onclick", function() {
4893- // Cloning a node shouldn't copy over any
4894- // bound event handlers (IE does this)
4895- support.noCloneEvent = false;
4896- });
4897- div.cloneNode( true ).fireEvent( "onclick" );
4898- }
4899-
4900- // Check if a radio maintains its value
4901- // after being appended to the DOM
4902- input = document.createElement("input");
4903- input.value = "t";
4904- input.setAttribute("type", "radio");
4905- support.radioValue = input.value === "t";
4906-
4907- input.setAttribute("checked", "checked");
4908-
4909- // #11217 - WebKit loses check when the name is after the checked attribute
4910- input.setAttribute( "name", "t" );
4911-
4912- div.appendChild( input );
4913- fragment = document.createDocumentFragment();
4914- fragment.appendChild( div.lastChild );
4915-
4916- // WebKit doesn't clone checked state correctly in fragments
4917- support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;
4918-
4919- // Check if a disconnected checkbox will retain its checked
4920- // value of true after appended to the DOM (IE6/7)
4921- support.appendChecked = input.checked;
4922-
4923- fragment.removeChild( input );
4924- fragment.appendChild( div );
4925-
4926- // Technique from Juriy Zaytsev
4927- // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/
4928- // We only care about the case where non-standard event systems
4929- // are used, namely in IE. Short-circuiting here helps us to
4930- // avoid an eval call (in setAttribute) which can cause CSP
4931- // to go haywire. See: https://developer.mozilla.org/en/Security/CSP
4932- if ( div.attachEvent ) {
4933- for ( i in {
4934- submit: 1,
4935- change: 1,
4936- focusin: 1
4937- }) {
4938- eventName = "on" + i;
4939- isSupported = ( eventName in div );
4940- if ( !isSupported ) {
4941- div.setAttribute( eventName, "return;" );
4942- isSupported = ( typeof div[ eventName ] === "function" );
4943- }
4944- support[ i + "Bubbles" ] = isSupported;
4945- }
4946- }
4947-
4948- fragment.removeChild( div );
4949-
4950- // Null elements to avoid leaks in IE
4951- fragment = select = opt = div = input = null;
4952-
4953- // Run tests that need a body at doc ready
4954- jQuery(function() {
4955- var container, outer, inner, table, td, offsetSupport,
4956- marginDiv, conMarginTop, style, html, positionTopLeftWidthHeight,
4957- paddingMarginBorderVisibility, paddingMarginBorder,
4958- body = document.getElementsByTagName("body")[0];
4959-
4960- if ( !body ) {
4961- // Return for frameset docs that don't have a body
4962- return;
4963- }
4964-
4965- conMarginTop = 1;
4966- paddingMarginBorder = "padding:0;margin:0;border:";
4967- positionTopLeftWidthHeight = "position:absolute;top:0;left:0;width:1px;height:1px;";
4968- paddingMarginBorderVisibility = paddingMarginBorder + "0;visibility:hidden;";
4969- style = "style='" + positionTopLeftWidthHeight + paddingMarginBorder + "5px solid #000;";
4970- html = "<div " + style + "display:block;'><div style='" + paddingMarginBorder + "0;display:block;overflow:hidden;'></div></div>" +
4971- "<table " + style + "' cellpadding='0' cellspacing='0'>" +
4972- "<tr><td></td></tr></table>";
4973-
4974- container = document.createElement("div");
4975- container.style.cssText = paddingMarginBorderVisibility + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px";
4976- body.insertBefore( container, body.firstChild );
4977-
4978- // Construct the test element
4979- div = document.createElement("div");
4980- container.appendChild( div );
4981-
4982- // Check if table cells still have offsetWidth/Height when they are set
4983- // to display:none and there are still other visible table cells in a
4984- // table row; if so, offsetWidth/Height are not reliable for use when
4985- // determining if an element has been hidden directly using
4986- // display:none (it is still safe to use offsets if a parent element is
4987- // hidden; don safety goggles and see bug #4512 for more information).
4988- // (only IE 8 fails this test)
4989- div.innerHTML = "<table><tr><td style='" + paddingMarginBorder + "0;display:none'></td><td>t</td></tr></table>";
4990- tds = div.getElementsByTagName( "td" );
4991- isSupported = ( tds[ 0 ].offsetHeight === 0 );
4992-
4993- tds[ 0 ].style.display = "";
4994- tds[ 1 ].style.display = "none";
4995-
4996- // Check if empty table cells still have offsetWidth/Height
4997- // (IE <= 8 fail this test)
4998- support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
4999-
5000- // Check if div with explicit width and no margin-right incorrectly
The diff has been truncated for viewing.