This commit is contained in:
Tim Bentley 2011-03-17 17:17:29 +00:00
commit 0db91eaba7
35 changed files with 1283 additions and 786 deletions

View File

@ -17,3 +17,9 @@ Helper Classes & Functions
.. automodule:: openlp.plugins.remotes.lib .. automodule:: openlp.plugins.remotes.lib
:members: :members:
.. autoclass:: openlp.plugins.remotes.lib.httpserver.HttpConnection
:members:
.. autoclass:: openlp.plugins.remotes.lib.httpserver.HttpResponse
:members:

View File

@ -49,11 +49,13 @@ class MediaPlugin(Plugin):
u'audio/ac3': [u'.ac3'], u'audio/ac3': [u'.ac3'],
u'audio/flac': [u'.flac'], u'audio/flac': [u'.flac'],
u'audio/x-m4a': [u'.m4a'], u'audio/x-m4a': [u'.m4a'],
u'audio/midi': [u'.mid', u'.midi'],
u'audio/x-mp3': [u'.mp3'], u'audio/x-mp3': [u'.mp3'],
u'audio/mpeg': [u'.mp3', u'.mp2', u'.mpga', u'.mpega', u'.m4a'], u'audio/mpeg': [u'.mp3', u'.mp2', u'.mpga', u'.mpega', u'.m4a'],
u'audio/qcelp': [u'.qcp'], u'audio/qcelp': [u'.qcp'],
u'audio/x-wma': [u'.wma'], u'audio/x-wma': [u'.wma'],
u'audio/x-ms-wma': [u'.wma'], u'audio/x-ms-wma': [u'.wma'],
u'video/x-flv': [u'.flv'],
u'video/x-matroska': [u'.mpv', u'.mkv'], u'video/x-matroska': [u'.mpv', u'.mkv'],
u'video/x-wmv': [u'.wmv'], u'video/x-wmv': [u'.wmv'],
u'video/x-ms-wmv': [u'.wmv']} u'video/x-ms-wmv': [u'.wmv']}

View File

@ -26,4 +26,68 @@
""" """
The :mod:`remotes` plugin allows OpenLP to be controlled from another machine The :mod:`remotes` plugin allows OpenLP to be controlled from another machine
over a network connection. over a network connection.
Routes:
``/``
Go to the web interface.
``/files/{filename}``
Serve a static file.
``/api/poll``
Poll to see if there are any changes. Returns a JSON-encoded dict of
any changes that occurred::
{"results": {"type": "controller"}}
Or, if there were no results, False::
{"results": False}
``/api/controller/{live|preview}/{action}``
Perform ``{action}`` on the live or preview controller. Valid actions
are:
``next``
Load the next slide.
``previous``
Load the previous slide.
``jump``
Jump to a specific slide. Requires an id return in a JSON-encoded
dict like so::
{"request": {"id": 1}}
``first``
Load the first slide.
``last``
Load the last slide.
``text``
Request the text of the current slide.
``/api/service/{action}``
Perform ``{action}`` on the service manager (e.g. go live). Data is
passed as a json-encoded ``data`` parameter. Valid actions are:
``next``
Load the next item in the service.
``previous``
Load the previews item in the service.
``jump``
Jump to a specific item in the service. Requires an id returned in
a JSON-encoded dict like so::
{"request": {"id": 1}}
``list``
Request a list of items in the service.
""" """

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

View File

