From e8a7044f1489b9aa8bd37382cd8b518a5288046c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96man?= Date: Thu, 16 Jul 2009 11:10:41 +0000 Subject: [PATCH] * Channel editor has been reworked a bit. It uses an editorGrid, similar to how other grids work in Tvheadend. Tags are mapped inline using a list-of-values combobox (http://lovcombo.extjs.eu/) --- debian/changelog | 7 +- src/webui/extjs.c | 334 +++-------- src/webui/static/app/chconf.js | 520 ++++++----------- src/webui/static/app/ext.css | 33 +- src/webui/static/app/extensions.js | 888 +++++++++-------------------- src/webui/static/app/xmltv.js | 74 +-- 6 files changed, 606 insertions(+), 1250 deletions(-) diff --git a/debian/changelog b/debian/changelog index 5611667f..cdc0b788 100644 --- a/debian/changelog +++ b/debian/changelog @@ -52,7 +52,12 @@ hts-tvheadend (2.3) hts; urgency=low from local to nationwide broadcast (AC3 audio is only present in nationwide broadcast) Ticket #78 - + + * Channel editor has been reworked a bit. It uses an editorGrid, similar + to how other grids work in Tvheadend. Tags are mapped inline using + a list-of-values combobox (http://lovcombo.extjs.eu/) + + hts-tvheadend (2.2) hts; urgency=low * Set $HOME so forked processes (XMLTV) will have correct environment diff --git a/src/webui/extjs.c b/src/webui/extjs.c index ecc94a63..177de6ad 100644 --- a/src/webui/extjs.c +++ b/src/webui/extjs.c @@ -270,32 +270,106 @@ extjs_tablemgr(http_connection_t *hc, const char *remain, void *opaque) /** * */ -static int -extjs_chlist(http_connection_t *hc, const char *remain, void *opaque) +static void +extjs_channels_delete(htsmsg_t *in) { - htsbuf_queue_t *hq = &hc->hc_reply; - htsmsg_t *out, *array, *c; + htsmsg_field_t *f; channel_t *ch; - out = htsmsg_create_map(); + TAILQ_FOREACH(f, &in->hm_fields, hmf_link) + if(f->hmf_type == HMF_S64 && + (ch = channel_find_by_identifier(f->hmf_s64)) != NULL) + channel_delete(ch); +} - array = htsmsg_create_list(); - pthread_mutex_lock(&global_lock); +/** + * + */ +static void +extjs_channels_update(htsmsg_t *in) +{ + htsmsg_field_t *f; + channel_t *ch; + htsmsg_t *c; + uint32_t id; + const char *s; - RB_FOREACH(ch, &channel_name_tree, ch_name_link) { - c = htsmsg_create_map(); - htsmsg_add_str(c, "name", ch->ch_name); - htsmsg_add_u32(c, "chid", ch->ch_id); - htsmsg_add_msg(array, NULL, c); + TAILQ_FOREACH(f, &in->hm_fields, hmf_link) { + if((c = htsmsg_get_map_by_field(f)) == NULL || + htsmsg_get_u32(c, "id", &id)) + continue; + + if((ch = channel_find_by_identifier(id)) == NULL) + continue; + + if((s = htsmsg_get_str(c, "name")) != NULL) + channel_rename(ch, s); + + if((s = htsmsg_get_str(c, "xmltvsrc")) != NULL) + channel_set_xmltv_source(ch, xmltv_channel_find_by_displayname(s)); + + if((s = htsmsg_get_str(c, "tags")) != NULL) + channel_set_tags_from_list(ch, s); + } +} + +/** + * + */ +static int +extjs_channels(http_connection_t *hc, const char *remain, void *opaque) +{ + htsbuf_queue_t *hq = &hc->hc_reply; + htsmsg_t *array, *c; + channel_t *ch; + char buf[1024]; + channel_tag_mapping_t *ctm; + const char *op = http_arg_get(&hc->hc_req_args, "op"); + const char *entries = http_arg_get(&hc->hc_req_args, "entries"); + + htsmsg_autodtor(in) = + entries != NULL ? htsmsg_json_deserialize(entries) : NULL; + + htsmsg_autodtor(out) = htsmsg_create_map(); + + scopedgloballock(); + + if(!strcmp(op, "list")) { + array = htsmsg_create_list(); + + RB_FOREACH(ch, &channel_name_tree, ch_name_link) { + c = htsmsg_create_map(); + htsmsg_add_str(c, "name", ch->ch_name); + htsmsg_add_u32(c, "chid", ch->ch_id); + + if(ch->ch_xc != NULL) + htsmsg_add_str(c, "xmltvsrc", ch->ch_xc->xc_displayname); + + buf[0] = 0; + LIST_FOREACH(ctm, &ch->ch_ctms, ctm_channel_link) { + snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), + "%s%d", strlen(buf) == 0 ? "" : ",", + ctm->ctm_tag->ct_identifier); + } + htsmsg_add_str(c, "tags", buf); + + htsmsg_add_msg(array, NULL, c); + } + + htsmsg_add_msg(out, "entries", array); + + } else if(!strcmp(op, "delete") && in != NULL) { + extjs_channels_delete(in); + + } else if(!strcmp(op, "update") && in != NULL) { + extjs_channels_update(in); + + } else { + return 400; } - pthread_mutex_unlock(&global_lock); - - htsmsg_add_msg(out, "entries", array); - htsmsg_json_serialize(out, hq, 0); - htsmsg_destroy(out); http_output_content(hc, "text/x-json; charset=UTF-8"); return 0; } @@ -388,229 +462,6 @@ json_single_record(htsmsg_t *rec, const char *root) } -/** - * - */ -static htsmsg_t * -build_transport_msg(th_transport_t *t) -{ - htsmsg_t *r = htsmsg_create_map(); - th_stream_t *st; - const char *n; - char video[200]; - char audio[200]; - char subtitles[200]; - char scrambling[200]; - - htsmsg_add_u32(r, "enabled", t->tht_enabled); - htsmsg_add_str(r, "name", t->tht_svcname); - - htsmsg_add_str(r, "provider", t->tht_provider ?: ""); - if((n = t->tht_networkname(t)) != NULL) - htsmsg_add_str(r, "network", n); - htsmsg_add_str(r, "source", t->tht_sourcename(t)); - - htsmsg_add_str(r, "status", ""); - - video[0] = 0; - audio[0] = 0; - subtitles[0] = 0; - scrambling[0] = 0; - - LIST_FOREACH(st, &t->tht_components, st_link) { - - switch(st->st_type) { - case SCT_TELETEXT: - case SCT_SUBTITLES: - case SCT_PAT: - case SCT_PMT: - break; - - case SCT_MPEG2VIDEO: - snprintf(video + strlen(video), sizeof(video) - strlen(video), - "%sMPEG-2 (PID:%d", strlen(video) > 0 ? ", " : "", - st->st_pid); - - video: - if(st->st_frame_duration) { - snprintf(video + strlen(video), sizeof(video) - strlen(video), - ", %d Hz)", 90000 / st->st_frame_duration); - } else { - snprintf(video + strlen(video), sizeof(video) - strlen(video), - ")"); - } - - break; - - case SCT_H264: - snprintf(video + strlen(video), sizeof(video) - strlen(video), - "%sH.264 (PID:%d", strlen(video) > 0 ? ", " : "", - st->st_pid); - goto video; - - case SCT_MPEG2AUDIO: - snprintf(audio + strlen(audio), sizeof(audio) - strlen(audio), - "%sMPEG-2 (PID:%d", strlen(audio) > 0 ? ", " : "", - st->st_pid); - audio: - if(st->st_lang[0]) { - snprintf(audio + strlen(audio), sizeof(audio) - strlen(audio), - ", languange: \"%s\")", st->st_lang); - } else { - snprintf(audio + strlen(audio), sizeof(audio) - strlen(audio), - ")"); - } - break; - - case SCT_AC3: - snprintf(audio + strlen(audio), sizeof(audio) - strlen(audio), - "%sAC3 (PID:%d", strlen(audio) > 0 ? ", " : "", - st->st_pid); - goto audio; - - case SCT_AAC: - snprintf(audio + strlen(audio), sizeof(audio) - strlen(audio), - "%sAAC (PID:%d", strlen(audio) > 0 ? ", " : "", - st->st_pid); - goto audio; - - case SCT_CA: - snprintf(scrambling + strlen(scrambling), - sizeof(scrambling) - strlen(scrambling), - "%s%s", strlen(scrambling) > 0 ? ", " : "", - psi_caid2name(st->st_caid)); - break; - } - } - - htsmsg_add_str(r, "video", video); - htsmsg_add_str(r, "audio", audio); - htsmsg_add_str(r, "scrambling", scrambling[0] ? scrambling : "none"); - return r; -} - - -/** - * - */ -static int -extjs_channel(http_connection_t *hc, const char *remain, void *opaque) -{ - htsbuf_queue_t *hq = &hc->hc_reply; - const char *s = http_arg_get(&hc->hc_req_args, "chid"); - const char *op = http_arg_get(&hc->hc_req_args, "op"); - channel_t *ch; - channel_t *ch2; - th_transport_t *t; - int reloadchlist = 0; - htsmsg_t *out, *array, *r; - channel_tag_mapping_t *ctm; - char buf[200]; - - pthread_mutex_lock(&global_lock); - - if(http_access_verify(hc, ACCESS_ADMIN)) { - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_UNAUTHORIZED; - } - - ch = s ? channel_find_by_identifier(atoi(s)) : NULL; - if(ch == NULL) { - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_BAD_REQUEST; - } - - if(!strcmp(op, "load")) { - r = htsmsg_create_map(); - htsmsg_add_u32(r, "id", ch->ch_id); - htsmsg_add_str(r, "name", ch->ch_name); - htsmsg_add_str(r, "comdetect", "tt192"); - - if(ch->ch_xc != NULL) - htsmsg_add_str(r, "xmltvchannel", ch->ch_xc->xc_displayname); - - buf[0] = 0; - LIST_FOREACH(ctm, &ch->ch_ctms, ctm_channel_link) { - snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), - "%s%d", strlen(buf) == 0 ? "" : ",", - ctm->ctm_tag->ct_identifier); - } - htsmsg_add_str(r, "tags", buf); - - out = json_single_record(r, "channels"); - - } else if(!strcmp(op, "gettransports")) { - out = htsmsg_create_map(); - array = htsmsg_create_list(); - LIST_FOREACH(t, &ch->ch_transports, tht_ch_link) - htsmsg_add_msg(array, NULL, build_transport_msg(t)); - - htsmsg_add_msg(out, "entries", array); - - } else if(!strcmp(op, "delete")) { - - channel_delete(ch); - - out = htsmsg_create_map(); - htsmsg_add_u32(out, "reloadchlist", 1); - htsmsg_add_u32(out, "success", 1); - - } else if(!strcmp(op, "mergefrom")) { - - if((s = http_arg_get(&hc->hc_req_args, "srcch")) == NULL) - return HTTP_STATUS_BAD_REQUEST; - - ch2 = channel_find_by_identifier(atoi(s)); - if(ch2 == NULL || ch2 == ch) - return HTTP_STATUS_BAD_REQUEST; - - channel_merge(ch, ch2); /* ch2 goes away here */ - - out = htsmsg_create_map(); - htsmsg_add_u32(out, "reloadchlist", 1); - htsmsg_add_u32(out, "success", 1); - - - } else if(!strcmp(op, "save")) { - - if((s = http_arg_get(&hc->hc_req_args, "tags")) != NULL) - channel_set_tags_from_list(ch, s); - - s = http_arg_get(&hc->hc_req_args, "xmltvchannel"); - channel_set_xmltv_source(ch, s?xmltv_channel_find_by_displayname(s):NULL); - - if((s = http_arg_get(&hc->hc_req_args, "name")) != NULL && - strcmp(s, ch->ch_name)) { - - if(channel_rename(ch, s)) { - out = htsmsg_create_map(); - htsmsg_add_u32(out, "success", 0); - htsmsg_add_str(out, "errormsg", "Channel name already exist"); - goto response; - } else { - reloadchlist = 1; - } - } - - out = htsmsg_create_map(); - htsmsg_add_u32(out, "reloadchlist", 1); - htsmsg_add_u32(out, "success", 1); - - } else { - pthread_mutex_unlock(&global_lock); - return HTTP_STATUS_BAD_REQUEST; - } - - response: - pthread_mutex_unlock(&global_lock); - - htsmsg_json_serialize(out, hq, 0); - htsmsg_destroy(out); - - http_output_content(hc, "text/x-json; charset=UTF-8"); - return 0; -} - /** * */ @@ -1528,8 +1379,7 @@ extjs_start(void) http_path_add("/extjs.html", NULL, extjs_root, ACCESS_WEB_INTERFACE); http_path_add("/tablemgr", NULL, extjs_tablemgr, ACCESS_WEB_INTERFACE); http_path_add("/dvbnetworks", NULL, extjs_dvbnetworks, ACCESS_WEB_INTERFACE); - http_path_add("/chlist", NULL, extjs_chlist, ACCESS_WEB_INTERFACE); - http_path_add("/channel", NULL, extjs_channel, ACCESS_WEB_INTERFACE); + http_path_add("/channels", NULL, extjs_channels, ACCESS_WEB_INTERFACE); http_path_add("/xmltv", NULL, extjs_xmltv, ACCESS_WEB_INTERFACE); http_path_add("/channeltags", NULL, extjs_channeltags, ACCESS_WEB_INTERFACE); http_path_add("/epg", NULL, extjs_epg, ACCESS_WEB_INTERFACE); diff --git a/src/webui/static/app/chconf.js b/src/webui/static/app/chconf.js index dfe07822..949830d7 100644 --- a/src/webui/static/app/chconf.js +++ b/src/webui/static/app/chconf.js @@ -8,7 +8,9 @@ tvheadend.channelTags = new Ext.data.JsonStore({ fields: ['identifier', 'name'], id: 'identifier', url:'channeltags', - baseParams: {op: 'listTags'} + baseParams: { + op: 'listTags' + } }); tvheadend.comet.on('channeltags', function(m) { @@ -23,9 +25,12 @@ tvheadend.comet.on('channeltags', function(m) { tvheadend.channels = new Ext.data.JsonStore({ autoLoad: true, root:'entries', - fields: ['name', 'chid'], + fields: ['name', 'chid', 'xmltvsrc', 'tags'], id: 'chid', - url: "chlist" + url: "channels", + baseParams: { + op: 'list' + } }); tvheadend.comet.on('channels', function(m) { @@ -34,343 +39,198 @@ tvheadend.comet.on('channels', function(m) { }); -/** - * Channel details - */ -tvheadend.channeldetails = function(chid, chname) { - var fm = Ext.form; - var xg = Ext.grid; - - var expander = new xg.RowExpander({ - tpl : new Ext.Template( - '
Video:{video}
', - '
Audio:{audio}
', - '
Subtitling:{subtitles}
', - '
Scrambling:{scrambling}
' - ) - }); - - var enabledColumn = new Ext.grid.CheckColumn({ - header: "Enabled", - dataIndex: 'enabled', - width: 60 - }); - - var cm = new Ext.grid.ColumnModel([expander, - enabledColumn, - { - width: 125, - id:'name', - header: "Original name", - dataIndex: 'name' - },{ - width: 125, - id:'status', - header: "Last status", - dataIndex: 'status' - },{ - width: 125, - id:'provider', - header: "Provider", - dataIndex: 'provider' - },{ - width: 125, - id:'network', - header: "Network", - dataIndex: 'network' - },{ - width: 250, - id:'source', - header: "Source", - dataIndex: 'source' - } - ]); - - - var transportRecord = Ext.data.Record.create([ - {name: 'enabled'}, - {name: 'status'}, - {name: 'name'}, - {name: 'provider'}, - {name: 'network'}, - {name: 'source'}, - {name: 'video'}, - {name: 'audio'}, - {name: 'scrambling'}, - {name: 'subtitles'} - ]); - - var transportsstore = - new Ext.data.JsonStore({root: 'entries', - fields: transportRecord, - url: "channel", - autoLoad: true, - id: 'id', - storeid: 'id', - baseParams: {chid: chid, op: "gettransports"} - }); - - - var transportsgrid = new Ext.grid.EditorGridPanel({ - title:'Transports', - anchor: '100% 50%', - stripeRows:true, - plugins:[enabledColumn, expander], - store: transportsstore, - clicksToEdit: 2, - viewConfig: {forceFit:true}, - cm: cm, - selModel: new Ext.grid.RowSelectionModel({singleSelect:false}) - }); - - - var confreader = new Ext.data.JsonReader({ - root: 'channels' - }, ['name','xmltvchannel','tags']); - - - var xmltvChannels = new Ext.data.JsonStore({ - root:'entries', - fields: [{name: 'xcTitle'}, {name: 'xcIcon'}], - url:'xmltv', - baseParams: {op: 'listChannels'} - }); - - - var confpanel = new Ext.FormPanel({ - border:false, - disabled:true, - bodyStyle:'padding:15px', - anchor: '100% 50%', - labelAlign: 'right', - labelWidth: 150, - waitMsgTarget: true, - reader: confreader, - - items: [{ - layout:'column', - border:false, - items:[{ - border:false, - columnWidth:.5, - layout: 'form', - defaultType: 'textfield', - items: [ - { - fieldLabel: 'Channel name', - name: 'name' - },new Ext.form.ComboBox({ - loadingText: 'Loading...', - fieldLabel: 'XML-TV Source', - name: 'xmltvchannel', - width: 200, - displayField:'xcTitle', - valueField:'xcTitle', - store: xmltvChannels, - forceSelection: true, - mode: 'remote', - editable: false, - triggerAction: 'all', - emptyText: 'None' - }) - ] - },{ - border:false, - columnWidth:.5, - layout: 'form', - items: [{ - fieldLabel: 'Tags', - xtype:"multiselect", - name:"tags", - valueField:"identifier", - displayField:"name", - width:200, - height:200, - store:tvheadend.channelTags - }] - }] - }] - }); - - confpanel.getForm().load({url:'channel', - params:{'chid': chid, 'op':'load'}, - success:function(form, action) { - confpanel.enable(); - }}); - - - function saveChanges() { - confpanel.getForm().submit({url:'channel', - params:{'chid': chid, 'op':'save'}, - waitMsg:'Saving Data...', - failure: function(form, action) { - Ext.Msg.alert('Save failed', action.result.errormsg); - } - }); - } - - function deleteChannel() { - Ext.MessageBox.confirm('Message', - 'Do you really want to delete "' + chname + '"', - function(button) { - if(button == 'no') - return; - Ext.Ajax.request({url: 'channel', - params:{'chid': chid, 'op':'delete'}, - success: function() { - panel.destroy(); - } - }); - } - ); - } - - var panel = new Ext.Panel({ - title: chname, - border:false, - tbar: [{ - tooltip: 'Delete channel "' + chname + '". All mapped transports will be unmapped', - iconCls:'remove', - text: "Delete channel", - handler: deleteChannel - }, '-', { - tooltip: 'Save changes made to channel configuration below and the mapped transports', - iconCls:'save', - text: "Save configuration", - handler: saveChanges - }, '->', { - text: 'Help', - handler: function() { - new tvheadend.help('Channel configuration', - 'config_channels.html'); - } - }], - defaults: { - border:false - }, - layout:'anchor', - items: [confpanel,transportsgrid] - }); - - - panel.on('afterlayout', function(parent, n) { - var DropTargetEl = parent.body.dom; - - var DropTarget = new Ext.dd.DropTarget(DropTargetEl, { - ddGroup : 'chconfddgroup', - notifyEnter : function(ddSource, e, data) { - - //Add some flare to invite drop. - parent.body.stopFx(); - parent.body.highlight(); - }, - notifyDrop : function(ddSource, e, data){ - - // Reference the record (single selection) for readability - var selectedRecord = ddSource.dragData.selections[0]; - - Ext.MessageBox.confirm('Merge channels', - 'Copy transport configuration from "' + selectedRecord.data.name + - '" to "' + chname + '". This will also remove the channel "' + - selectedRecord.data.name + '"', - function(button) { - if(button == 'no') - return; - Ext.Ajax.request({url: 'channel', - params:{chid: chid, - op:'mergefrom', - srcch: selectedRecord.data.chid}, - success: function() { - transportsstore.reload(); - }}); - } - ); - } - }); - }); - return panel; -} - /** * */ -tvheadend.chconf = function() { - var chlist = new Ext.grid.GridPanel({ - viewConfig: {forceFit:true}, - ddGroup: 'chconfddgroup', - enableDragDrop: true, - stripeRows:true, - region:'west', - width: 300, - columns: [{id:'name', - header: "Channel name", - width: 260, - dataIndex: 'name'} - ], - selModel: new Ext.grid.RowSelectionModel({singleSelect:true}), - store: tvheadend.channels - }); - - var details = new Ext.Panel({ - region:'center', layout:'fit', - items:[{border: false}] +tvheadend.chconf = function() +{ + var xmltvChannels = new Ext.data.JsonStore({ + root:'entries', + fields: ['xcTitle','xcIcon'], + url:'xmltv', + baseParams: { + op: 'listChannels' + } }); - var panel = new Ext.Panel({ - border: false, - title:'Channels', - layout:'border', - items: [chlist, details] - }); + var fm = Ext.form; + var cm = new Ext.grid.ColumnModel([ + { + header: "Name", + dataIndex: 'name', + width: 150, + editor: new fm.TextField({ + allowBlank: false + }) + }, + { + header: "XMLTV source", + dataIndex: 'xmltvsrc', + width: 150, + editor: new fm.ComboBox({ + loadingText: 'Loading...', + store: xmltvChannels, + allowBlank: true, + typeAhead: true, + minChars: 2, + lazyRender: true, + triggerAction: 'all', + mode: 'remote', + displayField:'xcTitle', + valueField:'xcTitle' + }) + }, + { + header: "Tags", + dataIndex: 'tags', + width: 300, + renderer: function(value, metadata, record, row, col, store) { + if (typeof value === 'undefined' || value.length < 1) { + return 'No tags'; + } - chlist.on('rowclick', function(grid, n) { - var rec = tvheadend.channels.getAt(n); - - details.remove(details.getComponent(0)); - details.doLayout(); - - var newpanel = new tvheadend.channeldetails(rec.data.chid, - rec.data.name); - - details.add(newpanel); - details.doLayout(); - }); - - - /** - * Setup Drop Targets - */ - - // This will make sure we only drop to the view container - - /* - var DropTargetEl = details.getView(); - - var DropTarget = new Ext.dd.DropTarget(DropTargetEl, { - ddGroup : 'chconfddgroup', - notifyEnter : function(ddSource, e, data) { - - //Add some flare to invite drop. - panel.body.stopFx(); - panel.body.highlight(); + ret = []; + tags = value.split(','); + for (var i = 0; i < tags.length; i++) { + var tag = tvheadend.channelTags.getById(tags[i]); + if (typeof tag !== 'undefined') { + ret.push(tag.data.name); + } + } + return ret.join(', '); }, - notifyDrop : function(ddSource, e, data){ - - // Reference the record (single selection) for readability - var selectedRecord = ddSource.dragData.selections[0]; - - console.log(selectedRecord); - } - }); + editor: new Ext.ux.form.LovCombo({ + store: tvheadend.channelTags, + mode:'local', + valueField: 'identifier', + displayField: 'name' + }) + } + ]); - */ - /* - details.on('afterlayout', function(parent, n) { - console.log(parent); + + function delSelected() { + var selectedKeys = grid.selModel.selections.keys; + if(selectedKeys.length > 0) { + Ext.MessageBox.confirm('Message', + 'Do you really want to delete selection?', + deleteRecord); + } else { + Ext.MessageBox.alert('Message', + 'Please select at least one item to delete'); + } + } + + + function deleteRecord(btn) { + if(btn=='yes') { + var selectedKeys = grid.selModel.selections.keys; + + Ext.Ajax.request({ + url: "channels", + params: { + op:"delete", + entries:Ext.encode(selectedKeys) + }, + failure:function(response,options) { + Ext.MessageBox.alert('Server Error','Unable to delete'); + } + }) + } + } + + function saveChanges() { + var mr = tvheadend.channels.getModifiedRecords(); + var out = new Array(); + for (var x = 0; x < mr.length; x++) { + v = mr[x].getChanges(); + out[x] = v; + out[x].id = mr[x].id; + } + + Ext.Ajax.request({ + url: "channels", + params: { + op:"update", + entries:Ext.encode(out) + }, + success:function(response,options) { + tvheadend.channels.commitChanges(); + }, + failure:function(response,options) { + Ext.MessageBox.alert('Message', response.statusText); + } }); - */ - return panel; + } + + var selModel = new Ext.grid.RowSelectionModel({ + singleSelect:false + }); + + var delBtn = new Ext.Toolbar.Button({ + tooltip: 'Delete one or more selected channels', + iconCls:'remove', + text: 'Delete selected', + handler: delSelected, + disabled: true + }); + + selModel.on('selectionchange', function(s) { + delBtn.setDisabled(s.getCount() == 0); + }); + + var saveBtn = new Ext.Toolbar.Button({ + tooltip: 'Save any changes made (Changed cells have red borders).', + iconCls:'save', + text: "Save changes", + handler: saveChanges, + disabled: true + }); + + var rejectBtn = new Ext.Toolbar.Button({ + tooltip: 'Revert any changes made (Changed cells have red borders).', + iconCls:'undo', + text: "Revert changes", + handler: function() { + tvheadend.channels.rejectChanges(); + }, + disabled: true + }); + + + var grid = new Ext.grid.EditorGridPanel({ + stripeRows: true, + title: 'Channels', + store: tvheadend.channels, + clicksToEdit: 2, + cm: cm, + viewConfig: { + forceFit:true + }, + selModel: selModel, + tbar: [ + delBtn, '-', saveBtn, rejectBtn, '->', { + text: 'Help', + handler: function() { + new tvheadend.help(title, helpContent); + } + } + ] + }); + + tvheadend.channels.on('update', function(s, r, o) { + d = s.getModifiedRecords().length == 0 + saveBtn.setDisabled(d); + rejectBtn.setDisabled(d); + }); + + tvheadend.channelTags.on('load', function(s, r, o) { + if(grid.rendered) + grid.getView().refresh(); + }); + + return grid; } diff --git a/src/webui/static/app/ext.css b/src/webui/static/app/ext.css index b9207b07..11ad0ba4 100644 --- a/src/webui/static/app/ext.css +++ b/src/webui/static/app/ext.css @@ -273,4 +273,35 @@ float:left; } -/* eof */ +/** vim: ts=4:sw=4:nu:fdc=4:nospell + * + * Ext.ux.form.LovCombo CSS File + * + * @author Ing.Jozef Sakáloš + * @copyright (c) 2008, by Ing. Jozef Sakáloš + * @date 5. April 2008 + * @version $Id: Ext.ux.form.LovCombo.css 189 2008-04-16 21:01:06Z jozo $ + * + * @license Ext.ux.form.LovCombo.css is licensed under the terms of the Open Source + * LGPL 3.0 license. Commercial use is permitted to the extent that the + * code/component(s) do NOT become part of another Open Source or Commercially + * licensed development library or toolkit without explicit permission. + * + * License details: http://www.gnu.org/licenses/lgpl.html + */ + +.ux-lovcombo-icon { + width:16px; + height:16px; + float:left; + background-position: -1px -1px ! important; + background-repeat:no-repeat ! important; +} +.ux-lovcombo-icon-checked { + background: transparent url(../extjs/resources/images/default/menu/checked.gif); +} +.ux-lovcombo-icon-unchecked { + background: transparent url(../extjs/resources/images/default/menu/unchecked.gif); +} + +/* eof */ diff --git a/src/webui/static/app/extensions.js b/src/webui/static/app/extensions.js index 50f421e8..b5aecf66 100644 --- a/src/webui/static/app/extensions.js +++ b/src/webui/static/app/extensions.js @@ -179,610 +179,6 @@ Ext.extend(Ext.grid.RowExpander, Ext.util.Observable, { -/* - * Software License Agreement (BSD License) - * Copyright (c) 2008, Nige "Animal" White - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * * Neither the name of the original author nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ -/** - * @class Ext.ux.DDView - *

A DnD-enabled version of {@link Ext.DataView}. Drag/drop is implemented by adding - * {@link Ext.data.Record}s to the target DDView. If copying is not being performed, - * the original {@link Ext.data.Record} is removed from the source DDView.

- * @constructor - * Create a new DDView - * @param {Object} config The configuration properties. - */ -Ext.ux.DDView = function(config) { - if (!config.itemSelector) { - var tpl = config.tpl; - if (this.classRe.test(tpl)) { - config.tpl = tpl.replace(this.classRe, 'class=$1x-combo-list-item $2$1'); - } - else { - config.tpl = tpl.replace(this.tagRe, '$1 class="x-combo-list-item" $2'); - } - config.itemSelector = ".x-combo-list-item"; - } - Ext.ux.DDView.superclass.constructor.call(this, Ext.apply(config, { - border: false - })); -}; - -Ext.extend(Ext.ux.DDView, Ext.DataView, { - /** - * @cfg {String/Array} dragGroup The ddgroup name(s) for the View's DragZone (defaults to undefined). - */ - /** - * @cfg {String/Array} dropGroup The ddgroup name(s) for the View's DropZone (defaults to undefined). - */ - /** - * @cfg {Boolean} copy Causes drag operations to copy nodes rather than move (defaults to false). - */ - /** - * @cfg {Boolean} allowCopy Causes ctrl/drag operations to copy nodes rather than move (defaults to false). - */ - /** - * @cfg {String} sortDir Sort direction for the view, 'ASC' or 'DESC' (defaults to 'ASC'). - */ - sortDir: 'ASC', - - // private - isFormField: true, - classRe: /class=(['"])(.*)\1/, - tagRe: /(<\w*)(.*?>)/, - reset: Ext.emptyFn, - clearInvalid: Ext.form.Field.prototype.clearInvalid, - - // private - afterRender: function() { - Ext.ux.DDView.superclass.afterRender.call(this); - if (this.dragGroup) { - this.setDraggable(this.dragGroup.split(",")); - } - if (this.dropGroup) { - this.setDroppable(this.dropGroup.split(",")); - } - if (this.deletable) { - this.setDeletable(); - } - this.isDirtyFlag = false; - this.addEvents( - "drop" - ); - }, - - // private - validate: function() { - return true; - }, - - // private - destroy: function() { - this.purgeListeners(); - this.getEl().removeAllListeners(); - this.getEl().remove(); - if (this.dragZone) { - if (this.dragZone.destroy) { - this.dragZone.destroy(); - } - } - if (this.dropZone) { - if (this.dropZone.destroy) { - this.dropZone.destroy(); - } - } - }, - - /** - * Allows this class to be an Ext.form.Field so it can be found using {@link Ext.form.BasicForm#findField}. - */ - getName: function() { - return this.name; - }, - - /** - * Loads the View from a JSON string representing the Records to put into the Store. - * @param {String} value The JSON string - */ - setValue: function(v) { - if (!this.store) { - throw "DDView.setValue(). DDView must be constructed with a valid Store"; - } - var data = {}; - data[this.store.reader.meta.root] = v ? [].concat(v) : []; - this.store.proxy = new Ext.data.MemoryProxy(data); - this.store.load(); - }, - - /** - * Returns the view's data value as a list of ids. - * @return {String} A parenthesised list of the ids of the Records in the View, e.g. (1,3,8). - */ - getValue: function() { - var result = '('; - this.store.each(function(rec) { - result += rec.id + ','; - }); - return result.substr(0, result.length - 1) + ')'; - }, - - getIds: function() { - var i = 0, result = new Array(this.store.getCount()); - this.store.each(function(rec) { - result[i++] = rec.id; - }); - return result; - }, - - /** - * Returns true if the view's data has changed, else false. - * @return {Boolean} - */ - isDirty: function() { - return this.isDirtyFlag; - }, - - /** - * Part of the Ext.dd.DropZone interface. If no target node is found, the - * whole Element becomes the target, and this causes the drop gesture to append. - */ - getTargetFromEvent : function(e) { - var target = e.getTarget(); - while ((target !== null) && (target.parentNode != this.el.dom)) { - target = target.parentNode; - } - if (!target) { - target = this.el.dom.lastChild || this.el.dom; - } - return target; - }, - - /** - * Create the drag data which consists of an object which has the property "ddel" as - * the drag proxy element. - */ - getDragData : function(e) { - var target = this.findItemFromChild(e.getTarget()); - if(target) { - if (!this.isSelected(target)) { - delete this.ignoreNextClick; - this.onItemClick(target, this.indexOf(target), e); - this.ignoreNextClick = true; - } - var dragData = { - sourceView: this, - viewNodes: [], - records: [], - copy: this.copy || (this.allowCopy && e.ctrlKey) - }; - if (this.getSelectionCount() == 1) { - var i = this.getSelectedIndexes()[0]; - var n = this.getNode(i); - dragData.viewNodes.push(dragData.ddel = n); - dragData.records.push(this.store.getAt(i)); - dragData.repairXY = Ext.fly(n).getXY(); - } else { - dragData.ddel = document.createElement('div'); - dragData.ddel.className = 'multi-proxy'; - this.collectSelection(dragData); - } - return dragData; - } - return false; - }, - - // override the default repairXY. - getRepairXY : function(e){ - return this.dragData.repairXY; - }, - - // private - collectSelection: function(data) { - data.repairXY = Ext.fly(this.getSelectedNodes()[0]).getXY(); - if (this.preserveSelectionOrder === true) { - Ext.each(this.getSelectedIndexes(), function(i) { - var n = this.getNode(i); - var dragNode = n.cloneNode(true); - dragNode.id = Ext.id(); - data.ddel.appendChild(dragNode); - data.records.push(this.store.getAt(i)); - data.viewNodes.push(n); - }, this); - } else { - var i = 0; - this.store.each(function(rec){ - if (this.isSelected(i)) { - var n = this.getNode(i); - var dragNode = n.cloneNode(true); - dragNode.id = Ext.id(); - data.ddel.appendChild(dragNode); - data.records.push(this.store.getAt(i)); - data.viewNodes.push(n); - } - i++; - }, this); - } - }, - - /** - * Specify to which ddGroup items in this DDView may be dragged. - * @param {String} ddGroup The DD group name to assign this view to. - */ - setDraggable: function(ddGroup) { - if (ddGroup instanceof Array) { - Ext.each(ddGroup, this.setDraggable, this); - return; - } - if (this.dragZone) { - this.dragZone.addToGroup(ddGroup); - } else { - this.dragZone = new Ext.dd.DragZone(this.getEl(), { - containerScroll: true, - ddGroup: ddGroup - }); - // Draggability implies selection. DragZone's mousedown selects the element. - if (!this.multiSelect) { this.singleSelect = true; } - - // Wire the DragZone's handlers up to methods in *this* - this.dragZone.getDragData = this.getDragData.createDelegate(this); - this.dragZone.getRepairXY = this.getRepairXY; - this.dragZone.onEndDrag = this.onEndDrag; - } - }, - - /** - * Specify from which ddGroup this DDView accepts drops. - * @param {String} ddGroup The DD group name from which to accept drops. - */ - setDroppable: function(ddGroup) { - if (ddGroup instanceof Array) { - Ext.each(ddGroup, this.setDroppable, this); - return; - } - if (this.dropZone) { - this.dropZone.addToGroup(ddGroup); - } else { - this.dropZone = new Ext.dd.DropZone(this.getEl(), { - owningView: this, - containerScroll: true, - ddGroup: ddGroup - }); - - // Wire the DropZone's handlers up to methods in *this* - this.dropZone.getTargetFromEvent = this.getTargetFromEvent.createDelegate(this); - this.dropZone.onNodeEnter = this.onNodeEnter.createDelegate(this); - this.dropZone.onNodeOver = this.onNodeOver.createDelegate(this); - this.dropZone.onNodeOut = this.onNodeOut.createDelegate(this); - this.dropZone.onNodeDrop = this.onNodeDrop.createDelegate(this); - } - }, - - // private - getDropPoint : function(e, n, dd){ - if (n == this.el.dom) { return "above"; } - var t = Ext.lib.Dom.getY(n), b = t + n.offsetHeight; - var c = t + (b - t) / 2; - var y = Ext.lib.Event.getPageY(e); - if(y <= c) { - return "above"; - }else{ - return "below"; - } - }, - - // private - isValidDropPoint: function(pt, n, data) { - if (!data.viewNodes || (data.viewNodes.length != 1)) { - return true; - } - var d = data.viewNodes[0]; - if (d == n) { - return false; - } - if ((pt == "below") && (n.nextSibling == d)) { - return false; - } - if ((pt == "above") && (n.previousSibling == d)) { - return false; - } - return true; - }, - - // private - onNodeEnter : function(n, dd, e, data){ - if (this.highlightColor && (data.sourceView != this)) { - this.el.highlight(this.highlightColor); - } - return false; - }, - - // private - onNodeOver : function(n, dd, e, data){ - var dragElClass = this.dropNotAllowed; - var pt = this.getDropPoint(e, n, dd); - if (this.isValidDropPoint(pt, n, data)) { - if (this.appendOnly || this.sortField) { - return "x-tree-drop-ok-below"; - } - - // set the insert point style on the target node - if (pt) { - var targetElClass; - if (pt == "above"){ - dragElClass = n.previousSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-above"; - targetElClass = "x-view-drag-insert-above"; - } else { - dragElClass = n.nextSibling ? "x-tree-drop-ok-between" : "x-tree-drop-ok-below"; - targetElClass = "x-view-drag-insert-below"; - } - if (this.lastInsertClass != targetElClass){ - Ext.fly(n).replaceClass(this.lastInsertClass, targetElClass); - this.lastInsertClass = targetElClass; - } - } - } - return dragElClass; - }, - - // private - onNodeOut : function(n, dd, e, data){ - this.removeDropIndicators(n); - }, - - // private - onNodeDrop : function(n, dd, e, data){ - if (this.fireEvent("drop", this, n, dd, e, data) === false) { - return false; - } - var pt = this.getDropPoint(e, n, dd); - var insertAt = (this.appendOnly || (n == this.el.dom)) ? this.store.getCount() : n.viewIndex; - if (pt == "below") { - insertAt++; - } - - // Validate if dragging within a DDView - if (data.sourceView == this) { - // If the first element to be inserted below is the target node, remove it - if (pt == "below") { - if (data.viewNodes[0] == n) { - data.viewNodes.shift(); - } - } else { // If the last element to be inserted above is the target node, remove it - if (data.viewNodes[data.viewNodes.length - 1] == n) { - data.viewNodes.pop(); - } - } - - // Nothing to drop... - if (!data.viewNodes.length) { - return false; - } - - // If we are moving DOWN, then because a store.remove() takes place first, - // the insertAt must be decremented. - if (insertAt > this.store.indexOf(data.records[0])) { - insertAt--; - } - } - - // Dragging from a Tree. Use the Tree's recordFromNode function. - if (data.node instanceof Ext.tree.TreeNode) { - var r = data.node.getOwnerTree().recordFromNode(data.node); - if (r) { - data.records = [ r ]; - } - } - - if (!data.records) { - alert("Programming problem. Drag data contained no Records"); - return false; - } - - for (var i = 0; i < data.records.length; i++) { - var r = data.records[i]; - var dup = this.store.getById(r.id); - if (dup && (dd != this.dragZone)) { - if(!this.allowDup && !this.allowTrash){ - Ext.fly(this.getNode(this.store.indexOf(dup))).frame("red", 1); - return true - } - var x=new Ext.data.Record(); - r.id=x.id; - delete x; - } - if (data.copy) { - this.store.insert(insertAt++, r.copy()); - } else { - if (data.sourceView) { - data.sourceView.isDirtyFlag = true; - data.sourceView.store.remove(r); - } - if(!this.allowTrash)this.store.insert(insertAt++, r); - } - if(this.sortField){ - this.store.sort(this.sortField, this.sortDir); - } - this.isDirtyFlag = true; - } - this.dragZone.cachedTarget = null; - return true; - }, - - // private - onEndDrag: function(data, e) { - var d = Ext.get(this.dragData.ddel); - if (d && d.hasClass("multi-proxy")) { - d.remove(); - //delete this.dragData.ddel; - } - }, - - // private - removeDropIndicators : function(n){ - if(n){ - Ext.fly(n).removeClass([ - "x-view-drag-insert-above", - "x-view-drag-insert-left", - "x-view-drag-insert-right", - "x-view-drag-insert-below"]); - this.lastInsertClass = "_noclass"; - } - }, - - /** - * Add a delete option to the DDView's context menu. - * @param {String} imageUrl The URL of the "delete" icon image. - */ - setDeletable: function(imageUrl) { - if (!this.singleSelect && !this.multiSelect) { - this.singleSelect = true; - } - var c = this.getContextMenu(); - this.contextMenu.on("itemclick", function(item) { - switch (item.id) { - case "delete": - this.remove(this.getSelectedIndexes()); - break; - } - }, this); - this.contextMenu.add({ - icon: imageUrl || AU.resolveUrl("/images/delete.gif"), - id: "delete", - text: AU.getMessage("deleteItem") - }); - }, - - /** - * Return the context menu for this DDView. - * @return {Ext.menu.Menu} The context menu - */ - getContextMenu: function() { - if (!this.contextMenu) { - // Create the View's context menu - this.contextMenu = new Ext.menu.Menu({ - id: this.id + "-contextmenu" - }); - this.el.on("contextmenu", this.showContextMenu, this); - } - return this.contextMenu; - }, - - /** - * Disables the view's context menu. - */ - disableContextMenu: function() { - if (this.contextMenu) { - this.el.un("contextmenu", this.showContextMenu, this); - } - }, - - // private - showContextMenu: function(e, item) { - item = this.findItemFromChild(e.getTarget()); - if (item) { - e.stopEvent(); - this.select(this.getNode(item), this.multiSelect && e.ctrlKey, true); - this.contextMenu.showAt(e.getXY()); - } - }, - - /** - * Remove {@link Ext.data.Record}s at the specified indices. - * @param {Array/Number} selectedIndices The index (or Array of indices) of Records to remove. - */ - remove: function(selectedIndices) { - selectedIndices = [].concat(selectedIndices); - for (var i = 0; i < selectedIndices.length; i++) { - var rec = this.store.getAt(selectedIndices[i]); - this.store.remove(rec); - } - }, - - /** - * Double click fires the {@link #dblclick} event. Additionally, if this DDView is draggable, and there is only one other - * related DropZone that is in another DDView, it drops the selected node on that DDView. - */ - onDblClick : function(e){ - var item = this.findItemFromChild(e.getTarget()); - if(item){ - if (this.fireEvent("dblclick", this, this.indexOf(item), item, e) === false) { - return false; - } - if (this.dragGroup) { - var targets = Ext.dd.DragDropMgr.getRelated(this.dragZone, true); - - // Remove instances of this View's DropZone - while (targets.indexOf(this.dropZone) !== -1) { - targets.remove(this.dropZone); - } - - // If there's only one other DropZone, and it is owned by a DDView, then drop it in - if ((targets.length == 1) && (targets[0].owningView)) { - this.dragZone.cachedTarget = null; - var el = Ext.get(targets[0].getEl()); - var box = el.getBox(true); - targets[0].onNodeDrop(el.dom, { - target: el.dom, - xy: [box.x, box.y + box.height - 1] - }, null, this.getDragData(e)); - } - } - } - }, - - // private - onItemClick : function(item, index, e){ - // The DragZone's mousedown->getDragData already handled selection - if (this.ignoreNextClick) { - delete this.ignoreNextClick; - return; - } - - if(this.fireEvent("beforeclick", this, index, item, e) === false){ - return false; - } - if(this.multiSelect || this.singleSelect){ - if(this.multiSelect && e.shiftKey && this.lastSelection){ - this.select(this.getNodes(this.indexOf(this.lastSelection), index), false); - } else if (this.isSelected(item) && e.ctrlKey) { - this.deselect(item); - }else{ - this.deselect(item); - this.select(item, this.multiSelect && e.ctrlKey); - this.lastSelection = item; - } - e.preventDefault(); - } - return true; - } -}); - - - /* * Ext JS Library 2.2 * Copyright(c) 2006-2008, Ext JS, LLC. @@ -1602,4 +998,286 @@ Ext.extend(Ext.ux.grid.RowActions, Ext.util.Observable, { // registre xtype Ext.reg('rowactions', Ext.ux.grid.RowActions); -// eof + +/** + * Ext.ux.form.LovCombo, List of Values Combo + * + * @author Ing. Jozef Sakáloš + * @copyright (c) 2008, by Ing. Jozef Sakáloš + * @date 16. April 2008 + * @version $Id: Ext.ux.form.LovCombo.js 285 2008-06-06 09:22:20Z jozo $ + * + * @license Ext.ux.form.LovCombo.js is licensed under the terms of the Open Source + * LGPL 3.0 license. Commercial use is permitted to the extent that the + * code/component(s) do NOT become part of another Open Source or Commercially + * licensed development library or toolkit without explicit permission. + * + * License details: http://www.gnu.org/licenses/lgpl.html + */ + +/*global Ext */ + +// add RegExp.escape if it has not been already added +if('function' !== typeof RegExp.escape) { + RegExp.escape = function(s) { + if('string' !== typeof s) { + return s; + } + // Note: if pasting from forum, precede ]/\ with backslash manually + return s.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); + }; // eo function escape +} + +// create namespace +Ext.ns('Ext.ux.form'); + +/** + * + * @class Ext.ux.form.LovCombo + * @extends Ext.form.ComboBox + */ +Ext.ux.form.LovCombo = Ext.extend(Ext.form.ComboBox, { + + // {{{ + // configuration options + /** + * @cfg {String} checkField name of field used to store checked state. + * It is automatically added to existing fields. + * Change it only if it collides with your normal field. + */ + checkField:'checked' + + /** + * @cfg {String} separator separator to use between values and texts + */ + ,separator:',' + + /** + * @cfg {String/Array} tpl Template for items. + * Change it only if you know what you are doing. + */ + // }}} + // {{{ + ,initComponent:function() { + + // template with checkbox + if(!this.tpl) { + this.tpl = + '' + +'
' + +'' + +'
{' + (this.displayField || 'text' )+ '}
' + +'
' + +'
' + ; + } + + // call parent + Ext.ux.form.LovCombo.superclass.initComponent.apply(this, arguments); + + // install internal event handlers + this.on({ + scope:this + ,beforequery:this.onBeforeQuery + ,blur:this.onRealBlur + }); + + // remove selection from input field + this.onLoad = this.onLoad.createSequence(function() { + if(this.el) { + var v = this.el.dom.value; + this.el.dom.value = ''; + this.el.dom.value = v; + } + }); + + } // e/o function initComponent + // }}} + // {{{ + /** + * Disables default tab key bahavior + * @private + */ + ,initEvents:function() { + Ext.ux.form.LovCombo.superclass.initEvents.apply(this, arguments); + + // disable default tab handling - does no good + this.keyNav.tab = false; + + } // eo function initEvents + // }}} + // {{{ + /** + * clears value + */ + ,clearValue:function() { + this.value = ''; + this.setRawValue(this.value); + this.store.clearFilter(); + this.store.each(function(r) { + r.set(this.checkField, false); + }, this); + if(this.hiddenField) { + this.hiddenField.value = ''; + } + this.applyEmptyText(); + } // eo function clearValue + // }}} + // {{{ + /** + * @return {String} separator (plus space) separated list of selected displayFields + * @private + */ + ,getCheckedDisplay:function() { + var re = new RegExp(this.separator, "g"); + return this.getCheckedValue(this.displayField).replace(re, this.separator + ' '); + } // eo function getCheckedDisplay + // }}} + // {{{ + /** + * @return {String} separator separated list of selected valueFields + * @private + */ + ,getCheckedValue:function(field) { + field = field || this.valueField; + var c = []; + + // store may be filtered so get all records + var snapshot = this.store.snapshot || this.store.data; + + snapshot.each(function(r) { + if(r.get(this.checkField)) { + c.push(r.get(field)); + } + }, this); + + return c.join(this.separator); + } // eo function getCheckedValue + // }}} + // {{{ + /** + * beforequery event handler - handles multiple selections + * @param {Object} qe query event + * @private + */ + ,onBeforeQuery:function(qe) { + qe.query = qe.query.replace(new RegExp(this.getCheckedDisplay() + '[ ' + this.separator + ']*'), ''); + } // eo function onBeforeQuery + // }}} + // {{{ + /** + * blur event handler - runs only when real blur event is fired + */ + ,onRealBlur:function() { + this.list.hide(); + var rv = this.getRawValue(); + var rva = rv.split(new RegExp(RegExp.escape(this.separator) + ' *')); + var va = []; + var snapshot = this.store.snapshot || this.store.data; + + // iterate through raw values and records and check/uncheck items + Ext.each(rva, function(v) { + snapshot.each(function(r) { + if(v === r.get(this.displayField)) { + va.push(r.get(this.valueField)); + } + }, this); + }, this); + this.setValue(va.join(this.separator)); + this.store.clearFilter(); + } // eo function onRealBlur + // }}} + // {{{ + /** + * Combo's onSelect override + * @private + * @param {Ext.data.Record} record record that has been selected in the list + * @param {Number} index index of selected (clicked) record + */ + ,onSelect:function(record, index) { + if(this.fireEvent('beforeselect', this, record, index) !== false){ + + // toggle checked field + record.set(this.checkField, !record.get(this.checkField)); + + // display full list + if(this.store.isFiltered()) { + this.doQuery(this.allQuery); + } + + // set (update) value and fire event + this.setValue(this.getCheckedValue()); + this.fireEvent('select', this, record, index); + } + } // eo function onSelect + // }}} + // {{{ + /** + * Sets the value of the LovCombo + * @param {Mixed} v value + */ + ,setValue:function(v) { + if(v) { + v = '' + v; + if(this.valueField) { + this.store.clearFilter(); + this.store.each(function(r) { + var checked = !(!v.match( + '(^|' + this.separator + ')' + RegExp.escape(r.get(this.valueField)) + +'(' + this.separator + '|$)')) + ; + + r.set(this.checkField, checked); + }, this); + this.value = this.getCheckedValue(); + this.setRawValue(this.getCheckedDisplay()); + if(this.hiddenField) { + this.hiddenField.value = this.value; + } + } + else { + this.value = v; + this.setRawValue(v); + if(this.hiddenField) { + this.hiddenField.value = v; + } + } + if(this.el) { + this.el.removeClass(this.emptyClass); + } + } + else { + this.clearValue(); + } + } // eo function setValue + // }}} + // {{{ + /** + * Selects all items + */ + ,selectAll:function() { + this.store.each(function(record){ + // toggle checked field + record.set(this.checkField, true); + }, this); + + //display full list + this.doQuery(this.allQuery); + this.setValue(this.getCheckedValue()); + } // eo full selectAll + // }}} + // {{{ + /** + * Deselects all items. Synonym for clearValue + */ + ,deselectAll:function() { + this.clearValue(); + } // eo full deselectAll + // }}} + +}); // eo extend + +// register xtype +Ext.reg('lovcombo', Ext.ux.form.LovCombo); diff --git a/src/webui/static/app/xmltv.js b/src/webui/static/app/xmltv.js index 19b3f848..e194e764 100644 --- a/src/webui/static/app/xmltv.js +++ b/src/webui/static/app/xmltv.js @@ -2,79 +2,11 @@ tvheadend.grabberStore = new Ext.data.JsonStore({ root:'entries', fields: ['identifier','name','version','apiconfig'], url:'xmltv', - baseParams: {op: 'listGrabbers'} + baseParams: { + op: 'listGrabbers' + } }); -/* - -tvheadend.xmltvsetup = function() { - - var deck1info = new Ext.form.Label({ - fieldLabel: 'Version', - html:'', - }); - - var deck1cb = new Ext.form.ComboBox({ - loadingText: 'Scanning for XMLTV grabbers, please wait...', - fieldLabel: 'XML-TV Source', - name: 'xmltvchannel', - width: 350, - displayField:'name', - valueField:'identifier', - store: tvheadend.grabberStore, - forceSelection: true, - editable: false, - triggerAction: 'all', - mode: 'remote', - emptyText: 'Select grabber' - }); - - var deck1 = new Ext.FormPanel({ - labelAlign: 'right', - labelWidth: 100, - bodyStyle: 'padding: 5px', - defaultType: 'label', - layout: 'form', - border:false, - items: [deck1cb, deck1info] - }); - - - - var win = new Ext.Window({ - title: 'Configure XMLTV grabbers', - bodyStyle: 'padding: 5px', - layout: 'fit', - width: 500, - height: 500, - constrainHeader: true, - buttonAlign: 'center', - items: [deck1], - bbar: [ - { - id: 'move-back', - text: 'Back', - disabled: true - }, - '->', - { - id: 'move-next', - text: 'Next' - } - ] - }); - - win.show(); - - deck1cb.on('select', function(c,r,i) { - deck1info.setText(r.data.version); - }); -} -*/ - - - - tvheadend.xmltv = function() { var confreader = new Ext.data.JsonReader({