Forráskód Böngészése

Merge branch 'master' of https://github.com/AkiraLaine/music-app

unknown 9 éve
szülő
commit
2aef6605f4

+ 169 - 0
app/app.css

@@ -1,5 +1,147 @@
 @import url(http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300);
 
+/*!
+ * Slider for Bootstrap
+ *
+ * Copyright 2012 Stefan Petre
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ */
+.slider {
+  display: inline-block;
+  vertical-align: middle;
+  position: relative;
+}
+.slider.slider-horizontal {
+  width: 210px;
+  height: 20px;
+}
+.slider.slider-horizontal .slider-track {
+  height: 10px;
+  width: 100%;
+  margin-top: -5px;
+  top: 50%;
+  left: 0;
+}
+.slider.slider-horizontal .slider-selection {
+  height: 100%;
+  top: 0;
+  bottom: 0;
+}
+.slider.slider-horizontal .slider-handle {
+  margin-left: -10px;
+  margin-top: -5px;
+}
+.slider.slider-horizontal .slider-handle.triangle {
+  border-width: 0 10px 10px 10px;
+  width: 0;
+  height: 0;
+  border-bottom-color: #0480be;
+  margin-top: 0;
+}
+.slider.slider-vertical {
+  height: 210px;
+  width: 20px;
+}
+.slider.slider-vertical .slider-track {
+  width: 10px;
+  height: 100%;
+  margin-left: -5px;
+  left: 50%;
+  top: 0;
+}
+.slider.slider-vertical .slider-selection {
+  width: 100%;
+  left: 0;
+  top: 0;
+  bottom: 0;
+}
+.slider.slider-vertical .slider-handle {
+  margin-left: -5px;
+  margin-top: -10px;
+}
+.slider.slider-vertical .slider-handle.triangle {
+  border-width: 10px 0 10px 10px;
+  width: 1px;
+  height: 1px;
+  border-left-color: #0480be;
+  margin-left: 0;
+}
+.slider input {
+  display: none;
+}
+.slider .tooltip-inner {
+  white-space: nowrap;
+}
+.slider-track {
+  position: absolute;
+  cursor: pointer;
+  background-color: #f7f7f7;
+  background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));
+  background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9);
+  background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9);
+  background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);
+  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+  -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.slider-selection {
+  position: absolute;
+  background-color: #f7f7f7;
+  background-image: -moz-linear-gradient(top, #f9f9f9, #f5f5f5);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f9f9f9), to(#f5f5f5));
+  background-image: -webkit-linear-gradient(top, #f9f9f9, #f5f5f5);
+  background-image: -o-linear-gradient(top, #f9f9f9, #f5f5f5);
+  background-image: linear-gradient(to bottom, #f9f9f9, #f5f5f5);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
+  -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+  -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.slider-handle {
+  position: absolute;
+  width: 20px;
+  height: 20px;
+  background-color: #0e90d2;
+  background-image: -moz-linear-gradient(top, #149bdf, #0480be);
+  background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));
+  background-image: -webkit-linear-gradient(top, #149bdf, #0480be);
+  background-image: -o-linear-gradient(top, #149bdf, #0480be);
+  background-image: linear-gradient(to bottom, #149bdf, #0480be);
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
+  -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
+  box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
+  opacity: 0.8;
+  border: 0px solid transparent;
+}
+.slider-handle.round {
+  -webkit-border-radius: 20px;
+  -moz-border-radius: 20px;
+  border-radius: 20px;
+}
+.slider-handle.triangle {
+  background: transparent none;
+}
+/*
+* End of slider css
+*/
+
 *{
   box-sizing: border-box;
   margin: 0;
@@ -690,3 +832,30 @@ footer a:hover{
 #doorbell-button{
   margin-right: 2000px;
 }
+
+#volume-container {
+  width: 160px; /*12.5 px each side*/
+  float: right;
+  height: 100%;
+  background-color: #97D8C8;
+}
+#volume-container > .slider {
+  top: 40%;
+  transform: translateY(-40%);
+  padding-right: 12px;
+  padding-left: 12px;
+  margin-left: 12px;
+}
+
+#volume-container-admin {
+  display: inline-block;
+  width: 174px;
+  margin-left: 10px;
+}
+
+#volume-container-admin > .slider {
+  width: 150px !important;
+  padding-left: 12px;
+  padding-right: 12px;
+  margin-left: 12px;
+}

+ 94 - 12
app/app.js

@@ -174,10 +174,24 @@ if (Meteor.isClient) {
     });
 
     Template.dashboard.helpers({
-      rooms: function() {
-        return Rooms.find({});
-      }
-    })
+        rooms: function() {
+          return Rooms.find({});
+        },
+        currentSong: function() {
+            var history = History.find({type: this.type}).fetch();
+            if (history.length < 1) {
+                return {};
+            } else {
+                history = history[0];
+                return history.history[history.history.length - 1];
+            }
+        }
+    });
+
+    Template.dashboard.onCreated(function() {
+        if (_sound !== undefined) _sound.stop();
+        Meteor.subscribe("history");
+    });
 
     Template.room.events({
         "click #add-song-button": function(e){
@@ -321,6 +335,31 @@ if (Meteor.isClient) {
             var player = document.getElementById("player");
             player.style.height = (player.offsetWidth / 16 * 9) + "px";
         });
+        $(document).ready(function() {
+            function makeSlider(){
+                var slider = $("#volume-slider").slider();
+                var volume = localStorage.getItem("volume") || 20;
+                $("#volume-slider").slider("setValue", volume);
+                if (slider.length === 0) {
+                    Meteor.setTimeout(function() {
+                        makeSlider();
+                    }, 500);
+                } else {
+                    slider.on("slide", function(val) {
+                        if (yt_player !== undefined) {
+                            yt_player.setVolume(val.value);
+                            localStorage.setItem("volume", val.value);
+                        } else if (_sound !== undefined) {
+                            //_sound
+                            var volume = val.value / 100;
+                            _sound.setVolume(volume);
+                            localStorage.setItem("volume", val.value);
+                        }
+                    });
+                }
+            }
+            makeSlider();
+        });
     });
 
     Template.room.helpers({
@@ -387,6 +426,7 @@ if (Meteor.isClient) {
             var song = Session.get("song");
             var id = song.id;
             var type = song.type;
+            var volume = localStorage.getItem("volume") || 20;
 
             if (type === "YouTube") {
                 if (yt_player === undefined) {
@@ -394,10 +434,23 @@ if (Meteor.isClient) {
                         height: 540,
                         width: 568,
                         videoId: id,
-                        playerVars: {autoplay: 1, controls: 0, iv_load_policy: 3},
+                        playerVars: {autoplay: 1, controls: 0, iv_load_policy: 3, showinfo: 0},
                         events: {
                             'onReady': function(event) {
                                 event.target.playVideo();
+                                event.target.setVolume(volume);
+                            },
+                            'onStateChange': function(event){
+                                if (event.data == YT.PlayerState.PAUSED) {
+                                    event.target.playVideo();
+                                }
+                                if (event.data == YT.PlayerState.PLAYING) {
+                                    $("#play").attr("disabled", true);
+                                    $("#stop").attr("disabled", false);
+                                } else {
+                                    $("#play").attr("disabled", false);
+                                    $("#stop").attr("disabled", true);
+                                }
                             }
                         }
                     });
@@ -408,7 +461,7 @@ if (Meteor.isClient) {
             } else if (type === "SoundCloud") {
                 SC.stream("/tracks/" + song.id, function(sound) {
                     _sound = sound;
-                    sound._player._volume = 0.3;
+                    sound.setVolume(volume / 100);
                     sound.play();
                 });
             }
@@ -457,6 +510,29 @@ if (Meteor.isClient) {
                 $("#stop").attr("disabled", true);
             }
         });
+        $(document).ready(function() {
+            function makeSlider(){
+                var slider = $("#volume-slider").slider();
+                var volume = localStorage.getItem("volume") || 20;
+                $("#volume-slider").slider("setValue", volume);
+                if (slider.length === 0) {
+                    Meteor.setTimeout(function() {
+                        makeSlider();
+                    }, 500);
+                } else {
+                    slider.on("slide", function(val) {
+                        localStorage.setItem("volume", val.value);
+                        if (yt_player !== undefined) {
+                            yt_player.setVolume(val.value);
+                        } else if (_sound !== undefined) {
+                            var volume = val.value / 100;
+                            _sound.setVolume(volume);
+                        }
+                    });
+                }
+            }
+            makeSlider();
+        });
     });
 
     Template.playlist.helpers({
@@ -474,6 +550,8 @@ if (Meteor.isClient) {
     Meteor.subscribe("chat");
 
     Template.room.onCreated(function () {
+        yt_player = undefined;
+        _sound = undefined;
         Session.set("videoShown", false);
         var tag = document.createElement("script");
         tag.src = "https://www.youtube.com/iframe_api";
@@ -483,8 +561,6 @@ if (Meteor.isClient) {
         var currentSong = undefined;
         var nextSong = undefined;
         var afterSong = undefined;
-        var _sound = undefined;
-        var yt_player = undefined;
         var size = 0;
         var artistStr;
         var temp = "";
@@ -513,12 +589,14 @@ if (Meteor.isClient) {
                 if (_sound !== undefined) _sound.stop();
                 if (yt_player !== undefined && yt_player.stopVideo !== undefined) yt_player.stopVideo();
 
-                if (currentSong.type === "soundcloud") {
+                var volume = localStorage.getItem("volume") || 20;
+
+                if (currentSong.type === "SoundCloud") {
                   $("#player").attr("src", "")
                   getSongInfo(currentSong);
                   SC.stream("/tracks/" + currentSong.id + "#t=20s", function(sound){
                     _sound = sound;
-                    sound._player._volume = 0.3;
+                    sound.setVolume(volume / 100);
                     sound.play();
                     var interval = setInterval(function() {
                         if (sound.getState() === "playing") {
@@ -537,10 +615,12 @@ if (Meteor.isClient) {
                             height: 540,
                             width: 960,
                             videoId: currentSong.id,
+                            playerVars: {controls: 0, iv_load_policy: 3, rel: 0, showinfo: 0},
                             events: {
                                 'onReady': function(event) {
                                     event.target.seekTo(getTimeElapsed() / 1000);
                                     event.target.playVideo();
+                                    event.target.setVolume(volume);
                                     resizeSeekerbar();
                                 },
                                 'onStateChange': function(event){
@@ -641,7 +721,6 @@ if (Meteor.isServer) {
     Meteor.users.deny({remove: function () { return true; }});
 
     function getSongDuration(query, artistName){
-        console.log(artistName);
         var duration;
         var search = query;
         query = query.toLowerCase().split(" ").join("%20");
@@ -659,7 +738,6 @@ if (Meteor.isServer) {
     }
 
     function getSongAlbumArt(query, artistName){
-        console.log(artistName);
         var albumart;
         var search = query;
         query = query.toLowerCase().split(" ").join("%20");
@@ -978,6 +1056,10 @@ Router.route("/privacy", {
     template: "privacy"
 });
 
+Router.route("/about", {
+    template: "about"
+});
+
 Router.route("/admin", {
     waitOn: function() {
         return Meteor.subscribe("isAdmin", Meteor.userId());

+ 1 - 0
app/head.html

@@ -7,6 +7,7 @@
     <meta name=viewport content='width=700'>
     <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
     <link href='http://fonts.googleapis.com/css?family=Oxygen:400,300,700' rel='stylesheet' type='text/css'>
+    <script src="/bootstrap-slider.js"></script>
     <script type="application/javascript">
         addEventListener("load", function() {
             setTimeout(hideURLbar, 0);

+ 388 - 0
app/public/bootstrap-slider.js

@@ -0,0 +1,388 @@
+/* =========================================================
+ * bootstrap-slider.js v2.0.0
+ * http://www.eyecon.ro/bootstrap-slider
+ * =========================================================
+ * Copyright 2012 Stefan Petre
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================= */
+
+!function( $ ) {
+
+    var Slider = function(element, options) {
+        this.element = $(element);
+        this.picker = $('<div class="slider">'+
+            '<div class="slider-track">'+
+            '<div class="slider-selection"></div>'+
+            '<div class="slider-handle"></div>'+
+            '<div class="slider-handle"></div>'+
+            '</div>'+
+            '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'+
+            '</div>')
+            .insertBefore(this.element)
+            .append(this.element);
+        this.id = this.element.data('slider-id')||options.id;
+        if (this.id) {
+            this.picker[0].id = this.id;
+        }
+
+        if (typeof Modernizr !== 'undefined' && Modernizr.touch) {
+            this.touchCapable = true;
+        }
+
+        var tooltip = this.element.data('slider-tooltip')||options.tooltip;
+
+        this.tooltip = this.picker.find('.tooltip');
+        this.tooltipInner = this.tooltip.find('div.tooltip-inner');
+
+        this.orientation = this.element.data('slider-orientation')||options.orientation;
+        switch(this.orientation) {
+            case 'vertical':
+                this.picker.addClass('slider-vertical');
+                this.stylePos = 'top';
+                this.mousePos = 'pageY';
+                this.sizePos = 'offsetHeight';
+                this.tooltip.addClass('right')[0].style.left = '100%';
+                break;
+            default:
+                this.picker
+                    .addClass('slider-horizontal')
+                    .css('width', this.element.outerWidth());
+                this.orientation = 'horizontal';
+                this.stylePos = 'left';
+                this.mousePos = 'pageX';
+                this.sizePos = 'offsetWidth';
+                this.tooltip.addClass('top')[0].style.top = -this.tooltip.outerHeight() - 14 + 'px';
+                break;
+        }
+
+        this.min = this.element.data('slider-min')||options.min;
+        this.max = this.element.data('slider-max')||options.max;
+        this.step = this.element.data('slider-step')||options.step;
+        this.value = this.element.data('slider-value')||options.value;
+        if (this.value[1]) {
+            this.range = true;
+        }
+
+        this.selection = this.element.data('slider-selection')||options.selection;
+        this.selectionEl = this.picker.find('.slider-selection');
+        if (this.selection === 'none') {
+            this.selectionEl.addClass('hide');
+        }
+        this.selectionElStyle = this.selectionEl[0].style;
+
+
+        this.handle1 = this.picker.find('.slider-handle:first');
+        this.handle1Stype = this.handle1[0].style;
+        this.handle2 = this.picker.find('.slider-handle:last');
+        this.handle2Stype = this.handle2[0].style;
+
+        var handle = this.element.data('slider-handle')||options.handle;
+        switch(handle) {
+            case 'round':
+                this.handle1.addClass('round');
+                this.handle2.addClass('round');
+                break
+            case 'triangle':
+                this.handle1.addClass('triangle');
+                this.handle2.addClass('triangle');
+                break
+        }
+
+        if (this.range) {
+            this.value[0] = Math.max(this.min, Math.min(this.max, this.value[0]));
+            this.value[1] = Math.max(this.min, Math.min(this.max, this.value[1]));
+        } else {
+            this.value = [ Math.max(this.min, Math.min(this.max, this.value))];
+            this.handle2.addClass('hide');
+            if (this.selection == 'after') {
+                this.value[1] = this.max;
+            } else {
+                this.value[1] = this.min;
+            }
+        }
+        this.diff = this.max - this.min;
+        this.percentage = [
+            (this.value[0]-this.min)*100/this.diff,
+            (this.value[1]-this.min)*100/this.diff,
+            this.step*100/this.diff
+        ];
+
+        this.offset = this.picker.offset();
+        this.size = this.picker[0][this.sizePos];
+
+        this.formater = options.formater;
+
+        this.layout();
+
+        if (this.touchCapable) {
+            // Touch: Bind touch events:
+            this.picker.on({
+                touchstart: $.proxy(this.mousedown, this)
+            });
+        } else {
+            this.picker.on({
+                mousedown: $.proxy(this.mousedown, this)
+            });
+        }
+
+        if (tooltip === 'show') {
+            this.picker.on({
+                mouseenter: $.proxy(this.showTooltip, this),
+                mouseleave: $.proxy(this.hideTooltip, this)
+            });
+        } else {
+            this.tooltip.addClass('hide');
+        }
+    };
+
+    Slider.prototype = {
+        constructor: Slider,
+
+        over: false,
+        inDrag: false,
+
+        showTooltip: function(){
+            this.tooltip.addClass('in');
+            //var left = Math.round(this.percent*this.width);
+            //this.tooltip.css('left', left - this.tooltip.outerWidth()/2);
+            this.over = true;
+        },
+
+        hideTooltip: function(){
+            if (this.inDrag === false) {
+                this.tooltip.removeClass('in');
+            }
+            this.over = false;
+        },
+
+        layout: function(){
+            this.handle1Stype[this.stylePos] = this.percentage[0]+'%';
+            this.handle2Stype[this.stylePos] = this.percentage[1]+'%';
+            if (this.orientation == 'vertical') {
+                this.selectionElStyle.top = Math.min(this.percentage[0], this.percentage[1]) +'%';
+                this.selectionElStyle.height = Math.abs(this.percentage[0] - this.percentage[1]) +'%';
+            } else {
+                this.selectionElStyle.left = Math.min(this.percentage[0], this.percentage[1]) +'%';
+                this.selectionElStyle.width = Math.abs(this.percentage[0] - this.percentage[1]) +'%';
+            }
+            if (this.range) {
+                this.tooltipInner.text(
+                    this.formater(this.value[0]) +
+                    ' : ' +
+                    this.formater(this.value[1])
+                );
+                this.tooltip[0].style[this.stylePos] = this.size * (this.percentage[0] + (this.percentage[1] - this.percentage[0])/2)/100 - (this.orientation === 'vertical' ? this.tooltip.outerHeight()/2 : this.tooltip.outerWidth()/2) +'px';
+            } else {
+                this.tooltipInner.text(
+                    this.formater(this.value[0])
+                );
+                this.tooltip[0].style[this.stylePos] = this.size * this.percentage[0]/100 - (this.orientation === 'vertical' ? this.tooltip.outerHeight()/2 : this.tooltip.outerWidth()/2) +'px';
+            }
+        },
+
+        mousedown: function(ev) {
+
+            // Touch: Get the original event:
+            if (this.touchCapable && ev.type === 'touchstart') {
+                ev = ev.originalEvent;
+            }
+
+            this.offset = this.picker.offset();
+            this.size = this.picker[0][this.sizePos];
+
+            var percentage = this.getPercentage(ev);
+
+            if (this.range) {
+                var diff1 = Math.abs(this.percentage[0] - percentage);
+                var diff2 = Math.abs(this.percentage[1] - percentage);
+                this.dragged = (diff1 < diff2) ? 0 : 1;
+            } else {
+                this.dragged = 0;
+            }
+
+            this.percentage[this.dragged] = percentage;
+            this.layout();
+
+            if (this.touchCapable) {
+                // Touch: Bind touch events:
+                $(document).on({
+                    touchmove: $.proxy(this.mousemove, this),
+                    touchend: $.proxy(this.mouseup, this)
+                });
+            } else {
+                $(document).on({
+                    mousemove: $.proxy(this.mousemove, this),
+                    mouseup: $.proxy(this.mouseup, this)
+                });
+            }
+
+            this.inDrag = true;
+            var val = this.calculateValue();
+            this.element.trigger({
+                type: 'slideStart',
+                value: val
+            }).trigger({
+                type: 'slide',
+                value: val
+            });
+            return false;
+        },
+
+        mousemove: function(ev) {
+
+            // Touch: Get the original event:
+            if (this.touchCapable && ev.type === 'touchmove') {
+                ev = ev.originalEvent;
+            }
+
+            var percentage = this.getPercentage(ev);
+            if (this.range) {
+                if (this.dragged === 0 && this.percentage[1] < percentage) {
+                    this.percentage[0] = this.percentage[1];
+                    this.dragged = 1;
+                } else if (this.dragged === 1 && this.percentage[0] > percentage) {
+                    this.percentage[1] = this.percentage[0];
+                    this.dragged = 0;
+                }
+            }
+            this.percentage[this.dragged] = percentage;
+            this.layout();
+            var val = this.calculateValue();
+            this.element
+                .trigger({
+                    type: 'slide',
+                    value: val
+                })
+                .data('value', val)
+                .prop('value', val);
+            return false;
+        },
+
+        mouseup: function(ev) {
+            if (this.touchCapable) {
+                // Touch: Bind touch events:
+                $(document).off({
+                    touchmove: this.mousemove,
+                    touchend: this.mouseup
+                });
+            } else {
+                $(document).off({
+                    mousemove: this.mousemove,
+                    mouseup: this.mouseup
+                });
+            }
+
+            this.inDrag = false;
+            if (this.over == false) {
+                this.hideTooltip();
+            }
+            this.element;
+            var val = this.calculateValue();
+            this.element
+                .trigger({
+                    type: 'slideStop',
+                    value: val
+                })
+                .data('value', val)
+                .prop('value', val);
+            return false;
+        },
+
+        calculateValue: function() {
+            var val;
+            if (this.range) {
+                val = [
+                    (this.min + Math.round((this.diff * this.percentage[0]/100)/this.step)*this.step),
+                    (this.min + Math.round((this.diff * this.percentage[1]/100)/this.step)*this.step)
+                ];
+                this.value = val;
+            } else {
+                val = (this.min + Math.round((this.diff * this.percentage[0]/100)/this.step)*this.step);
+                this.value = [val, this.value[1]];
+            }
+            return val;
+        },
+
+        getPercentage: function(ev) {
+            if (this.touchCapable) {
+                ev = ev.touches[0];
+            }
+            var percentage = (ev[this.mousePos] - this.offset[this.stylePos])*100/this.size;
+            percentage = Math.round(percentage/this.percentage[2])*this.percentage[2];
+            return Math.max(0, Math.min(100, percentage));
+        },
+
+        getValue: function() {
+            if (this.range) {
+                return this.value;
+            }
+            return this.value[0];
+        },
+
+        setValue: function(val) {
+            this.value = val;
+
+            if (this.range) {
+                this.value[0] = Math.max(this.min, Math.min(this.max, this.value[0]));
+                this.value[1] = Math.max(this.min, Math.min(this.max, this.value[1]));
+            } else {
+                this.value = [ Math.max(this.min, Math.min(this.max, this.value))];
+                this.handle2.addClass('hide');
+                if (this.selection == 'after') {
+                    this.value[1] = this.max;
+                } else {
+                    this.value[1] = this.min;
+                }
+            }
+            this.diff = this.max - this.min;
+            this.percentage = [
+                (this.value[0]-this.min)*100/this.diff,
+                (this.value[1]-this.min)*100/this.diff,
+                this.step*100/this.diff
+            ];
+            this.layout();
+        }
+    };
+
+    $.fn.slider = function ( option, val ) {
+        return this.each(function () {
+            var $this = $(this),
+                data = $this.data('slider'),
+                options = typeof option === 'object' && option;
+            if (!data)  {
+                $this.data('slider', (data = new Slider(this, $.extend({}, $.fn.slider.defaults,options))));
+            }
+            if (typeof option == 'string') {
+                data[option](val);
+            }
+        })
+    };
+
+    $.fn.slider.defaults = {
+        min: 0,
+        max: 10,
+        step: 1,
+        orientation: 'horizontal',
+        value: 5,
+        selection: 'before',
+        tooltip: 'show',
+        handle: 'round',
+        formater: function(value) {
+            return value;
+        }
+    };
+
+    $.fn.slider.Constructor = Slider;
+
+}( window.jQuery );

+ 11 - 0
app/templates/about.html

@@ -0,0 +1,11 @@
+<template name="about">
+    {{> header}}
+    <div class="row">
+        <div class="about col-md-8 col-md-offset-2">
+            <h2>About</h2>
+            <p>
+                &lt;insert description here/>
+            </p>
+        </div>
+    </div>
+</template>

+ 3 - 0
app/templates/admin.html

@@ -64,6 +64,9 @@
                         <div width="960" height="540" id="previewPlayer"></div>
                         <button id="play" class="btn btn-success"><i class="fa fa-play"></i></button>
                         <button id="stop" class="btn btn-danger" disabled><i class="fa fa-stop"></i></button>
+                        <div id="volume-container-admin">
+                            <input type="text" id="volume-slider" class="span2" value="" data-slider-min="0" data-slider-max="100" data-slider-step="1" data-slider-value="50" data-slider-orientation="horizontal" data-slider-selection="after" data-slider-tooltip="hide">
+                        </div>
                     </div>
                     <div class="modal-footer">
                         <button id="close-modal" type="button" class="btn btn-default" data-dismiss="modal">Close</button>

+ 3 - 1
app/templates/dashboard.html

@@ -5,7 +5,9 @@
               <div class="col-md-4 col-sm-6 col-xs-12">
                   <div class="station">
                       <h3>{{type}}</h3>
-                      <h5>Song</h5>
+                      {{#with type=type}}
+                        <h5>{{currentSong.song.title}}</h5>
+                      {{/with}}
                       <a href="/{{type}}" class="station_link"></a>
                   </div>
               </div>

+ 2 - 2
app/templates/footer.html

@@ -3,7 +3,7 @@
         <p>Copyright © 2015 All Right Reserved</p>
         <a class="footerButtons" id="apiButton">API |</a>
         <a href="/terms" class="footerButtons" id="termsButton">Terms |</a>
-		    <a href="/privacy" class="footerButtons" id="privacyButton">Privacy |</a>
-        <a class="footerButtons" id="aboutButton">About</a>
+        <a href="/privacy" class="footerButtons" id="privacyButton">Privacy |</a>
+        <a href="/about" class="footerButtons" id="aboutButton">About</a>
     </footer>
 </template>

+ 33 - 31
app/templates/privacy.html

@@ -1,37 +1,39 @@
 <template name="privacy">
     {{> header}}
-    <div class="privacy">
-        <h2>Privacy Policy</h2>
+    <div class="row">
+        <div class="privacy col-md-8 col-md-offset-2">
+            <h2>Privacy Policy</h2>
 
-        <p>
-            Your privacy is very important to us. Accordingly, we have developed this Policy in order for you to understand how we collect, use, communicate, disclose and make use of personal information. The following outlines our privacy policy.
-        </p>
+            <p>
+                Your privacy is very important to us. Accordingly, we have developed this Policy in order for you to understand how we collect, use, communicate, disclose and make use of personal information. The following outlines our privacy policy.
+            </p>
 
-        <ul type="disc">
-            <li>
-                Before or at the time of collecting personal information, we will identify the purposes for which information is being collected.
-            </li>
-            <li>
-                We will collect and use personal information solely with the objective of fulfilling those purposes specified by us and for other compatible purposes, unless we obtain the consent of the individual concerned or as required by law.
-            </li>
-            <li>
-                We will only retain personal information as long as necessary for the fulfillment of those purposes.
-            </li>
-            <li>
-                We will collect personal information by lawful and fair means and, where appropriate, with the knowledge or consent of the individual concerned.
-            </li>
-            <li>
-                Personal data should be relevant to the purposes for which it is to be used, and, to the extent necessary for those purposes, should be accurate, complete, and up-to-date.
-            </li>
-            <li>
-                We will protect personal information by reasonable security safeguards against loss or theft, as well as unauthorized access, disclosure, copying, use or modification.
-            </li>
-            <li>
-                We will make readily available to customers information about our policies and practices relating to the management of personal information.
-            </li>
-        </ul>
-        <p>
-            We are committed to conducting our business in accordance with these principles in order to ensure that the confidentiality of personal information is protected and maintained.
-        </p>
+            <ul type="disc">
+                <li>
+                    Before or at the time of collecting personal information, we will identify the purposes for which information is being collected.
+                </li>
+                <li>
+                    We will collect and use personal information solely with the objective of fulfilling those purposes specified by us and for other compatible purposes, unless we obtain the consent of the individual concerned or as required by law.
+                </li>
+                <li>
+                    We will only retain personal information as long as necessary for the fulfillment of those purposes.
+                </li>
+                <li>
+                    We will collect personal information by lawful and fair means and, where appropriate, with the knowledge or consent of the individual concerned.
+                </li>
+                <li>
+                    Personal data should be relevant to the purposes for which it is to be used, and, to the extent necessary for those purposes, should be accurate, complete, and up-to-date.
+                </li>
+                <li>
+                    We will protect personal information by reasonable security safeguards against loss or theft, as well as unauthorized access, disclosure, copying, use or modification.
+                </li>
+                <li>
+                    We will make readily available to customers information about our policies and practices relating to the management of personal information.
+                </li>
+            </ul>
+            <p>
+                We are committed to conducting our business in accordance with these principles in order to ensure that the confidentiality of personal information is protected and maintained.
+            </p>
+        </div>
     </div>
 </template>

+ 5 - 2
app/templates/room.html

@@ -4,8 +4,11 @@
           <div class="row">
             <div class="col-md-9" id="station-main">
               <nav>
-                <a class="back" href="/"><i class="fa fa-chevron-left"></i></a>
-                <h3>{{{type}}}</h3>
+                  <a class="back" href="/"><i class="fa fa-chevron-left"></i></a>
+                  <h3>{{{type}}}</h3>
+                  <div id="volume-container">
+                      <input type="text" id="volume-slider" class="span2" value="" data-slider-min="0" data-slider-max="100" data-slider-step="1" data-slider-value="50" data-slider-orientation="horizontal" data-slider-selection="after" data-slider-tooltip="hide">
+                  </div>
               </nav>
               <div id="seeker-container">
                   <div id="seeker-bar"></div>

+ 78 - 76
app/templates/terms.html

@@ -1,80 +1,82 @@
 <template name="terms">
     {{> header}}
-    <div class="terms">
-        <h1>Terms of Service</h1>
-        <small>Last updated: Septemeber 29, 2015</small>
-        <hr>
-        <p>
-            By using the Music App's web site ("Service"), or any services of Music App ("MusicApp"), you are agreeing to be bound by the following terms and conditions ("Terms of Service").
-        </p>
-        <p>
-            MusicApp reserves the right to update and change the Terms of Service from time to time without notice. Any new features that augment or enhance the current Service, including the release of new tools and resources, shall be subject to the Terms of Service. Continued use of the Service after any such changes shall constitute your consent to such changes. You can review the most current version of the Terms of Service at any time.
-        </p>
-        <p>
-            Violation of any of the terms below will result in the termination of your Account. While MusicApp prohibits such conduct and Content on the Service, you understand and agree that MusicApp cannot be responsible for the Content posted on the Service and you nonetheless may be exposed to such materials. You agree to use the Service at your own risk.
-        </p>
-        <hr>
-        <h2>Account Terms</h2>
-        <ol>
-            <li>You must be a human. Accounts registered by "bots" or other automated methods are not permitted.</li>
-            <li>You must provide your legal full name, a valid email address, and any other information requested in order to complete the signup process.</li>
-            <li>Your login may only be used by one person - a single login shared by multiple people is not permitted.</li>
-            <li>You are responsible for maintaining the security of your account and password. MusicApp cannot and will not be liable for any loss or damage from your failure to comply with this security obligation.</li>
-            <li>You are responsible for all Content posted and activity that occurs under your account (even when Content is posted by others who have accounts under your account).</li>
-            <li>You may not use the Service for any illegal or unauthorized purpose. You must not, in the use of the Service, violate any laws in your jurisdiction (including but not limited to copyright or trademark laws).</li>
-        </ol>
-        <hr>
-        <!--h2>Payment, Refunds, Upgrading and Downgrading Terms</h2> We currently do not have anything that requires payment, thus this is not included in the terms of service.
-                <ol>
-                    <li>All paid plans must provide a valid account through one of the approved payment providers provided. Free accounts are not required to provide an account through an approved payment provider.</li>
-                    <li>An upgrade from the free plan to any paying plan will immediately bill you.</li>
-                    <li>The Service is billed instantly and is non-refundable. There will be no refunds or credits for partial months of service, upgrade/downgrade refunds, or refunds for months unused with an open account. In order to treat everyone equally, no exceptions will be made.</li>
-                    <li>All fees are exclusive of all taxes, levies, or duties imposed by taxing authorities, and you shall be responsible for payment of all such taxes, levies, or duties.</li>
-                    <li>For any upgrade or downgrade in plan level, your payment account that you provided will automatically be charged the new rate.</li>
-                </ol>
-            <hr-->
-        <h2>Cancellation and Termination</h2>
-        <ol>
-            <li>You are solely responsible for properly canceling your account. In order to cancel your account you must email the site staff with your cancellation request. </li>
-            <li>All of your Content will be immediately deleted from the Service upon cancellation. This information can not be recovered once your account is cancelled.</li>
-            <!--li>If you cancel the Service before the end of your current paid up month, your cancellation will take effect immediately and you will not be charged again.</li-->
-            <li>MusicApp, in its sole discretion, has the right to suspend or terminate your account and refuse any and all current or future use of the Service, or any other MusicApp service, for any reason at any time. Such termination of the Service will result in the deactivation or deletion of your Account or your access to your Account, and the forfeiture and relinquishment of all Content in your Account.</li>
-            <li>MusicApp reserves the right to refuse service to anyone for any reason at any time.</li>
-        </ol>
-        <hr>
-        <h2>Modifications to the Service and Prices</h2>
-        <ol>
-            <li>MusicApp reserves the right at any time and from time to time to modify or discontinue, temporarily or permanently, the Service (or any part thereof) with or without notice.</li>
-            <li>Prices of all Services, including but not limited to monthly subscription plan fees to the Service, are subject to change upon 30 days notice from us. Such notice may be provided at any time by posting the changes to the MusicApp site or the Service itself.</li>
-            <li>MusicApp shall not be liable to you or to any third party for any modification, price change, suspension or discontinuance of the Service.</li>
-        </ol>
-        <hr>
-        <h2>Copyright and Content Ownership</h2>
-        <ol>
-            <li>We claim no intellectual property rights over the material you provide to the Service. Your profile and materials remain yours. By agreeing to these terms, you agree to allow others to view your Content.</li>
-            <li>MusicApp does not pre-screen Content, but MusicApp and its designee have the right (but not the obligation) in their sole discretion to refuse or remove any Content that is available via the Service.</li>
-            <li>You shall defend MusicApp against any claim, demand, suit or proceeding made or brought against MusicApp by a third party alleging that Your Content, or Your use of the Service in violation of this Agreement, infringes or misappropriates the intellectual property rights of a third party or violates applicable law, and shall indemnify MusicApp for any damages finally awarded against, and for reasonable attorney’s fees incurred by, MusicApp in connection with any such claim, demand, suit or proceeding; provided, that MusicApp (a) promptly gives You written notice of the claim, demand, suit or proceeding; (b) gives You sole control of the defense and settlement of the claim, demand, suit or proceeding (provided that You may not settle any claim, demand, suit or proceeding unless the settlement unconditionally releases MusicApp of all liability); and (c) provides to You all reasonable assistance, at Your expense.</li>
-            <li>The look and feel of the Service is copyright © 2015 MusicApp. All rights reserved. You may not duplicate, copy, or reuse any portion of the HTML/CSS, Javascript, or visual design elements or concepts without express written permission from MusicApp.</li>
-        </ol>
-        <hr>
-        <h2>General Conditions</h2>
-        <ol>
-            <li>Your use of the Service is at your sole risk. The service is provided on an "as is" and "as available" basis.</li>
-            <li>Technical support is only available via email, the site and/or Twitter.</li>
-            <li>You understand that MusicApp uses third party vendors and hosting partners to provide the necessary hardware, software, networking, storage, and related technology required to run the Service.</li>
-            <li>You must not modify, adapt or hack the Service or modify another website so as to falsely imply that it is associated with the Service, MusicApp, or any other MusicApp service.</li>
-            <li>You agree not to reproduce, duplicate, copy, sell, resell or exploit any portion of the Service, use of the Service, or access to the Service without the express written permission by MusicApp.</li>
-            <li>We may, but have no obligation to, remove Content and Accounts containing Content that we determine in our sole discretion are unlawful, offensive, threatening, libelous, defamatory, pornographic, obscene or otherwise objectionable or violates any party's intellectual property or these Terms of Service.</li>
-            <li>Verbal, physical, written or other abuse (including threats of abuse or retribution) of any MusicApp customer, employee, member, or officer will result in immediate account termination.</li>
-            <li>You understand that the technical processing and transmission of the Service, including your Content, may be transferred unencrypted and involve (a) transmissions over various networks; and (b) changes to conform and adapt to technical requirements of connecting networks or devices.</li>
-            <li>You must not upload, post, host, or transmit unsolicited email, SMSs, or "spam" messages.</li>
-            <li>You must not transmit any worms or viruses or any code of a destructive nature.</li>
-            <li>MusicApp does not warrant that (i) the service will meet your specific requirements, (ii) the service will be uninterrupted, timely, secure, or error-free, (iii) the results that may be obtained from the use of the service will be accurate or reliable, (iv) the quality of any products, services, information, or other material purchased or obtained by you through the service will meet your expectations, and (v) any errors in the Service will be corrected.</li>
-            <li>You expressly understand and agree that MusicApp shall not be liable for any direct, indirect, incidental, special, consequential or exemplary damages, including but not limited to, damages for loss of profits, goodwill, use, data or other intangible losses (even if MusicApp has been advised of the possibility of such damages), resulting from: (i) the use or the inability to use the service; (ii) the cost of procurement of substitute goods and services resulting from any goods, data, information or services purchased or obtained or messages received or transactions entered into through or from the service; (iii) unauthorized access to or alteration of your transmissions or data; (iv) statements or conduct of any third party on the service; (v) or any other matter relating to the service.</li>
-            <li>The failure of MusicApp to exercise or enforce any right or provision of the Terms of Service shall not constitute a waiver of such right or provision. The Terms of Service constitutes the entire agreement between you and MusicApp and govern your use of the Service, superseding any prior agreements between you and MusicApp (including, but not limited to, any prior versions of the Terms of Service). You agree that these Terms of Service and Your use of the Service are governed under law.</li>
-            <li>Questions about the Terms of Service should be sent to the site staff
-            </li>
-        </ol>
-        <hr>
+    <div class="row">
+        <div class="terms col-md-8 col-md-offset-2">
+            <h1>Terms of Service</h1>
+            <small>Last updated: Septemeber 29, 2015</small>
+            <hr>
+            <p>
+                By using the Music App's web site ("Service"), or any services of Music App ("MusicApp"), you are agreeing to be bound by the following terms and conditions ("Terms of Service").
+            </p>
+            <p>
+                MusicApp reserves the right to update and change the Terms of Service from time to time without notice. Any new features that augment or enhance the current Service, including the release of new tools and resources, shall be subject to the Terms of Service. Continued use of the Service after any such changes shall constitute your consent to such changes. You can review the most current version of the Terms of Service at any time.
+            </p>
+            <p>
+                Violation of any of the terms below will result in the termination of your Account. While MusicApp prohibits such conduct and Content on the Service, you understand and agree that MusicApp cannot be responsible for the Content posted on the Service and you nonetheless may be exposed to such materials. You agree to use the Service at your own risk.
+            </p>
+            <hr>
+            <h2>Account Terms</h2>
+            <ol>
+                <li>You must be a human. Accounts registered by "bots" or other automated methods are not permitted.</li>
+                <li>You must provide your legal full name, a valid email address, and any other information requested in order to complete the signup process.</li>
+                <li>Your login may only be used by one person - a single login shared by multiple people is not permitted.</li>
+                <li>You are responsible for maintaining the security of your account and password. MusicApp cannot and will not be liable for any loss or damage from your failure to comply with this security obligation.</li>
+                <li>You are responsible for all Content posted and activity that occurs under your account (even when Content is posted by others who have accounts under your account).</li>
+                <li>You may not use the Service for any illegal or unauthorized purpose. You must not, in the use of the Service, violate any laws in your jurisdiction (including but not limited to copyright or trademark laws).</li>
+            </ol>
+            <hr>
+            <!--h2>Payment, Refunds, Upgrading and Downgrading Terms</h2> We currently do not have anything that requires payment, thus this is not included in the terms of service.
+                    <ol>
+                        <li>All paid plans must provide a valid account through one of the approved payment providers provided. Free accounts are not required to provide an account through an approved payment provider.</li>
+                        <li>An upgrade from the free plan to any paying plan will immediately bill you.</li>
+                        <li>The Service is billed instantly and is non-refundable. There will be no refunds or credits for partial months of service, upgrade/downgrade refunds, or refunds for months unused with an open account. In order to treat everyone equally, no exceptions will be made.</li>
+                        <li>All fees are exclusive of all taxes, levies, or duties imposed by taxing authorities, and you shall be responsible for payment of all such taxes, levies, or duties.</li>
+                        <li>For any upgrade or downgrade in plan level, your payment account that you provided will automatically be charged the new rate.</li>
+                    </ol>
+                <hr-->
+            <h2>Cancellation and Termination</h2>
+            <ol>
+                <li>You are solely responsible for properly canceling your account. In order to cancel your account you must email the site staff with your cancellation request. </li>
+                <li>All of your Content will be immediately deleted from the Service upon cancellation. This information can not be recovered once your account is cancelled.</li>
+                <!--li>If you cancel the Service before the end of your current paid up month, your cancellation will take effect immediately and you will not be charged again.</li-->
+                <li>MusicApp, in its sole discretion, has the right to suspend or terminate your account and refuse any and all current or future use of the Service, or any other MusicApp service, for any reason at any time. Such termination of the Service will result in the deactivation or deletion of your Account or your access to your Account, and the forfeiture and relinquishment of all Content in your Account.</li>
+                <li>MusicApp reserves the right to refuse service to anyone for any reason at any time.</li>
+            </ol>
+            <hr>
+            <h2>Modifications to the Service and Prices</h2>
+            <ol>
+                <li>MusicApp reserves the right at any time and from time to time to modify or discontinue, temporarily or permanently, the Service (or any part thereof) with or without notice.</li>
+                <li>Prices of all Services, including but not limited to monthly subscription plan fees to the Service, are subject to change upon 30 days notice from us. Such notice may be provided at any time by posting the changes to the MusicApp site or the Service itself.</li>
+                <li>MusicApp shall not be liable to you or to any third party for any modification, price change, suspension or discontinuance of the Service.</li>
+            </ol>
+            <hr>
+            <h2>Copyright and Content Ownership</h2>
+            <ol>
+                <li>We claim no intellectual property rights over the material you provide to the Service. Your profile and materials remain yours. By agreeing to these terms, you agree to allow others to view your Content.</li>
+                <li>MusicApp does not pre-screen Content, but MusicApp and its designee have the right (but not the obligation) in their sole discretion to refuse or remove any Content that is available via the Service.</li>
+                <li>You shall defend MusicApp against any claim, demand, suit or proceeding made or brought against MusicApp by a third party alleging that Your Content, or Your use of the Service in violation of this Agreement, infringes or misappropriates the intellectual property rights of a third party or violates applicable law, and shall indemnify MusicApp for any damages finally awarded against, and for reasonable attorney’s fees incurred by, MusicApp in connection with any such claim, demand, suit or proceeding; provided, that MusicApp (a) promptly gives You written notice of the claim, demand, suit or proceeding; (b) gives You sole control of the defense and settlement of the claim, demand, suit or proceeding (provided that You may not settle any claim, demand, suit or proceeding unless the settlement unconditionally releases MusicApp of all liability); and (c) provides to You all reasonable assistance, at Your expense.</li>
+                <li>The look and feel of the Service is copyright © 2015 MusicApp. All rights reserved. You may not duplicate, copy, or reuse any portion of the HTML/CSS, Javascript, or visual design elements or concepts without express written permission from MusicApp.</li>
+            </ol>
+            <hr>
+            <h2>General Conditions</h2>
+            <ol>
+                <li>Your use of the Service is at your sole risk. The service is provided on an "as is" and "as available" basis.</li>
+                <li>Technical support is only available via email, the site and/or Twitter.</li>
+                <li>You understand that MusicApp uses third party vendors and hosting partners to provide the necessary hardware, software, networking, storage, and related technology required to run the Service.</li>
+                <li>You must not modify, adapt or hack the Service or modify another website so as to falsely imply that it is associated with the Service, MusicApp, or any other MusicApp service.</li>
+                <li>You agree not to reproduce, duplicate, copy, sell, resell or exploit any portion of the Service, use of the Service, or access to the Service without the express written permission by MusicApp.</li>
+                <li>We may, but have no obligation to, remove Content and Accounts containing Content that we determine in our sole discretion are unlawful, offensive, threatening, libelous, defamatory, pornographic, obscene or otherwise objectionable or violates any party's intellectual property or these Terms of Service.</li>
+                <li>Verbal, physical, written or other abuse (including threats of abuse or retribution) of any MusicApp customer, employee, member, or officer will result in immediate account termination.</li>
+                <li>You understand that the technical processing and transmission of the Service, including your Content, may be transferred unencrypted and involve (a) transmissions over various networks; and (b) changes to conform and adapt to technical requirements of connecting networks or devices.</li>
+                <li>You must not upload, post, host, or transmit unsolicited email, SMSs, or "spam" messages.</li>
+                <li>You must not transmit any worms or viruses or any code of a destructive nature.</li>
+                <li>MusicApp does not warrant that (i) the service will meet your specific requirements, (ii) the service will be uninterrupted, timely, secure, or error-free, (iii) the results that may be obtained from the use of the service will be accurate or reliable, (iv) the quality of any products, services, information, or other material purchased or obtained by you through the service will meet your expectations, and (v) any errors in the Service will be corrected.</li>
+                <li>You expressly understand and agree that MusicApp shall not be liable for any direct, indirect, incidental, special, consequential or exemplary damages, including but not limited to, damages for loss of profits, goodwill, use, data or other intangible losses (even if MusicApp has been advised of the possibility of such damages), resulting from: (i) the use or the inability to use the service; (ii) the cost of procurement of substitute goods and services resulting from any goods, data, information or services purchased or obtained or messages received or transactions entered into through or from the service; (iii) unauthorized access to or alteration of your transmissions or data; (iv) statements or conduct of any third party on the service; (v) or any other matter relating to the service.</li>
+                <li>The failure of MusicApp to exercise or enforce any right or provision of the Terms of Service shall not constitute a waiver of such right or provision. The Terms of Service constitutes the entire agreement between you and MusicApp and govern your use of the Service, superseding any prior agreements between you and MusicApp (including, but not limited to, any prior versions of the Terms of Service). You agree that these Terms of Service and Your use of the Service are governed under law.</li>
+                <li>Questions about the Terms of Service should be sent to the site staff
+                </li>
+            </ol>
+            <hr>
+        </div>
     </div>
 </template>