@ -1,57 +1,73 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" <!DOCTYPE html>
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr"> <head>
<head> <meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>OpenLP 2.0 Remote</title>
<title>OpenLP Remote Controller</title> <link rel="stylesheet" href="/files/jquery.mobile.css" />
<link rel="stylesheet" href="/files/openlp.css" />
<script type="text/javascript" src="/files/jquery.js"></script> <script type="text/javascript" src="/files/jquery.js"></script>
<script type='text/javascript' src="/files/openlp.js"></script> <script type="text/javascript" src="/files/openlp.js"></script>
<script type='text/javascript' src="/files/init.js"></script> <script type="text/javascript" src="/files/jquery.mobile.js"></script>
<link rel="stylesheet" href="/files/style.css" type="text/css" />
</head> </head>
<body> <body>
<h1>OpenLP Controller</h1> <div data-role="page" id="home">
<p>Quick Links: <a href="#service-manager">Service Manager</a> | <a href="#slide-controller">Slide Controller</a> | <a href="#miscellaneous">Miscellaneous</a></p> <div data-role="header">
<h2 id="service-manager">Service Manager</h2> <h1>OpenLP 2.0 Remote</h1>
<div id="service"></div>
<p><em>(Click service item to go live.)</em></p>
<fieldset>
<legend>Controls</legend>
<div id="service-buttons">
<input type="button" value="Refresh Service" id="servicemanager_list_request" />
</div> </div>
<div id="item-buttons"> <div data-role="content">
<input type="button" value="&lt;- Previous Item" id="servicemanager_previous_item" /> <div data-role="controlgroup">
<input type="button" value="Next Item -&gt;" id="servicemanager_next_item" /> <a href="#service-manager" data-role="button" data-icon="arrow-r" data-iconpos="right">Service Manager</a>
<a href="#slide-controller" data-role="button" data-icon="arrow-r" data-iconpos="right">Slide Controller</a>
<a href="#alerts" data-role="button" data-icon="arrow-r" data-iconpos="right">Alerts</a>
</div> </div>
</fieldset>
<hr>
<h2 id="slide-controller">Slide Controller</h2>
<div id="current-item"></div>
<p><em>(Click verse to display.)</em></p>
<fieldset>
<legend>Controls</legend>
<div id="item-buttons">
<input type="button" value="Refresh Item" id="slidecontroller_live_text_request" />
</div> </div>
<div id="slide-buttons"> </div>
<input type="button" value="&lt;- Previous Slide" id="slidecontroller_live_previous" /> <div data-role="page" id="service-manager">
<input type="button" value="Next Slide -&gt;" id="slidecontroller_live_next" /> <div data-role="header">
<a href="#" data-rel="back" data-icon="arrow-l">Back</a>
<h1>Service Manager</h1>
</div> </div>
</fieldset> <div data-role="content">
<hr> <ul data-role="listview" data-inset="true">
<h2 id="miscellaneous">Miscellaneous</h2> </ul>
<div id="display-buttons">
<input type="button" value="Blank" id="slidecontroller_live_blank" />
<input type="button" value="Unblank" id="slidecontroller_live_unblank" />
</div> </div>
<div id="alert-details"> <div data-role="footer" data-theme="b" class="ui-bar">
<label for="alert-text">Alert text:</label> <a href="#" id="service-blank" data-role="button" data-icon="blank">Blank</a>
<input type="text" id="alert-text" /> <a href="#" id="service-unblank" data-role="button" data-icon="unblank">Unblank</a>
<input type="button" value="Send" id="alert-send" /> <a href="#" id="service-refresh" data-role="button" data-icon="refresh">Refresh</a>
<a href="#" id="service-previous" data-role="button" data-icon="arrow-l">Previous</a>
<a href="#" id="service-next" data-role="button" data-icon="arrow-r" data-iconpos="right">Next</a>
</div> </div>
<hr> </div>
<a href="http://openlp.org/">OpenLP website</a> <div data-role="page" id="slide-controller">
<div data-role="header">
<a href="#" data-rel="back" data-icon="arrow-l">Back</a>
<h1>Slide Controller</h1>
</div>
<div data-role="content">
<ul data-role="listview" data-inset="true">
</ul>
</div>
<div data-role="footer" data-theme="b" class="ui-bar">
<a href="#" id="controller-blank" data-role="button" data-icon="blank">Blank</a>
<a href="#" id="controller-unblank" data-role="button" data-icon="unblank">Unblank</a>
<a href="#" id="controller-refresh" data-role="button" data-icon="refresh">Refresh</a>
<a href="#" id="controller-previous" data-role="button" data-icon="arrow-l">Previous</a>
<a href="#" id="controller-next" data-role="button" data-icon="arrow-r" data-iconpos="right">Next</a>
</div>
</div>
<div data-role="page" id="alerts">
<div data-role="header">
<a href="#" data-rel="back" data-icon="arrow-l">Back</a>
<h1>Alerts</h1>
</div>
<div data-role="content">
<div data-role="fieldcontain">
<label for="alert-text">Text:</label>
<input type="text" name="alert-text" id="alert-text" value="" />
</div>
<a href="#" id="alert-submit" data-role="button">Show Alert</a>
</div>
</div>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,121 @@
/*!
* jQuery Mobile v1.0a3
* http://jquerymobile.com/
*
* Copyright 2010, jQuery Project
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*/
(function(a,d){if(a.cleanData){var c=a.cleanData;a.cleanData=function(b){for(var g=0,e;(e=b[g])!=null;g++)a(e).triggerHandler("remove");c(b)}}else{var f=a.fn.remove;a.fn.remove=function(b,g){return this.each(function(){if(!g)if(!b||a.filter(b,[this]).length)a("*",this).add([this]).each(function(){a(this).triggerHandler("remove")});return f.call(a(this),b,g)})}}a.widget=function(b,g,e){var i=b.split(".")[0],h;b=b.split(".")[1];h=i+"-"+b;if(!e){e=g;g=a.Widget}a.expr[":"][h]=function(k){return!!a.data(k,
b)};a[i]=a[i]||{};a[i][b]=function(k,j){arguments.length&&this._createWidget(k,j)};g=new g;g.options=a.extend(true,{},g.options);a[i][b].prototype=a.extend(true,g,{namespace:i,widgetName:b,widgetEventPrefix:a[i][b].prototype.widgetEventPrefix||b,widgetBaseClass:h},e);a.widget.bridge(b,a[i][b])};a.widget.bridge=function(b,g){a.fn[b]=function(e){var i=typeof e==="string",h=Array.prototype.slice.call(arguments,1),k=this;e=!i&&h.length?a.extend.apply(null,[true,e].concat(h)):e;if(i&&e.charAt(0)==="_")return k;
i?this.each(function(){var j=a.data(this,b);if(!j)throw"cannot call methods on "+b+" prior to initialization; attempted to call method '"+e+"'";if(!a.isFunction(j[e]))throw"no such method '"+e+"' for "+b+" widget instance";var o=j[e].apply(j,h);if(o!==j&&o!==d){k=o;return false}}):this.each(function(){var j=a.data(this,b);j?j.option(e||{})._init():a.data(this,b,new g(e,this))});return k}};a.Widget=function(b,g){arguments.length&&this._createWidget(b,g)};a.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",
options:{disabled:false},_createWidget:function(b,g){a.data(g,this.widgetName,this);this.element=a(g);this.options=a.extend(true,{},this.options,this._getCreateOptions(),b);var e=this;this.element.bind("remove."+this.widgetName,function(){e.destroy()});this._create();this._trigger("create");this._init()},_getCreateOptions:function(){var b={};if(a.metadata)b=a.metadata.get(element)[this.widgetName];return b},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName);
this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+"-disabled ui-state-disabled")},widget:function(){return this.element},option:function(b,g){var e=b;if(arguments.length===0)return a.extend({},this.options);if(typeof b==="string"){if(g===d)return this.options[b];e={};e[b]=g}this._setOptions(e);return this},_setOptions:function(b){var g=this;a.each(b,function(e,i){g._setOption(e,i)});return this},_setOption:function(b,g){this.options[b]=g;if(b===
"disabled")this.widget()[g?"addClass":"removeClass"](this.widgetBaseClass+"-disabled ui-state-disabled").attr("aria-disabled",g);return this},enable:function(){return this._setOption("disabled",false)},disable:function(){return this._setOption("disabled",true)},_trigger:function(b,g,e){var i=this.options[b];g=a.Event(g);g.type=(b===this.widgetEventPrefix?b:this.widgetEventPrefix+b).toLowerCase();e=e||{};if(g.originalEvent){b=a.event.props.length;for(var h;b;){h=a.event.props[--b];g[h]=g.originalEvent[h]}}this.element.trigger(g,
e);return!(a.isFunction(i)&&i.call(this.element[0],g,e)===false||g.isDefaultPrevented())}}})(jQuery);(function(a,d){a.widget("mobile.widget",{_getCreateOptions:function(){var c=this.element,f={};a.each(this.options,function(b){var g=c.data(b.replace(/[A-Z]/g,function(e){return"-"+e.toLowerCase()}));if(g!==d)f[b]=g});return f}})})(jQuery);
(function(a){function d(){var g=c.width(),e=[],i=[],h;f.removeClass("min-width-"+b.join("px min-width-")+"px max-width-"+b.join("px max-width-")+"px");a.each(b,function(k,j){g>=j&&e.push("min-width-"+j+"px");g<=j&&i.push("max-width-"+j+"px")});if(e.length)h=e.join(" ");if(i.length)h+=" "+i.join(" ");f.addClass(h)}var c=a(window),f=a("html"),b=[320,480,768,1024];a.mobile.media=function(){var g={},e=a("<div id='jquery-mediatest'>"),i=a("<body>").append(e);return function(h){if(!(h in g)){var k=document.createElement("style"),
j="@media "+h+" { #jquery-mediatest { position:absolute; } }";k.type="text/css";if(k.styleSheet)k.styleSheet.cssText=j;else k.appendChild(document.createTextNode(j));f.prepend(i).prepend(k);g[h]=e.css("position")==="absolute";i.add(k).remove()}return g[h]}}();a.mobile.addResolutionBreakpoints=function(g){if(a.type(g)==="array")b=b.concat(g);else b.push(g);b.sort(function(e,i){return e-i});d()};a(document).bind("mobileinit.htmlclass",function(){c.bind("orientationchange.htmlclass resize.htmlclass",
function(g){g.orientation&&f.removeClass("portrait landscape").addClass(g.orientation);d()})});a(function(){c.trigger("orientationchange.htmlclass")})})(jQuery);
(function(a,d){function c(h){var k=h.charAt(0).toUpperCase()+h.substr(1);h=(h+" "+g.join(k+" ")+k).split(" ");for(var j in h)if(b[j]!==d)return true}var f=a("<body>").prependTo("html"),b=f[0].style,g=["webkit","moz","o"],e=window.palmGetResource||window.PalmServiceBridge,i=window.blackberry;a.extend(a.support,{orientation:"orientation"in window,touch:"ontouchend"in document,cssTransitions:"WebKitTransitionEvent"in window,pushState:!!history.pushState,mediaquery:a.mobile.media("only all"),cssPseudoElement:!!c("content"),
boxShadow:!!c("boxShadow")&&!i,scrollTop:("pageXOffset"in window||"scrollTop"in document.documentElement||"scrollTop"in f[0])&&!e,dynamicBaseTag:function(){var h=location.protocol+"//"+location.host+location.pathname+"ui-dir/",k=a("head base"),j=null,o="";if(k.length)o=k.attr("href");else k=j=a("<base>",{href:h}).appendTo("head");var p=a("<a href='testurl'></a>").prependTo(f)[0].href;k[0].href=o?o:location.pathname;j&&j.remove();return p.indexOf(h)===0}()});f.remove();a.support.boxShadow||a("html").addClass("ui-mobile-nosupport-boxshadow")})(jQuery);
(function(a,d){a.each("touchstart touchmove touchend orientationchange tap taphold swipe swipeleft swiperight scrollstart scrollstop".split(" "),function(e,i){a.fn[i]=function(h){return h?this.bind(i,h):this.trigger(i)};a.attrFn[i]=true});var c=a.support.touch,f=c?"touchstart":"mousedown",b=c?"touchend":"mouseup",g=c?"touchmove":"mousemove";a.event.special.scrollstart={enabled:true,setup:function(){function e(j,o){h=o;var p=j.type;j.type=h?"scrollstart":"scrollstop";a.event.handle.call(i,j);j.type=
p}var i=this,h,k;a(i).bind("touchmove scroll",function(j){if(a.event.special.scrollstart.enabled){h||e(j,true);clearTimeout(k);k=setTimeout(function(){e(j,false)},50)}})}};a.event.special.tap={setup:function(){var e=this,i=a(e);i.bind("mousedown touchstart",function(h){function k(n){if(n.type=="scroll")j=true;else{n=n.type=="touchmove"?n.originalEvent.touches[0]:n;if(Math.abs(v[0]-n.pageX)>10||Math.abs(v[1]-n.pageY)>10)j=true}}if(h.which&&h.which!==1||i.data("prevEvent")&&i.data("prevEvent")!==h.type)return false;
i.data("prevEvent",h.type);setTimeout(function(){i.removeData("prevEvent")},800);var j=false,o=true,p=h.target,t=h.originalEvent,v=h.type=="touchstart"?[t.touches[0].pageX,t.touches[0].pageY]:[h.pageX,h.pageY],m,r;r=setTimeout(function(){if(o&&!j){m=h.type;h.type="taphold";a.event.handle.call(e,h);h.type=m}},750);a(window).one("scroll",k);i.bind("mousemove touchmove",k).one("mouseup touchend",function(n){i.unbind("mousemove touchmove",k);a(window).unbind("scroll",k);clearTimeout(r);o=false;if(!j&&
p==n.target){m=n.type;n.type="tap";a.event.handle.call(e,n);n.type=m}})})}};a.event.special.swipe={setup:function(){var e=a(this);e.bind(f,function(i){function h(p){if(j){var t=p.originalEvent.touches?p.originalEvent.touches[0]:p;o={time:(new Date).getTime(),coords:[t.pageX,t.pageY]};Math.abs(j.coords[0]-o.coords[0])>10&&p.preventDefault()}}var k=i.originalEvent.touches?i.originalEvent.touches[0]:i,j={time:(new Date).getTime(),coords:[k.pageX,k.pageY],origin:a(i.target)},o;e.bind(g,h).one(b,function(){e.unbind(g,
h);if(j&&o)if(o.time-j.time<1E3&&Math.abs(j.coords[0]-o.coords[0])>30&&Math.abs(j.coords[1]-o.coords[1])<75)j.origin.trigger("swipe").trigger(j.coords[0]>o.coords[0]?"swipeleft":"swiperight");j=o=d})})}};(function(e){function i(){var o=k();if(o!==j){j=o;h.trigger("orientationchange")}}var h=e(window),k,j;e.event.special.orientationchange={setup:function(){if(e.support.orientation)return false;j=k();h.bind("resize",i)},teardown:function(){if(e.support.orientation)return false;h.unbind("resize",i)},
add:function(o){var p=o.handler;o.handler=function(t){t.orientation=k();return p.apply(this,arguments)}}};k=function(){var o=document.documentElement;return o&&o.clientWidth/o.clientHeight<1.1?"portrait":"landscape"}})(jQuery);a.each({scrollstop:"scrollstart",taphold:"tap",swipeleft:"swipe",swiperight:"swipe"},function(e,i){a.event.special[e]={setup:function(){a(this).bind(i,a.noop)}}})})(jQuery);
(function(a,d,c){function f(j){j=j||location.href;return"#"+j.replace(/^[^#]*#?(.*)$/,"$1")}var b="hashchange",g=document,e,i=a.event.special,h=g.documentMode,k="on"+b in d&&(h===c||h>7);a.fn[b]=function(j){return j?this.bind(b,j):this.trigger(b)};a.fn[b].delay=50;i[b]=a.extend(i[b],{setup:function(){if(k)return false;a(e.start)},teardown:function(){if(k)return false;a(e.stop)}});e=function(){function j(){var n=f(),u=r(t);if(n!==t){m(t=n,u);a(d).trigger(b)}else if(u!==t)location.href=location.href.replace(/#.*/,
"")+u;p=setTimeout(j,a.fn[b].delay)}var o={},p,t=f(),v=function(n){return n},m=v,r=v;o.start=function(){p||j()};o.stop=function(){p&&clearTimeout(p);p=c};a.browser.msie&&!k&&function(){var n,u;o.start=function(){if(!n){u=(u=a.fn[b].src)&&u+f();n=a('<iframe tabindex="-1" title="empty"/>').hide().one("load",function(){u||m(f());j()}).attr("src",u||"javascript:0").insertAfter("body")[0].contentWindow;g.onpropertychange=function(){try{if(event.propertyName==="title")n.document.title=g.title}catch(l){}}}};
o.stop=v;r=function(){return f(n.location.href)};m=function(l,s){var q=n.document,w=a.fn[b].domain;if(l!==s){q.title=g.title;q.open();w&&q.write('<script>document.domain="'+w+'"<\/script>');q.close();n.location.hash=l}}}();return o}()})(jQuery,this);
(function(a){a.widget("mobile.page",a.mobile.widget,{options:{backBtnText:"Back",addBackBtn:true,degradeInputs:{color:false,date:false,datetime:false,"datetime-local":false,email:false,month:false,number:false,range:"number",search:true,tel:false,time:false,url:false,week:false},keepNative:null},_create:function(){var d=this.element,c=this.options;this.keepNative="[data-role='none'], [data-role='nojs']"+(c.keepNative?", "+c.keepNative:"");if(this._trigger("beforeCreate")!==false){d.find("[data-role='page'], [data-role='content']").andSelf().each(function(){a(this).addClass("ui-"+
a(this).data("role"))});d.find("[data-role='nojs']").addClass("ui-nojs");d.find("[data-role]").andSelf().each(function(){var f=a(this),b=f.data("role"),g=f.data("theme");if(b==="header"||b==="footer"){f.addClass("ui-bar-"+(g||f.parent("[data-role=page]").data("theme")||"a"));f.attr("role",b==="header"?"banner":"contentinfo");g=f.children("a");var e=g.hasClass("ui-btn-left"),i=g.hasClass("ui-btn-right");if(!e)e=g.eq(0).not(".ui-btn-right").addClass("ui-btn-left").length;i||g.eq(1).addClass("ui-btn-right");
c.addBackBtn&&b==="header"&&a(".ui-page").length>1&&d.data("url")!==a.mobile.path.stripHash(location.hash)&&!e&&f.data("backbtn")!==false&&a("<a href='#' class='ui-btn-left' data-rel='back' data-icon='arrow-l'>"+c.backBtnText+"</a>").prependTo(f);f.children("h1, h2, h3, h4, h5, h6").addClass("ui-title").attr({tabindex:"0",role:"heading","aria-level":"1"})}else if(b==="content"){g&&f.addClass("ui-body-"+g);f.attr("role","main")}else if(b==="page")f.addClass("ui-body-"+(g||"c"));switch(b){case "header":case "footer":case "page":case "content":f.addClass("ui-"+
b);break;case "collapsible":case "fieldcontain":case "navbar":case "listview":case "dialog":f[b]()}});this._enhanceControls();d.find("[data-role='button'], .ui-bar > a, .ui-header > a, .ui-footer > a").not(".ui-btn").not(this.keepNative).buttonMarkup();d.find("[data-role='controlgroup']").controlgroup();d.find("a:not(.ui-btn):not(.ui-link-inherit)").not(this.keepNative).addClass("ui-link");d.fixHeaderFooter()}},_enhanceControls:function(){var d=this.options;this.element.find("input").not(this.keepNative).each(function(){var b=
this.getAttribute("type"),g=d.degradeInputs[b]||"text";d.degradeInputs[b]&&a(this).replaceWith(a("<div>").html(a(this).clone()).html().replace(/type="([a-zA-Z]+)"/,"type="+g+" data-type='$1'"))});var c=this.element.find("input, textarea, select, button"),f=c.not(this.keepNative);c=c.filter("input[type=text]");c.length&&typeof c[0].autocorrect!=="undefined"&&c.each(function(){this.setAttribute("autocorrect","off");this.setAttribute("autocomplete","off")});f.filter("[type='radio'], [type='checkbox']").checkboxradio();
f.filter("button, [type='button'], [type='submit'], [type='reset'], [type='image']").button();f.filter("input, textarea").not("[type='radio'], [type='checkbox'], [type='button'], [type='submit'], [type='reset'], [type='image'], [type='hidden']").textinput();f.filter("input, select").filter("[data-role='slider'], [data-type='range']").slider();f.filter("select:not([data-role='slider'])").selectmenu()}})})(jQuery);
(function(a,d,c){a.extend(a.mobile,{subPageUrlKey:"ui-page",nonHistorySelectors:"dialog",activePageClass:"ui-page-active",activeBtnClass:"ui-btn-active",ajaxEnabled:true,hashListeningEnabled:true,ajaxLinksEnabled:true,ajaxFormsEnabled:true,defaultTransition:"slide",loadingMessage:"loading",metaViewportContent:"width=device-width, minimum-scale=1, maximum-scale=1",gradeA:function(){return a.support.mediaquery},autoInitialize:true,keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,
COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});a(d.document).trigger("mobileinit");if(a.mobile.gradeA()){var f=a(d),b=a("html"),g=a("head"),e=a.mobile.loadingMessage?a("<div class='ui-loader ui-body-a ui-corner-all'><span class='ui-icon ui-icon-loading spin'></span><h1>"+
a.mobile.loadingMessage+"</h1></div>"):c;b.addClass("ui-mobile ui-mobile-rendering");a.mobile.metaViewportContent&&a("<meta>",{name:"viewport",content:a.mobile.metaViewportContent}).prependTo(g);a.extend(a.mobile,{pageLoading:function(i){if(i)b.removeClass("ui-loading");else{if(a.mobile.loadingMessage){i=a("."+a.mobile.activeBtnClass).first();e.appendTo(a.mobile.pageContainer).css({top:a.support.scrollTop&&a(d).scrollTop()+a(d).height()/2||i.length&&i.offset().top||100})}b.addClass("ui-loading")}},
silentScroll:function(i){i=i||0;a.event.special.scrollstart.enabled=false;setTimeout(function(){d.scrollTo(0,i);a(document).trigger("silentscroll",{x:0,y:i})},20);setTimeout(function(){a.event.special.scrollstart.enabled=true},150)},initializePage:function(){var i=a("[data-role='page']");i.add("[data-role='dialog']").each(function(){a(this).attr("data-url",a(this).attr("id"))});a.mobile.firstPage=i.first();a.mobile.pageContainer=i.first().parent().addClass("ui-mobile-viewport");a.mobile.pageLoading();
!a.mobile.hashListeningEnabled||!a.mobile.path.stripHash(location.hash)?a.mobile.changePage(a.mobile.firstPage,false,true,false,true):f.trigger("hashchange",[true])}});a.mobile.autoInitialize&&a(a.mobile.initializePage);f.load(a.mobile.silentScroll)}})(jQuery,this);
(function(a,d){function c(l){if(i&&(!i.closest(".ui-page-active").length||l))i.removeClass(a.mobile.activeBtnClass);i=null}var f=a(window),b=a("html"),g=a("head"),e={get:function(l){if(l==d)l=location.hash;return e.stripHash(l).replace(/[^\/]*\.[^\/*]+$/,"")},getFilePath:function(l){var s="&"+a.mobile.subPageUrlKey;return l&&l.split(s)[0].split(t)[0]},set:function(l){location.hash=l},origin:"",setOrigin:function(){e.origin=e.get(location.protocol+"//"+location.host+location.pathname)},makeAbsolute:function(l){return e.get()+
l},clean:function(l){return l.replace(location.protocol+"//"+location.host,"")},stripHash:function(l){return l.replace(/^#/,"")},isExternal:function(l){return e.hasProtocol(e.clean(l))},hasProtocol:function(l){return/^(:?\w+:)/.test(l)},isRelative:function(l){return/^[^\/|#]/.test(l)&&!e.hasProtocol(l)},isEmbeddedPage:function(l){return/^#/.test(l)}},i=null,h={stack:[],activeIndex:0,getActive:function(){return h.stack[h.activeIndex]},getPrev:function(){return h.stack[h.activeIndex-1]},getNext:function(){return h.stack[h.activeIndex+
1]},addNew:function(l,s){h.getNext()&&h.clearForward();h.stack.push({url:l,transition:s});h.activeIndex=h.stack.length-1},clearForward:function(){h.stack=h.stack.slice(0,h.activeIndex+1)},ignoreNextHashChange:true},k="[tabindex],a,button:visible,select:visible,input",j=null,o=[],p=false,t="&ui-state=dialog",v=g.children("base"),m=location.protocol+"//"+location.host,r=e.get(m+location.pathname),n=r;if(v.length){var u=v.attr("href");if(u)n=u.search(/^[^:/]+:\/\/[^/]+\/?/)==-1?u.charAt(0)=="/"?m+u:
r+u:u;n+=n.charAt(n.length-1)=="/"?" ":"/"}base=a.support.dynamicBaseTag?{element:v.length?v:a("<base>",{href:n}).prependTo(g),set:function(l){base.element.attr("href",n+e.get(l))},reset:function(){base.element.attr("href",n)}}:d;e.setOrigin();a.fn.animationComplete=function(l){if(a.support.cssTransitions)return a(this).one("webkitAnimationEnd",l);else setTimeout(l,0)};a.mobile.updateHash=e.set;a.mobile.path=e;a.mobile.base=base;a.mobile.urlstack=h.stack;a.mobile.urlHistory=h;a.mobile.changePage=
function(l,s,q,w,z){function F(){function A(){if(w!==false&&x){h.ignoreNextHashChange=false;e.set(x)}!I&&!K&&h.addNew(x,s);c();a.mobile.silentScroll(l.data("lastScroll"));var G=l,P=G.find(".ui-title:eq(0)");P.length?P.focus():G.find(k).eq(0).focus();y&&y.data("page")._trigger("hide",null,{nextPage:l});l.data("page")._trigger("show",null,{prevPage:y||a("")});a.mobile.activePage=l;L!=null&&L.remove();b.removeClass("ui-mobile-rendering");p=false;o.length>0&&a.mobile.changePage.apply(a.mobile,o.pop())}
function B(G){a.mobile.pageContainer.addClass(G);D.push(G)}a.mobile.silentScroll();var M=f.scrollTop(),J=["flip"],D=[];if(x.indexOf("&"+a.mobile.subPageUrlKey)>-1)l=a("[data-url='"+x+"']");if(y){y.data("lastScroll",M);y.data("page")._trigger("beforehide",{nextPage:l})}l.data("page")._trigger("beforeshow",{prevPage:y||a("")});if(s&&s!=="none"){a.mobile.pageLoading(true);a.inArray(s,J)>=0&&B("ui-mobile-viewport-perspective");B("ui-mobile-viewport-transitioning");if(y)y.addClass(s+" out "+(q?"reverse":
""));l.addClass(a.mobile.activePageClass+" "+s+" in "+(q?"reverse":""));l.animationComplete(function(){y.add(l).removeClass("out in reverse "+s);y&&y.removeClass(a.mobile.activePageClass);A();a.mobile.pageContainer.removeClass(D.join(" "));D=[]})}else{a.mobile.pageLoading(true);y&&y.removeClass(a.mobile.activePageClass);l.addClass(a.mobile.activePageClass);A()}}function Q(){if(j||l.data("role")=="dialog"){x=h.getActive().url+t;if(j){l.attr("data-role",j);j=null}}l.page()}var E=a.type(l)==="array",
H=a.type(l)==="object",y=E?l[0]:a.mobile.activePage;l=E?l[1]:l;var x=fileUrl=a.type(l)==="string"?e.stripHash(l):"",C=d,N="get",R=false,L=null,O=h.getActive(),I=false,K=false;if(!(O&&h.stack.length>1&&O.url===x&&!E&&!H))if(p)o.unshift(arguments);else{p=true;if(z){a.each(h.stack,function(A){if(this.url==x){urlIndex=A;I=A<h.activeIndex;K=!I;h.activeIndex=A}});if(I){q=true;s=s||O.transition}else if(K)s=s||h.getActive().transition}if(H&&l.url){x=l.url;C=l.data;N=l.type;R=true;if(C&&N=="get"){if(a.type(C)==
"object")C=a.param(C);x+="?"+C;C=d}}base&&base.reset();a(window.document.activeElement).add("input:focus, textarea:focus, select:focus").blur();if(x){l=a("[data-url='"+x+"']");fileUrl=e.getFilePath(x)}else{E=l.attr("data-url");H=e.getFilePath(E);if(E!=H)fileUrl=H}if(s===d)s=a.mobile.defaultTransition;if(l.length&&!R){fileUrl&&base&&base.set(fileUrl);Q();F()}else{if(l.length)L=l;a.mobile.pageLoading();a.ajax({url:fileUrl,type:N,data:C,success:function(A){var B=/ data-url="(.*)"/.test(A)&&RegExp.$1;
if(B){base&&base.set(B);x=fileUrl=e.getFilePath(B)}else base&&base.set(fileUrl);B=a("<div></div>");B.get(0).innerHTML=A;l=B.find('[data-role="page"], [data-role="dialog"]').first();if(!a.support.dynamicBaseTag){var M=e.get(fileUrl);l.find("[src],link[href]").each(function(){var J=a(this).is("[href]")?"href":"src",D=a(this).attr(J);D.replace(location.protocol+"//"+location.host+location.pathname,"");/^(\w+:|#|\/)/.test(D)||a(this).attr(J,M+D)})}l.attr("data-url",fileUrl).appendTo(a.mobile.pageContainer);
Q();setTimeout(function(){F()},0)},error:function(){a.mobile.pageLoading(true);c(true);base&&base.set(e.get());a("<div class='ui-loader ui-overlay-shadow ui-body-e ui-corner-all'><h1>Error Loading Page</h1></div>").css({display:"block",opacity:0.96,top:a(window).scrollTop()+100}).appendTo(a.mobile.pageContainer).delay(800).fadeOut(400,function(){a(this).remove()})}})}}};a("form[data-ajax!='false']").live("submit",function(l){if(a.mobile.ajaxEnabled&&a.mobile.ajaxFormsEnabled){var s=a(this).attr("method"),
q=e.clean(a(this).attr("action"));if(!e.isExternal(q)){if(e.isRelative(q))q=e.makeAbsolute(q);a.mobile.changePage({url:q,type:s,data:a(this).serialize()},d,d,true);l.preventDefault()}}});a("a").live("click",function(l){var s=a(this),q=s.attr("href")||"#";q=e.clean(q);var w=s.is("[rel='external']"),z=e.isEmbeddedPage(q);w=e.isExternal(q)||w&&!z;z=s.is("[target]");var F=s.is("[data-ajax='false']");if(s.is("[data-rel='back']")){window.history.back();return false}if(q==="#")return false;i=s.closest(".ui-btn").addClass(a.mobile.activeBtnClass);
if(w||F||z||!a.mobile.ajaxEnabled||!a.mobile.ajaxLinksEnabled){c(true);if(z)window.open(q);else if(F)return;else location.href=q}else{w=s.data("transition");z=(z=s.data("direction"))&&z=="reverse"||s.data("back");j=s.attr("data-rel");if(e.isRelative(q))q=e.makeAbsolute(q);q=e.stripHash(q);a.mobile.changePage(q,w,z)}l.preventDefault()});f.bind("hashchange",function(){var l=e.stripHash(location.hash),s=a.mobile.urlHistory.stack.length===0?false:d;if(!a.mobile.hashListeningEnabled||!h.ignoreNextHashChange||
h.stack.length>1&&l.indexOf(t)>-1&&!a.mobile.activePage.is(".ui-dialog")){if(!h.ignoreNextHashChange)h.ignoreNextHashChange=true}else l?a.mobile.changePage(l,s,d,false,true):a.mobile.changePage(a.mobile.firstPage,s,true,false,true)})})(jQuery);
(function(a,d){a.fn.fixHeaderFooter=function(){if(!a.support.scrollTop)return this;return this.each(function(){var c=a(this);c.data("fullscreen")&&c.addClass("ui-page-fullscreen");c.find('.ui-header[data-position="fixed"]').addClass("ui-header-fixed ui-fixed-inline fade");c.find('.ui-footer[data-position="fixed"]').addClass("ui-footer-fixed ui-fixed-inline fade")})};a.fixedToolbars=function(){function c(){if(!e&&g=="overlay"){i||a.fixedToolbars.hide(true);a.fixedToolbars.startShowTimer()}}function f(m){var r=
0;if(m){var n=m.offsetParent,u=document.body;for(r=m.offsetTop;m&&m!=u;){r+=m.scrollTop||0;if(m==n){r+=n.offsetTop;n=m.offsetParent}m=m.parentNode}}return r}function b(m){var r=a(window).scrollTop(),n=f(m[0]),u=m.css("top")=="auto"?0:parseFloat(m.css("top")),l=window.innerHeight,s=m.outerHeight(),q=m.parents(".ui-page:not(.ui-page-fullscreen)").length;if(m.is(".ui-header-fixed")){u=r-n+u;if(u<n)u=0;return m.css("top",q?u:r)}else{u=r+l-s-(n-u);return m.css("top",q?u:r+l-s)}}if(a.support.scrollTop){var g=
"inline",e=false,i,h,k=a.support.touch,j=k?"touchstart":"mousedown",o=k?"touchend":"mouseup",p=null,t=false,v=true;a(function(){a(document).bind(j,function(m){if(v)a(m.target).closest("a,input,textarea,select,button,label,.ui-header-fixed,.ui-footer-fixed").length||(p=g)}).bind(o,function(m){if(v)if(!a(m.target).closest("a,input,textarea,select,button,label,.ui-header-fixed,.ui-footer-fixed").length)if(!t){a.fixedToolbars.toggle(p);p=null}}).bind("scrollstart",function(m){if(!a(m.target).closest("a,input,textarea,select,button,label,.ui-header-fixed,.ui-footer-fixed").length){t=
true;if(p==null)p=g;if(e=(m=p=="overlay")||!!i){a.fixedToolbars.clearShowTimer();m&&a.fixedToolbars.hide(true)}}}).bind("scrollstop",function(m){if(!a(m.target).closest("a,input,textarea,select,button,label,.ui-header-fixed,.ui-footer-fixed").length){t=false;if(e){e=false;a.fixedToolbars.startShowTimer()}p=null}}).bind("silentscroll",c);a(window).bind("resize",c)});a(".ui-page").live("pagebeforeshow",function(m){m=a(m.target).find('[data-role="footer"]:not(.ui-sticky-footer)');var r=m.data("id");
h=null;if(r){h=a('.ui-footer[data-id="'+r+'"].ui-sticky-footer');if(h.length==0){h=m;m=h.clone();h.addClass("ui-sticky-footer").before(m)}m.addClass("ui-footer-duplicate");h.appendTo(a.pageContainer).css("top",0);b(h)}});a(".ui-page").live("pageshow",function(m){h&&h.length&&h.appendTo(m.target).css("top",0);a.fixedToolbars.show(true,this)});return{show:function(m,r){a.fixedToolbars.clearShowTimer();g="overlay";return(r?a(r):a.mobile.activePage?a.mobile.activePage:a(".ui-page-active")).children(".ui-header-fixed:first, .ui-footer-fixed:not(.ui-footer-duplicate):last").each(function(){var n=
a(this),u=a(window).scrollTop(),l=f(n[0]),s=window.innerHeight,q=n.outerHeight();u=n.is(".ui-header-fixed")&&u<=l+q||n.is(".ui-footer-fixed")&&l<=u+s;n.addClass("ui-fixed-overlay").removeClass("ui-fixed-inline");!u&&!m&&n.animationComplete(function(){n.removeClass("in")}).addClass("in");b(n)})},hide:function(m){g="inline";return(a.mobile.activePage?a.mobile.activePage:a(".ui-page-active")).children(".ui-header-fixed:first, .ui-footer-fixed:not(.ui-footer-duplicate):last").each(function(){var r=a(this),
n=r.css("top");n=n=="auto"?0:parseFloat(n);r.addClass("ui-fixed-inline").removeClass("ui-fixed-overlay");if(n<0||r.is(".ui-header-fixed")&&n!=0)if(m)r.css("top",0);else r.css("top")!=="auto"&&parseFloat(r.css("top"))!==0&&r.animationComplete(function(){r.removeClass("out reverse");r.css("top",0)}).addClass("out reverse")})},startShowTimer:function(){a.fixedToolbars.clearShowTimer();var m=a.makeArray(arguments);i=setTimeout(function(){i=d;a.fixedToolbars.show.apply(null,m)},100)},clearShowTimer:function(){i&&
clearTimeout(i);i=d},toggle:function(m){if(m)g=m;return g=="overlay"?a.fixedToolbars.hide():a.fixedToolbars.show()},setTouchToggleEnabled:function(m){v=m}}}}()})(jQuery);
(function(a,d){a.widget("mobile.checkboxradio",a.mobile.widget,{options:{theme:null},_create:function(){var c=this,f=this.element,b=f.closest("form,fieldset,[data-role='page']").find("label[for='"+f.attr("id")+"']"),g=f.attr("type"),e="ui-icon-"+g+"-off";if(!(g!="checkbox"&&g!="radio")){if(!this.options.theme)this.options.theme=this.element.data("theme");b.buttonMarkup({theme:this.options.theme,icon:this.element.parents("[data-type='horizontal']").length?d:e,shadow:false});f.add(b).wrapAll("<div class='ui-"+
g+"'></div>");b.bind({mouseover:function(){if(a(this).parent().is(".ui-disabled"))return false},touchmove:function(i){i=i.originalEvent.touches[0];if(b.data("movestart")){if(Math.abs(b.data("movestart")[0]-i.pageX)>10||Math.abs(abel.data("movestart")[1]-i.pageY)>10)b.data("moved",true)}else b.data("movestart",[parseFloat(i.pageX),parseFloat(i.pageY)])},"touchend mouseup":function(i){b.removeData("movestart");if(b.data("etype")&&b.data("etype")!==i.type||b.data("moved")){b.removeData("etype").removeData("moved");
b.data("moved")&&b.removeData("moved");return false}b.data("etype",i.type);c._cacheVals();f.attr("checked",g==="radio"&&true||!f.is(":checked"));c._updateAll();i.preventDefault()},click:false});f.bind({mousedown:function(){this._cacheVals()},click:function(){c._updateAll()},focus:function(){b.addClass("ui-focus")},blur:function(){b.removeClass("ui-focus")}});this.refresh()}},_cacheVals:function(){this._getInputSet().each(function(){a(this).data("cacheVal",a(this).is(":checked"))})},_getInputSet:function(){return this.element.closest("form,fieldset,[data-role='page']").find("input[name='"+
this.element.attr("name")+"'][type='"+this.element.attr("type")+"']")},_updateAll:function(){this._getInputSet().each(function(){var c=a(this).data("cacheVal");if(c&&c!==a(this).is(":checked")||a(this).is("[type='checkbox']"))a(this).trigger("change")}).checkboxradio("refresh")},refresh:function(){var c=this.element,f=c.closest("form,fieldset,[data-role='page']").find("label[for='"+c.attr("id")+"']"),b=c.attr("type"),g=f.find(".ui-icon"),e="ui-icon-"+b+"-on";b="ui-icon-"+b+"-off";if(c[0].checked){f.addClass("ui-btn-active");
g.addClass(e);g.removeClass(b)}else{f.removeClass("ui-btn-active");g.removeClass(e);g.addClass(b)}c.is(":disabled")?this.disable():this.enable()},disable:function(){this.element.attr("disabled",true).parent().addClass("ui-disabled")},enable:function(){this.element.attr("disabled",false).parent().removeClass("ui-disabled")}})})(jQuery);
(function(a){a.widget("mobile.textinput",a.mobile.widget,{options:{theme:null},_create:function(){var d=this.element,c=this.options,f=c.theme;if(!f){f=this.element.closest("[class*='ui-bar-'],[class*='ui-body-']");f=f.length?/ui-(bar|body)-([a-z])/.exec(f.attr("class"))[2]:"c"}f=" ui-body-"+f;a("label[for="+d.attr("id")+"]").addClass("ui-input-text");d.addClass("ui-input-text ui-body-"+c.theme);var b=d;if(d.is('[type="search"],[data-type="search"]')){b=d.wrap('<div class="ui-input-search ui-shadow-inset ui-btn-corner-all ui-btn-shadow ui-icon-searchfield'+
f+'"></div>').parent();var g=a('<a href="#" class="ui-input-clear" title="clear text">clear text</a>').tap(function(h){d.val("").focus();d.trigger("change");g.addClass("ui-input-clear-hidden");h.preventDefault()}).appendTo(b).buttonMarkup({icon:"delete",iconpos:"notext",corners:true,shadow:true});c=function(){d.val()==""?g.addClass("ui-input-clear-hidden"):g.removeClass("ui-input-clear-hidden")};c();d.keyup(c)}else d.addClass("ui-corner-all ui-shadow-inset"+f);d.focus(function(){b.addClass("ui-focus")}).blur(function(){b.removeClass("ui-focus")});
if(d.is("textarea")){var e=function(){var h=d[0].scrollHeight;d[0].clientHeight<h&&d.css({height:h+15})},i;d.keyup(function(){clearTimeout(i);i=setTimeout(e,100)})}},disable:function(){(this.element.attr("disabled",true).is('[type="search"],[data-type="search"]')?this.element.parent():this.element).addClass("ui-disabled")},enable:function(){(this.element.attr("disabled",false).is('[type="search"],[data-type="search"]')?this.element.parent():this.element).removeClass("ui-disabled")}})})(jQuery);
(function(a){a.widget("mobile.selectmenu",a.mobile.widget,{options:{theme:null,disabled:false,icon:"arrow-d",iconpos:"right",inline:null,corners:true,shadow:true,iconshadow:true,menuPageTheme:"b",overlayTheme:"a",hidePlaceholderMenuItems:true,closeText:"Close",nativeMenu:false},_create:function(){var d=this,c=this.options,f=this.element.wrap("<div class='ui-select'>"),b=f.attr("id"),g=a("label[for="+b+"]").addClass("ui-select"),e=(d.options.nativeMenu?a("<div/>"):a("<a>",{href:"#",role:"button",id:k,
"aria-haspopup":"true","aria-owns":j})).text(a(f[0].options.item(f[0].selectedIndex)).text()).insertBefore(f).buttonMarkup({theme:c.theme,icon:c.icon,iconpos:c.iconpos,inline:c.inline,corners:c.corners,shadow:c.shadow,iconshadow:c.iconshadow}),i=d.isMultiple=f[0].multiple;c.nativeMenu&&window.opera&&window.opera.version&&f.addClass("ui-select-nativeonly");if(!c.nativeMenu){var h=f.find("option"),k=b+"-button",j=b+"-menu",o=f.closest(".ui-page"),p=/ui-btn-up-([a-z])/.exec(e.attr("class"))[1],t=a("<div data-role='dialog' data-theme='"+
c.menuPageTheme+"'><div data-role='header'><div class='ui-title'>"+g.text()+"</div></div><div data-role='content'></div></div>").appendTo(a.mobile.pageContainer).page(),v=t.find(".ui-content"),m=t.find(".ui-header a"),r=a("<div>",{"class":"ui-selectmenu-screen ui-screen-hidden"}).appendTo(o),n=a("<div>",{"class":"ui-selectmenu ui-selectmenu-hidden ui-overlay-shadow ui-corner-all pop ui-body-"+c.overlayTheme}).insertAfter(r),u=a("<ul>",{"class":"ui-selectmenu-list",id:j,role:"listbox","aria-labelledby":k,
"data-theme":p}).appendTo(n);p=a("<div>",{"class":"ui-header ui-bar-"+p}).prependTo(n);var l=a("<h1>",{"class":"ui-title"}).appendTo(p),s=a("<a>",{"data-iconpos":"notext","data-icon":"delete",text:c.closeText,href:"#","class":"ui-btn-left"}).appendTo(p).buttonMarkup()}if(i)d.buttonCount=a("<span>").addClass("ui-li-count ui-btn-up-c ui-btn-corner-all").hide().appendTo(e);c.disabled&&this.disable();f.change(function(){d.refresh()});a.extend(d,{select:f,optionElems:h,selectID:b,label:g,buttonId:k,menuId:j,
thisPage:o,button:e,menuPage:t,menuPageContent:v,screen:r,listbox:n,list:u,menuType:void 0,header:p,headerClose:s,headerTitle:l,placeholder:""});if(c.nativeMenu){f.appendTo(e).bind("touchstart mousedown",function(){e.addClass(a.mobile.activeBtnClass)}).bind("focus mouseover",function(){e.trigger("mouseover")}).bind("touchmove",function(){e.removeClass(a.mobile.activeBtnClass)}).bind("change blur mouseout",function(){e.trigger("mouseout").removeClass(a.mobile.activeBtnClass)});e.attr("tabindex","-1")}else{d.refresh();
f.attr("tabindex","-1").focus(function(){a(this).blur();e.focus()});e.bind("touchstart",function(q){a(this).data("startTouches",a.extend({},q.originalEvent.touches[0]))}).bind(a.support.touch?"touchend":"mouseup",function(q){a(this).data("moved")?a(this).removeData("moved"):d.open();q.preventDefault()}).bind("touchmove",function(q){q=q.originalEvent.touches[0];var w=a(this).data("startTouches"),z=Math.abs(q.pageY-w.pageY);if(Math.abs(q.pageX-w.pageX)>10||z>10)a(this).data("moved",true)});u.delegate("li:not(.ui-disabled, .ui-li-divider)",
"click",function(q){if(a(q.target).is("a")){var w=u.find("li:not(.ui-li-divider)").index(this);w=d.optionElems.eq(w)[0];w.selected=i?!w.selected:true;i&&a(this).find(".ui-icon").toggleClass("ui-icon-checkbox-on",w.selected).toggleClass("ui-icon-checkbox-off",!w.selected);f.trigger("change");i||d.close();q.preventDefault()}});r.add(s).add(m).bind("click",function(q){d.close();q.preventDefault();a.contains(m[0],q.target)&&q.stopImmediatePropagation()})}},_buildList:function(){var d=this,c=this.options,
f=this.placeholder,b=[],g=[],e=d.isMultiple?"checkbox-off":"false";d.list.empty().filter(".ui-listview").listview("destroy");d.select.find("option").each(function(){var i=a(this),h=i.parent(),k=i.text(),j="<a href='#'>"+k+"</a>",o=[],p=[];if(h.is("optgroup")){h=h.attr("label");if(a.inArray(h,b)===-1){g.push("<li data-role='list-divider'>"+h+"</li>");b.push(h)}}if(!this.getAttribute("value")||k.length==0||i.data("placeholder")){c.hidePlaceholderMenuItems&&o.push("ui-selectmenu-placeholder");f=d.placeholder=
k}if(this.disabled){o.push("ui-disabled");p.push("aria-disabled='true'")}g.push("<li data-icon='"+e+"' class='"+o.join(" ")+"' "+p.join(" ")+">"+j+"</li>")});d.list.html(g.join(" "));this.isMultiple||this.headerClose.hide();!this.isMultiple&&!f.length?this.header.hide():this.headerTitle.text(this.placeholder);d.list.listview()},refresh:function(d){var c=this,f=this.element,b=this.isMultiple,g=this.optionElems=f.find("option"),e=g.filter(":selected"),i=e.map(function(){return g.index(this)}).get();
if(!c.options.nativeMenu&&(d||f[0].options.length>c.list.find("li").length))c._buildList();c.button.find(".ui-btn-text").text(function(){if(!b)return e.text();return e.length?e.map(function(){return a(this).text()}).get().join(", "):c.placeholder});if(b)c.buttonCount[e.length>1?"show":"hide"]().text(e.length);c.options.nativeMenu||c.list.find("li:not(.ui-li-divider)").removeClass(a.mobile.activeBtnClass).attr("aria-selected",false).each(function(h){if(a.inArray(h,i)>-1){h=a(this).addClass(a.mobile.activeBtnClass);
h.find("a").attr("aria-selected",true);b&&h.find(".ui-icon").removeClass("ui-icon-checkbox-off").addClass("ui-icon-checkbox-on")}})},open:function(){function d(){c.list.find(".ui-btn-active").focus()}if(!(this.options.disabled||this.options.nativeMenu)){var c=this,f=c.list.outerHeight(),b=c.list.outerWidth(),g=a(window).scrollTop(),e=c.button.offset().top,i=window.innerHeight,h=c.list.parents(".ui-dialog").length;c.button.addClass(a.mobile.activeBtnClass);if(h||f>i-80||!a.support.scrollTop){g==0&&
e>i&&c.thisPage.one("pagehide",function(){a(this).data("lastScroll",e)});c.menuPage.one("pageshow",function(){a(window).one("silentscroll",function(){d()})});c.menuType="page";c.menuPageContent.append(c.list);a.mobile.changePage(c.menuPage,"pop",false,true)}else{c.menuType="overlay";c.screen.height(a(document).height()).removeClass("ui-screen-hidden");h=e-g;var k=g+i-e,j=f/2;f=h>f/2&&k>f/2?e+c.button.outerHeight()/2-j:h>k?g+i-f-30:g+30;b=c.button.offset().left+c.button.outerWidth()/2-b/2;c.listbox.append(c.list).removeClass("ui-selectmenu-hidden").css({top:f,
left:b}).addClass("in");d()}setTimeout(function(){c.isOpen=true},400)}},close:function(){function d(){setTimeout(function(){c.button.focus();c.button.removeClass(a.mobile.activeBtnClass)},40);c.listbox.removeAttr("style").append(c.list)}if(!(this.options.disabled||!this.isOpen||this.options.nativeMenu)){var c=this;if(c.menuType=="page"){a.mobile.changePage([c.menuPage,c.thisPage],"pop",true,false);c.menuPage.one("pagehide",d)}else{c.screen.addClass("ui-screen-hidden");c.listbox.addClass("ui-selectmenu-hidden").removeAttr("style").removeClass("in");
d()}this.isOpen=false}},disable:function(){this.element.attr("disabled",true);this.button.addClass("ui-disabled").attr("aria-disabled",true);return this._setOption("disabled",true)},enable:function(){this.element.attr("disabled",false);this.button.removeClass("ui-disabled").attr("aria-disabled",false);return this._setOption("disabled",false)}})})(jQuery);
(function(a){a.fn.buttonMarkup=function(c){return this.each(function(){var f=a(this),b=a.extend({},a.fn.buttonMarkup.defaults,f.data(),c),g,e="ui-btn-inner",i;d&&d();if(!b.theme){g=f.closest("[class*='ui-bar-'],[class*='ui-body-']");b.theme=g.length?/ui-(bar|body)-([a-z])/.exec(g.attr("class"))[2]:"c"}g="ui-btn ui-btn-up-"+b.theme;if(b.inline)g+=" ui-btn-inline";if(b.icon){b.icon="ui-icon-"+b.icon;b.iconpos=b.iconpos||"left";i="ui-icon "+b.icon;if(b.shadow)i+=" ui-icon-shadow"}if(b.iconpos){g+=" ui-btn-icon-"+
b.iconpos;b.iconpos=="notext"&&!f.attr("title")&&f.attr("title",f.text())}if(b.corners){g+=" ui-btn-corner-all";e+=" ui-btn-corner-all"}if(b.shadow)g+=" ui-shadow";f.attr("data-theme",b.theme).addClass(g);b=("<D class='"+e+"'><D class='ui-btn-text'></D>"+(b.icon?"<span class='"+i+"'></span>":"")+"</D>").replace(/D/g,b.wrapperEls);f.wrapInner(b)})};a.fn.buttonMarkup.defaults={corners:true,shadow:true,iconshadow:true,wrapperEls:"span"};var d=function(){a(".ui-btn:not(.ui-disabled)").live({"touchstart mousedown":function(){var c=
a(this).attr("data-theme");a(this).removeClass("ui-btn-up-"+c).addClass("ui-btn-down-"+c)},"touchmove touchend mouseup":function(){var c=a(this).attr("data-theme");a(this).removeClass("ui-btn-down-"+c).addClass("ui-btn-up-"+c)},"mouseover focus":function(){var c=a(this).attr("data-theme");a(this).removeClass("ui-btn-up-"+c).addClass("ui-btn-hover-"+c)},"mouseout blur":function(){var c=a(this).attr("data-theme");a(this).removeClass("ui-btn-hover-"+c).addClass("ui-btn-up-"+c)}});d=null}})(jQuery);
(function(a){a.widget("mobile.button",a.mobile.widget,{options:{theme:null,icon:null,iconpos:null,inline:null,corners:true,shadow:true,iconshadow:true},_create:function(){var d=this.element,c=this.options;this.button=a("<div></div>").text(d.text()||d.val()).buttonMarkup({theme:c.theme,icon:c.icon,iconpos:c.iconpos,inline:c.inline,corners:c.corners,shadow:c.shadow,iconshadow:c.iconshadow}).insertBefore(d).append(d.addClass("ui-btn-hidden"));d.attr("type")!=="reset"&&d.click(function(){var f=a("<input>",
{type:"hidden",name:d.attr("name"),value:d.attr("value")}).insertBefore(d);a(document).submit(function(){f.remove()})})},enable:function(){this.element.attr("disabled",false);this.button.removeClass("ui-disabled").attr("aria-disabled",false);return this._setOption("disabled",false)},disable:function(){this.element.attr("disabled",true);this.button.addClass("ui-disabled").attr("aria-disabled",true);return this._setOption("disabled",true)}})})(jQuery);
(function(a){a.widget("mobile.slider",a.mobile.widget,{options:{theme:null,trackTheme:null,disabled:false},_create:function(){var d=this,c=this.element,f=c.parents("[class*=ui-bar-],[class*=ui-body-]").eq(0);f=f.length?f.attr("class").match(/ui-(bar|body)-([a-z])/)[2]:"c";var b=this.options.theme?this.options.theme:f,g=this.options.trackTheme?this.options.trackTheme:f,e=c[0].nodeName.toLowerCase();f=e=="select"?"ui-slider-switch":"";var i=c.attr("id"),h=i+"-label";i=a("[for="+i+"]").attr("id",h);
var k=function(){return e=="input"?parseFloat(c.val()):c[0].selectedIndex},j=e=="input"?parseFloat(c.attr("min")):0,o=e=="input"?parseFloat(c.attr("max")):c.find("option").length-1,p=window.parseFloat(c.attr("step")||1),t=a('<div class="ui-slider '+f+" ui-btn-down-"+g+' ui-btn-corner-all" role="application"></div>'),v=a('<a href="#" class="ui-slider-handle"></a>').appendTo(t).buttonMarkup({corners:true,theme:b,shadow:true}).attr({role:"slider","aria-valuemin":j,"aria-valuemax":o,"aria-valuenow":k(),
"aria-valuetext":k(),title:k(),"aria-labelledby":h});a.extend(this,{slider:t,handle:v,dragging:false,beforeStart:null});if(e=="select"){t.wrapInner('<div class="ui-slider-inneroffset"></div>');c.find("option");c.find("option").each(function(m){var r=m==0?"b":"a",n=m==0?"right":"left";m=m==0?" ui-btn-down-"+g:" ui-btn-active";a('<div class="ui-slider-labelbg ui-slider-labelbg-'+r+m+" ui-btn-corner-"+n+'"></div>').prependTo(t);a('<span class="ui-slider-label ui-slider-label-'+r+m+" ui-btn-corner-"+
n+'" role="img">'+a(this).text()+"</span>").prependTo(v)})}i.addClass("ui-slider");c.addClass(e=="input"?"ui-slider-input":"ui-slider-switch").change(function(){d.refresh(k(),true)}).keyup(function(){d.refresh(k(),true,true)}).blur(function(){d.refresh(k(),true)});a(document).bind("touchmove mousemove",function(m){if(d.dragging){d.refresh(m);return false}});t.bind("touchstart mousedown",function(m){d.dragging=true;if(e==="select")d.beforeStart=c[0].selectedIndex;d.refresh(m);return false});t.add(document).bind("touchend mouseup",
function(){if(d.dragging){d.dragging=false;if(e==="select"){if(d.beforeStart===c[0].selectedIndex)d.refresh(d.beforeStart===0?1:0);var m=k();m=Math.round(m/(o-j)*100);v.addClass("ui-slider-handle-snapping").css("left",m+"%").animationComplete(function(){v.removeClass("ui-slider-handle-snapping")})}return false}});t.insertAfter(c);this.handle.bind("touchstart mousedown",function(){a(this).focus()});this.handle.bind("keydown",function(m){var r=k();if(!d.options.disabled){switch(m.keyCode){case a.mobile.keyCode.HOME:case a.mobile.keyCode.END:case a.mobile.keyCode.PAGE_UP:case a.mobile.keyCode.PAGE_DOWN:case a.mobile.keyCode.UP:case a.mobile.keyCode.RIGHT:case a.mobile.keyCode.DOWN:case a.mobile.keyCode.LEFT:m.preventDefault();
if(!d._keySliding){d._keySliding=true;a(this).addClass("ui-state-active")}}switch(m.keyCode){case a.mobile.keyCode.HOME:d.refresh(j);break;case a.mobile.keyCode.END:d.refresh(o);break;case a.mobile.keyCode.PAGE_UP:case a.mobile.keyCode.UP:case a.mobile.keyCode.RIGHT:d.refresh(r+p);break;case a.mobile.keyCode.PAGE_DOWN:case a.mobile.keyCode.DOWN:case a.mobile.keyCode.LEFT:d.refresh(r-p)}}}).keyup(function(){if(d._keySliding){d._keySliding=false;a(this).removeClass("ui-state-active")}});this.refresh()},
refresh:function(d,c,f){if(!this.options.disabled){var b=this.element,g=b[0].nodeName.toLowerCase(),e=g==="input"?parseFloat(b.attr("min")):0,i=g==="input"?parseFloat(b.attr("max")):b.find("option").length-1;if(typeof d==="object"){d=d.originalEvent.touches?d.originalEvent.touches[0]:d;if(!this.dragging||d.pageX<this.slider.offset().left-8||d.pageX>this.slider.offset().left+this.slider.width()+8)return;d=Math.round((d.pageX-this.slider.offset().left)/this.slider.width()*100)}else{if(d==null)d=g===
"input"?parseFloat(b.val()):b[0].selectedIndex;d=(parseFloat(d)-e)/(i-e)*100}if(!isNaN(d)){if(d<0)d=0;if(d>100)d=100;var h=Math.round(d/100*(i-e))+e;if(h<e)h=e;if(h>i)h=i;this.handle.css("left",d+"%");this.handle.attr({"aria-valuenow":g==="input"?h:b.find("option").eq(h).attr("value"),"aria-valuetext":g==="input"?h:b.find("option").eq(h).text(),title:h});if(g==="select")h===0?this.slider.addClass("ui-slider-switch-a").removeClass("ui-slider-switch-b"):this.slider.addClass("ui-slider-switch-b").removeClass("ui-slider-switch-a");
if(!f){if(g==="input")b.val(h);else b[0].selectedIndex=h;c||b.trigger("change")}}}},enable:function(){this.element.attr("disabled",false);this.slider.removeClass("ui-disabled").attr("aria-disabled",false);return this._setOption("disabled",false)},disable:function(){this.element.attr("disabled",true);this.slider.addClass("ui-disabled").attr("aria-disabled",true);return this._setOption("disabled",true)}})})(jQuery);
(function(a){a.widget("mobile.collapsible",a.mobile.widget,{options:{expandCueText:" click to expand contents",collapseCueText:" click to collapse contents",collapsed:false,heading:">:header,>legend",theme:null,iconTheme:"d"},_create:function(){var d=this.element,c=this.options,f=d.addClass("ui-collapsible-contain"),b=d.find(c.heading).eq(0),g=f.wrapInner('<div class="ui-collapsible-content"></div>').find(".ui-collapsible-content");d=d.closest('[data-role="collapsible-set"]').addClass("ui-collapsible-set");
if(b.is("legend")){b=a('<div role="heading">'+b.html()+"</div>").insertBefore(b);b.next().remove()}b.insertBefore(g);b.addClass("ui-collapsible-heading").append('<span class="ui-collapsible-heading-status"></span>').wrapInner('<a href="#" class="ui-collapsible-heading-toggle"></a>').find("a:eq(0)").buttonMarkup({shadow:!!!d.length,corners:false,iconPos:"left",icon:"plus",theme:c.theme}).find(".ui-icon").removeAttr("class").buttonMarkup({shadow:true,corners:true,iconPos:"notext",icon:"plus",theme:c.iconTheme});
if(d.length)f.data("collapsible-last")&&b.find("a:eq(0), .ui-btn-inner").addClass("ui-corner-bottom");else b.find("a:eq(0)").addClass("ui-corner-all").find(".ui-btn-inner").addClass("ui-corner-all");f.bind("collapse",function(e){if(!e.isDefaultPrevented()){e.preventDefault();b.addClass("ui-collapsible-heading-collapsed").find(".ui-collapsible-heading-status").text(c.expandCueText);b.find(".ui-icon").removeClass("ui-icon-minus").addClass("ui-icon-plus");g.addClass("ui-collapsible-content-collapsed").attr("aria-hidden",
true);f.data("collapsible-last")&&b.find("a:eq(0), .ui-btn-inner").addClass("ui-corner-bottom")}}).bind("expand",function(e){if(!e.isDefaultPrevented()){e.preventDefault();b.removeClass("ui-collapsible-heading-collapsed").find(".ui-collapsible-heading-status").text(c.collapseCueText);b.find(".ui-icon").removeClass("ui-icon-plus").addClass("ui-icon-minus");g.removeClass("ui-collapsible-content-collapsed").attr("aria-hidden",false);f.data("collapsible-last")&&b.find("a:eq(0), .ui-btn-inner").removeClass("ui-corner-bottom")}}).trigger(c.collapsed?
"collapse":"expand");if(d.length&&!d.data("collapsiblebound")){d.data("collapsiblebound",true).bind("expand",function(e){a(this).find(".ui-collapsible-contain").not(a(e.target).closest(".ui-collapsible-contain")).not("> .ui-collapsible-contain .ui-collapsible-contain").trigger("collapse")});d=d.find("[data-role=collapsible]");d.first().find("a:eq(0)").addClass("ui-corner-top").find(".ui-btn-inner").addClass("ui-corner-top");d.last().data("collapsible-last",true)}b.bind(a.support.touch?"touchstart":
"click",function(){b.is(".ui-collapsible-heading-collapsed")?f.trigger("expand"):f.trigger("collapse");return false})}})})(jQuery);
(function(a){a.fn.controlgroup=function(d){return this.each(function(){function c(e){e.removeClass("ui-btn-corner-all ui-shadow").eq(0).addClass(g[0]).end().filter(":last").addClass(g[1]).addClass("ui-controlgroup-last")}var f=a.extend({direction:a(this).data("type")||"vertical",shadow:false},d),b=a(this).find(">legend"),g=f.direction=="horizontal"?["ui-corner-left","ui-corner-right"]:["ui-corner-top","ui-corner-bottom"];a(this).find("input:eq(0)").attr("type");if(b.length){a(this).wrapInner('<div class="ui-controlgroup-controls"></div>');
a('<div role="heading" class="ui-controlgroup-label">'+b.html()+"</div>").insertBefore(a(this).children(0));b.remove()}a(this).addClass("ui-corner-all ui-controlgroup ui-controlgroup-"+f.direction);c(a(this).find(".ui-btn"));c(a(this).find(".ui-btn-inner"));f.shadow&&a(this).addClass("ui-shadow")})}})(jQuery);(function(a){a.fn.fieldcontain=function(){return this.addClass("ui-field-contain ui-body ui-br")}})(jQuery);
(function(a){a.widget("mobile.listview",a.mobile.widget,{options:{theme:"c",countTheme:"c",headerTheme:"b",dividerTheme:"b",splitIcon:"arrow-r",splitTheme:"b",inset:false},_create:function(){var d=this.element,c=this.options;d.addClass("ui-listview").attr("role","listbox");c.inset&&d.addClass("ui-listview-inset ui-corner-all ui-shadow");d.delegate(".ui-li","focusin",function(){a(this).attr("tabindex","0")});this._itemApply(d,d);this.refresh(true);d.keydown(function(f){var b=a(f.target),g=b.closest("li");
switch(f.keyCode){case 38:f=g.prev();if(f.length){b.blur().attr("tabindex","-1");f.find("a").first().focus()}return false;case 40:f=g.next();if(f.length){b.blur().attr("tabindex","-1");f.find("a").first().focus()}return false;case 39:f=g.find("a.ui-li-link-alt");if(f.length){b.blur();f.first().focus()}return false;case 37:f=g.find("a.ui-link-inherit");if(f.length){b.blur();f.first().focus()}return false;case 13:case 32:b.trigger("click");return false}});d.delegate("li","click",function(f){if(!a(f.target).closest("a").length){a(this).find("a").first().trigger("click");
return false}})},_itemApply:function(d,c){c.find(".ui-li-count").addClass("ui-btn-up-"+(d.data("counttheme")||this.options.countTheme)+" ui-btn-corner-all");c.find("h1, h2, h3, h4, h5, h6").addClass("ui-li-heading");c.find("p, dl").addClass("ui-li-desc");c.find("li").find("img:eq(0)").addClass("ui-li-thumb").each(function(){a(this).closest("li").addClass(a(this).is(".ui-li-icon")?"ui-li-has-icon":"ui-li-has-thumb")});var f=c.find(".ui-li-aside");f.length&&f.each(function(b,g){a(g).prependTo(a(g).parent())});
a.support.cssPseudoElement||a.nodeName(c[0],"ol")},_removeCorners:function(d){d.add(d.find(".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb")).removeClass("ui-corner-top ui-corner-bottom ui-corner-br ui-corner-bl ui-corner-tr ui-corner-tl")},refresh:function(d){this._createSubPages();var c=this.options,f=this.element,b=this,g=f.data("dividertheme")||c.dividerTheme,e=f.children("li"),i=a.support.cssPseudoElement||!a.nodeName(f[0],"ol")?0:1;i&&f.find(".ui-li-dec").remove();e.attr({role:"option",tabindex:"-1"});
e.first().attr("tabindex","0");e.each(function(h){var k=a(this),j="ui-li";if(!(!d&&k.hasClass("ui-li"))){var o=k.data("theme")||c.theme,p=k.find("a");if(p.length){var t=k.data("icon");k.buttonMarkup({wrapperEls:"div",shadow:false,corners:false,iconpos:"right",icon:p.length>1||t===false?false:t||"arrow-r",theme:o});p.first().addClass("ui-link-inherit");if(p.length>1){j+=" ui-li-has-alt";p=p.last();t=f.data("splittheme")||p.data("theme")||c.splitTheme;p.appendTo(k).attr("title",p.text()).addClass("ui-li-link-alt").empty().buttonMarkup({shadow:false,
corners:false,theme:o,icon:false,iconpos:false}).find(".ui-btn-inner").append(a("<span>").buttonMarkup({shadow:true,corners:true,theme:t,iconpos:"notext",icon:f.data("spliticon")||p.data("icon")||c.splitIcon}))}}else if(k.data("role")==="list-divider"){j+=" ui-li-divider ui-btn ui-bar-"+g;k.attr("role","heading");if(i)i=1}else j+=" ui-li-static ui-btn-up-"+o;if(c.inset){if(h===0){j+=" ui-corner-top";k.add(k.find(".ui-btn-inner")).find(".ui-li-link-alt").addClass("ui-corner-tr").end().find(".ui-li-thumb").addClass("ui-corner-tl");
k.next().next().length&&b._removeCorners(k.next())}if(h===e.length-1){j+=" ui-corner-bottom";k.add(k.find(".ui-btn-inner")).find(".ui-li-link-alt").addClass("ui-corner-br").end().find(".ui-li-thumb").addClass("ui-corner-bl");k.prev().prev().length&&b._removeCorners(k.prev())}}i&&j.indexOf("ui-li-divider")<0&&k.find(".ui-link-inherit").first().addClass("ui-li-jsnumbering").prepend("<span class='ui-li-dec'>"+i++ +". </span>");k.addClass(j);d||b._itemApply(f,k)}})},_idStringEscape:function(d){return d.replace(/[^a-zA-Z0-9]/g,
"-")},_createSubPages:function(){var d=this.element,c=d.closest(".ui-page"),f=c.data("url"),b=this.options,g=this,e=c.find("[data-role='footer']").data("id");a(d.find("ul, ol").toArray().reverse()).each(function(i){var h=a(this),k=h.parent(),j=a.trim(k.contents()[0].nodeValue)||k.find("a:first").text();i=f+"&"+a.mobile.subPageUrlKey+"="+g._idStringEscape(j+" "+i);var o=h.data("theme")||b.theme,p=h.data("counttheme")||d.data("counttheme")||b.countTheme;h.wrap("<div data-role='page'><div data-role='content'></div></div>").parent().before("<div data-role='header' data-theme='"+
b.headerTheme+"'><div class='ui-title'>"+j+"</div></div>").after(e?a("<div>",{"data-role":"footer","data-id":e,"class":"ui-footer-duplicate"}):"").parent().attr({"data-url":i,"data-theme":o,"data-count-theme":p}).appendTo(a.mobile.pageContainer).page();h=k.find("a:first");h.length||(h=a("<a></a>").html(j).prependTo(k.empty()));h.attr("href","#"+i)}).listview()}})})(jQuery);
(function(a){a.mobile.listview.prototype.options.filter=false;a("[data-role='listview']").live("listviewcreate",function(){var d=a(this);if(d.data("listview").options.filter){var c=a("<form>",{"class":"ui-listview-filter ui-bar-c",role:"search"});a("<input>",{placeholder:"Filter results...","data-type":"search"}).bind("keyup change",function(){var f=this.value.toLowerCase();d.children().show();f&&d.children().filter(function(){return a(this).text().toLowerCase().indexOf(f)===-1}).hide()}).appendTo(c).textinput();
c.insertBefore(d)}})})(jQuery);
(function(a){a.widget("mobile.dialog",a.mobile.widget,{options:{},_create:function(){this.element.attr("role","dialog").addClass("ui-page ui-dialog ui-body-a").find("[data-role=header]").addClass("ui-corner-top ui-overlay-shadow").prepend('<a href="#" data-icon="delete" data-rel="back" data-iconpos="notext">Close</a>').end().find('.ui-content:not([class*="ui-body-"])').addClass("ui-body-c").end().find(".ui-content,[data-role=footer]").last().addClass("ui-corner-bottom ui-overlay-shadow");this.element.bind("click submit",
function(d){d=d.type=="click"?a(d.target).closest("a"):a(d.target).closest("form");d.length&&!d.data("transition")&&d.attr("data-transition",a.mobile.urlHistory.getActive().transition).attr("data-direction","reverse")})},close:function(){window.history.back()}})})(jQuery);
(function(a,d){a.widget("mobile.navbar",a.mobile.widget,{options:{iconpos:"top",grid:null},_create:function(){var c=this.element,f=c.find("a"),b=f.filter("[data-icon]").length?this.options.iconpos:d;c.addClass("ui-navbar").attr("role","navigation").find("ul").grid({grid:this.options.grid});b||c.addClass("ui-navbar-noicons");f.buttonMarkup({corners:false,shadow:false,iconpos:b});c.delegate("a","click",function(){f.removeClass("ui-btn-active");a(this).addClass("ui-btn-active")})}})})(jQuery);
(function(a){a.fn.grid=function(d){return this.each(function(){var c=a.extend({grid:null},d),f=a(this).children(),b={a:2,b:3,c:4,d:5};c=c.grid;if(!c)if(f.length<=5)for(var g in b){if(b[g]==f.length)c=g}else c="a";b=b[c];a(this).addClass("ui-grid-"+c);f.filter(":nth-child("+b+"n+1)").addClass("ui-block-a");f.filter(":nth-child("+b+"n+2)").addClass("ui-block-b");b>2&&f.filter(":nth-child(3n+3)").addClass("ui-block-c");b>3&&f.filter(":nth-child(4n+4)").addClass("ui-block-d");b>4&&f.filter(":nth-child(5n+5)").addClass("ui-block-e")})}})(jQuery);

View File

@ -0,0 +1,480 @@
/*
http://www.JSON.org/json2.js
2011-02-23
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
This file creates a global JSON object containing two methods: stringify
and parse.
JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array of strings.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or '&nbsp;'),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the value
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array of strings, then it will be
used to select the members to be serialized. It filters the results
such that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = JSON.stringify(['e', {pluribus: 'unum'}]);
// text is '["e",{"pluribus":"unum"}]'
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
text = JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
// text is '["Date(---current time---)"]'
JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
// Parse the text. Values that look like ISO date strings will
// be converted to Date objects.
myData = JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
*/
/*jslint evil: true, strict: false, regexp: false */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
var JSON;
if (!JSON) {
JSON = {};
}
(function () {
"use strict";
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return isFinite(this.valueOf()) ?
this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z' : null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string' ? c :
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' : '"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === '[object Array]') {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0 ? '[]' : gap ?
'[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
'[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
if (typeof rep[i] === 'string') {
k = rep[i];
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0 ? '{}' : gap ?
'{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
'{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== 'function') {
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== 'function') {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function' ?
walk({'': j}, '') : j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse');
};
}
}());

View File

@ -20,13 +20,10 @@
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
*****************************************************************************/ *****************************************************************************/
/** .ui-icon-blank {
* init.js - In certain browsers (yes, IE, I'm looking at you!), DocumentReady background-image: url(images/ui-icon-blank.png);
* JavaScript functions can only be run very last on the page. This file is the }
* last JavaScript file to be included on the page, and provides a work-around
* for this bug in certain browsers.
*/
$(document).ready(function () { .ui-icon-unblank {
OpenLP.Events.init(); background-image: url(images/ui-icon-unblank.png);
}); }

View File

@ -20,85 +20,7 @@
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
*****************************************************************************/ *****************************************************************************/
window["OpenLP"] = { window.OpenLP = {
Namespace: {
/**
* Create a Javascript namespace.
* Based on: http://code.google.com/p/namespacedotjs/
* Idea behind this is to created nested namespaces that are not ugly.
*/
create: function (name, attributes) {
var parts = name.split('.'),
ns = window,
i = 0;
// find the deepest part of the namespace
// that is already defined
for(; i < parts.length && parts[i] in ns; i++)
ns = ns[parts[i]];
// initialize any remaining parts of the namespace
for(; i < parts.length; i++)
ns = ns[parts[i]] = {};
// copy the attributes into the namespace
for (var attr in attributes)
ns[attr] = attributes[attr];
},
exists: function (namespace) {
/**
* Determine the namespace of a page
*/
page_namespace = $ScribeEngine.Namespace.get_page_namespace();
return (namespace == page_namespace);
},
get_page_namespace: function () {
return $("#content > h2").attr("id");
}
}
};
Array.prototype.append = function (elem) {
this[this.length] = elem;
}
OpenLP.Namespace.create("OpenLP.Events", {
// Local variables
onload_functions: Array(),
// Functions
bindLoad: function (func) {
this.onload_functions.append(func);
},
bindClick: function (selector, func) {
$(selector).bind("click", func);
},
bindChange: function (selector, func) {
$(selector).bind("change", func);
},
bindSubmit: function (selector, func) {
$(selector).bind("submit", func);
},
bindBlur: function (selector, func) {
$(selector).bind("blur", func);
},
bindPaste: function (selector, func) {
$(selector).bind("paste", func);
},
bindKeyUp: function (selector, func) {
$(selector).bind("keyup", func);
},
bindKeyDown: function (selector, func) {
$(selector).bind("keydown", func);
},
bindKeyPress: function (selector, func) {
$(selector).bind("keypress", func);
},
bindMouseEnter: function (selector, func) {
$(selector).bind("mouseenter", func);
},
bindMouseLeave: function (selector, func) {
$(selector).bind("mouseleave", func);
},
liveClick: function (selector, func) {
$(selector).live("click", func);
},
getElement: function(event) { getElement: function(event) {
var targ; var targ;
if (!event) { if (!event) {
@ -116,128 +38,167 @@ OpenLP.Namespace.create("OpenLP.Events", {
} }
return $(targ); return $(targ);
}, },
init: function () { loadService: function (event) {
for (idx in this.onload_functions) { $.getJSON(
func = this.onload_functions[idx]; "/api/service/list",
func(); function (data, status) {
var ul = $("#service-manager > div[data-role=content] > ul[data-role=listview]");
ul.html("");
$.each(data.results.items, function (idx, value) {
var li = $("<li data-icon=\"false\">").append(
$("<a href=\"#\">").attr("value", parseInt(idx, 10)).text(value["title"]));
li.children("a").click(OpenLP.setItem);
ul.append(li);
});
ul.listview("refresh");
} }
} );
});
OpenLP.Namespace.create("OpenLP.Remote", {
sendEvent: function (eventName, eventData)
{
var url = "/";
if (eventName.substr(-8) == "_request")
{
url += "request";
}
else
{
url += "send";
}
url += "/" + eventName;
var args = {};
if (eventData != null && eventData != "")
{
args.q = escape(eventData);
}
$.ajax({
url: url,
dataType: "json",
data: args,
success: function (data)
{
OpenLP.Remote.handleEvent(eventName, data);
}, },
error: function (xhr, textStatus, errorThrown) loadController: function (event) {
{ $.getJSON(
if (eventName == "remotes_poll_request") "/api/controller/live/text",
{ function (data, status) {
OpenLP.Remote.handleEvent("remotes_poll_request"); var ul = $("#slide-controller > div[data-role=content] > ul[data-role=listview]");
ul.html("");
for (idx in data.results.slides) {
var li = $("<li data-icon=\"false\">").append(
$("<a href=\"#\">").attr("value", parseInt(idx, 10)).html(data.results.slides[idx]["text"]));
if (data.results.slides[idx]["selected"]) {
li.attr("data-theme", "e");
} }
li.children("a").click(OpenLP.setSlide);
ul.append(li);
}
ul.listview("refresh");
}
);
},
setItem: function (event) {
var item = OpenLP.getElement(event);
var id = item.attr("value");
var text = JSON.stringify({"request": {"id": id}});
$.getJSON(
"/api/service/set",
{"data": text},
function (data, status) {
$("#service-manager > div[data-role=content] ul[data-role=listview] li").attr("data-theme", "c").removeClass("ui-btn-up-e").addClass("ui-btn-up-c");
while (item[0].tagName != "LI") {
item = item.parent();
}
item.attr("data-theme", "e").removeClass("ui-btn-up-c").addClass("ui-btn-up-e");
$("#service-manager > div[data-role=content] ul[data-role=listview]").listview("refresh");
}
);
},
setSlide: function (event) {
var slide = OpenLP.getElement(event);
console.log(slide);
var id = slide.attr("value");
var text = JSON.stringify({"request": {"id": id}});
$.getJSON(
"/api/controller/live/set",
{"data": text},
function (data, status) {
$("#slide-controller div[data-role=content] ul[data-role=listview] li").attr("data-theme", "c").removeClass("ui-btn-up-e").addClass("ui-btn-up-c");
while (slide[0].tagName != "LI") {
slide = slide.parent();
}
slide.attr("data-theme", "e").removeClass("ui-btn-up-c").addClass("ui-btn-up-e");
$("#slide-controller div[data-role=content] ul[data-role=listview]").listview("refresh");
}
);
},
pollServer: function () {
$.getJSON(
"/api/poll",
function (data, status) {
OpenLP.currentSlide = data.results.slide;
OpenLP.currentItem = data.results.item;
if ($("#service-manager").is(":visible")) {
$("#service-manager div[data-role=content] ul[data-role=listview] li").attr("data-theme", "c").removeClass("ui-btn-up-e").addClass("ui-btn-up-c");
$("#service-manager div[data-role=content] ul[data-role=listview] li a").each(function () {
var item = $(this);
if (item.text() == OpenLP.currentItem) {
while (item[0].tagName != "LI") {
item = item.parent();
}
item.attr("data-theme", "e").removeClass("ui-btn-up-c").addClass("ui-btn-up-e");
return false;
} }
}); });
}, $("#service-manager div[data-role=content] ul[data-role=listview]").listview("refresh");
handleEvent: function (eventName, eventData)
{
switch (eventName)
{
case "servicemanager_list_request":
var table = $("<table>");
$.each(eventData, function (row, item) {
var trow = $("<tr>")
.attr("value", parseInt(row))
.click(OpenLP.Remote.sendSetItem);
if (item["selected"])
{
trow.addClass("selected");
} }
trow.append($("<td>").text(parseInt(row) + 1)); if ($("#slide-controller").is(":visible")) {
trow.append($("<td>").text(item["title"])); var idx = 0;
trow.append($("<td>").text(item["plugin"])); $("#slide-controller div[data-role=content] ul[data-role=listview] li").attr("data-theme", "c").removeClass("ui-btn-up-e").addClass("ui-btn-up-c");
trow.append($("<td>").text("Notes: " + item["notes"])); $("#slide-controller div[data-role=content] ul[data-role=listview] li a").each(function () {
table.append(trow); var item = $(this);
if (idx == OpenLP.currentSlide) {
while (item[0].tagName != "LI") {
item = item.parent();
}
item.attr("data-theme", "e").removeClass("ui-btn-up-c").addClass("ui-btn-up-e");
return false;
}
idx++;
}); });
$("#service").html(table); $("#slide-controller div[data-role=content] ul[data-role=listview]").listview("refresh");
break;
case "slidecontroller_live_text_request":
var table = $("<table>");
$.each(eventData, function (row, item) {
var trow = $("<tr>")
.attr("value", parseInt(row))
.click(OpenLP.Remote.sendLiveSet);
if (item["selected"])
{
trow.addClass("selected");
} }
trow.append($("<td>").text(item["tag"]));
trow.append($("<td>").html(item["text"] ? item["text"].replace(/\\n/g, "<br />") : ""));
table.append(trow);
});
$("#current-item").html(table);
break;
case "remotes_poll_request":
OpenLP.Remote.sendEvent("remotes_poll_request");
OpenLP.Remote.sendEvent("servicemanager_list_request");
OpenLP.Remote.sendEvent("slidecontroller_live_text_request");
break;
} }
);
}, },
sendLiveSet: function (e) nextItem: function (event) {
{ $.getJSON("/api/service/next");
var tr = OpenLP.Events.getElement(e).parent();
if (tr[0].tagName != "TR")
{
tr = tr.parent();
}
OpenLP.Remote.sendEvent("slidecontroller_live_set", tr.attr("value"));
return false; return false;
}, },
sendSetItem: function (e) previousItem: function (event) {
{ $.getJSON("/api/service/previous");
var id = OpenLP.Events.getElement(e).parent().attr("value");
OpenLP.Remote.sendEvent("servicemanager_set_item", id);
return false; return false;
}, },
sendAlert: function (e) nextSlide: function (event) {
{ $.getJSON("/api/controller/live/next");
var alert_text = $("#alert-text").val();
OpenLP.Remote.sendEvent("alerts_text", alert_text);
return false; return false;
}, },
buttonClick: function (e) previousSlide: function (event) {
{ $.getJSON("/api/controller/live/previous");
var id = OpenLP.Events.getElement(e).attr("id"); return false;
OpenLP.Remote.sendEvent(id); },
blankDisplay: function (event) {
$.getJSON("/api/display/hide");
return false;
},
unblankDisplay: function (event) {
$.getJSON("/api/display/show");
return false;
},
showAlert: function (event) {
var text = JSON.stringify({"request": {"text": $("#alert-text").val()}});
$.getJSON(
"/api/alert",
{"data": text},
function () {
$("#alert-text").val("");
}
);
return false; return false;
} }
}); }
// Service Manager
OpenLP.Events.bindLoad(function () { $("#service-manager").live("pagebeforeshow", OpenLP.loadService);
OpenLP.Events.bindClick("input[type=button][id!=alert-send]", OpenLP.Remote.buttonClick); $("#service-refresh").live("click", OpenLP.loadService);
OpenLP.Events.bindClick("#alert-send", OpenLP.Remote.sendAlert); $("#service-next").live("click", OpenLP.nextItem);
OpenLP.Remote.sendEvent("servicemanager_list_request"); $("#service-previous").live("click", OpenLP.previousItem);
OpenLP.Remote.sendEvent("slidecontroller_live_text_request"); $("#service-blank").live("click", OpenLP.blankDisplay);
OpenLP.Remote.sendEvent("remotes_poll_request"); $("#service-unblank").live("click", OpenLP.unblankDisplay);
}); // Slide Controller
$("#slide-controller").live("pagebeforeshow", OpenLP.loadController);
$("#controller-refresh").live("click", OpenLP.loadController);
$("#controller-next").live("click", OpenLP.nextSlide);
$("#controller-previous").live("click", OpenLP.previousSlide);
$("#controller-blank").live("click", OpenLP.blankDisplay);
$("#controller-unblank").live("click", OpenLP.unblankDisplay);
// Alerts
$("#alert-submit").live("click", OpenLP.showAlert);
// Poll the server twice a second to get any updates.
setInterval("OpenLP.pollServer();", 500);
OpenLP.pollServer();

View File

@ -1,44 +0,0 @@
/*****************************************************************************
* OpenLP - Open Source Lyrics Projection *
* ------------------------------------------------------------------------- *
* Copyright (c) 2008-2010 Raoul Snyman *
* Portions copyright (c) 2008-2010 Tim Bentley, Jonathan Corwin, Michael *
* Gorven, Scott Guerrieri, Christian Richter, Maikel Stuivenberg, Martin *
* Thompson, Jon Tibble, Carsten Tinggaard *
* ------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the *
* Free Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General *
* Public License for more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., *
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
*****************************************************************************/
OpenLP.Namespace.create("OpenLP.Service", {
addServiceItem: function (elem, item)
{
var trow = $("<tr>")
.attr("id", "item-" + item.id)
.addClass("item")
.append($("<td>").text(item.tag))
.append($("<td>").text(item.tag.replace(/\n/g, "<br />")));
return false;
},
sendLive: function (e)
{
var elem = OpenLP.Events.getElement(e);
var row = elem.attr("id").substr(5);
elem.addStyle("font-weight", "bold");
return false;
}
});
OpenLP.Events.load(function (){
OpenLP.Events.liveClick(".item", OpenLP.Service.sendLive);
});

View File

@ -1,45 +0,0 @@
body
{
background-color: #fff;
font-family: Lucida Grande, Lucida Sans, Arial, Helvetica, sans-serif;
font-size: 80%;
}
a
{
color: #3c60a5;
text-decoration: none;
}
a:hover
{
color: #304d85;
text-decoration: underline;
}
fieldset
{
border: 1px solid #ccc;
}
td
{
cursor: pointer;
padding: 3px 5px;
}
tr:hover
{
background-color: #eee;
}
tr.selected:hover
{
background-color: #ccc;
}
.selected
{
background-color: #ddd;
font-weight: bold;
}

View File

@ -26,3 +26,5 @@
from remotetab import RemoteTab from remotetab import RemoteTab
from httpserver import HttpServer from httpserver import HttpServer
__all__ = [u'RemoteTab', u'HttpServer']

View File

@ -24,9 +24,94 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
"""
The :mod:`http` module contains the API web server. This is a lightweight web
server used by remotes to interact with OpenLP. It uses JSON to communicate with
the remotes.
*Routes:*
``/``
Go to the web interface.
``/files/{filename}``
Serve a static file.
``/api/poll``
Poll to see if there are any changes. Returns a JSON-encoded dict of
any changes that occurred::
{"results": {"type": "controller"}}
Or, if there were no results, False::
{"results": False}
``/api/display/{hide|show}``
Blank or unblank the screen.
``/api/alert``
Sends an alert message to the alerts plugin. This method expects a
JSON-encoded dict like this::
{"request": {"text": "<your alert text>"}}
``/api/controller/{live|preview}/{action}``
Perform ``{action}`` on the live or preview controller. Valid actions
are:
``next``
Load the next slide.
``previous``
Load the previous slide.
``set``
Set a specific slide. Requires an id return in a JSON-encoded dict like
this::
{"request": {"id": 1}}
``first``
Load the first slide.
``last``
Load the last slide.
``text``
Fetches the text of the current song. The output is a JSON-encoded
dict which looks like this::
{"result": {"slides": ["...", "..."]}}
``/api/service/{action}``
Perform ``{action}`` on the service manager (e.g. go live). Data is
passed as a json-encoded ``data`` parameter. Valid actions are:
``next``
Load the next item in the service.
``previous``
Load the previews item in the service.
``set``
Set a specific item in the service. Requires an id returned in a
JSON-encoded dict like this::
{"request": {"id": 1}}
``list``
Request a list of items in the service. Returns a list of items in the
current service in a JSON-encoded dict like this::
{"results": {"items": [{...}, {...}]}}
"""
import logging import logging
import os import os
import urlparse import urlparse
import re
from pprint import pformat
try: try:
import json import json
@ -36,10 +121,29 @@ except ImportError:
from PyQt4 import QtCore, QtNetwork from PyQt4 import QtCore, QtNetwork
from openlp.core.lib import Receiver from openlp.core.lib import Receiver
from openlp.core.ui import HideMode
from openlp.core.utils import AppLocation from openlp.core.utils import AppLocation
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class HttpResponse(object):
"""
A simple object to encapsulate a pseudo-http response.
"""
code = '200 OK'
content = ''
headers = {
'Content-Type': 'text/html; charset="utf-8"\r\n'
}
def __init__(self, content='', headers={}, code=None):
self.content = content
for key, value in headers.iteritems():
self.headers[key] = value
if code:
self.code = code
class HttpServer(object): class HttpServer(object):
""" """
Ability to control OpenLP via a webbrowser Ability to control OpenLP via a webbrowser
@ -90,22 +194,12 @@ class HttpServer(object):
Slide change listener. Store the item and tell the clients Slide change listener. Store the item and tell the clients
""" """
self.current_slide = row self.current_slide = row
self.send_poll()
def item_change(self, items): def item_change(self, items):
""" """
Item (song) change listener. Store the slide and tell the clients Item (song) change listener. Store the slide and tell the clients
""" """
self.current_item = items[0].title self.current_item = items[0]
self.send_poll()
def send_poll(self):
"""
Tell the clients something has changed
"""
Receiver.send_message(u'remotes_poll_response',
{'slide': self.current_slide,
'item': self.current_item})
def new_connection(self): def new_connection(self):
""" """
@ -122,6 +216,7 @@ class HttpServer(object):
The connection has been closed. Clean up The connection has been closed. Clean up
""" """
log.debug(u'close http connection') log.debug(u'close http connection')
if connection in self.connections:
self.connections.remove(connection) self.connections.remove(connection)
def close(self): def close(self):
@ -138,17 +233,44 @@ class HttpConnection(object):
""" """
def __init__(self, parent, socket): def __init__(self, parent, socket):
""" """
Initialise the http connection. Listen out for socket signals Initialise the http connection. Listen out for socket signals.
""" """
log.debug(u'Initialise HttpConnection: %s' % log.debug(u'Initialise HttpConnection: %s' %
socket.peerAddress().toString()) socket.peerAddress().toString())
self.socket = socket self.socket = socket
self.parent = parent self.parent = parent
self.routes = [
(u'^/$', self.serve_file),
(r'^/files/(.*)$', self.serve_file),
(r'^/api/poll$', self.poll),
(r'^/api/controller/(live|preview)/(.*)$', self.controller),
(r'^/api/service/(.*)$', self.service),
(r'^/api/display/(hide|show)$', self.display),
(r'^/api/alert$', self.alert)
]
QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'readyRead()'), QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'readyRead()'),
self.ready_read) self.ready_read)
QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'disconnected()'), QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'disconnected()'),
self.disconnected) self.disconnected)
def _get_service_items(self):
service_items = []
service_manager = self.parent.parent.serviceManager
item = service_manager.findServiceItem()[0]
if item >= 0 and item < len(service_manager.serviceItems):
curitem = service_manager.serviceItems[item]
else:
curitem = None
for item in service_manager.serviceItems:
service_item = item[u'service_item']
service_items.append({
u'title': unicode(service_item.get_display_title()),
u'plugin': unicode(service_item.name),
u'notes': unicode(service_item.notes),
u'selected': (item == curitem)
})
return service_items
def ready_read(self): def ready_read(self):
""" """
Data has been sent from the client. Respond to it Data has been sent from the client. Respond to it
@ -158,32 +280,27 @@ class HttpConnection(object):
data = unicode(self.socket.readLine()) data = unicode(self.socket.readLine())
log.debug(u'received: ' + data) log.debug(u'received: ' + data)
words = data.split(u' ') words = data.split(u' ')
html = None response = None
mimetype = None
if words[0] == u'GET': if words[0] == u'GET':
url = urlparse.urlparse(words[1]) url = urlparse.urlparse(words[1])
params = self.load_params(url.query) self.url_params = urlparse.parse_qs(url.query)
folders = url.path.split(u'/') # Loop through the routes we set up earlier and execute them
if folders[1] == u'': for route, func in self.routes:
mimetype, html = self.serve_file(u'') match = re.match(route, url.path)
elif folders[1] == u'files': if match:
mimetype, html = self.serve_file(os.sep.join(folders[2:])) log.debug('Route "%s" matched "%s"', route, url.path)
elif folders[1] == u'send': args = []
html = self.process_event(folders[2], params) for param in match.groups():
elif folders[1] == u'request': args.append(param)
if self.process_request(folders[2], params): response = func(*args)
return break
if html: if response:
if mimetype: self.send_response(response)
self.send_200_ok(mimetype)
else: else:
self.send_200_ok() self.send_response(HttpResponse(code='404 Not Found'))
self.socket.write(html)
else:
self.send_404_not_found()
self.close() self.close()
def serve_file(self, filename): def serve_file(self, filename=None):
""" """
Send a file to the socket. For now, just a subset of file types Send a file to the socket. For now, just a subset of file types
and must be top level inside the html folder. and must be top level inside the html folder.
@ -197,7 +314,7 @@ class HttpConnection(object):
filename = u'index.html' filename = u'index.html'
path = os.path.normpath(os.path.join(self.parent.html_dir, filename)) path = os.path.normpath(os.path.join(self.parent.html_dir, filename))
if not path.startswith(self.parent.html_dir): if not path.startswith(self.parent.html_dir):
return None return HttpResponse(code=u'404 Not Found')
ext = os.path.splitext(filename)[1] ext = os.path.splitext(filename)[1]
if ext == u'.html': if ext == u'.html':
mimetype = u'text/html' mimetype = u'text/html'
@ -212,126 +329,116 @@ class HttpConnection(object):
elif ext == u'.png': elif ext == u'.png':
mimetype = u'image/png' mimetype = u'image/png'
else: else:
return (None, None) mimetype = u'text/plain'
file_handle = None file_handle = None
try: try:
file_handle = open(path, u'rb') file_handle = open(path, u'rb')
log.debug(u'Opened %s' % path) log.debug(u'Opened %s' % path)
html = file_handle.read() content = file_handle.read()
except IOError: except IOError:
log.exception(u'Failed to open %s' % path) log.exception(u'Failed to open %s' % path)
return None return HttpResponse(code=u'404 Not Found')
finally: finally:
if file_handle: if file_handle:
file_handle.close() file_handle.close()
return (mimetype, html) return HttpResponse(content, {u'Content-Type': mimetype})
def load_params(self, query): def poll(self):
""" """
Decode the query string parameters sent from the browser Poll OpenLP to determine the current slide number and item name.
""" """
log.debug(u'loading params %s' % query) result = {
params = urlparse.parse_qs(query) u'slide': self.parent.current_slide or 0,
if not params: u'item': self.parent.current_item.title \
return None if self.parent.current_item else u''
}
return HttpResponse(json.dumps({u'results': result}),
{u'Content-Type': u'application/json'})
def display(self, action):
"""
Hide or show the display screen.
``action``
This is the action, either ``hide`` or ``show``.
"""
event = u'maindisplay_%s' % action
Receiver.send_message(event, HideMode.Blank)
return HttpResponse(json.dumps({u'results': {u'success': True}}),
{u'Content-Type': u'application/json'})
def alert(self):
"""
Send an alert.
"""
text = json.loads(self.url_params[u'data'][0])[u'request'][u'text']
Receiver.send_message(u'alerts_text', [text])
return HttpResponse(json.dumps({u'results': {u'success': True}}),
{u'Content-Type': u'application/json'})
def controller(self, type, action):
"""
Perform an action on the slide controller.
``type``
This is the type of slide controller, either ``preview`` or
``live``.
``action``
The action to perform.
"""
event = u'slidecontroller_%s_%s' % (type, action)
if action == u'text':
current_item = self.parent.current_item
data = []
if current_item:
for index, frame in enumerate(current_item.get_frames()):
item = {}
if current_item.is_text():
item[u'tag'] = unicode(frame[u'verseTag'])
item[u'text'] = unicode(frame[u'html'])
else: else:
return params['q'] item[u'tag'] = unicode(index)
item[u'text'] = u''
def process_event(self, event, params): item[u'selected'] = (self.parent.current_slide == index)
""" data.append(item)
Send a signal to openlp to perform an action. json_data = {u'results': {u'slides': data}}
Currently lets anything through. Later we should restrict and perform else:
basic parameter checking, otherwise rogue clients could crash openlp if self.url_params and self.url_params.get(u'data'):
""" data = json.loads(self.url_params[u'data'][0])
log.debug(u'Processing event %s' % event) log.info(data)
if params: # This slot expects an int within a list.
Receiver.send_message(event, params) id = data[u'request'][u'id']
Receiver.send_message(event, [id])
else: else:
Receiver.send_message(event) Receiver.send_message(event)
return json.dumps([u'OK']) json_data = {u'results': {u'success': True}}
return HttpResponse(json.dumps(json_data),
{u'Content-Type': u'application/json'})
def process_request(self, event, params): def service(self, action):
""" event = u'servicemanager_%s' % action
Client has requested data. Send the signal and parameters for openlp if action == u'list':
to handle, then listen out for a corresponding ``_request`` signal return HttpResponse(
which will have the data to return. json.dumps({u'results': {u'items': self._get_service_items()}}),
{u'Content-Type': u'application/json'})
For most events, timeout after 10 seconds (i.e. in case the signal
recipient isn't listening). ``remotes_poll_request`` is a special case
however, this is a ajax long poll which is just waiting for slide
change/song change activity. This can wait longer (one minute).
``event``
The event from the web page.
``params``
Parameters sent with the event.
"""
log.debug(u'Processing request %s' % event)
if not event.endswith(u'_request'):
return False
self.event = event
response = event.replace(u'_request', u'_response')
QtCore.QObject.connect(Receiver.get_receiver(),
QtCore.SIGNAL(response), self.process_response)
self.timer = QtCore.QTimer()
self.timer.setSingleShot(True)
QtCore.QObject.connect(self.timer,
QtCore.SIGNAL(u'timeout()'), self.timeout)
if event == 'remotes_poll_request':
self.timer.start(60000)
else: else:
self.timer.start(10000) event += u'_item'
if params: if self.url_params and self.url_params.get(u'data'):
Receiver.send_message(event, params) data = json.loads(self.url_params[u'data'][0])
Receiver.send_message(event, data[u'request'][u'id'])
else: else:
Receiver.send_message(event) Receiver.send_message(event)
return True return HttpResponse(json.dumps({u'results': {u'success': True}}),
{u'Content-Type': u'application/json'})
def process_response(self, data): def send_response(self, response):
""" http = u'HTTP/1.1 %s\r\n' % response.code
The recipient of a _request signal has sent data. Convert this to for header, value in response.headers.iteritems():
json and return it to client http += '%s: %s\r\n' % (header, value)
""" http += '\r\n'
log.debug(u'Processing response for %s' % self.event) self.socket.write(http)
if not self.socket: self.socket.write(response.content)
return
self.timer.stop()
html = json.dumps(data)
self.send_200_ok()
self.socket.write(html)
self.close()
def send_200_ok(self, mimetype='text/html; charset="utf-8"'):
"""
Successful request. Send OK headers. Assume html for now.
"""
self.socket.write(u'HTTP/1.1 200 OK\r\n' + \
u'Content-Type: %s\r\n\r\n' % mimetype)
def send_404_not_found(self):
"""
Invalid url. Say so
"""
self.socket.write(u'HTTP/1.1 404 Not Found\r\n'+ \
u'Content-Type: text/html; charset="utf-8"\r\n' + \
u'\r\n')
def send_408_timeout(self):
"""
A _request hasn't returned anything in the timeout period.
Return timeout
"""
self.socket.write(u'HTTP/1.1 408 Request Timeout\r\n')
def timeout(self):
"""
Listener for timeout signal
"""
if not self.socket:
return
self.send_408_timeout()
self.close()
def disconnected(self): def disconnected(self):
""" """

