quartz_flow 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. data/etc/quartz.rb +1 -1
  2. data/lib/quartz_flow/home.rb +33 -0
  3. data/lib/quartz_flow/plugin.rb +69 -0
  4. data/lib/quartz_flow/server.rb +33 -4
  5. data/lib/quartz_flow/settings_helper.rb +22 -1
  6. data/lib/quartz_flow/torrent_manager.rb +53 -6
  7. data/lib/quartz_flow/wrappers.rb +1 -0
  8. data/public/bootstrap/css/bootstrap-theme.css +459 -0
  9. data/public/bootstrap/css/bootstrap-theme.min.css +9 -0
  10. data/public/bootstrap/css/bootstrap.css +4927 -3996
  11. data/public/bootstrap/css/bootstrap.min.css +6 -6
  12. data/public/bootstrap/js/bootstrap.js +1156 -1434
  13. data/public/bootstrap/js/bootstrap.min.js +8 -5
  14. data/public/{bootstrap → bootstrap-2.3.2}/css/bootstrap-responsive.css +0 -0
  15. data/public/{bootstrap → bootstrap-2.3.2}/css/bootstrap-responsive.min.css +0 -0
  16. data/public/bootstrap-2.3.2/css/bootstrap.css +6167 -0
  17. data/public/bootstrap-2.3.2/css/bootstrap.min.css +9 -0
  18. data/public/{bootstrap → bootstrap-2.3.2}/img/glyphicons-halflings-white.png +0 -0
  19. data/public/{bootstrap → bootstrap-2.3.2}/img/glyphicons-halflings.png +0 -0
  20. data/public/bootstrap-2.3.2/js/bootstrap.js +2280 -0
  21. data/public/bootstrap-2.3.2/js/bootstrap.min.js +6 -0
  22. data/public/bootstrap-3.0.2/css/bootstrap-theme.css +459 -0
  23. data/public/bootstrap-3.0.2/css/bootstrap-theme.min.css +9 -0
  24. data/public/bootstrap-3.0.2/css/bootstrap.css +7098 -0
  25. data/public/bootstrap-3.0.2/css/bootstrap.min.css +9 -0
  26. data/public/bootstrap-3.0.2/js/bootstrap.js +2002 -0
  27. data/public/bootstrap-3.0.2/js/bootstrap.min.js +9 -0
  28. data/public/js/quartz.js +118 -22
  29. data/public/style.css +24 -0
  30. data/views/config_partial.haml +18 -18
  31. data/views/index.haml +17 -1
  32. data/views/menu_partial.haml +9 -0
  33. data/views/torrent_detail_partial.haml +21 -17
  34. data/views/torrent_table_partial.haml +29 -27
  35. metadata +24 -10
