]> git.ipfire.org Git - thirdparty/tvheadend.git/commitdiff
ui: Add alternative/similar broadcast buttons, fixes #5335, #5336
authorE.Smith <31170571+azlm8t@users.noreply.github.com>
Tue, 20 Nov 2018 17:24:37 +0000 (17:24 +0000)
committerJaroslav Kysela <perex@perex.cz>
Sun, 25 Nov 2018 19:54:29 +0000 (20:54 +0100)
Add two buttons to EPG and DVR info dialogs, one to show related
broadcasts and one to show similar broadcasts. From this dialog,
you can then select an entry to record the episode.

Main changes are to return a sorted list from the api and
display this in live grid so we get comet updates.

Main api change is to share some epg config so it can be
used in the new dialog.

Issue: #5335, #5336.

Makefile.webui
src/access.h
src/api/api_epg.c
src/dvr/dvr_db.c
src/webui/static/app/dvr.js
src/webui/static/app/epg.js
src/webui/static/app/epgevent.js [new file with mode: 0644]
src/webui/static/app/ext.css
src/webui/static/app/tvheadend.js

index 01166ce7437e33670dc25b5be06802f539a06086..c0465fe0ee30f3096e7c32ddf229106114c7b35c 100644 (file)
@@ -150,6 +150,7 @@ JAVASCRIPT += $(ROOTPATH)/app/timeshift.js
 endif
 JAVASCRIPT += $(ROOTPATH)/app/chconf.js
 JAVASCRIPT += $(ROOTPATH)/app/epg.js
+JAVASCRIPT += $(ROOTPATH)/app/epgevent.js
 JAVASCRIPT += $(ROOTPATH)/app/dvr.js
 JAVASCRIPT += $(ROOTPATH)/app/epggrab.js
 JAVASCRIPT += $(ROOTPATH)/app/config.js
index 75830657f63cdc3ba04ed8095cc9305b6f815ba6..a03fc73b5e7f71a234254d5e7e547b56a46b28f4 100644 (file)
@@ -261,7 +261,7 @@ access_get_theme(access_t *a);
  *
  * Return 0 if access is granted, -1 otherwise
  */
-static inline int access_verify2(access_t *a, uint32_t mask)
+static inline int access_verify2(const access_t *a, uint32_t mask)
   { return (mask & ACCESS_OR) ?
       ((a->aa_rights & mask) ? 0 : -1) :
       ((a->aa_rights & mask) == mask ? 0 : -1); }
index b82489d972901791efe379c99b7efef80bc4009a..e15d1a382227aa5352fb1c73f3dda8fff649cd18 100644 (file)
@@ -72,7 +72,7 @@ api_epg_add_channel ( htsmsg_t *m, channel_t *ch, const char *blank )
 }
 
 static htsmsg_t *
-api_epg_entry ( epg_broadcast_t *eb, const char *lang, access_t *perm, const char **blank )
+api_epg_entry ( epg_broadcast_t *eb, const char *lang, const access_t *perm, const char **blank )
 {
   const char *s, *blank2 = NULL;
   char buf[64];
@@ -508,27 +508,80 @@ api_epg_grid
   return 0;
 }
 