View File

@ -33,7 +33,7 @@ from openlp.core.lib import Receiver, translate
from openlp.core.lib.ui import UiStrings, add_widget_completer, \ from openlp.core.lib.ui import UiStrings, add_widget_completer, \
critical_error_message_box critical_error_message_box
from openlp.plugins.songs.forms import EditVerseForm from openlp.plugins.songs.forms import EditVerseForm
from openlp.plugins.songs.lib import SongXML, VerseType from openlp.plugins.songs.lib import SongXML, VerseType, clean_song
from openlp.plugins.songs.lib.db import Book, Song, Author, Topic from openlp.plugins.songs.lib.db import Book, Song, Author, Topic
from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.ui import SongStrings
from editsongdialog import Ui_EditSongDialog from editsongdialog import Ui_EditSongDialog
@ -728,17 +728,15 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog):
self.song.title = unicode(self.titleEdit.text()) self.song.title = unicode(self.titleEdit.text())
self.song.alternate_title = unicode(self.alternativeEdit.text()) self.song.alternate_title = unicode(self.alternativeEdit.text())
self.song.copyright = unicode(self.copyrightEdit.text()) self.song.copyright = unicode(self.copyrightEdit.text())
if self.song.alternate_title: # Values will be set when cleaning the song.
self.song.search_title = self.song.title + u'@' + \ self.song.search_title = u''
self.song.alternate_title self.song.search_lyrics = u''
else: self.song.verse_order = u''
self.song.search_title = self.song.title
self.song.comments = unicode(self.commentsEdit.toPlainText()) self.song.comments = unicode(self.commentsEdit.toPlainText())
ordertext = unicode(self.verseOrderEdit.text()) ordertext = unicode(self.verseOrderEdit.text())
order = [] order = []
for item in ordertext.split(): for item in ordertext.split():
verse_tag = VerseType.Tags[ verse_tag = VerseType.Tags[VerseType.from_translated_tag(item[0])]
VerseType.from_translated_tag(item[0])]
verse_num = item[1:].lower() verse_num = item[1:].lower()
order.append(u'%s%s' % (verse_tag, verse_num)) order.append(u'%s%s' % (verse_tag, verse_num))
self.song.verse_order = u' '.join(order) self.song.verse_order = u' '.join(order)
@ -756,7 +754,6 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog):
else: else:
self.song.theme_name = None self.song.theme_name = None
self.processLyrics() self.processLyrics()
self.processTitle()
self.song.authors = [] self.song.authors = []
for row in range(self.authorsListView.count()): for row in range(self.authorsListView.count()):
item = self.authorsListView.item(row) item = self.authorsListView.item(row)
@ -767,6 +764,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog):
item = self.topicsListView.item(row) item = self.topicsListView.item(row)
topicId = (item.data(QtCore.Qt.UserRole)).toInt()[0] topicId = (item.data(QtCore.Qt.UserRole)).toInt()[0]
self.song.topics.append(self.manager.get_object(Topic, topicId)) self.song.topics.append(self.manager.get_object(Topic, topicId))
clean_song(self.manager, self.song)
self.manager.save_object(self.song) self.manager.save_object(self.song)
if not preview: if not preview:
self.song = None self.song = None
@ -779,7 +777,6 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog):
log.debug(u'processLyrics') log.debug(u'processLyrics')
try: try:
sxml = SongXML() sxml = SongXML()
text = u''
multiple = [] multiple = []
for i in range(0, self.verseListWidget.rowCount()): for i in range(0, self.verseListWidget.rowCount()):
item = self.verseListWidget.item(i, 0) item = self.verseListWidget.item(i, 0)
@ -788,11 +785,8 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog):
verse_num = verseId[1:] verse_num = verseId[1:]
sxml.add_verse_to_lyrics(verse_tag, verse_num, sxml.add_verse_to_lyrics(verse_tag, verse_num,
unicode(item.text())) unicode(item.text()))
text = text + self.whitespace.sub(u' ', if verse_num > u'1' and verse_tag not in multiple:
unicode(self.verseListWidget.item(i, 0).text())) + u' '
if (verse_num > u'1') and (verse_tag not in multiple):
multiple.append(verse_tag) multiple.append(verse_tag)
self.song.search_lyrics = text.lower()
self.song.lyrics = unicode(sxml.extract_xml(), u'utf-8') self.song.lyrics = unicode(sxml.extract_xml(), u'utf-8')
for verse in multiple: for verse in multiple:
self.song.verse_order = re.sub(u'([' + verse.upper() + self.song.verse_order = re.sub(u'([' + verse.upper() +
@ -801,13 +795,3 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog):
except: except:
log.exception(u'Problem processing song Lyrics \n%s', log.exception(u'Problem processing song Lyrics \n%s',
sxml.dump_xml()) sxml.dump_xml())
def processTitle(self):
"""
Process the song title entered by the user to remove stray punctuation
characters.
"""
# This method must only be run after the self.song = Song() assignment.
log.debug(u'processTitle')
self.song.search_title = re.sub(r'[\'"`,;:(){}?]+', u'',
unicode(self.song.search_title)).lower().strip()

View File

@ -23,6 +23,7 @@
# with this program; if not, write to the Free Software Foundation, Inc., 59 # # with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
import re
from PyQt4 import QtGui from PyQt4 import QtGui
@ -244,9 +245,11 @@ def retrieve_windows_encoding(recommendation=None):
return None return None
return filter(lambda item: item[1] == choice[0], encodings)[0][0] return filter(lambda item: item[1] == choice[0], encodings)[0][0]
def add_author_unknown(manager, song): def clean_song(manager, song):
""" """
Add the default author *Author Unknown* to the song. Cleans the search title, rebuilds the search lyrics, adds a default author
if the song does not have one and other clean ups. This should always
called when a new song is added or changed.
``manager`` ``manager``
The song's manager. The song's manager.
@ -254,8 +257,25 @@ def add_author_unknown(manager, song):
``song`` ``song``
The song object. The song object.
""" """
song.title = song.title.strip() if song.title else u''
if song.alternate_title is None:
song.alternate_title = u''
song.alternate_title = song.alternate_title.strip()
whitespace = re.compile(r'\W+', re.UNICODE)
song.search_title = (whitespace.sub(u' ', song.title).strip() + u'@' +
whitespace.sub(u' ', song.alternate_title).strip()).strip().lower()
# Remove the old "language" attribute from lyrics tag (prior to 1.9.5). This
# is not very important, but this keeps the database clean. This can be
# removed when everybody has cleaned his songs.
song.lyrics = song.lyrics.replace(u'<lyrics language="en">', u'<lyrics>')
verses = SongXML().get_verses(song.lyrics)
lyrics = u' '.join([whitespace.sub(u' ', verse[1]) for verse in verses])
song.search_lyrics = lyrics.lower()
# The song does not have any author, add one.
if not song.authors:
name = SongStrings.AuthorUnknown name = SongStrings.AuthorUnknown
author = manager.get_object_filtered(Author, Author.display_name == name) author = manager.get_object_filtered(
Author, Author.display_name == name)
if author is None: if author is None:
author = Author.populate( author = Author.populate(
display_name=name, last_name=u'', first_name=u'') display_name=name, last_name=u'', first_name=u'')

View File

@ -94,7 +94,7 @@ import os
from lxml import etree, objectify from lxml import etree, objectify
from openlp.core.ui.wizard import WizardStrings from openlp.core.ui.wizard import WizardStrings
from openlp.plugins.songs.lib import add_author_unknown, VerseType from openlp.plugins.songs.lib import clean_song, VerseType
from openlp.plugins.songs.lib.songimport import SongImport from openlp.plugins.songs.lib.songimport import SongImport
from openlp.plugins.songs.lib.db import Author, Book, Song, Topic from openlp.plugins.songs.lib.db import Author, Book, Song, Topic
from openlp.plugins.songs.lib.xml import SongXML from openlp.plugins.songs.lib.xml import SongXML
@ -212,9 +212,13 @@ class FoilPresenter(object):
# No xml get out of here. # No xml get out of here.
if not xml: if not xml:
return None return None
song = Song()
if xml[:5] == u'<?xml': if xml[:5] == u'<?xml':
xml = xml[38:] xml = xml[38:]
song = Song()
# Values will be set when cleaning the song.
song.search_lyrics = u''
song.verse_order = u''
song.search_title = u''
# Because "text" seems to be an reserverd word, we have to recompile it. # Because "text" seems to be an reserverd word, we have to recompile it.
xml = re.compile(u'<text>').sub(u'<text_>', xml) xml = re.compile(u'<text>').sub(u'<text_>', xml)
xml = re.compile(u'</text>').sub(u'</text_>', xml) xml = re.compile(u'</text>').sub(u'</text_>', xml)
@ -229,6 +233,7 @@ class FoilPresenter(object):
self._process_authors(foilpresenterfolie, song) self._process_authors(foilpresenterfolie, song)
self._process_songbooks(foilpresenterfolie, song) self._process_songbooks(foilpresenterfolie, song)
self._process_topics(foilpresenterfolie, song) self._process_topics(foilpresenterfolie, song)
clean_song(self.manager, song)
self.manager.save_object(song) self.manager.save_object(song)
return song.id return song.id
@ -348,8 +353,6 @@ class FoilPresenter(object):
first_name = u' '.join(display_name.split(u' ')[:-1])) first_name = u' '.join(display_name.split(u' ')[:-1]))
self.manager.save_object(author) self.manager.save_object(author)
song.authors.append(author) song.authors.append(author)
if not song.authors:
add_author_unknown(self.manager, song)
def _process_cclinumber(self, foilpresenterfolie, song): def _process_cclinumber(self, foilpresenterfolie, song):
""" """
@ -407,7 +410,6 @@ class FoilPresenter(object):
The song object. The song object.
""" """
sxml = SongXML() sxml = SongXML()
search_text = u''
temp_verse_order = {} temp_verse_order = {}
temp_verse_order_backup = [] temp_verse_order_backup = []
temp_sortnr_backup = 1 temp_sortnr_backup = 1
@ -452,7 +454,6 @@ class FoilPresenter(object):
else: else:
verse_type = u'O' verse_type = u'O'
verse_number = re.compile(u'[a-zA-Z.+-_ ]*').sub(u'', verse_name) verse_number = re.compile(u'[a-zA-Z.+-_ ]*').sub(u'', verse_name)
#verse_part = re.compile(u'[0-9]*').sub(u'', verse_name[1:])
# Foilpresenter allows e. g. "C", but we need "C1". # Foilpresenter allows e. g. "C", but we need "C1".
if not verse_number: if not verse_number:
verse_number = unicode(versenumber[verse_type]) verse_number = unicode(versenumber[verse_type])
@ -470,8 +471,6 @@ class FoilPresenter(object):
temp_verse_order_backup.append(u''.join((verse_type[0], temp_verse_order_backup.append(u''.join((verse_type[0],
verse_number))) verse_number)))
sxml.add_verse_to_lyrics(verse_type, verse_number, text) sxml.add_verse_to_lyrics(verse_type, verse_number, text)
search_text = search_text + text
song.search_lyrics = search_text.lower()
song.lyrics = unicode(sxml.extract_xml(), u'utf-8') song.lyrics = unicode(sxml.extract_xml(), u'utf-8')
# Process verse order # Process verse order
verse_order = [] verse_order = []
@ -534,13 +533,9 @@ class FoilPresenter(object):
for titelstring in foilpresenterfolie.titel.titelstring: for titelstring in foilpresenterfolie.titel.titelstring:
if not song.title: if not song.title:
song.title = self._child(titelstring) song.title = self._child(titelstring)
song.search_title = unicode(song.title)
song.alternate_title = u'' song.alternate_title = u''
else: else:
song.alternate_title = self._child(titelstring) song.alternate_title = self._child(titelstring)
song.search_title += u'@' + song.alternate_title
song.search_title = re.sub(r'[\'"`,;:(){}?]+', u'',
unicode(song.search_title)).lower().strip()
def _process_topics(self, foilpresenterfolie, song): def _process_topics(self, foilpresenterfolie, song):
""" """
@ -565,10 +560,3 @@ class FoilPresenter(object):
song.topics.append(topic) song.topics.append(topic)
except AttributeError: except AttributeError:
pass pass
def _dump_xml(self, xml):
"""
Debugging aid to dump XML so that we can see what we have.
"""
return etree.tostring(xml, encoding=u'UTF-8',
xml_declaration=True, pretty_print=True)