@@ -0,0 +1,9 @@
1
+ /*!
2
+ * Bootstrap v3.0.2 by @fat and @mdo
3
+ * Copyright 2013 Twitter, Inc.
4
+ * Licensed under http://www.apache.org/licenses/LICENSE-2.0
5
+ *
6
+ * Designed and built with all the love in the world by @mdo and @fat.
7
+ */
8
+
9
+ if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]');if(a.length){var b=this.$element.find("input").prop("checked",!this.$element.hasClass("active")).trigger("change");"radio"===b.prop("type")&&a.find(".active").removeClass("active")}this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('<div class="dropdown-backdrop"/>').insertAfter(a(this)).on("click",b),f.trigger(d=a.Event("show.bs.dropdown")),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown"),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=a("[role=menu] li:not(.divider):visible a",f);if(h.length){var i=h.index(h.filter(":focus"));38==b.keyCode&&i>0&&i--,40==b.keyCode&&i<h.length-1&&i++,~i||(i=0),h.eq(i).focus()}}}};var g=a.fn.dropdown;a.fn.dropdown=function(b){return this.each(function(){var c=a(this),d=c.data("dropdown");d||c.data("dropdown",d=new f(this)),"string"==typeof b&&d[b].call(c)})},a.fn.dropdown.Constructor=f,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=g,this},a(document).on("click.bs.dropdown.data-api",b).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",e,f.prototype.toggle).on("keydown.bs.dropdown.data-api",e+", [role=menu]",f.prototype.keydown)}(jQuery),+function(a){"use strict";var b=function(b,c){this.options=c,this.$element=a(b),this.$backdrop=this.isShown=null,this.options.remote&&this.$element.load(this.options.remote)};b.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},b.prototype.toggle=function(a){return this[this.isShown?"hide":"show"](a)},b.prototype.show=function(b){var c=this,d=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(d),this.isShown||d.isDefaultPrevented()||(this.isShown=!0,this.escape(),this.$element.on("click.dismiss.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.backdrop(function(){var d=a.support.transition&&c.$element.hasClass("fade");c.$element.parent().length||c.$element.appendTo(document.body),c.$element.show(),d&&c.$element[0].offsetWidth,c.$element.addClass("in").attr("aria-hidden",!1),c.enforceFocus();var e=a.Event("shown.bs.modal",{relatedTarget:b});d?c.$element.find(".modal-dialog").one(a.support.transition.end,function(){c.$element.focus().trigger(e)}).emulateTransitionEnd(300):c.$element.focus().trigger(e)}))},b.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").attr("aria-hidden",!0).off("click.dismiss.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one(a.support.transition.end,a.proxy(this.hideModal,this)).emulateTransitionEnd(300):this.hideModal())},b.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.focus()},this))},b.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keyup.dismiss.bs.modal")},b.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden.bs.modal")})},b.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},b.prototype.backdrop=function(b){var c=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var d=a.support.transition&&c;if(this.$backdrop=a('<div class="modal-backdrop '+c+'" />').appendTo(document.body),this.$element.on("click.dismiss.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focus",i="hover"==g?"mouseleave":"blur";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show),void 0):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide),void 0):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this.tip();this.setContent(),this.options.animation&&c.addClass("fade");var d="function"==typeof this.options.placement?this.options.placement.call(this,c[0],this.$element[0]):this.options.placement,e=/\s?auto?\s?/i,f=e.test(d);f&&(d=d.replace(e,"")||"top"),c.detach().css({top:0,left:0,display:"block"}).addClass(d),this.options.container?c.appendTo(this.options.container):c.insertAfter(this.$element);var g=this.getPosition(),h=c[0].offsetWidth,i=c[0].offsetHeight;if(f){var j=this.$element.parent(),k=d,l=document.documentElement.scrollTop||document.body.scrollTop,m="body"==this.options.container?window.innerWidth:j.outerWidth(),n="body"==this.options.container?window.innerHeight:j.outerHeight(),o="body"==this.options.container?0:j.offset().left;d="bottom"==d&&g.top+g.height+i-l>n?"top":"top"==d&&g.top-l-i<0?"bottom":"right"==d&&g.right+h>m?"left":"left"==d&&g.left-h<o?"right":d,c.removeClass(k).addClass(d)}var p=this.getCalculatedOffset(d,g,h,i);this.applyPlacement(p,d),this.$element.trigger("shown.bs."+this.type)}},b.prototype.applyPlacement=function(a,b){var c,d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css("margin-top"),10),h=parseInt(d.css("margin-left"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),a.top=a.top+g,a.left=a.left+h,d.offset(a).addClass("in");var i=d[0].offsetWidth,j=d[0].offsetHeight;if("top"==b&&j!=f&&(c=!0,a.top=a.top+f-j),/bottom|top/.test(b)){var k=0;a.left<0&&(k=-2*a.left,a.left=0,d.offset(a),i=d[0].offsetWidth,j=d[0].offsetHeight),this.replaceArrow(k-e+i,i,"left")}else this.replaceArrow(j-f,j,"top");c&&d.offset(a)},b.prototype.replaceArrow=function(a,b,c){this.arrow().css(c,a?50*(1-a/b)+"%":"")},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},b.prototype.hide=function(){function b(){"in"!=c.hoverState&&d.detach()}var c=this,d=this.tip(),e=a.Event("hide.bs."+this.type);return this.$element.trigger(e),e.isDefaultPrevented()?void 0:(d.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d.one(a.support.transition.end,b).emulateTransitionEnd(150):b(),this.$element.trigger("hidden.bs."+this.type),this)},b.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},b.prototype.hasContent=function(){return this.getTitle()},b.prototype.getPosition=function(){var b=this.$element[0];return a.extend({},"function"==typeof b.getBoundingClientRect?b.getBoundingClientRect():{width:b.offsetWidth,height:b.offsetHeight},this.$element.offset())},b.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},b.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},b.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},b.prototype.validate=function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},b.prototype.enable=function(){this.enabled=!0},b.prototype.disable=function(){this.enabled=!1},b.prototype.toggleEnabled=function(){this.enabled=!this.enabled},b.prototype.toggle=function(b){var c=b?a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type):this;c.tip().hasClass("in")?c.leave(c):c.enter(c)},b.prototype.destroy=function(){this.hide().$element.off("."+this.type).removeData("bs."+this.type)};var c=a.fn.tooltip;a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof c&&c;e||d.data("bs.tooltip",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.tooltip.Constructor=b,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=c,this}}(jQuery),+function(a){"use strict";var b=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");b.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery);
data/public/js/quartz.js CHANGED
@@ -8,13 +8,14 @@ quartzModule.config(function($routeProvider) {
8
8
  when('/', {controller: TorrentTableCtrl, templateUrl:'/torrent_table'}).
9
9
  when('/details/:torrent', {controller: TorrentDetailsCtrl, templateUrl:'/torrent_detail'}).
10
10
  when('/config', {controller: ConfigCtrl, templateUrl:'/config'}).
11
+ when('/show_list', {controller: ShowListCtrl, templateUrl:'/show_list'}).
11
12
  otherwise({redirectTo:'/'});
12
13
 
13
14
  });
