From: E.Smith <31170571+azlm8t@users.noreply.github.com> Date: Tue, 20 Nov 2018 17:24:37 +0000 (+0000) Subject: ui: Add alternative/similar broadcast buttons, fixes #5335, #5336 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=04cd487bb8;p=thirdparty%2Ftvheadend.git ui: Add alternative/similar broadcast buttons, fixes #5335, #5336 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. --- diff --git a/Makefile.webui b/Makefile.webui index 01166ce74..c0465fe0e 100644 --- a/Makefile.webui +++ b/Makefile.webui @@ -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 diff --git a/src/access.h b/src/access.h index 75830657f..a03fc73b5 100644 --- a/src/access.h +++ b/src/access.h @@ -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); } diff --git a/src/api/api_epg.c b/src/api/api_epg.c index b82489d97..e15d1a382 100644 --- a/src/api/api_epg.c +++ b/src/api/api_epg.c @@ -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; iepisodelink; - 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); diff --git a/src/dvr/dvr_db.c b/src/dvr/dvr_db.c index b4d2fee70..c41256f2f 100644 --- a/src/dvr/dvr_db.c +++ b/src/dvr/dvr_db.c @@ -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, diff --git a/src/webui/static/app/dvr.js b/src/webui/static/app/dvr.js index f0294cbae..2420f28c4 100644 --- a/src/webui/static/app/dvr.js +++ b/src/webui/static/app/dvr.js @@ -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; diff --git a/src/webui/static/app/epg.js b/src/webui/static/app/epg.js index 193a08285..d7140bec1 100644 --- a/src/webui/static/app/epg.js +++ b/src/webui/static/app/epg.js @@ -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 = ' '; 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 index 000000000..c79db65a6 --- /dev/null +++ b/src/webui/static/app/epgevent.js @@ -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); + } +}; diff --git a/src/webui/static/app/ext.css b/src/webui/static/app/ext.css index 6b0966d47..aa953969c 100644 --- a/src/webui/static/app/ext.css +++ b/src/webui/static/app/ext.css @@ -228,6 +228,10 @@ 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; } diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js index 951a15297..a7ec8c77f 100644 --- a/src/webui/static/app/tvheadend.js +++ b/src/webui/static/app/tvheadend.js @@ -277,6 +277,24 @@ tvheadend.getContentTypeIcons = function(rec, style) { tvheadend.applyHighResIconPath(tvheadend.uniqueArray(ret_minor)).join("") + ''; } +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 '';