View File

@ -266,9 +266,8 @@ class SongMediaItem(MediaManagerItem):
Receiver.send_message(u'songs_load_list') Receiver.send_message(u'songs_load_list')
def onExportClick(self): def onExportClick(self):
if not hasattr(self, u'export_wizard'): export_wizard = SongExportForm(self, self.parent)
self.export_wizard = SongExportForm(self, self.parent) export_wizard.exec_()
self.export_wizard.exec_()
def onNewClick(self): def onNewClick(self):
log.debug(u'onNewClick') log.debug(u'onNewClick')
@ -412,29 +411,32 @@ class SongMediaItem(MediaManagerItem):
def serviceLoad(self, item): def serviceLoad(self, item):
""" """
Triggered by a song being loaded by the service item Triggered by a song being loaded by the service manager.
""" """
log.debug(u'serviceLoad') log.debug(u'serviceLoad')
if self.plugin.status != PluginStatus.Active or not item.data_string: if self.plugin.status != PluginStatus.Active or not item.data_string:
return return
if item.data_string[u'title'].find(u'@') == -1:
# This file seems to be an old one (prior to 1.9.5), which means,
# that the search title (data_string[u'title']) is probably wrong.
# We add "@" to search title and hope that we do not add any
# duplicate. This should work for songs without alternate title.
search_results = self.parent.manager.get_all_objects(Song, search_results = self.parent.manager.get_all_objects(Song,
Song.search_title == re.compile(r'\W+', re.UNICODE).sub(u' ', Song.search_title == (re.compile(r'\W+', re.UNICODE).sub(u' ',
item.data_string[u'title'].split(u'@')[0].lower()).strip(), item.data_string[u'title'].strip()) + u'@').strip().lower(),
Song.search_title.asc())
else:
search_results = self.parent.manager.get_all_objects(Song,
Song.search_title == item.data_string[u'title'],
Song.search_title.asc()) Song.search_title.asc())
author_list = item.data_string[u'authors'].split(u', ') author_list = item.data_string[u'authors'].split(u', ')
# The service item always has an author (at least it has u'' as
# author). However, songs saved in the database do not have to have
# an author.
if u'' in author_list:
author_list.remove(u'')
editId = 0 editId = 0
add_song = True add_song = True
if search_results: if search_results:
for song in search_results: for song in search_results:
same_authors = True same_authors = True
# If the author counts are different, we do not have to do any # If the author counts are different, we do not have to do any
# further checking. This is also important when a song does not # further checking.
# have any author (because we can not loop over an empty list).
if len(song.authors) == len(author_list): if len(song.authors) == len(author_list):
for author in song.authors: for author in song.authors:
if author.display_name not in author_list: if author.display_name not in author_list:

