├── .gitignore ├── README.md ├── app.js ├── package.json ├── public ├── 404.html ├── css │ └── app.css ├── favicon.ico ├── js │ ├── app.js │ ├── controllers.js │ ├── directives.js │ ├── filters.js │ ├── lib │ │ └── angular │ │ │ ├── angular-bootstrap-prettify.js │ │ │ ├── angular-bootstrap-prettify.min.js │ │ │ ├── angular-bootstrap.js │ │ │ ├── angular-bootstrap.min.js │ │ │ ├── angular-cookies.js │ │ │ ├── angular-cookies.min.js │ │ │ ├── angular-loader.js │ │ │ ├── angular-loader.min.js │ │ │ ├── angular-mocks.js │ │ │ ├── angular-resource.js │ │ │ ├── angular-resource.min.js │ │ │ ├── angular-sanitize.js │ │ │ ├── angular-sanitize.min.js │ │ │ ├── angular-scenario.js │ │ │ ├── angular.js │ │ │ ├── angular.min.js │ │ │ └── version.txt │ └── services.js ├── robots.txt ├── scripts │ ├── app.js │ ├── controllers │ │ ├── list.js │ │ ├── queue.js │ │ └── view.js │ ├── directives │ │ └── scroll.js │ ├── filters │ │ └── htmlify.js │ ├── services │ │ ├── localstore.js │ │ ├── queue.js │ │ ├── socket.js │ │ └── youtube.js │ └── vendor │ │ ├── angular-sanitize.min.js │ │ ├── angular.js │ │ ├── angular.min.js │ │ ├── es5-shim.min.js │ │ └── json3.min.js └── styles │ ├── font-awesome.min.css │ ├── font │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff │ ├── foundation.css │ └── toogles.css ├── routes ├── api.js ├── index.js └── socket.js └── views ├── index.html └── partials ├── about.html ├── browse.html ├── contact.html ├── list.html ├── playlists.html ├── queue.html ├── search.html ├── user.html ├── videos.html └── view.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | views_bkp/ 3 | config.js 4 | *.log 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angularjs App on RaspberryPi 2 | 3 | This project combines [RaspberryPiTV](https://github.com/viperfx/RaspberryPiTV) and the AngularJS app [Toogles](https://github.com/mikecrittenden/toogles) to easily browse and stream youtube videos on the raspberrypi. 4 | Clicking on the "Send to RPi" button in the screenshot below processes the youtube id using youtube-dl and lauches the video using omxplayer on raspberrypi. 5 | 6 | ![alt text](http://cl.ly/image/1C2l2w3r3c1m/Screen%20Shot%202013-06-11%20at%2000.34.07.png "AngularJS+Toogle+RaspberryTV") 7 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var express = require('express'), 7 | routes = require('./routes'), 8 | socket = require('./routes/socket.js'), 9 | config = require('./config.js'), 10 | app = module.exports = express(), 11 | server = require('http').createServer(app) 12 | , https = require('https') 13 | , path = require('path') 14 | , io = require('socket.io').listen(server) 15 | , spawn = require('child_process').spawn 16 | , omx = require('omxcontrol') 17 | , EventEmitter = require( "events" ).EventEmitter; 18 | 19 | 20 | // Configuration 21 | 22 | app.configure(function(){ 23 | app.set('views', __dirname + '/views'); 24 | app.set('view engine', 'html'); 25 | app.engine('html', require('ejs').renderFile); 26 | app.use(express.bodyParser()); 27 | app.use(express.methodOverride()); 28 | app.use(express.static(__dirname + '/public')); 29 | app.use(app.router); 30 | }); 31 | 32 | app.configure('development', function(){ 33 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 34 | }); 35 | 36 | app.configure('production', function(){ 37 | app.use(express.errorHandler()); 38 | }); 39 | 40 | // Routes 41 | app.get('/', routes.index); 42 | app.get('/partials/:name', routes.partials); 43 | 44 | // redirect all others to the index (HTML5 history) 45 | app.get('*', routes.index); 46 | 47 | // Socket.io Communication 48 | 49 | //Run and pipe shell script output 50 | function run_shell(cmd, args, cb, end) { 51 | var spawn = require('child_process').spawn, 52 | child = spawn(cmd, args), 53 | me = this; 54 | child.stdout.on('readable', function () { cb(me, child.stdout); }); 55 | child.stdout.on('end', end); 56 | } 57 | var ss; 58 | //Socket.io Server 59 | io.sockets.on('connection', function (socket) { 60 | 61 | socket.on("screen", function(data){ 62 | socket.type = "screen"; 63 | ss = socket; 64 | console.log("Screen ready..."); 65 | }); 66 | socket.on("remote", function(data){ 67 | socket.type = "remote"; 68 | console.log("Remote ready..."); 69 | }); 70 | 71 | socket.on("controll", function(data){ 72 | console.log(data); 73 | if(socket.type === "remote"){ 74 | 75 | if(data.action === "tap"){ 76 | if(ss != undefined){ 77 | ss.emit("controlling", {action:"enter"}); 78 | } 79 | } 80 | else if(data.action === "swipeLeft"){ 81 | if(ss != undefined){ 82 | ss.emit("controlling", {action:"goLeft"}); 83 | } 84 | } 85 | else if(data.action === "swipeRight"){ 86 | if(ss != undefined){ 87 | ss.emit("controlling", {action:"goRight"}); 88 | } 89 | } 90 | } 91 | }); 92 | socket.on("log", function(data){ 93 | console.log(data); 94 | }); 95 | socket.on("video", function(data){ 96 | 97 | if( data.action === "play"){ 98 | console.log(data.video_id); 99 | var id = data.video_id, 100 | url = "http://www.youtube.com/watch?v="+id; 101 | var runShell = new run_shell('youtube-dl',['-gf', '18/22/34/35/37', url], 102 | function (me, stdout) { 103 | me.stdout = stdout.read().toString().replace(/[\r\n]/g, ""); 104 | socket.emit("video",{output: "loading"}); 105 | omx.start(me.stdout); 106 | omx.ev.on('omx_status', function(data) { 107 | socket.emit("video",{status: data, video_id:id}); 108 | }); 109 | }, 110 | function (me) { 111 | socket.emit("video",{status: "now_playing", video_id:id}); 112 | }); 113 | } 114 | if( data.action == "stream") { 115 | var id = data.video_id, 116 | url = "https://api.put.io/v2/files/"+id+"/mp4/stream/?oauth_token="+config.settings.PUTIO_KEY; 117 | var options = { 118 | host: 'api.put.io', 119 | port: 443, 120 | path: '/v2/files/'+id+'/stream?oauth_token='+config.settings.PUTIO_KEY, 121 | method: 'GET' 122 | }; 123 | 124 | var req = https.request(options, function(res) { 125 | console.log('STATUS: ' + res.statusCode); 126 | console.log('HEADERS: ' + JSON.stringify(res.headers)); 127 | omx.start(res.headers.location); 128 | }); 129 | req.end(); 130 | req.on('error', function(e) { 131 | console.log('problem with request: ' + e.message); 132 | }); 133 | } 134 | if (data.action == "pause") { 135 | omx.sendKey('p'); 136 | } 137 | if (data.action == 'quit') { 138 | omx.sendKey('q'); 139 | } 140 | }); 141 | }); 142 | 143 | // Start server 144 | 145 | server.listen(3000, function(){ 146 | console.log("Express server listening on port %d in %s mode", this.address().port, app.settings.env); 147 | }); 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name" 3 | , "version": "0.0.1" 4 | , "private": true 5 | , "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "dependencies": { 9 | "express": "3.0.0", 10 | "socket.io": ">= 0.9.6", 11 | "jade": "*", 12 | "socket.io":"0.9.14", 13 | "omxcontrol":"*" 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Not found :(

145 |

Sorry, but the page you were trying to view does not exist.

146 |

It looks like this was the result of either:

147 | 151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /public/css/app.css: -------------------------------------------------------------------------------- 1 | /* app css stylesheet */ 2 | 3 | .menu { 4 | list-style: none; 5 | border-bottom: 0.1em solid black; 6 | margin-bottom: 2em; 7 | padding: 0 0 0.5em; 8 | } 9 | 10 | .menu:before { 11 | content: "["; 12 | } 13 | 14 | .menu:after { 15 | content: "]"; 16 | } 17 | 18 | .menu > li { 19 | display: inline; 20 | } 21 | 22 | .menu > li:before { 23 | content: "|"; 24 | padding-right: 0.3em; 25 | } 26 | 27 | .menu > li:nth-child(1):before { 28 | content: ""; 29 | padding: 0; 30 | } 31 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viperfx/angular-rpitv/eece593d8ea14f1fa88396c9f971770dec519053/public/favicon.ico -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | // Declare app level module which depends on filters, and services 5 | var app = angular.module('myApp', ['myApp.filters', 'myApp.services', 'myApp.directives']). 6 | config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { 7 | $routeProvider.when('/view1', {templateUrl: 'partials/partial1', controller: MyCtrl1}); 8 | $routeProvider.when('/view2', {templateUrl: 'partials/partial2', controller: MyCtrl2}); 9 | $routeProvider.otherwise({redirectTo: '/view1'}); 10 | $locationProvider.html5Mode(true); 11 | }]); -------------------------------------------------------------------------------- /public/js/controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Controllers */ 4 | 5 | function AppCtrl($scope, socket) { 6 | socket.on('send:name', function (data) { 7 | $scope.name = data.name; 8 | }); 9 | } 10 | 11 | function MyCtrl1($scope, socket) { 12 | socket.on('send:time', function (data) { 13 | $scope.time = data.time; 14 | }); 15 | } 16 | MyCtrl1.$inject = ['$scope', 'socket']; 17 | 18 | 19 | function MyCtrl2() { 20 | } 21 | MyCtrl2.$inject = []; 22 | -------------------------------------------------------------------------------- /public/js/directives.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Directives */ 4 | 5 | 6 | angular.module('myApp.directives', []). 7 | directive('appVersion', ['version', function(version) { 8 | return function(scope, elm, attrs) { 9 | elm.text(version); 10 | }; 11 | }]); 12 | -------------------------------------------------------------------------------- /public/js/filters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Filters */ 4 | 5 | angular.module('myApp.filters', []). 6 | filter('interpolate', ['version', function(version) { 7 | return function(text) { 8 | return String(text).replace(/\%VERSION\%/mg, version); 9 | } 10 | }]); 11 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-bootstrap-prettify.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.5 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(q,k,I){'use strict';function G(c){return c.replace(/\&/g,"&").replace(/\/g,">").replace(/"/g,""")}function D(c,e){var b=k.element("
"+e+"
");c.html("");c.append(b.contents());return c}var t={},w={value:{}},L={"angular.js":"http://code.angularjs.org/"+k.version.full+"/angular.min.js","angular-resource.js":"http://code.angularjs.org/"+k.version.full+"/angular-resource.min.js","angular-sanitize.js":"http://code.angularjs.org/"+k.version.full+"/angular-sanitize.min.js", 7 | "angular-cookies.js":"http://code.angularjs.org/"+k.version.full+"/angular-cookies.min.js"};t.jsFiddle=function(c,e,b){return{terminal:!0,link:function(x,a,r){function d(a,b){return''}var H={html:"",css:"",js:""};k.forEach(r.jsFiddle.split(" "),function(a,b){var d=a.split(".")[1];H[d]+=d=="html"?b==0?"
\n"+c(a,2):"\n\n\n <\!-- CACHE FILE: "+a+' --\>\n 38 | 39 | 40 | */ 41 | factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) { 42 | var cookies = {}, 43 | lastCookies = {}, 44 | lastBrowserCookies, 45 | runEval = false, 46 | copy = angular.copy, 47 | isUndefined = angular.isUndefined; 48 | 49 | //creates a poller fn that copies all cookies from the $browser to service & inits the service 50 | $browser.addPollFn(function() { 51 | var currentCookies = $browser.cookies(); 52 | if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl 53 | lastBrowserCookies = currentCookies; 54 | copy(currentCookies, lastCookies); 55 | copy(currentCookies, cookies); 56 | if (runEval) $rootScope.$apply(); 57 | } 58 | })(); 59 | 60 | runEval = true; 61 | 62 | //at the end of each eval, push cookies 63 | //TODO: this should happen before the "delayed" watches fire, because if some cookies are not 64 | // strings or browser refuses to store some cookies, we update the model in the push fn. 65 | $rootScope.$watch(push); 66 | 67 | return cookies; 68 | 69 | 70 | /** 71 | * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. 72 | */ 73 | function push() { 74 | var name, 75 | value, 76 | browserCookies, 77 | updated; 78 | 79 | //delete any cookies deleted in $cookies 80 | for (name in lastCookies) { 81 | if (isUndefined(cookies[name])) { 82 | $browser.cookies(name, undefined); 83 | } 84 | } 85 | 86 | //update all cookies updated in $cookies 87 | for(name in cookies) { 88 | value = cookies[name]; 89 | if (!angular.isString(value)) { 90 | if (angular.isDefined(lastCookies[name])) { 91 | cookies[name] = lastCookies[name]; 92 | } else { 93 | delete cookies[name]; 94 | } 95 | } else if (value !== lastCookies[name]) { 96 | $browser.cookies(name, value); 97 | updated = true; 98 | } 99 | } 100 | 101 | //verify what was actually stored 102 | if (updated){ 103 | updated = false; 104 | browserCookies = $browser.cookies(); 105 | 106 | for (name in cookies) { 107 | if (cookies[name] !== browserCookies[name]) { 108 | //delete or reset all cookies that the browser dropped from $cookies 109 | if (isUndefined(browserCookies[name])) { 110 | delete cookies[name]; 111 | } else { 112 | cookies[name] = browserCookies[name]; 113 | } 114 | updated = true; 115 | } 116 | } 117 | } 118 | } 119 | }]). 120 | 121 | 122 | /** 123 | * @ngdoc object 124 | * @name ngCookies.$cookieStore 125 | * @requires $cookies 126 | * 127 | * @description 128 | * Provides a key-value (string-object) storage, that is backed by session cookies. 129 | * Objects put or retrieved from this storage are automatically serialized or 130 | * deserialized by angular's toJson/fromJson. 131 | * @example 132 | */ 133 | factory('$cookieStore', ['$cookies', function($cookies) { 134 | 135 | return { 136 | /** 137 | * @ngdoc method 138 | * @name ngCookies.$cookieStore#get 139 | * @methodOf ngCookies.$cookieStore 140 | * 141 | * @description 142 | * Returns the value of given cookie key 143 | * 144 | * @param {string} key Id to use for lookup. 145 | * @returns {Object} Deserialized cookie value. 146 | */ 147 | get: function(key) { 148 | return angular.fromJson($cookies[key]); 149 | }, 150 | 151 | /** 152 | * @ngdoc method 153 | * @name ngCookies.$cookieStore#put 154 | * @methodOf ngCookies.$cookieStore 155 | * 156 | * @description 157 | * Sets a value for given cookie key 158 | * 159 | * @param {string} key Id for the `value`. 160 | * @param {Object} value Value to be stored. 161 | */ 162 | put: function(key, value) { 163 | $cookies[key] = angular.toJson(value); 164 | }, 165 | 166 | /** 167 | * @ngdoc method 168 | * @name ngCookies.$cookieStore#remove 169 | * @methodOf ngCookies.$cookieStore 170 | * 171 | * @description 172 | * Remove given cookie 173 | * 174 | * @param {string} key Id of the key-value pair to delete. 175 | */ 176 | remove: function(key) { 177 | delete $cookies[key]; 178 | } 179 | }; 180 | 181 | }]); 182 | 183 | })(window, window.angular); 184 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-cookies.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.5 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(m,f,l){'use strict';f.module("ngCookies",["ng"]).factory("$cookies",["$rootScope","$browser",function(d,c){var b={},g={},h,i=!1,j=f.copy,k=f.isUndefined;c.addPollFn(function(){var a=c.cookies();h!=a&&(h=a,j(a,g),j(a,b),i&&d.$apply())})();i=!0;d.$watch(function(){var a,e,d;for(a in g)k(b[a])&&c.cookies(a,l);for(a in b)e=b[a],f.isString(e)?e!==g[a]&&(c.cookies(a,e),d=!0):f.isDefined(g[a])?b[a]=g[a]:delete b[a];if(d)for(a in e=c.cookies(),b)b[a]!==e[a]&&(k(e[a])?delete b[a]:b[a]=e[a])});return b}]).factory("$cookieStore", 7 | ["$cookies",function(d){return{get:function(c){return f.fromJson(d[c])},put:function(c,b){d[c]=f.toJson(b)},remove:function(c){delete d[c]}}}])})(window,window.angular); 8 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.5 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | 7 | ( 8 | 9 | /** 10 | * @ngdoc interface 11 | * @name angular.Module 12 | * @description 13 | * 14 | * Interface for configuring angular {@link angular.module modules}. 15 | */ 16 | 17 | function setupModuleLoader(window) { 18 | 19 | function ensure(obj, name, factory) { 20 | return obj[name] || (obj[name] = factory()); 21 | } 22 | 23 | return ensure(ensure(window, 'angular', Object), 'module', function() { 24 | /** @type {Object.} */ 25 | var modules = {}; 26 | 27 | /** 28 | * @ngdoc function 29 | * @name angular.module 30 | * @description 31 | * 32 | * The `angular.module` is a global place for creating and registering Angular modules. All 33 | * modules (angular core or 3rd party) that should be available to an application must be 34 | * registered using this mechanism. 35 | * 36 | * 37 | * # Module 38 | * 39 | * A module is a collocation of services, directives, filters, and configuration information. Module 40 | * is used to configure the {@link AUTO.$injector $injector}. 41 | * 42 | *
 43 |      * // Create a new module
 44 |      * var myModule = angular.module('myModule', []);
 45 |      *
 46 |      * // register a new service
 47 |      * myModule.value('appName', 'MyCoolApp');
 48 |      *
 49 |      * // configure existing services inside initialization blocks.
 50 |      * myModule.config(function($locationProvider) {
 51 | 'use strict';
 52 |      *   // Configure existing providers
 53 |      *   $locationProvider.hashPrefix('!');
 54 |      * });
 55 |      * 
56 | * 57 | * Then you can create an injector and load your modules like this: 58 | * 59 | *
 60 |      * var injector = angular.injector(['ng', 'MyModule'])
 61 |      * 
62 | * 63 | * However it's more likely that you'll just use 64 | * {@link ng.directive:ngApp ngApp} or 65 | * {@link angular.bootstrap} to simplify this process for you. 66 | * 67 | * @param {!string} name The name of the module to create or retrieve. 68 | * @param {Array.=} requires If specified then new module is being created. If unspecified then the 69 | * the module is being retrieved for further configuration. 70 | * @param {Function} configFn Optional configuration function for the module. Same as 71 | * {@link angular.Module#config Module#config()}. 72 | * @returns {module} new module with the {@link angular.Module} api. 73 | */ 74 | return function module(name, requires, configFn) { 75 | if (requires && modules.hasOwnProperty(name)) { 76 | modules[name] = null; 77 | } 78 | return ensure(modules, name, function() { 79 | if (!requires) { 80 | throw Error('No module: ' + name); 81 | } 82 | 83 | /** @type {!Array.>} */ 84 | var invokeQueue = []; 85 | 86 | /** @type {!Array.} */ 87 | var runBlocks = []; 88 | 89 | var config = invokeLater('$injector', 'invoke'); 90 | 91 | /** @type {angular.Module} */ 92 | var moduleInstance = { 93 | // Private state 94 | _invokeQueue: invokeQueue, 95 | _runBlocks: runBlocks, 96 | 97 | /** 98 | * @ngdoc property 99 | * @name angular.Module#requires 100 | * @propertyOf angular.Module 101 | * @returns {Array.} List of module names which must be loaded before this module. 102 | * @description 103 | * Holds the list of modules which the injector will load before the current module is loaded. 104 | */ 105 | requires: requires, 106 | 107 | /** 108 | * @ngdoc property 109 | * @name angular.Module#name 110 | * @propertyOf angular.Module 111 | * @returns {string} Name of the module. 112 | * @description 113 | */ 114 | name: name, 115 | 116 | 117 | /** 118 | * @ngdoc method 119 | * @name angular.Module#provider 120 | * @methodOf angular.Module 121 | * @param {string} name service name 122 | * @param {Function} providerType Construction function for creating new instance of the service. 123 | * @description 124 | * See {@link AUTO.$provide#provider $provide.provider()}. 125 | */ 126 | provider: invokeLater('$provide', 'provider'), 127 | 128 | /** 129 | * @ngdoc method 130 | * @name angular.Module#factory 131 | * @methodOf angular.Module 132 | * @param {string} name service name 133 | * @param {Function} providerFunction Function for creating new instance of the service. 134 | * @description 135 | * See {@link AUTO.$provide#factory $provide.factory()}. 136 | */ 137 | factory: invokeLater('$provide', 'factory'), 138 | 139 | /** 140 | * @ngdoc method 141 | * @name angular.Module#service 142 | * @methodOf angular.Module 143 | * @param {string} name service name 144 | * @param {Function} constructor A constructor function that will be instantiated. 145 | * @description 146 | * See {@link AUTO.$provide#service $provide.service()}. 147 | */ 148 | service: invokeLater('$provide', 'service'), 149 | 150 | /** 151 | * @ngdoc method 152 | * @name angular.Module#value 153 | * @methodOf angular.Module 154 | * @param {string} name service name 155 | * @param {*} object Service instance object. 156 | * @description 157 | * See {@link AUTO.$provide#value $provide.value()}. 158 | */ 159 | value: invokeLater('$provide', 'value'), 160 | 161 | /** 162 | * @ngdoc method 163 | * @name angular.Module#constant 164 | * @methodOf angular.Module 165 | * @param {string} name constant name 166 | * @param {*} object Constant value. 167 | * @description 168 | * Because the constant are fixed, they get applied before other provide methods. 169 | * See {@link AUTO.$provide#constant $provide.constant()}. 170 | */ 171 | constant: invokeLater('$provide', 'constant', 'unshift'), 172 | 173 | /** 174 | * @ngdoc method 175 | * @name angular.Module#filter 176 | * @methodOf angular.Module 177 | * @param {string} name Filter name. 178 | * @param {Function} filterFactory Factory function for creating new instance of filter. 179 | * @description 180 | * See {@link ng.$filterProvider#register $filterProvider.register()}. 181 | */ 182 | filter: invokeLater('$filterProvider', 'register'), 183 | 184 | /** 185 | * @ngdoc method 186 | * @name angular.Module#controller 187 | * @methodOf angular.Module 188 | * @param {string} name Controller name. 189 | * @param {Function} constructor Controller constructor function. 190 | * @description 191 | * See {@link ng.$controllerProvider#register $controllerProvider.register()}. 192 | */ 193 | controller: invokeLater('$controllerProvider', 'register'), 194 | 195 | /** 196 | * @ngdoc method 197 | * @name angular.Module#directive 198 | * @methodOf angular.Module 199 | * @param {string} name directive name 200 | * @param {Function} directiveFactory Factory function for creating new instance of 201 | * directives. 202 | * @description 203 | * See {@link ng.$compileProvider#directive $compileProvider.directive()}. 204 | */ 205 | directive: invokeLater('$compileProvider', 'directive'), 206 | 207 | /** 208 | * @ngdoc method 209 | * @name angular.Module#config 210 | * @methodOf angular.Module 211 | * @param {Function} configFn Execute this function on module load. Useful for service 212 | * configuration. 213 | * @description 214 | * Use this method to register work which needs to be performed on module loading. 215 | */ 216 | config: config, 217 | 218 | /** 219 | * @ngdoc method 220 | * @name angular.Module#run 221 | * @methodOf angular.Module 222 | * @param {Function} initializationFn Execute this function after injector creation. 223 | * Useful for application initialization. 224 | * @description 225 | * Use this method to register work which should be performed when the injector is done 226 | * loading all modules. 227 | */ 228 | run: function(block) { 229 | runBlocks.push(block); 230 | return this; 231 | } 232 | }; 233 | 234 | if (configFn) { 235 | config(configFn); 236 | } 237 | 238 | return moduleInstance; 239 | 240 | /** 241 | * @param {string} provider 242 | * @param {string} method 243 | * @param {String=} insertMethod 244 | * @returns {angular.Module} 245 | */ 246 | function invokeLater(provider, method, insertMethod) { 247 | return function() { 248 | invokeQueue[insertMethod || 'push']([provider, method, arguments]); 249 | return moduleInstance; 250 | } 251 | } 252 | }); 253 | }; 254 | }); 255 | 256 | } 257 | )(window); 258 | 259 | /** 260 | * Closure compiler type information 261 | * 262 | * @typedef { { 263 | * requires: !Array., 264 | * invokeQueue: !Array.>, 265 | * 266 | * service: function(string, Function):angular.Module, 267 | * factory: function(string, Function):angular.Module, 268 | * value: function(string, *):angular.Module, 269 | * 270 | * filter: function(string, Function):angular.Module, 271 | * 272 | * init: function(Function):angular.Module 273 | * } } 274 | */ 275 | angular.Module; 276 | 277 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-loader.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.5 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(i){'use strict';function d(c,b,e){return c[b]||(c[b]=e())}return d(d(i,"angular",Object),"module",function(){var c={};return function(b,e,f){e&&c.hasOwnProperty(b)&&(c[b]=null);return d(c,b,function(){function a(a,b,d){return function(){c[d||"push"]([a,b,arguments]);return g}}if(!e)throw Error("No module: "+b);var c=[],d=[],h=a("$injector","invoke"),g={_invokeQueue:c,_runBlocks:d,requires:e,name:b,provider:a("$provide","provider"),factory:a("$provide","factory"),service:a("$provide","service"), 7 | value:a("$provide","value"),constant:a("$provide","constant","unshift"),filter:a("$filterProvider","register"),controller:a("$controllerProvider","register"),directive:a("$compileProvider","directive"),config:h,run:function(a){d.push(a);return this}};f&&h(f);return g})}})})(window); 8 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-resource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.5 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 'use strict'; 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name ngResource 12 | * @description 13 | */ 14 | 15 | /** 16 | * @ngdoc object 17 | * @name ngResource.$resource 18 | * @requires $http 19 | * 20 | * @description 21 | * A factory which creates a resource object that lets you interact with 22 | * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. 23 | * 24 | * The returned resource object has action methods which provide high-level behaviors without 25 | * the need to interact with the low level {@link ng.$http $http} service. 26 | * 27 | * @param {string} url A parameterized URL template with parameters prefixed by `:` as in 28 | * `/user/:username`. If you are using a URL with a port number (e.g. 29 | * `http://example.com:8080/api`), you'll need to escape the colon character before the port 30 | * number, like this: `$resource('http://example.com\\:8080/api')`. 31 | * 32 | * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in 33 | * `actions` methods. 34 | * 35 | * Each key value in the parameter object is first bound to url template if present and then any 36 | * excess keys are appended to the url search query after the `?`. 37 | * 38 | * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in 39 | * URL `/path/greet?salutation=Hello`. 40 | * 41 | * If the parameter value is prefixed with `@` then the value of that parameter is extracted from 42 | * the data object (useful for non-GET operations). 43 | * 44 | * @param {Object.=} actions Hash with declaration of custom action that should extend the 45 | * default set of resource actions. The declaration should be created in the following format: 46 | * 47 | * {action1: {method:?, params:?, isArray:?}, 48 | * action2: {method:?, params:?, isArray:?}, 49 | * ...} 50 | * 51 | * Where: 52 | * 53 | * - `action` – {string} – The name of action. This name becomes the name of the method on your 54 | * resource object. 55 | * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, 56 | * and `JSONP` 57 | * - `params` – {object=} – Optional set of pre-bound parameters for this action. 58 | * - isArray – {boolean=} – If true then the returned object for this action is an array, see 59 | * `returns` section. 60 | * 61 | * @returns {Object} A resource "class" object with methods for the default set of resource actions 62 | * optionally extended with custom `actions`. The default set contains these actions: 63 | * 64 | * { 'get': {method:'GET'}, 65 | * 'save': {method:'POST'}, 66 | * 'query': {method:'GET', isArray:true}, 67 | * 'remove': {method:'DELETE'}, 68 | * 'delete': {method:'DELETE'} }; 69 | * 70 | * Calling these methods invoke an {@link ng.$http} with the specified http method, 71 | * destination and parameters. When the data is returned from the server then the object is an 72 | * instance of the resource class. The actions `save`, `remove` and `delete` are available on it 73 | * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, 74 | * read, update, delete) on server-side data like this: 75 | *
 76 |         var User = $resource('/user/:userId', {userId:'@id'});
 77 |         var user = User.get({userId:123}, function() {
 78 |           user.abc = true;
 79 |           user.$save();
 80 |         });
 81 |      
82 | * 83 | * It is important to realize that invoking a $resource object method immediately returns an 84 | * empty reference (object or array depending on `isArray`). Once the data is returned from the 85 | * server the existing reference is populated with the actual data. This is a useful trick since 86 | * usually the resource is assigned to a model which is then rendered by the view. Having an empty 87 | * object results in no rendering, once the data arrives from the server then the object is 88 | * populated with the data and the view automatically re-renders itself showing the new data. This 89 | * means that in most case one never has to write a callback function for the action methods. 90 | * 91 | * The action methods on the class object or instance object can be invoked with the following 92 | * parameters: 93 | * 94 | * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` 95 | * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` 96 | * - non-GET instance actions: `instance.$action([parameters], [success], [error])` 97 | * 98 | * 99 | * @example 100 | * 101 | * # Credit card resource 102 | * 103 | *
104 |      // Define CreditCard class
105 |      var CreditCard = $resource('/user/:userId/card/:cardId',
106 |       {userId:123, cardId:'@id'}, {
107 |        charge: {method:'POST', params:{charge:true}}
108 |       });
109 | 
110 |      // We can retrieve a collection from the server
111 |      var cards = CreditCard.query(function() {
112 |        // GET: /user/123/card
113 |        // server returns: [ {id:456, number:'1234', name:'Smith'} ];
114 | 
115 |        var card = cards[0];
116 |        // each item is an instance of CreditCard
117 |        expect(card instanceof CreditCard).toEqual(true);
118 |        card.name = "J. Smith";
119 |        // non GET methods are mapped onto the instances
120 |        card.$save();
121 |        // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
122 |        // server returns: {id:456, number:'1234', name: 'J. Smith'};
123 | 
124 |        // our custom method is mapped as well.
125 |        card.$charge({amount:9.99});
126 |        // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
127 |      });
128 | 
129 |      // we can create an instance as well
130 |      var newCard = new CreditCard({number:'0123'});
131 |      newCard.name = "Mike Smith";
132 |      newCard.$save();
133 |      // POST: /user/123/card {number:'0123', name:'Mike Smith'}
134 |      // server returns: {id:789, number:'01234', name: 'Mike Smith'};
135 |      expect(newCard.id).toEqual(789);
136 |  * 
137 | * 138 | * The object returned from this function execution is a resource "class" which has "static" method 139 | * for each action in the definition. 140 | * 141 | * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`. 142 | * When the data is returned from the server then the object is an instance of the resource type and 143 | * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD 144 | * operations (create, read, update, delete) on server-side data. 145 | 146 |
147 |      var User = $resource('/user/:userId', {userId:'@id'});
148 |      var user = User.get({userId:123}, function() {
149 |        user.abc = true;
150 |        user.$save();
151 |      });
152 |    
153 | * 154 | * It's worth noting that the success callback for `get`, `query` and other method gets passed 155 | * in the response that came from the server as well as $http header getter function, so one 156 | * could rewrite the above example and get access to http headers as: 157 | * 158 |
159 |      var User = $resource('/user/:userId', {userId:'@id'});
160 |      User.get({userId:123}, function(u, getResponseHeaders){
161 |        u.abc = true;
162 |        u.$save(function(u, putResponseHeaders) {
163 |          //u => saved user object
164 |          //putResponseHeaders => $http header getter
165 |        });
166 |      });
167 |    
168 | 169 | * # Buzz client 170 | 171 | Let's look at what a buzz client created with the `$resource` service looks like: 172 | 173 | 174 | 194 | 195 |
196 | 197 | 198 |
199 |
200 |

201 | 202 | {{item.actor.name}} 203 | Expand replies: {{item.links.replies[0].count}} 204 |

205 | {{item.object.content | html}} 206 |
207 | 208 | {{reply.actor.name}}: {{reply.content | html}} 209 |
210 |
211 |
212 |
213 | 214 | 215 |
216 | */ 217 | angular.module('ngResource', ['ng']). 218 | factory('$resource', ['$http', '$parse', function($http, $parse) { 219 | var DEFAULT_ACTIONS = { 220 | 'get': {method:'GET'}, 221 | 'save': {method:'POST'}, 222 | 'query': {method:'GET', isArray:true}, 223 | 'remove': {method:'DELETE'}, 224 | 'delete': {method:'DELETE'} 225 | }; 226 | var noop = angular.noop, 227 | forEach = angular.forEach, 228 | extend = angular.extend, 229 | copy = angular.copy, 230 | isFunction = angular.isFunction, 231 | getter = function(obj, path) { 232 | return $parse(path)(obj); 233 | }; 234 | 235 | /** 236 | * We need our custom method because encodeURIComponent is too aggressive and doesn't follow 237 | * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path 238 | * segments: 239 | * segment = *pchar 240 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 241 | * pct-encoded = "%" HEXDIG HEXDIG 242 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 243 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 244 | * / "*" / "+" / "," / ";" / "=" 245 | */ 246 | function encodeUriSegment(val) { 247 | return encodeUriQuery(val, true). 248 | replace(/%26/gi, '&'). 249 | replace(/%3D/gi, '='). 250 | replace(/%2B/gi, '+'); 251 | } 252 | 253 | 254 | /** 255 | * This method is intended for encoding *key* or *value* parts of query component. We need a custom 256 | * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be 257 | * encoded per http://tools.ietf.org/html/rfc3986: 258 | * query = *( pchar / "/" / "?" ) 259 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 260 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 261 | * pct-encoded = "%" HEXDIG HEXDIG 262 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 263 | * / "*" / "+" / "," / ";" / "=" 264 | */ 265 | function encodeUriQuery(val, pctEncodeSpaces) { 266 | return encodeURIComponent(val). 267 | replace(/%40/gi, '@'). 268 | replace(/%3A/gi, ':'). 269 | replace(/%24/g, '$'). 270 | replace(/%2C/gi, ','). 271 | replace((pctEncodeSpaces ? null : /%20/g), '+'); 272 | } 273 | 274 | function Route(template, defaults) { 275 | this.template = template = template + '#'; 276 | this.defaults = defaults || {}; 277 | var urlParams = this.urlParams = {}; 278 | forEach(template.split(/\W/), function(param){ 279 | if (param && (new RegExp("(^|[^\\\\]):" + param + "\\W").test(template))) { 280 | urlParams[param] = true; 281 | } 282 | }); 283 | this.template = template.replace(/\\:/g, ':'); 284 | } 285 | 286 | Route.prototype = { 287 | url: function(params) { 288 | var self = this, 289 | url = this.template, 290 | val, 291 | encodedVal; 292 | 293 | params = params || {}; 294 | forEach(this.urlParams, function(_, urlParam){ 295 | val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; 296 | if (angular.isDefined(val) && val !== null) { 297 | encodedVal = encodeUriSegment(val); 298 | url = url.replace(new RegExp(":" + urlParam + "(\\W)", "g"), encodedVal + "$1"); 299 | } else { 300 | url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W)", "g"), function(match, 301 | leadingSlashes, tail) { 302 | if (tail.charAt(0) == '/') { 303 | return tail; 304 | } else { 305 | return leadingSlashes + tail; 306 | } 307 | }); 308 | } 309 | }); 310 | url = url.replace(/\/?#$/, ''); 311 | var query = []; 312 | forEach(params, function(value, key){ 313 | if (!self.urlParams[key]) { 314 | query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value)); 315 | } 316 | }); 317 | query.sort(); 318 | url = url.replace(/\/*$/, ''); 319 | return url + (query.length ? '?' + query.join('&') : ''); 320 | } 321 | }; 322 | 323 | 324 | function ResourceFactory(url, paramDefaults, actions) { 325 | var route = new Route(url); 326 | 327 | actions = extend({}, DEFAULT_ACTIONS, actions); 328 | 329 | function extractParams(data, actionParams){ 330 | var ids = {}; 331 | actionParams = extend({}, paramDefaults, actionParams); 332 | forEach(actionParams, function(value, key){ 333 | ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; 334 | }); 335 | return ids; 336 | } 337 | 338 | function Resource(value){ 339 | copy(value || {}, this); 340 | } 341 | 342 | forEach(actions, function(action, name) { 343 | action.method = angular.uppercase(action.method); 344 | var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH'; 345 | Resource[name] = function(a1, a2, a3, a4) { 346 | var params = {}; 347 | var data; 348 | var success = noop; 349 | var error = null; 350 | switch(arguments.length) { 351 | case 4: 352 | error = a4; 353 | success = a3; 354 | //fallthrough 355 | case 3: 356 | case 2: 357 | if (isFunction(a2)) { 358 | if (isFunction(a1)) { 359 | success = a1; 360 | error = a2; 361 | break; 362 | } 363 | 364 | success = a2; 365 | error = a3; 366 | //fallthrough 367 | } else { 368 | params = a1; 369 | data = a2; 370 | success = a3; 371 | break; 372 | } 373 | case 1: 374 | if (isFunction(a1)) success = a1; 375 | else if (hasBody) data = a1; 376 | else params = a1; 377 | break; 378 | case 0: break; 379 | default: 380 | throw "Expected between 0-4 arguments [params, data, success, error], got " + 381 | arguments.length + " arguments."; 382 | } 383 | 384 | var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); 385 | $http({ 386 | method: action.method, 387 | url: route.url(extend({}, extractParams(data, action.params || {}), params)), 388 | data: data 389 | }).then(function(response) { 390 | var data = response.data; 391 | 392 | if (data) { 393 | if (action.isArray) { 394 | value.length = 0; 395 | forEach(data, function(item) { 396 | value.push(new Resource(item)); 397 | }); 398 | } else { 399 | copy(data, value); 400 | } 401 | } 402 | (success||noop)(value, response.headers); 403 | }, error); 404 | 405 | return value; 406 | }; 407 | 408 | 409 | Resource.prototype['$' + name] = function(a1, a2, a3) { 410 | var params = extractParams(this), 411 | success = noop, 412 | error; 413 | 414 | switch(arguments.length) { 415 | case 3: params = a1; success = a2; error = a3; break; 416 | case 2: 417 | case 1: 418 | if (isFunction(a1)) { 419 | success = a1; 420 | error = a2; 421 | } else { 422 | params = a1; 423 | success = a2 || noop; 424 | } 425 | case 0: break; 426 | default: 427 | throw "Expected between 1-3 arguments [params, success, error], got " + 428 | arguments.length + " arguments."; 429 | } 430 | var data = hasBody ? this : undefined; 431 | Resource[name].call(this, params, data, success, error); 432 | }; 433 | }); 434 | 435 | Resource.bind = function(additionalParamDefaults){ 436 | return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); 437 | }; 438 | 439 | return Resource; 440 | } 441 | 442 | return ResourceFactory; 443 | }]); 444 | 445 | })(window, window.angular); 446 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-resource.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.5 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(C,d,w){'use strict';d.module("ngResource",["ng"]).factory("$resource",["$http","$parse",function(x,y){function s(b,e){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(e?null:/%20/g,"+")}function t(b,e){this.template=b+="#";this.defaults=e||{};var a=this.urlParams={};h(b.split(/\W/),function(f){f&&RegExp("(^|[^\\\\]):"+f+"\\W").test(b)&&(a[f]=!0)});this.template=b.replace(/\\:/g,":")}function u(b,e,a){function f(m,a){var b= 7 | {},a=o({},e,a);h(a,function(a,z){var c;a.charAt&&a.charAt(0)=="@"?(c=a.substr(1),c=y(c)(m)):c=a;b[z]=c});return b}function g(a){v(a||{},this)}var k=new t(b),a=o({},A,a);h(a,function(a,b){a.method=d.uppercase(a.method);var e=a.method=="POST"||a.method=="PUT"||a.method=="PATCH";g[b]=function(b,c,d,B){var j={},i,l=p,q=null;switch(arguments.length){case 4:q=B,l=d;case 3:case 2:if(r(c)){if(r(b)){l=b;q=c;break}l=c;q=d}else{j=b;i=c;l=d;break}case 1:r(b)?l=b:e?i=b:j=b;break;case 0:break;default:throw"Expected between 0-4 arguments [params, data, success, error], got "+ 8 | arguments.length+" arguments.";}var n=this instanceof g?this:a.isArray?[]:new g(i);x({method:a.method,url:k.url(o({},f(i,a.params||{}),j)),data:i}).then(function(b){var c=b.data;if(c)a.isArray?(n.length=0,h(c,function(a){n.push(new g(a))})):v(c,n);(l||p)(n,b.headers)},q);return n};g.prototype["$"+b]=function(a,d,h){var m=f(this),j=p,i;switch(arguments.length){case 3:m=a;j=d;i=h;break;case 2:case 1:r(a)?(j=a,i=d):(m=a,j=d||p);case 0:break;default:throw"Expected between 1-3 arguments [params, success, error], got "+ 9 | arguments.length+" arguments.";}g[b].call(this,m,e?this:w,j,i)}});g.bind=function(d){return u(b,o({},e,d),a)};return g}var A={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},p=d.noop,h=d.forEach,o=d.extend,v=d.copy,r=d.isFunction;t.prototype={url:function(b){var e=this,a=this.template,f,g,b=b||{};h(this.urlParams,function(h,c){f=b.hasOwnProperty(c)?b[c]:e.defaults[c];d.isDefined(f)&&f!==null?(g=s(f,!0).replace(/%26/gi,"&").replace(/%3D/gi, 10 | "=").replace(/%2B/gi,"+"),a=a.replace(RegExp(":"+c+"(\\W)","g"),g+"$1")):a=a.replace(RegExp("(/?):"+c+"(\\W)","g"),function(a,b,c){return c.charAt(0)=="/"?c:b+c})});var a=a.replace(/\/?#$/,""),k=[];h(b,function(a,b){e.urlParams[b]||k.push(s(b)+"="+s(a))});k.sort();a=a.replace(/\/*$/,"");return a+(k.length?"?"+k.join("&"):"")}};return u}])})(window,window.angular); 11 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-sanitize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.0.5 3 | * (c) 2010-2012 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 'use strict'; 8 | 9 | /** 10 | * @ngdoc overview 11 | * @name ngSanitize 12 | * @description 13 | */ 14 | 15 | /* 16 | * HTML Parser By Misko Hevery (misko@hevery.com) 17 | * based on: HTML Parser By John Resig (ejohn.org) 18 | * Original code by Erik Arvidsson, Mozilla Public License 19 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 20 | * 21 | * // Use like so: 22 | * htmlParser(htmlString, { 23 | * start: function(tag, attrs, unary) {}, 24 | * end: function(tag) {}, 25 | * chars: function(text) {}, 26 | * comment: function(text) {} 27 | * }); 28 | * 29 | */ 30 | 31 | 32 | /** 33 | * @ngdoc service 34 | * @name ngSanitize.$sanitize 35 | * @function 36 | * 37 | * @description 38 | * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are 39 | * then serialized back to properly escaped html string. This means that no unsafe input can make 40 | * it into the returned string, however, since our parser is more strict than a typical browser 41 | * parser, it's possible that some obscure input, which would be recognized as valid HTML by a 42 | * browser, won't make it through the sanitizer. 43 | * 44 | * @param {string} html Html input. 45 | * @returns {string} Sanitized html. 46 | * 47 | * @example 48 | 49 | 50 | 58 |
59 | Snippet: 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
FilterSourceRendered
html filter 69 |
<div ng-bind-html="snippet">
</div>
70 |
72 |
73 |
no filter
<div ng-bind="snippet">
</div>
unsafe html filter
<div ng-bind-html-unsafe="snippet">
</div>
86 |
87 |
88 | 89 | it('should sanitize the html snippet ', function() { 90 | expect(using('#html-filter').element('div').html()). 91 | toBe('

an html\nclick here\nsnippet

'); 92 | }); 93 | 94 | it('should escape snippet without any filter', function() { 95 | expect(using('#escaped-html').element('div').html()). 96 | toBe("<p style=\"color:blue\">an html\n" + 97 | "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 98 | "snippet</p>"); 99 | }); 100 | 101 | it('should inline raw snippet if filtered as unsafe', function() { 102 | expect(using('#html-unsafe-filter').element("div").html()). 103 | toBe("

an html\n" + 104 | "click here\n" + 105 | "snippet

"); 106 | }); 107 | 108 | it('should update', function() { 109 | input('snippet').enter('new text'); 110 | expect(using('#html-filter').binding('snippet')).toBe('new text'); 111 | expect(using('#escaped-html').element('div').html()).toBe("new <b>text</b>"); 112 | expect(using('#html-unsafe-filter').binding("snippet")).toBe('new text'); 113 | }); 114 |
115 |
116 | */ 117 | var $sanitize = function(html) { 118 | var buf = []; 119 | htmlParser(html, htmlSanitizeWriter(buf)); 120 | return buf.join(''); 121 | }; 122 | 123 | 124 | // Regular Expressions for parsing tags and attributes 125 | var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, 126 | END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, 127 | ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, 128 | BEGIN_TAG_REGEXP = /^/g, 131 | CDATA_REGEXP = //g, 132 | URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/, 133 | NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) 134 | 135 | 136 | // Good source of info about elements and attributes 137 | // http://dev.w3.org/html5/spec/Overview.html#semantics 138 | // http://simon.html5.org/html-elements 139 | 140 | // Safe Void Elements - HTML5 141 | // http://dev.w3.org/html5/spec/Overview.html#void-elements 142 | var voidElements = makeMap("area,br,col,hr,img,wbr"); 143 | 144 | // Elements that you can, intentionally, leave open (and which close themselves) 145 | // http://dev.w3.org/html5/spec/Overview.html#optional-tags 146 | var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), 147 | optionalEndTagInlineElements = makeMap("rp,rt"), 148 | optionalEndTagElements = angular.extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); 149 | 150 | // Safe Block Elements - HTML5 151 | var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article,aside," + 152 | "blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6," + 153 | "header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); 154 | 155 | // Inline Elements - HTML5 156 | var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b,bdi,bdo," + 157 | "big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small," + 158 | "span,strike,strong,sub,sup,time,tt,u,var")); 159 | 160 | 161 | // Special Elements (can contain anything) 162 | var specialElements = makeMap("script,style"); 163 | 164 | var validElements = angular.extend({}, voidElements, blockElements, inlineElements, optionalEndTagElements); 165 | 166 | //Attributes that have href and hence need to be sanitized 167 | var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); 168 | var validAttrs = angular.extend({}, uriAttrs, makeMap( 169 | 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ 170 | 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ 171 | 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ 172 | 'scope,scrolling,shape,span,start,summary,target,title,type,'+ 173 | 'valign,value,vspace,width')); 174 | 175 | function makeMap(str) { 176 | var obj = {}, items = str.split(','), i; 177 | for (i = 0; i < items.length; i++) obj[items[i]] = true; 178 | return obj; 179 | } 180 | 181 | 182 | /** 183 | * @example 184 | * htmlParser(htmlString, { 185 | * start: function(tag, attrs, unary) {}, 186 | * end: function(tag) {}, 187 | * chars: function(text) {}, 188 | * comment: function(text) {} 189 | * }); 190 | * 191 | * @param {string} html string 192 | * @param {object} handler 193 | */ 194 | function htmlParser( html, handler ) { 195 | var index, chars, match, stack = [], last = html; 196 | stack.last = function() { return stack[ stack.length - 1 ]; }; 197 | 198 | while ( html ) { 199 | chars = true; 200 | 201 | // Make sure we're not in a script or style element 202 | if ( !stack.last() || !specialElements[ stack.last() ] ) { 203 | 204 | // Comment 205 | if ( html.indexOf(""); 207 | 208 | if ( index >= 0 ) { 209 | if (handler.comment) handler.comment( html.substring( 4, index ) ); 210 | html = html.substring( index + 3 ); 211 | chars = false; 212 | } 213 | 214 | // end tag 215 | } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { 216 | match = html.match( END_TAG_REGEXP ); 217 | 218 | if ( match ) { 219 | html = html.substring( match[0].length ); 220 | match[0].replace( END_TAG_REGEXP, parseEndTag ); 221 | chars = false; 222 | } 223 | 224 | // start tag 225 | } else if ( BEGIN_TAG_REGEXP.test(html) ) { 226 | match = html.match( START_TAG_REGEXP ); 227 | 228 | if ( match ) { 229 | html = html.substring( match[0].length ); 230 | match[0].replace( START_TAG_REGEXP, parseStartTag ); 231 | chars = false; 232 | } 233 | } 234 | 235 | if ( chars ) { 236 | index = html.indexOf("<"); 237 | 238 | var text = index < 0 ? html : html.substring( 0, index ); 239 | html = index < 0 ? "" : html.substring( index ); 240 | 241 | if (handler.chars) handler.chars( decodeEntities(text) ); 242 | } 243 | 244 | } else { 245 | html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ 246 | text = text. 247 | replace(COMMENT_REGEXP, "$1"). 248 | replace(CDATA_REGEXP, "$1"); 249 | 250 | if (handler.chars) handler.chars( decodeEntities(text) ); 251 | 252 | return ""; 253 | }); 254 | 255 | parseEndTag( "", stack.last() ); 256 | } 257 | 258 | if ( html == last ) { 259 | throw "Parse Error: " + html; 260 | } 261 | last = html; 262 | } 263 | 264 | // Clean up any remaining tags 265 | parseEndTag(); 266 | 267 | function parseStartTag( tag, tagName, rest, unary ) { 268 | tagName = angular.lowercase(tagName); 269 | if ( blockElements[ tagName ] ) { 270 | while ( stack.last() && inlineElements[ stack.last() ] ) { 271 | parseEndTag( "", stack.last() ); 272 | } 273 | } 274 | 275 | if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { 276 | parseEndTag( "", tagName ); 277 | } 278 | 279 | unary = voidElements[ tagName ] || !!unary; 280 | 281 | if ( !unary ) 282 | stack.push( tagName ); 283 | 284 | var attrs = {}; 285 | 286 | rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) { 287 | var value = doubleQuotedValue 288 | || singleQoutedValue 289 | || unqoutedValue 290 | || ''; 291 | 292 | attrs[name] = decodeEntities(value); 293 | }); 294 | if (handler.start) handler.start( tagName, attrs, unary ); 295 | } 296 | 297 | function parseEndTag( tag, tagName ) { 298 | var pos = 0, i; 299 | tagName = angular.lowercase(tagName); 300 | if ( tagName ) 301 | // Find the closest opened tag of the same type 302 | for ( pos = stack.length - 1; pos >= 0; pos-- ) 303 | if ( stack[ pos ] == tagName ) 304 | break; 305 | 306 | if ( pos >= 0 ) { 307 | // Close all the open elements, up the stack 308 | for ( i = stack.length - 1; i >= pos; i-- ) 309 | if (handler.end) handler.end( stack[ i ] ); 310 | 311 | // Remove the open elements from the stack 312 | stack.length = pos; 313 | } 314 | } 315 | } 316 | 317 | /** 318 | * decodes all entities into regular string 319 | * @param value 320 | * @returns {string} A string with decoded entities. 321 | */ 322 | var hiddenPre=document.createElement("pre"); 323 | function decodeEntities(value) { 324 | hiddenPre.innerHTML=value.replace(//g, '>'); 343 | } 344 | 345 | /** 346 | * create an HTML/XML writer which writes to buffer 347 | * @param {Array} buf use buf.jain('') to get out sanitized html string 348 | * @returns {object} in the form of { 349 | * start: function(tag, attrs, unary) {}, 350 | * end: function(tag) {}, 351 | * chars: function(text) {}, 352 | * comment: function(text) {} 353 | * } 354 | */ 355 | function htmlSanitizeWriter(buf){ 356 | var ignore = false; 357 | var out = angular.bind(buf, buf.push); 358 | return { 359 | start: function(tag, attrs, unary){ 360 | tag = angular.lowercase(tag); 361 | if (!ignore && specialElements[tag]) { 362 | ignore = tag; 363 | } 364 | if (!ignore && validElements[tag] == true) { 365 | out('<'); 366 | out(tag); 367 | angular.forEach(attrs, function(value, key){ 368 | var lkey=angular.lowercase(key); 369 | if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { 370 | out(' '); 371 | out(key); 372 | out('="'); 373 | out(encodeEntities(value)); 374 | out('"'); 375 | } 376 | }); 377 | out(unary ? '/>' : '>'); 378 | } 379 | }, 380 | end: function(tag){ 381 | tag = angular.lowercase(tag); 382 | if (!ignore && validElements[tag] == true) { 383 | out(''); 386 | } 387 | if (tag == ignore) { 388 | ignore = false; 389 | } 390 | }, 391 | chars: function(chars){ 392 | if (!ignore) { 393 | out(encodeEntities(chars)); 394 | } 395 | } 396 | }; 397 | } 398 | 399 | 400 | // define ngSanitize module and register $sanitize service 401 | angular.module('ngSanitize', []).value('$sanitize', $sanitize); 402 | 403 | /** 404 | * @ngdoc directive 405 | * @name ngSanitize.directive:ngBindHtml 406 | * 407 | * @description 408 | * Creates a binding that will sanitize the result of evaluating the `expression` with the 409 | * {@link ngSanitize.$sanitize $sanitize} service and innerHTML the result into the current element. 410 | * 411 | * See {@link ngSanitize.$sanitize $sanitize} docs for examples. 412 | * 413 | * @element ANY 414 | * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. 415 | */ 416 | angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($sanitize) { 417 | return function(scope, element, attr) { 418 | element.addClass('ng-binding').data('$binding', attr.ngBindHtml); 419 | scope.$watch(attr.ngBindHtml, function ngBindHtmlWatchAction(value) { 420 | value = $sanitize(value); 421 | element.html(value || ''); 422 | }); 423 | }; 424 | }]); 425 | /** 426 | * @ngdoc filter 427 | * @name ngSanitize.filter:linky 428 | * @function 429 | * 430 | * @description 431 | * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and 432 | * plain email address links. 433 | * 434 | * @param {string} text Input text. 435 | * @returns {string} Html-linkified text. 436 | * 437 | * @usage 438 | 439 | * 440 | * @example 441 | 442 | 443 | 453 |
454 | Snippet: 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 466 | 469 | 470 | 471 | 472 | 473 | 474 | 475 |
FilterSourceRendered
linky filter 464 |
<div ng-bind-html="snippet | linky">
</div>
465 |
467 |
468 |
no filter
<div ng-bind="snippet">
</div>
476 | 477 | 478 | it('should linkify the snippet with urls', function() { 479 | expect(using('#linky-filter').binding('snippet | linky')). 480 | toBe('Pretty text with some links: ' + 481 | 'http://angularjs.org/, ' + 482 | 'us@somewhere.org, ' + 483 | 'another@somewhere.org, ' + 484 | 'and one more: ftp://127.0.0.1/.'); 485 | }); 486 | 487 | it ('should not linkify snippet without the linky filter', function() { 488 | expect(using('#escaped-html').binding('snippet')). 489 | toBe("Pretty text with some links:\n" + 490 | "http://angularjs.org/,\n" + 491 | "mailto:us@somewhere.org,\n" + 492 | "another@somewhere.org,\n" + 493 | "and one more: ftp://127.0.0.1/."); 494 | }); 495 | 496 | it('should update', function() { 497 | input('snippet').enter('new http://link.'); 498 | expect(using('#linky-filter').binding('snippet | linky')). 499 | toBe('new http://link.'); 500 | expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); 501 | }); 502 | 503 | 504 | */ 505 | angular.module('ngSanitize').filter('linky', function() { 506 | var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, 507 | MAILTO_REGEXP = /^mailto:/; 508 | 509 | return function(text) { 510 | if (!text) return text; 511 | var match; 512 | var raw = text; 513 | var html = []; 514 | // TODO(vojta): use $sanitize instead 515 | var writer = htmlSanitizeWriter(html); 516 | var url; 517 | var i; 518 | while ((match = raw.match(LINKY_URL_REGEXP))) { 519 | // We can not end in these as they are sometimes found at the end of the sentence 520 | url = match[0]; 521 | // if we did not match ftp/http/mailto then assume mailto 522 | if (match[2] == match[3]) url = 'mailto:' + url; 523 | i = match.index; 524 | writer.chars(raw.substr(0, i)); 525 | writer.start('a', {href:url}); 526 | writer.chars(match[0].replace(MAILTO_REGEXP, '')); 527 | writer.end('a'); 528 | raw = raw.substring(i + match[0].length); 529 | } 530 | writer.chars(raw); 531 | return html.join(''); 532 | }; 533 | }); 534 | 535 | })(window, window.angular); 536 | -------------------------------------------------------------------------------- /public/js/lib/angular/angular-sanitize.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.5 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(I,g){'use strict';function i(a){var d={},a=a.split(","),b;for(b=0;b=0;e--)if(f[e]==b)break;if(e>=0){for(c=f.length-1;c>=e;c--)d.end&&d.end(f[c]);f.length= 7 | e}}var c,h,f=[],j=a;for(f.last=function(){return f[f.length-1]};a;){h=!0;if(!f.last()||!q[f.last()]){if(a.indexOf("<\!--")===0)c=a.indexOf("--\>"),c>=0&&(d.comment&&d.comment(a.substring(4,c)),a=a.substring(c+3),h=!1);else if(B.test(a)){if(c=a.match(r))a=a.substring(c[0].length),c[0].replace(r,e),h=!1}else if(C.test(a)&&(c=a.match(s)))a=a.substring(c[0].length),c[0].replace(s,b),h=!1;h&&(c=a.indexOf("<"),h=c<0?a:a.substring(0,c),a=c<0?"":a.substring(c),d.chars&&d.chars(k(h)))}else a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+ 8 | f.last()+"[^>]*>","i"),function(b,a){a=a.replace(D,"$1").replace(E,"$1");d.chars&&d.chars(k(a));return""}),e("",f.last());if(a==j)throw"Parse Error: "+a;j=a}e()}function k(a){l.innerHTML=a.replace(//g,">")}function u(a){var d=!1,b=g.bind(a,a.push);return{start:function(a,c,h){a=g.lowercase(a);!d&&q[a]&&(d=a);!d&&v[a]== 9 | !0&&(b("<"),b(a),g.forEach(c,function(a,c){var e=g.lowercase(c);if(G[e]==!0&&(w[e]!==!0||a.match(H)))b(" "),b(c),b('="'),b(t(a)),b('"')}),b(h?"/>":">"))},end:function(a){a=g.lowercase(a);!d&&v[a]==!0&&(b(""));a==d&&(d=!1)},chars:function(a){d||b(t(a))}}}var s=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,r=/^<\s*\/\s*([\w:-]+)[^>]*>/,A=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,C=/^/g, 10 | E=//g,H=/^((ftp|https?):\/\/|mailto:|#)/,F=/([^\#-~| |!])/g,p=i("area,br,col,hr,img,wbr"),x=i("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),y=i("rp,rt"),o=g.extend({},y,x),m=g.extend({},x,i("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),n=g.extend({},y,i("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")), 11 | q=i("script,style"),v=g.extend({},p,m,n,o),w=i("background,cite,href,longdesc,src,usemap"),G=g.extend({},w,i("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,span,start,summary,target,title,type,valign,value,vspace,width")),l=document.createElement("pre");g.module("ngSanitize",[]).value("$sanitize",function(a){var d=[]; 12 | z(a,u(d));return d.join("")});g.module("ngSanitize").directive("ngBindHtml",["$sanitize",function(a){return function(d,b,e){b.addClass("ng-binding").data("$binding",e.ngBindHtml);d.$watch(e.ngBindHtml,function(c){c=a(c);b.html(c||"")})}}]);g.module("ngSanitize").filter("linky",function(){var a=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/,d=/^mailto:/;return function(b){if(!b)return b;for(var e=b,c=[],h=u(c),f,g;b=e.match(a);)f=b[0],b[2]==b[3]&&(f="mailto:"+f),g=b.index, 13 | h.chars(e.substr(0,g)),h.start("a",{href:f}),h.chars(b[0].replace(d,"")),h.end("a"),e=e.substring(g+b[0].length);h.chars(e);return c.join("")}})})(window,window.angular); 14 | -------------------------------------------------------------------------------- /public/js/lib/angular/version.txt: -------------------------------------------------------------------------------- 1 | 1.0.5 2 | -------------------------------------------------------------------------------- /public/js/services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Services */ 4 | 5 | 6 | // Demonstrate how to register services 7 | // In this case it is a simple value service. 8 | angular.module('myApp.services', []). 9 | value('version', '0.1'). 10 | factory('socket', function ($rootScope) { 11 | var socket = io.connect(); 12 | return { 13 | on: function (eventName, callback) { 14 | socket.on(eventName, function () { 15 | var args = arguments; 16 | $rootScope.$apply(function () { 17 | callback.apply(socket, args); 18 | }); 19 | }); 20 | }, 21 | emit: function (eventName, data, callback) { 22 | socket.emit(eventName, data, function () { 23 | var args = arguments; 24 | $rootScope.$apply(function () { 25 | if (callback) { 26 | callback.apply(socket, args); 27 | } 28 | }); 29 | }) 30 | } 31 | }; 32 | }); 33 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org/ 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /public/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Angular setup 4 | var tooglesApp = angular.module('tooglesApp', ['ngSanitize']) 5 | .config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { 6 | $routeProvider.when('/browse', { templateUrl: 'partials/list', controller: 'ListCtrl' }); 7 | $routeProvider.when('/queue', { templateUrl: 'partials/list', controller: 'QueueCtrl' }); 8 | $routeProvider.when('/browse/:category', { templateUrl: 'partials/list', controller: 'ListCtrl' }); 9 | $routeProvider.when('/search/:query', { templateUrl: 'partials/list', controller: 'ListCtrl' }); 10 | $routeProvider.when('/view/:id', { templateUrl: 'partials/view', controller: 'ViewCtrl' }); 11 | $routeProvider.when('/playlist/:id', { templateUrl: 'partials/view', controller: 'ViewCtrl' }); 12 | $routeProvider.when('/playlist/:id/:start', { templateUrl: 'partials/view', controller: 'ViewCtrl' }); 13 | $routeProvider.when('/user/:username', { templateUrl: 'partials/list', controller: 'ListCtrl' }); 14 | $routeProvider.when('/user/:username/:feed', { templateUrl: 'partials/list', controller: 'ListCtrl' }); 15 | $routeProvider.otherwise({ redirectTo: '/browse' }); 16 | // $locationProvider.html5Mode(true); 17 | }]); 18 | -------------------------------------------------------------------------------- /public/scripts/controllers/list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The controller used when searching/browsing videos. 3 | */ 4 | tooglesApp.controller('ListCtrl', ['$scope', '$routeParams', '$location', 'youtube', 'queue', 'socket', function($scope, $routeParams, $location, youtube, queue, socket) { 5 | $scope.location = $location; 6 | $scope.searchsort = $location.search()['searchsort'] || false; 7 | $scope.searchduration = $location.search()['searchduration'] || false; 8 | $scope.searchtime = $location.search()['searchtime'] || false; 9 | $scope.section = $location.path().split('/')[1]; 10 | $scope.searchtype = $location.search()['searchtype'] || 'videos'; 11 | $scope.streaming = false; 12 | $scope.nowPlaying = null; 13 | window.searchCallback = function(data) { 14 | if (!$scope.videos) { 15 | $scope.videos = data.feed.entry; 16 | } else { 17 | $scope.videos.push.apply($scope.videos, data.feed.entry); 18 | } 19 | } 20 | 21 | window.userCallback = function(data) { 22 | $scope.user = data.entry; 23 | } 24 | 25 | $scope.getLink = function(video, index) { 26 | if ($scope.resulttype == 'playlists') { 27 | return '#/playlist/' + video.yt$playlistId.$t; 28 | } 29 | return '#/view/' + youtube.urlToID(video.media$group.yt$videoid.$t); 30 | } 31 | 32 | $scope.page = 0; 33 | $scope.loadMore = function() { 34 | $scope.page = $scope.page + 1; 35 | $scope.search(); 36 | } 37 | 38 | $scope.categories = [ 39 | {key: "Autos", title: "Autos & Vehicles"}, 40 | {key: "Comedy", title: "Comedy"}, 41 | {key: "Education", title: "Education"}, 42 | {key: "Entertainment", title: "Entertainment"}, 43 | {key: "Film", title: "Film & Animation"}, 44 | {key: "Howto", title: "How To & Style"}, 45 | {key: "Music", title: "Music"}, 46 | {key: "News", title: "News & Politics"}, 47 | {key: "People", title: "People & Blogs"}, 48 | {key: "Animals", title: "Pets & Animals"}, 49 | {key: "Tech", title: "Science & Technology"}, 50 | {key: "Sports", title: "Sports"}, 51 | {key: "Travel", title: "Travel & Events"}, 52 | ] 53 | 54 | $scope.search = function() { 55 | youtube.setPage($scope.page); 56 | youtube.setCallback('searchCallback'); 57 | if ($routeParams.query !== undefined && $routeParams.query !== "" && $routeParams.query !== "0") { 58 | // This is a search with a specific query. 59 | document.title = $routeParams.query + " | Toogles"; 60 | $scope.query = $routeParams.query; 61 | youtube.getVideos('search', $scope.query); 62 | 63 | } else if ($routeParams.category !== undefined) { 64 | // This is a category page. 65 | document.title = $routeParams.category + " | Toogles";; 66 | youtube.getVideos('category', $routeParams.category); 67 | 68 | } else if ($routeParams.username !== undefined) { 69 | // This is a user page. 70 | var type = 'user'; 71 | if ($routeParams.feed !== undefined) { 72 | type += '_' + $routeParams.feed; 73 | if ($routeParams.feed === 'playlists') { 74 | $scope.resulttype = 'playlists' 75 | } 76 | } 77 | document.title = $routeParams.username + " | Toogles";; 78 | youtube.getVideos(type, $routeParams.username); 79 | youtube.setCallback('userCallback'); 80 | youtube.getItem('users', $routeParams.username); 81 | 82 | } else { 83 | document.title = "Toogles | Awesome goggles for YouTube"; 84 | youtube.getVideos('browse', ''); 85 | } 86 | } 87 | 88 | $scope.$watch('searchsort + searchtime + searchduration + searchtype', function() { 89 | $scope.videos = false; 90 | youtube.setSort($scope.searchsort); 91 | youtube.setTime($scope.searchtime); 92 | youtube.setDuration($scope.searchduration); 93 | youtube.setType($scope.searchtype); 94 | $scope.resulttype = $scope.searchtype; 95 | $scope.search(); 96 | }) 97 | 98 | $scope.urlToID = function(url) { 99 | return youtube.urlToID(url); 100 | } 101 | $scope.formatDuration = function(seconds) { 102 | return youtube.formatDuration(seconds); 103 | } 104 | $scope.isPlaying = function (video) { 105 | if (video.media$group.yt$videoid.$t === $scope.nowPlaying){ 106 | return 'Playing' 107 | }else{ 108 | return 'Play' 109 | } 110 | } 111 | $scope.addToQueue = function(video) { 112 | queue.addToQueue(video); 113 | } 114 | $scope.streaming_toggle = function(video, event) { 115 | //console.log(vid); 116 | if ($scope.streaming == false){ 117 | socket.emit('video', {action:'play', video_id:video.media$group.yt$videoid.$t}); 118 | //angular.element(event.target).html('Loading'); 119 | //$scope.nowPlaying = video.media$group.yt$videoid.$t; 120 | queue.addToQueue(video); 121 | }else{ 122 | socket.emit('video', {action:'pause', video_id:video.media$group.yt$videoid.$t}); 123 | } 124 | } 125 | socket.on('video', function(data){ 126 | if (data.status == 'now_playing') { 127 | $scope.nowPlaying = data.video_id; 128 | } 129 | if (data.status == 'exit'){ 130 | //omx quit, play next vid 131 | //var q = queue.getQueue(); 132 | queue.removeFromQueue(queue.getVideo(data.video_id)); 133 | socket.emit('video', {action:'play', video_id:queue.getQueue().media$group.yt$videoid.$t}); 134 | } 135 | }); 136 | $scope.stop_stream = function() { 137 | socket.emit('video', {action:'quit', video_id:$routeParams.id}) 138 | $scope.streaming = false; 139 | } 140 | }]); 141 | -------------------------------------------------------------------------------- /public/scripts/controllers/queue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The controller used to view queued videos 3 | */ 4 | tooglesApp.controller('QueueCtrl', ['$scope', '$location', 'youtube', 'queue', 'local', function($scope, $location, youtube, queue, local) { 5 | $scope.location = $location; 6 | $scope.section = $location.path().split('/')[1]; 7 | $scope.videos = queue.getQueue(); 8 | $scope.resulttype = 'videos' 9 | 10 | $scope.getLink = function(video, index) { 11 | if ($scope.resulttype == 'playlists') { 12 | return '#/playlist/' + video.yt$playlistId.$t; 13 | } 14 | return '#/view/' + youtube.urlToID(video.media$group.yt$videoid.$t); 15 | } 16 | 17 | $scope.loadMore = function() { 18 | } 19 | 20 | $scope.urlToID = function(url) { 21 | return youtube.urlToID(url); 22 | } 23 | $scope.formatDuration = function(seconds) { 24 | return youtube.formatDuration(seconds); 25 | } 26 | $scope.clearQueue = function() { 27 | queue.emptyQueue(); 28 | $scope.videos = []; 29 | } 30 | }]); -------------------------------------------------------------------------------- /public/scripts/controllers/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The controller used when viewing an individual video. 3 | */ 4 | tooglesApp.controller('ViewCtrl', ['$scope', '$routeParams', '$location', 'youtube', 'socket', function($scope, $routeParams, $location, youtube, socket) { 5 | 6 | $scope.location = $location; // Access $location inside the view. 7 | $scope.showSidebar = true; 8 | $scope.showRelated = false; 9 | $scope.section = $location.path().split('/')[1]; 10 | $scope.videoTab = $scope.section === 'view' ? 'Related' : 'Playlist'; 11 | $scope.streaming_text = 'Send to RaspberryPi' 12 | $scope.streaming = false; 13 | window.viewCallback = function(data) { 14 | if ($scope.section === 'view') { 15 | $scope.video = data.entry; 16 | $scope.video.video_id = $routeParams.id; 17 | $scope.video.embedurl = "http://www.youtube.com/embed/" + $scope.video.video_id + "?autoplay=0&theme=dark&color=white&iv_load_policy=3"; 18 | } else { 19 | var start = $routeParams.start || 0; 20 | $scope.video = data.feed.entry[start]; 21 | $scope.video.video_id = $scope.video.media$group.yt$videoid.$t; 22 | $scope.video.embedurl = "http://www.youtube.com/embed/videoseries?list=" + $routeParams.id + "&autoplay=0&theme=dark&color=white&iv_load_policy=3&index=" + start; 23 | $scope.videos = data.feed.entry; 24 | } 25 | onYouTubeIframeAPIReady($scope.video.video_id, $scope.section); 26 | document.title = $scope.video.title.$t + " | Toogles"; 27 | } 28 | 29 | window.relatedCallback = function(data) { 30 | $scope.videos = data.feed.entry; 31 | } 32 | 33 | $scope.fetchRelated = function() { 34 | if (!$scope.videos) { 35 | youtube.setCallback('relatedCallback'); 36 | youtube.getVideos('related', $routeParams.id); 37 | } 38 | $scope.showRelated = true; 39 | } 40 | 41 | $scope.getLink = function(video, index) { 42 | if ($scope.section == 'view') { 43 | return '#/view/' + youtube.urlToID(video.media$group.yt$videoid.$t); 44 | } else if ($scope.section = 'playlist') { 45 | return '#/playlist/' + $routeParams.id + '/' + index 46 | } 47 | } 48 | 49 | $scope.formatDuration = function(seconds) { 50 | return youtube.formatDuration(seconds); 51 | } 52 | 53 | youtube.setCallback('viewCallback'); 54 | if ($scope.section === 'view') { 55 | youtube.getItem('videos', $routeParams.id); 56 | } else { 57 | youtube.getItem('playlists', $routeParams.id); 58 | } 59 | 60 | started = false; 61 | 62 | var onYouTubeIframeAPIReady = function(id, section) { 63 | var player = new YT.Player('player', { 64 | videoId: id, 65 | events: { 66 | 'onStateChange': function (event) { 67 | if (event.data == 1) { 68 | started = true; 69 | } 70 | if (started && event.data == -1) { 71 | // When a new video is started in an existing player, open up its dedicated page. 72 | if (section === 'view') { 73 | var video_url = event.target.getVideoUrl(); 74 | var video_id = video_url.replace('http://www.youtube.com/watch?v=', '').replace('&feature=player_embedded', ''); 75 | window.location = '#/view/' + video_id; 76 | } else if (section === 'playlist') { 77 | window.location = '#/playlist/' + event.target.getPlaylistId() + '/' + event.target.getPlaylistIndex(); 78 | } 79 | } 80 | } 81 | } 82 | }); 83 | } 84 | }]); 85 | -------------------------------------------------------------------------------- /public/scripts/directives/scroll.js: -------------------------------------------------------------------------------- 1 | tooglesApp.directive('whenScrolled', function() { 2 | return function(scope, elm, attr) { 3 | var raw = elm[0]; 4 | 5 | window.onscroll = function() { 6 | if (window.innerHeight + window.pageYOffset >= document.body.offsetHeight) { 7 | scope.$apply(attr.whenScrolled); 8 | } 9 | }; 10 | }; 11 | }); 12 | -------------------------------------------------------------------------------- /public/scripts/filters/htmlify.js: -------------------------------------------------------------------------------- 1 | tooglesApp.filter('htmlify', function() { 2 | return function(input) { 3 | if (!input) { 4 | return ""; 5 | } 6 | var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; 7 | var out = input.replace(exp,"$1"); 8 | out = out.replace(/\n/g, '
'); 9 | return out; 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /public/scripts/services/localstore.js: -------------------------------------------------------------------------------- 1 | /* Start angularLocalStorage */ 2 | 3 | // var angularLocalStorage = angular.module('LocalStorageModule', []); 4 | 5 | // You should set a prefix to avoid overwriting any local storage variables from the rest of your app 6 | // e.g. angularLocalStorage.constant('prefix', 'youAppName'); 7 | tooglesApp.constant('prefix', 'tooglesApp'); 8 | // Cookie options (usually in case of fallback) 9 | // expiry = Number of days before cookies expire // 0 = Does not expire 10 | // path = The web path the cookie represents 11 | tooglesApp.constant('cookie', { expiry:30, path: '/'}); 12 | 13 | tooglesApp.service('local', [ 14 | '$rootScope', 15 | 'prefix', 16 | 'cookie', 17 | function($rootScope, prefix, cookie) { 18 | 19 | // If there is a prefix set in the config lets use that with an appended period for readability 20 | //var prefix = angularLocalStorage.constant; 21 | if (prefix.substr(-1)!=='.') { 22 | prefix = !!prefix ? prefix + '.' : ''; 23 | } 24 | 25 | // Checks the browser to see if local storage is supported 26 | var browserSupportsLocalStorage = function () { 27 | try { 28 | return ('localStorage' in window && window['localStorage'] !== null); 29 | } catch (e) { 30 | $rootScope.$broadcast('LocalStorageModule.notification.error',e.Description); 31 | return false; 32 | } 33 | }; 34 | 35 | // Directly adds a value to local storage 36 | // If local storage is not available in the browser use cookies 37 | // Example use: localStorageService.add('library','angular'); 38 | var addToLocalStorage = function (key, value) { 39 | 40 | // If this browser does not support local storage use cookies 41 | if (!browserSupportsLocalStorage()) { 42 | $rootScope.$broadcast('LocalStorageModule.notification.warning','LOCAL_STORAGE_NOT_SUPPORTED'); 43 | return false; 44 | } 45 | 46 | // 0 and "" is allowed as a value but let's limit other falsey values like "undefined" 47 | if (!value && value!==0 && value!=="") return false; 48 | 49 | try { 50 | localStorage.setItem(prefix+key, JSON.stringify(value)); 51 | } catch (e) { 52 | $rootScope.$broadcast('LocalStorageModule.notification.error',e.Description); 53 | return false; 54 | } 55 | return true; 56 | }; 57 | 58 | // Directly get a value from local storage 59 | // Example use: localStorageService.get('library'); // returns 'angular' 60 | var getFromLocalStorage = function (key) { 61 | if (!browserSupportsLocalStorage()) { 62 | $rootScope.$broadcast('LocalStorageModule.notification.warning','LOCAL_STORAGE_NOT_SUPPORTED'); 63 | return false; 64 | } 65 | 66 | var item = localStorage.getItem(prefix+key); 67 | if (!item) return null; 68 | return JSON.parse(item); 69 | }; 70 | 71 | // Remove an item from local storage 72 | // Example use: localStorageService.remove('library'); // removes the key/value pair of library='angular' 73 | var removeFromLocalStorage = function (key) { 74 | if (!browserSupportsLocalStorage()) { 75 | $rootScope.$broadcast('LocalStorageModule.notification.warning','LOCAL_STORAGE_NOT_SUPPORTED'); 76 | return false; 77 | } 78 | 79 | try { 80 | localStorage.removeItem(prefix+key); 81 | } catch (e) { 82 | $rootScope.$broadcast('LocalStorageModule.notification.error',e.Description); 83 | return false; 84 | } 85 | return true; 86 | }; 87 | 88 | // Remove all data for this app from local storage 89 | // Example use: localStorageService.clearAll(); 90 | // Should be used mostly for development purposes 91 | var clearAllFromLocalStorage = function () { 92 | 93 | if (!browserSupportsLocalStorage()) { 94 | $rootScope.$broadcast('LocalStorageModule.notification.warning','LOCAL_STORAGE_NOT_SUPPORTED'); 95 | return false; 96 | } 97 | 98 | var prefixLength = prefix.length; 99 | 100 | for (var key in localStorage) { 101 | // Only remove items that are for this app 102 | if (key.substr(0,prefixLength) === prefix) { 103 | try { 104 | removeFromLocalStorage(key.substr(prefixLength)); 105 | } catch (e) { 106 | $rootScope.$broadcast('LocalStorageModule.notification.error',e.Description); 107 | return false; 108 | } 109 | } 110 | } 111 | return true; 112 | }; 113 | 114 | // Checks the browser to see if cookies are supported 115 | var browserSupportsCookies = function() { 116 | try { 117 | return navigator.cookieEnabled || 118 | ("cookie" in document && (document.cookie.length > 0 || 119 | (document.cookie = "test").indexOf.call(document.cookie, "test") > -1)); 120 | } catch (e) { 121 | $rootScope.$broadcast('LocalStorageModule.notification.error',e.Description); 122 | return false; 123 | } 124 | } 125 | 126 | // Directly adds a value to cookies 127 | // Typically used as a fallback is local storage is not available in the browser 128 | // Example use: localStorageService.cookie.add('library','angular'); 129 | var addToCookies = function (key, value) { 130 | 131 | if (typeof value == "undefined") return false; 132 | 133 | if (!browserSupportsCookies()) { 134 | $rootScope.$broadcast('LocalStorageModule.notification.error','COOKIES_NOT_SUPPORTED'); 135 | return false; 136 | } 137 | 138 | try { 139 | var expiry = '', expiryDate = new Date(); 140 | if (value === null) { 141 | cookie.expiry = -1; 142 | value = ''; 143 | } 144 | if (cookie.expiry !== 0) { 145 | expiryDate.setTime(expiryDate.getTime() + (cookie.expiry*24*60*60*1000)); 146 | expiry = "; expires="+expiryDate.toGMTString(); 147 | } 148 | document.cookie = prefix + key + "=" + encodeURIComponent(value) + expiry + "; path="+cookie.path; 149 | } catch (e) { 150 | $rootScope.$broadcast('LocalStorageModule.notification.error',e.Description); 151 | return false; 152 | } 153 | return true; 154 | }; 155 | 156 | // Directly get a value from a cookie 157 | // Example use: localStorageService.cookie.get('library'); // returns 'angular' 158 | var getFromCookies = function (key) { 159 | if (!browserSupportsCookies()) { 160 | $rootScope.$broadcast('LocalStorageModule.notification.error','COOKIES_NOT_SUPPORTED'); 161 | return false; 162 | } 163 | 164 | var cookies = document.cookie.split(';'); 165 | for(var i=0;i < cookies.length;i++) { 166 | var thisCookie = cookies[i]; 167 | while (thisCookie.charAt(0)==' ') { 168 | thisCookie = thisCookie.substring(1,thisCookie.length); 169 | } 170 | if (thisCookie.indexOf(prefix+key+'=') == 0) { 171 | return decodeURIComponent(thisCookie.substring(prefix.length+key.length+1,thisCookie.length)); 172 | } 173 | } 174 | return null; 175 | }; 176 | 177 | var removeFromCookies = function (key) { 178 | addToCookies(key,null); 179 | } 180 | 181 | var clearAllFromCookies = function () { 182 | var thisCookie = null, thisKey = null; 183 | var prefixLength = prefix.length; 184 | var cookies = document.cookie.split(';'); 185 | for(var i=0;i < cookies.length;i++) { 186 | thisCookie = cookies[i]; 187 | while (thisCookie.charAt(0)==' ') { 188 | thisCookie = thisCookie.substring(1,thisCookie.length); 189 | } 190 | key = thisCookie.substring(prefixLength,thisCookie.indexOf('=')); 191 | removeFromCookies(key); 192 | } 193 | } 194 | 195 | 196 | return { 197 | isSupported: browserSupportsLocalStorage, 198 | add: addToLocalStorage, 199 | get: getFromLocalStorage, 200 | remove: removeFromLocalStorage, 201 | clearAll: clearAllFromLocalStorage, 202 | cookie: { 203 | add: addToCookies, 204 | get: getFromCookies, 205 | remove: removeFromCookies, 206 | clearAll: clearAllFromCookies 207 | } 208 | }; 209 | 210 | }]); -------------------------------------------------------------------------------- /public/scripts/services/queue.js: -------------------------------------------------------------------------------- 1 | tooglesApp.service('queue', function(local) { 2 | 3 | var queuedVideos = local.get('queuedVideos') || []; 4 | var hashOfQueued = local.get('hashOfQueued') || {}; 5 | 6 | this.addToQueue = function (video) { 7 | video.queued = true; 8 | hashOfQueued[video.media$group.yt$videoid.$t] = true; 9 | queuedVideos.push(video); 10 | local.add('queuedVideos', queuedVideos); 11 | local.add('hashOfQueued', hashOfQueued); 12 | } 13 | 14 | this.emptyQueue = function () { 15 | local.clearAll('queuedVideos'); 16 | local.clearAll('hashOfQueued'); 17 | queuedVideos = []; 18 | hashOfQueued = {}; 19 | } 20 | 21 | this.getQueue = function () { 22 | return queuedVideos; 23 | } 24 | 25 | var inQueue = function (video) { 26 | return hashOfQueued[video.media$group.yt$videoid.$t] === true; 27 | } 28 | this.getVideo = function(video_id) { 29 | for (var i = queuedVideos.length -1; i >= 0; i--){ 30 | if (video_id === queuedVideos[i].media$group.yt$videoid.$t){ 31 | return queuedVideos[i]; 32 | } 33 | } 34 | } 35 | this.removeFromQueue = function (video) { 36 | if (!inQueue(video)) { 37 | return; 38 | } 39 | 40 | for(var i = queuedVideos.length -1; i >= 0; i--) { 41 | if (video.media$group.yt$videoid.$t === queuedVideos[i].media$group.yt$videoid.$t) { 42 | queuedVideos.splice(i, 1); 43 | break; 44 | } 45 | } 46 | delete hashOfQueued[video.media$group.yt$videoid.$t]; 47 | local.remove('hashOfQueued', video.media$group.yt$videoid.$t); 48 | } 49 | 50 | }); -------------------------------------------------------------------------------- /public/scripts/services/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Services */ 4 | 5 | 6 | // Demonstrate how to register services 7 | // In this case it is a simple value service. 8 | tooglesApp.service('socket', ['$rootScope', function($rootScope) { 9 | var socket = io.connect(); 10 | return { 11 | on: function (eventName, callback) { 12 | socket.on(eventName, function () { 13 | var args = arguments; 14 | $rootScope.$apply(function () { 15 | callback.apply(socket, args); 16 | }); 17 | }); 18 | }, 19 | emit: function (eventName, data, callback) { 20 | socket.emit(eventName, data, function () { 21 | var args = arguments; 22 | $rootScope.$apply(function () { 23 | if (callback) { 24 | callback.apply(socket, args); 25 | } 26 | }); 27 | }) 28 | } 29 | }; 30 | }]); 31 | -------------------------------------------------------------------------------- /public/scripts/services/youtube.js: -------------------------------------------------------------------------------- 1 | tooglesApp.service('youtube', ['$http', function($http) { 2 | 3 | var urlBase = "https://gdata.youtube.com/feeds/api/"; 4 | 5 | var offset = 1; 6 | var count = 24; 7 | var callback = 'searchCallback'; 8 | var duration = false; 9 | var time = false; 10 | var orderBy = false; 11 | var searchType = 'videos'; 12 | 13 | this.setPage = function(page) { 14 | offset = page * count + 1; 15 | } 16 | this.setSort = function(sort) { 17 | orderBy = sort; 18 | } 19 | this.setTime = function(when) { 20 | time = when; 21 | } 22 | this.setDuration = function(length) { 23 | duration = length; 24 | } 25 | this.setType = function(type) { 26 | searchType = type; 27 | } 28 | this.setCallback = function(fn) { 29 | callback = fn; 30 | } 31 | 32 | this.getItem = function(type, id) { 33 | var url = 'https://gdata.youtube.com/feeds/api/' + type + '/' + id + '?safeSearch=none&v=2&alt=json&callback=' + callback; 34 | $http.jsonp(url); 35 | } 36 | 37 | this.getVideos = function(type, query) { 38 | query = encodeURIComponent(query); 39 | if (type === 'related') { 40 | // All videos by a user 41 | var url = urlBase + 'videos/' + query + '/related?&v=2&alt=json&callback=' + callback; 42 | 43 | } else if (type === 'user') { 44 | // All videos by a user 45 | var url = urlBase + 'users/' + query + '/uploads?start-index=' + offset + '&max-results=' + count + '&v=2&alt=json&callback=' + callback; 46 | 47 | } else if (type === 'user_favorites') { 48 | // All videos by a user 49 | var url = urlBase + 'users/' + query + '/favorites?start-index=' + offset + '&max-results=' + count + '&v=2&alt=json&callback=' + callback; 50 | 51 | } else if (type === 'user_subscriptions') { 52 | // All videos by a user 53 | var url = urlBase + 'users/' + query + '/newsubscriptionvideos?start-index=' + offset + '&max-results=' + count + '&v=2&alt=json&callback=' + callback; 54 | 55 | } else if (type === 'user_playlists') { 56 | // All videos by a user 57 | var url = urlBase + 'users/' + query + '/playlists?start-index=' + offset + '&max-results=' + count + '&v=2&alt=json&callback=' + callback; 58 | 59 | } else if (type === 'category') { 60 | // All videos within a category 61 | var url = urlBase + "standardfeeds/most_viewed_" + query + "?time=today&start-index=" + offset + "&max-results=" + count + "&safeSearch=none&v=2&alt=json&callback=" + callback; 62 | 63 | } else if (type === 'search') { 64 | // A search query for videos 65 | path = 'videos'; 66 | if (searchType == 'playlists') { 67 | path = 'playlists/snippets'; 68 | } 69 | var url = urlBase + path + "?q=" + query + "&start-index=" + offset + "&max-results=" + count + "&safeSearch=none&v=2&alt=json&callback=" + callback; 70 | if (time) { 71 | url += '&time=' + time; 72 | } 73 | if (duration) { 74 | url += '&duration=' + duration; 75 | } 76 | if (orderBy && searchType != 'playlists') { 77 | url += '&orderby=' + orderBy; 78 | } 79 | } else { 80 | // Most popular recent videos 81 | var url = urlBase + "standardfeeds/most_viewed?time=today&start-index=" + offset + "&max-results=" + count + "&safeSearch=none&v=2&alt=json&callback=" + callback; 82 | } 83 | $http.jsonp(url); 84 | } 85 | 86 | // Take a URL with an ID in it and grab the ID out of it. Helper function for YouTube URLs. 87 | this.urlToID = function(url) { 88 | if (url) { 89 | var parts = url.split('/'); 90 | if (parts.length === 1) { 91 | parts = url.split(':'); // Some URLs are separated with : instead of / 92 | } 93 | return parts.pop(); 94 | } 95 | } 96 | 97 | this.formatDuration = function(seconds) { 98 | sec_numb = parseInt(seconds); 99 | var hours = Math.floor(sec_numb / 3600); 100 | var minutes = Math.floor((sec_numb - (hours * 3600)) / 60); 101 | var seconds = sec_numb - (hours * 3600) - (minutes * 60); 102 | 103 | if (minutes < 10 && hours !== 0) { 104 | minutes = "0" + minutes; 105 | } 106 | if (seconds < 10) { 107 | seconds = "0" + seconds; 108 | } 109 | var time = minutes+':'+seconds; 110 | if (hours !== 0) { 111 | time = hours + ":" + time; 112 | } 113 | return time; 114 | } 115 | }]); 116 | -------------------------------------------------------------------------------- /public/scripts/vendor/angular-sanitize.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.0.5 3 | (c) 2010-2012 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(I,g){'use strict';function i(a){var d={},a=a.split(","),b;for(b=0;b=0;e--)if(f[e]==b)break;if(e>=0){for(c=f.length-1;c>=e;c--)d.end&&d.end(f[c]);f.length= 7 | e}}var c,h,f=[],j=a;for(f.last=function(){return f[f.length-1]};a;){h=!0;if(!f.last()||!q[f.last()]){if(a.indexOf("<\!--")===0)c=a.indexOf("--\>"),c>=0&&(d.comment&&d.comment(a.substring(4,c)),a=a.substring(c+3),h=!1);else if(B.test(a)){if(c=a.match(r))a=a.substring(c[0].length),c[0].replace(r,e),h=!1}else if(C.test(a)&&(c=a.match(s)))a=a.substring(c[0].length),c[0].replace(s,b),h=!1;h&&(c=a.indexOf("<"),h=c<0?a:a.substring(0,c),a=c<0?"":a.substring(c),d.chars&&d.chars(k(h)))}else a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+ 8 | f.last()+"[^>]*>","i"),function(b,a){a=a.replace(D,"$1").replace(E,"$1");d.chars&&d.chars(k(a));return""}),e("",f.last());if(a==j)throw"Parse Error: "+a;j=a}e()}function k(a){l.innerHTML=a.replace(//g,">")}function u(a){var d=!1,b=g.bind(a,a.push);return{start:function(a,c,h){a=g.lowercase(a);!d&&q[a]&&(d=a);!d&&v[a]== 9 | !0&&(b("<"),b(a),g.forEach(c,function(a,c){var e=g.lowercase(c);if(G[e]==!0&&(w[e]!==!0||a.match(H)))b(" "),b(c),b('="'),b(t(a)),b('"')}),b(h?"/>":">"))},end:function(a){a=g.lowercase(a);!d&&v[a]==!0&&(b(""));a==d&&(d=!1)},chars:function(a){d||b(t(a))}}}var s=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,r=/^<\s*\/\s*([\w:-]+)[^>]*>/,A=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,C=/^/g, 10 | E=//g,H=/^((ftp|https?):\/\/|mailto:|#)/,F=/([^\#-~| |!])/g,p=i("area,br,col,hr,img,wbr"),x=i("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),y=i("rp,rt"),o=g.extend({},y,x),m=g.extend({},x,i("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),n=g.extend({},y,i("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")), 11 | q=i("script,style"),v=g.extend({},p,m,n,o),w=i("background,cite,href,longdesc,src,usemap"),G=g.extend({},w,i("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,span,start,summary,target,title,type,valign,value,vspace,width")),l=document.createElement("pre");g.module("ngSanitize",[]).value("$sanitize",function(a){var d=[]; 12 | z(a,u(d));return d.join("")});g.module("ngSanitize").directive("ngBindHtml",["$sanitize",function(a){return function(d,b,e){b.addClass("ng-binding").data("$binding",e.ngBindHtml);d.$watch(e.ngBindHtml,function(c){c=a(c);b.html(c||"")})}}]);g.module("ngSanitize").filter("linky",function(){var a=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/,d=/^mailto:/;return function(b){if(!b)return b;for(var e=b,c=[],h=u(c),f,g;b=e.match(a);)f=b[0],b[2]==b[3]&&(f="mailto:"+f),g=b.index, 13 | h.chars(e.substr(0,g)),h.start("a",{href:f}),h.chars(b[0].replace(d,"")),h.end("a"),e=e.substring(g+b[0].length);h.chars(e);return c.join("")}})})(window,window.angular); 14 | -------------------------------------------------------------------------------- /public/scripts/vendor/es5-shim.min.js: -------------------------------------------------------------------------------- 1 | (function(n){"function"==typeof define?define(n):"function"==typeof YUI?YUI.add("es5",n):n()})(function(){function n(a){try{return Object.defineProperty(a,"sentinel",{}),"sentinel"in a}catch(c){}}Function.prototype.bind||(Function.prototype.bind=function(a){var c=this;if("function"!=typeof c)throw new TypeError("Function.prototype.bind called on incompatible "+c);var b=q.call(arguments,1),d=function(){if(this instanceof d){var e=function(){};e.prototype=c.prototype;var e=new e,g=c.apply(e,b.concat(q.call(arguments))); 2 | return Object(g)===g?g:e}return c.apply(a,b.concat(q.call(arguments)))};return d});var l=Function.prototype.call,f=Object.prototype,q=Array.prototype.slice,m=l.bind(f.toString),h=l.bind(f.hasOwnProperty),u,v,r,s,p;if(p=h(f,"__defineGetter__"))u=l.bind(f.__defineGetter__),v=l.bind(f.__defineSetter__),r=l.bind(f.__lookupGetter__),s=l.bind(f.__lookupSetter__);Array.isArray||(Array.isArray=function(a){return m(a)=="[object Array]"});Array.prototype.forEach||(Array.prototype.forEach=function(a,c){var b= 3 | i(this),d=-1,e=b.length>>>0;if(m(a)!="[object Function]")throw new TypeError;for(;++d>>0,e=Array(d);if(m(a)!="[object Function]")throw new TypeError(a+" is not a function");for(var g=0;g>>0,e=[],g;if(m(a)!="[object Function]")throw new TypeError(a+ 4 | " is not a function");for(var o=0;o>>0;if(m(a)!="[object Function]")throw new TypeError(a+" is not a function");for(var e=0;e>>0;if(m(a)!="[object Function]")throw new TypeError(a+" is not a function");for(var e= 5 | 0;e>>0;if(m(a)!="[object Function]")throw new TypeError(a+" is not a function");if(!b&&arguments.length==1)throw new TypeError("reduce of empty array with no initial value");var d=0,e;if(arguments.length>=2)e=arguments[1];else{do{if(d in c){e=c[d++];break}if(++d>=b)throw new TypeError("reduce of empty array with no initial value");}while(1)}for(;d>>0;if(m(a)!="[object Function]")throw new TypeError(a+" is not a function");if(!b&&arguments.length==1)throw new TypeError("reduceRight of empty array with no initial value");var d,b=b-1;if(arguments.length>=2)d=arguments[1];else{do{if(b in c){d=c[b--];break}if(--b<0)throw new TypeError("reduceRight of empty array with no initial value");}while(1)}do b in this&& 7 | (d=a.call(void 0,d,c[b],b,c));while(b--);return d});Array.prototype.indexOf||(Array.prototype.indexOf=function(a){var c=i(this),b=c.length>>>0;if(!b)return-1;var d=0;arguments.length>1&&(d=w(arguments[1]));for(d=d>=0?d:Math.max(0,b+d);d>>0;if(!b)return-1;var d=b-1;arguments.length>1&&(d=Math.min(d,w(arguments[1])));for(d=d>=0?d:b-Math.abs(d);d>=0;d--)if(d in 8 | c&&a===c[d])return d;return-1});Object.getPrototypeOf||(Object.getPrototypeOf=function(a){return a.__proto__||(a.constructor?a.constructor.prototype:f)});Object.getOwnPropertyDescriptor||(Object.getOwnPropertyDescriptor=function(a,c){if(typeof a!="object"&&typeof a!="function"||a===null)throw new TypeError("Object.getOwnPropertyDescriptor called on a non-object: "+a);if(h(a,c)){var b={enumerable:true,configurable:true};if(p){var d=a.__proto__;a.__proto__=f;var e=r(a,c),g=s(a,c);a.__proto__=d;if(e|| 9 | g){if(e)b.get=e;if(g)b.set=g;return b}}b.value=a[c];return b}});Object.getOwnPropertyNames||(Object.getOwnPropertyNames=function(a){return Object.keys(a)});Object.create||(Object.create=function(a,c){var b;if(a===null)b={__proto__:null};else{if(typeof a!="object")throw new TypeError("typeof prototype["+typeof a+"] != 'object'");b=function(){};b.prototype=a;b=new b;b.__proto__=a}c!==void 0&&Object.defineProperties(b,c);return b});if(Object.defineProperty){var l=n({}),z="undefined"==typeof document|| 10 | n(document.createElement("div"));if(!l||!z)var t=Object.defineProperty}if(!Object.defineProperty||t)Object.defineProperty=function(a,c,b){if(typeof a!="object"&&typeof a!="function"||a===null)throw new TypeError("Object.defineProperty called on non-object: "+a);if(typeof b!="object"&&typeof b!="function"||b===null)throw new TypeError("Property description must be an object: "+b);if(t)try{return t.call(Object,a,c,b)}catch(d){}if(h(b,"value"))if(p&&(r(a,c)||s(a,c))){var e=a.__proto__;a.__proto__=f; 11 | delete a[c];a[c]=b.value;a.__proto__=e}else a[c]=b.value;else{if(!p)throw new TypeError("getters & setters can not be defined on this javascript engine");h(b,"get")&&u(a,c,b.get);h(b,"set")&&v(a,c,b.set)}return a};Object.defineProperties||(Object.defineProperties=function(a,c){for(var b in c)h(c,b)&&b!="__proto__"&&Object.defineProperty(a,b,c[b]);return a});Object.seal||(Object.seal=function(a){return a});Object.freeze||(Object.freeze=function(a){return a});try{Object.freeze(function(){})}catch(E){Object.freeze= 12 | function(a){return function(c){return typeof c=="function"?c:a(c)}}(Object.freeze)}Object.preventExtensions||(Object.preventExtensions=function(a){return a});Object.isSealed||(Object.isSealed=function(){return false});Object.isFrozen||(Object.isFrozen=function(){return false});Object.isExtensible||(Object.isExtensible=function(a){if(Object(a)!==a)throw new TypeError;for(var c="";h(a,c);)c=c+"?";a[c]=true;var b=h(a,c);delete a[c];return b});if(!Object.keys){var x=!0,y="toString toLocaleString valueOf hasOwnProperty isPrototypeOf propertyIsEnumerable constructor".split(" "), 13 | A=y.length,j;for(j in{toString:null})x=!1;Object.keys=function(a){if(typeof a!="object"&&typeof a!="function"||a===null)throw new TypeError("Object.keys called on a non-object");var c=[],b;for(b in a)h(a,b)&&c.push(b);if(x)for(b=0;b9999?"+":"")+("00000"+Math.abs(d)).slice(0<=d&&d<=9999?-4:-6);for(c=a.length;c--;){b=a[c];b<10&&(a[c]="0"+b)}return d+"-"+a.slice(0,2).join("-")+"T"+a.slice(2).join(":")+"."+("000"+this.getUTCMilliseconds()).slice(-3)+"Z"};Date.now||(Date.now=function(){return(new Date).getTime()});Date.prototype.toJSON||(Date.prototype.toJSON=function(){if(typeof this.toISOString!= 15 | "function")throw new TypeError("toISOString property is not callable");return this.toISOString()});if(!Date.parse||864E13!==Date.parse("+275760-09-13T00:00:00.000Z"))Date=function(a){var c=function g(b,c,d,f,h,i,j){var k=arguments.length;if(this instanceof a){k=k==1&&""+b===b?new a(g.parse(b)):k>=7?new a(b,c,d,f,h,i,j):k>=6?new a(b,c,d,f,h,i):k>=5?new a(b,c,d,f,h):k>=4?new a(b,c,d,f):k>=3?new a(b,c,d):k>=2?new a(b,c):k>=1?new a(b):new a;k.constructor=g;return k}return a.apply(this,arguments)},b=RegExp("^(\\d{4}|[+-]\\d{6})(?:-(\\d{2})(?:-(\\d{2})(?:T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{3}))?)?(?:Z|(?:([-+])(\\d{2}):(\\d{2})))?)?)?)?$"), 16 | d;for(d in a)c[d]=a[d];c.now=a.now;c.UTC=a.UTC;c.prototype=a.prototype;c.prototype.constructor=c;c.parse=function(c){var d=b.exec(c);if(d){d.shift();for(var f=1;f<7;f++){d[f]=+(d[f]||(f<3?1:0));f==1&&d[f]--}var h=+d.pop(),i=+d.pop(),j=d.pop(),f=0;if(j){if(i>23||h>59)return NaN;f=(i*60+h)*6E4*(j=="+"?-1:1)}h=+d[0];if(0<=h&&h<=99){d[0]=h+400;return a.UTC.apply(this,d)+f-126227808E5}return a.UTC.apply(this,d)+f}return a.parse.apply(this,arguments)};return c}(Date);j="\t\n\x0B\u000c\r \u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\ufeff"; 17 | if(!String.prototype.trim||j.trim()){j="["+j+"]";var B=RegExp("^"+j+j+"*"),C=RegExp(j+j+"*$");String.prototype.trim=function(){if(this===void 0||this===null)throw new TypeError("can't convert "+this+" to object");return(""+this).replace(B,"").replace(C,"")}}var w=function(a){a=+a;a!==a?a=0:a!==0&&(a!==1/0&&a!==-(1/0))&&(a=(a>0||-1)*Math.floor(Math.abs(a)));return a},D="a"!="a"[0],i=function(a){if(a==null)throw new TypeError("can't convert "+a+" to object");return D&&typeof a=="string"&&a?a.split(""): 18 | Object(a)}}); -------------------------------------------------------------------------------- /public/scripts/vendor/json3.min.js: -------------------------------------------------------------------------------- 1 | /*! JSON v3.2.3 | http://bestiejs.github.com/json3 | Copyright 2012, Kit Cambridge | http://kit.mit-license.org */ 2 | ;(function(){var e=void 0,i=!0,k=null,l={}.toString,m,n,o="function"===typeof define&&define.c,q="object"==typeof exports&&exports,r='{"A":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}',t,u,x,y,z,C,D,E,F,G,H,I,J,K,O,P=new Date(-3509827334573292),Q,R,S;try{P=-109252==P.getUTCFullYear()&&0===P.getUTCMonth()&&1==P.getUTCDate()&&10==P.getUTCHours()&&37==P.getUTCMinutes()&&6==P.getUTCSeconds()&&708==P.getUTCMilliseconds()}catch(T){} 3 | P||(Q=Math.floor,R=[0,31,59,90,120,151,181,212,243,273,304,334],S=function(b,c){return R[c]+365*(b-1970)+Q((b-1969+(c=+(1-1?u[d]:d<" "?"\\u00"+x(2,d.charCodeAt(0).toString(16)):d);return c+'"'},z=function(b,c,a,d,j,f,p){var g=c[b],h,s,v,w,L,M,N,A,B;if(typeof g=="object"&&g){h=l.call(g);if(h=="[object Date]"&&!m.call(g,"toJSON"))if(g> 9 | -1/0&&g<1/0){if(S){v=Q(g/864E5);for(h=Q(v/365.2425)+1970-1;S(h+1,0)<=v;h++);for(s=Q((v-S(h,0))/30.42);S(h,s+1)<=v;s++);v=1+v-S(h,s);w=(g%864E5+864E5)%864E5;L=Q(w/36E5)%24;M=Q(w/6E4)%60;N=Q(w/1E3)%60;w=w%1E3}else{h=g.getUTCFullYear();s=g.getUTCMonth();v=g.getUTCDate();L=g.getUTCHours();M=g.getUTCMinutes();N=g.getUTCSeconds();w=g.getUTCMilliseconds()}g=(h<=0||h>=1E4?(h<0?"-":"+")+x(6,h<0?-h:h):x(4,h))+"-"+x(2,s+1)+"-"+x(2,v)+"T"+x(2,L)+":"+x(2,M)+":"+x(2,N)+"."+x(3,w)+"Z"}else g=k;else if(typeof g.toJSON== 10 | "function"&&(h!="[object Number]"&&h!="[object String]"&&h!="[object Array]"||m.call(g,"toJSON")))g=g.toJSON(b)}a&&(g=a.call(c,b,g));if(g===k)return"null";h=l.call(g);if(h=="[object Boolean]")return""+g;if(h=="[object Number]")return g>-1/0&&g<1/0?""+g:"null";if(h=="[object String]")return y(g);if(typeof g=="object"){for(b=p.length;b--;)if(p[b]===g)throw TypeError();p.push(g);A=[];c=f;f=f+j;if(h=="[object Array]"){s=0;for(b=g.length;s0){d="";for(a>10&&(a=10);d.length-1)K++;else{if("{}[]:,".indexOf(a)>-1){K++;return a}if(a=='"'){d="@";for(K++;K-1){d=d+E[a];K++}else if(a=="u"){j=++K;for(f=K+4;K="0"&&a<="9"||a>="a"&&a<="f"||a>="A"&&a<="F"||F()}d=d+D("0x"+b.slice(j,K))}else F()}else{if(a=='"')break;d=d+a;K++}}if(b.charAt(K)=='"'){K++;return d}}else{j=K;if(a=="-"){p=i;a=b.charAt(++K)}if(a>="0"&&a<="9"){for(a=="0"&&(a=b.charAt(K+1),a>="0"&&a<="9")&&F();K="0"&&a<="9");K++);if(b.charAt(K)=="."){for(f=++K;f="0"&&a<="9");f++);f==K&&F();K=f}a=b.charAt(K);if(a=="e"||a=="E"){a=b.charAt(++K);(a=="+"||a=="-")&&K++;for(f=K;f="0"&&a<="9");f++);f==K&&F();K=f}return+b.slice(j,K)}p&&F();if(b.slice(K,K+4)=="true"){K=K+4;return i}if(b.slice(K,K+5)=="false"){K=K+5;return false}if(b.slice(K,K+4)=="null"){K=K+4;return k}}F()}}return"$"},H=function(b){var c,a;b=="$"&&F();if(typeof b=="string"){if(b.charAt(0)=="@")return b.slice(1);if(b=="["){for(c=[];;a||(a=i)){b=G();if(b=="]")break;if(a)if(b== 15 | ","){b=G();b=="]"&&F()}else F();b==","&&F();c.push(H(b))}return c}if(b=="{"){for(c={};;a||(a=i)){b=G();if(b=="}")break;if(a)if(b==","){b=G();b=="}"&&F()}else F();(b==","||typeof b!="string"||b.charAt(0)!="@"||G()!=":")&&F();c[b.slice(1)]=H(G())}return c}F()}return b},J=function(b,c,a){a=I(b,c,a);a===e?delete b[c]:b[c]=a},I=function(b,c,a){var d=b[c],j;if(typeof d=="object"&&d)if(l.call(d)=="[object Array]")for(j=d.length;j--;)J(d,j,a);else n(d,function(b){J(d,b,a)});return a.call(b,c,d)},q.parse= 16 | function(b,c){K=0;O=b;var a=H(G());G()!="$"&&F();K=O=k;return c&&l.call(c)=="[object Function]"?I((P={},P[""]=a,P),"",c):a})}; 17 | }()); -------------------------------------------------------------------------------- /public/styles/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /* Font Awesome 3.0 2 | the iconic font designed for use with Twitter Bootstrap 3 | ------------------------------------------------------- 4 | The full suite of pictographic icons, examples, and documentation 5 | can be found at: http://fortawesome.github.com/Font-Awesome/ 6 | 7 | License 8 | ------------------------------------------------------- 9 | • The Font Awesome font is licensed under the SIL Open Font License - http://scripts.sil.org/OFL 10 | • Font Awesome CSS, LESS, and SASS files are licensed under the MIT License - 11 | http://opensource.org/licenses/mit-license.html 12 | • The Font Awesome pictograms are licensed under the CC BY 3.0 License - http://creativecommons.org/licenses/by/3.0/ 13 | • Attribution is no longer required in Font Awesome 3.0, but much appreciated: 14 | "Font Awesome by Dave Gandy - http://fortawesome.github.com/Font-Awesome" 15 | 16 | Contact 17 | ------------------------------------------------------- 18 | Email: dave@davegandy.com 19 | Twitter: http://twitter.com/fortaweso_me 20 | Work: Lead Product Designer @ http://kyruus.com 21 | 22 | */ 23 | 24 | @font-face { 25 | font-family:'FontAwesome'; 26 | src:url('font/fontawesome-webfont.eot'); 27 | src:url('font/fontawesome-webfont.eot?#iefix') format('embedded-opentype'), 28 | url('font/fontawesome-webfont.woff') format('woff'), 29 | url('font/fontawesome-webfont.ttf') format('truetype'); 30 | font-weight:normal; 31 | font-style:normal 32 | } 33 | 34 | [class^="icon-"],[class*=" icon-"]{font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;display:inline;width:auto;height:auto;line-height:normal;vertical-align:baseline;background-image:none!important;background-position:0 0;background-repeat:repeat}[class^="icon-"]:before,[class*=" icon-"]:before{text-decoration:inherit;display:inline-block;speak:none}a [class^="icon-"],a [class*=" icon-"]{display:inline-block}.icon-large:before{vertical-align:-10%;font-size:1.3333333333333333em}.btn [class^="icon-"],.nav [class^="icon-"],.btn [class*=" icon-"],.nav [class*=" icon-"]{display:inline;line-height:.6em}.btn [class^="icon-"].icon-spin,.nav [class^="icon-"].icon-spin,.btn [class*=" icon-"].icon-spin,.nav [class*=" icon-"].icon-spin{display:inline-block}li [class^="icon-"],li [class*=" icon-"]{display:inline-block;width:1.25em;text-align:center}li [class^="icon-"].icon-large,li [class*=" icon-"].icon-large{width:1.5625em}ul.icons{list-style-type:none;text-indent:-0.75em}ul.icons li [class^="icon-"],ul.icons li [class*=" icon-"]{width:.75em}.icon-muted{color:#eee}.icon-border{border:solid 1px #eee;padding:.2em .25em .15em;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.icon-2x{font-size:2em}.icon-2x.icon-border{border-width:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.icon-3x{font-size:3em}.icon-3x.icon-border{border-width:3px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.icon-4x{font-size:4em}.icon-4x.icon-border{border-width:4px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.pull-right{float:right}.pull-left{float:left}[class^="icon-"].pull-left,[class*=" icon-"].pull-left{margin-right:.35em}[class^="icon-"].pull-right,[class*=" icon-"].pull-right{margin-left:.35em}.btn [class^="icon-"].pull-left.icon-2x,.btn [class*=" icon-"].pull-left.icon-2x,.btn [class^="icon-"].pull-right.icon-2x,.btn [class*=" icon-"].pull-right.icon-2x{margin-top:.35em}.btn [class^="icon-"].icon-spin.icon-large,.btn [class*=" icon-"].icon-spin.icon-large{height:.75em}.btn.btn-small [class^="icon-"].pull-left.icon-2x,.btn.btn-small [class*=" icon-"].pull-left.icon-2x,.btn.btn-small [class^="icon-"].pull-right.icon-2x,.btn.btn-small [class*=" icon-"].pull-right.icon-2x{margin-top:.45em}.btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x,.btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-top:.2em}.icon-spin{display:inline-block;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.icon-glass:before{content:"\f000"}.icon-music:before{content:"\f001"}.icon-search:before{content:"\f002"}.icon-envelope:before{content:"\f003"}.icon-heart:before{content:"\f004"}.icon-star:before{content:"\f005"}.icon-star-empty:before{content:"\f006"}.icon-user:before{content:"\f007"}.icon-film:before{content:"\f008"}.icon-th-large:before{content:"\f009"}.icon-th:before{content:"\f00a"}.icon-th-list:before{content:"\f00b"}.icon-ok:before{content:"\f00c"}.icon-remove:before{content:"\f00d"}.icon-zoom-in:before{content:"\f00e"}.icon-zoom-out:before{content:"\f010"}.icon-off:before{content:"\f011"}.icon-signal:before{content:"\f012"}.icon-cog:before{content:"\f013"}.icon-trash:before{content:"\f014"}.icon-home:before{content:"\f015"}.icon-file:before{content:"\f016"}.icon-time:before{content:"\f017"}.icon-road:before{content:"\f018"}.icon-download-alt:before{content:"\f019"}.icon-download:before{content:"\f01a"}.icon-upload:before{content:"\f01b"}.icon-inbox:before{content:"\f01c"}.icon-play-circle:before{content:"\f01d"}.icon-repeat:before{content:"\f01e"}.icon-refresh:before{content:"\f021"}.icon-list-alt:before{content:"\f022"}.icon-lock:before{content:"\f023"}.icon-flag:before{content:"\f024"}.icon-headphones:before{content:"\f025"}.icon-volume-off:before{content:"\f026"}.icon-volume-down:before{content:"\f027"}.icon-volume-up:before{content:"\f028"}.icon-qrcode:before{content:"\f029"}.icon-barcode:before{content:"\f02a"}.icon-tag:before{content:"\f02b"}.icon-tags:before{content:"\f02c"}.icon-book:before{content:"\f02d"}.icon-bookmark:before{content:"\f02e"}.icon-print:before{content:"\f02f"}.icon-camera:before{content:"\f030"}.icon-font:before{content:"\f031"}.icon-bold:before{content:"\f032"}.icon-italic:before{content:"\f033"}.icon-text-height:before{content:"\f034"}.icon-text-width:before{content:"\f035"}.icon-align-left:before{content:"\f036"}.icon-align-center:before{content:"\f037"}.icon-align-right:before{content:"\f038"}.icon-align-justify:before{content:"\f039"}.icon-list:before{content:"\f03a"}.icon-indent-left:before{content:"\f03b"}.icon-indent-right:before{content:"\f03c"}.icon-facetime-video:before{content:"\f03d"}.icon-picture:before{content:"\f03e"}.icon-pencil:before{content:"\f040"}.icon-map-marker:before{content:"\f041"}.icon-adjust:before{content:"\f042"}.icon-tint:before{content:"\f043"}.icon-edit:before{content:"\f044"}.icon-share:before{content:"\f045"}.icon-check:before{content:"\f046"}.icon-move:before{content:"\f047"}.icon-step-backward:before{content:"\f048"}.icon-fast-backward:before{content:"\f049"}.icon-backward:before{content:"\f04a"}.icon-play:before{content:"\f04b"}.icon-pause:before{content:"\f04c"}.icon-stop:before{content:"\f04d"}.icon-forward:before{content:"\f04e"}.icon-fast-forward:before{content:"\f050"}.icon-step-forward:before{content:"\f051"}.icon-eject:before{content:"\f052"}.icon-chevron-left:before{content:"\f053"}.icon-chevron-right:before{content:"\f054"}.icon-plus-sign:before{content:"\f055"}.icon-minus-sign:before{content:"\f056"}.icon-remove-sign:before{content:"\f057"}.icon-ok-sign:before{content:"\f058"}.icon-question-sign:before{content:"\f059"}.icon-info-sign:before{content:"\f05a"}.icon-screenshot:before{content:"\f05b"}.icon-remove-circle:before{content:"\f05c"}.icon-ok-circle:before{content:"\f05d"}.icon-ban-circle:before{content:"\f05e"}.icon-arrow-left:before{content:"\f060"}.icon-arrow-right:before{content:"\f061"}.icon-arrow-up:before{content:"\f062"}.icon-arrow-down:before{content:"\f063"}.icon-share-alt:before{content:"\f064"}.icon-resize-full:before{content:"\f065"}.icon-resize-small:before{content:"\f066"}.icon-plus:before{content:"\f067"}.icon-minus:before{content:"\f068"}.icon-asterisk:before{content:"\f069"}.icon-exclamation-sign:before{content:"\f06a"}.icon-gift:before{content:"\f06b"}.icon-leaf:before{content:"\f06c"}.icon-fire:before{content:"\f06d"}.icon-eye-open:before{content:"\f06e"}.icon-eye-close:before{content:"\f070"}.icon-warning-sign:before{content:"\f071"}.icon-plane:before{content:"\f072"}.icon-calendar:before{content:"\f073"}.icon-random:before{content:"\f074"}.icon-comment:before{content:"\f075"}.icon-magnet:before{content:"\f076"}.icon-chevron-up:before{content:"\f077"}.icon-chevron-down:before{content:"\f078"}.icon-retweet:before{content:"\f079"}.icon-shopping-cart:before{content:"\f07a"}.icon-folder-close:before{content:"\f07b"}.icon-folder-open:before{content:"\f07c"}.icon-resize-vertical:before{content:"\f07d"}.icon-resize-horizontal:before{content:"\f07e"}.icon-bar-chart:before{content:"\f080"}.icon-twitter-sign:before{content:"\f081"}.icon-facebook-sign:before{content:"\f082"}.icon-camera-retro:before{content:"\f083"}.icon-key:before{content:"\f084"}.icon-cogs:before{content:"\f085"}.icon-comments:before{content:"\f086"}.icon-thumbs-up:before{content:"\f087"}.icon-thumbs-down:before{content:"\f088"}.icon-star-half:before{content:"\f089"}.icon-heart-empty:before{content:"\f08a"}.icon-signout:before{content:"\f08b"}.icon-linkedin-sign:before{content:"\f08c"}.icon-pushpin:before{content:"\f08d"}.icon-external-link:before{content:"\f08e"}.icon-signin:before{content:"\f090"}.icon-trophy:before{content:"\f091"}.icon-github-sign:before{content:"\f092"}.icon-upload-alt:before{content:"\f093"}.icon-lemon:before{content:"\f094"}.icon-phone:before{content:"\f095"}.icon-check-empty:before{content:"\f096"}.icon-bookmark-empty:before{content:"\f097"}.icon-phone-sign:before{content:"\f098"}.icon-twitter:before{content:"\f099"}.icon-facebook:before{content:"\f09a"}.icon-github:before{content:"\f09b"}.icon-unlock:before{content:"\f09c"}.icon-credit-card:before{content:"\f09d"}.icon-rss:before{content:"\f09e"}.icon-hdd:before{content:"\f0a0"}.icon-bullhorn:before{content:"\f0a1"}.icon-bell:before{content:"\f0a2"}.icon-certificate:before{content:"\f0a3"}.icon-hand-right:before{content:"\f0a4"}.icon-hand-left:before{content:"\f0a5"}.icon-hand-up:before{content:"\f0a6"}.icon-hand-down:before{content:"\f0a7"}.icon-circle-arrow-left:before{content:"\f0a8"}.icon-circle-arrow-right:before{content:"\f0a9"}.icon-circle-arrow-up:before{content:"\f0aa"}.icon-circle-arrow-down:before{content:"\f0ab"}.icon-globe:before{content:"\f0ac"}.icon-wrench:before{content:"\f0ad"}.icon-tasks:before{content:"\f0ae"}.icon-filter:before{content:"\f0b0"}.icon-briefcase:before{content:"\f0b1"}.icon-fullscreen:before{content:"\f0b2"}.icon-group:before{content:"\f0c0"}.icon-link:before{content:"\f0c1"}.icon-cloud:before{content:"\f0c2"}.icon-beaker:before{content:"\f0c3"}.icon-cut:before{content:"\f0c4"}.icon-copy:before{content:"\f0c5"}.icon-paper-clip:before{content:"\f0c6"}.icon-save:before{content:"\f0c7"}.icon-sign-blank:before{content:"\f0c8"}.icon-reorder:before{content:"\f0c9"}.icon-list-ul:before{content:"\f0ca"}.icon-list-ol:before{content:"\f0cb"}.icon-strikethrough:before{content:"\f0cc"}.icon-underline:before{content:"\f0cd"}.icon-table:before{content:"\f0ce"}.icon-magic:before{content:"\f0d0"}.icon-truck:before{content:"\f0d1"}.icon-pinterest:before{content:"\f0d2"}.icon-pinterest-sign:before{content:"\f0d3"}.icon-google-plus-sign:before{content:"\f0d4"}.icon-google-plus:before{content:"\f0d5"}.icon-money:before{content:"\f0d6"}.icon-caret-down:before{content:"\f0d7"}.icon-caret-up:before{content:"\f0d8"}.icon-caret-left:before{content:"\f0d9"}.icon-caret-right:before{content:"\f0da"}.icon-columns:before{content:"\f0db"}.icon-sort:before{content:"\f0dc"}.icon-sort-down:before{content:"\f0dd"}.icon-sort-up:before{content:"\f0de"}.icon-envelope-alt:before{content:"\f0e0"}.icon-linkedin:before{content:"\f0e1"}.icon-undo:before{content:"\f0e2"}.icon-legal:before{content:"\f0e3"}.icon-dashboard:before{content:"\f0e4"}.icon-comment-alt:before{content:"\f0e5"}.icon-comments-alt:before{content:"\f0e6"}.icon-bolt:before{content:"\f0e7"}.icon-sitemap:before{content:"\f0e8"}.icon-umbrella:before{content:"\f0e9"}.icon-paste:before{content:"\f0ea"}.icon-lightbulb:before{content:"\f0eb"}.icon-exchange:before{content:"\f0ec"}.icon-cloud-download:before{content:"\f0ed"}.icon-cloud-upload:before{content:"\f0ee"}.icon-user-md:before{content:"\f0f0"}.icon-stethoscope:before{content:"\f0f1"}.icon-suitcase:before{content:"\f0f2"}.icon-bell-alt:before{content:"\f0f3"}.icon-coffee:before{content:"\f0f4"}.icon-food:before{content:"\f0f5"}.icon-file-alt:before{content:"\f0f6"}.icon-building:before{content:"\f0f7"}.icon-hospital:before{content:"\f0f8"}.icon-ambulance:before{content:"\f0f9"}.icon-medkit:before{content:"\f0fa"}.icon-fighter-jet:before{content:"\f0fb"}.icon-beer:before{content:"\f0fc"}.icon-h-sign:before{content:"\f0fd"}.icon-plus-sign-alt:before{content:"\f0fe"}.icon-double-angle-left:before{content:"\f100"}.icon-double-angle-right:before{content:"\f101"}.icon-double-angle-up:before{content:"\f102"}.icon-double-angle-down:before{content:"\f103"}.icon-angle-left:before{content:"\f104"}.icon-angle-right:before{content:"\f105"}.icon-angle-up:before{content:"\f106"}.icon-angle-down:before{content:"\f107"}.icon-desktop:before{content:"\f108"}.icon-laptop:before{content:"\f109"}.icon-tablet:before{content:"\f10a"}.icon-mobile-phone:before{content:"\f10b"}.icon-circle-blank:before{content:"\f10c"}.icon-quote-left:before{content:"\f10d"}.icon-quote-right:before{content:"\f10e"}.icon-spinner:before{content:"\f110"}.icon-circle:before{content:"\f111"}.icon-reply:before{content:"\f112"}.icon-github-alt:before{content:"\f113"}.icon-folder-close-alt:before{content:"\f114"}.icon-folder-open-alt:before{content:"\f115"} 35 | -------------------------------------------------------------------------------- /public/styles/font/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viperfx/angular-rpitv/eece593d8ea14f1fa88396c9f971770dec519053/public/styles/font/FontAwesome.otf -------------------------------------------------------------------------------- /public/styles/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viperfx/angular-rpitv/eece593d8ea14f1fa88396c9f971770dec519053/public/styles/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/styles/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viperfx/angular-rpitv/eece593d8ea14f1fa88396c9f971770dec519053/public/styles/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/styles/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viperfx/angular-rpitv/eece593d8ea14f1fa88396c9f971770dec519053/public/styles/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/styles/toogles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Source Sans Pro', sans-serif; 3 | } 4 | .ng-cloak { 5 | display: none; 6 | } 7 | .container { 8 | margin-top: 25px; 9 | } 10 | h1, h2, h3, h4, h5, h6 { 11 | font-family: 'Source Sans Pro', sans-serif; 12 | font-weight: 200; 13 | } 14 | .progress { 15 | height: 5px; 16 | border-radius: 0; 17 | } 18 | .expanded-info a, 19 | .toggle-menu, 20 | .back-menu { 21 | font-size: 30px; 22 | margin-bottom: 8px; 23 | } 24 | .video { 25 | display: block; 26 | position: relative; 27 | text-align: center; 28 | background: black; 29 | } 30 | .related { 31 | max-height: 475px; 32 | overflow-y: scroll; 33 | } 34 | .related .four { 35 | width: 100%; 36 | float: none; 37 | } 38 | .video h3 { 39 | text-align: left; 40 | font-size: 14px; 41 | color: white; 42 | background: black; 43 | margin: 0; 44 | padding: 0; 45 | position: absolute; 46 | width: 100%; 47 | line-height: 1.2; 48 | } 49 | .video h3 span { 50 | padding: 3%; 51 | display: block; 52 | font-weight: normal; 53 | } 54 | .video img { 55 | cursor: pointer; 56 | margin-top: 26px; 57 | } 58 | .search-form input { 59 | margin-bottom: 0; 60 | } 61 | .container { 62 | width: 100%; 63 | max-width: 100%; 64 | } 65 | .rating { 66 | height: 8px; 67 | background-color: #C60F13; 68 | margin-bottom: 30px; 69 | margin-top: -2px; 70 | } 71 | .meta h3 { 72 | font-size: 40px; 73 | } 74 | .meta h3 a { 75 | color: #222; 76 | } 77 | .meta h3 a:hover { 78 | color: #2ba6cb; 79 | } 80 | .meta ul { 81 | list-style: none; 82 | margin: 0 0 20px; 83 | padding: 0; 84 | } 85 | .meta ul li { 86 | font-size: 18px; 87 | } 88 | .meta .rating { 89 | margin-bottom: 20px; 90 | } 91 | .meta a:hover { 92 | color: black; 93 | } 94 | .rating .like { 95 | height: 8px; 96 | background-color: #457A1A; 97 | } 98 | .description { 99 | max-height: 250px; 100 | overflow-x: hidden; 101 | padding-right: 10px; 102 | } 103 | .sidebar ::-webkit-scrollbar, .sidebar ::-webkit-scrollbar:horizontal { 104 | width: 6px; 105 | } 106 | .sidebar ::-webkit-scrollbar-track { 107 | -webkit-box-shadow: inset 0 0 3px rgba(0,0,0,0.3); 108 | -webkit-border-radius: 10px; 109 | border-radius: 10px; 110 | } 111 | .sidebar ::-webkit-scrollbar-thumb { 112 | -webkit-border-radius: 10px; 113 | border-radius: 10px; 114 | background: #ccc; 115 | } 116 | .fixed { 117 | width: 25%; 118 | margin-top: 25px; 119 | padding: 0 15px; 120 | height: 100%; 121 | overflow-y: auto; 122 | } 123 | @media only screen and (max-width: 767px) { 124 | .fixed { 125 | position: static; 126 | width: 100%; 127 | margin: 0; 128 | padding: 0; 129 | } 130 | } 131 | .expanded-info a.icon { 132 | display: inline-block !important; 133 | margin-right: 15px; 134 | } 135 | .expanded-info h3 { 136 | width: 75%; 137 | margin: 0 auto; 138 | margin-bottom: 10px; 139 | margin-top: -40px; 140 | text-align: center; 141 | } 142 | .expanded-info h3 a { 143 | color: #222; 144 | } 145 | .expanded-info h3 a:hover { 146 | color: #2ba6cb; 147 | } 148 | .duration { 149 | float: right; 150 | position: relative; 151 | top: -19px; 152 | background: rgba(0,0,0,0.8); 153 | padding: 2px 5px; 154 | color: white; 155 | right: 0px; 156 | } 157 | .stream { 158 | float: left; 159 | position: relative; 160 | top: -35px; 161 | background: rgb(34, 132, 161); 162 | padding: 10px 25px; 163 | color: white; 164 | left: 0px; 165 | } 166 | .enqueue { 167 | float: left; 168 | position: relative; 169 | top: -28px; 170 | background: rgba(0,0,0,0.7); 171 | padding: 6px 12px; 172 | border-top: 1px solid gray; 173 | color: white; 174 | left: 0px; 175 | } 176 | 177 | .tabs.vertical li ul { 178 | padding-top:10px; 179 | padding-bottom:10px; 180 | border-left:solid 3px #eee; 181 | border-right:solid 1px #eee; 182 | margin-left:-4px; 183 | -webkit-box-shadow:1px 0 0 #FFF inset; 184 | -moz-box-shadow:1px 0 0 #FFF inset; 185 | box-shadow:1px 0 0 #FFF inset; 186 | } 187 | 188 | .tabs.vertical li ul li a { 189 | cursor: pointer; 190 | font-weight:400; 191 | color:#666!important; 192 | background:#fff!important; 193 | padding:7px 0 7px 20px; 194 | border: none; 195 | } 196 | 197 | .tabs.vertical li ul li a:hover { 198 | color:#111!important; 199 | } 200 | 201 | .tabs.vertical li ul li.active a { 202 | font-weight:700; 203 | color:#222!important; 204 | } 205 | 206 | .tabs.vertical li ul li,.tabs.vertical li ul li.active { 207 | border:none; 208 | } 209 | -------------------------------------------------------------------------------- /routes/api.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Serve JSON to our AngularJS client 3 | */ 4 | 5 | exports.name = function (req, res) { 6 | res.json({ 7 | name: 'Bob' 8 | }); 9 | }; -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * GET home page. 3 | */ 4 | 5 | exports.index = function(req, res){ 6 | res.render('index'); 7 | }; 8 | 9 | exports.partials = function (req, res) { 10 | var name = req.params.name; 11 | res.render('partials/' + name); 12 | }; -------------------------------------------------------------------------------- /routes/socket.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Serve content over a socket 3 | */ 4 | 5 | module.exports = function (socket) { 6 | socket.emit('send:name', { 7 | name: 'Bob' 8 | }); 9 | 10 | setInterval(function () { 11 | socket.emit('send:time', { 12 | time: (new Date()).toString() 13 | }); 14 | }, 1000); 15 | }; 16 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Toogles: Awesome Goggles for YouTube 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /views/partials/about.html: -------------------------------------------------------------------------------- 1 |

Toogles is a super sweet pair of awesome goggles for YouTube.

2 | -------------------------------------------------------------------------------- /views/partials/browse.html: -------------------------------------------------------------------------------- 1 | 13 | 24 | -------------------------------------------------------------------------------- /views/partials/contact.html: -------------------------------------------------------------------------------- 1 |

Find me on Twitter.

2 | -------------------------------------------------------------------------------- /views/partials/list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 | -------------------------------------------------------------------------------- /views/partials/playlists.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /views/partials/queue.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /views/partials/search.html: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /views/partials/user.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /views/partials/videos.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{video.title.$t}}

4 | 5 |
{{formatDuration(video.media$group.yt$duration.seconds)}}
6 |
{{isPlaying(video)}}
7 |
Enqueue
8 |
9 | 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /views/partials/view.html: -------------------------------------------------------------------------------- 1 |
2 | 37 |
38 |
39 | 40 | 41 |

{{video.title.$t}}

42 |
43 | 46 |
47 |
48 | --------------------------------------------------------------------------------