+static int
+api_epg_sort_by_time_t(const void *a, const void *b, void *arg)
+{
+  const time_t *at= (const time_t*)a;
+  const time_t *bt= (const time_t*)b;
+  return *at - *bt;
+}
+
+/// Generate a sorted list of episodes that
+/// do NOT match ebc_skip in to message l.
+/// @return number of entries allocated.
+static uint32_t
+api_epg_episode_sorted(const struct epg_set *set,
+                       const access_t *perm,
+                       htsmsg_t *l,
+                       const char *lang,
+                       const epg_broadcast_t *ebc_skip)
+{
+  typedef struct {
+    time_t start;
+    htsmsg_t *m;
+  } bcast_entry_t;
+
+  epg_broadcast_t *ebc;
+  htsmsg_t *m;
+  bcast_entry_t *bcast_entries = NULL;
+  const epg_set_item_t *item;
+  bcast_entry_t new_bcast_entry;
+  size_t num_allocated = 0;
+  size_t num_entries = 0;
+  size_t i;
+
+  LIST_FOREACH(item, &set->broadcasts, item_link) {
+    ebc = item->broadcast;
+    if (ebc != ebc_skip) {
+      m = api_epg_entry(ebc, lang, perm, NULL);
+      if (num_entries == num_allocated) {
+        num_allocated = MAX(100, num_allocated + 100);
+        /* We don't expect any/many reallocs so we store physical struct instead of pointers */
+        bcast_entries = realloc(bcast_entries, num_allocated * sizeof(bcast_entry_t));
+      }
+
+      new_bcast_entry.start = htsmsg_get_u32_or_default(m, "start", 0);
+      new_bcast_entry.m = m;
+      bcast_entries[num_entries++] = new_bcast_entry;
+    }
+  }
+
+  tvh_qsort_r(bcast_entries, num_entries, sizeof(bcast_entry_t), api_epg_sort_by_time_t, 0);
+
+  for (i=0; i<num_entries; ++i) {
+    htsmsg_t *m = bcast_entries[i].m;
+    htsmsg_add_msg(l, NULL, m);
+  }
+  free(bcast_entries);
+
+  return num_entries;
+}
+
+
 static void
 api_epg_episode_broadcasts
   ( access_t *perm, htsmsg_t *l, const char *lang, epg_broadcast_t *ep,
     uint32_t *entries, epg_broadcast_t *ebc_skip )
 {
-  epg_broadcast_t *ebc;
-  htsmsg_t *m;
   epg_set_t *episodelink = ep->episodelink;
-  epg_set_item_t *item;
 
   if (episodelink == NULL)
     return;
 
-  LIST_FOREACH(item, &episodelink->broadcasts, item_link) {
-    ebc = item->broadcast;
-    if (ebc != ebc_skip) {
-      m = api_epg_entry(ebc, lang, perm, NULL);
-      htsmsg_add_msg(l, NULL, m);
-      (*entries)++;
-    }
-  }
+  /* Need to sort these ourselves since they are used as a livegrid
+   * which requires remote sort.
+   */
+  *entries = api_epg_episode_sorted(episodelink, perm, l, lang, ebc_skip);
 }
 
 static int