View File

@ -36,7 +36,7 @@ from sqlalchemy.orm.exc import UnmappedClassError
from openlp.core.lib import translate from openlp.core.lib import translate
from openlp.core.lib.db import BaseModel from openlp.core.lib.db import BaseModel
from openlp.plugins.songs.lib import add_author_unknown from openlp.plugins.songs.lib import clean_song
from openlp.plugins.songs.lib.db import Author, Book, Song, Topic #, MediaFile from openlp.plugins.songs.lib.db import Author, Book, Song, Topic #, MediaFile
from songimport import SongImport from songimport import SongImport
@ -167,12 +167,11 @@ class OpenLPSongImport(SongImport):
old_titles = song.search_title.split(u'@') old_titles = song.search_title.split(u'@')
if len(old_titles) > 1: if len(old_titles) > 1:
new_song.alternate_title = old_titles[1] new_song.alternate_title = old_titles[1]
else: # Values will be set when cleaning the song.
new_song.alternate_title = u'' new_song.search_title = u''
new_song.search_title = song.search_title.strip() new_song.search_lyrics = u''
new_song.song_number = song.song_number new_song.song_number = song.song_number
new_song.lyrics = song.lyrics new_song.lyrics = song.lyrics
new_song.search_lyrics = song.search_lyrics
new_song.verse_order = song.verse_order new_song.verse_order = song.verse_order
new_song.copyright = song.copyright new_song.copyright = song.copyright
new_song.comments = song.comments new_song.comments = song.comments
@ -181,31 +180,26 @@ class OpenLPSongImport(SongImport):
for author in song.authors: for author in song.authors:
existing_author = self.manager.get_object_filtered( existing_author = self.manager.get_object_filtered(
Author, Author.display_name == author.display_name) Author, Author.display_name == author.display_name)
if existing_author: if existing_author is None:
new_song.authors.append(existing_author) existing_author = Author.populate(
else:
new_song.authors.append(Author.populate(
first_name=author.first_name, first_name=author.first_name,
last_name=author.last_name, last_name=author.last_name,
display_name=author.display_name)) display_name=author.display_name)
if not new_song.authors: new_song.authors.append(existing_author)
add_author_unknown(self.manager, new_song)
if song.book: if song.book:
existing_song_book = self.manager.get_object_filtered( existing_song_book = self.manager.get_object_filtered(
Book, Book.name == song.book.name) Book, Book.name == song.book.name)
if existing_song_book: if existing_song_book is None:
new_song.book = existing_song_book existing_song_book = Book.populate(name=song.book.name,
else:
new_song.book = Book.populate(name=song.book.name,
publisher=song.book.publisher) publisher=song.book.publisher)
new_song.book = existing_song_book
if song.topics: if song.topics:
for topic in song.topics: for topic in song.topics:
existing_topic = self.manager.get_object_filtered( existing_topic = self.manager.get_object_filtered(
Topic, Topic.name == topic.name) Topic, Topic.name == topic.name)
if existing_topic: if existing_topic is None:
existing_topic = Topic.populate(name=topic.name)
new_song.topics.append(existing_topic) new_song.topics.append(existing_topic)
else:
new_song.topics.append(Topic.populate(name=topic.name))
# if has_media_files: # if has_media_files:
# if song.media_files: # if song.media_files:
# for media_file in song.media_files: # for media_file in song.media_files:
@ -217,6 +211,7 @@ class OpenLPSongImport(SongImport):
# else: # else:
# new_song.media_files.append(MediaFile.populate( # new_song.media_files.append(MediaFile.populate(
# file_name=media_file.file_name)) # file_name=media_file.file_name))
clean_song(self.manager, new_song)
self.manager.save_object(new_song) self.manager.save_object(new_song)
song_count += 1 song_count += 1
if self.stop_import_flag: if self.stop_import_flag:

