simple_pvr 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +1 -4
- data/Gemfile.lock +72 -61
- data/README.md +51 -33
- data/bin/pvr_server +14 -6
- data/changelog.txt +11 -0
- data/development_server +14 -6
- data/features/channel_overview.feature +1 -1
- data/features/recordings.feature +30 -0
- data/features/step_definitions/pvr_steps.rb +34 -0
- data/features/support/env.rb +1 -2
- data/features/support/paths.rb +2 -0
- data/lib/simple_pvr.rb +2 -0
- data/lib/simple_pvr/model/database_initializer.rb +8 -0
- data/lib/simple_pvr/model/programme.rb +12 -2
- data/lib/simple_pvr/model/programme_actor.rb +14 -0
- data/lib/simple_pvr/model/programme_category.rb +14 -0
- data/lib/simple_pvr/model/programme_director.rb +13 -0
- data/lib/simple_pvr/model/programme_presenter.rb +13 -0
- data/lib/simple_pvr/model/recording.rb +12 -0
- data/lib/simple_pvr/programme_icon_fetcher.rb +15 -0
- data/lib/simple_pvr/pvr_initializer.rb +4 -4
- data/lib/simple_pvr/recorder.rb +4 -3
- data/lib/simple_pvr/recording_manager.rb +44 -24
- data/lib/simple_pvr/recording_planner.rb +3 -3
- data/lib/simple_pvr/scheduler.rb +12 -4
- data/lib/simple_pvr/server/base_controller.rb +12 -13
- data/lib/simple_pvr/server/channels_controller.rb +5 -5
- data/lib/simple_pvr/server/programmes_controller.rb +2 -2
- data/lib/simple_pvr/server/schedules_controller.rb +4 -4
- data/lib/simple_pvr/server/secured_controller.rb +71 -0
- data/lib/simple_pvr/server/shows_controller.rb +13 -8
- data/lib/simple_pvr/server/status_controller.rb +1 -1
- data/lib/simple_pvr/server/upcoming_recordings_controller.rb +1 -1
- data/lib/simple_pvr/version.rb +1 -1
- data/lib/simple_pvr/xmltv_reader.rb +37 -4
- data/public/css/typeahead.js-bootstrap.css +83 -0
- data/public/index.html +45 -37
- data/public/js/angular/http-auth-interceptor.js +122 -0
- data/public/js/app.js +4 -37
- data/public/js/controllers.js +22 -14
- data/public/js/directives.js +102 -0
- data/public/js/filters.js +0 -31
- data/public/js/services.js +64 -0
- data/public/js/typeahead/typeahead.min.js +7 -0
- data/public/partials/about.html +15 -13
- data/public/partials/channels.html +41 -36
- data/public/partials/programme.html +20 -4
- data/public/partials/programmeListing.html +14 -13
- data/public/partials/schedule.html +91 -86
- data/public/partials/schedules.html +28 -16
- data/public/partials/search.html +10 -11
- data/public/partials/show.html +26 -9
- data/public/partials/shows.html +5 -6
- data/public/partials/status.html +1 -1
- data/public/templates/loginDialog.html +30 -0
- data/public/templates/logoutLink.html +1 -0
- data/public/templates/titleSearch.html +5 -3
- data/simple_pvr.gemspec +6 -4
- data/spec/resources/dummyImage.png +1 -0
- data/spec/resources/programmes-with-categories.xmltv +27 -0
- data/spec/resources/programmes-with-credits.xmltv +24 -0
- data/spec/resources/programmes-with-icons.xmltv +25 -0
- data/spec/resources/programmes-with-presenters.xmltv +19 -0
- data/spec/resources/{programs-without-icon.xmltv → programmes-without-icon.xmltv} +0 -0
- data/spec/resources/{programs.xmltv → programmes.xmltv} +0 -4
- data/spec/simple_pvr/ffmpeg_spec.rb +24 -22
- data/spec/simple_pvr/hdhomerun_spec.rb +69 -67
- data/spec/simple_pvr/model/channel_spec.rb +101 -101
- data/spec/simple_pvr/model/programme_spec.rb +104 -104
- data/spec/simple_pvr/model/schedule_spec.rb +74 -74
- data/spec/simple_pvr/programme_icon_fetcher_spec.rb +25 -0
- data/spec/simple_pvr/pvr_initializer_spec.rb +40 -38
- data/spec/simple_pvr/recorder_spec.rb +37 -26
- data/spec/simple_pvr/recording_manager_spec.rb +160 -133
- data/spec/simple_pvr/recording_planner_spec.rb +213 -211
- data/spec/simple_pvr/scheduler_spec.rb +189 -172
- data/spec/simple_pvr/server/secured_controller_spec.rb +118 -0
- data/spec/simple_pvr/xmltv_reader_spec.rb +89 -41
- data/test/karma.conf.js +7 -4
- data/test/unit/filtersSpec.js +0 -36
- metadata +79 -63
- data/public/css/bootstrap-responsive.min.css +0 -9
- data/public/css/bootstrap.min.css +0 -9
- data/public/img/glyphicons-halflings-white.png +0 -0
- data/public/img/glyphicons-halflings.png +0 -0
- data/public/js/angular/angular-resource.min.js +0 -10
- data/public/js/angular/angular.min.js +0 -162
- data/public/js/bootstrap/bootstrap.min.js +0 -6
- data/public/js/jquery/jquery.min.js +0 -5
- data/test/lib/angular/angular-mocks.js +0 -1768
data/public/index.html
CHANGED
@@ -1,51 +1,59 @@
|
|
1
1
|
<!doctype html>
|
2
2
|
<html lang="en" ng-app="simplePvr" ng-cloak>
|
3
3
|
<head>
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
}
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
5
|
+
<base href="/">
|
6
|
+
<link href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
|
7
|
+
<link href="/app/css/typeahead.js-bootstrap.css" rel="stylesheet">
|
8
|
+
<link href="/app/css/simplepvr.css" rel="stylesheet">
|
9
|
+
|
10
|
+
<style>
|
11
|
+
body { padding-top: 70px; }
|
13
12
|
</style>
|
14
|
-
<link href="/app/css/bootstrap-responsive.min.css" rel="stylesheet">
|
15
13
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
14
|
+
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
|
15
|
+
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular.min.js"></script>
|
16
|
+
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular-route.min.js"></script>
|
17
|
+
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular-resource.min.js"></script>
|
18
|
+
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.6/angular-cookies.min.js"></script>
|
19
|
+
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
|
20
|
+
<script src="app/js/angular/http-auth-interceptor.js"></script>
|
21
|
+
<script src="app/js/typeahead/typeahead.min.js"></script>
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
<script src="app/js/controllers.js"></script>
|
24
|
+
<script src="app/js/services.js"></script>
|
25
|
+
<script src="app/js/filters.js"></script>
|
26
|
+
<script src="app/js/directives.js"></script>
|
27
|
+
<script src="app/js/app.js"></script>
|
25
28
|
</head>
|
26
29
|
|
27
30
|
<body>
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
<
|
37
|
-
<
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
</
|
46
|
-
|
31
|
+
<login-dialog></login-dialog>
|
32
|
+
|
33
|
+
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
|
34
|
+
<div class="container">
|
35
|
+
<div class="navbar-header">
|
36
|
+
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
|
37
|
+
<span class="sr-only">Toggle navigation</span>
|
38
|
+
<span class="icon-bar"></span>
|
39
|
+
<span class="icon-bar"></span>
|
40
|
+
<span class="icon-bar"></span>
|
41
|
+
</button>
|
42
|
+
<a class="navbar-brand" href="about">SimplePVR</a>
|
43
|
+
</div>
|
44
|
+
|
45
|
+
<div class="collapse navbar-collapse navbar-ex1-collapse">
|
46
|
+
<ul class="nav navbar-nav">
|
47
|
+
<navbar-item route="/schedules">Schedules</navbar-item>
|
48
|
+
<navbar-item route="/channels">Channels</navbar-item>
|
49
|
+
<navbar-item route="/shows">Recordings</navbar-item>
|
50
|
+
<navbar-item route="/status">Status</navbar-item>
|
51
|
+
</ul>
|
52
|
+
<title-search></title-search>
|
53
|
+
<logout-link></logout-link>
|
47
54
|
</div>
|
48
55
|
</div>
|
56
|
+
</nav>
|
49
57
|
|
50
58
|
<div class="container" id="contents" ng-view>
|
51
59
|
</div>
|
@@ -0,0 +1,122 @@
|
|
1
|
+
/*global angular:true, browser:true */
|
2
|
+
|
3
|
+
/**
|
4
|
+
* @license HTTP Auth Interceptor Module for AngularJS
|
5
|
+
* (c) 2012 Witold Szczerba
|
6
|
+
* License: MIT
|
7
|
+
*/
|
8
|
+
(function () {
|
9
|
+
'use strict';
|
10
|
+
|
11
|
+
angular.module('http-auth-interceptor', ['http-auth-interceptor-buffer'])
|
12
|
+
|
13
|
+
.factory('authService', ['$rootScope','httpBuffer', function($rootScope, httpBuffer) {
|
14
|
+
return {
|
15
|
+
/**
|
16
|
+
* Call this function to indicate that authentication was successfull and trigger a
|
17
|
+
* retry of all deferred requests.
|
18
|
+
* @param data an optional argument to pass on to $broadcast which may be useful for
|
19
|
+
* example if you need to pass through details of the user that was logged in
|
20
|
+
*/
|
21
|
+
loginConfirmed: function(data, configUpdater) {
|
22
|
+
var updater = configUpdater || function(config) {return config;};
|
23
|
+
$rootScope.$broadcast('event:auth-loginConfirmed', data);
|
24
|
+
httpBuffer.retryAll(updater);
|
25
|
+
},
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Call this function to indicate that authentication should not proceed.
|
29
|
+
* All deferred requests will be abandoned or rejected (if reason is provided).
|
30
|
+
* @param data an optional argument to pass on to $broadcast.
|
31
|
+
* @param reason if provided, the requests are rejected; abandoned otherwise.
|
32
|
+
*/
|
33
|
+
loginCancelled: function(data, reason) {
|
34
|
+
httpBuffer.rejectAll(reason);
|
35
|
+
$rootScope.$broadcast('event:auth-loginCancelled', data);
|
36
|
+
}
|
37
|
+
};
|
38
|
+
}])
|
39
|
+
|
40
|
+
/**
|
41
|
+
* $http interceptor.
|
42
|
+
* On 401 response (without 'ignoreAuthModule' option) stores the request
|
43
|
+
* and broadcasts 'event:angular-auth-loginRequired'.
|
44
|
+
*/
|
45
|
+
.config(['$httpProvider', function($httpProvider) {
|
46
|
+
$httpProvider.interceptors.push(['$rootScope', '$q', 'httpBuffer', function($rootScope, $q, httpBuffer) {
|
47
|
+
return {
|
48
|
+
responseError: function(rejection) {
|
49
|
+
if (rejection.status === 401 && !rejection.config.ignoreAuthModule) {
|
50
|
+
var deferred = $q.defer();
|
51
|
+
httpBuffer.append(rejection.config, deferred);
|
52
|
+
$rootScope.$broadcast('event:auth-loginRequired', rejection);
|
53
|
+
return deferred.promise;
|
54
|
+
}
|
55
|
+
// otherwise, default behaviour
|
56
|
+
return $q.reject(rejection);
|
57
|
+
}
|
58
|
+
};
|
59
|
+
}]);
|
60
|
+
}]);
|
61
|
+
|
62
|
+
/**
|
63
|
+
* Private module, a utility, required internally by 'http-auth-interceptor'.
|
64
|
+
*/
|
65
|
+
angular.module('http-auth-interceptor-buffer', [])
|
66
|
+
|
67
|
+
.factory('httpBuffer', ['$injector', function($injector) {
|
68
|
+
/** Holds all the requests, so they can be re-requested in future. */
|
69
|
+
var buffer = [];
|
70
|
+
|
71
|
+
/** Service initialized later because of circular dependency problem. */
|
72
|
+
var $http;
|
73
|
+
|
74
|
+
function retryHttpRequest(config, deferred) {
|
75
|
+
// Make room for new 'Authenticate' header value
|
76
|
+
delete config.headers['Authorization'];
|
77
|
+
|
78
|
+
function successCallback(response) {
|
79
|
+
deferred.resolve(response);
|
80
|
+
}
|
81
|
+
function errorCallback(response) {
|
82
|
+
deferred.reject(response);
|
83
|
+
}
|
84
|
+
$http = $http || $injector.get('$http');
|
85
|
+
$http(config).then(successCallback, errorCallback);
|
86
|
+
}
|
87
|
+
|
88
|
+
return {
|
89
|
+
/**
|
90
|
+
* Appends HTTP request configuration object with deferred response attached to buffer.
|
91
|
+
*/
|
92
|
+
append: function(config, deferred) {
|
93
|
+
buffer.push({
|
94
|
+
config: config,
|
95
|
+
deferred: deferred
|
96
|
+
});
|
97
|
+
},
|
98
|
+
|
99
|
+
/**
|
100
|
+
* Abandon or reject (if reason provided) all the buffered requests.
|
101
|
+
*/
|
102
|
+
rejectAll: function(reason) {
|
103
|
+
if (reason) {
|
104
|
+
for (var i = 0; i < buffer.length; ++i) {
|
105
|
+
buffer[i].deferred.reject(reason);
|
106
|
+
}
|
107
|
+
}
|
108
|
+
buffer = [];
|
109
|
+
},
|
110
|
+
|
111
|
+
/**
|
112
|
+
* Retries all the buffered requests clears the buffer.
|
113
|
+
*/
|
114
|
+
retryAll: function(updater) {
|
115
|
+
for (var i = 0; i < buffer.length; ++i) {
|
116
|
+
retryHttpRequest(updater(buffer[i].config), buffer[i].deferred);
|
117
|
+
}
|
118
|
+
buffer = [];
|
119
|
+
}
|
120
|
+
};
|
121
|
+
}]);
|
122
|
+
})();
|
data/public/js/app.js
CHANGED
@@ -1,42 +1,9 @@
|
|
1
1
|
'use strict';
|
2
2
|
|
3
|
-
angular.module('simplePvr', ['simplePvrServices', 'simplePvrFilters']).
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
restrict: 'E',
|
8
|
-
replace: true,
|
9
|
-
controller: SearchProgrammesCtrl,
|
10
|
-
link: function(scope, element, attributes, controller) {
|
11
|
-
element.find('input').typeahead({
|
12
|
-
source: scope.autocomplete,
|
13
|
-
updater: scope.updater
|
14
|
-
});
|
15
|
-
}
|
16
|
-
};
|
17
|
-
}).
|
18
|
-
directive('navbarItem', function($location) {
|
19
|
-
return {
|
20
|
-
template: '<li><a ng-href="{{route}}" ng-transclude></a></li>',
|
21
|
-
restrict: 'E',
|
22
|
-
transclude: true,
|
23
|
-
replace: true,
|
24
|
-
scope: { route:'@route' },
|
25
|
-
link: function(scope, element, attributes, controller) {
|
26
|
-
scope.$on('$routeChangeSuccess', function() {
|
27
|
-
var path = $location.path();
|
28
|
-
var isSamePath = path == scope.route;
|
29
|
-
var isSubpath = path.indexOf(scope.route + '/') == 0;
|
30
|
-
if (isSamePath || isSubpath) {
|
31
|
-
element.addClass('active');
|
32
|
-
} else {
|
33
|
-
element.removeClass('active');
|
34
|
-
}
|
35
|
-
});
|
36
|
-
}
|
37
|
-
};
|
38
|
-
}).
|
39
|
-
config(function($routeProvider, $locationProvider) {
|
3
|
+
angular.module('simplePvr', ['ngRoute', 'ngCookies', 'simplePvrServices', 'simplePvrFilters', 'simplePvrDirectives', 'http-auth-interceptor']).
|
4
|
+
config(function($routeProvider, $locationProvider, $httpProvider) {
|
5
|
+
$httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
6
|
+
|
40
7
|
$locationProvider.html5Mode(true).hashPrefix('');
|
41
8
|
|
42
9
|
$routeProvider.
|
data/public/js/controllers.js
CHANGED
@@ -1,5 +1,24 @@
|
|
1
1
|
'use strict';
|
2
2
|
|
3
|
+
function CredentialsController($scope, loginService) {
|
4
|
+
$scope.credentials = { userName: '', password: '' };
|
5
|
+
|
6
|
+
$scope.submit = function() {
|
7
|
+
loginService.setUserNameAndPassword($scope.credentials.userName, $scope.credentials.password);
|
8
|
+
}
|
9
|
+
}
|
10
|
+
|
11
|
+
function LoginController($scope, $location, loginService) {
|
12
|
+
$scope.isLoggedIn = function() {
|
13
|
+
return loginService.isLoggedIn();
|
14
|
+
}
|
15
|
+
|
16
|
+
$scope.logOut = function() {
|
17
|
+
loginService.logOut();
|
18
|
+
$location.path('/about');
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
3
22
|
function SchedulesCtrl($scope, $http, Schedule, UpcomingRecording, Channel) {
|
4
23
|
var updateView = function() {
|
5
24
|
$scope.schedules = Schedule.query();
|
@@ -50,7 +69,7 @@ function ChannelsCtrl($scope, $http, Channel) {
|
|
50
69
|
if (programme == null) {
|
51
70
|
return '';
|
52
71
|
}
|
53
|
-
return programme.is_conflicting ? '
|
72
|
+
return programme.is_conflicting ? 'danger' : (programme.is_scheduled ? 'success' : '');
|
54
73
|
}
|
55
74
|
$scope.hideChannel = function(channel) {
|
56
75
|
// I wish Angular could let me define this operation on the Channel object
|
@@ -71,7 +90,7 @@ function ProgrammeListingCtrl($scope, $routeParams, ProgrammeListing) {
|
|
71
90
|
$scope.programmeListing = ProgrammeListing.get({channelId: $scope.channelId, date: $scope.date});
|
72
91
|
|
73
92
|
$scope.classForProgrammeLine = function(programme) {
|
74
|
-
return programme.is_conflicting ? '
|
93
|
+
return programme.is_conflicting ? 'danger' : (programme.is_scheduled ? 'success' : '');
|
75
94
|
}
|
76
95
|
}
|
77
96
|
|
@@ -119,7 +138,7 @@ function ShowCtrl($scope, $routeParams, $http, Show, Recording) {
|
|
119
138
|
}
|
120
139
|
|
121
140
|
$scope.deleteRecording = function(recording) {
|
122
|
-
if (confirm("Really delete
|
141
|
+
if (confirm("Really delete this recording of show\n" + $scope.show.name + "\n?")) {
|
123
142
|
recording.$delete(loadRecordings);
|
124
143
|
}
|
125
144
|
}
|
@@ -134,17 +153,6 @@ function ShowCtrl($scope, $routeParams, $http, Show, Recording) {
|
|
134
153
|
}
|
135
154
|
|
136
155
|
function SearchProgrammesCtrl($scope, $http, $location) {
|
137
|
-
$scope.autocomplete = function(query, process) {
|
138
|
-
$http.get('/api/programmes/title_search', {params: {query: query}}).success(process);
|
139
|
-
}
|
140
|
-
|
141
|
-
$scope.updater = function(item) {
|
142
|
-
$scope.$apply(function() {
|
143
|
-
$scope.title = item;
|
144
|
-
});
|
145
|
-
return item;
|
146
|
-
}
|
147
|
-
|
148
156
|
$scope.search = function() {
|
149
157
|
$location.path('/search').search({query: $scope.title});
|
150
158
|
}
|
@@ -0,0 +1,102 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
angular.module('simplePvrDirectives', []).
|
4
|
+
directive('loginDialog', function($timeout) {
|
5
|
+
return {
|
6
|
+
templateUrl: '/app/templates/loginDialog.html',
|
7
|
+
restrict: 'E',
|
8
|
+
replace: true,
|
9
|
+
controller: CredentialsController,
|
10
|
+
link: function(scope, element, attributes, controller) {
|
11
|
+
var isShowing = false;
|
12
|
+
|
13
|
+
element.on('shown.bs.modal', function(e) {
|
14
|
+
element.find('#userName').focus();
|
15
|
+
});
|
16
|
+
|
17
|
+
scope.$on('event:auth-loginRequired', function() {
|
18
|
+
if (isShowing) {
|
19
|
+
return;
|
20
|
+
}
|
21
|
+
|
22
|
+
// If we're in the process of hiding the modal, we need to wait for
|
23
|
+
// all CSS animations to complete before showing the modal again.
|
24
|
+
// Otherwise, we might end up with an invisible modal, making the whole
|
25
|
+
// view rather unusable. I've been unable to control the transitions
|
26
|
+
// between "showing", "shown", "hiding", and "hidden" tightly using
|
27
|
+
// JQuery notifications without collecting more and more modal backdrops
|
28
|
+
// in the DOM, so the dirty solution here is to simply wait a second
|
29
|
+
// before showing the log-in dialog.
|
30
|
+
isShowing = true;
|
31
|
+
$timeout(function() {
|
32
|
+
element.modal('show');
|
33
|
+
isShowing = false;
|
34
|
+
}, 1000);
|
35
|
+
});
|
36
|
+
|
37
|
+
scope.$on('event:auth-loginConfirmed', function() {
|
38
|
+
element.modal('hide');
|
39
|
+
scope.credentials.password = '';
|
40
|
+
});
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}).
|
44
|
+
directive('logoutLink', function() {
|
45
|
+
return {
|
46
|
+
templateUrl: '/app/templates/logoutLink.html',
|
47
|
+
restrict: 'E',
|
48
|
+
replace: true,
|
49
|
+
controller: LoginController
|
50
|
+
}
|
51
|
+
}).
|
52
|
+
directive('titleSearch', function(loginService) {
|
53
|
+
return {
|
54
|
+
templateUrl: '/app/templates/titleSearch.html',
|
55
|
+
restrict: 'E',
|
56
|
+
replace: true,
|
57
|
+
controller: SearchProgrammesCtrl,
|
58
|
+
link: function(scope, element, attributes, controller) {
|
59
|
+
var inputField = element.find('input');
|
60
|
+
inputField.typeahead({
|
61
|
+
remote: '/api/programmes/title_search?query=%QUERY'
|
62
|
+
});
|
63
|
+
|
64
|
+
var updateTitle = function() {
|
65
|
+
scope.$apply(function() {
|
66
|
+
scope.title = inputField.val();
|
67
|
+
});
|
68
|
+
};
|
69
|
+
var updateTitleAndPerformSearch = function() {
|
70
|
+
scope.$apply(function() {
|
71
|
+
scope.title = inputField.val();
|
72
|
+
scope.search();
|
73
|
+
})
|
74
|
+
}
|
75
|
+
|
76
|
+
inputField.change(updateTitle);
|
77
|
+
inputField.on('typeahead:autocompleted', updateTitle);
|
78
|
+
inputField.on('typeahead:selected', updateTitleAndPerformSearch);
|
79
|
+
}
|
80
|
+
};
|
81
|
+
}).
|
82
|
+
directive('navbarItem', function($location) {
|
83
|
+
return {
|
84
|
+
template: '<li><a ng-href="{{route}}" ng-transclude></a></li>',
|
85
|
+
restrict: 'E',
|
86
|
+
transclude: true,
|
87
|
+
replace: true,
|
88
|
+
scope: { route:'@route' },
|
89
|
+
link: function(scope, element, attributes, controller) {
|
90
|
+
scope.$on('$routeChangeSuccess', function() {
|
91
|
+
var path = $location.path();
|
92
|
+
var isSamePath = path == scope.route;
|
93
|
+
var isSubpath = path.indexOf(scope.route + '/') == 0;
|
94
|
+
if (isSamePath || isSubpath) {
|
95
|
+
element.addClass('active');
|
96
|
+
} else {
|
97
|
+
element.removeClass('active');
|
98
|
+
}
|
99
|
+
});
|
100
|
+
}
|
101
|
+
};
|
102
|
+
});
|
data/public/js/filters.js
CHANGED
@@ -1,37 +1,6 @@
|
|
1
1
|
'use strict';
|
2
2
|
|
3
3
|
angular.module('simplePvrFilters', []).
|
4
|
-
filter('chunk', function() {
|
5
|
-
function chunkArray(array, chunkSize) {
|
6
|
-
var result = [];
|
7
|
-
var currentChunk = [];
|
8
|
-
for (var i=0; i<array.length; i++) {
|
9
|
-
currentChunk.push(array[i]);
|
10
|
-
if (currentChunk.length == chunkSize) {
|
11
|
-
result.push(currentChunk);
|
12
|
-
currentChunk = [];
|
13
|
-
}
|
14
|
-
}
|
15
|
-
if (currentChunk.length > 0) {
|
16
|
-
result.push(currentChunk);
|
17
|
-
}
|
18
|
-
return result;
|
19
|
-
}
|
20
|
-
|
21
|
-
function defineHashKeys(array) {
|
22
|
-
for (var i=0; i<array.length; i++) {
|
23
|
-
array[i].$$hashKey = i;
|
24
|
-
}
|
25
|
-
}
|
26
|
-
|
27
|
-
return function(array, chunkSize) {
|
28
|
-
if (!(array instanceof Array)) return array;
|
29
|
-
if (!chunkSize) return array;
|
30
|
-
var result = chunkArray(array, chunkSize);
|
31
|
-
defineHashKeys(result);
|
32
|
-
return result;
|
33
|
-
}
|
34
|
-
}).
|
35
4
|
filter('formatEpisode', function() {
|
36
5
|
return function(episodeNum) {
|
37
6
|
return episodeNum ? episodeNum.replace(' .', '').replace('. ', '') : '';
|