@@ -565,11 +618,10 @@ api_epg_related
   ( access_t *perm, void *opaque, const char *op, htsmsg_t *args, htsmsg_t **resp )
 {
   uint32_t id, entries = 0;
-  htsmsg_t *l = htsmsg_create_list(), *m;
-  epg_broadcast_t *e, *ebc;
+  htsmsg_t *l = htsmsg_create_list();
+  epg_broadcast_t *e;
   char *lang;
   epg_set_t *serieslink;
-  epg_set_item_t *item;
   
   if (htsmsg_get_u32(args, "eventId", &id))
     return -EINVAL;
@@ -579,16 +631,9 @@ api_epg_related
   pthread_mutex_lock(&global_lock);
   e = epg_broadcast_find_by_id(id);
   serieslink = e->serieslink;
-  if (serieslink) {
-    LIST_FOREACH(item, &serieslink->broadcasts, item_link) {
-      ebc = item->broadcast;
-      if (ebc != e) {
-        m = api_epg_entry(ebc, lang, perm, NULL);
-        htsmsg_add_msg(l, NULL, m);
-        entries++;
-      }
-    }
-  }
+  if (serieslink)
+    entries = api_epg_episode_sorted(serieslink, perm, l, lang, e);
+
   pthread_mutex_unlock(&global_lock);
   free(lang);
 
index b4d2fee7007bde4495724f6e1d01e6ee884ce183..c41256f2f643bb8e5fe868ba9ac94511fd6cdf06 100644 (file)
@@ -4480,7 +4480,8 @@ const idclass_t dvr_entry_class = {
       .desc     = N_("Broadcast."),
       .set      = dvr_entry_class_broadcast_set,
       .get      = dvr_entry_class_broadcast_get,
-      .opts     = PO_RDONLY | PO_NOUI,
+      /* Has to be available to UI for "show duplicate" from dvr upcoming */
+      .opts     = PO_RDONLY | PO_HIDDEN,
     },
     {
       .type     = PT_STR,
index f0294cbae2c37ee7f0ffff9135dddd4b77efb111..2420f28c46287cdbf267766f1b6b9d30bb1edcff 100644 (file)
@@ -153,8 +153,10 @@ tvheadend.dvrDetails = function(grid, index) {
       return content
     }
 
-  function getDialogButtons(title) {
+  function getDialogButtons(d) {
+        var title = getTitle(d);
         var buttons = [];
+        var eventId = d[0].params[24].value;
 
         var comboGetInfo = new Ext.form.ComboBox({
             store: new Ext.data.ArrayStore({
@@ -188,6 +190,17 @@ tvheadend.dvrDetails = function(grid, index) {
         if (title)
             buttons.push(comboGetInfo);
 
+        buttons.push(new Ext.Toolbar.Button({
+            handler: function() { epgAlternativeShowingsDialog(eventId, true) },
+            iconCls: 'duprec',
+            tooltip: _('Find alternative showings for the DVR entry.'),
+        }));
+        buttons.push(new Ext.Toolbar.Button({
+            handler: function() { epgAlternativeShowingsDialog(eventId, false) },
+            iconCls: 'epgrelated',
+            tooltip: _('Find related showings for the DVR entry.'),
+        }));
+
         buttons.push(new Ext.Button({
               id: previousButtonId,
               handler: previousEvent,
@@ -260,8 +273,9 @@ tvheadend.dvrDetails = function(grid, index) {
   function showit(d) {
        var dialogTitle = getDialogTitle(d);
        var content = getDialogContent(d);
-       var buttons = getDialogButtons(getTitle(d));
+       var buttons = getDialogButtons(d);
        var windowHeight = Ext.getBody().getViewSize().height - 150;
+
        win = new Ext.Window({
             title: dialogTitle,
             iconCls: 'info',
@@ -289,7 +303,159 @@ tvheadend.dvrDetails = function(grid, index) {
             list: 'channel_icon,disp_title,disp_subtitle,disp_summary,episode_disp,start_real,stop_real,' +
                   'duration,disp_description,status,filesize,comment,duplicate,' +
                   'autorec_caption,timerec_caption,image,copyright_year,credits,keyword,category,' +
-                  'first_aired,genre,channelname,fanart_image',
+                  'first_aired,genre,channelname,fanart_image,broadcast',
+        },
+        success: function(d) {
+            d = json_decode(d);
+            tvheadend.loading(0),
+            cb(d);
+        },
+        failure: function(d) {
+            tvheadend.loading(0);
+        }
+      });
+  }                           // load
+
+  function previousEvent(b, e) {
+      --current_index;
+      load(store,current_index,updateit);
+  }
+  function nextEvent(b, e) {
+      ++current_index;
+      var cbWin = b.findParentByType(Ext.Window);
+      load(store,current_index,updateit);
+  }
+  function dvrAlternativeShowings(eventId) {
+      var store = getAlternativeShowingsStore(eventId);
+
+    var detailsfcn = function(grid, rec, act, row) {
+      var store = grid.getStore();
+      var event = store.getAt(row);
+      var data = event.data;
+        new tvheadend.epgDetails(grid, row);
+    };
+
+    var eventdetails = new Ext.ux.grid.RowActions({
+        id: 'details',
+        header: _('Details'),
+        tooltip: _('Details'),
+        width: 67,
+        dataIndex: 'actions',
+        callbacks: {
+            'recording':      detailsfcn,
+            'recordingError': detailsfcn,
+            'scheduled':      detailsfcn,
+            'completed':      detailsfcn,
+            'completedError': detailsfcn
+        },
+        actions: [
+            {
+                iconCls: 'broadcast_details',
+                qtip: _('Broadcast details'),
+                cb: detailsfcn
+            },
+            {
+                iconIndex: 'dvrState'
+            }
+        ]
+    });
+
+     var epgView = new Ext.ux.grid.livegrid.GridView({
+         nearLimit: 100,
+         loadMask: {
+             msg: _('Buffering. Please wait…')
+         },
+ });
+
+    var grid = new Ext.ux.grid.livegrid.GridPanel({
+              store: store,
+              plugins: [eventdetails],
+              iconCls: 'epg',
+              view: epgView,
+              cm: new Ext.grid.ColumnModel({
+                columns: [
+                  eventdetails,
+                  {
+                    width: 250,
+                    id: 'title',
+                    header: _("Title"),
+                    tooltip: _("Title"),
+                    dataIndex: 'title',
+                  },
+                  {
+                    width: 250,
+                    id: 'extratext',
+                    header: _("Extra text"),
+                    tooltip: _("Extra text: subtitle or summary or description"),
+                    dataIndex: 'extratext',
+                    renderer: dvrRenderExtraText
+                  },
+                  {
+                    width: 200,
+                    id: 'start',
+                    header: _("Start Time"),
+                    tooltip: _("Start Time"),
+                    dataIndex: 'start',
+                    renderer: dvrRenderDate
+                  },
+            {
+                width: 200,
+                id: 'stop',
+                header: _("End Time"),
+                tooltip: _("End Time"),
+                dataIndex: 'stop',
+                renderer: dvrRenderDate
+            },
+            {
+                width: 250,
+                id: 'channelName',
+                header: _("Channel"),
+                tooltip: _("Channel"),
+                dataIndex: 'channelName',
+            },
+                ],
+              }),
+       });                      // grid
+
+
+       var windowHeight = Ext.getBody().getViewSize().height - 150;
+
+       win = new Ext.Window({
+            title: 'Alternative Showings',
+            iconCls: 'info',
+            layout: 'fit',
+            width: 1200,
+            height: windowHeight,
+            constrainHeader: true,
+            buttonAlign: 'center',
+            autoScroll: true,
+            items: grid,
+        });
+        // Handle comet updates until user closes dialog.
+        var update = function(m) {
+            tvheadend.epgCometUpdate(m, store);
+        };
+       tvheadend.comet.on('epg', update);
+       win.on('close', function(panel, opts) {
+           tvheadend.comet.un('epg', update);
+       });
+
+       win.show();
+       updateDialogFanart(d);
+       checkButtonAvailability(win.fbar)
+  }
+
+  function load(store, index, cb) {
+      var uuid = store.getAt(index).id;
+      tvheadend.loading(1);
+      Ext.Ajax.request({
+        url: 'api/idnode/load',
+        params: {
+            uuid: uuid,
+            list: 'channel_icon,disp_title,disp_subtitle,disp_summary,episode_disp,start_real,stop_real,' +
+                  'duration,disp_description,status,filesize,comment,duplicate,' +
+                  'autorec_caption,timerec_caption,image,copyright_year,credits,keyword,category,' +
+                  'first_aired,genre,channelname,fanart_image,broadcast',
         },
         success: function(d) {
             d = json_decode(d);
@@ -323,7 +489,7 @@ tvheadend.dvrDetails = function(grid, index) {
   function updateit(d) {
         var dialogTitle = getDialogTitle(d);
         var content = getDialogContent(d);
-        var buttons = getDialogButtons(getTitle(d));
+        var buttons = getDialogButtons(d);
         win.removeAll();
         // Can't update buttons at the same time...
         win.update({html: content});
@@ -552,29 +718,6 @@ tvheadend.dvr_upcoming = function(panel, index) {
         }
     };
 
-    function updateDupText(button, dup) {
-        button.setText(dup ? _('Hide duplicates') : _('Show duplicates'));
-    }
-
-    var dupButton = {
-        name: 'dup',
-        builder: function() {
-            return new Ext.Toolbar.Button({
-                tooltip: _('Toggle the view of the duplicate DVR entries.'),
-                iconCls: 'duprec',
-                text: _('Show duplicates')
-            });
-        },
-        callback: function(conf, e, store, select) {
-            duplicates ^= 1;
-            select.grid.colModel.setHidden(columnId, !duplicates);
-            select.grid.bottomToolbar.changePage(0);
-            updateDupText(this, duplicates);
-            store.baseParams.duplicates = duplicates;
-            store.reload();
-        }
-    };
-
     function selected(s, abuttons) {
         var recording = 0;
         s.each(function(s) {
@@ -584,6 +727,9 @@ tvheadend.dvr_upcoming = function(panel, index) {
         abuttons.stop.setDisabled(recording < 1);
         abuttons.abort.setDisabled(recording < 1);
         abuttons.prevrec.setDisabled(recording >= 1);
+        var r = s.getSelections();
+        abuttons.epgalt.setDisabled(r.length <= 0);
+        abuttons.epgrelated.setDisabled(r.length <= 0);
     }
 
     function beforeedit(e, grid) {
@@ -591,15 +737,6 @@ tvheadend.dvr_upcoming = function(panel, index) {
             return false;
     }
 
-    function viewready(grid) {
-        var d = grid.store.baseParams.duplicates;
-        updateDupText(grid.abuttons['dup'], d);
-        if (!d) {
-            columnId = grid.colModel.findColumnIndex('duplicate');
-            grid.colModel.setHidden(columnId, true);
-        }
-    }
-
     tvheadend.idnode_grid(panel, {
         url: 'api/dvr/entry',
         gridURL: 'api/dvr/entry/grid_upcoming',
@@ -625,7 +762,7 @@ tvheadend.dvr_upcoming = function(panel, index) {
         del: true,
         list: 'category,enabled,duplicate,disp_title,disp_extratext,episode_disp,' +
               'channel,image,copyright_year,start_real,stop_real,duration,pri,filesize,' +
-              'sched_status,errors,data_errors,config_name,owner,creator,comment,genre',
+              'sched_status,errors,data_errors,config_name,owner,creator,comment,genre,broadcast',
         columns: {
             disp_title: {
                 renderer: tvheadend.displayWithYearAndDuplicateRenderer(),
@@ -661,10 +798,9 @@ tvheadend.dvr_upcoming = function(panel, index) {
             actions,
             tvheadend.contentTypeAction,
         ],
-        tbar: [stopButton, abortButton, prevrecButton, dupButton],
+        tbar: [stopButton, abortButton, prevrecButton, epgShowRelatedButtonConf, epgShowAlternativesButtonConf],
         selected: selected,
         beforeedit: beforeedit,
-        viewready: viewready
     });
 
     return panel;
index 193a08285e3fe0570a9196b38fe0e9414476f739..d7140bec150832989b510ba2eb94f8814fdd6d89 100644 (file)
@@ -3,6 +3,84 @@ insertContentGroupClearOption = function(scope, records, options) {
     scope.insert(0,new placeholder({val: _('(Clear filter)'), key: '-1'}));
 };
 
+epgDetailsfcn = function(grid, rec, act, row) {
+    new tvheadend.epgDetails(grid, row);
+};
+
+// Each column model needs their own copy
+// since it gets explicitly detstroyed.
+var getEPGEventDetails = function() {
+    var ra = new Ext.ux.grid.RowActions({
+        id: 'details',
+        header: _('Details'),
+        tooltip: _('Details'),
+        width: 67,
+        dataIndex: 'actions',
+        callbacks: {
+            'recording':      epgDetailsfcn,
+            'recordingError': epgDetailsfcn,
+            'scheduled':      epgDetailsfcn,
+            'completed':      epgDetailsfcn,
+            'completedError': epgDetailsfcn
+        },
+        actions: [
+            {
+                iconCls: 'broadcast_details',
+                qtip: _('Broadcast details'),
+                cb: epgDetailsfcn
+            },
+            {
+                iconIndex: 'dvrState'
+            }
+        ]
+    });
+    // RowActions do not have a destroy function which means when the
+    // ColumnModel is destroyed (such as in a dialog box), then we get
+    // an exception.  So define our own null destroy function.
+    ra.destroy=function() {}
+    return ra;
+}
+
+tvheadend.epgCometUpdate = function(m, epgStore) {
+    if ('delete' in m)
+        Ext.each(m['delete'], function(d) {
+            var r = epgStore.getById(d);
+            if (r)
+              epgStore.remove(r);
+        });
+    if (m.update || m.dvr_update || m.dvr_delete) {
+        var a = m.update || m.dvr_update || m.dvr_delete;
+        if (m.update && m.dvr_update)
+            var a = m.update.concat(m.dvr_update);
+        if (m.update || m.dvr_update)
+            a = a.concat(m.dvr_delete);
+        var ids = [];
+        Ext.each(a, function (id) {
+            var r = epgStore.getById(id);
+            if (r)
+              ids.push(r.id);
+        });
+        if (ids) {
+            Ext.Ajax.request({
+                url: 'api/epg/events/load',
+                params: {
+                    eventId: Ext.encode(ids)
+                },
+                success: function(d) {
+                    d = json_decode(d);
+                    Ext.each(d, function(jd) {
+                        tvheadend.replace_entry(epgStore.getById(jd.eventId), jd);
+                    });
+                },
+                failure: function(response, options) {
+                    Ext.MessageBox.alert(_('EPG Update'), response.statusText);
+                }
+            });
+        }
+    }
+};
+
+
 tvheadend.ContentGroupStore = tvheadend.idnode_get_enum({
     url: 'api/epg/content_type/list',
     listeners: {
@@ -346,6 +424,19 @@ tvheadend.epgDetails = function(grid, index) {
               tooltip: _('Create an automatic recording rule to record all future programs that match the current query.'),
               text: event.serieslinkUri ? _("Record series") : _("Autorec")
           }));
+
+          var eventId = event.eventId;
+          buttons.push(new Ext.Toolbar.Button({
+              handler: function() { epgAlternativeShowingsDialog(eventId, true) },
+              iconCls: 'duprec',
+              tooltip: _('Find alternative showings for the DVR entry.'),
+          }));
+          buttons.push(new Ext.Toolbar.Button({
+              handler: function() { epgAlternativeShowingsDialog(eventId, false) },
+              iconCls: 'epgrelated',
+              tooltip: _('Find related showings for the DVR entry.'),
+          }));
+
           buttons.push(new Ext.Button({
               id: previousButtonId,
               handler: previousEvent,
@@ -371,8 +462,8 @@ tvheadend.epgDetails = function(grid, index) {
     }                             //getDialogButtons
 
     var current_index = index;
-    var event = grid.getStore().getAt(index).data;
     var store = grid.getStore();
+    var event = store.getAt(index).data;
     var content = getDialogContent(event);
     var buttons = getDialogButtons();
     var windowHeight = Ext.getBody().getViewSize().height - 150;
@@ -494,39 +585,12 @@ tvheadend.epg = function() {
     var lookup = '<span class="x-linked">&nbsp;</span>';
     var epgChannelCurrentIndex = 0;
 
-    var detailsfcn = function(grid, rec, act, row) {
-        new tvheadend.epgDetails(grid, row);
-    };
     var watchfcn = function(grid, rec, act, row) {
         var item = grid.getStore().getAt(row);
         new tvheadend.VideoPlayer(item.data.channelUuid);
     };
 
-    var eventdetails = new Ext.ux.grid.RowActions({
-        id: 'details',
-        header: _('Details'),
-        tooltip: _('Details'),
-        width: 67,
-        dataIndex: 'actions',
-        callbacks: {
-            'recording':      detailsfcn,
-            'recordingError': detailsfcn,
-            'scheduled':      detailsfcn,
-            'completed':      detailsfcn,
-            'completedError': detailsfcn
-        },
-        actions: [
-            {
-                iconCls: 'broadcast_details',
-                qtip: _('Broadcast details'),
-                cb: detailsfcn
-            },
-            {
-                iconIndex: 'dvrState'
-            }
-        ]
-    });
-
+    var eventdetails = getEPGEventDetails();
     var eventactions = new Ext.ux.grid.RowActions({
         id: 'eventactions',
         header: _('Actions'),
@@ -1304,42 +1368,7 @@ tvheadend.epg = function() {
     tvheadend.comet.on('epg', function(m) {
         if (!panel.isVisible())
             return;
-        if ('delete' in m)
-            Ext.each(m['delete'], function(d) {
-                var r = epgStore.getById(d);
-                if (r)
-                  epgStore.remove(r);
-            });
-        if (m.update || m.dvr_update || m.dvr_delete) {
-            var a = m.update || m.dvr_update || m.dvr_delete;
-            if (m.update && m.dvr_update)
-                var a = m.update.concat(m.dvr_update);
-            if (m.update || m.dvr_update)
-                a = a.concat(m.dvr_delete);
-            var ids = [];
-            Ext.each(a, function (id) {
-                var r = epgStore.getById(id);
-                if (r)
-                  ids.push(r.id);
-            });
-            if (ids) {
-                Ext.Ajax.request({
-                    url: 'api/epg/events/load',
-                    params: {
-                        eventId: Ext.encode(ids)
-                    },
-                    success: function(d) {
-                        d = json_decode(d);
-                        Ext.each(d, function(jd) {
-                            tvheadend.replace_entry(epgStore.getById(jd.eventId), jd);
-                        });
-                    },
-                    failure: function(response, options) {
-                        Ext.MessageBox.alert(_('EPG Update'), response.statusText);
-                    }
-                });
-            }
-        }
+        tvheadend.epgCometUpdate(m, epgStore);
     });
 
     // Always reload the store when the tab is activated
diff --git a/src/webui/static/app/epgevent.js b/src/webui/static/app/epgevent.js
new file mode 100644 (file)
index 0000000..c79db65
--- /dev/null
@@ -0,0 +1,214 @@
+/*
+ * epgevent.js
+ * EPG dialogs for broadcast events.
+ * Copyright (C) 2018 Tvheadend Foundation CIC
+ */
+
+/// Display dialog showing alternative showings for a broadcast event.
+/// @param alternative - If true then display "alternatives", otherwise display "related" broadcasts
+function epgAlternativeShowingsDialog(eventId, alternative) {
+    // Default params only exist in ECMA2015+, so do it old way.
+    alternative = (typeof alternative !== 'undefined') ?  alternative : true;
+
+    function getAlternativeShowingsStore(eventId) {
+        var base = alternative ? "alternative" : "related";
+        return new Ext.ux.grid.livegrid.Store({
+            autoLoad: true,
+            // Passing params doesn't seem to work, so force eventId in to url.
+            url: 'api/epg/events/' + base + '?eventId='+eventId,
+            baseParams: {
+                eventId: eventId,
+            },
+            bufferSize: 300,
+            selModel: new Ext.ux.grid.livegrid.RowSelectionModel(),
+            reader: new Ext.ux.grid.livegrid.JsonReader({
+                root: 'entries',
+                totalProperty: 'totalCount',
+                id: 'eventId'
+            }, [
+                // We need a complete set of fields since user can request
+                // dialog that retrieves its data from our store.
+                { name: 'eventId' },
+                { name: 'channelName' },
+                { name: 'channelUuid' },
+                { name: 'channelNumber' },
+                { name: 'channelIcon' },
+                { name: 'title' },
+                { name: 'subtitle' },
+                { name: 'summary' },
+                { name: 'description' },
+                { name: 'extratext' },
+                { name: 'episodeOnscreen' },
+                { name: 'image' },
+                {
+                    name: 'start',
+                    type: 'date',
+                    dateFormat: 'U' /* unix time */
+                },
+                {
+                    name: 'stop',
+                    type: 'date',
+                    dateFormat: 'U' /* unix time */
+                },
+                {
+                    name: 'first_aired',
+                    type: 'date',
+                    dateFormat: 'U' /* unix time */
+                },
+                { name: 'duration' },
+                { name: 'starRating' },
+                { name: 'credits' },
+                { name: 'category' },
+                { name: 'keyword' },
+                { name: 'ageRating' },
+                { name: 'copyright_year' },
+                { name: 'new' },
+                { name: 'genre' },
+                { name: 'dvrUuid' },
+                { name: 'dvrState' },
+                { name: 'serieslinkUri' }
+            ])
+        });
+    }                           //getAlternativeShowingsStore
+
+
+    var store = getAlternativeShowingsStore(eventId);
+    var epgView = new Ext.ux.grid.livegrid.GridView({
+        nearLimit: 100,
+        loadMask: {
+            msg: _('Buffering. Please wait…')
+        },
+    });
+
+    var epgEventDetails = getEPGEventDetails();
+
+    var grid = new Ext.ux.grid.livegrid.GridPanel({
+        store: store,
+        plugins: [epgEventDetails],
+        iconCls: 'epg',
+        view: epgView,
+        cm: new Ext.grid.ColumnModel({
+            columns: [
+                epgEventDetails,
+                {
+                    width: 250,
+                    id: 'title',
+                    header: _("Title"),
+                    tooltip: _("Title"),
+                    dataIndex: 'title',
+                },
+                {
+                    width: 250,
+                    id: 'extratext',
+                    header: _("Extra text"),
+                    tooltip: _("Extra text: subtitle or summary or description"),
+                    dataIndex: 'extratext',
+                    renderer: tvheadend.renderExtraText
+                },
+                {
+                    width: 100,
+                    id: 'episodeOnscreen',
+                    header: _("Episode"),
+                    tooltip: _("Episode"),
+                    dataIndex: 'episodeOnscreen',
+                },
+                {
+                    width: 200,
+                    id: 'start',
+                    header: _("Start Time"),
+                    tooltip: _("Start Time"),
+                    dataIndex: 'start',
+                    renderer: tvheadend.renderCustomDate
+                },
+                {
+                    width: 200,
+                    id: 'stop',
+                    header: _("End Time"),
+                    tooltip: _("End Time"),
+                    dataIndex: 'stop',
+                    renderer: tvheadend.renderCustomDate
+                },
+                {
+                    width: 250,
+                    id: 'channelName',
+                    header: _("Channel"),
+                    tooltip: _("Channel"),
+                    dataIndex: 'channelName',
+                },
+            ],
+        }),
+    });                      // grid
+
+
+    var windowHeight = Ext.getBody().getViewSize().height - 150;
+
+    var win = new Ext.Window({
+        title: alternative ? _('Alternative Showings') : _('Related Showings'),
+        iconCls: 'info',
+        layout: 'fit',
+        width: 1317,
+        height: windowHeight,
+        constrainHeader: true,
+        buttonAlign: 'center',
+        autoScroll: false,  // Internal grid has its own scrollbars so no need for us to have them
+        items: grid,
+        bbar: new Ext.ux.grid.livegrid.Toolbar(
+        tvheadend.PagingToolbarConf({view: epgView},_('Events'),0,0)
+        ),
+
+    });
+
+    // Handle comet updates until user closes dialog.
+    var update = function(m) {
+        tvheadend.epgCometUpdate(m, store);
+    };
+    tvheadend.comet.on('epg', update);
+    win.on('close', function(panel, opts) {
+         tvheadend.comet.un('epg', update);
+     });
+
+    win.show();
+}
+
+
+var epgAlternativeShowingsDialogForSelection = function(conf, e, store, select, alternative) {
+    var r = select.getSelections();
+    if (r && r.length > 0) {
+        for (var i = 0; i < r.length; i++) {
+            var rec = r[i];
+            var eventId = rec.data['broadcast'];
+            if (eventId)
+                epgAlternativeShowingsDialog(eventId, alternative);
+        }
+    }
+};
+
+var epgShowRelatedButtonConf = {
+    name: 'epgrelated',
+    builder: function() {
+        return new Ext.Toolbar.Button({
+            tooltip: _('Display dialog of related broadcasts'),
+            iconCls: 'epgrelated',
+            text: _('Related broadcasts'),
+            disabled: true
+        });
+    },
+    callback: function(conf, e, store, select) {
+        epgAlternativeShowingsDialogForSelection(conf, e, store, select, false);
+    }
+}
+
+var epgShowAlternativesButtonConf = {
+    name: 'epgalt',
+    builder: function() {
+        return new Ext.Toolbar.Button({
+            tooltip: _('Display dialog showing alternative broadcasts'),
+            iconCls: 'duprec',
+            text: _('Alternative showings'),
+            disabled: true
+        });
+    },
+    callback: function(conf, e, store, select) {
+        epgAlternativeShowingsDialogForSelection(conf, e, store, select, true);
+    }
+};
index 6b0966d47b24246ddce631b8e1458e09c841d5bd..aa953969c6731cc3deeb32092a7aac59abd80664 100644 (file)
     background-image: url(../icons/accept.png) !important;
 }
 
+.epgrelated {
+    background-image: url(../icons/clock.png) !important;
+}
+
 .duprec {
     background-image: url(../icons/control_repeat_blue.png) !important;
 }
index 951a15297d2656fe9b29e5adb8298e217737ec28..a7ec8c77f84251708c6bc18372806c09d30dc35e 100644 (file)
@@ -277,6 +277,24 @@ tvheadend.getContentTypeIcons = function(rec, style) {
     tvheadend.applyHighResIconPath(tvheadend.uniqueArray(ret_minor)).join("") + '</span>';
 }
 
+tvheadend.renderCustomDate = function(value, meta, record) {
+    if (value) {
+        var dt = new Date(value);
+        return tvheadend.toCustomDate(dt,tvheadend.date_mask);
+    }
+    return "";
+}
+
+tvheadend.renderExtraText = function(value, meta, record) {
+    value = record.data.subtitle;
+    if (!value) {
+        value = record.data.summary;
+        if (!value)
+            value = record.data.description;
+    }
+  return value;
+}
+
 tvheadend.displayCategoryIcon = function(value, meta, record, ri, ci, store) {
   if (value == null)
     return '';