View File

@ -29,7 +29,7 @@ import re
from PyQt4 import QtCore from PyQt4 import QtCore
from openlp.core.lib import Receiver, translate from openlp.core.lib import Receiver, translate
from openlp.plugins.songs.lib import add_author_unknown, VerseType from openlp.plugins.songs.lib import clean_song, VerseType
from openlp.plugins.songs.lib.db import Song, Author, Topic, Book, MediaFile from openlp.plugins.songs.lib.db import Song, Author, Topic, Book, MediaFile
from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.ui import SongStrings
from openlp.plugins.songs.lib.xml import SongXML from openlp.plugins.songs.lib.xml import SongXML
@ -245,12 +245,6 @@ class SongImport(QtCore.QObject):
else: else:
return True return True
def remove_punctuation(self, text):
"""
Extracts alphanumeric words for searchable fields
"""
return re.sub(r'\W+', u' ', text, re.UNICODE)
def finish(self): def finish(self):
""" """
All fields have been set to this song. Write the song to disk. All fields have been set to this song. Write the song to disk.
@ -259,11 +253,11 @@ class SongImport(QtCore.QObject):
song = Song() song = Song()
song.title = self.title song.title = self.title
song.alternate_title = self.alternate_title song.alternate_title = self.alternate_title
song.search_title = self.remove_punctuation(self.title).lower() \ # Values will be set when cleaning the song.
+ '@' + self.remove_punctuation(self.alternate_title).lower() song.search_title = u''
song.search_title = song.search_title.strip()
song.song_number = self.song_number
song.search_lyrics = u'' song.search_lyrics = u''
song.verse_order = u''
song.song_number = self.song_number
verses_changed_to_other = {} verses_changed_to_other = {}
sxml = SongXML() sxml = SongXML()
other_count = 1 other_count = 1
@ -280,8 +274,6 @@ class SongImport(QtCore.QObject):
new_verse_def) new_verse_def)
verse_def = new_verse_def verse_def = new_verse_def
sxml.add_verse_to_lyrics(verse_tag, verse_def[1:], verse_text, lang) sxml.add_verse_to_lyrics(verse_tag, verse_def[1:], verse_text, lang)
song.search_lyrics += u' ' + self.remove_punctuation(verse_text)
song.search_lyrics = song.search_lyrics.lower()
song.lyrics = unicode(sxml.extract_xml(), u'utf-8') song.lyrics = unicode(sxml.extract_xml(), u'utf-8')
if not len(self.verse_order_list) and \ if not len(self.verse_order_list) and \
self.verse_order_list_generated_useful: self.verse_order_list_generated_useful:
@ -303,9 +295,6 @@ class SongImport(QtCore.QObject):
last_name=authortext.split(u' ')[-1], last_name=authortext.split(u' ')[-1],
first_name=u' '.join(authortext.split(u' ')[:-1])) first_name=u' '.join(authortext.split(u' ')[:-1]))
song.authors.append(author) song.authors.append(author)
# No author, add the default author.
if not song.authors:
add_author_unknown(self.manager, song)
for filename in self.media_files: for filename in self.media_files:
media_file = self.manager.get_object_filtered(MediaFile, media_file = self.manager.get_object_filtered(MediaFile,
MediaFile.file_name == filename) MediaFile.file_name == filename)
@ -326,6 +315,7 @@ class SongImport(QtCore.QObject):
if topic is None: if topic is None:
topic = Topic.populate(name=topictext) topic = Topic.populate(name=topictext)
song.topics.append(topic) song.topics.append(topic)
clean_song(self.manager, song)
self.manager.save_object(song) self.manager.save_object(song)
self.set_defaults() self.set_defaults()

