/krum/webui/static/js/matching.js
JavaScript | 425 lines | 346 code | 32 blank | 47 comment | 42 complexity | 72aa89aa204081ba29d1a2e5d301e4bc MD5 | raw file
- function cmpAsNum(a,b) {
- if (a === b) {
- return 0;
- }
- var l = parseInt(a, 10);
- var r = parseInt(b, 10);
- l = l === NaN ? a : l;
- r = r === NaN ? b : r;
- return l < r ? -1 : 1;
- }
- function cmpEps(a,b) {
- return cmpAsNum(a.episode, b.episode);
- }
- // The content model
- var Content = Backbone.Model.extend({
- urlRoot: "/api/1/content",
- defaults: function() {
- return {
- id: null,
- original_path: "Unknown",
- metadata_id: null,
- original_hash: null,
- };
- },
- initialize: function() {
- if (!this.get("original_path")) {
- this.set({"original_path": this.defaults.original_path});
- }
- },
- clear: function() {
- this.destroy();
- },
- });
- var ContentList = Backbone.Collection.extend({
- model: Content,
- url: "/api/1/content",
- });
- unmatchedContent = new ContentList();
- $(function() {
- // The select item template
- var ContentSelectRowView = Backbone.View.extend({
- tagName: 'tr',
- template: Handlebars.compile($('#tmpl_selectitem').html()),
- events: {
- },
- initialize: function() {
- this.$el.attr('data-id', this.model.id)
- },
- render: function() {
- this.$el.html(this.template(this.model.attributes));
- return this;
- },
- });
- var MetaSearch = Backbone.View.extend({
- tagName: 'input',
- events: {
- },
- initialize: function() {
- this.mediaType = this.options.mediaType || 1; // defaults to movies
- // TODO: allow for other options of the autocomplete search (limits/in_library/etc)
- var self = this;
- this.$el.autocomplete({
- minLength: 3,
- source: function(req, rsp) {
- req.media_type = self.mediaType
- $.getJSON('/api/1/autocomplete?limit=10', req, function(d,s,x) {
- var results = [];
- try {
- $.each(d, function(k, v) {
- $.each(v, function(i, item) {
- item.label = item.name + ' ' + item.year
- });
- v.unshift(v.length);
- v.unshift(-1);
- results.splice.apply(results, v);
- });
- }
- catch (err) {
- }
- rsp(results);
- });
- },
- select: function(e, ui) {
- self.trigger('select', ui.item);
- self.$el.val(ui.item.value);
- return false;
- },
- focus: function(e, ui) {
- self.$el.val(ui.item.value);
- return false;
- },
- }).change(function(e) {
- console.log('Change! ' + self.$el.val());
- });
- },
- });
- // This works with a set of selected rows within a table to pick the series, season and episode of each
- var SeriesPicker = Backbone.View.extend({
- events: {
- },
- initialize: function() {
- this.views = this.options.views;
- this.episodes = [];
- // make up all the input elements, disabled to start
- // build series picker
- this.showPicker = new MetaSearch({mediaType: 2});
- this.showPicker.$el.appendTo(this.views[0].$el.find('.meta-data-1'));
- this.showPicker.bind('select', this.showPicked, this);
- // build season picker
- this.seasonPicker = new Autocomplete({minLength: 0, shortCutProp: 'label'});
- this.seasonPicker.$el.appendTo(this.views[0].$el.find('.meta-data-2'));
- this.seasonPicker.bind('select', this.seasonPicked, this);
- // build episode pickers
- this.epPickers = [];
- _.each(this.views, function(view) {
- var picker = new Autocomplete({model: view.model, matchOnStart: true, shortCutProp: 'ep_num'});
- this.epPickers.push(picker);
- picker.$el.appendTo(view.$el.find('.meta-data-3'));
- picker.bind('select', this.episodePicked, this);
- }, this);
- if (this.options.autofocus) {
- this.showPicker.$el.focus();
- }
- },
- showPicked: function(item) {
- var metaID = item.id;
- // go fetch episode listing and such
- var self = this;
- $.getJSON(formatStr('/api/1/series/{}/episodes', metaID), function(episodes, status, xhr) {
- // get a list of Seasons
- var seasons = _.uniq(_.map(episodes, function(ep) {return ep.season;}));
- // add in nice labels
- _.each(episodes, function(ep, i) {
- ep.label = formatStr('{} - {}', ep.ep_num, ep.ep_name)
- });
- // now set up the picker
- self.episodes = episodes;
- self.seasonPicker
- .setSource(seasons)
- .open();
- });
- },
- seasonPicked: function(item) {
- // filter down the episode list
- var eps = _.filter(this.episodes, function(ep) {return ep.season==item.value;});
- // set up the episode pickers
- _.each(this.epPickers, function(ep) {
- ep.setSource(eps);
- });
- },
- episodePicked: function(item, picker) {
- // commit and mark "done", can still be edited
- // TODO: This should be modeled as adding this content to the episodes content list, not fiddling with a foreign key reference.
- var metaID = item.id;
- picker.model.save({metadata_id: metaID});
- },
- cancel: function() {
- // walk back through the edit stages
- // if we're already at the start
- this.trigger('cancel');
- },
- });
- // A Backbone view style wrapper around jQuery UI's autocomplete widget
- var Autocomplete = Backbone.View.extend({
- // OPTIONS:
- // matchOnStart: true|false.
- // If present and true, the autocomplete list will be filtered
- // down to those items whose label text starts with the term,
- // rather than those which contain the term.
- // shortCutProp: propName.
- // If present, this will allow a user to avoid having to "pick"
- // an item from the list by using the text
- // TODO: if a user types an item in exactly, make it so it's as if they chose that option
- tagName: 'input',
- events: {
- },
- initialize: function() {
- var self = this;
- this.$el.autocomplete({
- minLength: (this.options.minLength == undefined ? 1 : this.options.minLength),
- delay: this.options.delay || 50,
- select: function(e, ui) {
- self.trigger('select', ui.item, self);
- },
- });
- this.setSource(this.options.source);
- if (this.options.matchOnStart) {
- this.$el.bind('autocompleteresponse', function(event, ui) {
- // this is complicated because I need to manipulate in-place
- var val = $(this).val();
- // get things to reject
- var rejects = _.map(ui.content, function(c, i) {
- if (c.value.slice(0, val.length) != val) {
- return i;
- }
- });
- // now remove in reverse order so indices aren't broken by removals
- rejects.reverse();
- _.each(rejects, function(r) {
- if (r != undefined) {
- ui.content.splice(r, 1);
- }
- });
- });
- }
- if (this.options.shortCutProp) {
- // save the last set of options
- this.$el.bind('autocompleteresponse', function(event, ui) {
- self.lastOptions = ui.content;
- });
- // shortcut if we can
- var prop = this.options.shortCutProp;
- this.$el.bind('autocompletechange', function(event, ui) {
- if (ui.item != null) { // they picked from the list, so leave it alone
- return;
- }
- var val = self.$el.val();
- var newitem = _.find(self.lastOptions, function(item) {
- return item[prop] == val;
- });
- if (newitem != null) {
- self.$el.val(newitem.value);
- self.trigger('select', newitem, self);
- }
- });
- }
- },
- setEnabled: function(enabled) {
- if (enabled) {
- this.$el.removeAttr('disabled');
- }
- else {
- this.$el.attr('disabled', true);
- }
- return this;
- },
- setSource: function(source) {
- this.options.source = source;
- this.$el.autocomplete('option', 'source', source);
- return this;
- },
- // Clear the value that we're storing.
- clear: function() {
- this.$el.val('');
- return this;
- },
- focus: function() {
- this.$el.focus();
- return this;
- },
- open: function() {
- this.$el.autocomplete('search', '');
- return this;
- },
- });
- // draw the entire matching app area
- var MatcherView = Backbone.View.extend({
- tagName: 'table',
- events: {
- },
- initialize: function() {
- unmatchedContent.bind('add', this.addOne, this);
- unmatchedContent.bind('remove', this.removeOne, this);
- unmatchedContent.bind('reset', this.addAll, this);
- this.activePicker = null; // this is the meta-data "picker" view
- this.rows = $('<tbody>').appendTo(this.$el);
- this.$el.selecttable();
- this.viewMap = {};
- $('body').keydown(_.bind(this.keyPress, this));
- // get ourselves some data
- $.getJSON('/api/1/content?hasmetadata=false', function(data, status, xhr) {
- unmatchedContent.reset(data);
- });
- },
- addOne: function(unmatched) {
- var view = new ContentSelectRowView({model: unmatched});
- this.rows.append(view.render().el);
- this.viewMap[unmatched.id] = view;
- },
- removeOne: function(unmatched) {
- var view = this.viewMap[unmatched.id];
- if (view != undefined) {
- view.$el.remove();
- this.viewMap[unmatched.id] = undefined;
- }
- },
- addAll: function() {
- this.viewMap = {};
- unmatchedContent.each(this.addOne, this);
- },
- keyPress: function(e) {
- // TODO: get keybinding solution...
- var key = String.fromCharCode(e.which).toLowerCase();
- // TODO: can use $.ui.keyCode for special keys
- // var keyCode = $.ui.keyCode;
- // switch( event.keyCode ) {
- if (key == 'm') {
- return this.startPickMovie();
- }
- else if (key == 's') {
- return this.startPickShow();
- }
- else if (e.which == 46) {
- return this.deleteContent();
- }
- else if (e.which == 27) { // escape
- return this.cancelEditing();
- }
- },
- deleteContent: function(e) {
- var models = this.selectedModels();
- _.each(models, function(m) {
- m.destroy({wait: true});
- });
- },
- startPickMovie: function() {
- var rows = this.$el.selecttable('getRows');
- if (this.editStage != 0 || rows.length < 1) {
- return true;
- }
- // go into edit mode
- this.editStage = 1;
- this.$el.selecttable('lockRows');
- // create movie picker in first row
- var picker = new MetaSearch({mediaType: 1});
- picker.$el.appendTo(rows.first().find('.meta-data-1'));
- picker.$el.focus();
- // set up "on-pick" handler
- var self = this;
- picker.bind('select', function(item) {
- var metaID = item.id;
- var models = this.selectedModels();
- $.each(models, function(i, m) {
- m.save({metadata_id: metaID});
- });
- // clean up
- self.editStage = 0;
- self.$el.selecttable('unlockRows');
- self.$el.selecttable('clearRows');
- picker.remove();
- picker.unbind();
- }, this);
- return false;
- },
- selectedModels: function() {
- var rows = this.$el.selecttable('getRows');
- if (rows.length < 1) {
- return [];
- }
- var ids = {}; // hold a quick search cache of ids
- $.each(rows, function(i,e) {
- ids[e.getAttribute('data-id')] = true;
- });
- return unmatchedContent.filter(function(m) {return m.id in ids;});
- },
- selectedViews: function() {
- var rows = this.$el.selecttable('getRows');
- if (rows.length < 1) {
- return [];
- }
- return _.map(rows, function(row) {
- return this.viewMap[row.getAttribute('data-id')];
- }, this);
- },
- startPickShow: function() {
- // TODO: make generic and use for both movies and series, factory for picker...
- var rows = this.$el.selecttable('getRows');
- if (this.activePicker != null || rows.length < 1) {
- return true;
- }
- // go into edit mode
- this.$el.selecttable('lockRows');
- this.activePicker = new SeriesPicker({
- el: this.el,
- views: this.selectedViews(),
- autofocus: true,
- });
- this.activePicker.bind('cancel', function() {
- this.$el.selecttable('unlockRows');
- });
- return false;
- },
- cancelEditing: function() {
- if (this.activePicker != null) {
- this.activePicker.cancel();
- return false;
- }
- },
- });
- // create the new app
- var matcher = new MatcherView;
- $('#matcher').append(matcher.el)
- });