simple_pvr 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +1 -4
  5. data/Gemfile.lock +72 -61
  6. data/README.md +51 -33
  7. data/bin/pvr_server +14 -6
  8. data/changelog.txt +11 -0
  9. data/development_server +14 -6
  10. data/features/channel_overview.feature +1 -1
  11. data/features/recordings.feature +30 -0
  12. data/features/step_definitions/pvr_steps.rb +34 -0
  13. data/features/support/env.rb +1 -2
  14. data/features/support/paths.rb +2 -0
  15. data/lib/simple_pvr.rb +2 -0
  16. data/lib/simple_pvr/model/database_initializer.rb +8 -0
  17. data/lib/simple_pvr/model/programme.rb +12 -2
  18. data/lib/simple_pvr/model/programme_actor.rb +14 -0
  19. data/lib/simple_pvr/model/programme_category.rb +14 -0
  20. data/lib/simple_pvr/model/programme_director.rb +13 -0
  21. data/lib/simple_pvr/model/programme_presenter.rb +13 -0
  22. data/lib/simple_pvr/model/recording.rb +12 -0
  23. data/lib/simple_pvr/programme_icon_fetcher.rb +15 -0
  24. data/lib/simple_pvr/pvr_initializer.rb +4 -4
  25. data/lib/simple_pvr/recorder.rb +4 -3
  26. data/lib/simple_pvr/recording_manager.rb +44 -24
  27. data/lib/simple_pvr/recording_planner.rb +3 -3
  28. data/lib/simple_pvr/scheduler.rb +12 -4
  29. data/lib/simple_pvr/server/base_controller.rb +12 -13
  30. data/lib/simple_pvr/server/channels_controller.rb +5 -5
  31. data/lib/simple_pvr/server/programmes_controller.rb +2 -2
  32. data/lib/simple_pvr/server/schedules_controller.rb +4 -4
  33. data/lib/simple_pvr/server/secured_controller.rb +71 -0
  34. data/lib/simple_pvr/server/shows_controller.rb +13 -8
  35. data/lib/simple_pvr/server/status_controller.rb +1 -1
  36. data/lib/simple_pvr/server/upcoming_recordings_controller.rb +1 -1
  37. data/lib/simple_pvr/version.rb +1 -1
  38. data/lib/simple_pvr/xmltv_reader.rb +37 -4
  39. data/public/css/typeahead.js-bootstrap.css +83 -0
  40. data/public/index.html +45 -37
  41. data/public/js/angular/http-auth-interceptor.js +122 -0
  42. data/public/js/app.js +4 -37
  43. data/public/js/controllers.js +22 -14
  44. data/public/js/directives.js +102 -0
  45. data/public/js/filters.js +0 -31
  46. data/public/js/services.js +64 -0
  47. data/public/js/typeahead/typeahead.min.js +7 -0
  48. data/public/partials/about.html +15 -13
  49. data/public/partials/channels.html +41 -36
  50. data/public/partials/programme.html +20 -4
  51. data/public/partials/programmeListing.html +14 -13
  52. data/public/partials/schedule.html +91 -86
  53. data/public/partials/schedules.html +28 -16
  54. data/public/partials/search.html +10 -11
  55. data/public/partials/show.html +26 -9
  56. data/public/partials/shows.html +5 -6
  57. data/public/partials/status.html +1 -1
  58. data/public/templates/loginDialog.html +30 -0
  59. data/public/templates/logoutLink.html +1 -0
  60. data/public/templates/titleSearch.html +5 -3
  61. data/simple_pvr.gemspec +6 -4
  62. data/spec/resources/dummyImage.png +1 -0
  63. data/spec/resources/programmes-with-categories.xmltv +27 -0
  64. data/spec/resources/programmes-with-credits.xmltv +24 -0
  65. data/spec/resources/programmes-with-icons.xmltv +25 -0
  66. data/spec/resources/programmes-with-presenters.xmltv +19 -0
  67. data/spec/resources/{programs-without-icon.xmltv → programmes-without-icon.xmltv} +0 -0
  68. data/spec/resources/{programs.xmltv → programmes.xmltv} +0 -4
  69. data/spec/simple_pvr/ffmpeg_spec.rb +24 -22
  70. data/spec/simple_pvr/hdhomerun_spec.rb +69 -67
  71. data/spec/simple_pvr/model/channel_spec.rb +101 -101
  72. data/spec/simple_pvr/model/programme_spec.rb +104 -104
  73. data/spec/simple_pvr/model/schedule_spec.rb +74 -74
  74. data/spec/simple_pvr/programme_icon_fetcher_spec.rb +25 -0
  75. data/spec/simple_pvr/pvr_initializer_spec.rb +40 -38
  76. data/spec/simple_pvr/recorder_spec.rb +37 -26
  77. data/spec/simple_pvr/recording_manager_spec.rb +160 -133
  78. data/spec/simple_pvr/recording_planner_spec.rb +213 -211
  79. data/spec/simple_pvr/scheduler_spec.rb +189 -172
  80. data/spec/simple_pvr/server/secured_controller_spec.rb +118 -0
  81. data/spec/simple_pvr/xmltv_reader_spec.rb +89 -41
  82. data/test/karma.conf.js +7 -4
  83. data/test/unit/filtersSpec.js +0 -36
  84. metadata +79 -63
  85. data/public/css/bootstrap-responsive.min.css +0 -9
  86. data/public/css/bootstrap.min.css +0 -9
  87. data/public/img/glyphicons-halflings-white.png +0 -0
  88. data/public/img/glyphicons-halflings.png +0 -0
  89. data/public/js/angular/angular-resource.min.js +0 -10
  90. data/public/js/angular/angular.min.js +0 -162
  91. data/public/js/bootstrap/bootstrap.min.js +0 -6
  92. data/public/js/jquery/jquery.min.js +0 -5
  93. data/test/lib/angular/angular-mocks.js +0 -1768
@@ -24,4 +24,68 @@ factory('Recording', function($resource) {
24
24
  }).
25
25
  factory('Status', function($resource) {
26
26
  return $resource('/api/status');
27
+ }).
28
+ service('loginService', function($http, $cookieStore, authService) {
29
+ // Taken from http://wemadeyoulook.at/en/blog/implementing-basic-http-authentication-http-requests-angular/
30
+ function encodeBase64(input) {
31
+ var keyStr = 'ABCDEFGHIJKLMNOP' +
32
+ 'QRSTUVWXYZabcdef' +
33
+ 'ghijklmnopqrstuv' +
34
+ 'wxyz0123456789+/' +
35
+ '=';
36
+
37
+ var output = '';
38
+ var chr1, chr2, chr3 = '';
39
+ var enc1, enc2, enc3, enc4 = '';
40
+ var i = 0;
41
+
42
+ do {
43
+ chr1 = input.charCodeAt(i++);
44
+ chr2 = input.charCodeAt(i++);
45
+ chr3 = input.charCodeAt(i++);
46
+
47
+ enc1 = chr1 >> 2;
48
+ enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
49
+ enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
50
+ enc4 = chr3 & 63;
51
+
52
+ if (isNaN(chr2)) {
53
+ enc3 = enc4 = 64;
54
+ } else if (isNaN(chr3)) {
55
+ enc4 = 64;
56
+ }
57
+
58
+ output = output +
59
+ keyStr.charAt(enc1) +
60
+ keyStr.charAt(enc2) +
61
+ keyStr.charAt(enc3) +
62
+ keyStr.charAt(enc4);
63
+ chr1 = chr2 = chr3 = '';
64
+ enc1 = enc2 = enc3 = enc4 = '';
65
+ } while (i < input.length);
66
+
67
+ return output;
68
+ }
69
+
70
+ var basicCredentials = $cookieStore.get('basicCredentials');
71
+ if (basicCredentials) {
72
+ $http.defaults.headers.common['Authorization'] = 'Basic ' + basicCredentials;
73
+ }
74
+
75
+ return {
76
+ setUserNameAndPassword: function(userName, password) {
77
+ var encodedUserNameAndPassword = encodeBase64(userName + ':' + password);
78
+ $cookieStore.put('basicCredentials', encodedUserNameAndPassword);
79
+
80
+ $http.defaults.headers.common['Authorization'] = 'Basic ' + encodedUserNameAndPassword;
81
+ authService.loginConfirmed();
82
+ },
83
+ isLoggedIn: function() {
84
+ return $cookieStore.get('basicCredentials') !== undefined;
85
+ },
86
+ logOut: function() {
87
+ $cookieStore.remove('basicCredentials');
88
+ delete $http.defaults.headers.common['Authorization'];
89
+ }
90
+ }
27
91
  });
