Skip to content

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also .

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also .
...
  • 4 commits
  • 17 files changed
  • 0 commit comments
  • 1 contributor
Commits on Mar 24, 2012
@maritz Implement season seen/unseen toggle on backend and frontend
Fix series search (encodeURIComponent on search param)
Optimize Episode seen button rendering to only re-render on change
35f6428
Commits on Mar 26, 2012
@maritz Add pagination to episode list a0ccf1e
@maritz Add pagination to collection and listView be5df0b
@maritz Merge branch 'stack' 730ba57
View
87 controllers/EpisodeController.js
@@ -20,44 +20,87 @@ function EpisodeError(msg, code){
EpisodeError.prototype.__proto__ = Error.prototype;
+
app.get('/byShow/:id', auth.isLoggedIn, auth.may('list', 'Episode'), loadModel('Show'), function (req, res, next) {
var season = req.param('season');
- var cb = function (err, ids) {
+ var start = parseInt(req.param('offset'), 10) || 0;
+ var max = 30;
+ var more_after_max = false;
+ var total = 0;
+
+ var loadEpisodes = function (ids) {
+ async.map(ids, function (id, cb) {
+ nohm.factory('Episode', id, function (err, data) {
+ if (err) {
+ cb(err);
+ } else {
+ req.user.belongsTo(this, 'seen', function (err, belongs) {
+ if (err) {
+ cb(err);
+ }
+ data.seen = belongs;
+ data.id = id;
+ cb(null, data);
+ });
+ }
+ });
+ }, function (err, episodes) {
+ if (err) {
+ console.log(err);
+ next(new EpisodeError('Error while loading the episodes.'));
+ } else {
+ res.ok({
+ total: total,
+ per_page: max,
+ collection: episodes
+ });
+ }
+ });
+ };
+
+ var getAllCallback = function (err, ids) {
if (err) {
next(new EpisodeError('Error while retreiving the episode ids.'));
} else {
- async.map(ids, function (id, cb) {
- nohm.factory('Episode').load(id, function (err, data) {
+ total = ids.length;
+ if (total > max) {
+ more_after_max = (start+max > total);
+ Episode.sort({
+ field: 'number',
+ limit: [start, max]
+ }, ids,
+ function (err, ids) {
if (err) {
- cb(err);
+ next(new EpisodeError('Error while sorting the episode ids.'));
} else {
- req.user.belongsTo(this, 'seen', function (err, belongs) {
- if (err) {
- cb(err);
- }
- data.seen = belongs;
- data.id = id;
- cb(null, data);
- });
+ loadEpisodes(ids);
}
});
- }, function (err, episodes) {
- if (err) {
- console.log(err);
- next(new EpisodeError('Error while loading the episodes.'));
- } else {
- res.ok(episodes);
- }
- });
+ } else {
+ loadEpisodes(ids);
+ }
}
};
if (season) {
- req.loaded.Show.getAll('Episode', 'season'+season, cb);
+ req.loaded.Show.getAll('Episode', 'season'+season, getAllCallback);
} else {
- req.loaded.Show.getAll('Episode', cb);
+ req.loaded.Show.getAll('Episode', getAllCallback);
}
});
+app.get('/season_seen/:id', auth.isLoggedIn, auth.may('view', 'Episode'), loadModel('Episode'), function (req, res, next) {
+ req.loaded.Episode.toggleSeasonSeen(req.user, function (err, seen) {
+ if (err) {
+ if ( ! err instanceof Error) {
+ err = new EpisodeError(err);
+ }
+ next(err);
+ } else {
+ res.ok({seen: seen});
+ }
+ });
+});
+
app.get('/seen/:id', auth.isLoggedIn, auth.may('view', 'Episode'), loadModel('Episode'), function (req, res, next) {
var episode = req.loaded.Episode;
View
34 models/EpisodeModel.js
@@ -18,7 +18,8 @@ module.exports = nohm.model('Episode', {
type: 'integer'
},
number: {
- type: 'integer'
+ type: 'integer',
+ index: true
},
first_aired: {
type: 'timestamp'
@@ -101,7 +102,6 @@ module.exports = nohm.model('Episode', {
},
getSeasonEpisodes: function (callback) {
- var self = this;
var season = this.p('season');
this.getShow(function (err, show) {
if (err) {
@@ -112,6 +112,36 @@ module.exports = nohm.model('Episode', {
});
}
});
+ },
+
+ toggleSeasonSeen: function (user, callback) {
+ var season_rel = 'seen_season_'+this.p('season');
+ this.getSeasonEpisodes(function (err, ids, show) {
+ if (err) {
+ callback(err);
+ } else {
+ user.belongsTo(show, season_rel, function (err, seen) {
+ if (err) {
+ callback('Failed to toggle Season seen/unseen.');
+ } else {
+ var action = seen ? 'unlink' : 'link';
+ ids.forEach(function (id) {
+ var episode = nohm.factory('Episode');
+ episode.id = id;
+ user[action](episode, 'seen');
+ });
+ user[action](show, season_rel);
+ user.save(function (err) {
+ if (err) {
+ callback('Failed to set Season as '+(seen?'':'un')+'seen.');
+ } else {
+ callback(null, !seen);
+ }
+ });
+ }
+ });
+ }
+ });
}
}
});
View
2 models/tvdbModel.js
@@ -332,7 +332,7 @@ module.exports = nohm.model('tvdb', {
var redis = require(__dirname+'/../registry.js').redis;
redis.lrange(set_key, 0, -1, function (err, series) {
if (err || series.length === 0) {
- self._tvdbRequest('/GetSeries.php?seriesname='+name, function (err, doc) {
+ self._tvdbRequest('/GetSeries.php?seriesname='+encodeURIComponent(name), function (err, doc) {
if (err) {
callback(err);
} else {
View
2 socket_server.js
@@ -60,7 +60,7 @@ exports.init = function (server) {
}
channels[name].on('connection', connectionHandler);
} else {
- console.log('Warning: Found socket controller without connection Handler export.');
+ console.log('Warning: Found socket controller without connection Handler export:', name);
}
});
};
View
9 static/css/show.styl
@@ -19,9 +19,12 @@ ul.show_details
&:after
content ':'
-
-a.season_opener, a.episode_opener
- cursor pointer
+
+#season_contents
+ .loading
+ height 300px
+ .season_seen_container
+ margin 0 20px 10px
ul.episode_list
h3
View
10 static/css/style.styl
@@ -13,10 +13,20 @@
.error
color font-color-error
+.nav-pill
+ cursor pointer
+
.fake_link
cursor pointer
color link-color
+.jGrowl
+ position fixed
+ top 20px
+ left 50%
+ margin-left -250px
+ width 500px
+
.jGrowl-notification, .jGrowl-closer
background-color #ffffff
gradient #ccf, white
View
6 static/i18n/en_US/generic.js
@@ -17,7 +17,11 @@ module.exports = {
name: 'Name',
description: 'Description',
yes: "Yes",
- no: "No"
+ no: "No",
+ pagination: {
+ previous: '<<',
+ next: '>>'
+ }
},
overlays: {
login_needed: "Login",
View
9 static/i18n/en_US/show.js
@@ -54,6 +54,15 @@ module.exports = {
set_seen: "I've seen it",
set_not_seen: "I've NOT seen it",
wanna_see: "I want to see it",
+
+ has_seen_season: "You've seen this entire season",
+ has_not_seen_season: "Not seen all episodes",
+ not_released_season: "Hasn't finished airing all episodes",
+ set_seen_season: "I've seen every episode",
+ set_seen_season_available: "I've seen every episode that aired",
+ set_not_seen_season: "I haven't seen ANY episodes",
+ wanna_see_season: "I want to see all episodes",
+
none_found: "There are no episodes in our database for this season."
}
};
View
BIN static/images/logo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
16 static/js/collections/episodeCollection.js
@@ -1,11 +1,25 @@
_r(function (app) {
- app.collections.Episode = app.base.collection.extend({
+ app.collections.Season = app.base.collection.extend({
model: app.models.Episode,
url: '/REST/Episode/',
+ pagination_by_field: 'number',
+
comparator: function (episode) {
return episode.get('number');
+ },
+
+ toggleSeen: function () {
+ var self = this;
+ $.getJSON('/REST/Episode/season_seen/'+this.first().id, function (res) {
+ var seen = res.data.seen;
+ self.each(function (episode) {
+ episode.set({
+ seen: seen
+ });
+ });
+ });
}
});
View
92 static/js/libs/backbone/collection.js
@@ -1,11 +1,41 @@
_r(function (app) {
+
+ _.extend(Backbone.Collection.prototype, Backbone.Events);
+
app.base.collection = Backbone.Collection.extend({
+ paginated: false,
+ total: 0,
+ per_page: 0,
+ all_loaded: false,
+ pagination_by_field: 'id',
+ pages_loaded: [],
+
/**
- * Overwriting Backbone.Collection.parse() to use the proper root in the response json.
+ * Overwriting Backbone.Collection.parse() to use the proper root in the response json and allow pagination metadata to be parsed.
*/
parse: function (response) {
- return response.data ? response.data : [];
+ if (response.data) {
+ if (response.data.collection) {
+ this.per_page = response.data.per_page;
+ this.total = response.data.total;
+
+ if (this.total > this.per_page && !this.paginated) {
+ this.pages_loaded.push(1);
+ this.paginated = true;
+ }
+
+ if (this.length === this.total) {
+ this.all_loaded = true;
+ }
+ return response.data.collection;
+ } else {
+ return response.data;
+ }
+ } else {
+ console.log('Collection parse did not get proper response:', response);
+ return [];
+ }
},
/**
@@ -18,7 +48,65 @@ _r(function (app) {
error: options
};
}
+ if (this.paginated) {
+ options.add = true;
+ }
return Backbone.Collection.prototype.fetch.call(this, options);
+ },
+
+ _getLoadedPage: function (offset) {
+ var self = this;
+ var max = offset+this.per_page;
+ return this.filter(function (episode) {
+ var num = episode.get(self.pagination_by_field);
+ return num > offset && num <= max;
+ });
+ },
+
+ getPage: function (page, callback) {
+ var self = this;
+ var offset;
+
+ if (this.per_page*page > this.total) {
+ page = Math.ceil(this.total / this.per_page);
+ }
+ offset = Math.floor(this.per_page * (page-1));
+
+ if (this.pages_loaded.indexOf(page) !== -1) {
+ return callback(this._getLoadedPage(offset), page);
+ }
+
+ if (this.loading_page) {
+ var args = _.toArray(arguments);
+ this.once('page_loaded', function () {
+ self.getPage.apply(self, args);
+ });
+ return false;
+ }
+ this.loading_page = true;
+
+ var url = getUrl(this);
+ url += url.indexOf('?') === -1 ? '?' : '&';
+ url += 'offset='+offset;
+ this.fetch({
+ add: true,
+ url: url,
+ success: function () {
+ self.pages_loaded.push(page);
+ callback(self._getLoadedPage(offset), page);
+ self.loading_page = false;
+ self.trigger('page_loaded', page);
+ }
+ })
}
});
+
+
+ // this is directly copied from backbone.js
+ // Helper function to get a URL from a Model or Collection as a property
+ // or as a function.
+ var getUrl = function(object) {
+ if (!(object && object.url)) return null;
+ return _.isFunction(object.url) ? object.url() : object.url;
+ };
});
View
70 static/js/libs/backbone/view.js
@@ -1,5 +1,7 @@
_r(function (app) {
+ _.extend(Backbone.View.prototype, Backbone.Events);
+
app.base.pageView = Backbone.View.extend({
auto_render: false,
@@ -128,6 +130,7 @@ _r(function (app) {
render: function () {
var self = this;
+ this.rendered = false;
this.load(function (err, data) {
var locals = _.extend({
success: !err,
@@ -196,15 +199,38 @@ _r(function (app) {
app.base.listView = app.base.pageView.extend({
+ pagination_pager_selector: '.pagination',
+ pagination_content_selector: '.pagination_content',
+ current_page: 1,
+
initialize: function () {
+ var self = this;
+
if (this.collection) {
if ( ! this.collection.getByCid || typeof(this.collection.getByCid) !== 'function') {
this.collection = new this.collection();
this.collection_generated = true;
}
- this.addLocals({collection: this.collection});
+ this.addLocals({collcetion: this.collection});
}
+ this.$el.delegate(this.pagination_pager_selector+' a', 'click', function (e) {
+ e.preventDefault();
+ var $target = $(e.target);
+ var $li = $target.closest('li');
+ if ($li.hasClass('disabled') || $li.hasClass('active')) {
+ return false;
+ } else {
+ self.paginator($target.data('page'));
+ }
+ });
+
+ this.bind('rendered', function () {
+ if (self.collection.paginated) {
+ self.renderPagination();
+ }
+ });
+
if ( ! app.base.pageView.prototype.initialize.apply(this, arguments)) {
return false;
}
@@ -227,6 +253,48 @@ _r(function (app) {
callback(json.data, null);
}
});
+ },
+
+ renderPagination: function () {
+ var $pagination = this.$el.find(this.pagination_pager_selector);
+
+ if ($pagination.length === 0) {
+ console.log('Collection is paginated but no pagination element found with this selector:', this.pagination_pager_selector);
+ return false;
+ }
+
+ var locals = _.extend({}, this.locals, {
+ current: parseInt(this.current_page, 10),
+ num_pages: Math.ceil(this.collection.total / this.collection.per_page)
+ });
+ app.template('page', 'pagination', locals, function (html) {
+ $pagination.html(html);
+ });
+ },
+
+ paginator: function (page) {
+ var self = this;
+ var new_page = parseInt(page, 10);
+
+ if (isNaN(new_page)) {
+ return false;
+ }
+
+ if (window.location.hash.indexOf('page=') === -1) {
+ app.navigate(window.location.hash+'/?page='+new_page);
+ } else {
+ app.navigate(window.location.hash.replace(/page=[\d]*/, 'page='+new_page));
+ }
+
+ this.collection.getPage(new_page, function (collection, page) {
+ self.current_page = page;
+ self.locals.data = {
+ paginated: true,
+ models: collection
+ };
+ self.successRender(self.locals);
+ self.renderPagination();
+ });
}
});
View
5 static/js/models/EpisodeModel.js → static/js/models/episodeModel.js
@@ -4,13 +4,12 @@ _r(function (app) {
urlRoot: '/REST/Episode/',
nohmName: 'Episode',
- toggleSeen: function (callback) {
+ toggleSeen: function () {
var self = this;
- $.getJSON(this.urlRoot+'seen/'+this.id, function (res, result) {
+ $.getJSON(this.urlRoot+'seen/'+this.id, function (res) {
self.set({
seen: res.data.episode
});
- callback();
});
}
});
View
45 static/js/views/showView.js
@@ -36,9 +36,6 @@ _r(function (app) {
var $season_opener = self.$el.find('a.season_opener[data-num="'+self.params[1]+'"]');
if ($season_opener) {
$season_opener.click();
- $('html, body').animate({
- scrollTop: $season_opener.offset().top
- }, 300);
}
});
}
@@ -65,32 +62,37 @@ _r(function (app) {
var $episode_list = $('#season_contents div.tab-pane[data-num="'+num+'"]');
app.navigate('#show/details/'+this.model.get('name')+'/'+num);
- if ($episode_list.children().length === 0) {
+ if ($episode_list.children(':not(.loading)').length === 0) {
this.seasons[num] = new episode_list_view(
'show',
'episode_list',
$episode_list,
[self.model.get('id'), num]);
- return true;
}
}
});
var episode_list_view = app.base.listView.extend({
- collection: app.collections.Episode,
+ collection: app.collections.Season,
auto_render: true,
events: {
'click a.episode_opener': 'toggleEpisode',
- 'click a.set_seen, a.set_not_seen': 'toggleSeen'
+ 'click .episode_seen_button a.set_seen, .episode_seen_button a.set_not_seen': 'toggleEpisodeSeen',
+ 'click .season_seen_button a.set_seen, .season_seen_button a.set_not_seen': 'toggleSeasonSeen'
},
init: function() {
+ var self = this;
this.collection.url += 'byShow/'+this.params[0]+'?season='+this.params[1];
- this.bind('rendered', function () {
-
+
+ this.collection.bind('change:seen', function (episode) {
+ var $episode = self.$el.find('div.episode_seen_container')
+ .eq(episode.collection.indexOf(episode));
+ self.redrawEpisodeSeenButton($episode, episode);
+ self.redrawSeasonSeenButtons();
});
},
@@ -103,19 +105,34 @@ _r(function (app) {
$toggle_content.toggleClass('hidden');
},
- toggleSeen: function (e) {
+ toggleEpisodeSeen: function (e) {
e.preventDefault();
var $target = $(e.target);
var id = $target.closest('li.episode_detail').data('id');
var episode = this.collection.get(id);
+
+ episode.toggleSeen();
+ },
+
+ redrawEpisodeSeenButton: function ($el, episode) {
var locals = _.extend({}, this.locals, {
episode: episode
});
- episode.toggleSeen(function () {
- app.template('show', 'seen_button', locals, function (html) {
- $target.closest('div.seen_container').html(html);
- });
+ app.template('show', 'episode_seen_button', locals, function (html) {
+ $el.html(html);
+ });
+ },
+
+ redrawSeasonSeenButtons: function () {
+ var $seen_container = this.$el.find('div.season_seen_container');
+ app.template('show', 'season_seen_button', this.locals, function (html) {
+ $seen_container.html(html);
});
+ },
+
+ toggleSeasonSeen: function (e) {
+ e.preventDefault();
+ this.collection.toggleSeen();
}
});
View
1 static/js/views/userView.js
@@ -34,6 +34,7 @@ _r(function (app) {
max_age: 0,
checkAllowed: isNotLoggedIn,
wait_for_user_loaded: false,
+ reload_on_login: true,
render: function () {
if (app.user_self.get('name')) {
View
45 static/templates/tmpl-page.html
@@ -34,6 +34,51 @@
!=footer
</script>
+<script type="text/x-jade-tmpl" name="pagination">
+ul
+ li("class": (current === 1 ? 'disabled': ''))
+ a(href: '#', data-page: current-1)
+ =_t('pagination.previous')
+
+
+ if (num_pages > 10)
+
+ if (current > 4)
+ - var middle_start = current-2
+ li
+ a(href: '#', data-page: 1) 1
+ li.disabled
+ a(href: '#') ...
+ else
+ - var middle_start = 1
+
+ if (num_pages-current > 4)
+ - var middle_end = current+2
+ else
+ - var middle_end = num_pages
+
+ - for (var i = middle_start; i <= middle_end; i++)
+ li("class": (current === i ? 'active': ''))
+ a(href: '#', data-page: i)
+ =i
+
+ if (num_pages-current > 4)
+ li.disabled
+ a(href: '#') ...
+ li
+ a(href: '#', data-page: num_pages)= num_pages
+
+ else
+ - for (var i = 1; i <= num_pages; i++)
+ li("class": (current === i ? 'active': ''))
+ a(href: '#', data-page: i)
+ =i
+
+ li("class": (current === num_pages ? 'disabled': ''))
+ a(href: '#', data-page: current+1)
+ =_t('pagination.next')
+</script>
+
<script type="text/x-jade-tmpl" name="error">
- if (data && data.error && data.error.msg === 'need_login')
!=partial('need_login_error')
View
123 static/templates/tmpl-show.html
@@ -58,18 +58,20 @@
a.thumbnail.span10(href:'#show/details/'+model.get('name'))
span
=model.get('name')
- img(src:'/images/series_banners/'+model.get('banner'), title: model.get('name'), alt: model.get('name'))
+ if (model.get('banner'))
+ img(src:'/images/series_banners/'+model.get('banner'), title: model.get('name'), alt: model.get('name'))
- else
| No shows found.
</script>
<script type="text/x-jade-tmpl" name="details">
-img.thumbnail(src:'/images/series_banners/'+data.get('banner'))
+if (data.get('banner'))
+ img.thumbnail(src:'/images/series_banners/'+data.get('banner'))
h2
= data.get('name')
-
+
ul.show_details
li
- var genres = data.get('genre').replace(/(^\|)|(\|$)/g, '').split('|');
@@ -112,58 +114,68 @@
</script>
<script type="text/x-jade-tmpl" name="loading_episode_list">
-| Loading
+.loading Loading
</script>
<script type="text/x-jade-tmpl" name="episode_list">
if (data.models.length === 0)
=_t('none_found')
else
- ul.episode_list
- each episode in data.models
- li.well.span3.episode_detail(data-id: episode.id)
- h3
- if parseInt(episode.get('season'), 10) == 0
- =episode.get('name')
- else
- =_t('episode')+' '+episode.get('number')
-
- label
- = _t('name')
- span.one_line= episode.get('name')
-
- br
- - var date = moment(episode.get('first_aired'));
- time(datetime:date.format('YYYY-MM-DD'), title:date.format('MMM Do YYYY'))
- label
- =_t('air_date')
- =date.fromNow()
+ .season_seen_container
+ !=partial('season_seen_button', data)
- br
- .seen_container
- !=partial('seen_button', episode)
-
- a.episode_opener(href: '#')
- =_t('more')
-
- .episode_details.hidden
-
- label
- = _t('plot')
- = episode.get('plot')
-
- br
- if episode.get('imdb_link')
- label
- =_t('imdb_link')+':'
- a(href:'http://imdb.com/title/'+episode.get('imdb_id'), target: episode.get('name')+' IMDB')= episode.get('imdb_id')
+ if data.paginated
+ .pagination.pagination-centered
+
+ ul.episode_list.pagination_content
+ !=partial('episode_details', data.models)
+</script>
+
+<script type="text/x-jade-tmpl" name="episode_details">
+each episode in args[0]
+ li.well.span3.episode_detail(data-id: episode.id)
+ h3
+ if parseInt(episode.get('season'), 10) == 0
+ =episode.get('name')
+ else
+ =_t('episode')+' '+episode.get('number')
+
+ label
+ = _t('name')
+ span.one_line= episode.get('name')
+
+ br
+ - var date = moment(episode.get('first_aired'));
+ time(datetime:date.format('YYYY-MM-DD'), title:date.format('MMM Do YYYY'))
+ label
+ =_t('air_date')
+ =date.fromNow()
+
+ br
+ .episode_seen_container
+ !=partial('episode_seen_button', episode)
+
+ a.episode_opener(href: '#')
+ =_t('more')
+
+ .episode_details.hidden
+
+ label
+ = _t('plot')
+ = episode.get('plot')
+
+ br
+ if episode.get('imdb_link')
+ label
+ =_t('imdb_link')+':'
+ a(href:'http://imdb.com/title/'+episode.get('imdb_id'), target: episode.get('name')+' IMDB')= episode.get('imdb_id')
</script>
-<script type="text/x-jade-tmpl" name="seen_button">
+<script type="text/x-jade-tmpl" name="episode_seen_button">
- var episode = locals.episode ? episode : args[0]
- var seen = episode.get('seen'); var in_future = moment().add('d', 1) > moment(episode.get('first_aired'));
- var class_name = seen ? 'btn-success' : 'btn-info';
-.btn-group.dropdown.seen_button
+.btn-group.dropdown.episode_seen_button
a.btn.dropdown-toggle("class":class_name, data-toggle: "dropdown")
if seen
=_t('has_seen')
@@ -182,6 +194,31 @@
a.get_links(href: '#')= _t('wanna_see')
</script>
+<script type="text/x-jade-tmpl" name="season_seen_button">
+- var season = locals.collection ? collection : args[0];
+- var seen = _.all(season.models, function (ep) { return ep.get('seen'); });
+- var last_air_date = _.last(season.models).get('first_aired')
+- var in_future = moment().add('d', 1) > moment(last_air_date);
+- var class_name = seen ? 'btn-success' : 'btn-info';
+.btn-group.dropdown.season_seen_button
+ a.btn.dropdown-toggle("class":class_name, data-toggle: "dropdown")
+ if seen
+ =_t('has_seen_season')
+ else if in_future
+ =_t('has_not_seen_season')
+ else
+ =_t('not_released_season')
+ span.caret
+ ul.dropdown-menu
+ li( "class": seen ? '' : 'hidden' )
+ a.set_not_seen(href: '#')= _t('set_not_seen_season')
+
+ li( "class": seen ? 'hidden' : '' )
+ a.set_seen(href: '#')= _t('set_seen_season')
+ li
+ a.get_links(href: '#')= _t('wanna_see_season')
+</script>
+
<script type="text/x-jade-tmpl" name="import">
| Importing from TheTVDB. Please wait...
</script>

No commit comments for this range

Something went wrong with that request. Please try again.