View File

@ -154,7 +154,8 @@ class SongShowPlusImport(SongImport):
elif blockKey == COMMENTS: elif blockKey == COMMENTS:
self.comments = unicode(data, u'cp1252') self.comments = unicode(data, u'cp1252')
elif blockKey == VERSE_ORDER: elif blockKey == VERSE_ORDER:
verseTag = self.toOpenLPVerseTag(data) verseTag = self.toOpenLPVerseTag(data, True)
if verseTag:
self.sspVerseOrderList.append(unicode(verseTag, self.sspVerseOrderList.append(unicode(verseTag,
u'cp1252')) u'cp1252'))
elif blockKey == SONG_BOOK: elif blockKey == SONG_BOOK:
@ -174,7 +175,7 @@ class SongShowPlusImport(SongImport):
WizardStrings.ImportingType % file_name) WizardStrings.ImportingType % file_name)
return True return True
def toOpenLPVerseTag(self, verseName): def toOpenLPVerseTag(self, verseName, ignoreUnique=False):
if verseName.find(" ") != -1: if verseName.find(" ") != -1:
verseParts = verseName.split(" ") verseParts = verseName.split(" ")
verseType = verseParts[0] verseType = verseParts[0]
@ -195,6 +196,8 @@ class SongShowPlusImport(SongImport):
verseTag = "B" verseTag = "B"
else: else:
if not self.otherList.has_key(verseName): if not self.otherList.has_key(verseName):
if ignoreUnique:
return None
self.otherCount = self.otherCount + 1 self.otherCount = self.otherCount + 1
self.otherList[verseName] = str(self.otherCount) self.otherList[verseName] = str(self.otherCount)
verseTag = "O" verseTag = "O"