@@ -0,0 +1,7 @@
1
+ /*!
2
+ * typeahead.js 0.9.3
3
+ * https://github.com/twitter/typeahead
4
+ * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT
5
+ */
6
+
7
+ !function(a){var b="0.9.3",c={isMsie:function(){var a=/(msie) ([\w.]+)/i.exec(navigator.userAgent);return a?parseInt(a[2],10):!1},isBlankString:function(a){return!a||/^\s*$/.test(a)},escapeRegExChars:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isString:function(a){return"string"==typeof a},isNumber:function(a){return"number"==typeof a},isArray:a.isArray,isFunction:a.isFunction,isObject:a.isPlainObject,isUndefined:function(a){return"undefined"==typeof a},bind:a.proxy,bindAll:function(b){var c;for(var d in b)a.isFunction(c=b[d])&&(b[d]=a.proxy(c,b))},indexOf:function(a,b){for(var c=0;c<a.length;c++)if(a[c]===b)return c;return-1},each:a.each,map:a.map,filter:a.grep,every:function(b,c){var d=!0;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?void 0:!1}),!!d):d},some:function(b,c){var d=!1;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?!1:void 0}),!!d):d},mixin:a.extend,getUniqueId:function(){var a=0;return function(){return a++}}(),defer:function(a){setTimeout(a,0)},debounce:function(a,b,c){var d,e;return function(){var f,g,h=this,i=arguments;return f=function(){d=null,c||(e=a.apply(h,i))},g=c&&!d,clearTimeout(d),d=setTimeout(f,b),g&&(e=a.apply(h,i)),e}},throttle:function(a,b){var c,d,e,f,g,h;return g=0,h=function(){g=new Date,e=null,f=a.apply(c,d)},function(){var i=new Date,j=b-(i-g);return c=this,d=arguments,0>=j?(clearTimeout(e),e=null,g=i,f=a.apply(c,d)):e||(e=setTimeout(h,j)),f}},tokenizeQuery:function(b){return a.trim(b).toLowerCase().split(/[\s]+/)},tokenizeText:function(b){return a.trim(b).toLowerCase().split(/[\s\-_]+/)},getProtocol:function(){return location.protocol},noop:function(){}},d=function(){var a=/\s+/;return{on:function(b,c){var d;if(!c)return this;for(this._callbacks=this._callbacks||{},b=b.split(a);d=b.shift();)this._callbacks[d]=this._callbacks[d]||[],this._callbacks[d].push(c);return this},trigger:function(b,c){var d,e;if(!this._callbacks)return this;for(b=b.split(a);d=b.shift();)if(e=this._callbacks[d])for(var f=0;f<e.length;f+=1)e[f].call(this,{type:d,data:c});return this}}}(),e=function(){function b(b){b&&b.el||a.error("EventBus initialized without el"),this.$el=a(b.el)}var d="typeahead:";return c.mixin(b.prototype,{trigger:function(a){var b=[].slice.call(arguments,1);this.$el.trigger(d+a,b)}}),b}(),f=function(){function a(a){this.prefix=["__",a,"__"].join(""),this.ttlKey="__ttl__",this.keyMatcher=new RegExp("^"+this.prefix)}function b(){return(new Date).getTime()}function d(a){return JSON.stringify(c.isUndefined(a)?null:a)}function e(a){return JSON.parse(a)}var f,g;try{f=window.localStorage,f.setItem("~~~","!"),f.removeItem("~~~")}catch(h){f=null}return g=f&&window.JSON?{_prefix:function(a){return this.prefix+a},_ttlKey:function(a){return this._prefix(a)+this.ttlKey},get:function(a){return this.isExpired(a)&&this.remove(a),e(f.getItem(this._prefix(a)))},set:function(a,e,g){return c.isNumber(g)?f.setItem(this._ttlKey(a),d(b()+g)):f.removeItem(this._ttlKey(a)),f.setItem(this._prefix(a),d(e))},remove:function(a){return f.removeItem(this._ttlKey(a)),f.removeItem(this._prefix(a)),this},clear:function(){var a,b,c=[],d=f.length;for(a=0;d>a;a++)(b=f.key(a)).match(this.keyMatcher)&&c.push(b.replace(this.keyMatcher,""));for(a=c.length;a--;)this.remove(c[a]);return this},isExpired:function(a){var d=e(f.getItem(this._ttlKey(a)));return c.isNumber(d)&&b()>d?!0:!1}}:{get:c.noop,set:c.noop,remove:c.noop,clear:c.noop,isExpired:c.noop},c.mixin(a.prototype,g),a}(),g=function(){function a(a){c.bindAll(this),a=a||{},this.sizeLimit=a.sizeLimit||10,this.cache={},this.cachedKeysByAge=[]}return c.mixin(a.prototype,{get:function(a){return this.cache[a]},set:function(a,b){var c;this.cachedKeysByAge.length===this.sizeLimit&&(c=this.cachedKeysByAge.shift(),delete this.cache[c]),this.cache[a]=b,this.cachedKeysByAge.push(a)}}),a}(),h=function(){function b(a){c.bindAll(this),a=c.isString(a)?{url:a}:a,i=i||new g,h=c.isNumber(a.maxParallelRequests)?a.maxParallelRequests:h||6,this.url=a.url,this.wildcard=a.wildcard||"%QUERY",this.filter=a.filter,this.replace=a.replace,this.ajaxSettings={type:"get",cache:a.cache,timeout:a.timeout,dataType:a.dataType||"json",beforeSend:a.beforeSend},this._get=(/^throttle$/i.test(a.rateLimitFn)?c.throttle:c.debounce)(this._get,a.rateLimitWait||300)}function d(){j++}function e(){j--}function f(){return h>j}var h,i,j=0,k={};return c.mixin(b.prototype,{_get:function(a,b){function c(c){var e=d.filter?d.filter(c):c;b&&b(e),i.set(a,c)}var d=this;f()?this._sendRequest(a).done(c):this.onDeckRequestArgs=[].slice.call(arguments,0)},_sendRequest:function(b){function c(){e(),k[b]=null,f.onDeckRequestArgs&&(f._get.apply(f,f.onDeckRequestArgs),f.onDeckRequestArgs=null)}var f=this,g=k[b];return g||(d(),g=k[b]=a.ajax(b,this.ajaxSettings).always(c)),g},get:function(a,b){var d,e,f=this,g=encodeURIComponent(a||"");return b=b||c.noop,d=this.replace?this.replace(this.url,g):this.url.replace(this.wildcard,g),(e=i.get(d))?c.defer(function(){b(f.filter?f.filter(e):e)}):this._get(d,b),!!e}}),b}(),i=function(){function d(b){c.bindAll(this),c.isString(b.template)&&!b.engine&&a.error("no template engine specified"),b.local||b.prefetch||b.remote||a.error("one of local, prefetch, or remote is required"),this.name=b.name||c.getUniqueId(),this.limit=b.limit||5,this.minLength=b.minLength||1,this.header=b.header,this.footer=b.footer,this.valueKey=b.valueKey||"value",this.template=e(b.template,b.engine,this.valueKey),this.local=b.local,this.prefetch=b.prefetch,this.remote=b.remote,this.itemHash={},this.adjacencyList={},this.storage=b.name?new f(b.name):null}function e(a,b,d){var e,f;return c.isFunction(a)?e=a:c.isString(a)?(f=b.compile(a),e=c.bind(f.render,f)):e=function(a){return"<p>"+a[d]+"</p>"},e}var g={thumbprint:"thumbprint",protocol:"protocol",itemHash:"itemHash",adjacencyList:"adjacencyList"};return c.mixin(d.prototype,{_processLocalData:function(a){this._mergeProcessedData(this._processData(a))},_loadPrefetchData:function(d){function e(a){var b=d.filter?d.filter(a):a,e=m._processData(b),f=e.itemHash,h=e.adjacencyList;m.storage&&(m.storage.set(g.itemHash,f,d.ttl),m.storage.set(g.adjacencyList,h,d.ttl),m.storage.set(g.thumbprint,n,d.ttl),m.storage.set(g.protocol,c.getProtocol(),d.ttl)),m._mergeProcessedData(e)}var f,h,i,j,k,l,m=this,n=b+(d.thumbprint||"");return this.storage&&(f=this.storage.get(g.thumbprint),h=this.storage.get(g.protocol),i=this.storage.get(g.itemHash),j=this.storage.get(g.adjacencyList)),k=f!==n||h!==c.getProtocol(),d=c.isString(d)?{url:d}:d,d.ttl=c.isNumber(d.ttl)?d.ttl:864e5,i&&j&&!k?(this._mergeProcessedData({itemHash:i,adjacencyList:j}),l=a.Deferred().resolve()):l=a.getJSON(d.url).done(e),l},_transformDatum:function(a){var b=c.isString(a)?a:a[this.valueKey],d=a.tokens||c.tokenizeText(b),e={value:b,tokens:d};return c.isString(a)?(e.datum={},e.datum[this.valueKey]=a):e.datum=a,e.tokens=c.filter(e.tokens,function(a){return!c.isBlankString(a)}),e.tokens=c.map(e.tokens,function(a){return a.toLowerCase()}),e},_processData:function(a){var b=this,d={},e={};return c.each(a,function(a,f){var g=b._transformDatum(f),h=c.getUniqueId(g.value);d[h]=g,c.each(g.tokens,function(a,b){var d=b.charAt(0),f=e[d]||(e[d]=[h]);!~c.indexOf(f,h)&&f.push(h)})}),{itemHash:d,adjacencyList:e}},_mergeProcessedData:function(a){var b=this;c.mixin(this.itemHash,a.itemHash),c.each(a.adjacencyList,function(a,c){var d=b.adjacencyList[a];b.adjacencyList[a]=d?d.concat(c):c})},_getLocalSuggestions:function(a){var b,d=this,e=[],f=[],g=[];return c.each(a,function(a,b){var d=b.charAt(0);!~c.indexOf(e,d)&&e.push(d)}),c.each(e,function(a,c){var e=d.adjacencyList[c];return e?(f.push(e),(!b||e.length<b.length)&&(b=e),void 0):!1}),f.length<e.length?[]:(c.each(b,function(b,e){var h,i,j=d.itemHash[e];h=c.every(f,function(a){return~c.indexOf(a,e)}),i=h&&c.every(a,function(a){return c.some(j.tokens,function(b){return 0===b.indexOf(a)})}),i&&g.push(j)}),g)},initialize:function(){var b;return this.local&&this._processLocalData(this.local),this.transport=this.remote?new h(this.remote):null,b=this.prefetch?this._loadPrefetchData(this.prefetch):a.Deferred().resolve(),this.local=this.prefetch=this.remote=null,this.initialize=function(){return b},b},getSuggestions:function(a,b){function d(a){f=f.slice(0),c.each(a,function(a,b){var d,e=g._transformDatum(b);return d=c.some(f,function(a){return e.value===a.value}),!d&&f.push(e),f.length<g.limit}),b&&b(f)}var e,f,g=this,h=!1;a.length<this.minLength||(e=c.tokenizeQuery(a),f=this._getLocalSuggestions(e).slice(0,this.limit),f.length<this.limit&&this.transport&&(h=this.transport.get(a,d)),!h&&b&&b(f))}}),d}(),j=function(){function b(b){var d=this;c.bindAll(this),this.specialKeyCodeMap={9:"tab",27:"esc",37:"left",39:"right",13:"enter",38:"up",40:"down"},this.$hint=a(b.hint),this.$input=a(b.input).on("blur.tt",this._handleBlur).on("focus.tt",this._handleFocus).on("keydown.tt",this._handleSpecialKeyEvent),c.isMsie()?this.$input.on("keydown.tt keypress.tt cut.tt paste.tt",function(a){d.specialKeyCodeMap[a.which||a.keyCode]||c.defer(d._compareQueryToInputValue)}):this.$input.on("input.tt",this._compareQueryToInputValue),this.query=this.$input.val(),this.$overflowHelper=e(this.$input)}function e(b){return a("<span></span>").css({position:"absolute",left:"-9999px",visibility:"hidden",whiteSpace:"nowrap",fontFamily:b.css("font-family"),fontSize:b.css("font-size"),fontStyle:b.css("font-style"),fontVariant:b.css("font-variant"),fontWeight:b.css("font-weight"),wordSpacing:b.css("word-spacing"),letterSpacing:b.css("letter-spacing"),textIndent:b.css("text-indent"),textRendering:b.css("text-rendering"),textTransform:b.css("text-transform")}).insertAfter(b)}function f(a,b){return a=(a||"").replace(/^\s*/g,"").replace(/\s{2,}/g," "),b=(b||"").replace(/^\s*/g,"").replace(/\s{2,}/g," "),a===b}return c.mixin(b.prototype,d,{_handleFocus:function(){this.trigger("focused")},_handleBlur:function(){this.trigger("blured")},_handleSpecialKeyEvent:function(a){var b=this.specialKeyCodeMap[a.which||a.keyCode];b&&this.trigger(b+"Keyed",a)},_compareQueryToInputValue:function(){var a=this.getInputValue(),b=f(this.query,a),c=b?this.query.length!==a.length:!1;c?this.trigger("whitespaceChanged",{value:this.query}):b||this.trigger("queryChanged",{value:this.query=a})},destroy:function(){this.$hint.off(".tt"),this.$input.off(".tt"),this.$hint=this.$input=this.$overflowHelper=null},focus:function(){this.$input.focus()},blur:function(){this.$input.blur()},getQuery:function(){return this.query},setQuery:function(a){this.query=a},getInputValue:function(){return this.$input.val()},setInputValue:function(a,b){this.$input.val(a),!b&&this._compareQueryToInputValue()},getHintValue:function(){return this.$hint.val()},setHintValue:function(a){this.$hint.val(a)},getLanguageDirection:function(){return(this.$input.css("direction")||"ltr").toLowerCase()},isOverflow:function(){return this.$overflowHelper.text(this.getInputValue()),this.$overflowHelper.width()>this.$input.width()},isCursorAtEnd:function(){var a,b=this.$input.val().length,d=this.$input[0].selectionStart;return c.isNumber(d)?d===b:document.selection?(a=document.selection.createRange(),a.moveStart("character",-b),b===a.text.length):!0}}),b}(),k=function(){function b(b){c.bindAll(this),this.isOpen=!1,this.isEmpty=!0,this.isMouseOverDropdown=!1,this.$menu=a(b.menu).on("mouseenter.tt",this._handleMouseenter).on("mouseleave.tt",this._handleMouseleave).on("click.tt",".tt-suggestion",this._handleSelection).on("mouseover.tt",".tt-suggestion",this._handleMouseover)}function e(a){return a.data("suggestion")}var f={suggestionsList:'<span class="tt-suggestions"></span>'},g={suggestionsList:{display:"block"},suggestion:{whiteSpace:"nowrap",cursor:"pointer"},suggestionChild:{whiteSpace:"normal"}};return c.mixin(b.prototype,d,{_handleMouseenter:function(){this.isMouseOverDropdown=!0},_handleMouseleave:function(){this.isMouseOverDropdown=!1},_handleMouseover:function(b){var c=a(b.currentTarget);this._getSuggestions().removeClass("tt-is-under-cursor"),c.addClass("tt-is-under-cursor")},_handleSelection:function(b){var c=a(b.currentTarget);this.trigger("suggestionSelected",e(c))},_show:function(){this.$menu.css("display","block")},_hide:function(){this.$menu.hide()},_moveCursor:function(a){var b,c,d,f;if(this.isVisible()){if(b=this._getSuggestions(),c=b.filter(".tt-is-under-cursor"),c.removeClass("tt-is-under-cursor"),d=b.index(c)+a,d=(d+1)%(b.length+1)-1,-1===d)return this.trigger("cursorRemoved"),void 0;-1>d&&(d=b.length-1),f=b.eq(d).addClass("tt-is-under-cursor"),this._ensureVisibility(f),this.trigger("cursorMoved",e(f))}},_getSuggestions:function(){return this.$menu.find(".tt-suggestions > .tt-suggestion")},_ensureVisibility:function(a){var b=this.$menu.height()+parseInt(this.$menu.css("paddingTop"),10)+parseInt(this.$menu.css("paddingBottom"),10),c=this.$menu.scrollTop(),d=a.position().top,e=d+a.outerHeight(!0);0>d?this.$menu.scrollTop(c+d):e>b&&this.$menu.scrollTop(c+(e-b))},destroy:function(){this.$menu.off(".tt"),this.$menu=null},isVisible:function(){return this.isOpen&&!this.isEmpty},closeUnlessMouseIsOverDropdown:function(){this.isMouseOverDropdown||this.close()},close:function(){this.isOpen&&(this.isOpen=!1,this.isMouseOverDropdown=!1,this._hide(),this.$menu.find(".tt-suggestions > .tt-suggestion").removeClass("tt-is-under-cursor"),this.trigger("closed"))},open:function(){this.isOpen||(this.isOpen=!0,!this.isEmpty&&this._show(),this.trigger("opened"))},setLanguageDirection:function(a){var b={left:"0",right:"auto"},c={left:"auto",right:" 0"};"ltr"===a?this.$menu.css(b):this.$menu.css(c)},moveCursorUp:function(){this._moveCursor(-1)},moveCursorDown:function(){this._moveCursor(1)},getSuggestionUnderCursor:function(){var a=this._getSuggestions().filter(".tt-is-under-cursor").first();return a.length>0?e(a):null},getFirstSuggestion:function(){var a=this._getSuggestions().first();return a.length>0?e(a):null},renderSuggestions:function(b,d){var e,h,i,j,k,l="tt-dataset-"+b.name,m='<div class="tt-suggestion">%body</div>',n=this.$menu.find("."+l);0===n.length&&(h=a(f.suggestionsList).css(g.suggestionsList),n=a("<div></div>").addClass(l).append(b.header).append(h).append(b.footer).appendTo(this.$menu)),d.length>0?(this.isEmpty=!1,this.isOpen&&this._show(),i=document.createElement("div"),j=document.createDocumentFragment(),c.each(d,function(c,d){d.dataset=b.name,e=b.template(d.datum),i.innerHTML=m.replace("%body",e),k=a(i.firstChild).css(g.suggestion).data("suggestion",d),k.children().each(function(){a(this).css(g.suggestionChild)}),j.appendChild(k[0])}),n.show().find(".tt-suggestions").html(j)):this.clearSuggestions(b.name),this.trigger("suggestionsRendered")},clearSuggestions:function(a){var b=a?this.$menu.find(".tt-dataset-"+a):this.$menu.find('[class^="tt-dataset-"]'),c=b.find(".tt-suggestions");b.hide(),c.empty(),0===this._getSuggestions().length&&(this.isEmpty=!0,this._hide())}}),b}(),l=function(){function b(a){var b,d,f;c.bindAll(this),this.$node=e(a.input),this.datasets=a.datasets,this.dir=null,this.eventBus=a.eventBus,b=this.$node.find(".tt-dropdown-menu"),d=this.$node.find(".tt-query"),f=this.$node.find(".tt-hint"),this.dropdownView=new k({menu:b}).on("suggestionSelected",this._handleSelection).on("cursorMoved",this._clearHint).on("cursorMoved",this._setInputValueToSuggestionUnderCursor).on("cursorRemoved",this._setInputValueToQuery).on("cursorRemoved",this._updateHint).on("suggestionsRendered",this._updateHint).on("opened",this._updateHint).on("closed",this._clearHint).on("opened closed",this._propagateEvent),this.inputView=new j({input:d,hint:f}).on("focused",this._openDropdown).on("blured",this._closeDropdown).on("blured",this._setInputValueToQuery).on("enterKeyed tabKeyed",this._handleSelection).on("queryChanged",this._clearHint).on("queryChanged",this._clearSuggestions).on("queryChanged",this._getSuggestions).on("whitespaceChanged",this._updateHint).on("queryChanged whitespaceChanged",this._openDropdown).on("queryChanged whitespaceChanged",this._setLanguageDirection).on("escKeyed",this._closeDropdown).on("escKeyed",this._setInputValueToQuery).on("tabKeyed upKeyed downKeyed",this._managePreventDefault).on("upKeyed downKeyed",this._moveDropdownCursor).on("upKeyed downKeyed",this._openDropdown).on("tabKeyed leftKeyed rightKeyed",this._autocomplete)}function e(b){var c=a(g.wrapper),d=a(g.dropdown),e=a(b),f=a(g.hint);c=c.css(h.wrapper),d=d.css(h.dropdown),f.css(h.hint).css({backgroundAttachment:e.css("background-attachment"),backgroundClip:e.css("background-clip"),backgroundColor:e.css("background-color"),backgroundImage:e.css("background-image"),backgroundOrigin:e.css("background-origin"),backgroundPosition:e.css("background-position"),backgroundRepeat:e.css("background-repeat"),backgroundSize:e.css("background-size")}),e.data("ttAttrs",{dir:e.attr("dir"),autocomplete:e.attr("autocomplete"),spellcheck:e.attr("spellcheck"),style:e.attr("style")}),e.addClass("tt-query").attr({autocomplete:"off",spellcheck:!1}).css(h.query);try{!e.attr("dir")&&e.attr("dir","auto")}catch(i){}return e.wrap(c).parent().prepend(f).append(d)}function f(a){var b=a.find(".tt-query");c.each(b.data("ttAttrs"),function(a,d){c.isUndefined(d)?b.removeAttr(a):b.attr(a,d)}),b.detach().removeData("ttAttrs").removeClass("tt-query").insertAfter(a),a.remove()}var g={wrapper:'<span class="twitter-typeahead"></span>',hint:'<input class="tt-hint" type="text" autocomplete="off" spellcheck="off" disabled>',dropdown:'<span class="tt-dropdown-menu"></span>'},h={wrapper:{position:"relative",display:"inline-block"},hint:{position:"absolute",top:"0",left:"0",borderColor:"transparent",boxShadow:"none"},query:{position:"relative",verticalAlign:"top",backgroundColor:"transparent"},dropdown:{position:"absolute",top:"100%",left:"0",zIndex:"100",display:"none"}};return c.isMsie()&&c.mixin(h.query,{backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"}),c.isMsie()&&c.isMsie()<=7&&(c.mixin(h.wrapper,{display:"inline",zoom:"1"}),c.mixin(h.query,{marginTop:"-1px"})),c.mixin(b.prototype,d,{_managePreventDefault:function(a){var b,c,d=a.data,e=!1;switch(a.type){case"tabKeyed":b=this.inputView.getHintValue(),c=this.inputView.getInputValue(),e=b&&b!==c;break;case"upKeyed":case"downKeyed":e=!d.shiftKey&&!d.ctrlKey&&!d.metaKey}e&&d.preventDefault()},_setLanguageDirection:function(){var a=this.inputView.getLanguageDirection();a!==this.dir&&(this.dir=a,this.$node.css("direction",a),this.dropdownView.setLanguageDirection(a))},_updateHint:function(){var a,b,d,e,f,g=this.dropdownView.getFirstSuggestion(),h=g?g.value:null,i=this.dropdownView.isVisible(),j=this.inputView.isOverflow();h&&i&&!j&&(a=this.inputView.getInputValue(),b=a.replace(/\s{2,}/g," ").replace(/^\s+/g,""),d=c.escapeRegExChars(b),e=new RegExp("^(?:"+d+")(.*$)","i"),f=e.exec(h),this.inputView.setHintValue(a+(f?f[1]:"")))},_clearHint:function(){this.inputView.setHintValue("")},_clearSuggestions:function(){this.dropdownView.clearSuggestions()},_setInputValueToQuery:function(){this.inputView.setInputValue(this.inputView.getQuery())},_setInputValueToSuggestionUnderCursor:function(a){var b=a.data;this.inputView.setInputValue(b.value,!0)},_openDropdown:function(){this.dropdownView.open()},_closeDropdown:function(a){this.dropdownView["blured"===a.type?"closeUnlessMouseIsOverDropdown":"close"]()},_moveDropdownCursor:function(a){var b=a.data;b.shiftKey||b.ctrlKey||b.metaKey||this.dropdownView["upKeyed"===a.type?"moveCursorUp":"moveCursorDown"]()},_handleSelection:function(a){var b="suggestionSelected"===a.type,d=b?a.data:this.dropdownView.getSuggestionUnderCursor();d&&(this.inputView.setInputValue(d.value),b?this.inputView.focus():a.data.preventDefault(),b&&c.isMsie()?c.defer(this.dropdownView.close):this.dropdownView.close(),this.eventBus.trigger("selected",d.datum,d.dataset))},_getSuggestions:function(){var a=this,b=this.inputView.getQuery();c.isBlankString(b)||c.each(this.datasets,function(c,d){d.getSuggestions(b,function(c){b===a.inputView.getQuery()&&a.dropdownView.renderSuggestions(d,c)})})},_autocomplete:function(a){var b,c,d,e,f;("rightKeyed"!==a.type&&"leftKeyed"!==a.type||(b=this.inputView.isCursorAtEnd(),c="ltr"===this.inputView.getLanguageDirection()?"leftKeyed"===a.type:"rightKeyed"===a.type,b&&!c))&&(d=this.inputView.getQuery(),e=this.inputView.getHintValue(),""!==e&&d!==e&&(f=this.dropdownView.getFirstSuggestion(),this.inputView.setInputValue(f.value),this.eventBus.trigger("autocompleted",f.datum,f.dataset)))},_propagateEvent:function(a){this.eventBus.trigger(a.type)},destroy:function(){this.inputView.destroy(),this.dropdownView.destroy(),f(this.$node),this.$node=null},setQuery:function(a){this.inputView.setQuery(a),this.inputView.setInputValue(a),this._clearHint(),this._clearSuggestions(),this._getSuggestions()}}),b}();!function(){var b,d={},f="ttView";b={initialize:function(b){function g(){var b,d=a(this),g=new e({el:d});b=c.map(h,function(a){return a.initialize()}),d.data(f,new l({input:d,eventBus:g=new e({el:d}),datasets:h})),a.when.apply(a,b).always(function(){c.defer(function(){g.trigger("initialized")})})}var h;return b=c.isArray(b)?b:[b],0===b.length&&a.error("no datasets provided"),h=c.map(b,function(a){var b=d[a.name]?d[a.name]:new i(a);return a.name&&(d[a.name]=b),b}),this.each(g)},destroy:function(){function b(){var b=a(this),c=b.data(f);c&&(c.destroy(),b.removeData(f))}return this.each(b)},setQuery:function(b){function c(){var c=a(this).data(f);c&&c.setQuery(b)}return this.each(c)}},jQuery.fn.typeahead=function(a){return b[a]?b[a].apply(this,[].slice.call(arguments,1)):b.initialize.apply(this,arguments)}}()}(window.jQuery);
@@ -1,16 +1,18 @@
1
- <div class="hero-unit">
1
+ <div class="jumbotron">
2
2
  <h1>SimplePVR</h1>
3
3
  <p>A really, really simple PVR (Personal Video Recorder) system which only supports the HDHomeRun network tuners. It's written in Ruby and is highly hackable. If you don't want to hack it, but just want a solid PVR system, no worries: It's dead-simple to use.</p>
4
- <p><a href="https://github.com/olefriis/simplepvr" class="btn btn-primary btn-large">Learn more &raquo;</a></p>
4
+ <p><a href="https://github.com/olefriis/simplepvr-backend-ruby/wiki" class="btn btn-primary btn-large">Learn more &raquo;</a></p>
5
5
  </div>
6
- <h2>Thanks!</h2>
7
- <p>SimplePVR is built on top of a number of really great, free products. A huge "thank you!" goes to all the people developing and
8
- supporting them. Amongst others, we're talking
9
- <ul>
10
- <li><a href="http://jquery.com/">JQuery</a> - takes the pain out of web development</li>
11
- <li><a href="http://angularjs.org/">AngularJS</a> - makes web development actually fun!</li>
12
- <li><a href="http://twitter.github.io/bootstrap/">Twitter Bootstrap</a> - oooh, the beauty!</li>
13
- <li><a href="http://glyphicons.com/">Glyphicons</a> - crisp, delightful icons</li>
14
- <li><a href="http://www.ruby-lang.org/en/">Ruby</a> - my favorite programming language and community</li>
15
- </ul>
16
- </p>
6
+ <h2>Thanks!</h2>
7
+ <p>SimplePVR is built on top of a number of really great, free products. A huge "thank you!" goes to all the people developing and
8
+ supporting them!
9
+ <ul>
10
+ <li><a href="http://jquery.com/">JQuery</a> (<a href="https://jquery.org/license/">MIT license</a>)</li>
11
+ <li><a href="http://angularjs.org/">AngularJS</a> (<a href="https://github.com/angular/angular.js/blob/master/LICENSE">MIT license</a>)</li>
12
+ <li><a href="http://getbootstrap.com/">Twitter Bootstrap</a> (<a href="https://github.com/twbs/bootstrap/blob/master/LICENSE">MIT license</a>)</li>
13
+ <li><a href="http://glyphicons.com/">Glyphicons</a> (<a href="https://github.com/twbs/bootstrap/blob/master/LICENSE">MIT license</a>, as <a href="http://glyphicons.com/license/">it's used with Twitter Bootstrap</a>)</li>
14
+ <li>Slightly modified version of <a href="https://github.com/witoldsz/angular-http-auth">angular-http-auth</a> (<a href="https://github.com/witoldsz/angular-http-auth/blob/master/LICENSE">MIT license</a>)</li>
15
+ <li><a href="http://wemadeyoulook.at/en/blog/implementing-basic-http-authentication-http-requests-angular/">JavaScript Base64 encoder</a> (no license)</li>
16
+ </ul>
17
+ <p>SimplePVR is itself licensed under the <a href="http://www.wtfpl.net/">Do What the Fuck You Want to Public License</a>.
18
+ </p>
@@ -1,41 +1,46 @@
1
1
  <div class="row">
2
- <div class="span12">
2
+ <div class="col-md-12">
3
3
  <form class="form-inline">
4
- <input id="channel_filter" type="text" placeholder="Filter..." ng-model="channelFilter">
5
- <label class="checkbox">
6
- <input type="checkbox" ng-model="showHiddenChannels">Show hidden channels</input>
7
- </label>
4
+ <div class="form-group">
5
+ <input id="channel_filter" type="text" class="form-control" placeholder="Filter..." ng-model="channelFilter">
6
+ </div>
7
+ <div class="form-group">
8
+ <div class="checkbox">
9
+ <label>
10
+ <input type="checkbox" ng-model="showHiddenChannels"> Show hidden channels</input>
11
+ </label>
12
+ </div>
13
+ </div>
8
14
  </form>
9
15
  </div>
10
16
  </div>
11
- <div class="row" ng-repeat="channelChunk in channels | orderBy:name | filter:shouldShowChannel | filter:channelFilter | chunk:3">
12
- <div ng-repeat="channel in channelChunk" class="span4">
13
- <table class="table table-condensed">
14
- <thead>
15
- <tr>
16
- <th colspan="2">
17
- <img ng-show="channel.icon_url" class="channel-icon" ng-src="{{channel.icon_url}}" />
18
- <span class="channel-name">
19
- {{channel.name}}
20
- </span>
21
- <a ng-href="channels" ng-hide="channel.hidden" class="icon-remove pull-right" ng-click="hideChannel(channel)" title="Hide channel" ></a>
22
- <a ng-href="channels" ng-show="channel.hidden" class="icon-ok pull-right" ng-click="showChannel(channel)" title="Show channel"></a>
23
- </th>
24
- </tr>
25
- </thead>
26
- <tbody>
27
- <tr ng-show="channel.current_programme" ng-class="classForProgrammeLine(channel.current_programme)">
28
- <td class="span1">{{channel.current_programme.start_time | date:'HH:mm'}}</td>
29
- <td><a ng-href="/programmes/{{channel.current_programme.id}}">{{channel.current_programme.title}}</td></tr>
30
- <tr ng-repeat="upcomingProgramme in channel.upcoming_programmes" ng-class="classForProgrammeLine(upcomingProgramme)">
31
- <td class="span1">{{upcomingProgramme.start_time | date:'HH:mm'}}</td>
32
- <td><a ng-href="/programmes/{{upcomingProgramme.id}}">{{upcomingProgramme.title}}</a></td></tr>
33
- <tr>
34
- <td colspan="2">
35
- <a ng-href="/channels/{{channel.id}}/programmeListings/today" class="pull-right">...</a>
36
- </td>
37
- </tr>
38
- </tbody>
39
- </table>
40
- </div>
41
- </div>
17
+ <div class="col-md-4" ng-repeat-start="channel in channels | orderBy:name | filter:shouldShowChannel | filter:channelFilter">
18
+ <table class="table table-condensed">
19
+ <thead>
20
+ <tr>
21
+ <th colspan="2">
22
+ <img ng-show="channel.icon_url" class="channel-icon" ng-src="{{channel.icon_url}}" />
23
+ <span class="channel-name">
24
+ {{channel.name}}
25
+ </span>
26
+ <a ng-hide="channel.hidden" class="glyphicon glyphicon-remove pull-right" style="color:black" ng-click="hideChannel(channel)" title="Hide channel" ></a>
27
+ <a ng-href="channels" ng-show="channel.hidden" class="glyphicon glyphicon-ok pull-right" style="color:black" ng-click="showChannel(channel)" title="Show channel"></a>
28
+ </th>
29
+ </tr>
30
+ </thead>
31
+ <tbody>
32
+ <tr ng-show="channel.current_programme" ng-class="classForProgrammeLine(channel.current_programme)">
33
+ <td class="col-md-1">{{channel.current_programme.start_time | date:'HH:mm'}}</td>
34
+ <td><a ng-href="/programmes/{{channel.current_programme.id}}">{{channel.current_programme.title}}</td></tr>
35
+ <tr ng-repeat="upcomingProgramme in channel.upcoming_programmes" ng-class="classForProgrammeLine(upcomingProgramme)">
36
+ <td class="col-md-1">{{upcomingProgramme.start_time | date:'HH:mm'}}</td>
37
+ <td><a ng-href="/programmes/{{upcomingProgramme.id}}">{{upcomingProgramme.title}}</a></td></tr>
38
+ <tr>
39
+ <td colspan="2">
40
+ <a ng-href="/channels/{{channel.id}}/programmeListings/today" class="pull-right">...</a>
41
+ </td>
42
+ </tr>
43
+ </tbody>
44
+ </table>
45
+ </div>
46
+ <div class="clearfix" ng-show="$index % 3 == 2" ng-repeat-end></div>
@@ -1,9 +1,25 @@
1
- <div class="span6">
2
- <div class="row" ng-show="programme.id">
3
- <h1>{{programme.title}}</h1>
4
- <p>{{programme.start_time | date:'fullDate'}} at {{programme.start_time | date:'HH:mm'}} on channel {{programme.channel.name}}.</p>
1
+ <div class="col-md-12">
2
+ <div class="row media" ng-show="programme.id">
3
+ <img class="media-object pull-left" ng-if="programme.icon_url" ng-src="{{programme.icon_url}}">
4
+ <h1 class="media-heading">{{programme.title}}</h1>
5
5
  <p>{{programme.subtitle}}</p>
6
+ <p>{{programme.start_time | date:'fullDate'}} at {{programme.start_time | date:'HH:mm'}} on channel {{programme.channel.name}}.</p>
6
7
  <p>{{programme.description}}</p>
8
+ <p ng-repeat="director in programme.directors">
9
+ <strong>Director:</strong> {{director}}
10
+ </p>
11
+ <p ng-repeat="presenter in programme.presenters">
12
+ <strong>Presenter:</strong> {{presenter}}
13
+ </p>
14
+ <p ng-repeat="actor in programme.actors">
15
+ <strong ng-show="actor.role_name">{{actor.role_name}}:</strong> {{actor.actor_name}}
16
+ </p>
17
+ <p ng-show="programme.categories">
18
+ <strong>Categories:</strong>
19
+ <span ng-repeat="category in programme.categories">
20
+ {{category}}<span ng-hide="$last">, </span>
21
+ </span>
22
+ </p>
7
23
 
8
24
  <div ng-show="programme.episode_num">Episode {{programme.episode_num | formatEpisode}}</div>
9
25
  <div ng-show="programme.is_scheduled">
@@ -1,18 +1,19 @@
1
- <p>
1
+ <form class="form-inline">
2
2
  <a ng-show="programmeListing.previous_date" ng-href="/channels/{{programmeListing.channel.id}}/programmeListings/{{programmeListing.previous_date}}">&lt;&lt;</a>
3
3
  <a ng-show="programmeListing.next_date" ng-href="/channels/{{programmeListing.channel.id}}/programmeListings/{{programmeListing.next_date}}">&gt;&gt;</a>
4
- <input type="text" placeholder="Filter..." ng-model="programmeFilter">
4
+ <div class="form-group">
5
+ <input type="text" class="form-control" placeholder="Filter..." ng-model="programmeFilter">
6
+ </div>
5
7
  <img class="channel-icon" src="{{programmeListing.channel.icon_url}}" />
6
8
  <span class="channel-name">{{programmeListing.channel.name}}</span>
7
- </p>
8
- <div class="row" ng-repeat="dayChunk in programmeListing.days | chunk:3">
9
- <div class="span4" ng-repeat="day in dayChunk">
10
- <table class="table table-condensed table-striped">
11
- <th colspan="2">{{day.date | date:'fullDate'}}</th>
12
- <tr ng-repeat="programme in day.programmes | filter:programmeFilter" class="{{classForProgrammeLine(programme)}}">
13
- <td>{{programme.start_time | date:'HH:mm'}}</td>
14
- <td><a ng-href="/programmes/{{programme.id}}">{{programme.title}}</td>
15
- </tr>
16
- </table>
17
- </div>
9
+ </form>
10
+ <div class="col-lg-4" ng-repeat-start="day in programmeListing.days">
11
+ <table class="table table-condensed table-bordered">
12
+ <th colspan="2">{{day.date | date:'fullDate'}}</th>
13
+ <tr ng-repeat="programme in day.programmes | filter:programmeFilter" class="{{classForProgrammeLine(programme)}}">
14
+ <td class="col-md-1">{{programme.start_time | date:'HH:mm'}}</td>
15
+ <td><a ng-href="/programmes/{{programme.id}}">{{programme.title}}</td>
16
+ </tr>
17
+ </table>
18
18
  </div>
19
+ <div class="clearfix" ng-show="$index % 3 == 2" ng-repeat-end></div>
@@ -1,119 +1,124 @@
1
1
  <div class="row" ng-show="schedule">
2
- <form class="form-horizontal" name="scheduleForm">
3
- <div class="control-group">
4
- <label class="control-label" for="showName">Name of show</label>
5
- <div class="controls">
6
- <input type="text" ng-model="schedule.title" id="showName" placeholder="Show name">
2
+ <form class="form-horizontal" name="scheduleForm" role="form">
3
+ <div class="form-group">
4
+ <label class="col-lg-2 control-label" for="showName">Name of show</label>
5
+ <div class="col-lg-4">
6
+ <input type="text" class="form-control" ng-model="schedule.title" id="showName" placeholder="Show name">
7
7
  </div>
8
8
  </div>
9
- <div class="control-group">
10
- <label class="control-label" for="channel">On channel</label>
11
- <div class="controls">
12
- <select id="channel" ng-model="channel" ng-options="c.name for c in channels">
9
+ <div class="form-group">
10
+ <label class="col-lg-2 control-label" for="channel">On channel</label>
11
+ <div class="col-lg-4">
12
+ <select class="form-control" id="channel" ng-model="channel" ng-options="c.name for c in channels">
13
13
  <option value="">-- Any channel --</option>
14
14
  </select>
15
15
  </div>
16
16
  </div>
17
17
 
18
- <div class="control-group" ng-class="{error: scheduleForm.startEarly.$invalid}">
19
- <label class="control-label" for="startEarly">Start early</label>
20
- <div class="controls">
21
- <div class="input-append">
22
- <input type="text" class="input-mini" style="text-align: right" ng-model="schedule.custom_start_early_minutes" ng-pattern="/^[0-9]*$/" name="startEarly" id="startEarly" placeholder="2">
23
- <span class="add-on">minutes</span>
24
- </div>
18
+ <div class="form-group" ng-class="{error: scheduleForm.startEarly.$invalid}">
19
+ <label class="col-lg-2 control-label" for="startEarly">Start early</label>
20
+ <div class="col-lg-2 input-group">
21
+ <input type="text" class="form-control" style="text-align: right" ng-model="schedule.custom_start_early_minutes" ng-pattern="/^[0-9]*$/" name="startEarly" id="startEarly" placeholder="2">
22
+ <span class="input-group-addon">minutes</span>
25
23
  </div>
26
24
  </div>
27
- <div class="control-group" ng-class="{error: scheduleForm.endLate.$invalid}">
28
- <label class="control-label" for="endLate">End late</label>
29
- <div class="controls">
30
- <div class="input-append">
31
- <input type="text" class="input-mini" style="text-align: right" ng-model="schedule.custom_end_late_minutes" ng-pattern="/^[0-9]*$/" name="endLate" id="endLate" placeholder="5">
32
- <span class="add-on">minutes</span>
33
- </div>
25
+ <div class="form-group" ng-class="{error: scheduleForm.endLate.$invalid}">
26
+ <label class="col-lg-2 control-label" for="endLate">End late</label>
27
+ <div class="col-lg-2 input-group">
28
+ <input type="text" class="form-control" style="text-align: right" ng-model="schedule.custom_end_late_minutes" ng-pattern="/^[0-9]*$/" name="endLate" id="endLate" placeholder="5">
29
+ <span class="input-group-addon">minutes</span>
34
30
  </div>
35
31
  </div>
36
32
 
37
- <div class="control-group">
38
- <div class="controls">
39
- <label class="checkbox" for="filterByTimeOfDay">
40
- <input type="checkbox" id="filterByTimeOfDay" ng-model="schedule.filter_by_time_of_day">Filter by time of day
41
- </label>
33
+ <div class="form-group">
34
+ <div class="col-lg-offset-2 col-lg-10">
35
+ <div class="checkbox">
36
+ <label>
37
+ <input type="checkbox" id="filterByTimeOfDay" ng-model="schedule.filter_by_time_of_day">Filter by time of day
38
+ </label>
39
+ </div>
42
40
  </div>
43
41
  </div>
44
- <div class="control-group" ng-show="schedule.filter_by_time_of_day" ng-class="{error: scheduleForm.startTime.$invalid}">
45
- <label class="control-label" for="startTime">From</label>
46
- <div class="controls">
47
- <input type="text" class="input-mini" style="text-align: right" ng-model="schedule.from_time_of_day" ng-pattern="/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/" name="startTime" id="startTime" placeholder="00:00">
42
+ <div class="col-lg-offset-2 col-lg-12" ng-show="schedule.filter_by_time_of_day">
43
+ <div class="form-group" ng-class="{'has-error': scheduleForm.startTime.$invalid}">
44
+ <label class="col-lg-1 control-label" for="startTime">From</label>
45
+ <div class="col-lg-1">
46
+ <input type="text" class="form-control col-lg-2" style="text-align: right" ng-model="schedule.from_time_of_day" ng-pattern="/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/" name="startTime" id="startTime" placeholder="00:00">
47
+ </div>
48
48
  </div>
49
- </div>
50
- <div class="control-group" ng-show="schedule.filter_by_time_of_day">
51
- <label class="control-label" for="endTime">To</label>
52
- <div class="controls">
53
- <input type="text" class="input-mini" style="text-align: right" ng-model="schedule.to_time_of_day" ng-pattern="/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/" id="endTime" placeholder="00:00">
49
+ <div class="form-group" ng-class="{'has-error': scheduleForm.endTime.$invalid}">
50
+ <label class="col-lg-1 control-label" for="endTime">To</label>
51
+ <div class="col-lg-1">
52
+ <input type="text" class="form-control col-lg-2" style="text-align: right" ng-model="schedule.to_time_of_day" ng-pattern="/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/" id="endTime" placeholder="00:00">
53
+ </div>
54
54
  </div>
55
55
  </div>
56
56
 
57
- <div class="control-group">
58
- <div class="controls">
59
- <label class="checkbox" for="filterByWeekday">
60
- <input type="checkbox" id="filterByWeekday" ng-model="schedule.filter_by_weekday">Filter by weekday
61
- </label>
57
+ <div class="form-group">
58
+ <div class="col-lg-offset-2 col-lg-10">
59
+ <div class="checkbox">
60
+ <label>
61
+ <input type="checkbox" id="filterByWeekday" ng-model="schedule.filter_by_weekday">Filter by weekday
62
+ </label>
63
+ </div>
62
64
  </div>
63
65
  </div>
64
- <div class="control-group" ng-show="schedule.filter_by_weekday">
65
- <div class="controls">
66
- <label class="checkbox" for="recordMondays">
67
- <input type="checkbox" id="recordMondays" ng-model="schedule.monday">Record Mondays
68
- </label>
66
+ <div class="col-lg-offset-3 col-lg-9" ng-show="schedule.filter_by_weekday">
67
+ <div class="form-group">
68
+ <div class="checkbox">
69
+ <label>
70
+ <input type="checkbox" id="recordMondays" ng-model="schedule.monday">Record Mondays
71
+ </label>
72
+ </div>
69
73
  </div>
70
- </div>
71
- <div class="control-group" ng-show="schedule.filter_by_weekday">
72
- <div class="controls">
73
- <label class="checkbox" for="recordTuesdays">
74
- <input type="checkbox" id="recordTuesdays" ng-model="schedule.tuesday">Record Tuesdays
75
- </label>
74
+ <div class="form-group" ng-show="schedule.filter_by_weekday">
75
+ <div class="checkbox">
76
+ <label>
77
+ <input type="checkbox" id="recordTuesdays" ng-model="schedule.tuesday">Record Tuesdays
78
+ </label>
79
+ </div>
76
80
  </div>
77
- </div>
78
- <div class="control-group" ng-show="schedule.filter_by_weekday">
79
- <div class="controls">
80
- <label class="checkbox" for="recordWednesdays">
81
- <input type="checkbox" id="recordWednesdays" ng-model="schedule.wednesday">Record Wednesdays
82
- </label>
81
+ <div class="form-group" ng-show="schedule.filter_by_weekday">
82
+ <div class="checkbox">
83
+ <label>
84
+ <input type="checkbox" id="recordWednesdays" ng-model="schedule.wednesday">Record Wednesdays
85
+ </label>
86
+ </div>
83
87
  </div>
84
- </div>
85
- <div class="control-group" ng-show="schedule.filter_by_weekday">
86
- <div class="controls">
87
- <label class="checkbox" for="recordThursdays">
88
- <input type="checkbox" id="recordThursdays" ng-model="schedule.thursday">Record Thursdays
89
- </label>
88
+ <div class="form-group" ng-show="schedule.filter_by_weekday">
89
+ <div class="checkbox">
90
+ <label>
91
+ <input type="checkbox" id="recordThursdays" ng-model="schedule.thursday">Record Thursdays
92
+ </label>
93
+ </div>
90
94
  </div>
91
- </div>
92
- <div class="control-group" ng-show="schedule.filter_by_weekday">
93
- <div class="controls">
94
- <label class="checkbox" for="recordFridays">
95
- <input type="checkbox" id="recordFridays" ng-model="schedule.friday">Record Fridays
96
- </label>
95
+ <div class="form-group" ng-show="schedule.filter_by_weekday">
96
+ <div class="checkbox">
97
+ <label>
98
+ <input type="checkbox" id="recordFridays" ng-model="schedule.friday">Record Fridays
99
+ </label>
100
+ </div>
97
101
  </div>
98
- </div>
99
- <div class="control-group" ng-show="schedule.filter_by_weekday">
100
- <div class="controls">
101
- <label class="checkbox" for="recordSaturdays">
102
- <input type="checkbox" id="recordSaturdays" ng-model="schedule.saturday">Record Saturdays
103
- </label>
102
+ <div class="form-group" ng-show="schedule.filter_by_weekday">
103
+ <div class="checkbox">
104
+ <label>
105
+ <input type="checkbox" id="recordSaturdays" ng-model="schedule.saturday">Record Saturdays
106
+ </label>
107
+ </div>
104
108
  </div>
105
- </div>
106
- <div class="control-group" ng-show="schedule.filter_by_weekday">
107
- <div class="controls">
108
- <label class="checkbox" for="recordSundays">
109
- <input type="checkbox" id="recordSundays" ng-model="schedule.sunday">Record Sundays
110
- </label>
109
+ <div class="form-group" ng-show="schedule.filter_by_weekday">
110
+ <div class="checkbox">
111
+ <label>
112
+ <input type="checkbox" id="recordSundays" ng-model="schedule.sunday">Record Sundays
113
+ </label>
114
+ </div>
111
115
  </div>
112
116
  </div>
113
- <div class="control-group">
114
- <div class="controls">
117
+
118
+ <div class="form-group">
119
+ <div class="col-lg-offset-2 col-lg-10">
115
120
  <button type="submit" class="btn btn-primary" ng-class="{disabled: scheduleForm.$invalid}" ng-click="update()" ng-disabled="scheduleForm.$invalid">Update</button>
116
- <a ng-href="/schedules" class="btn">Cancel</a>
121
+ <a ng-href="/schedules" class="btn btn-default">Cancel</a>
117
122
  </div>
118
123
  </div>
119
124
  </form>