14
15
 
15
16
  /* Set up function that keeps retrieving torrent data from server on a timer.
16
17
  We store the data in the rootScope which means it's available in all scopes. */
17
- quartzModule.run(function($rootScope, $timeout, $http, $window) {
18
+ quartzModule.run(function($rootScope, $timeout, $http) {
18
19
  $rootScope.alerts = {};
19
20
 
20
21
  $rootScope.deleteRootscopeError = function(err){
@@ -32,12 +33,26 @@ quartzModule.run(function($rootScope, $timeout, $http, $window) {
32
33
  return rc;
33
34
  };
34
35
 
36
+ });
37
+
38
+ /* Controller for the torrent table view */
39
+ function TorrentTableCtrl($scope, $rootScope, $timeout, $http, $window) {
40
+ $scope.errors = [];
41
+ $scope.destroyed = false;
42
+
43
+ $scope.$on("$destroy", function(e){
44
+ console.log("Destroy called for TorrentTableCtrl");
45
+ $scope.destroyed = true;
46
+ });
47
+ console.log("TorrentTableCtrl called");
48
+
35
49
  // Load the list of torrent data every 1 second.
36
50
  var refresh = function() {
37
51
  // http://code.angularjs.org/1.0.8/docs/api/ng.$http
38
52
 
39
53
  var msg = "Server is unreachable.";
40
- $http.get("/torrent_data", {'timeout': 3000}).
54
+ var fields = ["recommendedName", "dataLength", "state", "infoHash", "downloadRate", "uploadRate","percentComplete","timeLeft","paused"]
55
+ $http.get("/torrent_data", {'timeout': 3000, "params": {"fields" : fields } }).
41
56
  success(function(data,status,headers,config){
42
57
  $rootScope.torrents = data;
43
58
  updateTableTorrentData($rootScope);
@@ -55,15 +70,11 @@ quartzModule.run(function($rootScope, $timeout, $http, $window) {
55
70
  }
56
71
  });
57
72
 
58
- $timeout(refresh, 1000);
73
+ if ( ! $scope.destroyed ){
74
+ $timeout(refresh, 1000);
75
+ }
59
76
  }
60
77
  refresh();
61
- });
62
-
63
- /* Controller for the torrent table view */
64
- function TorrentTableCtrl($scope, $rootScope, $timeout, $http) {
65
- $scope.errors = [];
66
-
67
78
 
68
79
  var checkIframeMessages = function() {
69
80
  while( iframeUploadResultMessages.length > 0 ) {
@@ -73,7 +84,9 @@ function TorrentTableCtrl($scope, $rootScope, $timeout, $http) {
73
84
  }
74
85
  }
75
86
 
76
- $timeout(checkIframeMessages, 1000);
87
+ if ( ! $scope.destroyed ){
88
+ $timeout(checkIframeMessages, 1000);
89
+ }
77
90
  }
78
91
  $timeout(checkIframeMessages, 1000);
79
92
 
@@ -91,9 +104,11 @@ function TorrentTableCtrl($scope, $rootScope, $timeout, $http) {
91
104
  $scope.monthlyUsage = data.monthlyUsage;
92
105
  })
93
106
 
94
- $timeout(getUsage, 2000);
107
+ if ( ! $scope.destroyed ){
108
+ $timeout(getUsage, 2000);
109
+ }
95
110
  }
96
- $timeout(getUsage, 2000);
111
+ getUsage();
97
112
 
98
113
  $scope.getTimes = function(n){
99
114
  var result = [];
@@ -217,13 +232,79 @@ function TorrentTableCtrl($scope, $rootScope, $timeout, $http) {
217
232
  }
218
233
 
219
234
  /* Controller for the torrent details view */
220
- function TorrentDetailsCtrl($scope, $routeParams, $http) {
221
- $scope.torrent = $scope.torrentsForTable[$routeParams.torrent]
235
+ function TorrentDetailsCtrl($scope, $rootScope, $timeout, $routeParams, $http, $window) {
236
+ $scope.destroyed = false;
237
+ $scope.errors = [];
238
+ $scope.torrent = null;
239
+
240
+ $scope.$on("$destroy", function(e){
241
+ console.log("Destroy called for TorrentDetailsCtrl");
242
+ $scope.destroyed = true;
243
+ });
244
+
245
+ $scope.stateForDisplay = function(){
246
+ var torrent = $scope.torrent;
247
+ if ( ! torrent ){
248
+ return "unknown";
249
+ }
250
+
251
+ var result = torrent.state;
252
+
253
+ if ( torrent.paused ){
254
+ result = result + " (paused)";
255
+ }
256
+
257
+ return result;
258
+ }
259
+
260
+ // Load the list of torrent data every 1 second.
261
+ var refresh = function() {
262
+ // http://code.angularjs.org/1.0.8/docs/api/ng.$http
263
+
264
+ var msg = "Server is unreachable.";
265
+ $http.get("/torrent_data", {'timeout': 3000, "params": {"where" : {"infoHash" : $routeParams.torrent} } }).
266
+ success(function(data,status,headers,config){
267
+ if ( ! $scope.torrent ){
268
+ $scope.torrent = data[$routeParams.torrent]
269
+ }
270
+ else {
271
+ updateTorrentData(data[$routeParams.torrent], $scope.torrent);
272
+ }
273
+ $rootScope.deleteRootscopeError(msg);
274
+ }).
275
+ error(function(data,status,headers,config){
276
+ $scope.torrent = null
277
+ if ( status == 0 ){
278
+ $rootScope.alerts[msg] = 1;
279
+ } else if (data == "Authentication required" ) {
280
+ $window.location.href = '/login';
281
+ } else {
282
+ $rootScope.alerts[data] = 1;
283
+ }
284
+ });
285
+
286
+ if ( ! $scope.destroyed ){
287
+ $timeout(refresh, 1000);
288
+ }
289
+ }
290
+ refresh();
222
291
 
223
292
  $scope.deleteError = function(err){
224
293
  genericDeleteError($scope, err);
225
294
  }
226
295
 
296
+ $scope.applyChange = function(propertyName){
297
+ var json = {"infoHash": $scope.torrent.infoHash};
298
+ json[propertyName] = $scope.torrent[propertyName];
299
+ $http.post("/change_torrent", json).
300
+ success(function(data,status,headers,config){
301
+ console.log("huzzah, changing setting succeeded");
302
+ }).
303
+ error(function(data,status,headers,config){
304
+ $scope.errors.push(data);
305
+ });
306
+ }
307
+
227
308
  $scope.applyDownloadRateLimit = function(){
228
309
  $http.post("/change_torrent", {"infoHash": $scope.torrent.infoHash, "downloadRateLimit" : $scope.torrent.downloadRateLimit}).
229
310
  success(function(data,status,headers,config){
@@ -238,6 +319,7 @@ function TorrentDetailsCtrl($scope, $routeParams, $http) {
238
319
 
239
320
  /* Controller for the config view */
240
321
  function ConfigCtrl($scope, $timeout, $http) {
322
+ $scope.errors = [];
241
323
  $scope.deleteError = function(err){
242
324
  genericDeleteError($scope, err);
243
325
  }
@@ -269,6 +351,7 @@ function LoginCtrl($scope, $window, $http) {
269
351
  $scope.password = null;
270
352
 
271
353
  $scope.doLogin = function(){
354
+ var msg = "Server is unreachable.";
272
355
  $http.post("/login", {"login": $scope.login, "password" : $scope.password}).
273
356
  success(function(data,status,headers,config){
274
357
  $window.location.href = '/';
@@ -294,10 +377,14 @@ function LoginCtrl($scope, $window, $http) {
294
377
  }
295
378
  }
296
379
 
297
- var torrentPropsNotToUpdate = { 'downloadRateLimit': 1 };
380
+ function ShowListCtrl($scope) {
381
+ }
298
382
 
299
383
  /* Helper used to update the $scope's list of torrent data shown in the table
300
- from the full data retrieved from the server */
384
+ from the full data retrieved from the server. One of the useful effects of this
385
+ function is that the table model is only changed if the data from the server is
386
+ different. This means AngularJs won't need to keep updating the view with the same
387
+ data every time. */
301
388
  function updateTableTorrentData($scope) {
302
389
  if ( ! $scope.torrents )
303
390
  return;
@@ -316,12 +403,7 @@ function updateTableTorrentData($scope) {
316
403
  else {
317
404
  // Update entry if needed
318
405
  existing = $scope.torrentsForTable[key];
319
- for (var prop in torrent) {
320
- if (torrent.hasOwnProperty(prop)) {
321
- if (existing[prop] != torrent[prop] && !torrentPropsNotToUpdate[prop])
322
- existing[prop] = torrent[prop];
323
- }
324
- }
406
+ updateTorrentData(torrent, existing);
325
407
  }
326
408
  }
327
409
  }
@@ -341,6 +423,20 @@ function updateTableTorrentData($scope) {
341
423
  updatePages($scope);
342
424
  }
343
425
 
426
+ var torrentPropsNotToUpdate = { 'downloadRateLimit': 1, 'uploadRateLimit': 1, 'ratio' : 1, 'uploadDuration' : 1 };
427
+
428
+ /* srcTorrent should be a JSON object representing torrent data retrieved from the server.
429
+ */
430
+ function updateTorrentData(srcTorrent, dstTorrent){
431
+ for (var prop in srcTorrent) {
432
+ if (srcTorrent.hasOwnProperty(prop)) {
433
+ if (dstTorrent[prop] != srcTorrent[prop] && !torrentPropsNotToUpdate[prop])
434
+ dstTorrent[prop] = srcTorrent[prop];
435
+ }
436
+ }
437
+ }
438
+
439
+
344
440
  function updatePages($scope) {
345
441
  // Set up the current page
346
442
  var pageSize = 5;
data/public/style.css CHANGED
@@ -23,3 +23,27 @@ iframe
23
23
  margin-right: 2em;
24
24
  margin-top: 0.4em;
25
25
  }
26
+
27
+ /* See http://www.abeautifulsite.net/blog/2013/08/whipping-file-inputs-into-shape-with-bootstrap-3/ */
28
+ .btn-file {
29
+ position: relative;
30
+ overflow: hidden;
31
+ }
32
+ .btn-file input[type=file] {
33
+ position: absolute;
34
+ top: 0;
35
+ right: 0;
36
+ min-width: 100%;
37
+ min-height: 100%;
38
+ font-size: 999px;
39
+ text-align: right;
40
+ filter: alpha(opacity=0);
41
+ opacity: 0;
42
+ background: red;
43
+ cursor: inherit;
44
+ display: block;
45
+ }
46
+ input[readonly] {
47
+ background-color: white !important;
48
+ cursor: text !important;
49
+ }
@@ -3,28 +3,28 @@
3
3
 
4
4
  %div{ :class => "row"}
5
5
 
6
- %ul{ :class => "nav nav-pills"}
7
- %li
8
- %a{ :href => "#" } Main
9
- %li{ :class => "active" }
10
- %a{ :href => "#/config" } Config
6
+ =locals[:menu]
11
7
 
12
- %div{ :class => "alert alert-error", "ng-repeat" => "error in errors" }
8
+ %div{ :class => "alert alert-danger", "ng-repeat" => "error in errors" }
13
9
  %button{ :type => "button", :class => "close", "ng-click" => "deleteError(error)"} &times;
14
10
  {{error}}
15
11
 
16
12
  %form{ :class => "form-horizontal", "ng-submit" => "applySettings()"}
17
- %div{ :class => "control-group"}
18
- %label{ :id => "uploadRateLabel", :class => "control-label", :for => "uploadRate", "data-toggle" => "tooltip", :title => "Default rate limit to apply to new torrents"} Default Upload Rate Limit
19
- %div{ :class => 'controls'}
20
- %input{ :type => "text", :id => "uploadRate", :placeholder => "Enter rate like '20 KB'", "ng-model" => "globalSettings.defaultUploadRateLimit"}
21
- %div{ :class => "control-group"}
22
- %label{ :id => "downloadRateLabel", :class => "control-label", :for => "downloadRate", "data-toggle" => "tooltip", :title => "Default rate limit to apply to new torrents"} Default Download Rate Limit
23
- %div{ :class => 'controls'}
24
- %input{ :type => "text", :id => "downloadRate", :placeholder => "Enter rate like '500 KB'", "ng-model" => "globalSettings.defaultDownloadRateLimit"}
25
- %div{ :class => "control-group"}
26
- %label{ :id => "ratioLabel", :class => "control-label", :for => "ratio", "data-toggle" => "tooltip", :title => "Default upload ratio to apply to new torrents"} Default Upload Ratio
27
- %div{ :class => 'controls'}
28
- %input{ :type => "text", :id => "ratio", :placeholder => "Enter decimal number like '1.5'", "ng-model" => "globalSettings.defaultRatio"}
13
+ %div{ :class => "form-group"}
14
+ %label{ :id => "uploadRateLabel", :class => "control-label col-sm-4", :for => "uploadRate", "data-toggle" => "tooltip", :title => "Default rate limit to apply to new torrents"} Default Upload Rate Limit
15
+ %div{ :class => "col-sm-4" }
16
+ %input{ :class => "form-control", :type => "text", :id => "uploadRate", :placeholder => "Enter rate like '20 KB'", "ng-model" => "globalSettings.defaultUploadRateLimit"}
17
+ %div{ :class => "form-group"}
18
+ %label{ :id => "downloadRateLabel", :class => "control-label col-sm-4 ", :for => "downloadRate", "data-toggle" => "tooltip", :title => "Default rate limit to apply to new torrents"} Default Download Rate Limit
19
+ %div{ :class => "col-sm-4" }
20
+ %input{ :class => "form-control", :type => "text", :id => "downloadRate", :placeholder => "Enter rate like '500 KB'", "ng-model" => "globalSettings.defaultDownloadRateLimit"}
21
+ %div{ :class => "form-group"}
22
+ %label{ :id => "ratioLabel", :class => "control-label col-sm-4", :for => "ratio", "data-toggle" => "tooltip", :title => "Default upload ratio to apply to new torrents"} Default Upload Ratio
23
+ %div{ :class => "col-sm-4" }
24
+ %input{ :class => "form-control", :type => "text", :id => "ratio", :placeholder => "Enter decimal number like '1.5'", "ng-model" => "globalSettings.defaultRatio"}
25
+ %div{ :class => "form-group"}
26
+ %label{ :id => "uploadDurationLabel", :class => "control-label col-sm-4", :for => "uploadDuration", "data-toggle" => "tooltip", :title => "Default duration to upload once torrent is complete"} Default Upload Duration
27
+ %div{ :class => "col-sm-4" }
28
+ %input{ :class => "form-control", :type => "text", :id => "uploadDuration", :placeholder => "Enter duration like '1h 20m'", "ng-model" => "globalSettings.defaultUploadDuration"}
29
29
  %button{ :class => "btn", :type => "submit" } Apply
30
30
 
data/views/index.haml CHANGED
@@ -10,7 +10,7 @@
10
10
  %body
11
11
  %h1{ :id => "mainheader", "ng-controller" => "LoginCtrl"}
12
12
  QuartzFlow
13
- %button{ :type => "button", :class => "btn btn-mini", :id => "logout", "ng-click" => "doLogout()" } logout
13
+ %button{ :type => "button", :class => "btn btn-xs", :id => "logout", "ng-click" => "doLogout()" } logout
14
14
 
15
15
  %div{ :class => "container", "ng-view" => ''}
16
16
 
@@ -19,3 +19,19 @@
19
19
 
20
20
  -# This is the file-upload iframe
21
21
  %iframe{ :id => "uploadiframe", :name => "uploadiframe", :onload => 'handleIframeLoad("uploadiframe")' }
22
+
23
+ -# File input handling. See http://www.abeautifulsite.net/blog/2013/08/whipping-file-inputs-into-shape-with-bootstrap-3/
24
+ :javascript
25
+ console.log("HI");
26
+ $(document)
27
+ .on('change', '.btn-file :file', function() {
28
+ var input = $(this),
29
+ numFiles = input.get(0).files ? input.get(0).files.length : 1,
30
+ label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
31
+ console.log("CHANGED: " + label);
32
+ input.trigger('fileselect', [numFiles, label]);
33
+
34
+ var textInput = $(this).parents('.input-group').find(':text');
35
+ textInput.val(label);
36
+
37
+ });
@@ -0,0 +1,9 @@
1
+ %ul{ :class => "nav nav-pills"}
2
+ - links = locals[:links]
3
+ - active_link = locals[:active_link]
4
+ - if links
5
+ - links.each do |name, path|
6
+ %li{ :class => "#{name == active_link ? "active" : ""}" }
7
+ %a{ :href => "#{path}" }
8
+ = name
9
+
@@ -1,14 +1,10 @@
1
- %ul{ :class => "nav nav-pills"}
2
- %li
3
- %a{ :href => "#" } Main
4
- %li
5
- %a{ :href => "#/config" } Config
1
+ =locals[:menu]
6
2
 
7
- %div{ :class => "alert alert-error", "ng-repeat" => "error in rootErrors()" }
3
+ %div{ :class => "alert alert-danger", "ng-repeat" => "error in rootErrors()" }
8
4
  %button{ :type => "button", :class => "close", "ng-click" => "deleteError(error)"} &times;
9
5
  {{error}}
10
6
 
11
- %div{ :class => "alert alert-error", "ng-repeat" => "error in errors" }
7
+ %div{ :class => "alert alert-danger", "ng-repeat" => "error in errors" }
12
8
  %button{ :type => "button", :class => "close", "ng-click" => "deleteError(error)"} &times;
13
9
  {{error}}
14
10
 
@@ -23,7 +19,7 @@
23
19
  %td {{torrent.completePieces}}/{{torrent.totalPieces}}
24
20
  %tr
25
21
  %th Status
26
- %td {{torrent.state}}
22
+ %td {{stateForDisplay()}}
27
23
  %tr
28
24
  %th Progress
29
25
  %td {{torrent.percentComplete}}%
@@ -36,27 +32,35 @@
36
32
  %tr
37
33
  %th Download Rate Limit
38
34
  %td
39
- %form{ "ng-submit" => "applyDownloadRateLimit()" }
35
+ %form{ "ng-submit" => "applyChange('downloadRateLimit')" }
40
36
  %fieldset
41
37
  %div{ :class => "input-append" }
42
- %input{ :class => "span2", :type => "text", "ng-model" => "torrent.downloadRateLimit" }
43
- %button{ :class => "btn", :type => "submit" } Change
38
+ %input{ :class => "span2", :type => "text", "ng-model" => "torrent.downloadRateLimit", :placeholder => "Enter rate like '500 KB'" }
39
+ %button{ :class => "btn btn-sm", :type => "submit" } Change
44
40
  %tr
45
41
  %th Upload Rate Limit
46
42
  %td
47
- %form
43
+ %form{ "ng-submit" => "applyChange('uploadRateLimit')" }
48
44
  %fieldset
49
45
  %div{ :class => "input-append" }
50
- %input{ :class => "span2", :type => "text", "ng-model" => "torrent.uploadRateLimit" }
51
- %button{ :class => "btn", :type => "submit" } Change
46
+ %input{ :class => "span2", :type => "text", "ng-model" => "torrent.uploadRateLimit", :placeholder => "Enter rate like '20 KB'" }
47
+ %button{ :class => "btn btn-sm", :type => "submit" } Change
52
48
  %tr
53
49
  %th Upload Ratio
54
50
  %td
55
- %form
51
+ %form{ "ng-submit" => "applyChange('ratio')" }
56
52
  %fieldset
57
53
  %div{ :class => "input-append" }
58
- %input{ :class => "span2", :type => "text", "ng-model" => "torrent.ratio" }
59
- %button{ :class => "btn", :type => "submit" } Change
54
+ %input{ :class => "span2", :type => "text", "ng-model" => "torrent.ratio", :placeholder => "Enter decimal number like '1.5'" }
55
+ %button{ :class => "btn btn-sm", :type => "submit" } Change
56
+ %tr
57
+ %th Upload Duration Limit
58
+ %td
59
+ %form{ "ng-submit" => "applyChange('uploadDuration')" }
60
+ %fieldset
61
+ %div{ :class => "input-append" }
62
+ %input{ :class => "span2", :type => "text", "ng-model" => "torrent.uploadDuration", :placeholder => "Enter duration like '1h 20m'" }
63
+ %button{ :class => "btn btn-sm", :type => "submit" } Change
60
64
  %tr
61
65
  %th Bytes Uploaded
62
66
  %td {{torrent.bytesUploaded}}
@@ -1,16 +1,12 @@
1
1
  %div{ :class => "row"}
2
+ =locals[:menu]
3
+ %p
2
4
 
3
- %ul{ :class => "nav nav-pills"}
4
- %li{ :class => "active" }
5
- %a{ :href => "#" } Main
6
- %li
7
- %a{ :href => "#/config" } Config
8
-
9
- %div{ :class => "alert alert-error", "ng-repeat" => "error in rootErrors()" }
5
+ %div{ :class => "alert alert-danger", "ng-repeat" => "error in rootErrors()" }
10
6
  %button{ :type => "button", :class => "close", "ng-click" => "deleteError(error)"} &times;
11
7
  {{error}}
12
8
 
13
- %div{ :class => "alert alert-error", "ng-repeat" => "error in errors" }
9
+ %div{ :class => "alert alert-danger", "ng-repeat" => "error in errors" }
14
10
  %button{ :type => "button", :class => "close", "ng-click" => "deleteError(error)"} &times;
15
11
  {{error}}
16
12
 
@@ -18,26 +14,33 @@
18
14
  %p
19
15
  Usage this month: {{monthlyUsage}}, today: {{dailyUsage}}.
20
16
 
21
- %div{ :class => "span4"}
17
+ %div{ :class => "col-md-4"}
22
18
  %form{ "ng-submit" => "downloadTorrentFile()" }
23
19
  %fieldset
24
20
  %label Download using .torrent link
25
- %div{ :class => "input-append" }
26
- %input{ :class => "span2", :type => "text", :placeholder => "paste url", "ng-model" => "torrentToDownload" }
27
- %button{ :class => "btn", :type => "submit" } Get
28
- %div{ :class => "span4"}
21
+ %div{ :class => "input-group input-group-sm" }
22
+ %input{ :class => "form-control", :style => "width: 14em", :type => "text", :placeholder => "paste url", "ng-model" => "torrentToDownload" }
23
+ %button{ :class => "input-group-addon btn", :style => "width: 4em", :type => "submit" } Get
24
+ %div{ :class => "col-md-4"}
29
25
  %form{ "ng-submit" => "startMagnetLink()" }
30
26
  %fieldset
31
27
  %label Download using Magnet
32
- %div{ :class => "input-append" }
33
- %input{ :class => "span2", :type => "text", :placeholder => "paste magnet url", "ng-model" => "magnetToStart" }
34
- %button{ :class => "btn", :type => "submit" } Get
35
- %div{ :class => "span4"}
28
+ %div{ :class => "input-group input-group-sm" }
29
+ %input{ :class => "form-control", :style => "width: 14em", :type => "text", :placeholder => "paste magnet url", "ng-model" => "magnetToStart" }
30
+ %button{ :class => "input-group-addon btn", :style => "width: 4em", :type => "submit" } Get
31
+ %div{ :class => "col-md-4"}
36
32
  %form{:method => "post", :enctype => "multipart/form-data", :action => "/upload_torrent", :target => "uploadiframe", :id => "torrent_upload_form" }
37
33
  %fieldset
38
34
  %label Download using .torrent
39
- %input{ :class => "span3", :type => "file", :placeholder => "browse...", :name => "torrentfile" }
40
- %button{ :class => "btn", :type => "submit" } Upload
35
+ %div{ :class => "input-group input-group-sm" }
36
+ %span{ :class => "input-group-btn" }
37
+ %span{ :class => "btn btn-default btn-file" }
38
+ Browse
39
+ %input{ :type => "file", :placeholder => "browse...", :name => "torrentfile" }
40
+ %input{ :type => "text", :style => "width: 12em", :class => "form-control", :readonly => "" }
41
+ %button{ :class => "input-group-addon btn", :style => "width: 4em", :type => "submit" } Upload
42
+
43
+ %p
41
44
 
42
45
  %table{ :class => "table table-striped table-condensed" }
43
46
  %tr
@@ -65,12 +68,11 @@
65
68
  %a{ :href =>"#", "ng-click" => "deleteTorrent(torrent.infoHash, true)"} Delete Torrent and Files
66
69
  %a{ :href =>"#/details/{{torrent.infoHash}}"} Details
67
70
 
68
- %div{ :class => "pagination pagination-right" }
69
- %ul
70
- %li
71
- %a{ :href => "", "ng-click" => "decrementPage()" } Prev
72
- %li{ "ng-repeat" => "page in pagesInfo", :class => "{{page.style}}" }
73
- %a{ :href => "", "ng-click" => "setPage(page.number)" } {{page.number}}
74
- %li
75
- %a{ :href => "", "ng-click" => "incrementPage()" } Next
71
+ %ul{ :class => "pagination pull-right" }
72
+ %li
73
+ %a{ :href => "", "ng-click" => "decrementPage()" } Prev
74
+ %li{ "ng-repeat" => "page in pagesInfo", :class => "{{page.style}}" }
75
+ %a{ :href => "", "ng-click" => "setPage(page.number)" } {{page.number}}
76
+ %li
77
+ %a{ :href => "", "ng-click" => "incrementPage()" } Next
76
78