View File

@ -66,7 +66,7 @@ import re
from lxml import etree, objectify from lxml import etree, objectify
from openlp.plugins.songs.lib import add_author_unknown, VerseType from openlp.plugins.songs.lib import clean_song, VerseType
from openlp.plugins.songs.lib.db import Author, Book, Song, Topic from openlp.plugins.songs.lib.db import Author, Book, Song, Topic
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -236,10 +236,9 @@ class OpenLyrics(object):
datetime.datetime.now().strftime(u'%Y-%m-%dT%H:%M:%S')) datetime.datetime.now().strftime(u'%Y-%m-%dT%H:%M:%S'))
properties = etree.SubElement(song_xml, u'properties') properties = etree.SubElement(song_xml, u'properties')
titles = etree.SubElement(properties, u'titles') titles = etree.SubElement(properties, u'titles')
self._add_text_to_element(u'title', titles, song.title.strip()) self._add_text_to_element(u'title', titles, song.title)
if song.alternate_title: if song.alternate_title:
self._add_text_to_element( self._add_text_to_element(u'title', titles, song.alternate_title)
u'title', titles, song.alternate_title.strip())
if song.comments: if song.comments:
comments = etree.SubElement(properties, u'comments') comments = etree.SubElement(properties, u'comments')
self._add_text_to_element(u'comment', comments, song.comments) self._add_text_to_element(u'comment', comments, song.comments)
@ -303,6 +302,10 @@ class OpenLyrics(object):
else: else:
return None return None
song = Song() song = Song()
# Values will be set when cleaning the song.
song.search_lyrics = u''
song.verse_order = u''
song.search_title = u''
self._process_copyright(properties, song) self._process_copyright(properties, song)
self._process_cclinumber(properties, song) self._process_cclinumber(properties, song)
self._process_titles(properties, song) self._process_titles(properties, song)
@ -312,6 +315,7 @@ class OpenLyrics(object):
self._process_authors(properties, song) self._process_authors(properties, song)
self._process_songbooks(properties, song) self._process_songbooks(properties, song)
self._process_topics(properties, song) self._process_topics(properties, song)
clean_song(self.manager, song)
self.manager.save_object(song) self.manager.save_object(song)
return song.id return song.id
@ -382,8 +386,6 @@ class OpenLyrics(object):
last_name=display_name.split(u' ')[-1], last_name=display_name.split(u' ')[-1],
first_name=u' '.join(display_name.split(u' ')[:-1])) first_name=u' '.join(display_name.split(u' ')[:-1]))
song.authors.append(author) song.authors.append(author)
if not song.authors:
add_author_unknown(self.manager, song)
def _process_cclinumber(self, properties, song): def _process_cclinumber(self, properties, song):
""" """
@ -443,7 +445,6 @@ class OpenLyrics(object):
The song object. The song object.
""" """
sxml = SongXML() sxml = SongXML()
search_text = u''
for verse in lyrics.verse: for verse in lyrics.verse:
text = u'' text = u''
for lines in verse.lines: for lines in verse.lines:
@ -462,8 +463,6 @@ class OpenLyrics(object):
if self._get(verse, u'lang'): if self._get(verse, u'lang'):
lang = self._get(verse, u'lang') lang = self._get(verse, u'lang')
sxml.add_verse_to_lyrics(verse_type, verse_number, text, lang) sxml.add_verse_to_lyrics(verse_type, verse_number, text, lang)
search_text = search_text + text
song.search_lyrics = search_text.lower()
song.lyrics = unicode(sxml.extract_xml(), u'utf-8') song.lyrics = unicode(sxml.extract_xml(), u'utf-8')
# Process verse order # Process verse order
if hasattr(properties, u'verseOrder'): if hasattr(properties, u'verseOrder'):
@ -510,13 +509,9 @@ class OpenLyrics(object):
for title in properties.titles.title: for title in properties.titles.title:
if not song.title: if not song.title:
song.title = self._text(title) song.title = self._text(title)
song.search_title = unicode(song.title)
song.alternate_title = u'' song.alternate_title = u''
else: else:
song.alternate_title = self._text(title) song.alternate_title = self._text(title)
song.search_title += u'@' + song.alternate_title
song.search_title = re.sub(r'[\'"`,;:(){}?]+', u'',
unicode(song.search_title)).lower().strip()
def _process_topics(self, properties, song): def _process_topics(self, properties, song):
""" """

View File

@ -25,7 +25,6 @@
############################################################################### ###############################################################################
import logging import logging
import re
import os import os
from tempfile import gettempdir from tempfile import gettempdir
@ -35,8 +34,7 @@ from openlp.core.lib import Plugin, StringContent, build_icon, translate, \
Receiver Receiver
from openlp.core.lib.db import Manager from openlp.core.lib.db import Manager
from openlp.core.lib.ui import UiStrings from openlp.core.lib.ui import UiStrings
from openlp.plugins.songs.lib import add_author_unknown, SongMediaItem, \ from openlp.plugins.songs.lib import clean_song, SongMediaItem, SongsTab
SongsTab, SongXML
from openlp.plugins.songs.lib.db import init_schema, Song from openlp.plugins.songs.lib.db import init_schema, Song
from openlp.plugins.songs.lib.importer import SongFormat from openlp.plugins.songs.lib.importer import SongFormat
from openlp.plugins.songs.lib.olpimport import OpenLPSongImport from openlp.plugins.songs.lib.olpimport import OpenLPSongImport
@ -62,7 +60,6 @@ class SongsPlugin(Plugin):
self.manager = Manager(u'songs', init_schema) self.manager = Manager(u'songs', init_schema)
self.icon_path = u':/plugins/plugin_songs.png' self.icon_path = u':/plugins/plugin_songs.png'
self.icon = build_icon(self.icon_path) self.icon = build_icon(self.icon_path)
self.whitespace = re.compile(r'\W+', re.UNICODE)
def initialise(self): def initialise(self):
log.info(u'Songs Initialising') log.info(u'Songs Initialising')
@ -141,38 +138,18 @@ class SongsPlugin(Plugin):
Rebuild each song. Rebuild each song.
""" """
maxSongs = self.manager.get_object_count(Song) maxSongs = self.manager.get_object_count(Song)
if maxSongs == 0:
return
progressDialog = QtGui.QProgressDialog( progressDialog = QtGui.QProgressDialog(
translate('SongsPlugin', 'Reindexing songs...'), UiStrings.Cancel, translate('SongsPlugin', 'Reindexing songs...'), UiStrings.Cancel,
0, maxSongs + 1, self.formparent) 0, maxSongs, self.formparent)
progressDialog.setWindowModality(QtCore.Qt.WindowModal) progressDialog.setWindowModality(QtCore.Qt.WindowModal)
songs = self.manager.get_all_objects(Song) songs = self.manager.get_all_objects(Song)
counter = 0 for number, song in enumerate(songs):
for song in songs: clean_song(self.manager, song)
counter += 1 progressDialog.setValue(number + 1)
# The song does not have any author, add one.
if not song.authors:
add_author_unknown(self.manager, song)
if song.title is None:
song.title = u''
if song.alternate_title is None:
song.alternate_title = u''
song.search_title = self.whitespace.sub(u' ', song.title.lower() +
u' ' + song.alternate_title.lower()).strip()
# Remove the "language" attribute from lyrics tag. This is not very
# important, but this keeps the database clean. This can be removed
# when everybody has run the reindex tool once.
song.lyrics = song.lyrics.replace(
u'<lyrics language="en">', u'<lyrics>')
lyrics = u''
verses = SongXML().get_verses(song.lyrics)
for verse in verses:
lyrics = lyrics + self.whitespace.sub(u' ', verse[1]) + u' '
song.search_lyrics = lyrics.lower()
progressDialog.setValue(counter)
self.manager.save_objects(songs) self.manager.save_objects(songs)
progressDialog.setValue(counter + 1) self.mediaItem.onSearchTextButtonClick()
self.mediaItem.displayResultsSong(
self.manager.get_all_objects(Song, order_by_ref=Song.search_title))
def onSongImportItemClicked(self): def onSongImportItemClicked(self):
if self.mediaItem: if self.mediaItem:
@ -183,10 +160,9 @@ class SongsPlugin(Plugin):
self.mediaItem.onExportClick() self.mediaItem.onExportClick()
def about(self): def about(self):
about_text = translate('SongsPlugin', '<strong>Songs Plugin</strong>' return translate('SongsPlugin', '<strong>Songs Plugin</strong>'
'<br />The songs plugin provides the ability to display and ' '<br />The songs plugin provides the ability to display and '
'manage songs.') 'manage songs.')
return about_text
def usesTheme(self, theme): def usesTheme(self, theme):
""" """
@ -258,6 +234,7 @@ class SongsPlugin(Plugin):
for sfile in os.listdir(db_dir): for sfile in os.listdir(db_dir):
if sfile.startswith(u'songs_') and sfile.endswith(u'.sqlite'): if sfile.startswith(u'songs_') and sfile.endswith(u'.sqlite'):
song_dbs.append(os.path.join(db_dir, sfile)) song_dbs.append(os.path.join(db_dir, sfile))
self.onToolsReindexItemTriggered()
if len(song_dbs) == 0: if len(song_dbs) == 0:
return return
progress = QtGui.QProgressDialog(self.formparent) progress = QtGui.QProgressDialog(self.formparent)
@ -273,9 +250,7 @@ class SongsPlugin(Plugin):
importer = OpenLPSongImport(self.manager, filename=db) importer = OpenLPSongImport(self.manager, filename=db)
importer.do_import() importer.do_import()
progress.setValue(len(song_dbs)) progress.setValue(len(song_dbs))
self.mediaItem.displayResultsSong( self.mediaItem.onSearchTextButtonClick()
self.manager.get_all_objects(Song, order_by_ref=Song.search_title))
self.onToolsReindexItemTriggered()
def finalise(self): def finalise(self):
""" """